Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
528 changes: 120 additions & 408 deletions src/js/animation/Animate.js

Large diffs are not rendered by default.

72 changes: 24 additions & 48 deletions src/js/core/Browser.js
Original file line number Diff line number Diff line change
@@ -1,68 +1,44 @@
/*
Based on Leaflet Browser
Browser and device detection utilities
Modernized to remove IE-specific detection
*/

export const ua = navigator ? navigator.userAgent.toLowerCase() : 'no-user-agent-specified';

const doc = document ? document.documentElement : null;
const phantomjs = ua ? ua.indexOf("phantom") !== -1 : false;


export const ie = window && 'ActiveXObject' in window

export const ie9 = Boolean(ie && ua.match(/MSIE 9/i))
export const ielt9 = ie && document && !document.addEventListener

export const webkit = ua.indexOf('webkit') !== -1
export const android = ua.indexOf('android') !== -1
export const mobile = window ? /mobile|tablet|ip(ad|hone|od)|android/i.test(ua) || 'ontouchstart' in window : false

export const android23 = ua.search('android [23]') !== -1
export const mobile = (window) ? typeof window.orientation !== 'undefined' : false
export const msPointer = (navigator && window) ? navigator.msPointerEnabled && navigator.msMaxTouchPoints && !window.PointerEvent : false
export const pointer = (navigator && window) ? (window.PointerEvent && navigator.pointerEnabled && navigator.maxTouchPoints) : msPointer

export const opera = window ? window.opera : false;
export const gecko = ua.indexOf("gecko") !== -1 && !webkit
export const firefox = gecko && ua.indexOf("firefox") !== -1
export const chrome = ua.indexOf("chrome") !== -1
export const edge = ua.indexOf("edge/") !== -1 || ua.indexOf("edg/") !== -1
export const safari = webkit && ua.indexOf("safari") !== -1 && !chrome && !edge

export const gecko = ua.indexOf("gecko") !== -1 && !webkit && !opera && !ie;
export const firefox = ua.indexOf("gecko") !== -1 && !webkit && !opera && !ie;
export const chrome = ua.indexOf("chrome") !== -1;
export const edge = ua.indexOf("edge/") !== -1;

export const ie3d = (doc) ? ie && 'transition' in doc.style : false
export const webkit3d = (window) ? ('WebKitCSSMatrix' in window) && ('m11' in new window.WebKitCSSMatrix()) && !android23 : false
export const gecko3d = (doc) ? 'MozPerspective' in doc.style : false
export const opera3d = (doc) ? 'OTransition' in doc.style : false

export const any3d = window && !window.L_DISABLE_3D &&
(ie3d || webkit3d || gecko3d || opera3d) && !phantomjs
// Modern browsers all support 3D transforms
export const webkit3d = window ? ('WebKitCSSMatrix' in window) && ('m11' in new window.WebKitCSSMatrix()) : false
export const any3d = window && !window.L_DISABLE_3D

export const mobileWebkit = mobile && webkit
export const mobileWebkit3d = mobile && webkit3d
export const mobileOpera = mobile && window.opera

export let retina = (window) ? 'devicePixelRatio' in window && window.devicePixelRatio > 1 : false
// Retina display detection
export let retina = window ? 'devicePixelRatio' in window && window.devicePixelRatio > 1 : false

if (!retina && window && 'matchMedia' in window) {
let media_matches = window.matchMedia('(min-resolution:144dpi)');
const media_matches = window.matchMedia('(min-resolution:144dpi), (-webkit-min-device-pixel-ratio: 1.5)');
retina = media_matches && media_matches.matches;
}

export const touch = window &&
!window.L_NO_TOUCH &&
!phantomjs &&
(pointer || 'ontouchstart' in window || (window.DocumentTouch && document instanceof window.DocumentTouch))

// Touch support - using modern pointer events when available
export const pointer = window && window.PointerEvent && navigator.maxTouchPoints > 0
export const touch = window && (pointer || 'ontouchstart' in window || (window.DocumentTouch && document instanceof window.DocumentTouch))

/**
* Determine device orientation based on viewport dimensions
* @returns {string} "portrait" or "landscape"
*/
export function orientation() {
var w = window.innerWidth,
h = window.innerHeight,
_orientation = "portrait";

if (w > h) {
_orientation = "landscape";
}
if (Math.abs(window.orientation) == 90) {
//_orientation = "landscape";
}
return _orientation;
const w = window.innerWidth;
const h = window.innerHeight;
return w > h ? "landscape" : "portrait";
}
161 changes: 66 additions & 95 deletions src/js/core/ConfigFactory.js
Original file line number Diff line number Diff line change
Expand Up @@ -302,118 +302,89 @@ export async function jsonFromGoogleURL(google_url, options) {
throw new TLError("Proxy option must be set to read data from Google")
}

var timeline_json = await readGoogleAsCSV(google_url, options['sheets_proxy']);

if (timeline_json) {
return timeline_json;
}
}

/**
* Helper function to handle errors when fetching timeline configuration.
* Creates a minimal TimelineConfig with the appropriate error logged.
*
* @param {Error} error - The error that occurred
* @param {function} callback - Callback to invoke with the error TimelineConfig
*/
function handleConfigError(error, callback) {
const tc = new TimelineConfig();
if (error.name == 'NetworkError') {
tc.logError(new TLError("network_err"));
} else if (error.name == 'TLError') {
tc.logError(error);
} else {
tc.logError(new TLError("unknown_read_err", error.name || error));
}
callback(tc);
return await readGoogleAsCSV(google_url, options['sheets_proxy']);
}

/**
* Helper function to create a TimelineConfig from JSON data.
* Logs any errors present in the JSON data itself.
* Using the given URL, fetch or create a JS Object suitable for configuring a timeline.
* Returns a Promise that resolves to a TimelineConfig.
* Even in error cases, a minimal TimelineConfig object will be created with logged errors.
*
* @param {Object} json - Timeline configuration in JSON format
* @param {function} callback - Callback to invoke with the created TimelineConfig
* @param {String} url - The URL or Google Spreadsheet key to fetch configuration from
* @param {Object|function} [optionsOrCallback] - Options object or legacy callback function
* @param {string} [optionsOrCallback.sheets_proxy] - Proxy URL for Google Sheets
* @param {function} [optionsOrCallback.callback] - Legacy callback (deprecated, use Promise instead)
* @returns {Promise<TimelineConfig>} Promise that resolves to a TimelineConfig
*/
function finalizeConfig(json, callback) {
const tc = new TimelineConfig(json);
if (json.errors) {
for (let i = 0; i < json.errors.length; i++) {
tc.logError(json.errors[i]);
export async function makeConfig(url, optionsOrCallback) {
// Support legacy callback pattern for backward compatibility
let callback = null;
let options = {};

if (typeof optionsOrCallback === 'function') {
callback = optionsOrCallback;
console.warn('makeConfig: callback parameter is deprecated, use the returned Promise instead');
} else if (typeof optionsOrCallback === 'object') {
options = optionsOrCallback;
if (typeof options.callback === 'function') {
callback = options.callback;
console.warn('makeConfig: callback parameter is deprecated, use the returned Promise instead');
}
}
callback(tc);
}

/**
* Using the given URL, fetch or create a JS Object suitable for configuring a timeline. Use
* that to create a TimelineConfig, and invoke the callback with that object as its argument.
* If the second argument is an object instead of a callback function, it must have a
* 'callback' property which will be invoked with the config.
* Even in error cases, a minimal TimelineConfig object will be created and passed to the callback
* so that error messages can be displayed in the host page.
*
* @param {String} url the URL or Google Spreadsheet key which can be used to get configuration information
* @param {function|object} callback_or_options either a callback function or an object with a 'callback' property and other configuration properties
*/
export async function makeConfig(url, callback_or_options) {

let callback = null,
options = {};
if (typeof(callback_or_options) == 'function') {
callback = callback_or_options
} else if (typeof(callback_or_options) == 'object') {
options = callback_or_options
callback = callback_or_options['callback']
if (typeof(options['callback']) == 'function') callback = options['callback']
}

if (!callback) {
throw new TLError("Second argument to makeConfig must be either a function or an object which includes a 'callback' property with a 'function' type value")
}

// Determine the source type and fetch the JSON configuration
let tc;
const key = parseGoogleSpreadsheetURL(url);

if (key) {
// Google Sheets: convert to CSV, then to JSON config format
try {
try {
if (key) {
// Handle Google Sheets URL
console.log(`reading url ${url}`);
const json = await jsonFromGoogleURL(url, options);
finalizeConfig(json, callback);
} catch (e) {
handleConfigError(e, callback);
}
} else if (isCSVURL(url)) {
// CSV file: parse and convert to JSON config format
try {
tc = new TimelineConfig(json);
if (json.errors) {
for (let i = 0; i < json.errors.length; i++) {
tc.logError(json.errors[i]);
}
}
} else if (isCSVURL(url)) {
// CSV file: parse and convert to JSON config format
console.log(`reading CSV from url ${url}`);
const json = await readCSVFromURL(url);
finalizeConfig(json, callback);
} catch (e) {
handleConfigError(e, callback);
}
} else {
// Direct JSON: fetch and use as-is
ajax({
url: url,
dataType: 'json',
success: function(data) {
try {
finalizeConfig(data, callback);
} catch (e) {
handleConfigError(e, callback);
tc = new TimelineConfig(json);
if (json.errors) {
for (let i = 0; i < json.errors.length; i++) {
tc.logError(json.errors[i]);
}
},
error: function(xhr, errorType) {
// Convert ajax error types to appropriate TLError
const tlError = errorType == 'parsererror'
? new TLError("invalid_url_err")
: new TLError("unknown_read_err", errorType);
handleConfigError(tlError, callback);
}
});
} else {
// Handle regular JSON URL using fetch
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
tc = new TimelineConfig(data);
}
} catch (e) {
// Even with an error, create a TimelineConfig to display messages in DOM
tc = new TimelineConfig();
if (e.name === 'NetworkError' || e.message.includes('HTTP error')) {
tc.logError(new TLError("network_err"));
} else if (e.name === 'TLError') {
tc.logError(e);
} else if (e.name === 'SyntaxError') {
tc.logError(new TLError("invalid_url_err"));
} else {
tc.logError(new TLError("unknown_read_err", e.message || e.name));
}
}

// Call legacy callback if provided
if (callback) {
callback(tc);
}

return tc;
}

function handleRow(event, timeline_config) {
Expand Down
Loading