define(function(require) { 'use strict'; /* global mozIntl, IntlHelper */ var constants = require('constants'); var Utils = {}; // Maintain references to millisecond multipliers var dateMultipliers = { days: 1000 * 60 * 60 * 24, hours: 1000 * 60 * 60, minutes: 1000 * 60, seconds: 1000, milliseconds: 1 }; var units = Object.keys(dateMultipliers); IntlHelper.define('weekday-short', 'datetime', { weekday: 'short' }); IntlHelper.define('time-html', 'mozdatetime', { dayperiod: true, hour: 'numeric', minute: 'numeric', }); IntlHelper.define('time-text', 'datetime', { hour: 'numeric', minute: 'numeric', }); /** * Define a singleton method that returns a unified instance * based on arguments. * * @param {function} constructor - A constructor function used * to create a new instance. * @param {function} [getKey] - A function called with (arguments), * and returns a lookup key for this singleton. * @return {object} - returns the instance either created or retrieved * from the singleton-map by the key. */ Utils.singleton = function(constructor, getKey) { var singletonMap = new Map(); return function() { var arglist = Array.prototype.slice.call(arguments); var key = (typeof getKey === 'function') ? getKey(arglist) : constructor; var instance = singletonMap.get(key); if (!instance) { instance = Object.create(constructor.prototype); constructor.apply(instance, arglist); singletonMap.set(key, instance); } return instance; }; }; Utils.memoizedDomPropertyDescriptor = function(selector) { var memoizedValue = null; return { get: function() { if (memoizedValue === null) { memoizedValue = document.querySelector(selector); } return memoizedValue; }, set: function(value) { memoizedValue = value; } }; }; /** * Extend the given prototype object with lazy getters. * selectorMap is a mapping of { propertyName: selector }. */ Utils.extendWithDomGetters = function(proto, selectorMap) { for (var property in selectorMap) { Object.defineProperty(proto, property, Utils.memoizedDomPropertyDescriptor(selectorMap[property])); } return proto; }; Utils.dateMath = { /** * Convert object literals containing interval length to milliseconds * * @param {Object|Date|Number} interval An object literal containing days, * hours, minutes etc. * Optionally a number or date object. * @param {Object} opts Options object with a unitsPartial property containing * (if desired) a restriction on which properties will be * searched on the interval. * @return {Number} Millisecond value for interval length. */ toMS: function(interval, opts) { var converted, sign, unitsPartial; // if a millisecond interval or a Date is passed in, return that if (interval instanceof Date || typeof interval === 'number') { return +interval; } opts = opts || {}; unitsPartial = opts.unitsPartial || units; // Allow for 'hours' or 'hour' unitsPartial = unitsPartial.map(function(unit) { // String.prototype.endsWith is available in FF17+ return unit.endsWith('s') ? unit : unit.concat('s'); }); // some will exit early when it returns a truthy value sign = unitsPartial.some(function(unit) { return interval[unit] < 0; }); // Using as a multiplier later sign = sign ? -1 : 1; // collect passed in units and multiply by their millisecond/unit count converted = unitsPartial.map(function(unit) { var partial; // we're storing the sign out of the iterator partial = Math.abs(interval[unit]); // A missing property and 0 should be treated the same return partial ? partial * dateMultipliers[unit] : 0; }); // add up each millisecond-converted term and multiply total by sign return sign * converted.reduce(function(a, b) { return a + b; }); }, /** * Convert millisecond values to object literals conformable to toMS() * * @param {Number} interval A millisecond value. * @param {Object} [opts] Options object with a unitsPartial property * containing (if desired) a restriction on which * properties will be reated for the return value. * @return {Object} Object literal with properties as deliniated by opts. */ fromMS: function(interval, opts) { var times, sign, unitsPartial; opts = opts || {}; unitsPartial = opts.unitsPartial || units; // Allow for 'hours' or 'hour' unitsPartial = unitsPartial.map(function(unit) { // String.prototype.endsWith is available in FF17+ return unit.endsWith('s') ? unit : unit.concat('s'); }); // For negative intervals (time previous to now) // update interval to absolute value and store the sign // to apply to all units if (interval < 0) { sign = -1; interval = Math.abs(interval); } else { sign = 1; } // divide the time interval by the highest millisecond multiplier // store the truncated result and subtract that from the interval // update the interval to be the remainder times = unitsPartial.map(function(unit, index) { var truncated, mult; mult = dateMultipliers[unit]; truncated = Math.floor(interval / mult); interval = interval - (truncated * mult); // units are either all positive or negative // only iterate to needed specificity return sign * truncated; }); // Populate the returned object using units as property names // and times for values return times.reduce(function(out, unitTime, index) { out[unitsPartial[index]] = unitTime; return out; }, {}); } }; Utils.extend = function(initialObject, extensions) { // extend({}, a, b, c ... d) -> {...} // rightmost properties (on 'd') take precedence extensions = Array.prototype.slice.call(arguments, 1); for (var i = 0; i < extensions.length; i++) { var extender = extensions[i]; for (var prop in extender) { var descriptor = Object.getOwnPropertyDescriptor(extender, prop); if (descriptor && descriptor.value !== undefined) { initialObject[prop] = extender[prop]; } } } return initialObject; }; Utils.escapeHTML = function(str, escapeQuotes) { var span = document.createElement('span'); span.textContent = str; if (escapeQuotes) { return span.innerHTML.replace(/"/g, '"').replace(/'/g, '''); } return span.innerHTML; }; Utils.getLocalizedTimeHtml = function(date) { var f = IntlHelper.get('time-html'); return f.format(date, { dayperiod: '$&' }); }; Utils.getLocalizedTimeText = function(date) { var f = IntlHelper.get('time-text'); return f.format(date); }; Utils.changeSelectByValue = function(selectElement, value) { var options = selectElement.options; for (var i = 0; i < options.length; i++) { if (options[i].value == value) { if (selectElement.selectedIndex != i) { selectElement.selectedIndex = i; } break; } } }; Utils.getSelectedValueByIndex = function(selectElement) { return selectElement.options[selectElement.selectedIndex].value; }; var wakeTarget = { requests: { cpu: new Map(), screen: new Map(), wifi: new Map() }, locks: { cpu: null, screen: null, wifi: null }, timeouts: { cpu: null, screen: null, wifi: null } }; function getLongestLock(type) { var max = 0; for (var i of wakeTarget.requests[type]) { var request = i[1]; if (request.time > max) { max = request.time; } } return { time: max, lock: wakeTarget.locks[type], timeout: wakeTarget.timeouts[type] }; } Utils.safeWakeLock = function(opts, fn) { /* * safeWakeLock * * Create a Wake lock that is automatically released after * timeoutMs. Locks are reentrant, and have no meaningful mutual * exclusion behavior. * * @param {Object} options - an object containing * [type] {string} a string passed to requestWakeLock * default = 'cpu'. This string can be any * resource exposed by the environment that * this application was designed to run in. * Gaia exposes three of them: 'cpu', * 'screen', and 'wifi'. Certified apps may * expose more. * timeoutMs {number} number of milliseconds to hold * the lock. * @param {Function} callback - a function to be called after all other * generated callbacks have been called. * function ([err]) -> undefined. */ opts = opts || {}; var type = opts.type || 'cpu'; var timeoutMs = opts.timeoutMs | 0; var now = Date.now(); var myKey = {}; wakeTarget.requests[type].set(myKey, { time: now + timeoutMs }); var max = getLongestLock(type); var unlockFn = function() { if (!myKey) { return; } wakeTarget.requests[type]. delete(myKey); var now = Date.now(); var max = getLongestLock(type); if (max.time > now) { clearTimeout(wakeTarget.timeouts[type]); wakeTarget.timeouts[type] = setTimeout(unlockFn, max.time - now); } else { if (wakeTarget.locks[type]) { wakeTarget.locks[type].unlock(); } wakeTarget.locks[type] = null; clearTimeout(wakeTarget.timeouts[type]); wakeTarget.timeouts[type] = null; } myKey = null; }; clearTimeout(wakeTarget.timeouts[type]); wakeTarget.timeouts[type] = setTimeout(unlockFn, max.time - now); try { if (!wakeTarget.locks[type] && max.time > now) { wakeTarget.locks[type] = navigator.requestWakeLock(type); } fn(unlockFn); } catch (err) { unlockFn(); throw err; } }; Utils.async = { generator: function(latchCallback) { /* * Generator * * Create an async generator. Each time the generator is * called, it will return a new callback. When all issued * callbacks have been called, the latchCallback is called. * * If any of the callbacks are called with and error as * the first argument, the latchCallback will be called * immediately with that error. * * @latchCallback {Function} a function to be called after * all other generated callbacks have been * called * function ([err]) -> undefined */ var tracker = new Map(); var issuedCallbackCount = 0; var disabled = false; var testFn = function(err) { var trackerSize; if (!disabled) { // FF18 defines size to be a method, so we need to test here: // Remove with FF18 support if (typeof tracker.size === 'function') { trackerSize = tracker.size(); } else { trackerSize = tracker.size; } if (err || trackerSize === issuedCallbackCount) { disabled = true; latchCallback && latchCallback(err); } } }; return function() { return (function() { var i = issuedCallbackCount++; return function(err) { tracker.set(i, true); testFn(err); }; })(); }; }, namedParallel: function(names, latchCallback) { /* * namedParallel * * Create an async namedParallel. * * The return value is an object containing the parameters * specified in the names array. Each parameter is set to * a callback. When all callbacks have been called, latchCallback * is called. * * If any named callback is called with an error as the first * parameter, latchCallback is immediately called with that * error. Future calls to callbacks are then no-ops. * * @names {List} - A list of strings to be used as * parameter names for callbacks on the returned object. */ var generator = Utils.async.generator(latchCallback); var done = generator(); var ret = {}; for (var i = 0; i < names.length; i++) { ret[names[i]] = generator(); } done(); return ret; } }; Utils.data = { defaultCompare: function ud_defaultCompare(a, b) { if (typeof a === 'number' && typeof b === 'number') { var diff = a - b; return diff !== 0 ? diff / Math.abs(diff) : diff; } else if ((typeof a === 'string' || a instanceof String) && (typeof b === 'string' || b instanceof String)) { return (a < b) ? -1 : ((a > b) ? 1 : 0); } else if (Array.isArray(a) && Array.isArray(b)) { var commonLength = Math.min(a.length, b.length); for (var i = 0; i < commonLength; i++) { var compareResult = Utils.data.defaultCompare(a[i], b[i]); if (compareResult !== 0) { return compareResult; } } return b.length - a.length; } else { throw new Error('Cannot compare ' + JSON.stringify([a, b])); } }, keyedCompare: function ud_keyedCompare(key) { return function internal_keyedCompare(a, b) { return Utils.data.defaultCompare(a[key], b[key]); }; }, binarySearch: function ud_binarySearch(key, list, compare) { compare = compare || Utils.data.defaultCompare; var botI = 0; var topI = list.length; var midI, comp, value; if (list.length > 0) { do { midI = botI + ((topI - botI) / 2) | 0; value = list[midI]; comp = compare(value, key); if (comp < 0) { botI = midI + 1; } else if (comp > 0) { topI = midI - 1; } } while (comp !== 0 && botI < topI); } midI = comp === 0 ? midI : topI; value = list[midI]; if (comp === 0 || value && compare(value, key) === 0) { return { match: true, index: midI, value: value }; } else { var index; if (0 > midI) { index = 0; } else if (midI >= list.length) { index = list.length; } else { index = midI + (compare(value, key) < 0 ? 1 : 0); } return { match: false, index: index }; } }, sortedInsert: function ud_sortedInsert(item, list, compare, unique) { compare = compare || Utils.data.defaultCompare; var bs = Utils.data.binarySearch(item, list, compare); var inserted = !bs.match || !unique; if (inserted) { list.splice(bs.index, 0, item); } return (inserted) ? bs.index : null; }, sortedRemove: function ud_sortedRemove(item, list, compare, multiple) { compare = compare || Utils.data.defaultCompare; var removed = false; if (!multiple) { var bs = Utils.data.binarySearch(item, list, compare); if (bs.match) { list.splice(bs.index, 1); removed = true; } } else { var biasedCompare = function(target, result, ic) { return function(a, b) { if (ic(a, target) === 0) { return result; } else { return ic(a, b); } }; }; var leftBound = Utils.data.binarySearch(item, list, biasedCompare(item, 1, compare)); var rightBound = Utils.data.binarySearch(item, list, biasedCompare(item, -1, compare)); if (leftBound.index < rightBound.index) { list.splice(leftBound.index, rightBound.index - leftBound.index); removed = true; } } return removed; } }; Utils.addEventListenerOnce = function(element, type, fn, useCapture) { var handler = function(evt) { element.removeEventListener(type, handler, useCapture); fn(evt); }; element.addEventListener(type, handler, useCapture); }; Utils.summarizeDaysOfWeek = function(repeat) { var days = []; if (repeat) { for (var day in repeat) { if (repeat[day]) { days.push(day); } } } if (days.length === 7) { return Promise.resolve('everyday'); } else if (days.length === 5 && days.indexOf('6') === -1 && // Saturday days.indexOf('0') === -1) { // Sunday return Promise.resolve('weekdays'); } else if (days.length === 2 && days.indexOf('6') !== -1 && // Saturday days.indexOf('0') !== -1) { // Sunday return Promise.resolve('weekends'); } else if (days.length === 0) { return Promise.resolve('never'); } else { var formatter = IntlHelper.get('weekday-short'); var repeatStrings = []; for (var i = 0; i < 7; i++) { if (days.indexOf(i.toString()) !== -1) { var dayDate = new Date(constants.KNOWN_SUNDAY); dayDate.setDate(constants.KNOWN_SUNDAY.getDate() + i); repeatStrings.push(formatter.format(dayDate)); } } return mozIntl.formatList(repeatStrings).then(listString => { return { raw: listString }; }); } }; return Utils; });