Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
569 lines
17 KiB
JavaScript
Executable file
569 lines
17 KiB
JavaScript
Executable file
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: '<small>$&</small>'
|
|
});
|
|
};
|
|
|
|
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<String>} - 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;
|
|
|
|
});
|