Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/shared/js
diff options
context:
space:
mode:
authorDaniel Narvaez <dwnarvaez@gmail.com>2013-02-06 14:30:36 (GMT)
committer Daniel Narvaez <dwnarvaez@gmail.com>2013-02-06 14:30:36 (GMT)
commitf5bac2f1e1a51b83d215a07f9d5d87db337873f3 (patch)
tree15c5f594b1e00c6272552cc5544a1bc757713e34 /shared/js
parentc301ead6bb9c201801400964427b517dafb6a03c (diff)
Get the build to work
Diffstat (limited to 'shared/js')
-rw-r--r--shared/js/async_storage.js187
-rw-r--r--shared/js/blobview.js412
-rw-r--r--shared/js/custom_dialog.js112
-rw-r--r--shared/js/desktop.js115
-rw-r--r--shared/js/gesture_detector.js891
-rw-r--r--shared/js/idletimer.js127
-rw-r--r--shared/js/l10n.js1014
-rw-r--r--shared/js/l10n_date.js141
-rw-r--r--shared/js/manifest_helper.js26
-rw-r--r--shared/js/media/README1
-rw-r--r--shared/js/media/get_video_rotation.js143
-rw-r--r--shared/js/media/jpeg_metadata_parser.js314
-rw-r--r--shared/js/media/media_frame.js537
-rw-r--r--shared/js/media/video_player.js313
-rw-r--r--shared/js/mediadb.js1532
-rw-r--r--shared/js/mobile_operator.js96
-rw-r--r--shared/js/mouse_event_shim.js282
-rw-r--r--shared/js/notification_helper.js68
-rw-r--r--shared/js/phoneNumberJS/PhoneNumber.js335
-rw-r--r--shared/js/phoneNumberJS/PhoneNumberMetaData.js218
-rw-r--r--shared/js/phoneNumberJS/mcc_iso3166_table.js242
-rw-r--r--shared/js/settings_listener.js69
-rw-r--r--shared/js/simple_phone_matcher.js366
-rw-r--r--shared/js/tz_select.js155
-rw-r--r--shared/js/visibility_monitor.js494
25 files changed, 8190 insertions, 0 deletions
diff --git a/shared/js/async_storage.js b/shared/js/async_storage.js
new file mode 100644
index 0000000..6cca66d
--- /dev/null
+++ b/shared/js/async_storage.js
@@ -0,0 +1,187 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+/**
+ * This file defines an asynchronous version of the localStorage API, backed by
+ * an IndexedDB database. It creates a global asyncStorage object that has
+ * methods like the localStorage object.
+ *
+ * To store a value use setItem:
+ *
+ * asyncStorage.setItem('key', 'value');
+ *
+ * If you want confirmation that the value has been stored, pass a callback
+ * function as the third argument:
+ *
+ * asyncStorage.setItem('key', 'newvalue', function() {
+ * console.log('new value stored');
+ * });
+ *
+ * To read a value, call getItem(), but note that you must supply a callback
+ * function that the value will be passed to asynchronously:
+ *
+ * asyncStorage.getItem('key', function(value) {
+ * console.log('The value of key is:', value);
+ * });
+ *
+ * Note that unlike localStorage, asyncStorage does not allow you to store and
+ * retrieve values by setting and querying properties directly. You cannot just
+ * write asyncStorage.key; you have to explicitly call setItem() or getItem().
+ *
+ * removeItem(), clear(), length(), and key() are like the same-named methods of
+ * localStorage, but, like getItem() and setItem() they take a callback
+ * argument.
+ *
+ * The asynchronous nature of getItem() makes it tricky to retrieve multiple
+ * values. But unlike localStorage, asyncStorage does not require the values you
+ * store to be strings. So if you need to save multiple values and want to
+ * retrieve them together, in a single asynchronous operation, just group the
+ * values into a single object. The properties of this object may not include
+ * DOM elements, but they may include things like Blobs and typed arrays.
+ *
+ * Unit tests are in apps/gallery/test/unit/asyncStorage_test.js
+ */
+
+this.asyncStorage = (function() {
+
+ var DBNAME = 'asyncStorage';
+ var DBVERSION = 1;
+ var STORENAME = 'keyvaluepairs';
+ var db = null;
+
+ function withStore(type, f) {
+ if (db) {
+ f(db.transaction(STORENAME, type).objectStore(STORENAME));
+ } else {
+ var openreq = indexedDB.open(DBNAME, DBVERSION);
+ openreq.onerror = function withStoreOnError() {
+ console.error("asyncStorage: can't open database:", openreq.error.name);
+ };
+ openreq.onupgradeneeded = function withStoreOnUpgradeNeeded() {
+ // First time setup: create an empty object store
+ openreq.result.createObjectStore(STORENAME);
+ };
+ openreq.onsuccess = function withStoreOnSuccess() {
+ db = openreq.result;
+ f(db.transaction(STORENAME, type).objectStore(STORENAME));
+ };
+ }
+ }
+
+ function getItem(key, callback) {
+ withStore('readonly', function getItemBody(store) {
+ var req = store.get(key);
+ req.onsuccess = function getItemOnSuccess() {
+ var value = req.result;
+ if (value === undefined)
+ value = null;
+ callback(value);
+ };
+ req.onerror = function getItemOnError() {
+ console.error('Error in asyncStorage.getItem(): ', req.error.name);
+ };
+ });
+ }
+
+ function setItem(key, value, callback) {
+ withStore('readwrite', function setItemBody(store) {
+ var req = store.put(value, key);
+ if (callback) {
+ req.onsuccess = function setItemOnSuccess() {
+ callback();
+ };
+ }
+ req.onerror = function setItemOnError() {
+ console.error('Error in asyncStorage.setItem(): ', req.error.name);
+ };
+ });
+ }
+
+ function removeItem(key, callback) {
+ withStore('readwrite', function removeItemBody(store) {
+ var req = store.delete(key);
+ if (callback) {
+ req.onsuccess = function removeItemOnSuccess() {
+ callback();
+ };
+ }
+ req.onerror = function removeItemOnError() {
+ console.error('Error in asyncStorage.removeItem(): ', req.error.name);
+ };
+ });
+ }
+
+ function clear(callback) {
+ withStore('readwrite', function clearBody(store) {
+ var req = store.clear();
+ if (callback) {
+ req.onsuccess = function clearOnSuccess() {
+ callback();
+ };
+ }
+ req.onerror = function clearOnError() {
+ console.error('Error in asyncStorage.clear(): ', req.error.name);
+ };
+ });
+ }
+
+ function length(callback) {
+ withStore('readonly', function lengthBody(store) {
+ var req = store.count();
+ req.onsuccess = function lengthOnSuccess() {
+ callback(req.result);
+ };
+ req.onerror = function lengthOnError() {
+ console.error('Error in asyncStorage.length(): ', req.error.name);
+ };
+ });
+ }
+
+ function key(n, callback) {
+ if (n < 0) {
+ callback(null);
+ return;
+ }
+
+ withStore('readonly', function keyBody(store) {
+ var advanced = false;
+ var req = store.openCursor();
+ req.onsuccess = function keyOnSuccess() {
+ var cursor = req.result;
+ if (!cursor) {
+ // this means there weren't enough keys
+ callback(null);
+ return;
+ }
+ if (n === 0) {
+ // We have the first key, return it if that's what they wanted
+ callback(cursor.key);
+ } else {
+ if (!advanced) {
+ // Otherwise, ask the cursor to skip ahead n records
+ advanced = true;
+ cursor.advance(n);
+ } else {
+ // When we get here, we've got the nth key.
+ callback(cursor.key);
+ }
+ }
+ };
+ req.onerror = function keyOnError() {
+ console.error('Error in asyncStorage.key(): ', req.error.name);
+ };
+ });
+ }
+
+ return {
+ getItem: getItem,
+ setItem: setItem,
+ removeItem: removeItem,
+ clear: clear,
+ length: length,
+ key: key
+ };
+}());
+
diff --git a/shared/js/blobview.js b/shared/js/blobview.js
new file mode 100644
index 0000000..eda519a
--- /dev/null
+++ b/shared/js/blobview.js
@@ -0,0 +1,412 @@
+'use strict';
+
+var BlobView = (function() {
+ function fail(msg) {
+ throw Error(msg);
+ }
+
+ // This constructor is for internal use only.
+ // Use the BlobView.get() factory function or the getMore instance method
+ // to obtain a BlobView object.
+ function BlobView(blob, sliceOffset, sliceLength, slice,
+ viewOffset, viewLength, littleEndian)
+ {
+ this.blob = blob; // The parent blob that the data is from
+ this.sliceOffset = sliceOffset; // The start address within the blob
+ this.sliceLength = sliceLength; // How long the slice is
+ this.slice = slice; // The ArrayBuffer of slice data
+ this.viewOffset = viewOffset; // The start of the view within the slice
+ this.viewLength = viewLength; // The length of the view
+ this.littleEndian = littleEndian; // Read little endian by default?
+
+ // DataView wrapper around the ArrayBuffer
+ this.view = new DataView(slice, viewOffset, viewLength);
+
+ // These fields mirror those of DataView
+ this.buffer = slice;
+ this.byteLength = viewLength;
+ this.byteOffset = viewOffset;
+
+ this.index = 0; // The read methods keep track of the read position
+ }
+
+ // Async factory function
+ BlobView.get = function(blob, offset, length, callback, littleEndian) {
+ if (offset < 0)
+ fail('negative offset');
+ if (length < 0)
+ fail('negative length');
+ if (offset > blob.size)
+ fail('offset larger than blob size');
+
+ // Don't fail if the length is too big; just reduce the length
+ if (offset + length > blob.size)
+ length = blob.size - offset;
+
+ var slice = blob.slice(offset, offset + length);
+ var reader = new FileReader();
+ reader.readAsArrayBuffer(slice);
+ reader.onloadend = function() {
+ var result = null;
+ if (reader.result) {
+ result = new BlobView(blob, offset, length, reader.result,
+ 0, length, littleEndian || false);
+ }
+ callback(result, reader.error);
+ }
+ };
+
+ BlobView.prototype = {
+ constructor: BlobView,
+
+ // This instance method is like the BlobView.get() factory method,
+ // but it is here because if the current buffer includes the requested
+ // range of bytes, they can be passed directly to the callback without
+ // going back to the blob to read them
+ getMore: function(offset, length, callback) {
+ if (offset >= this.sliceOffset &&
+ offset + length <= this.sliceOffset + this.sliceLength) {
+ // The quick case: we already have that region of the blob
+ callback(new BlobView(this.blob,
+ this.sliceOffset, this.sliceLength, this.slice,
+ offset - this.sliceOffset, length,
+ this.littleEndian));
+ }
+ else {
+ // Otherwise, we have to do an async read to get more bytes
+ BlobView.get(this.blob, offset, length, callback, this.littleEndian);
+ }
+ },
+
+ // Set the default endianness for the other methods
+ littleEndian: function() {
+ this.littleEndian = true;
+ },
+ bigEndian: function() {
+ this.littleEndian = false;
+ },
+
+ // These "get" methods are just copies of the DataView methods, except
+ // that they honor the default endianness
+ getUint8: function(offset) {
+ return this.view.getUint8(offset);
+ },
+ getInt8: function(offset) {
+ return this.view.getInt8(offset);
+ },
+ getUint16: function(offset, le) {
+ return this.view.getUint16(offset,
+ le !== undefined ? le : this.littleEndian);
+ },
+ getInt16: function(offset, le) {
+ return this.view.getInt16(offset,
+ le !== undefined ? le : this.littleEndian);
+ },
+ getUint32: function(offset, le) {
+ return this.view.getUint32(offset,
+ le !== undefined ? le : this.littleEndian);
+ },
+ getInt32: function(offset, le) {
+ return this.view.getInt32(offset,
+ le !== undefined ? le : this.littleEndian);
+ },
+ getFloat32: function(offset, le) {
+ return this.view.getFloat32(offset,
+ le !== undefined ? le : this.littleEndian);
+ },
+ getFloat64: function(offset, le) {
+ return this.view.getFloat64(offset,
+ le !== undefined ? le : this.littleEndian);
+ },
+
+ // These "read" methods read from the current position in the view and
+ // update that position accordingly
+ readByte: function() {
+ return this.view.getInt8(this.index++);
+ },
+ readUnsignedByte: function() {
+ return this.view.getUint8(this.index++);
+ },
+ readShort: function(le) {
+ var val = this.view.getInt16(this.index,
+ le !== undefined ? le : this.littleEndian);
+ this.index += 2;
+ return val;
+ },
+ readUnsignedShort: function(le) {
+ var val = this.view.getUint16(this.index,
+ le !== undefined ? le : this.littleEndian);
+ this.index += 2;
+ return val;
+ },
+ readInt: function(le) {
+ var val = this.view.getInt32(this.index,
+ le !== undefined ? le : this.littleEndian);
+ this.index += 4;
+ return val;
+ },
+ readUnsignedInt: function(le) {
+ var val = this.view.getUint32(this.index,
+ le !== undefined ? le : this.littleEndian);
+ this.index += 4;
+ return val;
+ },
+ readFloat: function(le) {
+ var val = this.view.getFloat32(this.index,
+ le !== undefined ? le : this.littleEndian);
+ this.index += 4;
+ return val;
+ },
+ readDouble: function(le) {
+ var val = this.view.getFloat64(this.index,
+ le !== undefined ? le : this.littleEndian);
+ this.index += 8;
+ return val;
+ },
+
+ // Methods to get and set the current position
+ tell: function() {
+ return this.index;
+ },
+ seek: function(index) {
+ if (index < 0)
+ fail('negative index');
+ if (index >= this.byteLength)
+ fail('index greater than buffer size');
+ this.index = index;
+ },
+ advance: function(n) {
+ var index = this.index + n;
+ if (index < 0)
+ fail('advance past beginning of buffer');
+ if (index >= this.byteLength)
+ fail('advance past end of buffer');
+ this.index = index;
+ },
+
+ // Additional methods to read other useful things
+ getUnsignedByteArray: function(offset, n) {
+ return new Uint8Array(this.buffer, offset + this.viewOffset, n);
+ },
+
+ // Additional methods to read other useful things
+ readUnsignedByteArray: function(n) {
+ var val = new Uint8Array(this.buffer, this.index + this.viewOffset, n);
+ this.index += n;
+ return val;
+ },
+
+ getBit: function(offset, bit) {
+ var byte = this.view.getUint8(offset);
+ return (byte & (1 << bit)) !== 0;
+ },
+
+ getUint24: function(offset, le) {
+ var b1, b2, b3;
+ if (le !== undefined ? le : this.littleEndian) {
+ b1 = this.view.getUint8(offset);
+ b2 = this.view.getUint8(offset + 1);
+ b3 = this.view.getUint8(offset + 2);
+ }
+ else { // big end first
+ b3 = this.view.getUint8(offset);
+ b2 = this.view.getUint8(offset + 1);
+ b1 = this.view.getUint8(offset + 2);
+ }
+
+ return (b3 << 16) + (b2 << 8) + b1;
+ },
+
+ readUint24: function(le) {
+ var value = this.getUint24(this.index, le);
+ this.index += 3;
+ return value;
+ },
+
+ // There are lots of ways to read strings.
+ // ASCII, UTF-8, UTF-16.
+ // null-terminated, character length, byte length
+ // I'll implement string reading methods as needed
+
+ getASCIIText: function(offset, len) {
+ var bytes = new Uint8Array(this.buffer, offset + this.viewOffset, len);
+ return String.fromCharCode.apply(String, bytes);
+ },
+
+ readASCIIText: function(len) {
+ var bytes = new Uint8Array(this.buffer,
+ this.index + this.viewOffset,
+ len);
+ this.index += len;
+ return String.fromCharCode.apply(String, bytes);
+ },
+
+ // Replace this with the StringEncoding API when we've got it.
+ // See https://bugzilla.mozilla.org/show_bug.cgi?id=764234
+ getUTF8Text: function(offset, len) {
+ function fail() { throw new Error('Illegal UTF-8'); }
+
+ var pos = offset; // Current position in this.view
+ var end = offset + len; // Last position
+ var charcode; // Current charcode
+ var s = ''; // Accumulate the string
+ var b1, b2, b3, b4; // Up to 4 bytes per charcode
+
+ // See http://en.wikipedia.org/wiki/UTF-8
+ while (pos < end) {
+ var b1 = this.view.getUint8(pos);
+ if (b1 < 128) {
+ s += String.fromCharCode(b1);
+ pos += 1;
+ }
+ else if (b1 < 194) {
+ // unexpected continuation character...
+ fail();
+ }
+ else if (b1 < 224) {
+ // 2-byte sequence
+ if (pos + 1 >= end)
+ fail();
+ b2 = this.view.getUint8(pos + 1);
+ if (b2 < 128 || b2 > 191)
+ fail();
+ charcode = ((b1 & 0x1f) << 6) + (b2 & 0x3f);
+ s += String.fromCharCode(charcode);
+ pos += 2;
+ }
+ else if (b1 < 240) {
+ // 3-byte sequence
+ if (pos + 3 >= end)
+ fail();
+ b2 = this.view.getUint8(pos + 1);
+ if (b2 < 128 || b2 > 191)
+ fail();
+ b3 = this.view.getUint8(pos + 2);
+ if (b3 < 128 || b3 > 191)
+ fail();
+ charcode = ((b1 & 0x0f) << 12) + ((b2 & 0x3f) << 6) + (b3 & 0x3f);
+ s += String.fromCharCode(charcode);
+ pos += 3;
+ }
+ else if (b1 < 245) {
+ // 4-byte sequence
+ if (pos + 3 >= end)
+ fail();
+ b2 = this.view.getUint8(pos + 1);
+ if (b2 < 128 || b2 > 191)
+ fail();
+ b3 = this.view.getUint8(pos + 2);
+ if (b3 < 128 || b3 > 191)
+ fail();
+ b4 = this.view.getUint8(pos + 3);
+ if (b4 < 128 || b4 > 191)
+ fail();
+ charcode = ((b1 & 0x07) << 18) +
+ ((b2 & 0x3f) << 12) +
+ ((b3 & 0x3f) << 6) +
+ (b4 & 0x3f);
+
+ // Now turn this code point into two surrogate pairs
+ charcode -= 0x10000;
+ s += String.fromCharCode(0xd800 + ((charcode & 0x0FFC00) >>> 10));
+ s += String.fromCharCode(0xdc00 + (charcode & 0x0003FF));
+
+ pos += 4;
+ }
+ else {
+ // Illegal byte
+ fail();
+ }
+ }
+
+ return s;
+ },
+
+ readUTF8Text: function(len) {
+ try {
+ return this.getUTF8Text(this.index, len);
+ }
+ finally {
+ this.index += len;
+ }
+ },
+
+ // Read 4 bytes, ignore the high bit and combine them into a 28-bit
+ // big-endian unsigned integer.
+ // This format is used by the ID3v2 metadata.
+ getID3Uint28BE: function(offset) {
+ var b1 = this.view.getUint8(offset) & 0x7f;
+ var b2 = this.view.getUint8(offset + 1) & 0x7f;
+ var b3 = this.view.getUint8(offset + 2) & 0x7f;
+ var b4 = this.view.getUint8(offset + 3) & 0x7f;
+ return (b1 << 21) | (b2 << 14) | (b3 << 7) | b4;
+ },
+
+ readID3Uint28BE: function() {
+ var value = this.getID3Uint28BE(this.index);
+ this.index += 4;
+ return value;
+ },
+
+ // Read bytes up to and including a null terminator, but never
+ // more than size bytes. And return as a Latin1 string
+ readNullTerminatedLatin1Text: function(size) {
+ var s = '';
+ for (var i = 0; i < size; i++) {
+ var charcode = this.view.getUint8(this.index + i);
+ if (charcode === 0) {
+ i++;
+ break;
+ }
+ s += String.fromCharCode(charcode);
+ }
+ this.index += i;
+ return s;
+ },
+
+ // Read bytes up to and including a null terminator, but never
+ // more than size bytes. And return as a UTF8 string
+ readNullTerminatedUTF8Text: function(size) {
+ for (var len = 0; len < size; len++) {
+ if (this.view.getUint8(this.index + len) === 0)
+ break;
+ }
+ var s = this.readUTF8Text(len);
+ if (len < size) // skip the null terminator if we found one
+ this.advance(1);
+ return s;
+ },
+
+ // Read UTF16 text. If le is not specified, expect a BOM to define
+ // endianness. If le is true, read UTF16LE, if false, UTF16BE
+ // Read until we find a null-terminator, but never more than size bytes
+ readNullTerminatedUTF16Text: function(size, le) {
+ if (le == null) {
+ var BOM = this.readUnsignedShort();
+ size -= 2;
+ if (BOM === 0xFEFF)
+ le = false;
+ else
+ le = true;
+ }
+
+ var s = '';
+ for (var i = 0; i < size; i += 2) {
+ var charcode = this.getUint16(this.index + i, le);
+ if (charcode === 0) {
+ i += 2;
+ break;
+ }
+ s += String.fromCharCode(charcode);
+ }
+ this.index += i;
+ return s;
+ }
+ };
+
+ // We don't want users of this library to accidentally call the constructor
+ // instead of using the factory function, so we return a dummy object
+ // instead of the real constructor. If someone really needs to get at the
+ // real constructor, the contructor property of the prototype refers to it.
+ return { get: BlobView.get };
+}());
diff --git a/shared/js/custom_dialog.js b/shared/js/custom_dialog.js
new file mode 100644
index 0000000..f4fa772
--- /dev/null
+++ b/shared/js/custom_dialog.js
@@ -0,0 +1,112 @@
+//XXX: Waiting for the window.showModalDialog support in B2G
+
+'use strict';
+
+var CustomDialog = (function() {
+
+ var screen = null;
+ var dialog = null;
+ var header = null;
+ var message = null;
+ var yes = null;
+ var no = null;
+
+ return {
+ hide: function dialog_hide() {
+ if (screen === null)
+ return;
+
+ document.body.removeChild(screen);
+ screen = null;
+ dialog = null;
+ header = null;
+ message = null;
+ yes = null;
+ no = null;
+ },
+
+ /**
+ * Method that shows the dialog
+ * @param {String} title the title of the dialog. null or empty for
+ * no title.
+ * @param {String} msg message for the dialog.
+ * @param {Object} cancel {title, callback} object when confirm.
+ * @param {Object} confirm {title, callback} object when cancel.
+ */
+ show: function dialog_show(title, msg, cancel, confirm) {
+ if (screen === null) {
+ screen = document.createElement('section');
+ screen.setAttribute('role', 'region');
+ screen.id = 'dialog-screen';
+
+ dialog = document.createElement('div');
+ dialog.id = 'dialog-dialog';
+ dialog.setAttribute('role', 'dialog');
+ screen.appendChild(dialog);
+
+ var info = document.createElement('div');
+ info.className = 'inner';
+
+ if (title && title != '') {
+ header = document.createElement('h3');
+ header.id = 'dialog-title';
+ header.textContent = title;
+ info.appendChild(header);
+ }
+
+ message = document.createElement('p');
+ message.id = 'dialog-message';
+ info.appendChild(message);
+ dialog.appendChild(info);
+
+ var menu = document.createElement('menu');
+ menu.dataset['items'] = 1;
+
+ no = document.createElement('button');
+ var noText = document.createTextNode(cancel.title);
+ no.appendChild(noText);
+ no.id = 'dialog-no';
+ no.addEventListener('click', clickHandler);
+ menu.appendChild(no);
+
+ if (confirm) {
+ menu.dataset['items'] = 2;
+ yes = document.createElement('button');
+ var yesText = document.createTextNode(confirm.title);
+ yes.appendChild(yesText);
+ yes.id = 'dialog-yes';
+ yes.className = 'negative';
+ yes.addEventListener('click', clickHandler);
+ menu.appendChild(yes);
+ }
+
+ dialog.appendChild(menu);
+
+ document.body.appendChild(screen);
+ }
+
+ // Put the message in the dialog.
+ // Note plain text since this may include text from
+ // untrusted app manifests, for example.
+ message.textContent = msg;
+
+ // Make the screen visible
+ screen.classList.add('visible');
+
+ // This is the event listener function for the buttons
+ function clickHandler(evt) {
+
+ // Hide the dialog
+ screen.classList.remove('visible');
+
+ // Call the appropriate callback, if it is defined
+ if (evt.target === yes && confirm.callback) {
+ confirm.callback();
+ } else if (evt.target === no && cancel.callback) {
+ cancel.callback();
+ }
+ }
+ }
+ };
+}());
+
diff --git a/shared/js/desktop.js b/shared/js/desktop.js
new file mode 100644
index 0000000..af0294a
--- /dev/null
+++ b/shared/js/desktop.js
@@ -0,0 +1,115 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+/**
+ * This library should help debugging Gaia on a desktop browser, where APIs like
+ * mozTelephony or mozApps are not supported.
+ */
+
+// navigator.mozTelephony
+(function(window) {
+ var navigator = window.navigator;
+ if ('mozTelephony' in navigator)
+ return;
+
+ var TelephonyCalls = [];
+ if (typeof(RecentsDBManager) != 'undefined' && RecentsDBManager) {
+ RecentsDBManager.init(function() {
+ RecentsDBManager.prepopulateDB(function() {
+ RecentsDBManager.close();
+ });
+ });
+ }
+ navigator.mozTelephony = {
+ dial: function(number) {
+ var TelephonyCall = {
+ number: number,
+ state: 'dialing',
+ addEventListener: function() {},
+ hangUp: function() {},
+ removeEventListener: function() {}
+ };
+
+ TelephonyCalls.push(TelephonyCall);
+
+ return TelephonyCall;
+ },
+ addEventListener: function(name, handler) {
+ },
+ get calls() {
+ return TelephonyCalls;
+ },
+ muted: false,
+ speakerEnabled: false,
+
+ // Stubs
+ onincoming: null,
+ oncallschanged: null
+ };
+})(this);
+
+// Emulate device buttons. This is groteskly unsafe and should be removed soon.
+(function(window) {
+ var supportedEvents = { keydown: true, keyup: true };
+ var listeners = [];
+
+ var originalAddEventListener = window.addEventListener;
+ window.addEventListener = function(type, listener, capture) {
+ if (this === window && supportedEvents[type]) {
+ listeners.push({ type: type, listener: listener, capture: capture });
+ }
+ originalAddEventListener.call(this, type, listener, capture);
+ };
+
+ var originalRemoveEventListener = window.removeEventListener;
+ window.removeEventListener = function(type, listener) {
+ if (this === window && supportedEvents[type]) {
+ var newListeners = [];
+ for (var n = 0; n < listeners.length; ++n) {
+ if (listeners[n].type == type && listeners[n].listener == listener)
+ continue;
+ newListeners.push(listeners[n]);
+ }
+ listeners = newListeners;
+ }
+ originalRemoveEventListener.call(this, type, listener);
+ };
+
+ var KeyEventProto = {
+ DOM_VK_HOME: 36
+ };
+
+ window.addEventListener('message', function(event) {
+ var data = event.data;
+ if (typeof data === 'string' && data.indexOf('moz-key-') == 0) {
+ var type, key;
+ if (data.indexOf('moz-key-down-') == 0) {
+ type = 'keydown';
+ key = data.substr(13);
+ } else if (data.indexOf('moz-key-up-') == 0) {
+ type = 'keyup';
+ key = data.substr(11);
+ } else {
+ return;
+ }
+ key = KeyEvent[key];
+ for (var n = 0; n < listeners.length; ++n) {
+ if (listeners[n].type == type) {
+ var fn = listeners[n].listener;
+ var e = Object.create(KeyEventProto);
+ e.type = type;
+ e.keyCode = key;
+ if (typeof fn === 'function')
+ fn(e);
+ else if (typeof fn === 'object' && fn.handleEvent)
+ fn.handleEvent(e);
+ if (listeners[n].capture)
+ return;
+ }
+ }
+ }
+ });
+})(this);
+
diff --git a/shared/js/gesture_detector.js b/shared/js/gesture_detector.js
new file mode 100644
index 0000000..61e2759
--- /dev/null
+++ b/shared/js/gesture_detector.js
@@ -0,0 +1,891 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+/**
+ * GestureDetector.js: generate events for one and two finger gestures.
+ *
+ * A GestureDetector object listens for touch and mouse events on a specified
+ * element and generates higher-level events that describe one and two finger
+ * gestures on the element. The hope is that this will be useful for webapps
+ * that need to run on mouse (or trackpad)-based desktop browsers and also in
+ * touch-based mobile devices.
+ *
+ * Supported events:
+ *
+ * tap like a click event
+ * dbltap like dblclick
+ * pan one finger motion, or mousedown followed by mousemove
+ * swipe when a finger is released following pan events
+ * holdstart touch (or mousedown) and hold. Must set an option to get these.
+ * holdmove motion after a holdstart event
+ * holdend when the finger or mouse goes up after holdstart/holdmove
+ * transform 2-finger pinch and twist gestures for scaling and rotation
+ * These are touch-only; they can't be simulated with a mouse.
+ *
+ * Each of these events is a bubbling CustomEvent with important details in the
+ * event.detail field. The event details are not yet stable and are not yet
+ * documented. See the calls to emitEvent() for details.
+ *
+ * To use this library, create a GestureDetector object by passing an element to
+ * the GestureDetector() constructor and then calling startDetecting() on it.
+ * The element will be the target of all the emitted gesture events. You can
+ * also pass an optional object as the second constructor argument. If you're
+ * interested in holdstart/holdmove/holdend events, pass {holdEvents:true} as
+ * this second argument. Otherwise they will not be generated.
+ *
+ * Implementation note: event processing is done with a simple finite-state
+ * machine. This means that in general, the various kinds of gestures are
+ * mutually exclusive. You won't get pan events until your finger or mouse has
+ * moved more than a minimum threshold, for example, but it does, the FSM enters
+ * a new state in which it can emit pan and swipe events and cannot emit hold
+ * events. Similarly, if you've started a 1 finger pan/swipe gesture and
+ * accidentally touch with a second finger, you'll continue to get pan events,
+ * and won't suddenly start getting 2-finger transform events.
+ *
+ * This library never calls preventDefault() or stopPropagation on any of the
+ * events it processes, so the raw touch or mouse events should still be
+ * available for other code to process. It is not clear to me whether this is a
+ * feature or a bug.
+ */
+
+var GestureDetector = (function() {
+
+ //
+ // Constructor
+ //
+ function GD(e, options) {
+ this.element = e;
+ this.options = options || {};
+ this.state = initialState;
+ this.timers = {};
+ this.listeningForMouseEvents = true;
+ }
+
+ //
+ // Public methods
+ //
+
+ GD.prototype.startDetecting = function() {
+ var self = this;
+ eventtypes.forEach(function(t) {
+ self.element.addEventListener(t, self);
+ });
+ };
+
+ GD.prototype.stopDetecting = function() {
+ var self = this;
+ eventtypes.forEach(function(t) {
+ self.element.removeEventListener(t, self);
+ });
+ };
+
+ //
+ // Internal methods
+ //
+
+ GD.prototype.handleEvent = function(e) {
+ var handler = this.state[e.type];
+ if (!handler) return;
+
+ // If this is a touch event handle each changed touch separately
+ if (e.changedTouches) {
+ // If we ever receive a touch event, then we know we are on a
+ // touch device and we stop listening for mouse events. If we
+ // don't do that, then the touchstart touchend mousedown mouseup
+ // generated by a single tap gesture will cause us to output
+ // tap tap dbltap, which is wrong
+ if (this.listeningForMouseEvents) {
+ this.listeningForMouseEvents = false;
+ this.element.removeEventListener('mousedown', this);
+ }
+
+ // XXX https://bugzilla.mozilla.org/show_bug.cgi?id=785554
+ // causes touchend events to list all touches as changed, so
+ // warn if we see that bug
+ if (e.type === 'touchend' && e.changedTouches.length > 1) {
+ console.warn('gesture_detector.js: spurious extra changed touch on ' +
+ 'touchend. See ' +
+ 'https://bugzilla.mozilla.org/show_bug.cgi?id=785554');
+ }
+
+ for (var i = 0; i < e.changedTouches.length; i++) {
+ handler(this, e, e.changedTouches[i]);
+ // The first changed touch might have changed the state of the
+ // FSM. We need this line to workaround the bug 785554, but it is
+ // probably the right thing to have here, even once that bug is fixed.
+ handler = this.state[e.type];
+ }
+ }
+ else { // Otherwise, just dispatch the event to the handler
+ handler(this, e);
+ }
+ };
+
+ GD.prototype.startTimer = function(type, time) {
+ this.clearTimer(type);
+ var self = this;
+ this.timers[type] = setTimeout(function() {
+ self.timers[type] = null;
+ var handler = self.state[type];
+ if (handler)
+ handler(self, type);
+ }, time);
+ };
+
+ GD.prototype.clearTimer = function(type) {
+ if (this.timers[type]) {
+ clearTimeout(this.timers[type]);
+ this.timers[type] = null;
+ }
+ };
+
+ // Switch to a new FSM state, and call the init() function of that
+ // state, if it has one. The event and touch arguments are optional
+ // and are just passed through to the state init function.
+ GD.prototype.switchTo = function(state, event, touch) {
+ this.state = state;
+ if (state.init)
+ state.init(this, event, touch);
+ };
+
+ GD.prototype.emitEvent = function(type, detail) {
+ if (!this.target) {
+ console.error('Attempt to emit event with no target');
+ return;
+ }
+
+ var event = this.element.ownerDocument.createEvent('CustomEvent');
+ event.initCustomEvent(type, true, true, detail);
+ this.target.dispatchEvent(event);
+ }
+
+ //
+ // Tuneable parameters
+ //
+ GD.HOLD_INTERVAL = 1000; // Hold events after 1000 ms
+ GD.PAN_THRESHOLD = 20; // 20 pixels movement before touch panning
+ GD.MOUSE_PAN_THRESHOLD = 15; // Mice are more precise, so smaller threshold
+ GD.DOUBLE_TAP_DISTANCE = 50;
+ GD.DOUBLE_TAP_TIME = 500;
+ GD.VELOCITY_SMOOTHING = .5;
+
+ // Don't start sending transform events until the gesture exceeds a threshold
+ GD.SCALE_THRESHOLD = 20; // pixels
+ GD.ROTATE_THRESHOLD = 22.5; // degrees
+
+ // For pans and zooms, we compute new starting coordinates that are part way
+ // between the initial event and the event that crossed the threshold so that
+ // the first event we send doesn't cause a big lurch. This constant must be
+ // between 0 and 1 and says how far along the line between the initial value
+ // and the new value we pick
+ GD.THRESHOLD_SMOOTHING = 0.9;
+
+ //
+ // Helpful shortcuts and utility functions
+ //
+
+ var abs = Math.abs, floor = Math.floor, sqrt = Math.sqrt, atan2 = Math.atan2;
+ var PI = Math.PI;
+
+ // The names of events that we need to register handlers for
+ var eventtypes = [
+ 'touchstart',
+ 'touchmove',
+ 'touchend',
+ 'mousedown' // We register mousemove and mouseup manually
+ ];
+
+ // Return the event's timestamp in ms
+ function eventTime(e) {
+ // In gecko, synthetic events seem to be in microseconds rather than ms.
+ // So if the timestamp is much larger than the current time, assue it is
+ // in microseconds and divide by 1000
+ var ts = e.timeStamp;
+ if (ts > 2 * Date.now())
+ return Math.floor(ts / 1000);
+ else
+ return ts;
+ }
+
+
+ // Return an object containg the space and time coordinates of
+ // and event and touch. We freeze the object to make it immutable so
+ // we can pass it in events and not worry about values being changed.
+ function coordinates(e, t) {
+ return Object.freeze({
+ screenX: t.screenX,
+ screenY: t.screenY,
+ clientX: t.clientX,
+ clientY: t.clientY,
+ timeStamp: eventTime(e)
+ });
+ }
+
+ // Like coordinates(), but return the midpoint between two touches
+ function midpoints(e, t1, t2) {
+ return Object.freeze({
+ screenX: floor((t1.screenX + t2.screenX) / 2),
+ screenY: floor((t1.screenY + t2.screenY) / 2),
+ clientX: floor((t1.clientX + t2.clientX) / 2),
+ clientY: floor((t1.clientY + t2.clientY) / 2),
+ timeStamp: eventTime(e)
+ });
+ }
+
+ // Like coordinates(), but for a mouse event
+ function mouseCoordinates(e) {
+ return Object.freeze({
+ screenX: e.screenX,
+ screenY: e.screenY,
+ clientX: e.clientX,
+ clientY: e.clientY,
+ timeStamp: eventTime(e)
+ });
+ }
+
+ // Given coordinates objects c1 and c2, return a new coordinates object
+ // representing a point and time along the line between those points.
+ // The position of the point is controlled by the THRESHOLD_SMOOTHING constant
+ function between(c1, c2) {
+ var r = GD.THRESHOLD_SMOOTHING;
+ return Object.freeze({
+ screenX: floor(c1.screenX + r * (c2.screenX - c1.screenX)),
+ screenY: floor(c1.screenY + r * (c2.screenY - c1.screenY)),
+ clientX: floor(c1.clientX + r * (c2.clientX - c1.clientX)),
+ clientY: floor(c1.clientY + r * (c2.clientY - c1.clientY)),
+ timeStamp: floor(c1.timeStamp + r * (c2.timeStamp - c1.timeStamp))
+ });
+ }
+
+ // Compute the distance between two touches
+ function touchDistance(t1, t2) {
+ var dx = t2.screenX - t1.screenX;
+ var dy = t2.screenY - t1.screenY;
+ return sqrt(dx * dx + dy * dy);
+ }
+
+ // Compute the direction (as an angle) of the line between two touches
+ // Returns a number d, -180 < d <= 180
+ function touchDirection(t1, t2) {
+ return atan2(t2.screenY - t1.screenY,
+ t2.screenX - t1.screenX) * 180 / PI;
+ }
+
+ // Compute the clockwise angle between direction d1 and direction d2.
+ // Returns an angle a -180 < a <= 180.
+ function touchRotation(d1, d2) {
+ var angle = d2 - d1;
+ if (angle > 180)
+ angle -= 360;
+ else if (angle <= -180)
+ angle += 360;
+ return angle;
+ }
+
+ // Determine if two taps are close enough in time and space to
+ // trigger a dbltap event. The arguments are objects returned
+ // by the coordinates() function.
+ function isDoubleTap(lastTap, thisTap) {
+ var dx = abs(thisTap.screenX - lastTap.screenX);
+ var dy = abs(thisTap.screenY - lastTap.screenY);
+ var dt = thisTap.timeStamp - lastTap.timeStamp;
+ return (dx < GD.DOUBLE_TAP_DISTANCE &&
+ dy < GD.DOUBLE_TAP_DISTANCE &&
+ dt < GD.DOUBLE_TAP_TIME);
+ }
+
+ //
+ // The following objects are the states of our Finite State Machine
+ //
+
+ // In this state we're not processing any gestures, just waiting
+ // for an event to start a gesture and ignoring others
+ var initialState = {
+ name: 'initialState',
+ init: function(d) {
+ // When we enter or return to the initial state, clear
+ // the detector properties that were tracking gestures
+ // Don't clear d.lastTap here, though. We need it for dbltap events
+ d.target = null;
+ d.start = d.last = null;
+ d.touch1 = d.touch2 = null;
+ d.vx = d.vy = null;
+ d.startDistance = d.lastDistance = null;
+ d.startDirection = d.lastDirection = null;
+ d.lastMidpoint = null;
+ d.scaled = d.rotated = null;
+ },
+
+ // Switch to the touchstarted state and process the touch event there
+ // Once we've started processing a touch gesture we'll ignore mouse events
+ touchstart: function(d, e, t) {
+ d.switchTo(touchStartedState, e, t);
+ },
+
+ // Or if we see a mouse event first, then start processing a mouse-based
+ // gesture, and ignore any touch events
+ mousedown: function(d, e) {
+ d.switchTo(mouseDownState, e);
+ }
+ };
+
+ // One finger is down but we haven't generated any event yet. We're
+ // waiting to see... If the finger goes up soon, its a tap. If the finger
+ // stays down and still, its a hold. If the finger moves its a pan/swipe.
+ // And if a second finger goes down, its a transform
+ var touchStartedState = {
+ name: 'touchStartedState',
+ init: function(d, e, t) {
+ // Remember the target of the event
+ d.target = e.target;
+ // Remember the id of the touch that started
+ d.touch1 = t.identifier;
+ // Get the coordinates of the touch
+ d.start = d.last = coordinates(e, t);
+ // Start a timer for a hold
+ // If we're doing hold events, start a timer for them
+ if (d.options.holdEvents)
+ d.startTimer('holdtimeout', GD.HOLD_INTERVAL);
+ },
+
+ touchstart: function(d, e, t) {
+ // If another finger goes down in this state, then
+ // go to transform state to start 2-finger gestures.
+ d.clearTimer('holdtimeout');
+ d.switchTo(transformState, e, t);
+ },
+ touchmove: function(d, e, t) {
+ // Ignore any touches but the initial one
+ // This could happen if there was still a finger down after
+ // the end of a previous 2-finger gesture, e.g.
+ if (t.identifier !== d.touch1)
+ return;
+
+ if (abs(t.screenX - d.start.screenX) > GD.PAN_THRESHOLD ||
+ abs(t.screenY - d.start.screenY) > GD.PAN_THRESHOLD) {
+ d.clearTimer('holdtimeout');
+ d.switchTo(panStartedState, e, t);
+ }
+ },
+ touchend: function(d, e, t) {
+ // Ignore any touches but the initial one
+ if (t.identifier !== d.touch1)
+ return;
+
+ // If there was a previous tap that was close enough in time
+ // and space, then emit a 'dbltap' event
+ if (d.lastTap && isDoubleTap(d.lastTap, d.start)) {
+ d.emitEvent('tap', d.start);
+ d.emitEvent('dbltap', d.start);
+ // clear the lastTap property, so we don't get another one
+ d.lastTap = null;
+ }
+ else {
+ // Emit a 'tap' event using the starting coordinates
+ // as the event details
+ d.emitEvent('tap', d.start);
+
+ // Remember the coordinates of this tap so we can detect double taps
+ d.lastTap = coordinates(e, t);
+ }
+
+ // In either case clear the timer and go back to the initial state
+ d.clearTimer('holdtimeout');
+ d.switchTo(initialState);
+ },
+
+ holdtimeout: function(d) {
+ d.switchTo(holdState);
+ }
+
+ };
+
+ // A single touch has moved enough to exceed the pan threshold and now
+ // we're going to generate pan events after each move and a swipe event
+ // when the touch ends. We ignore any other touches that occur while this
+ // pan/swipe gesture is in progress.
+ var panStartedState = {
+ name: 'panStartedState',
+ init: function(d, e, t) {
+ // Panning doesn't start until the touch has moved more than a
+ // certain threshold. But we don't want the pan to have a jerky
+ // start where the first event is a big distance. So proceed as
+ // pan actually started at a point along the path between the
+ // first touch and this current touch.
+ d.start = d.last = between(d.start, coordinates(e, t));
+
+ // If we transition into this state with a touchmove event,
+ // then process it with that handler. If we don't do this then
+ // we can end up with swipe events that don't know their velocity
+ if (e.type === 'touchmove')
+ panStartedState.touchmove(d, e, t);
+ },
+
+ touchmove: function(d, e, t) {
+ // Ignore any fingers other than the one we're tracking
+ if (t.identifier !== d.touch1)
+ return;
+
+ // Each time the touch moves, emit a pan event but stay in this state
+ var current = coordinates(e, t);
+ d.emitEvent('pan', {
+ absolute: {
+ dx: current.screenX - d.start.screenX,
+ dy: current.screenY - d.start.screenY
+ },
+ relative: {
+ dx: current.screenX - d.last.screenX,
+ dy: current.screenY - d.last.screenY
+ },
+ position: current
+ });
+
+ // Track the pan velocity so we can report this with the swipe
+ // Use a exponential moving average for a bit of smoothing
+ // on the velocity
+ var dt = current.timeStamp - d.last.timeStamp;
+ var vx = (current.screenX - d.last.screenX) / dt;
+ var vy = (current.screenY - d.last.screenY) / dt;
+
+ if (d.vx == null) { // first time; no average
+ d.vx = vx;
+ d.vy = vy;
+ }
+ else {
+ d.vx = d.vx * GD.VELOCITY_SMOOTHING +
+ vx * (1 - GD.VELOCITY_SMOOTHING);
+ d.vy = d.vy * GD.VELOCITY_SMOOTHING +
+ vy * (1 - GD.VELOCITY_SMOOTHING);
+ }
+
+ d.last = current;
+ },
+ touchend: function(d, e, t) {
+ // Ignore any fingers other than the one we're tracking
+ if (t.identifier !== d.touch1)
+ return;
+
+ // Emit a swipe event when the finger goes up.
+ // Report start and end point, dx, dy, dt, velocity and direction
+ var current = coordinates(e, t);
+ var dx = current.screenX - d.start.screenX;
+ var dy = current.screenY - d.start.screenY;
+ // angle is a positive number of degrees, starting at 0 on the
+ // positive x axis and increasing clockwise.
+ var angle = atan2(dy, dx) * 180 / PI;
+ if (angle < 0)
+ angle += 360;
+
+ // Direction is 'right', 'down', 'left' or 'up'
+ var direction;
+ if (angle >= 315 || angle < 45)
+ direction = 'right';
+ else if (angle >= 45 && angle < 135)
+ direction = 'down';
+ else if (angle >= 135 && angle < 225)
+ direction = 'left';
+ else if (angle >= 225 && angle < 315)
+ direction = 'up';
+
+ d.emitEvent('swipe', {
+ start: d.start,
+ end: current,
+ dx: dx,
+ dy: dy,
+ dt: e.timeStamp - d.start.timeStamp,
+ vx: d.vx,
+ vy: d.vy,
+ direction: direction,
+ angle: angle
+ });
+
+ // Go back to the initial state
+ d.switchTo(initialState);
+ }
+ };
+
+ // We enter this state if the user touches and holds for long enough
+ // without moving much. When we enter we emit a holdstart event. Motion
+ // after the holdstart generates holdmove events. And when the touch ends
+ // we generate a holdend event. holdmove and holdend events can be used
+ // kind of like drag and drop events in a mouse-based UI. Currently,
+ // these events just report the coordinates of the touch. Do we need
+ // other details?
+ var holdState = {
+ name: 'holdState',
+ init: function(d) {
+ d.emitEvent('holdstart', d.start);
+ },
+
+ touchmove: function(d, e, t) {
+ var current = coordinates(e, t);
+ d.emitEvent('holdmove', {
+ absolute: {
+ dx: current.screenX - d.start.screenX,
+ dy: current.screenY - d.start.screenY
+ },
+ relative: {
+ dx: current.screenX - d.last.screenX,
+ dy: current.screenY - d.last.screenY
+ },
+ position: current
+ });
+
+ d.last = current;
+ },
+
+ touchend: function(d, e, t) {
+ var current = coordinates(e, t);
+ d.emitEvent('holdend', {
+ start: d.start,
+ end: current,
+ dx: current.screenX - d.start.screenX,
+ dy: current.screenY - d.start.screenY
+ });
+ d.switchTo(initialState);
+ }
+ };
+
+ // We enter this state if a second touch starts before we start
+ // recoginzing any other gesture. As the touches move we track the
+ // distance and angle between them to report scale and rotation values
+ // in transform events.
+ var transformState = {
+ name: 'transformState',
+ init: function(d, e, t) {
+ // Remember the id of the second touch
+ d.touch2 = t.identifier;
+
+ // Get the two Touch objects
+ var t1 = e.touches.identifiedTouch(d.touch1);
+ var t2 = e.touches.identifiedTouch(d.touch2);
+
+ // Compute and remember the initial distance and angle
+ d.startDistance = d.lastDistance = touchDistance(t1, t2);
+ d.startDirection = d.lastDirection = touchDirection(t1, t2);
+
+ // Don't start emitting events until we're past a threshold
+ d.scaled = d.rotated = false;
+ },
+
+ touchmove: function(d, e, t) {
+ // Ignore touches we're not tracking
+ if (t.identifier !== d.touch1 && t.identifier !== d.touch2)
+ return;
+
+ // Get the two Touch objects
+ var t1 = e.touches.identifiedTouch(d.touch1);
+ var t2 = e.touches.identifiedTouch(d.touch2);
+
+ // Compute the new midpoints, distance and direction
+ var midpoint = midpoints(e, t1, t2);
+ var distance = touchDistance(t1, t2);
+ var direction = touchDirection(t1, t2);
+ var rotation = touchRotation(d.startDirection, direction);
+
+ // Check all of these numbers against the thresholds. Otherwise
+ // the transforms are too jittery even when you try to hold your
+ // fingers still.
+ if (!d.scaled) {
+ if (abs(distance - d.startDistance) > GD.SCALE_THRESHOLD) {
+ d.scaled = true;
+ d.startDistance = d.lastDistance =
+ floor(d.startDistance +
+ GD.THRESHOLD_SMOOTHING * (distance - d.startDistance));
+ }
+ else
+ distance = d.startDistance;
+ }
+ if (!d.rotated) {
+ if (abs(rotation) > GD.ROTATE_THRESHOLD)
+ d.rotated = true;
+ else
+ direction = d.startDirection;
+ }
+
+ // If nothing has exceeded the threshold yet, then we
+ // don't even have to fire an event.
+ if (d.scaled || d.rotated) {
+ // The detail field for the transform gesture event includes
+ // 'absolute' transformations against the initial values and
+ // 'relative' transformations against the values from the last
+ // transformgesture event.
+ d.emitEvent('transform', {
+ absolute: { // transform details since gesture start
+ scale: distance / d.startDistance,
+ rotate: touchRotation(d.startDirection, direction)
+ },
+ relative: { // transform since last gesture change
+ scale: distance / d.lastDistance,
+ rotate: touchRotation(d.lastDirection, direction)
+ },
+ midpoint: midpoint
+ });
+
+ d.lastDistance = distance;
+ d.lastDirection = direction;
+ d.lastMidpoint = midpoint;
+ }
+ },
+
+ touchend: function(d, e, t) {
+ // If either finger goes up, we're done with the gesture.
+ // The user might move that finger and put it right back down
+ // again to begin another 2-finger gesture, so we can't go
+ // back to the initial state while one of the fingers remains up.
+ // On the other hand, we can't go back to touchStartedState because
+ // that would mean that the finger left down could cause a tap or
+ // pan event. So we need an afterTransform state that waits for
+ // a finger to come back down or the other finger to go up.
+ if (t.identifier === d.touch2)
+ d.touch2 = null;
+ else if (t.identifier === d.touch1) {
+ d.touch1 = d.touch2;
+ d.touch2 = null;
+ }
+ else
+ return; // It was a touch we weren't tracking
+
+ // If we emitted any transform events, now we need to emit
+ // a transformend event to end the series. The details of this
+ // event use the values from the last touchmove, and the
+ // relative amounts will 1 and 0, but they are included for
+ // completeness even though they are not useful.
+ if (d.scaled || d.rotated) {
+ d.emitEvent('transformend', {
+ absolute: { // transform details since gesture start
+ scale: d.lastDistance / d.startDistance,
+ rotate: touchRotation(d.startDirection, d.lastDirection)
+ },
+ relative: { // nothing has changed relative to the last touchmove
+ scale: 1,
+ rotate: 0
+ },
+ midpoint: d.lastMidpoint
+ });
+ }
+
+ d.switchTo(afterTransformState);
+ }
+ };
+
+ // We did a tranform and one finger went up. Wait for that finger to
+ // come back down or the other finger to go up too.
+ var afterTransformState = {
+ name: 'afterTransformState',
+ touchstart: function(d, e, t) {
+ d.switchTo(transformState, e, t);
+ },
+
+ touchend: function(d, e, t) {
+ if (t.identifier === d.touch1)
+ d.switchTo(initialState);
+ }
+ };
+
+ var mouseDownState = {
+ name: 'mouseDownState',
+ init: function(d, e) {
+ // Remember the target of the event
+ d.target = e.target;
+
+ // Register this detector as a *capturing* handler on the document
+ // so we get all subsequent mouse events until we remove these handlers
+ var doc = d.element.ownerDocument;
+ doc.addEventListener('mousemove', d, true);
+ doc.addEventListener('mouseup', d, true);
+
+ // Get the coordinates of the mouse event
+ d.start = d.last = mouseCoordinates(e);
+
+ // Start a timer for a hold
+ // If we're doing hold events, start a timer for them
+ if (d.options.holdEvents)
+ d.startTimer('holdtimeout', GD.HOLD_INTERVAL);
+ },
+
+ mousemove: function(d, e) {
+ // If the mouse has moved more than the panning threshold,
+ // then switch to the mouse panning state. Otherwise remain
+ // in this state
+
+ if (abs(e.screenX - d.start.screenX) > GD.MOUSE_PAN_THRESHOLD ||
+ abs(e.screenY - d.start.screenY) > GD.MOUSE_PAN_THRESHOLD) {
+ d.clearTimer('holdtimeout');
+ d.switchTo(mousePannedState, e);
+ }
+ },
+
+ mouseup: function(d, e) {
+ // Remove the capturing event handlers
+ var doc = d.element.ownerDocument;
+ doc.removeEventListener('mousemove', d, true);
+ doc.removeEventListener('mouseup', d, true);
+
+ // If there was a previous tap that was close enough in time
+ // and space, then emit a 'dbltap' event
+ if (d.lastTap && isDoubleTap(d.lastTap, d.start)) {
+ d.emitEvent('tap', d.start);
+ d.emitEvent('dbltap', d.start);
+ d.lastTap = null; // so we don't get another one
+ }
+ else {
+ // Emit a 'tap' event using the starting coordinates
+ // as the event details
+ d.emitEvent('tap', d.start);
+
+ // Remember the coordinates of this tap so we can detect double taps
+ d.lastTap = mouseCoordinates(e);
+ }
+
+ // In either case clear the timer and go back to the initial state
+ d.clearTimer('holdtimeout');
+ d.switchTo(initialState);
+ },
+
+ holdtimeout: function(d) {
+ d.switchTo(mouseHoldState);
+ }
+ };
+
+ // Like holdState, but for mouse events instead of touch events
+ var mouseHoldState = {
+ name: 'mouseHoldState',
+ init: function(d) {
+ d.emitEvent('holdstart', d.start);
+ },
+
+ mousemove: function(d, e) {
+ var current = mouseCoordinates(e);
+ d.emitEvent('holdmove', {
+ absolute: {
+ dx: current.screenX - d.start.screenX,
+ dy: current.screenY - d.start.screenY
+ },
+ relative: {
+ dx: current.screenX - d.last.screenX,
+ dy: current.screenY - d.last.screenY
+ },
+ position: current
+ });
+
+ d.last = current;
+ },
+
+ mouseup: function(d, e) {
+ var current = mouseCoordinates(e);
+ d.emitEvent('holdend', {
+ start: d.start,
+ end: current,
+ dx: current.screenX - d.start.screenX,
+ dy: current.screenY - d.start.screenY
+ });
+ d.switchTo(initialState);
+ }
+ };
+
+ var mousePannedState = {
+ name: 'mousePannedState',
+ init: function(d, e) {
+ // Panning doesn't start until the mouse has moved more than
+ // a certain threshold. But we don't want the pan to have a jerky
+ // start where the first event is a big distance. So reset the
+ // starting point to a point between the start point and this
+ // current point
+ d.start = d.last = between(d.start, mouseCoordinates(e));
+
+ // If we transition into this state with a mousemove event,
+ // then process it with that handler. If we don't do this then
+ // we can end up with swipe events that don't know their velocity
+ if (e.type === 'mousemove')
+ mousePannedState.mousemove(d, e);
+ },
+ mousemove: function(d, e) {
+ // Each time the mouse moves, emit a pan event but stay in this state
+ var current = mouseCoordinates(e);
+ d.emitEvent('pan', {
+ absolute: {
+ dx: current.screenX - d.start.screenX,
+ dy: current.screenY - d.start.screenY
+ },
+ relative: {
+ dx: current.screenX - d.last.screenX,
+ dy: current.screenY - d.last.screenY
+ },
+ position: current
+ });
+
+ // Track the pan velocity so we can report this with the swipe
+ // Use a exponential moving average for a bit of smoothing
+ // on the velocity
+ var dt = current.timeStamp - d.last.timeStamp;
+ var vx = (current.screenX - d.last.screenX) / dt;
+ var vy = (current.screenY - d.last.screenY) / dt;
+
+ if (d.vx == null) { // first time; no average
+ d.vx = vx;
+ d.vy = vy;
+ }
+ else {
+ d.vx = d.vx * GD.VELOCITY_SMOOTHING +
+ vx * (1 - GD.VELOCITY_SMOOTHING);
+ d.vy = d.vy * GD.VELOCITY_SMOOTHING +
+ vy * (1 - GD.VELOCITY_SMOOTHING);
+ }
+
+ d.last = current;
+ },
+ mouseup: function(d, e) {
+ // Remove the capturing event handlers
+ var doc = d.element.ownerDocument;
+ doc.removeEventListener('mousemove', d, true);
+ doc.removeEventListener('mouseup', d, true);
+
+ // Emit a swipe event when the mouse goes up.
+ // Report start and end point, dx, dy, dt, velocity and direction
+ var current = mouseCoordinates(e);
+
+ // FIXME:
+ // lots of code duplicated between this state and the corresponding
+ // touch state, can I combine them somehow?
+ var dx = current.screenX - d.start.screenX;
+ var dy = current.screenY - d.start.screenY;
+ // angle is a positive number of degrees, starting at 0 on the
+ // positive x axis and increasing clockwise.
+ var angle = atan2(dy, dx) * 180 / PI;
+ if (angle < 0)
+ angle += 360;
+
+ // Direction is 'right', 'down', 'left' or 'up'
+ var direction;
+ if (angle >= 315 || angle < 45)
+ direction = 'right';
+ else if (angle >= 45 && angle < 135)
+ direction = 'down';
+ else if (angle >= 135 && angle < 225)
+ direction = 'left';
+ else if (angle >= 225 && angle < 315)
+ direction = 'up';
+
+ d.emitEvent('swipe', {
+ start: d.start,
+ end: current,
+ dx: dx,
+ dy: dy,
+ dt: current.timeStamp - d.start.timeStamp,
+ vx: d.vx,
+ vy: d.vy,
+ direction: direction,
+ angle: angle
+ });
+
+ // Go back to the initial state
+ d.switchTo(initialState);
+ }
+ };
+
+ return GD;
+}());
+
diff --git a/shared/js/idletimer.js b/shared/js/idletimer.js
new file mode 100644
index 0000000..5c42384
--- /dev/null
+++ b/shared/js/idletimer.js
@@ -0,0 +1,127 @@
+/*
+ This file implements window.setIdleTimeout() and
+ window.clearIdleTimeout(). They look like setTimeout() except the
+ setIdleTimeout() function takes two callbacks:
+
+ setIdleTimeout(idleCallback, activeCallback, ms):
+ idleCallback will fire after specific microsecond of idle.
+ The time it takes will calculated from the time
+ setIdleTimeout() is called and resets as user interacts.
+
+ activeCallback will fire when the first user action *after*
+ idleCallback fires
+
+ returns id.
+
+ clearIdleTimeout(id):
+ takes the id returns from setIdleTimeout() and cancels it.
+
+*/
+
+// Wrap everything into a closure so we will not expose idleTimerRegistry
+
+'use strict';
+
+(function idleTimerAsAIdleObserverWrapper(win) {
+
+ // stuff the 0th element so id is always a truey value
+ var idleTimerRegistry = [undefined];
+
+ // setIdleTimeout()
+ win.setIdleTimeout = function setIdleTimeout(idleCallback,
+ activeCallback,
+ ms) {
+ var startTimestamp = Date.now();
+ var idleFired = false;
+
+ var idleTimer = {
+ timer: undefined,
+ resetStartTimestamp: function resetStartTimestamp() {
+ startTimestamp = Date.now();
+ }
+ };
+
+ // If the system unix time changes, we would need to update
+ // the number we kept in startTimestamp, or bad things will happen.
+ window.addEventListener('moztimechange', idleTimer.resetStartTimestamp);
+
+ // Create an idle observer with a very short time as
+ // we are not interested in when onidle fires (since it's inaccuate),
+ // instead, we need to know when onactive callback calls.
+ idleTimer.observer = {
+ onidle: function observerReportIdle() {
+ // Once the onidle fires, the next user action will trigger
+ // onactive.
+
+ // The time it takes for onidle to fire need to be subtracted from
+ // the real time we are going to set to setTimeout()
+ var time = (ms - (Date.now() - startTimestamp));
+
+ // Let's start the real count down and wait for that.
+ idleTimer.timer = setTimeout(function idled() {
+ // remove the timer
+ idleTimer.timer = undefined;
+
+ // set idleFired to true
+ idleFired = true;
+
+ // fire the real idleCallback
+ idleCallback();
+ }, time);
+ },
+ onactive: function observerReportActive() {
+ // Remove the timer set by onidle
+ if (idleTimer.timer) {
+ clearTimeout(idleTimer.timer);
+ idleTimer.timer = undefined;
+ }
+
+ // Reset the timestamp; the next real count down should start
+ // from the time onactive fires
+ startTimestamp = Date.now();
+
+ // If idleCallback is not called yet,
+ // we should not trigger activeCallback here
+ if (!idleFired)
+ return;
+
+ // fire the real activeCallback
+ activeCallback();
+
+ // reset the flag
+ idleFired = false;
+
+ // After a short time, onidle will fire again.
+ // timer will be registered there again.
+ },
+ time: 1
+ };
+
+ // Register the idleObserver
+ navigator.addIdleObserver(idleTimer.observer);
+
+ // Push the idleTimer object to the registry
+ idleTimerRegistry.push(idleTimer);
+
+ // return the id so people can do clearIdleTimeout();
+ return (idleTimerRegistry.length - 1);
+ };
+
+ // clearIdleTimeout()
+ win.clearIdleTimeout = function clearIdleTimeout(id) {
+ if (!idleTimerRegistry[id])
+ return;
+
+ // Get the idleTimer object and remove it from registry
+ var idleTimer = idleTimerRegistry[id];
+ idleTimerRegistry[id] = undefined;
+
+ // Properly clean it up, make sure we will never heard from
+ // those callbacks ever again.
+ navigator.removeIdleObserver(idleTimer.observer);
+ window.removeEventListener('moztimechange', idleTimer.resetStartTimestamp);
+ if (idleTimer.timer)
+ clearTimeout(idleTimer.timer);
+ };
+
+})(this);
diff --git a/shared/js/l10n.js b/shared/js/l10n.js
new file mode 100644
index 0000000..adbc51a
--- /dev/null
+++ b/shared/js/l10n.js
@@ -0,0 +1,1014 @@
+/* -*- Mode: js; js-indent-level: 2; indent-tabs-mode: nil -*- */
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+/**
+ * This library exposes a `navigator.mozL10n' object to handle client-side
+ * application localization. See: https://github.com/fabi1cazenave/webL10n
+ */
+
+(function(window) {
+ var gL10nData = {};
+ var gTextProp = 'textContent';
+ var gLanguage = '';
+ var gMacros = {};
+ var gReadyState = 'loading';
+
+
+ /**
+ * Synchronously loading l10n resources significantly minimizes flickering
+ * from displaying the app with non-localized strings and then updating the
+ * strings. Although this will block all script execution on this page, we
+ * expect that the l10n resources are available locally on flash-storage.
+ *
+ * As synchronous XHR is generally considered as a bad idea, we're still
+ * loading l10n resources asynchronously -- but we keep this in a setting,
+ * just in case... and applications using this library should hide their
+ * content until the `localized' event happens.
+ */
+
+ var gAsyncResourceLoading = true; // read-only
+
+
+ /**
+ * Debug helpers
+ *
+ * gDEBUG == 0: don't display any console message
+ * gDEBUG == 1: display only warnings, not logs
+ * gDEBUG == 2: display all console messages
+ */
+
+ var gDEBUG = 1;
+
+ function consoleLog(message) {
+ if (gDEBUG >= 2) {
+ console.log('[l10n] ' + message);
+ }
+ };
+
+ function consoleWarn(message) {
+ if (gDEBUG) {
+ console.warn('[l10n] ' + message);
+ }
+ };
+
+
+ /**
+ * DOM helpers for the so-called "HTML API".
+ *
+ * These functions are written for modern browsers. For old versions of IE,
+ * they're overridden in the 'startup' section at the end of this file.
+ */
+
+ function getL10nResourceLinks() {
+ return document.querySelectorAll('link[type="application/l10n"]');
+ }
+
+ function getL10nDictionary() {
+ var script = document.querySelector('script[type="application/l10n"]');
+ // TODO: support multiple and external JSON dictionaries
+ return script ? JSON.parse(script.innerHTML) : null;
+ }
+
+ function getTranslatableChildren(element) {
+ return element ? element.querySelectorAll('*[data-l10n-id]') : [];
+ }
+
+ function getL10nAttributes(element) {
+ if (!element)
+ return {};
+
+ var l10nId = element.getAttribute('data-l10n-id');
+ var l10nArgs = element.getAttribute('data-l10n-args');
+ var args = {};
+ if (l10nArgs) {
+ try {
+ args = JSON.parse(l10nArgs);
+ } catch (e) {
+ consoleWarn('could not parse arguments for #' + l10nId);
+ }
+ }
+ return { id: l10nId, args: args };
+ }
+
+ function fireL10nReadyEvent() {
+ var evtObject = document.createEvent('Event');
+ evtObject.initEvent('localized', false, false);
+ evtObject.language = gLanguage;
+ window.dispatchEvent(evtObject);
+ }
+
+
+ /**
+ * l10n resource parser:
+ * - reads (async XHR) the l10n resource matching `lang';
+ * - imports linked resources (synchronously) when specified;
+ * - parses the text data (fills `gL10nData');
+ * - triggers success/failure callbacks when done.
+ *
+ * @param {string} href
+ * URL of the l10n resource to parse.
+ *
+ * @param {string} lang
+ * locale (language) to parse.
+ *
+ * @param {Function} successCallback
+ * triggered when the l10n resource has been successully parsed.
+ *
+ * @param {Function} failureCallback
+ * triggered when the an error has occured.
+ *
+ * @return {void}
+ * uses the following global variables: gL10nData, gTextProp.
+ */
+
+ function parseResource(href, lang, successCallback, failureCallback) {
+ var baseURL = href.replace(/\/[^\/]*$/, '/');
+
+ // handle escaped characters (backslashes) in a string
+ function evalString(text) {
+ if (text.lastIndexOf('\\') < 0)
+ return text;
+ return text.replace(/\\\\/g, '\\')
+ .replace(/\\n/g, '\n')
+ .replace(/\\r/g, '\r')
+ .replace(/\\t/g, '\t')
+ .replace(/\\b/g, '\b')
+ .replace(/\\f/g, '\f')
+ .replace(/\\{/g, '{')
+ .replace(/\\}/g, '}')
+ .replace(/\\"/g, '"')
+ .replace(/\\'/g, "'");
+ }
+
+ // parse *.properties text data into an l10n dictionary
+ function parseProperties(text) {
+ var dictionary = [];
+
+ // token expressions
+ var reBlank = /^\s*|\s*$/;
+ var reComment = /^\s*#|^\s*$/;
+ var reSection = /^\s*\[(.*)\]\s*$/;
+ var reImport = /^\s*@import\s+url\((.*)\)\s*$/i;
+ var reSplit = /^([^=\s]*)\s*=\s*(.+)$/; // TODO: escape EOLs with '\'
+
+ // parse the *.properties file into an associative array
+ function parseRawLines(rawText, extendedSyntax) {
+ var entries = rawText.replace(reBlank, '').split(/[\r\n]+/);
+ var currentLang = '*';
+ var genericLang = lang.replace(/-[a-z]+$/i, '');
+ var skipLang = false;
+ var match = '';
+
+ for (var i = 0; i < entries.length; i++) {
+ var line = entries[i];
+
+ // comment or blank line?
+ if (reComment.test(line))
+ continue;
+
+ // the extended syntax supports [lang] sections and @import rules
+ if (extendedSyntax) {
+ if (reSection.test(line)) { // section start?
+ match = reSection.exec(line);
+ currentLang = match[1];
+ skipLang = (currentLang !== '*') &&
+ (currentLang !== lang) && (currentLang !== genericLang);
+ continue;
+ } else if (skipLang) {
+ continue;
+ }
+ if (reImport.test(line)) { // @import rule?
+ match = reImport.exec(line);
+ loadImport(baseURL + match[1]); // load the resource synchronously
+ }
+ }
+
+ // key-value pair
+ var tmp = line.match(reSplit);
+ if (tmp && tmp.length == 3) {
+ dictionary[tmp[1]] = evalString(tmp[2]);
+ }
+ }
+ }
+
+ // import another *.properties file
+ function loadImport(url) {
+ loadResource(url, function(content) {
+ parseRawLines(content, false); // don't allow recursive imports
+ }, null, false); // load synchronously
+ }
+
+ // fill the dictionary
+ parseRawLines(text, true);
+ return dictionary;
+ }
+
+ // load the specified resource file
+ function loadResource(url, onSuccess, onFailure, asynchronous) {
+ onSuccess = onSuccess || function _onSuccess(data) {};
+ onFailure = onFailure || function _onFailure() {
+ consoleWarn(url + ' not found.');
+ };
+
+ var xhr = new XMLHttpRequest();
+ xhr.open('GET', url, asynchronous);
+ if (xhr.overrideMimeType) {
+ xhr.overrideMimeType('text/plain; charset=utf-8');
+ }
+ xhr.onreadystatechange = function() {
+ if (xhr.readyState == 4) {
+ if (xhr.status == 200 || xhr.status === 0) {
+ onSuccess(xhr.responseText);
+ } else {
+ onFailure();
+ }
+ }
+ };
+ xhr.onerror = onFailure;
+ xhr.ontimeout = onFailure;
+
+ // in Firefox OS with the app:// protocol, trying to XHR a non-existing
+ // URL will raise an exception here -- hence this ugly try...catch.
+ try {
+ xhr.send(null);
+ } catch (e) {
+ onFailure();
+ }
+ }
+
+ // load and parse l10n data (warning: global variables are used here)
+ loadResource(href, function(response) {
+ // parse *.properties text data into an l10n dictionary
+ var data = parseProperties(response);
+
+ // find attribute descriptions, if any
+ for (var key in data) {
+ var id, prop, index = key.lastIndexOf('.');
+ if (index > 0) { // an attribute has been specified
+ id = key.substring(0, index);
+ prop = key.substr(index + 1);
+ } else { // no attribute: assuming text content by default
+ id = key;
+ prop = gTextProp;
+ }
+ if (!gL10nData[id]) {
+ gL10nData[id] = {};
+ }
+ gL10nData[id][prop] = data[key];
+ }
+
+ // trigger callback
+ if (successCallback) {
+ successCallback();
+ }
+ }, failureCallback, gAsyncResourceLoading);
+ };
+
+ // load and parse all resources for the specified locale
+ function loadLocale(lang, callback) {
+ callback = callback || function _callback() {};
+
+ clear();
+ gLanguage = lang;
+
+ // check all <link type="application/l10n" href="..." /> nodes
+ // and load the resource files
+ var langLinks = getL10nResourceLinks();
+ var langCount = langLinks.length;
+ if (langCount == 0) {
+ // we might have a pre-compiled dictionary instead
+ var dict = getL10nDictionary();
+ if (dict && dict.locales && dict.default_locale) {
+ consoleLog('using the embedded JSON directory, early way out');
+ gL10nData = dict.locales[lang] || dict.locales[dict.default_locale];
+ callback();
+ } else {
+ consoleLog('no resource to load, early way out');
+ }
+ // early way out
+ fireL10nReadyEvent(lang);
+ gReadyState = 'complete';
+ return;
+ }
+
+ // start the callback when all resources are loaded
+ var onResourceLoaded = null;
+ var gResourceCount = 0;
+ onResourceLoaded = function() {
+ gResourceCount++;
+ if (gResourceCount >= langCount) {
+ callback();
+ fireL10nReadyEvent(lang);
+ gReadyState = 'complete';
+ }
+ };
+
+ // load all resource files
+ function l10nResourceLink(link) {
+ var href = link.href;
+ var type = link.type;
+ this.load = function(lang, callback) {
+ var applied = lang;
+ parseResource(href, lang, callback, function() {
+ consoleWarn(href + ' not found.');
+ applied = '';
+ });
+ return applied; // return lang if found, an empty string if not found
+ };
+ }
+
+ for (var i = 0; i < langCount; i++) {
+ var resource = new l10nResourceLink(langLinks[i]);
+ var rv = resource.load(lang, onResourceLoaded);
+ if (rv != lang) { // lang not found, used default resource instead
+ consoleWarn('"' + lang + '" resource not found');
+ gLanguage = '';
+ }
+ }
+ }
+
+ // clear all l10n data
+ function clear() {
+ gL10nData = {};
+ gLanguage = '';
+ // TODO: clear all non predefined macros.
+ // There's no such macro /yet/ but we're planning to have some...
+ }
+
+
+ /**
+ * Get rules for plural forms (shared with JetPack), see:
+ * http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html
+ * https://github.com/mozilla/addon-sdk/blob/master/python-lib/plural-rules-generator.p
+ *
+ * @param {string} lang
+ * locale (language) used.
+ *
+ * @return {Function}
+ * returns a function that gives the plural form name for a given integer:
+ * var fun = getPluralRules('en');
+ * fun(1) -> 'one'
+ * fun(0) -> 'other'
+ * fun(1000) -> 'other'.
+ */
+
+ function getPluralRules(lang) {
+ var locales2rules = {
+ 'af': 3,
+ 'ak': 4,
+ 'am': 4,
+ 'ar': 1,
+ 'asa': 3,
+ 'az': 0,
+ 'be': 11,
+ 'bem': 3,
+ 'bez': 3,
+ 'bg': 3,
+ 'bh': 4,
+ 'bm': 0,
+ 'bn': 3,
+ 'bo': 0,
+ 'br': 20,
+ 'brx': 3,
+ 'bs': 11,
+ 'ca': 3,
+ 'cgg': 3,
+ 'chr': 3,
+ 'cs': 12,
+ 'cy': 17,
+ 'da': 3,
+ 'de': 3,
+ 'dv': 3,
+ 'dz': 0,
+ 'ee': 3,
+ 'el': 3,
+ 'en': 3,
+ 'eo': 3,
+ 'es': 3,
+ 'et': 3,
+ 'eu': 3,
+ 'fa': 0,
+ 'ff': 5,
+ 'fi': 3,
+ 'fil': 4,
+ 'fo': 3,
+ 'fr': 5,
+ 'fur': 3,
+ 'fy': 3,
+ 'ga': 8,
+ 'gd': 24,
+ 'gl': 3,
+ 'gsw': 3,
+ 'gu': 3,
+ 'guw': 4,
+ 'gv': 23,
+ 'ha': 3,
+ 'haw': 3,
+ 'he': 2,
+ 'hi': 4,
+ 'hr': 11,
+ 'hu': 0,
+ 'id': 0,
+ 'ig': 0,
+ 'ii': 0,
+ 'is': 3,
+ 'it': 3,
+ 'iu': 7,
+ 'ja': 0,
+ 'jmc': 3,
+ 'jv': 0,
+ 'ka': 0,
+ 'kab': 5,
+ 'kaj': 3,
+ 'kcg': 3,
+ 'kde': 0,
+ 'kea': 0,
+ 'kk': 3,
+ 'kl': 3,
+ 'km': 0,
+ 'kn': 0,
+ 'ko': 0,
+ 'ksb': 3,
+ 'ksh': 21,
+ 'ku': 3,
+ 'kw': 7,
+ 'lag': 18,
+ 'lb': 3,
+ 'lg': 3,
+ 'ln': 4,
+ 'lo': 0,
+ 'lt': 10,
+ 'lv': 6,
+ 'mas': 3,
+ 'mg': 4,
+ 'mk': 16,
+ 'ml': 3,
+ 'mn': 3,
+ 'mo': 9,
+ 'mr': 3,
+ 'ms': 0,
+ 'mt': 15,
+ 'my': 0,
+ 'nah': 3,
+ 'naq': 7,
+ 'nb': 3,
+ 'nd': 3,
+ 'ne': 3,
+ 'nl': 3,
+ 'nn': 3,
+ 'no': 3,
+ 'nr': 3,
+ 'nso': 4,
+ 'ny': 3,
+ 'nyn': 3,
+ 'om': 3,
+ 'or': 3,
+ 'pa': 3,
+ 'pap': 3,
+ 'pl': 13,
+ 'ps': 3,
+ 'pt': 3,
+ 'rm': 3,
+ 'ro': 9,
+ 'rof': 3,
+ 'ru': 11,
+ 'rwk': 3,
+ 'sah': 0,
+ 'saq': 3,
+ 'se': 7,
+ 'seh': 3,
+ 'ses': 0,
+ 'sg': 0,
+ 'sh': 11,
+ 'shi': 19,
+ 'sk': 12,
+ 'sl': 14,
+ 'sma': 7,
+ 'smi': 7,
+ 'smj': 7,
+ 'smn': 7,
+ 'sms': 7,
+ 'sn': 3,
+ 'so': 3,
+ 'sq': 3,
+ 'sr': 11,
+ 'ss': 3,
+ 'ssy': 3,
+ 'st': 3,
+ 'sv': 3,
+ 'sw': 3,
+ 'syr': 3,
+ 'ta': 3,
+ 'te': 3,
+ 'teo': 3,
+ 'th': 0,
+ 'ti': 4,
+ 'tig': 3,
+ 'tk': 3,
+ 'tl': 4,
+ 'tn': 3,
+ 'to': 0,
+ 'tr': 0,
+ 'ts': 3,
+ 'tzm': 22,
+ 'uk': 11,
+ 'ur': 3,
+ 've': 3,
+ 'vi': 0,
+ 'vun': 3,
+ 'wa': 4,
+ 'wae': 3,
+ 'wo': 0,
+ 'xh': 3,
+ 'xog': 3,
+ 'yo': 0,
+ 'zh': 0,
+ 'zu': 3
+ };
+
+ // utility functions for plural rules methods
+ function isIn(n, list) {
+ return list.indexOf(n) !== -1;
+ }
+ function isBetween(n, start, end) {
+ return start <= n && n <= end;
+ }
+
+ // list of all plural rules methods:
+ // map an integer to the plural form name to use
+ var pluralRules = {
+ '0': function(n) {
+ return 'other';
+ },
+ '1': function(n) {
+ if ((isBetween((n % 100), 3, 10)))
+ return 'few';
+ if (n === 0)
+ return 'zero';
+ if ((isBetween((n % 100), 11, 99)))
+ return 'many';
+ if (n == 2)
+ return 'two';
+ if (n == 1)
+ return 'one';
+ return 'other';
+ },
+ '2': function(n) {
+ if (n !== 0 && (n % 10) === 0)
+ return 'many';
+ if (n == 2)
+ return 'two';
+ if (n == 1)
+ return 'one';
+ return 'other';
+ },
+ '3': function(n) {
+ if (n == 1)
+ return 'one';
+ return 'other';
+ },
+ '4': function(n) {
+ if ((isBetween(n, 0, 1)))
+ return 'one';
+ return 'other';
+ },
+ '5': function(n) {
+ if ((isBetween(n, 0, 2)) && n != 2)
+ return 'one';
+ return 'other';
+ },
+ '6': function(n) {
+ if (n === 0)
+ return 'zero';
+ if ((n % 10) == 1 && (n % 100) != 11)
+ return 'one';
+ return 'other';
+ },
+ '7': function(n) {
+ if (n == 2)
+ return 'two';
+ if (n == 1)
+ return 'one';
+ return 'other';
+ },
+ '8': function(n) {
+ if ((isBetween(n, 3, 6)))
+ return 'few';
+ if ((isBetween(n, 7, 10)))
+ return 'many';
+ if (n == 2)
+ return 'two';
+ if (n == 1)
+ return 'one';
+ return 'other';
+ },
+ '9': function(n) {
+ if (n === 0 || n != 1 && (isBetween((n % 100), 1, 19)))
+ return 'few';
+ if (n == 1)
+ return 'one';
+ return 'other';
+ },
+ '10': function(n) {
+ if ((isBetween((n % 10), 2, 9)) && !(isBetween((n % 100), 11, 19)))
+ return 'few';
+ if ((n % 10) == 1 && !(isBetween((n % 100), 11, 19)))
+ return 'one';
+ return 'other';
+ },
+ '11': function(n) {
+ if ((isBetween((n % 10), 2, 4)) && !(isBetween((n % 100), 12, 14)))
+ return 'few';
+ if ((n % 10) === 0 ||
+ (isBetween((n % 10), 5, 9)) ||
+ (isBetween((n % 100), 11, 14)))
+ return 'many';
+ if ((n % 10) == 1 && (n % 100) != 11)
+ return 'one';
+ return 'other';
+ },
+ '12': function(n) {
+ if ((isBetween(n, 2, 4)))
+ return 'few';
+ if (n == 1)
+ return 'one';
+ return 'other';
+ },
+ '13': function(n) {
+ if ((isBetween((n % 10), 2, 4)) && !(isBetween((n % 100), 12, 14)))
+ return 'few';
+ if (n != 1 && (isBetween((n % 10), 0, 1)) ||
+ (isBetween((n % 10), 5, 9)) ||
+ (isBetween((n % 100), 12, 14)))
+ return 'many';
+ if (n == 1)
+ return 'one';
+ return 'other';
+ },
+ '14': function(n) {
+ if ((isBetween((n % 100), 3, 4)))
+ return 'few';
+ if ((n % 100) == 2)
+ return 'two';
+ if ((n % 100) == 1)
+ return 'one';
+ return 'other';
+ },
+ '15': function(n) {
+ if (n === 0 || (isBetween((n % 100), 2, 10)))
+ return 'few';
+ if ((isBetween((n % 100), 11, 19)))
+ return 'many';
+ if (n == 1)
+ return 'one';
+ return 'other';
+ },
+ '16': function(n) {
+ if ((n % 10) == 1 && n != 11)
+ return 'one';
+ return 'other';
+ },
+ '17': function(n) {
+ if (n == 3)
+ return 'few';
+ if (n === 0)
+ return 'zero';
+ if (n == 6)
+ return 'many';
+ if (n == 2)
+ return 'two';
+ if (n == 1)
+ return 'one';
+ return 'other';
+ },
+ '18': function(n) {
+ if (n === 0)
+ return 'zero';
+ if ((isBetween(n, 0, 2)) && n !== 0 && n != 2)
+ return 'one';
+ return 'other';
+ },
+ '19': function(n) {
+ if ((isBetween(n, 2, 10)))
+ return 'few';
+ if ((isBetween(n, 0, 1)))
+ return 'one';
+ return 'other';
+ },
+ '20': function(n) {
+ if ((isBetween((n % 10), 3, 4) || ((n % 10) == 9)) && !(
+ isBetween((n % 100), 10, 19) ||
+ isBetween((n % 100), 70, 79) ||
+ isBetween((n % 100), 90, 99)
+ ))
+ return 'few';
+ if ((n % 1000000) === 0 && n !== 0)
+ return 'many';
+ if ((n % 10) == 2 && !isIn((n % 100), [12, 72, 92]))
+ return 'two';
+ if ((n % 10) == 1 && !isIn((n % 100), [11, 71, 91]))
+ return 'one';
+ return 'other';
+ },
+ '21': function(n) {
+ if (n === 0)
+ return 'zero';
+ if (n == 1)
+ return 'one';
+ return 'other';
+ },
+ '22': function(n) {
+ if ((isBetween(n, 0, 1)) || (isBetween(n, 11, 99)))
+ return 'one';
+ return 'other';
+ },
+ '23': function(n) {
+ if ((isBetween((n % 10), 1, 2)) || (n % 20) === 0)
+ return 'one';
+ return 'other';
+ },
+ '24': function(n) {
+ if ((isBetween(n, 3, 10) || isBetween(n, 13, 19)))
+ return 'few';
+ if (isIn(n, [2, 12]))
+ return 'two';
+ if (isIn(n, [1, 11]))
+ return 'one';
+ return 'other';
+ }
+ };
+
+ // return a function that gives the plural form name for a given integer
+ var index = locales2rules[lang.replace(/-.*$/, '')];
+ if (!(index in pluralRules)) {
+ consoleWarn('plural form unknown for [' + lang + ']');
+ return function() { return 'other'; };
+ }
+ return pluralRules[index];
+ }
+
+ // pre-defined 'plural' macro
+ gMacros.plural = function(str, param, key, prop) {
+ var n = parseFloat(param);
+ if (isNaN(n))
+ return str;
+
+ // TODO: support other properties (l20n still doesn't...)
+ if (prop != gTextProp)
+ return str;
+
+ // initialize _pluralRules
+ if (!gMacros._pluralRules) {
+ gMacros._pluralRules = getPluralRules(gLanguage);
+ }
+ var index = '[' + gMacros._pluralRules(n) + ']';
+
+ // try to find a [zero|one|two] key if it's defined
+ if (n === 0 && (key + '[zero]') in gL10nData) {
+ str = gL10nData[key + '[zero]'][prop];
+ } else if (n == 1 && (key + '[one]') in gL10nData) {
+ str = gL10nData[key + '[one]'][prop];
+ } else if (n == 2 && (key + '[two]') in gL10nData) {
+ str = gL10nData[key + '[two]'][prop];
+ } else if ((key + index) in gL10nData) {
+ str = gL10nData[key + index][prop];
+ } else if ((key + '[other]') in gL10nData) {
+ str = gL10nData[key + '[other]'][prop];
+ }
+
+ return str;
+ };
+
+
+ /**
+ * l10n dictionary functions
+ */
+
+ // fetch an l10n object, warn if not found, apply `args' if possible
+ function getL10nData(key, args) {
+ var data = gL10nData[key];
+ if (!data) {
+ consoleWarn('#' + key + ' is undefined.');
+ }
+
+ /** This is where l10n expressions should be processed.
+ * The plan is to support C-style expressions from the l20n project;
+ * until then, only two kinds of simple expressions are supported:
+ * {[ index ]} and {{ arguments }}.
+ */
+ var rv = {};
+ for (var prop in data) {
+ var str = data[prop];
+ str = substIndexes(str, args, key, prop);
+ str = substArguments(str, args, key);
+ rv[prop] = str;
+ }
+ return rv;
+ }
+
+ // replace {[macros]} with their values
+ function substIndexes(str, args, key, prop) {
+ var reIndex = /\{\[\s*([a-zA-Z]+)\(([a-zA-Z]+)\)\s*\]\}/;
+ var reMatch = reIndex.exec(str);
+ if (!reMatch || !reMatch.length)
+ return str;
+
+ // an index/macro has been found
+ // Note: at the moment, only one parameter is supported
+ var macroName = reMatch[1];
+ var paramName = reMatch[2];
+ var param;
+ if (args && paramName in args) {
+ param = args[paramName];
+ } else if (paramName in gL10nData) {
+ param = gL10nData[paramName];
+ }
+
+ // there's no macro parser yet: it has to be defined in gMacros
+ if (macroName in gMacros) {
+ var macro = gMacros[macroName];
+ str = macro(str, param, key, prop);
+ }
+ return str;
+ }
+
+ // replace {{arguments}} with their values
+ function substArguments(str, args, key) {
+ var reArgs = /\{\{\s*(.+?)\s*\}\}/;
+ var match = reArgs.exec(str);
+ while (match) {
+ if (!match || match.length < 2)
+ return str; // argument key not found
+
+ var arg = match[1];
+ var sub = '';
+ if (args && arg in args) {
+ sub = args[arg];
+ } else if (arg in gL10nData) {
+ sub = gL10nData[arg][gTextProp];
+ } else {
+ consoleLog('argument {{' + arg + '}} for #' + key + ' is undefined.');
+ return str;
+ }
+
+ str = str.substring(0, match.index) + sub +
+ str.substr(match.index + match[0].length);
+ match = reArgs.exec(str);
+ }
+ return str;
+ }
+
+ // translate an HTML element
+ function translateElement(element) {
+ var l10n = getL10nAttributes(element);
+ if (!l10n.id) {
+ return;
+ }
+
+ // get the related l10n object
+ var data = getL10nData(l10n.id, l10n.args);
+ if (!data) {
+ consoleWarn('#' + l10n.id + ' is undefined.');
+ return;
+ }
+
+ // translate element (TODO: security checks?)
+ if (data[gTextProp]) { // XXX
+ if (element.children.length === 0) {
+ element[gTextProp] = data[gTextProp];
+ } else {
+ // this element has element children: replace the content of the first
+ // (non-empty) child textNode and clear other child textNodes
+ var children = element.childNodes;
+ var found = false;
+ for (var i = 0, l = children.length; i < l; i++) {
+ if (children[i].nodeType === 3 && /\S/.test(children[i].nodeValue)) {
+ if (found) {
+ children[i].nodeValue = '';
+ } else {
+ children[i].nodeValue = data[gTextProp];
+ found = true;
+ }
+ }
+ }
+ // if no (non-empty) textNode is found, insert a textNode before the
+ // first element child.
+ if (!found) {
+ var textNode = document.createTextNode(data[gTextProp]);
+ element.insertBefore(textNode, element.firstChild);
+ }
+ }
+ delete data[gTextProp];
+ }
+
+ for (var k in data) {
+ element[k] = data[k];
+ }
+ }
+
+ // translate an HTML subtree
+ function translateFragment(element) {
+ element = element || document.documentElement;
+
+ // check all translatable children (= w/ a `data-l10n-id' attribute)
+ var children = getTranslatableChildren(element);
+ var elementCount = children.length;
+ for (var i = 0; i < elementCount; i++) {
+ translateElement(children[i]);
+ }
+
+ // translate element itself if necessary
+ translateElement(element);
+ }
+
+
+ /**
+ * Startup & Public API
+ *
+ * This section is quite specific to the B2G project: old browsers are not
+ * supported and the API is slightly different from the standard webl10n one.
+ */
+
+ // load the default locale on startup
+ function l10nStartup() {
+ gReadyState = 'interactive';
+ consoleLog('loading [' + navigator.language + '] resources, ' +
+ (gAsyncResourceLoading ? 'asynchronously.' : 'synchronously.'));
+
+ // load the default locale and translate the document if required
+ if (document.documentElement.lang === navigator.language) {
+ loadLocale(navigator.language);
+ } else {
+ loadLocale(navigator.language, translateFragment);
+ }
+ }
+
+ // the B2G build system doesn't expose any `document'...
+ if (typeof(document) !== 'undefined') {
+ if (document.readyState === 'complete' ||
+ document.readyState === 'interactive') {
+ window.setTimeout(l10nStartup);
+ } else {
+ document.addEventListener('DOMContentLoaded', l10nStartup);
+ }
+ }
+
+ // load the appropriate locale if the language setting has changed
+ if ('mozSettings' in navigator && navigator.mozSettings) {
+ navigator.mozSettings.addObserver('language.current', function(event) {
+ loadLocale(event.settingValue, translateFragment);
+ });
+ }
+
+ // public API
+ navigator.mozL10n = {
+ // get a localized string
+ get: function l10n_get(key, args, fallback) {
+ var data = getL10nData(key, args) || fallback;
+ if (data) {
+ return 'textContent' in data ? data.textContent : '';
+ }
+ return '{{' + key + '}}';
+ },
+
+ // get|set the document language and direction
+ get language() {
+ return {
+ // get|set the document language (ISO-639-1)
+ get code() { return gLanguage; },
+ set code(lang) { loadLocale(lang, translateFragment); },
+
+ // get the direction (ltr|rtl) of the current language
+ get direction() {
+ // http://www.w3.org/International/questions/qa-scripts
+ // Arabic, Hebrew, Farsi, Pashto, Urdu
+ var rtlList = ['ar', 'he', 'fa', 'ps', 'ur'];
+ return (rtlList.indexOf(gLanguage) >= 0) ? 'rtl' : 'ltr';
+ }
+ };
+ },
+
+ // translate an element or document fragment
+ translate: translateFragment,
+
+ // get (a clone of) the dictionary for the current locale
+ get dictionary() { return JSON.parse(JSON.stringify(gL10nData)); },
+
+ // this can be used to prevent race conditions
+ get readyState() { return gReadyState; },
+ ready: function l10n_ready(callback) {
+ if (!callback) {
+ return;
+ } else if (gReadyState == 'complete' || gReadyState == 'interactive') {
+ window.setTimeout(callback);
+ } else {
+ window.addEventListener('localized', callback);
+ }
+ }
+ };
+
+ consoleLog('library loaded.');
+})(this);
+
diff --git a/shared/js/l10n_date.js b/shared/js/l10n_date.js
new file mode 100644
index 0000000..eb461a3
--- /dev/null
+++ b/shared/js/l10n_date.js
@@ -0,0 +1,141 @@
+/* -*- Mode: js; js-indent-level: 2; indent-tabs-mode: nil -*- */
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+/**
+ * This lib relies on `l10n.js' to implement localizable date/time strings.
+ *
+ * The proposed `DateTimeFormat' object should provide all the features that are
+ * planned for the `Intl.DateTimeFormat' constructor, but the API does not match
+ * exactly the ES-i18n draft.
+ * - https://bugzilla.mozilla.org/show_bug.cgi?id=769872
+ * - http://wiki.ecmascript.org/doku.php?id=globalization:specification_drafts
+ *
+ * Besides, this `DateTimeFormat' object provides two features that aren't
+ * planned in the ES-i18n spec:
+ * - a `toLocaleFormat()' that really works (i.e. fully translated);
+ * - a `fromNow()' method to handle relative dates ("pretty dates").
+ *
+ * WARNING: this library relies on the non-standard `toLocaleFormat()' method,
+ * which is specific to Firefox -- no other browser is supported.
+ */
+
+navigator.mozL10n.DateTimeFormat = function(locales, options) {
+ var _ = navigator.mozL10n.get;
+
+ // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/toLocaleFormat
+ function localeFormat(d, format) {
+ var tokens = format.match(/(%E.|%O.|%.)/g);
+
+ for (var i = 0; tokens && i < tokens.length; i++) {
+ var value = '';
+
+ // http://pubs.opengroup.org/onlinepubs/007908799/xsh/strftime.html
+ switch (tokens[i]) {
+ // localized day/month names
+ case '%a':
+ value = _('weekday-' + d.getDay() + '-short');
+ break;
+ case '%A':
+ value = _('weekday-' + d.getDay() + '-long');
+ break;
+ case '%b':
+ case '%h':
+ value = _('month-' + d.getMonth() + '-short');
+ break;
+ case '%B':
+ value = _('month-' + d.getMonth() + '-long');
+ break;
+
+ // like %H, but in 12-hour format and without any leading zero
+ case '%I':
+ value = d.getHours() % 12 || 12;
+ break;
+
+ // like %d, without any leading zero
+ case '%e':
+ value = d.getDate();
+ break;
+
+ // localized date/time strings
+ case '%c':
+ case '%x':
+ case '%X':
+ // ensure the localized format string doesn't contain any %c|%x|%X
+ var tmp = _('dateTimeFormat_' + tokens[i]);
+ if (tmp && !(/(%c|%x|%X)/).test(tmp)) {
+ value = localeFormat(d, tmp);
+ }
+ break;
+
+ // other tokens don't require any localization
+ }
+
+ format = format.replace(tokens[i], value || d.toLocaleFormat(tokens[i]));
+ }
+
+ return format;
+ }
+
+ // variant of John Resig's PrettyDate.js
+ function prettyDate(time, useCompactFormat) {
+ switch (time.constructor) {
+ case String: // timestamp
+ time = parseInt(time);
+ break;
+ case Date:
+ time = time.getTime();
+ break;
+ }
+
+ var secDiff = (Date.now() - time) / 1000;
+ if (isNaN(secDiff)) {
+ return _('incorrectDate');
+ }
+
+ var f = useCompactFormat ? '-short' : '-long';
+
+ if (secDiff >= 0) { // past
+ var dayDiff = Math.floor(secDiff / 86400);
+ if (secDiff < 3600) {
+ return _('minutesAgo' + f, { m: Math.floor(secDiff / 60) });
+ } else if (dayDiff === 0) {
+ return _('hoursAgo' + f, { h: Math.floor(secDiff / 3600) });
+ } else if (dayDiff < 10) {
+ return _('daysAgo' + f, { d: dayDiff });
+ }
+ }
+
+ if (secDiff < 0) { // future
+ secDiff = -secDiff;
+ dayDiff = Math.floor(secDiff / 86400);
+ if (secDiff < 3600) {
+ return _('inMinutes' + f, { m: Math.floor(secDiff / 60) });
+ } else if (dayDiff === 0) {
+ return _('inHours' + f, { h: Math.floor(secDiff / 3600) });
+ } else if (dayDiff < 10) {
+ return _('inDays' + f, { d: dayDiff });
+ }
+ }
+
+ // too far: return an absolute date
+ return localeFormat(new Date(time), '%x');
+ }
+
+ // API
+ return {
+ localeDateString: function localeDateString(d) {
+ return localeFormat(d, '%x');
+ },
+ localeTimeString: function localeTimeString(d) {
+ return localeFormat(d, '%X');
+ },
+ localeString: function localeString(d) {
+ return localeFormat(d, '%c');
+ },
+ localeFormat: localeFormat,
+ fromNow: prettyDate
+ };
+};
+
diff --git a/shared/js/manifest_helper.js b/shared/js/manifest_helper.js
new file mode 100644
index 0000000..d207603
--- /dev/null
+++ b/shared/js/manifest_helper.js
@@ -0,0 +1,26 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+/**
+ * Helper object to access manifest information with locale support.
+ */
+
+var ManifestHelper = function(manifest) {
+ var localeRoot = manifest;
+ var locales = manifest.locales;
+
+ if (locales) {
+ var lang = document.documentElement.lang;
+
+ // If there is a manifest entry for the curret locale, use it, otherwise
+ // fallback on the default manifest.
+ localeRoot = locales[lang] || locales[lang.split('-')[0]] || manifest;
+ }
+
+ // Bind the localized property values.
+ for (var prop in manifest) {
+ this[prop] = localeRoot[prop] || manifest[prop];
+ }
+};
diff --git a/shared/js/media/README b/shared/js/media/README
new file mode 100644
index 0000000..05a67a0
--- /dev/null
+++ b/shared/js/media/README
@@ -0,0 +1 @@
+This directory contains files shared by the Camera, Gallery and Video apps.
diff --git a/shared/js/media/get_video_rotation.js b/shared/js/media/get_video_rotation.js
new file mode 100644
index 0000000..3359b92
--- /dev/null
+++ b/shared/js/media/get_video_rotation.js
@@ -0,0 +1,143 @@
+'use strict';
+
+//
+// Given an MP4/Quicktime based video file as a blob, read through its
+// atoms to find the track header "tkhd" atom and extract the rotation
+// matrix from it. Convert the matrix value to rotation in degrees and
+// pass that number to the specified callback function. If no value is
+// found but the video file is valid, pass null to the callback. If
+// any errors occur, pass an error message (a string) callback.
+//
+// See also:
+// http://androidxref.com/4.0.4/xref/frameworks/base/media/libstagefright/MPEG4Writer.cpp
+// https://developer.apple.com/library/mac/#documentation/QuickTime/QTFF/QTFFChap2/qtff2.html
+//
+function getVideoRotation(blob, rotationCallback) {
+
+ // A utility for traversing the tree of atoms in an MP4 file
+ function MP4Parser(blob, handlers) {
+ // Start off with a 1024 chunk from the start of the blob.
+ BlobView.get(blob, 0, 1024, function(data, error) {
+ // Make sure that the blob is, in fact, some kind of MP4 file
+ if (data.getASCIIText(4, 4) !== 'ftyp') {
+ handlers.errorHandler('not an MP4 file');
+ return;
+ }
+ parseAtom(data);
+ });
+
+ // Call this with a BlobView object that includes the first 16 bytes of
+ // an atom. It doesn't matter whether the body of the atom is included.
+ function parseAtom(data) {
+ var offset = data.sliceOffset + data.viewOffset; // atom position in blob
+ var size = data.readUnsignedInt(); // atom length
+ var type = data.readASCIIText(4); // atom type
+ var contentOffset = 8; // position of content
+
+ if (size === 0) {
+ // Zero size means the rest of the file
+ size = blob.size - offset;
+ }
+ else if (size === 1) {
+ // A size of 1 means the size is in bytes 8-15
+ size = data.readUnsignedInt() * 4294967296 + data.readUnsignedInt();
+ contentOffset = 16;
+ }
+
+ var handler = handlers[type] || handlers.defaultHandler;
+ if (typeof handler === 'function') {
+ // If the handler is a function, pass that function a
+ // DataView object that contains the entire atom
+ // including size and type. Then use the return value
+ // of the function as instructions on what to do next.
+ data.getMore(data.sliceOffset + data.viewOffset, size, function(atom) {
+ // Pass the entire atom to the handler function
+ var rv = handler(atom);
+
+ // If the return value is 'done', stop parsing.
+ // Otherwise, continue with the next atom.
+ // XXX: For more general parsing we need a way to pop some
+ // stack levels. A return value that is an atom name should mean
+ // pop back up to this atom type and go on to the next atom
+ // after that.
+ if (rv !== 'done') {
+ parseAtomAt(data, offset + size);
+ }
+ });
+ }
+ else if (handler === 'children') {
+ // If the handler is this string, then assume that the atom is
+ // a container atom and do its next child atom next
+ var skip = (type === 'meta') ? 4 : 0; // special case for meta atoms
+ parseAtomAt(data, offset + contentOffset + skip);
+ }
+ else if (handler === 'skip' || !handler) {
+ // Skip the atom entirely and go on to the next one.
+ // If there is no next one, call the eofHandler or just return
+ parseAtomAt(data, offset + size);
+ }
+ else if (handler === 'done') {
+ // Stop parsing
+ return;
+ }
+ }
+
+ function parseAtomAt(data, offset) {
+ if (offset >= blob.size) {
+ if (handlers.eofHandler)
+ handlers.eofHandler();
+ return;
+ }
+ else {
+ data.getMore(offset, 8, parseAtom);
+ }
+ }
+ }
+
+ // We want to loop through the top-level atoms until we find the 'moov'
+ // atom. Then, within this atom, there are one or more 'trak' atoms.
+ // Each 'trak' should begin with a 'tkhd' atom. The tkhd atom has
+ // a transformation matrix at byte 48. The matrix is 9 32 bit integers.
+ // We'll interpret those numbers as a rotation of 0, 90, 180 or 270.
+ // If the video has more than one track, we expect all of them to have
+ // the same rotation, so we'll only look at the first 'trak' atom that
+ // we find.
+ MP4Parser(blob, {
+ errorHandler: function(msg) { rotationCallback(msg); },
+ eofHandler: function() { rotationCallback(null); },
+ defaultHandler: 'skip', // Skip all atoms other than those listed below
+ moov: 'children', // Enumerate children of the moov atom
+ trak: 'children', // Enumerate children of the trak atom
+ tkhd: function(data) { // Pass the tkhd atom to this function
+ // The matrix begins at byte 48
+ data.advance(48);
+
+ var a = data.readUnsignedInt();
+ var b = data.readUnsignedInt();
+ data.advance(4); // we don't care about this number
+ var c = data.readUnsignedInt();
+ var d = data.readUnsignedInt();
+
+ if (a === 0 && d === 0) { // 90 or 270 degrees
+ if (b === 0x00010000 && c === 0xFFFF0000)
+ rotationCallback(90);
+ else if (b === 0xFFFF0000 && c === 0x00010000)
+ rotationCallback(270);
+ else
+ rotationCallback('unexpected rotation matrix');
+ }
+ else if (b === 0 && c === 0) { // 0 or 180 degrees
+ if (a === 0x00010000 && d === 0x00010000)
+ rotationCallback(0);
+ else if (a === 0xFFFF0000 && d === 0xFFFF0000)
+ rotationCallback(180);
+ else
+ rotationCallback('unexpected rotation matrix');
+ }
+ else {
+ rotationCallback('unexpected rotation matrix');
+ }
+ return 'done';
+ }
+ });
+}
diff --git a/shared/js/media/jpeg_metadata_parser.js b/shared/js/media/jpeg_metadata_parser.js
new file mode 100644
index 0000000..d8b2b02
--- /dev/null
+++ b/shared/js/media/jpeg_metadata_parser.js
@@ -0,0 +1,314 @@
+'use strict';
+
+//
+// This file defines a single function that asynchronously reads a
+// JPEG file (or blob) to determine its width and height and find the
+// location and size of the embedded preview image, if it has one. If
+// it succeeds, it passes an object containing this data to the
+// specified callback function. If it fails, it passes an error message
+// to the specified error function instead.
+//
+// This function is capable of parsing and returning EXIF data for a
+// JPEG file, but for speed, it ignores all EXIF data except the embedded
+// preview image.
+//
+// This function requires the BlobView utility class
+//
+function parseJPEGMetadata(file, metadataCallback, metadataError) {
+ // This is the object we'll pass to metadataCallback
+ var metadata = {};
+
+ // Start off reading a 16kb slice of the JPEG file.
+ // Hopefully, this will be all we need and everything else will
+ // be synchronous
+ BlobView.get(file, 0, Math.min(16 * 1024, file.size), function(data) {
+ if (data.byteLength < 2 ||
+ data.getUint8(0) !== 0xFF ||
+ data.getUint8(1) !== 0xD8) {
+ metadataError('Not a JPEG file');
+ return;
+ }
+
+ // Now start reading JPEG segments
+ // getSegment() and segmentHandler() are defined below.
+ getSegment(data, 2, segmentHandler);
+ });
+
+ // Read the JPEG segment at the specified offset and
+ // pass it to the callback function.
+ // Offset is relative to the current data offsets.
+ // We assume that data has enough data in it that we can
+ // can determine the size of the segment, and we guarantee that
+ // we read extra bytes so the next call works
+ function getSegment(data, offset, callback) {
+ try {
+ var header = data.getUint8(offset);
+ if (header !== 0xFF) {
+ metadataError('Malformed JPEG file: bad segment header');
+ return;
+ }
+
+ var type = data.getUint8(offset + 1);
+ var size = data.getUint16(offset + 2) + 2;
+
+ // the absolute position of the segment
+ var start = data.sliceOffset + data.viewOffset + offset;
+ // If this isn't the last segment in the file, add 4 bytes
+ // so we can read the size of the next segment
+ var isLast = (start + size >= file.size);
+ var length = isLast ? size : size + 4;
+
+ data.getMore(start, length,
+ function(data) {
+ callback(type, size, data, isLast);
+ });
+ }
+ catch (e) {
+ metadataError(e.toString() + '\n' + e.stack);
+ }
+ }
+
+ // This is a callback function for getNextSegment that handles the
+ // various types of segments we expect to see in a jpeg file
+ function segmentHandler(type, size, data, isLastSegment) {
+ try {
+ switch (type) {
+ case 0xC0: // Some actual image data, including image dimensions
+ case 0xC1:
+ case 0xC2:
+ case 0xC3:
+ // Get image dimensions
+ metadata.height = data.getUint16(5);
+ metadata.width = data.getUint16(7);
+
+ // We're done. All the EXIF data will come before this segment
+ // So call the callback
+ metadataCallback(metadata);
+ break;
+
+ case 0xE1: // APP1 segment. Probably holds EXIF metadata
+ parseAPP1(data);
+ /* fallthrough */
+
+ default:
+ // A segment we don't care about, so just go on and read the next one
+ if (isLastSegment) {
+ metadataError('unexpected end of JPEG file');
+ return;
+ }
+ getSegment(data, size, segmentHandler);
+ }
+ }
+ catch (e) {
+ metadataError(e.toString() + '\n' + e.stack);
+ }
+ }
+
+ function parseAPP1(data) {
+ if (data.getUint32(4, false) === 0x45786966) { // "Exif"
+ var exif = parseEXIFData(data);
+
+ if (exif.THUMBNAIL && exif.THUMBNAILLENGTH) {
+ var start = data.sliceOffset + data.viewOffset + 10 + exif.THUMBNAIL;
+ metadata.preview = {
+ start: start,
+ end: start + exif.THUMBNAILLENGTH
+ };
+ }
+ }
+ }
+
+ // Parse an EXIF segment from a JPEG file and return an object
+ // of metadata attributes. The argument must be a DataView object
+ function parseEXIFData(data) {
+ var exif = {};
+
+ var byteorder = data.getUint8(10);
+ if (byteorder === 0x4D) { // big endian
+ byteorder = false;
+ } else if (byteorder === 0x49) { // little endian
+ byteorder = true;
+ } else {
+ throw Error('invalid byteorder in EXIF segment');
+ }
+
+ if (data.getUint16(12, byteorder) !== 42) { // magic number
+ throw Error('bad magic number in EXIF segment');
+ }
+
+ var offset = data.getUint32(14, byteorder);
+
+ /*
+ * This is how we would parse all EXIF metadata more generally.
+ * I'm leaving this code in as a comment in case we need other EXIF
+ * data in the future.
+ *
+ parseIFD(data, offset + 10, byteorder, exif);
+
+ if (exif.EXIFIFD) {
+ parseIFD(data, exif.EXIFIFD + 10, byteorder, exif);
+ delete exif.EXIFIFD;
+ }
+
+ if (exif.GPSIFD) {
+ parseIFD(data, exif.GPSIFD + 10, byteorder, exif);
+ delete exif.GPSIFD;
+ }
+ */
+
+ // Instead of a general purpose EXIF parse, we're going to drill
+ // down directly to the thumbnail image.
+ // We're in IFD0 here. We want the offset of IFD1
+ var ifd0entries = data.getUint16(offset + 10, byteorder);
+ var ifd1 = data.getUint32(offset + 12 + 12 * ifd0entries, byteorder);
+ // If there is an offset for IFD1, parse that
+ if (ifd1 !== 0)
+ parseIFD(data, ifd1 + 10, byteorder, exif, true);
+
+ return exif;
+ }
+
+ function parseIFD(data, offset, byteorder, exif, onlyParseOne) {
+ var numentries = data.getUint16(offset, byteorder);
+ for (var i = 0; i < numentries; i++) {
+ parseEntry(data, offset + 2 + 12 * i, byteorder, exif);
+ }
+
+ if (onlyParseOne)
+ return;
+
+ var next = data.getUint32(offset + 2 + 12 * numentries, byteorder);
+ if (next !== 0 && next < file.size) {
+ parseIFD(data, next + 10, byteorder, exif);
+ }
+ }
+
+ // size, in bytes, of each TIFF data type
+ var typesize = [
+ 0, // Unused
+ 1, // BYTE
+ 1, // ASCII
+ 2, // SHORT
+ 4, // LONG
+ 8, // RATIONAL
+ 1, // SBYTE
+ 1, // UNDEFINED
+ 2, // SSHORT
+ 4, // SLONG
+ 8, // SRATIONAL
+ 4, // FLOAT
+ 8 // DOUBLE
+ ];
+
+ // This object maps EXIF tag numbers to their names.
+ // Only list the ones we want to bother parsing and returning.
+ // All others will be ignored.
+ var tagnames = {
+ /*
+ * We don't currently use any of these EXIF tags for anything.
+ *
+ *
+ '256': 'ImageWidth',
+ '257': 'ImageHeight',
+ '40962': 'PixelXDimension',
+ '40963': 'PixelYDimension',
+ '306': 'DateTime',
+ '315': 'Artist',
+ '33432': 'Copyright',
+ '36867': 'DateTimeOriginal',
+ '33434': 'ExposureTime',
+ '33437': 'FNumber',
+ '34850': 'ExposureProgram',
+ '34867': 'ISOSpeed',
+ '37377': 'ShutterSpeedValue',
+ '37378': 'ApertureValue',
+ '37379': 'BrightnessValue',
+ '37380': 'ExposureBiasValue',
+ '37382': 'SubjectDistance',
+ '37383': 'MeteringMode',
+ '37384': 'LightSource',
+ '37385': 'Flash',
+ '37386': 'FocalLength',
+ '41986': 'ExposureMode',
+ '41987': 'WhiteBalance',
+ '41991': 'GainControl',
+ '41992': 'Contrast',
+ '41993': 'Saturation',
+ '41994': 'Sharpness',
+ // These are special tags that we handle internally
+ '34665': 'EXIFIFD', // Offset of EXIF data
+ '34853': 'GPSIFD', // Offset of GPS data
+ */
+ '513': 'THUMBNAIL', // Offset of thumbnail
+ '514': 'THUMBNAILLENGTH' // Length of thumbnail
+ };
+
+ function parseEntry(data, offset, byteorder, exif) {
+ var tag = data.getUint16(offset, byteorder);
+ var tagname = tagnames[tag];
+
+ if (!tagname) // If we don't know about this tag type, skip it
+ return;
+
+ var type = data.getUint16(offset + 2, byteorder);
+ var count = data.getUint32(offset + 4, byteorder);
+
+ var total = count * typesize[type];
+ var valueOffset = total <= 4 ? offset + 8 :
+ data.getUint32(offset + 8, byteorder);
+ exif[tagname] = parseValue(data, valueOffset, type, count, byteorder);
+ }
+
+ function parseValue(data, offset, type, count, byteorder) {
+ if (type === 2) { // ASCII string
+ var codes = [];
+ for (var i = 0; i < count - 1; i++) {
+ codes[i] = data.getUint8(offset + i);
+ }
+ return String.fromCharCode.apply(String, codes);
+ } else {
+ if (count == 1) {
+ return parseOneValue(data, offset, type, byteorder);
+ } else {
+ var values = [];
+ var size = typesize[type];
+ for (var i = 0; i < count; i++) {
+ values[i] = parseOneValue(data, offset + size * i, type, byteorder);
+ }
+ return values;
+ }
+ }
+ }
+
+ function parseOneValue(data, offset, type, byteorder) {
+ switch (type) {
+ case 1: // BYTE
+ case 7: // UNDEFINED
+ return data.getUint8(offset);
+ case 2: // ASCII
+ // This case is handed in parseValue
+ return null;
+ case 3: // SHORT
+ return data.getUint16(offset, byteorder);
+ case 4: // LONG
+ return data.getUint32(offset, byteorder);
+ case 5: // RATIONAL
+ return data.getUint32(offset, byteorder) /
+ data.getUint32(offset + 4, byteorder);
+ case 6: // SBYTE
+ return data.getInt8(offset);
+ case 8: // SSHORT
+ return data.getInt16(offset, byteorder);
+ case 9: // SLONG
+ return data.getInt32(offset, byteorder);
+ case 10: // SRATIONAL
+ return data.getInt32(offset, byteorder) /
+ data.getInt32(offset + 4, byteorder);
+ case 11: // FLOAT
+ return data.getFloat32(offset, byteorder);
+ case 12: // DOUBLE
+ return data.getFloat64(offset, byteorder);
+ }
+ return null;
+ }
+}
diff --git a/shared/js/media/media_frame.js b/shared/js/media/media_frame.js
new file mode 100644
index 0000000..aaf8fbe
--- /dev/null
+++ b/shared/js/media/media_frame.js
@@ -0,0 +1,537 @@
+/*
+ * media_frame.js:
+ *
+ * A MediaFrame displays a photo or a video. The gallery app uses
+ * three side by side to support smooth panning from one item to the
+ * next. The Camera app uses one for image and video preview. The
+ * Gallery app's open activity uses one of these to display the opened
+ * item.
+ *
+ * MediaFrames have different behavior depending on whether they display
+ * images or videos. Photo frames allow the user to zoom and pan on the photo.
+ * Video frames allow the user to play and pause but don't allow zooming.
+ *
+ * When a frame is displaying a video, it handles mouse events.
+ * When display a picture, however, it expects the client to handle events
+ * and call the pan() and zoom() methods.
+ *
+ * The pan() method is a little unusual. It "uses" as much of the pan
+ * event as it can, and returns a number indicating how much of the
+ * horizontal motion it did not use. The gallery uses this returned
+ * value for transitioning between frames. If a frame displays a
+ * photo that is not zoomed in at all, then it can't use any of the
+ * pan, and returns the full amount which the gallery app turns into a
+ * panning motion between frames. But if the photo is zoomed in, then
+ * the MediaFrame will just move the photo within itself, if it can, and
+ * return 0.
+ *
+ * Much of the code in this file used to be part of the PhotoState class.
+ */
+function MediaFrame(container, includeVideo) {
+ if (typeof container === 'string')
+ container = document.getElementById(container);
+ this.container = container;
+ this.image = document.createElement('img');
+ this.container.appendChild(this.image);
+ this.image.style.display = 'none';
+ if (includeVideo !== false) {
+ this.video = new VideoPlayer(container);
+ this.video.hide();
+ }
+ this.displayingVideo = false;
+ this.displayingImage = false;
+ this.blob = null;
+ this.url = null;
+}
+
+MediaFrame.prototype.displayImage = function displayImage(blob, width, height,
+ preview)
+{
+ this.clear(); // Reset everything
+
+ // Remember what we're displaying
+ this.blob = blob;
+ this.fullsizeWidth = width;
+ this.fullsizeHeight = height;
+ this.preview = preview;
+
+ // Keep track of what kind of content we have
+ this.displayingImage = true;
+
+ // Make the image element visible
+ this.image.style.display = 'block';
+
+ // If the preview is at least as big as the screen, display that.
+ // Otherwise, display the full-size image.
+ if (preview &&
+ (preview.width >= window.innerWidth ||
+ preview.height >= window.innerHeight)) {
+ this.displayingPreview = true;
+ this._displayImage(blob.slice(preview.start, preview.end, 'image/jpeg'),
+ preview.width, preview.height);
+ }
+ else {
+ this._displayImage(blob, width, height);
+ }
+};
+
+// A utility function we use to display the full-size image or the
+// preview The last two arguments are optimizations used by
+// switchToFullSizeImage() to make the transition from preview to
+// fullscreen smooth. If waitForPaint is true, then this function will
+// keep the old image on the screen until the new image is painted
+// over it so we (hopefully) don't end up with a blank screen or
+// flash. And if callback is specified, it will call the callback
+// when thew new images is visible on the screen. If either of those
+// arguments are specified, the width and height must be specified.
+MediaFrame.prototype._displayImage = function _displayImage(blob, width, height,
+ waitForPaint,
+ callback)
+{
+ var self = this;
+ var oldImage;
+
+ // Create a URL for the blob (or preview blob)
+ if (this.url)
+ URL.revokeObjectURL(this.url);
+ this.url = URL.createObjectURL(blob);
+
+ // If we don't know the width or the height yet, then set up an event
+ // handler to set the image size and position once it is loaded.
+ // This happens for the open activity.
+ if (!width || !height) {
+ this.image.src = this.url;
+ this.image.addEventListener('load', function onload() {
+ this.removeEventListener('load', onload);
+ self.itemWidth = this.naturalWidth;
+ self.itemHeight = this.naturalHeight;
+ self.computeFit();
+ self.setPosition();
+ });
+ return;
+ }
+
+ // Otherwise, we have a width and height, and we may also have to handle
+ // the waitForPaint and callback arguments
+
+ // If waitForPaint is set, then keep the old image around and displayed
+ // until the new image is loaded.
+ if (waitForPaint) {
+ // Remember the old image
+ oldImage = this.image;
+
+ // Create a new element to load the new image into.
+ // Insert it into the frame, but don't remove the old image yet
+ this.image = document.createElement('img');
+ this.container.appendChild(this.image);
+
+ // Change the old image slightly to give the user some immediate
+ // feedback that something is happening
+ oldImage.classList.add('swapping');
+ }
+
+ // Start loading the new image
+ this.image.src = this.url;
+ // Update image size and position
+ this.itemWidth = width;
+ this.itemHeight = height;
+ this.computeFit();
+ this.setPosition();
+
+ // If waitForPaint is set, or if there is a callback, then we need to
+ // run some code when the new image has loaded and been painted.
+ if (waitForPaint || callback) {
+ whenLoadedAndVisible(this.image, 1000, function() {
+ if (waitForPaint) {
+ // Remove the old image now that the new one is visible
+ self.container.removeChild(oldImage);
+ oldImage.src = null;
+ }
+
+ if (callback) {
+ // Let the caller know that the new image is ready, but
+ // wait for an animation frame before doing it. The point of
+ // using mozRequestAnimationFrame here is that it gives the
+ // removeChild() call above a chance to take effect.
+ mozRequestAnimationFrame(function() {
+ callback();
+ });
+ }
+ });
+ }
+
+ // Wait until the load event on the image fires, and then wait for a
+ // MozAfterPaint event after that, and then, finally, invoke the
+ // callback. Don't wait more than the timeout, though: we need to
+ // ensure that we always call the callback even if the image does not
+ // load or if we don't get a MozAfterPaint event.
+ function whenLoadedAndVisible(image, timeout, callback) {
+ var called = false;
+ var timer = setTimeout(function()
+ {
+ called = true;
+ callback();
+ },
+ timeout || 1000);
+
+ image.addEventListener('load', function onload() {
+ image.removeEventListener('load', onload);
+ window.addEventListener('MozAfterPaint', function onpaint() {
+ window.removeEventListener('MozAfterPaint', onpaint);
+ clearTimeout(timer);
+ if (!called) {
+ callback();
+ }
+ });
+ });
+ }
+};
+
+MediaFrame.prototype._switchToFullSizeImage = function _switchToFull(cb) {
+ if (this.displayingImage && this.displayingPreview) {
+ this.displayingPreview = false;
+ this._displayImage(this.blob, this.fullsizeWidth, this.fullsizeHeight,
+ true, cb);
+ }
+};
+
+MediaFrame.prototype._switchToPreviewImage = function _switchToPreview() {
+ if (this.displayingImage && !this.displayingPreview) {
+ this.displayingPreview = true;
+ this._displayImage(this.blob.slice(this.preview.start,
+ this.preview.end,
+ 'image/jpeg'),
+ this.preview.width,
+ this.preview.height);
+ }
+};
+
+MediaFrame.prototype.displayVideo = function displayVideo(blob, width, height,
+ rotation)
+{
+ if (!this.video)
+ return;
+
+ this.clear(); // reset everything
+
+ // Keep track of what kind of content we have
+ this.displayingVideo = true;
+
+ // Show the video player and hide the image
+ this.video.show();
+
+ // Remember the blob
+ this.blob = blob;
+
+ // Get a new URL for this blob
+ this.url = URL.createObjectURL(blob);
+
+ // Display it in the video element.
+ // The VideoPlayer class takes care of positioning itself, so we
+ // don't have to do anything here with computeFit() or setPosition()
+ this.video.load(this.url, rotation || 0);
+};
+
+// Reset the frame state, release any urls and and hide everything
+MediaFrame.prototype.clear = function clear() {
+ // Reset the saved state
+ this.displayingImage = false;
+ this.displayingPreview = false;
+ this.displayingVideo = false;
+ this.itemWidth = this.itemHeight = null;
+ this.blob = null;
+ this.fullsizeWidth = this.fullsizeHeight = null;
+ this.preview = null;
+ this.fit = null;
+ if (this.url) {
+ URL.revokeObjectURL(this.url);
+ this.url = null;
+ }
+
+ // Hide the image
+ this.image.style.display = 'none';
+ this.image.src = null; // XXX: use about:blank or '' here?
+
+ // Hide the video player
+ if (this.video) {
+ this.video.hide();
+
+ // If the video player has its src set, clear it and release resources
+ // We do this in a roundabout way to avoid getting a warning in the console
+ if (this.video.player.src) {
+ this.video.player.removeAttribute('src');
+ this.video.player.load();
+ }
+ }
+};
+
+// Set the item's position based on this.fit
+// The VideoPlayer object fits itself to its container, and it
+// can't be zoomed or panned, so we only need to do this for images
+MediaFrame.prototype.setPosition = function setPosition() {
+ if (!this.fit || !this.displayingImage)
+ return;
+
+ this.image.style.transform =
+ 'translate(' + this.fit.left + 'px,' + this.fit.top + 'px) ' +
+ 'scale(' + this.fit.scale + ')';
+};
+
+MediaFrame.prototype.computeFit = function computeFit() {
+ if (!this.displayingImage)
+ return;
+ this.viewportWidth = this.container.offsetWidth;
+ this.viewportHeight = this.container.offsetHeight;
+
+ var scalex = this.viewportWidth / this.itemWidth;
+ var scaley = this.viewportHeight / this.itemHeight;
+ var scale = Math.min(Math.min(scalex, scaley), 1);
+
+ // Set the image size and position
+ var width = Math.floor(this.itemWidth * scale);
+ var height = Math.floor(this.itemHeight * scale);
+
+ this.fit = {
+ width: width,
+ height: height,
+ left: Math.floor((this.viewportWidth - width) / 2),
+ top: Math.floor((this.viewportHeight - height) / 2),
+ scale: scale,
+ baseScale: scale
+ };
+};
+
+MediaFrame.prototype.reset = function reset() {
+ // If we're not displaying the preview image, but we have one,
+ // and it is the right size, then switch to it
+ if (this.displayingImage && !this.displayingPreview && this.preview &&
+ (this.preview.width >= window.innerWidth ||
+ this.preview.height >= window.innerHeight)) {
+ this._switchToPreviewImage(); // resets image size and position
+ return;
+ }
+
+ // Otherwise, if we are displaying the preview image but it is no
+ // longer big enough for the screen (such as after a resize event)
+ // then switch to full size. This case should be rare.
+ if (this.displayingImage && this.displayingPreview &&
+ this.preview.width < window.innerWidth &&
+ this.preview.height < window.innerHeight) {
+ this._switchToFullSizeImage(); // resets image size and position
+ return;
+ }
+
+ // Otherwise, just resize and position the item we're already displaying
+ this.computeFit();
+ this.setPosition();
+};
+
+// We call this from the resize handler when the user rotates the
+// screen or when going into or out of fullscreen mode. If the user
+// has not zoomed in, then we just fit the image to the new size (same
+// as reset). But if the user has zoomed in (and we need to stay
+// zoomed for the new size) then we adjust the fit properties so that
+// the pixel that was at the center of the screen before remains at
+// the center now, or as close as possible
+MediaFrame.prototype.resize = function resize() {
+ var oldWidth = this.viewportWidth;
+ var oldHeight = this.viewportHeight;
+ var newWidth = this.container.offsetWidth;
+ var newHeight = this.container.offsetHeight;
+
+ var oldfit = this.fit; // The current image fit
+
+ // If this is triggered by a resize event before the frame has computed
+ // its size, then there is nothing we can do yet.
+ if (!oldfit)
+ return;
+
+ // Compute the new fit.
+ // This updates the the viewportWidth, viewportHeight and fit properties
+ this.computeFit();
+
+ // This is how the image would fit at the new screen size
+ var newfit = this.fit;
+
+ // If no zooming has been done, then a resize is just a reset.
+ // The same is true if the new fit base scale is greater than the
+ // old scale.
+ if (oldfit.scale === oldfit.baseScale || newfit.baseScale > oldfit.scale) {
+ this.reset();
+ return;
+ }
+
+ // Otherwise, just adjust the old fit as needed and use that so we
+ // retain the zoom factor.
+ oldfit.left += (newWidth - oldWidth) / 2;
+ oldfit.top += (newHeight - oldHeight) / 2;
+ oldfit.baseScale = newfit.baseScale;
+ this.fit = oldfit;
+
+ // Reposition this image without resetting the zoom
+ this.setPosition();
+};
+
+// Zoom in by the specified factor, adjusting the pan amount so that
+// the image pixels at (centerX, centerY) remain at that position.
+// Assume that zoom gestures can't be done in the middle of swipes, so
+// if we're calling zoom, then the swipe property will be 0.
+// If time is specified and non-zero, then we set a CSS transition
+// to animate the zoom.
+MediaFrame.prototype.zoom = function zoom(scale, centerX, centerY, time) {
+ // Ignore zooms if we're not displaying an image
+ if (!this.displayingImage)
+ return;
+
+ // If we were displaying the preview, switch to the full-size image
+ if (this.displayingPreview) {
+ // If we want to to animate the zoom, then switch images, wait
+ // for the new one to load, and call this function again to process
+ // the zoom and animation. But if we're not animating, then just
+ // switch images and continue.
+ if (time) { // if animating
+ var self = this;
+ this._switchToFullSizeImage(function() {
+ self.zoom(scale, centerX, centerY, time);
+ });
+ return;
+ }
+ else {
+ this.switching = true;
+ var self = this;
+ this._switchToFullSizeImage(function() { self.switching = false; });
+ }
+ }
+
+ // Never zoom in farther than the native resolution of the image
+ if (this.fit.scale * scale > 1) {
+ scale = 1 / (this.fit.scale);
+ }
+ // And never zoom out to make the image smaller than it would normally be
+ else if (this.fit.scale * scale < this.fit.baseScale) {
+ scale = this.fit.baseScale / this.fit.scale;
+ }
+
+ this.fit.scale = this.fit.scale * scale;
+
+ // Change the size of the photo
+ this.fit.width = Math.floor(this.itemWidth * this.fit.scale);
+ this.fit.height = Math.floor(this.itemHeight * this.fit.scale);
+
+ // centerX and centerY are in viewport coordinates.
+ // These are the photo coordinates displayed at that point in the viewport
+ var photoX = centerX - this.fit.left;
+ var photoY = centerY - this.fit.top;
+
+ // After zooming, these are the new photo coordinates.
+ // Note we just use the relative scale amount here, not this.fit.scale
+ var photoX = Math.floor(photoX * scale);
+ var photoY = Math.floor(photoY * scale);
+
+ // To keep that point still, here are the new left and top values we need
+ this.fit.left = centerX - photoX;
+ this.fit.top = centerY - photoY;
+
+ // Now make sure we didn't pan too much: If the image fits on the
+ // screen, center it. If the image is bigger than the screen, then
+ // make sure we haven't gone past any edges
+ if (this.fit.width <= this.viewportWidth) {
+ this.fit.left = (this.viewportWidth - this.fit.width) / 2;
+ }
+ else {
+ // Don't let the left of the photo be past the left edge of the screen
+ if (this.fit.left > 0)
+ this.fit.left = 0;
+
+ // Right of photo shouldn't be to the left of the right edge
+ if (this.fit.left + this.fit.width < this.viewportWidth) {
+ this.fit.left = this.viewportWidth - this.fit.width;
+ }
+ }
+
+ if (this.fit.height <= this.viewportHeight) {
+ this.fit.top = (this.viewportHeight - this.fit.height) / 2;
+ }
+ else {
+ // Don't let the top of the photo be below the top of the screen
+ if (this.fit.top > 0)
+ this.fit.top = 0;
+
+ // bottom of photo shouldn't be above the bottom of screen
+ if (this.fit.top + this.fit.height < this.viewportHeight) {
+ this.fit.top = this.viewportHeight - this.fit.height;
+ }
+ }
+
+ if (this.switching)
+ return;
+
+ // If a time was specified, set up a transition so that the
+ // call to setPosition() below is animated
+ if (time) {
+ // If a time was specfied, animate the transformation
+ this.image.style.transition = 'transform ' + time + 'ms ease';
+ var self = this;
+ this.image.addEventListener('transitionend', function done(e) {
+ self.image.removeEventListener('transitionend', done);
+ self.image.style.transition = null;
+ });
+ }
+
+ this.setPosition();
+};
+
+// If the item being displayed is larger than the continer, pan it by
+// the specified amounts. Return the "unused" dx amount for the gallery app
+// to use for sideways swiping
+MediaFrame.prototype.pan = function(dx, dy) {
+ // We can only pan images, so return the entire dx amount
+ if (!this.displayingImage) {
+ return dx;
+ }
+
+ // Handle panning in the y direction first, since it is easier.
+ // Don't pan in the y direction if we already fit on the screen
+ if (this.fit.height > this.viewportHeight) {
+ this.fit.top += dy;
+
+ // Don't let the top of the photo be below the top of the screen
+ if (this.fit.top > 0)
+ this.fit.top = 0;
+
+ // bottom of photo shouldn't be above the bottom of screen
+ if (this.fit.top + this.fit.height < this.viewportHeight)
+ this.fit.top = this.viewportHeight - this.fit.height;
+ }
+
+ // Now handle the X dimension. If we've already panned as far as we can
+ // within the image (or if it isn't zoomed in) then return the "extra"
+ // unused dx amount to the caller so that the caller can use them to
+ // shift the frame left or right.
+ var extra = 0;
+
+ if (this.fit.width <= this.viewportWidth) {
+ // In this case, the photo isn't zoomed in, so it is all extra
+ extra = dx;
+ }
+ else {
+ this.fit.left += dx;
+
+ // If this would take the left edge of the photo past the
+ // left edge of the screen, then some of the motion is extra
+ if (this.fit.left > 0) {
+ extra = this.fit.left;
+ this.fit.left = 0;
+ }
+
+ // Or, if this would take the right edge of the photo past the
+ // right edge of the screen, then we've got extra.
+ if (this.fit.left + this.fit.width < this.viewportWidth) {
+ extra = this.fit.left + this.fit.width - this.viewportWidth;
+ this.fit.left = this.viewportWidth - this.fit.width;
+ }
+ }
+
+ this.setPosition();
+ return extra;
+};
diff --git a/shared/js/media/video_player.js b/shared/js/media/video_player.js
new file mode 100644
index 0000000..c79bb8b
--- /dev/null
+++ b/shared/js/media/video_player.js
@@ -0,0 +1,313 @@
+'use strict';
+
+// Create a <video> element and <div> containing a video player UI and
+// add them to the specified container. The UI requires a GestureDetector
+// to be running for the container or one of its ancestors.
+function VideoPlayer(container) {
+ if (typeof container === 'string')
+ container = document.getElementById(container);
+
+ function newelt(parent, type, classes) {
+ var e = document.createElement(type);
+ if (classes)
+ e.className = classes;
+ parent.appendChild(e);
+ return e;
+ }
+
+ // This copies the controls structure of the Video app
+ var player = newelt(container, 'video', 'videoPlayer');
+ var controls = newelt(container, 'div', 'videoPlayerControls');
+ var playbutton = newelt(controls, 'button', 'videoPlayerPlayButton');
+ var footer = newelt(controls, 'div', 'videoPlayerFooter hidden');
+ var pausebutton = newelt(footer, 'button', 'videoPlayerPauseButton');
+ var slider = newelt(footer, 'div', 'videoPlayerSlider');
+ var elapsedText = newelt(slider, 'span', 'videoPlayerElapsedText');
+ var progress = newelt(slider, 'div', 'videoPlayerProgress');
+ var backgroundBar = newelt(progress, 'div', 'videoPlayerBackgroundBar');
+ var elapsedBar = newelt(progress, 'div', 'videoPlayerElapsedBar');
+ var playHead = newelt(progress, 'div', 'videoPlayerPlayHead');
+ var durationText = newelt(slider, 'span', 'videoPlayerDurationText');
+
+ this.player = player;
+ this.controls = controls;
+
+ player.preload = 'metadata';
+
+ var self = this;
+ var controlsHidden = false;
+ var dragging = false;
+ var pausedBeforeDragging = false;
+ var screenLock; // keep the screen on when playing
+ var endedTimer;
+ var rotation; // Do we have to rotate the video? Set by load()
+
+ this.load = function(url, rotate) {
+ rotation = rotate || 0;
+ player.mozAudioChannelType = 'content';
+ player.src = url;
+ };
+
+ // Call this when the container size changes
+ this.setPlayerSize = setPlayerSize;
+
+ // Set up everything for the initial paused state
+ this.pause = function pause() {
+ // Pause video playback
+ player.pause();
+
+ // Hide the pause button and slider
+ footer.classList.add('hidden');
+ controlsHidden = true;
+
+ // Show the big central play button
+ playbutton.classList.remove('hidden');
+
+ // Unlock the screen so it can sleep on idle
+ if (screenLock) {
+ screenLock.unlock();
+ screenLock = null;
+ }
+
+ if (this.onpaused)
+ this.onpaused();
+ };
+
+ // Set up the playing state
+ this.play = function play() {
+ // If we're at the end of the video, restart at the beginning.
+ // This seems to happen automatically when an 'ended' event was fired.
+ // But some media types don't generate the ended event and don't
+ // automatically go back to the start.
+ if (player.currentTime >= player.duration - 0.5)
+ player.currentTime = 0;
+
+ // Start playing the video
+ player.play();
+
+ // Hide the play button
+ playbutton.classList.add('hidden');
+
+ // Show the controls
+ footer.classList.remove('hidden');
+ controlsHidden = false;
+
+ // Don't let the screen go to sleep
+ if (!screenLock)
+ screenLock = navigator.requestWakeLock('screen');
+
+ if (this.onplaying)
+ this.onplaying();
+ };
+
+ // Hook up the play button
+ playbutton.addEventListener('tap', function(e) {
+ // If we're paused, go to the play state
+ if (player.paused) {
+ self.play();
+ }
+ e.stopPropagation();
+ });
+
+ // Hook up the pause button
+ pausebutton.addEventListener('tap', function(e) {
+ self.pause();
+ e.stopPropagation();
+ });
+
+ // A click anywhere else on the screen should toggle the footer
+ // But only when the video is playing.
+ controls.addEventListener('tap', function(e) {
+ if (e.target === controls && !player.paused) {
+ footer.classList.toggle('hidden');
+ controlsHidden = !controlsHidden;
+ }
+ });
+
+ // Set the video size and duration when we get metadata
+ player.onloadedmetadata = function() {
+ durationText.textContent = formatTime(player.duration);
+ setPlayerSize();
+ // start off in the paused state
+ self.pause();
+ };
+
+ // Also resize the player on a resize event
+ // (when the user rotates the phone)
+ window.addEventListener('resize', function() {
+ setPlayerSize();
+ });
+
+ // If we reach the end of a video, reset to beginning
+ // This isn't always reliable, so we also set a timer in updateTime()
+ player.onended = ended;
+
+ function ended() {
+ if (dragging)
+ return;
+ if (endedTimer) {
+ clearTimeout(endedTimer);
+ endedTimer = null;
+ }
+ self.pause();
+ };
+
+ // Update the slider and elapsed time as the video plays
+ player.ontimeupdate = updateTime;
+
+ // Set the elapsed time and slider position
+ function updateTime() {
+ if (!controlsHidden) {
+ elapsedText.textContent = formatTime(player.currentTime);
+
+ // We can't update a progress bar if we don't know how long
+ // the video is. It is kind of a bug that the <video> element
+ // can't figure this out for ogv videos.
+ if (player.duration === Infinity || player.duration === 0)
+ return;
+
+ var percent = (player.currentTime / player.duration) * 100 + '%';
+ elapsedBar.style.width = percent;
+ playHead.style.left = percent;
+ }
+
+ // Since we don't always get reliable 'ended' events, see if
+ // we've reached the end this way.
+ // See: https://bugzilla.mozilla.org/show_bug.cgi?id=783512
+ // If we're within 1 second of the end of the video, register
+ // a timeout a half a second after we'd expect an ended event.
+ if (!endedTimer) {
+ if (!dragging && player.currentTime >= player.duration - 1) {
+ var timeUntilEnd = (player.duration - player.currentTime + .5);
+ endedTimer = setTimeout(ended, timeUntilEnd * 1000);
+ }
+ }
+ else if (dragging && player.currentTime < player.duration - 1) {
+ // If there is a timer set and we drag away from the end, cancel the timer
+ clearTimeout(endedTimer);
+ endedTimer = null;
+ }
+ }
+
+ // Make the video fit the container
+ function setPlayerSize() {
+ var containerWidth = container.clientWidth;
+ var containerHeight = container.clientHeight;
+
+ // Don't do anything if we don't know our size.
+ // This could happen if we get a resize event before our metadata loads
+ if (!player.videoWidth || !player.videoHeight)
+ return;
+
+ var width, height; // The size the video will appear, after rotation
+ switch (rotation) {
+ case 0:
+ case 180:
+ width = player.videoWidth;
+ height = player.videoHeight;
+ break;
+ case 90:
+ case 270:
+ width = player.videoHeight;
+ height = player.videoWidth;
+ }
+
+ var xscale = containerWidth / width;
+ var yscale = containerHeight / height;
+ var scale = Math.min(xscale, yscale);
+
+ // Scale large videos down, and scale small videos up.
+ // This might reduce image quality for small videos.
+ width *= scale;
+ height *= scale;
+
+ var left = ((containerWidth - width) / 2);
+ var top = ((containerHeight - height) / 2);
+
+ var transform;
+ switch (rotation) {
+ case 0:
+ transform = 'translate(' + left + 'px,' + top + 'px)';
+ break;
+ case 90:
+ transform =
+ 'translate(' + (left + width) + 'px,' + top + 'px) ' +
+ 'rotate(90deg)';
+ break;
+ case 180:
+ transform =
+ 'translate(' + (left + width) + 'px,' + (top + height) + 'px) ' +
+ 'rotate(180deg)';
+ break;
+ case 270:
+ transform =
+ 'translate(' + left + 'px,' + (top + height) + 'px) ' +
+ 'rotate(270deg)';
+ break;
+ }
+
+ transform += ' scale(' + scale + ')';
+
+ player.style.transform = transform;
+ }
+
+ // handle drags on the time slider
+ slider.addEventListener('pan', function pan(e) {
+ e.stopPropagation();
+ // We can't do anything if we don't know our duration
+ if (player.duration === Infinity)
+ return;
+
+ if (!dragging) { // Do this stuff on the first pan event only
+ dragging = true;
+ pausedBeforeDragging = player.paused;
+ if (!pausedBeforeDragging) {
+ player.pause();
+ }
+ }
+
+ var rect = backgroundBar.getBoundingClientRect();
+ var position = (e.detail.position.clientX - rect.left) / rect.width;
+ var pos = Math.min(Math.max(position, 0), 1);
+ player.currentTime = player.duration * pos;
+ updateTime();
+ });
+
+ slider.addEventListener('swipe', function swipe(e) {
+ e.stopPropagation();
+ dragging = false;
+ if (player.currentTime >= player.duration) {
+ self.pause();
+ } else if (!pausedBeforeDragging) {
+ player.play();
+ }
+ });
+
+ function formatTime(time) {
+ function padLeft(num, length) {
+ var r = String(num);
+ while (r.length < length) {
+ r = '0' + r;
+ }
+ return r;
+ }
+
+ time = Math.round(time);
+ var minutes = Math.floor(time / 60);
+ var seconds = time % 60;
+ if (minutes < 60) {
+ return padLeft(minutes, 2) + ':' + padLeft(seconds, 2);
+ }
+ return '';
+ }
+}
+
+VideoPlayer.prototype.hide = function() {
+ this.player.style.display = 'none';
+ this.controls.style.display = 'none';
+};
+
+VideoPlayer.prototype.show = function() {
+ this.player.style.display = 'block';
+ this.controls.style.display = 'block';
+};
diff --git a/shared/js/mediadb.js b/shared/js/mediadb.js
new file mode 100644
index 0000000..cfdab33
--- /dev/null
+++ b/shared/js/mediadb.js
@@ -0,0 +1,1532 @@
+/* -*- Mode: js; js-indent-level: 2; indent-tabs-mode: nil -*- */
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+/**
+ * MediaDB.js: a simple interface to DeviceStorage and IndexedDB that serves
+ * as a model of the filesystem and provides easy access to the
+ * user's media files and their metadata.
+ *
+ * Gaia's media apps (Gallery, Music, Videos) read media files from the phone
+ * using the DeviceStorage API. They need to keep track of the complete list of
+ * media files, as well as the metadata (image thumbnails, song titles, etc.)
+ * they have extracted from those files. It would be much too slow to scan the
+ * filesystem and read all the metadata from all files each time the apps starts
+ * up, so the apps need to store the list of files and metadata in an IndexedDB
+ * database. This library integrates both DeviceStorage and IndexedDB into a
+ * single API. It keeps the database in sync with the filesystem and provides
+ * notifications when files are added or deleted.
+ *
+ * CONSTRUCTOR
+ *
+ * Create a MediaDB object with the MediaDB() constructor. It takes three
+ * arguments:
+ *
+ * mediaType:
+ * one of the DeviceStorage media types: "pictures", "movies" or "music".
+ *
+ * metadataParser:
+ * your metadata parser function. This function should expect three
+ * arguments. It will be called with a file to parse and two callback
+ * functions. It should read metadata from the file and then pass an object
+ * of metadata to the first callback. If parsing fails it should pass an
+ * Error object or error message to the second callback. If you omit this
+ * argument or pass null, a dummy parser that invokes the callback with an
+ * empty object will be used instead.
+ *
+ * options:
+ * An optional object containing additional MediaDB options.
+ * Supported options are:
+ *
+ * directory:
+ * a subdirectory of the DeviceStorage directory. If you are only
+ * interested in images in the screenshots/ subdirectory for example,
+ * you can set this property to "screenshots/".
+ *
+ * mimeTypes:
+ * an array of MIME types that specifies the kind of files you are
+ * interested in and that your metadata parser function knows how to
+ * handle. DeviceStorage infers MIME type from filename extension and
+ * filters the files it returns based on their extension. Use this
+ * property if you want to restrict the set of mime types further.
+ *
+ * indexes:
+ * an array of IndexedDB key path specifications that specify which
+ * properties of each media record should be indexed. If you want to
+ * search or sort on anything other than the file name and date you
+ * should set this property. "size", and "type" are valid keypaths as
+ * is "metadata.x" where x is any metadata property returned by your
+ * metadata parser.
+ *
+ * version:
+ * The version of your IndexedDB database. The default value is 1
+ * Setting it to a larger value will delete all data in the database
+ * and rebuild it from scratch. If you ever change your metadata parser
+ * function or alter the array of indexes.
+ *
+ * autoscan:
+ * Whether MediaDB should automatically scan every time it becomes
+ * ready. The default is true. If you set this to false you are
+ * responsible for calling scan() in response to the 'ready' event.
+ *
+ * batchHoldTime:
+ * How long (in ms) to wait after finding a new file during a scan
+ * before reporting it. Longer hold times allow more batching of
+ * changes. The default is 100ms.
+ *
+ * batchSize:
+ * When batching changes, don't allow the batches to exceed this
+ * amount. The default is 0 which means no maximum batch size.
+ *
+ * MediaDB STATE
+ *
+ * A MediaDB object must asynchronously open a connection to its database, and
+ * asynchronously check on the availability of device storage, which means that
+ * it is not ready for use when first created. After calling the MediaDB()
+ * constructor, register an event listener for 'ready' events with
+ * addEventListener() or by setting the onready property. You must not use
+ * the MediaDB object until the ready event has been delivered or until
+ * the state property is set to MediaDB.READY.
+ *
+ * The DeviceStorage API is not always available, and MediaDB is not usable if
+ * DeviceStorage is not usable. If the user removes the SD card from their
+ * phone, then DeviceStorage will not be able to read or write files,
+ * obviously. Also, when a USB Mass Storage session is in progress,
+ * DeviceStorage is not available either. If DeviceStorage is not available
+ * when a MediaDB object is created, an 'unavailable' event will be fired
+ * instead of a 'ready' event. Subsequently, a 'ready' event will be fired
+ * whenever DeviceStorage becomes available, and 'unavailable' will be fired
+ * whenever DeviceStorage becomes unavailable. Media apps can handle the
+ * unavailable case by displaying an informative message in an overlay that
+ * prevents all user interaction with the app.
+ *
+ * The 'ready' and 'unavailable' events signal changes to the state of a
+ * MediaDB object. The state is also available in the state property of the
+ * object. The possible values of this property are the following:
+ *
+ * Value Constant Meaning
+ * ----------------------------------------------------------------------
+ * 'opening' MediaDB.OPENING MediaDB is initializing itself
+ * 'ready' MediaDB.READY MediaDB is available and ready for use
+ * 'nocard' MediaDB.NOCARD Unavailable because there is no sd card
+ * 'unmounted' MediaDB.UNMOUNTED Unavailable because the card is unmounted
+ * 'closed' MediaDB.CLOSED Unavailable because close() was called
+ *
+ * When an 'unavailable' event is fired, the detail property of the event
+ * object specifies the reason that the MediaDB is unavailable. It is one of
+ * the state values 'nocard', 'unmounted' or 'closed'.
+ *
+ * The 'nocard' state occurs when device storage is not available because
+ * there is no SD card in the device. This is typically a permanent failure
+ * state, and media apps cannot run without an SD card. It can occur
+ * transiently, however, if the user is swapping SD cards while a media app is
+ * open.
+ *
+ * The 'unmounted' state occurs when the device's SD card is unmounted. This
+ * is generally a temporary condition that occurs when the user starts a USB
+ * Mass Storage transfer session by plugging their device into a computer. In
+ * this case, MediaDB will become available again as soon as the device is
+ * unplugged (it may have different files on it, though: see the SCANNING
+ * section below).
+ *
+ * DATABASE RECORDS
+ *
+ * MediaDB stores a record in its IndexedDB database for each DeviceStorage
+ * file of the appropriate media type, directory and mime type. The records
+ * are objects of this form:
+ *
+ * {
+ * name: // the filename (relative to the DeviceStorage root)
+ * type: // the file MIME type (extension-based, from DeviceStorage)
+ * size: // the file size in bytes
+ * date: // file modification time (as ms since the epoch)
+ * metadata: // whatever object the metadata parser returned
+ * }
+ *
+ * Note that the database records do not include the file itself, but only its
+ * name. Use the getFile() method to get a File object (a Blob) by name.
+ *
+ * ENUMERATING FILES
+ *
+ * Typically, the first thing an app will do with a MediaDB object after the
+ * ready event is triggered is call the enumerate() method to obtain the list
+ * of files that MediaDB already knows about from previous app invocations.
+ * enumerate() gets records from the database and passes them to the specified
+ * callback. Each record that is passed to the callback is an object in the
+ * form shown above.
+ *
+ * If you pass only a callback to enumerate(), it calls the callback once for
+ * each entry in the database and then calls the callback with an argument of
+ * null to indicate that it is done.
+ *
+ * By default, entries are returned in alphabetical order by filename and all
+ * entries in the database are returned. You can specify other arguments to
+ * enumerate() to change the set of entries that are returned and the order that
+ * they are enumerated in. The full set of arguments are:
+ *
+ * key:
+ * A keypath specification that specifies what field to sort on. If you
+ * specify this argument, it must be 'name', 'date', or one of the values
+ * in the options.indexes array passed to the MediaDB() constructor. This
+ * argument is optional. If omitted, the default is to use the file name
+ * as the key.
+ *
+ * range:
+ * An IDBKeyRange object that optionally specifies upper and lower bounds on
+ * the specified key. This argument is optional. If omitted, all entries in
+ * the database are enumerated. See IndexedDB documentation for more on
+ * key ranges.
+ *
+ * direction:
+ * One of the IndexedDB direction string "next", "nextunique", "prev" or
+ * "prevunique". This argument is optional. If omitted, the default is
+ * "next", which enumerates entries in ascending order.
+ *
+ * callback:
+ * The function that database entries should be passed to. This argument is
+ * not optional, and is always passed as the last argument to enumerate().
+ *
+ * The enumerate() method returns database entries. These include file names,
+ * but not the files themselves. enumerate() interacts solely with the
+ * IndexedDB; it does not use DeviceStorage. If you want to use a media file
+ * (to play a song or display a photo, for example) call the getFile() method.
+ *
+ * enumerate() returns an object with a 'state' property that starts out as
+ * 'enumerating' and switches to 'complete' when the enumeration is done. You
+ * can cancel a pending enumeration by passing this object to the
+ * cancelEnumeration() method. This switches the state to 'cancelling' and then
+ * it switches to 'cancelled' when the cancellation is complete. If you call
+ * cancelEnumeration(), the callback function you passed to enumerate() is
+ * guaranteed not to be called again.
+ *
+ * In addition to enumerate(), there are two other methods you can use
+ * to enumerate database entries:
+ *
+ * enumerateAll() takes the same arguments and returns the same values
+ * as enumerate(), but it batches the results and passes them in an
+ * array to the callback function.
+ *
+ * getAll() takes a callback argument and passes it an array of all
+ * entries in the database, sorted by filename. It does not allow you
+ * to specify a key, range, or direction, but if you need all entries
+ * from the database, this method is is much faster than enumerating
+ * entries individually.
+ *
+ * FILESYSTEM CHANGES
+ *
+ * When media files are added or removed, MediaDB reports this by triggering
+ * 'created' and 'deleted' events.
+ *
+ * When a 'created' event is fired, the detail property of the event is an
+ * array of database record objects. When a single file is created (for
+ * example when the user takes a picture with the Camera app) this array has
+ * only a single element. But when MediaDB scans for new files (see SCANNING
+ * below) it may batch multiple records into a single created event. If a
+ * 'created' event has many records, apps may choose to simply rebuild their
+ * UI from scratch with a new call to enumerate() instead of handling the new
+ * files one at a time.
+ *
+ * When a 'deleted' event is fired, the detail property of the event is an
+ * array of the names of the files that have been deleted. As with 'created'
+ * events, the array may have a single element or may have many batched
+ * elements.
+ *
+ * If MediaDB detects that a file has been modified in place (because its
+ * size or date changes) it treats this as a deletion of the old version and
+ * the creation of a new version, and will fire a deleted event followed by
+ * a created event.
+ *
+ * The created and deleted events are not triggered until the corresponding
+ * files have actually been created and deleted and their database records
+ * have been updated.
+ *
+ * SCANNING
+ *
+ * MediaDB automatically scans for new and deleted files every time it enters
+ * the MediaDB.READY state. This happens when the MediaDB object is first
+ * created, and also when an SD card is removed and reinserted or when the
+ * user finishes a USB Mass Storage session. If the scan finds new files, it
+ * reports them with one or more 'created' events. If the scan finds that
+ * files have been deleted, it reports them with one or more 'deleted' events.
+ *
+ * MediaDB fires a 'scanstart' event when a scan begins and fires a 'scanend'
+ * event when the scan is complete. Apps can use these events to let the user
+ * know that a scan is in progress.
+ *
+ * The scan algorithm attempts to quickly look for new files and reports those
+ * first. It then begins a slower full scan phase where it checks that each of
+ * the files it already knows about is still present.
+ *
+ * EVENTS
+ *
+ * As described above, MediaDB sends events to communicate with the apps
+ * that use it. The event types and their meanings are:
+ *
+ * Event Meaning
+ * --------------------------------------------------------------------------
+ * ready MediaDB is ready for use
+ * unavailable MediaDB is unavailable (often because of USB file transfer)
+ * created One or more files were created
+ * deleted One or more files were deleted
+ * scanstart MediaDB is scanning
+ * scanend MediaDB has finished scanning
+ *
+ * Because MediaDB is a JavaScript library, these are not real DOM events, but
+ * simulations.
+ *
+ * MediaDB defines two-argument versions of addEventListener() and
+ * removeEventListener() and also allows you to define event handlers by
+ * setting 'on' properties like 'onready' and 'onscanstart'.
+ *
+ * The objects passed on MediaDB event handlers are not true Event objects but
+ * simulate a CustomEvent by defining type, target, currentTarget, timestamp
+ * and detail properties. For MediaDB events, it is the detail property that
+ * always holds the useful information. These simulated event objects do not
+ * have preventDefault(), stopPropagation() or stopImmediatePropagation()
+ * methods.
+ *
+ * MediaDB events do not bubble and cannot be captured.
+ *
+ * METHODS
+ *
+ * MediaDB defines the following methods:
+ *
+ * - addEventListener(): register a function to call when an event is fired
+ *
+ * - removeEventListener(): unregister an event listener function
+ *
+ * - enumerate(): for each file that MediaDB knows about, pass its database
+ * record object to the specified callback. By default, records are returned
+ * in alphabetical order by name, but optional arguments allow you to
+ * specify a database index, a key range, and a sort direction.
+ *
+ * - cancelEnumeration(): stops an enumeration in progress. Pass the object
+ * returned by enumerate().
+ *
+ * - getFile(): given a filename and a callback, this method looks up the
+ * named file in DeviceStorage and passes it (a Blob) to the callback.
+ * An error callback is available as an optional third argument.
+ *
+ * - count(): count the number of records in the database and pass the value
+ * to the specified callback. Like enumerate(), this method allows you
+ * to specify the name of an index and a key range if you only want to
+ * count some of the records.
+ *
+ * - updateMetadata(): updates the metadata associated with a named file
+ *
+ * - addFile(): given a filename and a blob this method saves the blob as a
+ * named file to device storage.
+ *
+ * - deleteFile(): deletes the named file from device storage and the database
+ *
+ * - close(): closes the IndexedDB connections and stops listening to
+ * DeviceStorage events. This permanently puts the MediaDB object into
+ * the MediaDB.CLOSED state in which it is unusable.
+ *
+ * - freeSpace(): call the DeviceStorage freeSpace() method and pass the
+ * result to the specified callback
+ */
+var MediaDB = (function() {
+
+ function MediaDB(mediaType, metadataParser, options) {
+ this.mediaType = mediaType;
+ this.metadataParser = metadataParser;
+ if (!options)
+ options = {};
+ this.indexes = options.indexes || [];
+ this.version = options.version || 1;
+ this.directory = options.directory || '';
+ this.mimeTypes = options.mimeTypes;
+ this.autoscan = (options.autoscan !== undefined) ? options.autoscan : true;
+ this.state = MediaDB.OPENING;
+ this.scanning = false; // becomes true while scanning
+
+ // While scanning, we attempt to send change events in batches.
+ // After finding a new or deleted file, we'll wait this long before
+ // sending events in case we find another new or deleted file right away.
+ this.batchHoldTime = options.batchHoldTime || 100;
+
+ // But we'll send a batch of changes right away if it gets this big
+ // A batch size of 0 means no maximum batch size
+ this.batchSize = options.batchSize || 0;
+
+ if (this.directory &&
+ this.directory[this.directory.length - 1] !== '/')
+ this.directory += '/';
+
+ this.dbname = 'MediaDB/' + this.mediaType + '/' + this.directory;
+
+ var media = this; // for the nested functions below
+
+ // Private implementation details in this object
+ this.details = {
+ // This maps event type -> array of listeners
+ // See addEventListener and removeEventListener
+ eventListeners: {},
+
+ // Properties for queuing up db insertions and deletions and also
+ // for queueing up notifications to be sent
+ pendingInsertions: [], // Array of filenames to insert
+ pendingDeletions: [], // Array of filenames to remove
+ whenDoneProcessing: [], // Functions to run when queue is empty
+
+ pendingCreateNotifications: [], // Array of fileinfo objects
+ pendingDeleteNotifications: [], // Ditto
+ pendingNotificationTimer: null,
+
+ // This property holds the modification date of the newest file we have.
+ // We need to know the newest file in order to look for newer ones during
+ // scanning. We initialize newestFileModTime during initialization by
+ // actually checking the database (using the date index). We also update
+ // this property every time a new record is added to the database.
+ newestFileModTime: 0
+ };
+
+ // Define a dummy metadata parser if we're not given one
+ if (!this.metadataParser) {
+ this.metadataParser = function(file, callback) {
+ setTimeout(function() { callback({}); }, 0);
+ }
+ }
+
+ // Open the database
+ // Note that the user can upgrade the version and we can upgrade the version
+ var openRequest = indexedDB.open(this.dbname,
+ this.version * MediaDB.VERSION);
+
+ // This should never happen for Gaia apps
+ openRequest.onerror = function(e) {
+ console.error('MediaDB():', openRequest.error.name);
+ };
+
+ // This should never happen for Gaia apps
+ openRequest.onblocked = function(e) {
+ console.error('indexedDB.open() is blocked in MediaDB()');
+ };
+
+ // This is where we create (or delete and recreate) the database
+ openRequest.onupgradeneeded = function(e) {
+ var db = openRequest.result;
+
+ // If there are already existing object stores, delete them all
+ // If the version number changes we just want to start over.
+ var existingStoreNames = db.objectStoreNames;
+ for (var i = 0; i < existingStoreNames.length; i++) {
+ db.deleteObjectStore(existingStoreNames[i]);
+ }
+
+ // Now build the database
+ var filestore = db.createObjectStore('files', { keyPath: 'name' });
+ // Always index the files by modification date
+ filestore.createIndex('date', 'date');
+ // And index them by any other file properties or metadata properties
+ // passed to the constructor
+ media.indexes.forEach(function(indexName) {
+ // Don't recreate indexes we've already got
+ if (indexName === 'name' || indexName === 'date')
+ return;
+ // the index name is also the keypath
+ filestore.createIndex(indexName, indexName);
+ });
+ }
+
+ // This is called when we've got the database open and ready.
+ openRequest.onsuccess = function(e) {
+ media.db = openRequest.result;
+
+ // Log any errors that propagate up to here
+ media.db.onerror = function(event) {
+ console.error('MediaDB: ',
+ event.target.error && event.target.error.name);
+ }
+
+ // Query the db to find the modification time of the newest file
+ var cursorRequest =
+ media.db.transaction('files', 'readonly')
+ .objectStore('files')
+ .index('date')
+ .openCursor(null, 'prev');
+
+ cursorRequest.onerror = function() {
+ // If anything goes wrong just display an error.
+ // If this fails, don't even attempt error recovery
+ console.error('MediaDB initialization error', cursorRequest.error);
+ };
+ cursorRequest.onsuccess = function() {
+ var cursor = cursorRequest.result;
+ if (cursor) {
+ media.details.newestFileModTime = cursor.value.date;
+ }
+ else {
+ // No files in the db yet, so use a really old time
+ media.details.newestFileModTime = 0;
+ }
+
+ // The DB is initialized, and we've got our mod time
+ // so move on and initialize device storage
+ initDeviceStorage();
+ };
+ };
+
+ function initDeviceStorage() {
+ // Set up DeviceStorage
+ // If storage is null, then there is no sdcard installed and
+ // we have to abort.
+ media.storage = navigator.getDeviceStorage(mediaType);
+
+ // Handle change notifications from device storage
+ // We set this onchange property to null in the close() method
+ // so don't use addEventListener here
+ media.storage.addEventListener('change', deviceStorageChangeHandler);
+ media.details.dsEventListener = deviceStorageChangeHandler;
+
+ // Use available() to figure out if there is actually an sdcard there
+ // and emit a ready or unavailable event
+ var availreq = media.storage.available();
+ availreq.onsuccess = function(e) {
+ switch (e.target.result) {
+ case 'available':
+ changeState(media, MediaDB.READY);
+ if (media.autoscan)
+ scan(media); // Start scanning as soon as we're ready
+ break;
+ case 'unavailable':
+ changeState(media, MediaDB.NOCARD);
+ break;
+ case 'shared':
+ changeState(media, MediaDB.UNMOUNTED);
+ break;
+ }
+ };
+ availreq.onerror = function(e) {
+ console.error('available() failed',
+ availreq.error && availreq.error.name);
+ changeState(media, MediaDB.UNMOUNTED);
+ };
+ }
+
+ function deviceStorageChangeHandler(e) {
+ var filename;
+ switch (e.reason) {
+ case 'available':
+ changeState(media, MediaDB.READY);
+ if (media.autoscan)
+ scan(media); // automatically scan every time the card comes back
+ break;
+ case 'unavailable':
+ changeState(media, MediaDB.NOCARD);
+ endscan(media);
+ break;
+ case 'shared':
+ changeState(media, MediaDB.UNMOUNTED);
+ endscan(media);
+ break;
+ case 'modified':
+ case 'deleted':
+ filename = e.path;
+ if (ignoreName(filename))
+ break;
+ if (media.directory) {
+ // Ignore changes outside of our directory
+ if (filename.substring(0, media.directory.length) !==
+ media.directory)
+ break;
+ // And strip the directory from changes inside of it
+ filename = filename.substring(media.directory.length);
+ }
+ if (e.reason === 'modified')
+ insertRecord(media, filename);
+ else
+ deleteRecord(media, filename);
+ break;
+ }
+ }
+ }
+
+ MediaDB.prototype = {
+ close: function close() {
+ // Close the database
+ this.db.close();
+
+ // There is no way to close device storage, but we at least want
+ // to stop receiving events from it.
+ this.storage.removeEventListener('change', this.details.dsEventListener);
+
+ // Change state and send out an event
+ changeState(this, MediaDB.CLOSED);
+ },
+
+ addEventListener: function addEventListener(type, listener) {
+ if (!this.details.eventListeners.hasOwnProperty(type))
+ this.details.eventListeners[type] = [];
+ var listeners = this.details.eventListeners[type];
+ if (listeners.indexOf(listener) !== -1)
+ return;
+ listeners.push(listener);
+ },
+
+ removeEventListener: function removeEventListener(type, listener) {
+ if (!this.details.eventListeners.hasOwnProperty(type))
+ return;
+ var listeners = this.details.eventListeners[type];
+ var position = listeners.indexOf(listener);
+ if (position === -1)
+ return;
+ listeners.splice(position, 1);
+ },
+
+ // Look up the specified filename in DeviceStorage and pass the
+ // resulting File object to the specified callback.
+ getFile: function getFile(filename, callback, errback) {
+ if (this.state !== MediaDB.READY)
+ throw Error('MediaDB is not ready. State: ' + this.state);
+
+ var getRequest = this.storage.get(this.directory + filename);
+ getRequest.onsuccess = function() {
+ callback(getRequest.result);
+ };
+ getRequest.onerror = function() {
+ var errmsg = getRequest.error && getRequest.error.name;
+ if (errback)
+ errback(errmsg);
+ else
+ console.error('MediaDB.getFile:', errmsg);
+ }
+ },
+
+ // Delete the named file from device storage.
+ // This will cause a device storage change event, which will cause
+ // mediadb to remove the file from the database and send out a
+ // mediadb change event, which will notify the application UI.
+ deleteFile: function deleteFile(filename) {
+ if (this.state !== MediaDB.READY)
+ throw Error('MediaDB is not ready. State: ' + this.state);
+
+ this.storage.delete(this.directory + filename).onerror = function(e) {
+ console.error('MediaDB.deleteFile(): Failed to delete', filename,
+ 'from DeviceStorage:', e.target.error);
+ };
+ },
+
+ //
+ // Save the specified blob to device storage, using the specified filename.
+ // This will cause device storage to send us an event, and that event
+ // will cause mediadb to add the file to its database, and that will
+ // send out a mediadb event to the application UI.
+ //
+ addFile: function addFile(filename, file) {
+ if (this.state !== MediaDB.READY)
+ throw Error('MediaDB is not ready. State: ' + this.state);
+
+ var media = this;
+
+ // Delete any existing file by this name, then save the file.
+ var deletereq = media.storage.delete(media.directory + filename);
+ deletereq.onsuccess = deletereq.onerror = save;
+
+ function save() {
+ var request = media.storage.addNamed(file, media.directory + filename);
+ request.onerror = function() {
+ console.error('MediaDB: Failed to store', filename,
+ 'in DeviceStorage:', storeRequest.error);
+ };
+ }
+ },
+
+ // Look up the database record for the named file, and copy the properties
+ // of the metadata object into the file's metadata, and then write the
+ // updated record back to the database. The third argument is optional. If
+ // you pass a function, it will be called when the metadata is written.
+ updateMetadata: function(filename, metadata, callback) {
+ if (this.state !== MediaDB.READY)
+ throw Error('MediaDB is not ready. State: ' + this.state);
+
+ var media = this;
+
+ // First, look up the fileinfo record in the db
+ var read = media.db.transaction('files', 'readonly')
+ .objectStore('files')
+ .get(filename);
+
+ read.onerror = function() {
+ console.error('MediaDB.updateMetadata called with unknown filename');
+ };
+
+ read.onsuccess = function() {
+ var fileinfo = read.result;
+
+ // Update the fileinfo metadata
+ Object.keys(metadata).forEach(function(key) {
+ fileinfo.metadata[key] = metadata[key];
+ });
+
+ // And write it back into the database.
+ var write = media.db.transaction('files', 'readwrite')
+ .objectStore('files')
+ .put(fileinfo);
+
+ write.onerror = function() {
+ console.error('MediaDB.updateMetadata: database write failed',
+ write.error && write.error.name);
+ };
+
+ if (callback) {
+ write.onsuccess = function() {
+ callback();
+ }
+ }
+ }
+ },
+
+ // Count the number of records in the database and pass that number to the
+ // specified callback. key is 'name', 'date' or one of the index names
+ // passed to the constructor. range is be an IDBKeyRange that defines a
+ // the range of key values to count. key and range are optional
+ // arguments. If one argument is passed, it is the callback. If two
+ // arguments are passed, they are assumed to be the range and callback.
+ count: function(key, range, callback) {
+ if (this.state !== MediaDB.READY)
+ throw Error('MediaDB is not ready. State: ' + this.state);
+
+ // range is an optional argument
+ if (arguments.length === 1) {
+ callback = key;
+ range = undefined;
+ key = undefined;
+ }
+ else if (arguments.length === 2) {
+ callback = range;
+ range = key;
+ key = undefined;
+ }
+
+ var store = this.db.transaction('files').objectStore('files');
+ if (key && key !== 'name')
+ store = store.index(key);
+
+ var countRequest = store.count(range || null);
+
+ countRequest.onerror = function() {
+ console.error('MediaDB.count() failed with', countRequest.error);
+ };
+
+ countRequest.onsuccess = function(e) {
+ callback(e.target.result);
+ };
+ },
+
+
+ // Enumerate all files in the filesystem, sorting by the specified
+ // property (which must be one of the indexes, or null for the filename).
+ // Direction is ascending or descending. Use whatever string
+ // constant IndexedDB uses. f is the function to pass each record to.
+ //
+ // Each record is an object like this:
+ //
+ // {
+ // // The basic fields are all from the File object
+ // name: // the filename
+ // type: // the file type
+ // size: // the file size
+ // date: // file mod time
+ // metadata: // whatever object the metadata parser returns
+ // }
+ //
+ // This method returns an object that you can pass to cancelEnumeration()
+ // to cancel an enumeration in progress. You can use the state property
+ // of the returned object to find out the state of the enumeration. It
+ // should be one of the strings 'enumerating', 'complete', 'cancelling'
+ // 'cancelled', or 'error'
+ //
+ enumerate: function enumerate(key, range, direction, callback) {
+ if (this.state !== MediaDB.READY)
+ throw Error('MediaDB is not ready. State: ' + this.state);
+
+ var handle = { state: 'enumerating' };
+
+ // The first three arguments are optional, but the callback
+ // is required, and we don't want to have to pass three nulls
+ if (arguments.length === 1) {
+ callback = key;
+ key = undefined;
+ }
+ else if (arguments.length === 2) {
+ callback = range;
+ range = undefined;
+ }
+ else if (arguments.length === 3) {
+ callback = direction;
+ direction = undefined;
+ }
+
+ var store = this.db.transaction('files').objectStore('files');
+
+ // If a key other than "name" is specified, then use the index for that
+ // key instead of the store.
+ if (key && key !== 'name')
+ store = store.index(key);
+
+ // Now create a cursor for the store or index.
+ var cursorRequest = store.openCursor(range || null, direction || 'next');
+
+ cursorRequest.onerror = function() {
+ console.error('MediaDB.enumerate() failed with', cursorRequest.error);
+ handle.state = 'error';
+ };
+
+ cursorRequest.onsuccess = function() {
+ // If the enumeration has been cancelled, return without
+ // calling the callback and without calling cursor.continue();
+ if (handle.state === 'cancelling') {
+ handle.state = 'cancelled';
+ return;
+ }
+
+ var cursor = cursorRequest.result;
+ if (cursor) {
+ try {
+ if (!cursor.value.fail) // if metadata parsing succeeded
+ callback(cursor.value);
+ }
+ catch (e) {
+ console.warn('MediaDB.enumerate(): callback threw', e);
+ }
+ cursor.continue();
+ }
+ else {
+ // Final time, tell the callback that there are no more.
+ handle.state = 'complete';
+ callback(null);
+ }
+ };
+
+ return handle;
+ },
+
+ // This method takes the same arguments as enumerate(), but batches
+ // the results into an array and passes them to the callback all at
+ // once when the enumeration is complete. It uses enumerate() so it
+ // is no faster than that method, but may be more convenient.
+ enumerateAll: function enumerateAll(key, range, direction, callback) {
+ var batch = [];
+
+ // The first three arguments are optional, but the callback
+ // is required, and we don't want to have to pass three nulls
+ if (arguments.length === 1) {
+ callback = key;
+ key = undefined;
+ }
+ else if (arguments.length === 2) {
+ callback = range;
+ range = undefined;
+ }
+ else if (arguments.length === 3) {
+ callback = direction;
+ direction = undefined;
+ }
+
+ return this.enumerate(key, range, direction, function(fileinfo) {
+ if (fileinfo !== null)
+ batch.push(fileinfo);
+ else
+ callback(batch);
+ });
+ },
+
+ // Cancel a pending enumeration. After calling this the callback for
+ // the specified enumeration will not be invoked again.
+ cancelEnumeration: function cancelEnumeration(handle) {
+ if (handle.state === 'enumerating')
+ handle.state = 'cancelling';
+ },
+
+ // Use the non-standard mozGetAll() function to return all of the
+ // records in the database in one big batch. The records will be
+ // sorted by filename
+ getAll: function getAll(callback) {
+ if (this.state !== MediaDB.READY)
+ throw Error('MediaDB is not ready. State: ' + this.state);
+
+ var store = this.db.transaction('files').objectStore('files');
+ var request = store.mozGetAll();
+ request.onerror = function() {
+ console.error('MediaDB.getAll() failed with', request.error);
+ };
+ request.onsuccess = function() {
+ var all = request.result; // All records in the object store
+
+ // Filter out files that failed metadata parsing
+ var good = all.filter(function(fileinfo) { return !fileinfo.fail; });
+
+ callback(good);
+ };
+ },
+
+ // Scan for new or deleted files.
+ // This is only necessary if you have explicitly disabled automatic
+ // scanning by setting autoscan:false in the options object.
+ scan: function() {
+ scan(this);
+ },
+
+ // Use the device storage freeSpace() method and pass the returned
+ // value to the callback.
+ freeSpace: function freeSpace(callback) {
+ if (this.state !== MediaDB.READY)
+ throw Error('MediaDB is not ready. State: ' + this.state);
+
+ var freereq = this.storage.freeSpace();
+ freereq.onsuccess = function() {
+ callback(freereq.result);
+ }
+ }
+ };
+
+ // This is the version number of the MediaDB schema. If we change this
+ // number it will cause existing data stores to be deleted and rebuilt,
+ // which is useful when the schema changes. Note that the user can also
+ // upgrade the version number with an option to the MediaDB constructor.
+ // The final indexedDB version number we use is the product of our version
+ // and the user's version.
+ // This is version 2 because we modified the default schema to include
+ // an index for file modification date.
+ MediaDB.VERSION = 2;
+
+ // These are the values of the state property of a MediaDB object
+ // The NOCARD, UNMOUNTED, and CLOSED values are also used as the detail
+ // property of 'unavailable' events
+ MediaDB.OPENING = 'opening'; // MediaDB is initializing itself
+ MediaDB.READY = 'ready'; // MediaDB is available and ready for use
+ MediaDB.NOCARD = 'nocard'; // Unavailable because there is no sd card
+ MediaDB.UNMOUNTED = 'unmounted'; // Unavailable because card unmounted
+ MediaDB.CLOSED = 'closed'; // Unavailalbe because MediaDB has closed
+
+ /* Details of helper functions follow */
+
+ //
+ // Return true if media db should ignore this file.
+ //
+ // If any components of the path begin with a . we'll ignore the file.
+ // The '.' prefix indicates hidden files and directories on Unix and
+ // when files are "moved to trash" during a USB Mass Storage session they
+ // are sometimes not actually deleted, but moved to a hidden directory.
+ //
+ // If an array of media types was specified when the MediaDB was created
+ // and the type of this file is not a member of that list, then ignore it.
+ //
+ function ignore(media, file) {
+ if (ignoreName(file.name))
+ return true;
+ if (media.mimeTypes && media.mimeTypes.indexOf(file.type) === -1)
+ return true;
+ return false;
+ }
+
+ // Test whether this filename is one we ignore.
+ // This is a separate function because device storage change events
+ // give us a name only, not the file object.
+ function ignoreName(filename) {
+ return (filename[0] === '.' || filename.indexOf('/.') !== -1);
+ }
+
+ // Tell the db to start a manual scan. I think we don't do
+ // this automatically from the constructor, but most apps will start
+ // a scan right after calling the constructor and then will proceed to
+ // enumerate what is already in the db. If scan performance is bad
+ // for large media collections, apps can just have the user specify
+ // when to rescan rather than doing it automatically. Until we have
+ // change event notifications, gaia apps might want to do a scan
+ // every time they are made visible.
+ //
+ // Filesystem changes discovered by a scan are generally
+ // batched. If a scan discovers 10 new files, the information
+ // about those files will generally be passed as an array to a the
+ // onchange handler rather than calling that handler once for each
+ // newly discovered file. Apps can decide whether to handle
+ // batches by processing each element individually or by just starting
+ // fresh with a new call to enumerate().
+ //
+ // Scan details are not tightly specified, but the goal is to be
+ // as efficient as possible. We'll try to do a quick date-based
+ // scan to look for new files and report those first. Following
+ // that, a full scan will be compared with a full dump of the DB
+ // to see if any files have been deleted.
+ //
+ function scan(media) {
+ media.scanning = true;
+ dispatchEvent(media, 'scanstart');
+
+ // First, scan for new files since the last scan, if there was one
+ // When the quickScan is done it will begin a full scan. If we don't
+ // have a last scan date, then the database is empty and we don't
+ // have to do a full scan, since there will be no changes or deletions.
+ quickScan(media.details.newestFileModTime);
+
+ // Do a quick scan and then follow with a full scan
+ function quickScan(timestamp) {
+ var cursor;
+ if (timestamp > 0) {
+ media.details.firstscan = false;
+ cursor = media.storage.enumerate(media.directory, {
+ // add 1 so we don't find the same newest file again
+ since: new Date(timestamp + 1)
+ });
+ }
+ else {
+ // If there is no timestamp then this is the first time we've
+ // scanned and we don't have any files in the database, which
+ // allows important optimizations during the scanning process
+ media.details.firstscan = true;
+ media.details.records = [];
+ cursor = media.storage.enumerate(media.directory);
+ }
+
+ cursor.onsuccess = function() {
+ var file = cursor.result;
+ if (file) {
+ if (!ignore(media, file))
+ insertRecord(media, file);
+ cursor.continue();
+ }
+ else {
+ // Quick scan is done. When the queue is empty, force out
+ // any batched created events and move on to the slower
+ // more thorough full scan.
+ whenDoneProcessing(media, function() {
+ sendNotifications(media);
+ if (media.details.firstscan) {
+ // If this was the first scan, then we're done
+ endscan(media);
+ }
+ else {
+ // If this was not the first scan, then we need to go
+ // ensure that all of the old files we know about are still there
+ fullScan();
+ }
+ });
+ }
+ };
+
+ cursor.onerror = function() {
+ // We can't scan if we can't read device storage.
+ // Perhaps the card was unmounted or pulled out
+ console.warning('Error while scanning', cursor.error);
+ endscan(media);
+ };
+ }
+
+ // Get a complete list of files from DeviceStorage
+ // Get a complete list of files from IndexedDB.
+ // Sort them both (the indexedDB list will already be sorted)
+ // Step through the lists noting deleted files and created files.
+ // Pay attention to files whose size or date has changed and
+ // treat those as deletions followed by insertions.
+ // Sync up the database while stepping through the lists.
+ function fullScan() {
+ if (media.state !== MediaDB.READY) {
+ endscan(media);
+ return;
+ }
+
+ // The db may be busy right about now, processing files that
+ // were found during the quick scan. So we'll start off by
+ // enumerating all files in device storage
+ var dsfiles = [];
+ var cursor = media.storage.enumerate(media.directory);
+ cursor.onsuccess = function() {
+ var file = cursor.result;
+ if (file) {
+ if (!ignore(media, file)) {
+ dsfiles.push(file);
+ }
+ cursor.continue();
+ }
+ else {
+ // We're done enumerating device storage, so get all files from db
+ getDBFiles();
+ }
+ }
+
+ cursor.onerror = function() {
+ // We can't scan if we can't read device storage.
+ // Perhaps the card was unmounted or pulled out
+ console.warning('Error while scanning', cursor.error);
+ endscan(media);
+ };
+
+ function getDBFiles() {
+ var store = media.db.transaction('files').objectStore('files');
+ var getAllRequest = store.mozGetAll();
+
+ getAllRequest.onsuccess = function() {
+ var dbfiles = getAllRequest.result; // Should already be sorted
+ compareLists(dbfiles, dsfiles);
+ };
+ }
+
+ function compareLists(dbfiles, dsfiles) {
+ // The dbfiles are sorted when we get them from the db.
+ // But the ds files are not sorted
+ dsfiles.sort(function(a, b) {
+ if (a.name < b.name)
+ return -1;
+ else
+ return 1;
+ });
+
+ // Loop through both the dsfiles and dbfiles lists
+ var dsindex = 0, dbindex = 0;
+ while (true) {
+ // Get the next DeviceStorage file or null
+ var dsfile;
+ if (dsindex < dsfiles.length)
+ dsfile = dsfiles[dsindex];
+ else
+ dsfile = null;
+
+ // Get the next DB file or null
+ var dbfile;
+ if (dbindex < dbfiles.length)
+ dbfile = dbfiles[dbindex];
+ else
+ dbfile = null;
+
+ // Case 1: both files are null. If so, we're done.
+ if (dsfile === null && dbfile === null)
+ break;
+
+ // Case 2: no more files in the db. This means that
+ // the file from ds is a new one
+ if (dbfile === null) {
+ insertRecord(media, dsfile);
+ dsindex++;
+ continue;
+ }
+
+ // Case 3: no more files in ds. This means that the db file
+ // has been deleted
+ if (dsfile === null) {
+ deleteRecord(media, dbfile.name);
+ dbindex++;
+ continue;
+ }
+
+ // Case 4: two files with the same name.
+ // 4a: date and size are the same for both: do nothing
+ // 4b: file has changed: it is both a deletion and a creation
+ if (dsfile.name === dbfile.name) {
+ var lastModified = dsfile.lastModifiedDate;
+ if ((lastModified && lastModified.getTime() !== dbfile.date) ||
+ dsfile.size !== dbfile.size) {
+ deleteRecord(media, dbfile.name);
+ insertRecord(media, dsfile);
+ }
+ dsindex++;
+ dbindex++;
+ continue;
+ }
+
+ // Case 5: the dsfile name is less than the dbfile name.
+ // This means that the dsfile is new. Like case 2
+ if (dsfile.name < dbfile.name) {
+ insertRecord(media, dsfile);
+ dsindex++;
+ continue;
+ }
+
+ // Case 6: the dsfile name is greater than the dbfile name.
+ // this means that the dbfile no longer exists on disk
+ if (dsfile.name > dbfile.name) {
+ deleteRecord(media, dbfile.name);
+ dbindex++;
+ continue;
+ }
+
+ // That should be an exhaustive set of possiblities
+ // and we should never reach this point.
+ console.error('Assertion failed');
+ }
+
+ // Push a special value onto the queue so that when it is
+ // processed we can trigger a 'scanend' event
+ insertRecord(media, null);
+ }
+ }
+ }
+
+ // Called to send out a scanend event when scanning is done.
+ // This event is sent on normal scan termination and also
+ // when something goes wrong, such as the device storage being
+ // unmounted during a scan.
+ function endscan(media) {
+ if (media.scanning) {
+ media.scanning = false;
+ dispatchEvent(media, 'scanend');
+ }
+ }
+
+ // Pass in a file, or a filename. The function queues it up for
+ // metadata parsing and insertion into the database, and will send a
+ // mediadb change event (possibly batched with other changes).
+ // Ensures that only one file is being parsed at a time, but tries
+ // to make as many db changes in one transaction as possible. The
+ // special value null indicates that scanning is complete. If the
+ // 2nd argument is a File, it should come from enumerate() so that
+ // the name property does not include the directory prefix. If it
+ // is a name, then the directory prefix must already have been
+ // stripped.
+ function insertRecord(media, fileOrName) {
+ var details = media.details;
+
+ // Add this file to the queue of files to process
+ details.pendingInsertions.push(fileOrName);
+
+ // If the queue is already being processed, just return
+ if (details.processingQueue)
+ return;
+
+ // Otherwise, start processing the queue.
+ processQueue(media);
+ }
+
+ // Delete the database record associated with filename.
+ // filename must not include the directory prefix.
+ function deleteRecord(media, filename) {
+ var details = media.details;
+
+ // Add this file to the queue of files to process
+ details.pendingDeletions.push(filename);
+
+ // If there is already a transaction in progress return now.
+ if (details.processingQueue)
+ return;
+
+ // Otherwise, start processing the queue
+ processQueue(media);
+ }
+
+ function whenDoneProcessing(media, f) {
+ var details = media.details;
+ if (details.processingQueue)
+ details.whenDoneProcessing.push(f);
+ else
+ f();
+ }
+
+ function processQueue(media) {
+ var details = media.details;
+
+ details.processingQueue = true;
+
+ // Now get one filename off a queue and store it
+ next();
+
+ // Take an item from a queue and process it.
+ // Deletions are always processed before insertions because we want
+ // to clear away non-functional parts of the UI ASAP.
+ function next() {
+ if (details.pendingDeletions.length > 0) {
+ deleteFiles();
+ }
+ else if (details.pendingInsertions.length > 0) {
+ insertFile(details.pendingInsertions.shift());
+ }
+ else {
+ details.processingQueue = false;
+ if (details.whenDoneProcessing.length > 0) {
+ var functions = details.whenDoneProcessing;
+ details.whenDoneProcessing = [];
+ functions.forEach(function(f) { f(); });
+ }
+ }
+ }
+
+ // Delete all of the pending files in a single transaction
+ function deleteFiles() {
+ var transaction = media.db.transaction('files', 'readwrite');
+ var store = transaction.objectStore('files');
+
+ deleteNextFile();
+
+ function deleteNextFile() {
+ if (details.pendingDeletions.length === 0) {
+ next();
+ return;
+ }
+ var filename = details.pendingDeletions.shift();
+ var request = store.delete(filename);
+ request.onerror = function() {
+ // This probably means that the file wasn't in the db yet
+ console.warn('MediaDB: Unknown file in deleteRecord:',
+ filename, getreq.error);
+ deleteNextFile();
+ };
+ request.onsuccess = function() {
+ // We succeeded, so remember to send out an event about it.
+ queueDeleteNotification(media, filename);
+ deleteNextFile();
+ };
+ }
+ }
+
+ // Insert a file into the db. One transaction per insertion.
+ // The argument might be a filename or a File object
+ // If it is a File, then it came from enumerate and its name
+ // property already has the directory stripped off. If it is a
+ // filename, it came from a device storage change event and we
+ // stripped of the directory before calling insertRecord.
+ function insertFile(f) {
+ // null is a special value pushed on to the queue when a scan()
+ // is complete. We use it to trigger a scanend event
+ // after all the change events from the scan are delivered
+ if (f === null) {
+ sendNotifications(media);
+ endscan(media);
+ next();
+ return;
+ }
+
+ // If we got a filename, look up the file in device storage
+ if (typeof f === 'string') {
+ var getreq = media.storage.get(media.directory + f);
+ getreq.onerror = function() {
+ console.warn('MediaDB: Unknown file in insertRecord:',
+ media.directory + f, getreq.error);
+ next();
+ };
+ getreq.onsuccess = function() {
+ parseMetadata(getreq.result, f);
+ };
+ }
+ else {
+ // otherwise f is the file we want
+ parseMetadata(f, f.name);
+ }
+ }
+
+ function parseMetadata(file, filename) {
+ if (!file.lastModifiedDate) {
+ console.warn('MediaDB: parseMetadata: no lastModifiedDate for',
+ filename,
+ 'using Date.now() until #793955 is fixed');
+ }
+
+ // Basic information about the file
+ var fileinfo = {
+ name: filename, // we can't trust file.name
+ type: file.type,
+ size: file.size,
+ date: file.lastModifiedDate ?
+ file.lastModifiedDate.getTime() :
+ Date.now()
+ };
+
+ if (fileinfo.date > details.newestFileModTime)
+ details.newestFileModTime = fileinfo.date;
+
+ // Get metadata about the file
+ media.metadataParser(file, gotMetadata, metadataError);
+ function metadataError(e) {
+ console.warn('MediaDB: error parsing metadata for',
+ filename, ':', e);
+ // If we get an error parsing the metadata, assume it is invalid
+ // and make a note in the fileinfo record that we store in the database
+ // If we don't store it in the database, we'll keep finding it
+ // on every scan. But we make sure never to return the invalid file
+ // on an enumerate call.
+ fileinfo.fail = true;
+ storeRecord(fileinfo);
+ }
+ function gotMetadata(metadata) {
+ fileinfo.metadata = metadata;
+ storeRecord(fileinfo);
+ }
+ }
+
+ function storeRecord(fileinfo) {
+ if (media.details.firstscan) {
+ // If this is the first scan then we know this is a new file and
+ // we can assume that adding it to the db will succeed.
+ // So we can just queue a notification about the new file without
+ // waiting for a db operation.
+ media.details.records.push(fileinfo);
+ if (!fileinfo.fail) {
+ queueCreateNotification(media, fileinfo);
+ }
+ // And go on to the next
+ next();
+ }
+ else {
+ // If this is not the first scan, then we may already have a db
+ // record for this new file. In that case, the call to add() above
+ // is going to fail. We need to handle that case, so we can't send
+ // out the new file notification until we get a response to the add().
+ var transaction = media.db.transaction('files', 'readwrite');
+ var store = transaction.objectStore('files');
+ var request = store.add(fileinfo);
+
+ request.onsuccess = function() {
+ // Remember to send an event about this new file
+ if (!fileinfo.fail)
+ queueCreateNotification(media, fileinfo);
+ // And go on to the next
+ next();
+ };
+ request.onerror = function(event) {
+ // If the error name is 'ConstraintError' it means that the
+ // file already exists in the database. So try again, using put()
+ // instead of add(). If that succeeds, then queue a delete
+ // notification along with the insert notification. If the
+ // second try fails, or if the error was something different
+ // then issue a warning and continue with the next.
+ if (request.error.name === 'ConstraintError') {
+ // Don't let the higher-level DB error handler report the error
+ event.stopPropagation();
+ // And don't spew a default error message to the console either
+ event.preventDefault();
+ var putrequest = store.put(fileinfo);
+ putrequest.onsuccess = function() {
+ queueDeleteNotification(media, fileinfo.name);
+ if (!fileinfo.fail)
+ queueCreateNotification(media, fileinfo);
+ next();
+ };
+ putrequest.onerror = function() {
+ // Report and move on
+ console.error('MediaDB: unexpected ConstraintError',
+ 'in insertRecord for file:', fileinfo.name);
+ next();
+ };
+ }
+ else {
+ // Something unexpected happened!
+ // All we can do is report it and move on
+ console.error('MediaDB: unexpected error in insertRecord:',
+ request.error, 'for file:', fileinfo.name);
+ next();
+ }
+ };
+ }
+ }
+ }
+
+ // Don't send out notification events right away. Wait a short time to
+ // see if others arrive that we can batch up. This is common for scanning
+ function queueCreateNotification(media, fileinfo) {
+ var creates = media.details.pendingCreateNotifications;
+ creates.push(fileinfo);
+ if (media.batchSize && creates.length >= media.batchSize)
+ sendNotifications(media);
+ else
+ resetNotificationTimer(media);
+ }
+
+ function queueDeleteNotification(media, filename) {
+ var deletes = media.details.pendingDeleteNotifications;
+ deletes.push(filename);
+ if (media.batchSize && deletes.length >= media.batchSize)
+ sendNotifications(media);
+ else
+ resetNotificationTimer(media);
+ }
+
+ function resetNotificationTimer(media) {
+ var details = media.details;
+ if (details.pendingNotificationTimer)
+ clearTimeout(details.pendingNotificationTimer);
+ details.pendingNotificationTimer =
+ setTimeout(function() { sendNotifications(media); },
+ media.batchHoldTime);
+ }
+
+ // Send out notifications for creations and deletions
+ function sendNotifications(media) {
+ var details = media.details;
+ if (details.pendingNotificationTimer) {
+ clearTimeout(details.pendingNotificationTimer);
+ details.pendingNotificationTimer = null;
+ }
+ if (details.pendingDeleteNotifications.length > 0) {
+ var deletions = details.pendingDeleteNotifications;
+ details.pendingDeleteNotifications = [];
+ dispatchEvent(media, 'deleted', deletions);
+ }
+
+ if (details.pendingCreateNotifications.length > 0) {
+
+ // If this is a first scan, and we have records that are not
+ // in the db yet, write them to the db now
+ if (details.firstscan && details.records.length > 0) {
+ var transaction = media.db.transaction('files', 'readwrite');
+ var store = transaction.objectStore('files');
+ for (var i = 0; i < details.records.length; i++)
+ store.add(details.records[i]);
+ details.records.length = 0;
+ }
+
+ var creations = details.pendingCreateNotifications;
+ details.pendingCreateNotifications = [];
+ dispatchEvent(media, 'created', creations);
+ }
+ }
+
+ function dispatchEvent(media, type, detail) {
+ var handler = media['on' + type];
+ var listeners = media.details.eventListeners[type];
+
+ // Return if there is nothing to handle the event
+ if (!handler && (!listeners || listeners.length == 0))
+ return;
+
+ // We use a fake event object
+ var event = {
+ type: type,
+ target: media,
+ currentTarget: media,
+ timestamp: Date.now(),
+ detail: detail
+ };
+
+ // Call the 'on' handler property if there is one
+ if (typeof handler === 'function') {
+ try {
+ handler.call(media, event);
+ }
+ catch (e) {
+ console.warn('MediaDB: ', 'on' + type, 'event handler threw', e);
+ }
+ }
+
+ // Now call the listeners if there are any
+ if (!listeners)
+ return;
+ for (var i = 0; i < listeners.length; i++) {
+ try {
+ var listener = listeners[i];
+ if (typeof listener === 'function') {
+ listener.call(media, event);
+ }
+ else {
+ listener.handleEvent(event);
+ }
+ }
+ catch (e) {
+ console.warn('MediaDB: ', type, 'event listener threw', e);
+ }
+ }
+ }
+
+ function changeState(media, state) {
+ if (media.state !== state) {
+ media.state = state;
+ if (state === MediaDB.READY)
+ dispatchEvent(media, 'ready');
+ else
+ dispatchEvent(media, 'unavailable', state);
+ }
+ }
+
+ return MediaDB;
+
+}());
diff --git a/shared/js/mobile_operator.js b/shared/js/mobile_operator.js
new file mode 100644
index 0000000..edd2b44
--- /dev/null
+++ b/shared/js/mobile_operator.js
@@ -0,0 +1,96 @@
+'use strict';
+
+var MobileOperator = {
+ BRAZIL_MCC: 724,
+ BRAZIL_CELLBROADCAST_CHANNEL: 50,
+
+ userFacingInfo: function mo_userFacingInfo(mobileConnection) {
+ var network = mobileConnection.voice.network;
+ var iccInfo = mobileConnection.iccInfo;
+ var operator = network.shortName || network.longName;
+
+ if (iccInfo.isDisplaySpnRequired && iccInfo.spn
+ && !mobileConnection.voice.roaming) {
+ if (iccInfo.isDisplayNetworkNameRequired) {
+ operator = operator + ' ' + iccInfo.spn;
+ } else {
+ operator = iccInfo.spn;
+ }
+ }
+
+ var carrier, region;
+ if (this.isBrazil(mobileConnection)) {
+ // We are in Brazil, It is legally required to show local info
+ // about current registered GSM network in a legally specified way.
+ var lac = mobileConnection.voice.cell.gsmLocationAreaCode % 100;
+ var carriers = MobileInfo.brazil.carriers;
+ var regions = MobileInfo.brazil.regions;
+
+ carrier = carriers[network.mnc] || (this.BRAZIL_MCC.toString() + network.mnc);
+ region = (regions[lac] ? regions[lac] + ' ' + lac : '');
+ }
+
+ return {
+ 'operator': operator,
+ 'carrier': carrier,
+ 'region': region
+ };
+ },
+
+ isBrazil: function mo_isBrazil(mobileConnection) {
+ var cell = mobileConnection.voice.cell;
+ return mobileConnection.voice.network.mcc == this.BRAZIL_MCC &&
+ cell && cell.gsmLocationAreaCode;
+ }
+};
+
+
+var MobileInfo = {
+ brazil: {
+ carriers: {
+ '0': 'NEXTEL',
+ '2': 'TIM', '3': 'TIM', '4': 'TIM',
+ '5': 'CLARO', '6': 'VIVO', '7': 'CTBC', '8': 'TIM',
+ '10': 'VIVO', '11': 'VIVO', '15': 'SERCOMTEL',
+ '16': 'OI', '23': 'VIVO', '24': 'OI', '31': 'OI',
+ '32': 'CTBC', '33': 'CTBC', '34': 'CTBC', '37': 'AEIOU'
+ },
+ regions: {
+ '11': 'SP', '12': 'SP', '13': 'SP', '14': 'SP', '15': 'SP', '16': 'SP',
+ '17': 'SP', '18': 'SP', '19': 'SP',
+ '21': 'RJ', '22': 'RJ', '24': 'RJ',
+ '27': 'ES', '28': 'ES',
+ '31': 'MG', '32': 'MG', '33': 'MG', '34': 'MG', '35': 'MG', '37': 'MG',
+ '38': 'MG',
+ '41': 'PR', '42': 'PR', '43': 'PR', '44': 'PR', '45': 'PR', '46': 'PR',
+ '47': 'SC', '48': 'SC', '49': 'SC',
+ '51': 'RS', '53': 'RS', '54': 'RS', '55': 'RS',
+ '61': 'DF',
+ '62': 'GO',
+ '63': 'TO',
+ '64': 'GO',
+ '65': 'MT', '66': 'MT',
+ '67': 'MS',
+ '68': 'AC',
+ '69': 'RO',
+ '71': 'BA', '73': 'BA', '74': 'BA', '75': 'BA', '77': 'BA',
+ '79': 'SE',
+ '81': 'PE',
+ '82': 'AL',
+ '83': 'PB',
+ '84': 'RN',
+ '85': 'CE',
+ '86': 'PI',
+ '87': 'PE',
+ '88': 'CE',
+ '89': 'PI',
+ '91': 'PA',
+ '92': 'AM',
+ '93': 'PA', '94': 'PA',
+ '95': 'RR',
+ '96': 'AP',
+ '97': 'AM',
+ '98': 'MA', '99': 'MA'
+ }
+ }
+};
diff --git a/shared/js/mouse_event_shim.js b/shared/js/mouse_event_shim.js
new file mode 100644
index 0000000..053ef7f
--- /dev/null
+++ b/shared/js/mouse_event_shim.js
@@ -0,0 +1,282 @@
+/**
+ * mouse_event_shim.js: generate mouse events from touch events.
+ *
+ * This library listens for touch events and generates mousedown, mousemove
+ * mouseup, and click events to match them. It captures and dicards any
+ * real mouse events (non-synthetic events with isTrusted true) that are
+ * send by gecko so that there are not duplicates.
+ *
+ * This library does emit mouseover/mouseout and mouseenter/mouseleave
+ * events. You can turn them off by setting MouseEventShim.trackMouseMoves to
+ * false. This means that mousemove events will always have the same target
+ * as the mousedown even that began the series. You can also call
+ * MouseEventShim.setCapture() from a mousedown event handler to prevent
+ * mouse tracking until the next mouseup event.
+ *
+ * This library does not support multi-touch but should be sufficient
+ * to do drags based on mousedown/mousemove/mouseup events.
+ *
+ * This library does not emit dblclick events or contextmenu events
+ */
+
+'use strict';
+
+(function() {
+ // Make sure we don't run more than once
+ if (MouseEventShim)
+ return;
+
+ // Bail if we're not on running on a platform that sends touch
+ // events. We don't need the shim code for mouse events.
+ try {
+ document.createEvent('TouchEvent');
+ } catch (e) {
+ return;
+ }
+
+ var starttouch; // The Touch object that we started with
+ var target; // The element the touch is currently over
+ var emitclick; // Will we be sending a click event after mouseup?
+
+ // Use capturing listeners to discard all mouse events from gecko
+ window.addEventListener('mousedown', discardEvent, true);
+ window.addEventListener('mouseup', discardEvent, true);
+ window.addEventListener('mousemove', discardEvent, true);
+ window.addEventListener('click', discardEvent, true);
+
+ function discardEvent(e) {
+ if (e.isTrusted) {
+ e.stopImmediatePropagation(); // so it goes no further
+ if (e.type === 'click')
+ e.preventDefault(); // so it doesn't trigger a change event
+ }
+ }
+
+ // Listen for touch events that bubble up to the window.
+ // If other code has called stopPropagation on the touch events
+ // then we'll never see them. Also, we'll honor the defaultPrevented
+ // state of the event and will not generate synthetic mouse events
+ window.addEventListener('touchstart', handleTouchStart);
+ window.addEventListener('touchmove', handleTouchMove);
+ window.addEventListener('touchend', handleTouchEnd);
+ window.addEventListener('touchcancel', handleTouchEnd); // Same as touchend
+
+ function handleTouchStart(e) {
+ // If we're already handling a touch, ignore this one
+ if (starttouch)
+ return;
+
+ // Ignore any event that has already been prevented
+ if (e.defaultPrevented)
+ return;
+
+ // Sometimes an unknown gecko bug causes us to get a touchstart event
+ // for an iframe target that we can't use because it is cross origin.
+ // Don't start handling a touch in that case
+ try {
+ e.changedTouches[0].target.ownerDocument;
+ }
+ catch (e) {
+ // Ignore the event if we can't see the properties of the target
+ return;
+ }
+
+ // If there is more than one simultaneous touch, ignore all but the first
+ starttouch = e.changedTouches[0];
+ target = starttouch.target;
+ emitclick = true;
+
+ // Move to the position of the touch
+ emitEvent('mousemove', target, starttouch);
+
+ // Now send a synthetic mousedown
+ var result = emitEvent('mousedown', target, starttouch);
+
+ // If the mousedown was prevented, pass that on to the touch event.
+ // And remember not to send a click event
+ if (!result) {
+ e.preventDefault();
+ emitclick = false;
+ }
+ }
+
+ function handleTouchEnd(e) {
+ if (!starttouch)
+ return;
+
+ // End a MouseEventShim.setCapture() call
+ if (MouseEventShim.capturing) {
+ MouseEventShim.capturing = false;
+ MouseEventShim.captureTarget = null;
+ }
+
+ for (var i = 0; i < e.changedTouches.length; i++) {
+ var touch = e.changedTouches[i];
+ // If the ended touch does not have the same id, skip it
+ if (touch.identifier !== starttouch.identifier)
+ continue;
+
+ emitEvent('mouseup', target, touch);
+
+ // If target is still the same element we started and the touch did not
+ // move more than the threshold and if the user did not prevent
+ // the mousedown, then send a click event, too.
+ if (emitclick)
+ emitEvent('click', starttouch.target, touch);
+
+ starttouch = null;
+ return;
+ }
+ }
+
+ function handleTouchMove(e) {
+ if (!starttouch)
+ return;
+
+ for (var i = 0; i < e.changedTouches.length; i++) {
+ var touch = e.changedTouches[i];
+ // If the ended touch does not have the same id, skip it
+ if (touch.identifier !== starttouch.identifier)
+ continue;
+
+ // Don't send a mousemove if the touchmove was prevented
+ if (e.defaultPrevented)
+ return;
+
+ // See if we've moved too much to emit a click event
+ var dx = Math.abs(touch.screenX - starttouch.screenX);
+ var dy = Math.abs(touch.screenY - starttouch.screenY);
+ if (dx > MouseEventShim.dragThresholdX ||
+ dy > MouseEventShim.dragThresholdY) {
+ emitclick = false;
+ }
+
+ var tracking = MouseEventShim.trackMouseMoves &&
+ !MouseEventShim.capturing;
+
+ if (tracking) {
+ // If the touch point moves, then the element it is over
+ // may have changed as well. Note that calling elementFromPoint()
+ // forces a layout if one is needed.
+ // XXX: how expensive is it to do this on each touchmove?
+ // Can we listen for (non-standard) touchleave events instead?
+ var oldtarget = target;
+ var newtarget = document.elementFromPoint(touch.clientX, touch.clientY);
+ if (newtarget === null) {
+ // this can happen as the touch is moving off of the screen, e.g.
+ newtarget = oldtarget;
+ }
+ if (newtarget !== oldtarget) {
+ leave(oldtarget, newtarget, touch); // mouseout, mouseleave
+ target = newtarget;
+ }
+ }
+ else if (MouseEventShim.captureTarget) {
+ target = MouseEventShim.captureTarget;
+ }
+
+ emitEvent('mousemove', target, touch);
+
+ if (tracking && newtarget !== oldtarget) {
+ enter(newtarget, oldtarget, touch); // mouseover, mouseenter
+ }
+ }
+ }
+
+ // Return true if element a contains element b
+ function contains(a, b) {
+ return (a.compareDocumentPosition(b) & 16) !== 0;
+ }
+
+ // A touch has left oldtarget and entered newtarget
+ // Send out all the events that are required
+ function leave(oldtarget, newtarget, touch) {
+ emitEvent('mouseout', oldtarget, touch, newtarget);
+
+ // If the touch has actually left oldtarget (and has not just moved
+ // into a child of oldtarget) send a mouseleave event. mouseleave
+ // events don't bubble, so we have to repeat this up the hierarchy.
+ for (var e = oldtarget; !contains(e, newtarget); e = e.parentNode) {
+ emitEvent('mouseleave', e, touch, newtarget);
+ }
+ }
+
+ // A touch has entered newtarget from oldtarget
+ // Send out all the events that are required.
+ function enter(newtarget, oldtarget, touch) {
+ emitEvent('mouseover', newtarget, touch, oldtarget);
+
+ // Emit non-bubbling mouseenter events if the touch actually entered
+ // newtarget and wasn't already in some child of it
+ for (var e = newtarget; !contains(e, oldtarget); e = e.parentNode) {
+ emitEvent('mouseenter', e, touch, oldtarget);
+ }
+ }
+
+ function emitEvent(type, target, touch, relatedTarget) {
+ var synthetic = document.createEvent('MouseEvents');
+ var bubbles = (type !== 'mouseenter' && type !== 'mouseleave');
+ var count =
+ (type === 'mousedown' || type === 'mouseup' || type === 'click') ? 1 : 0;
+
+ synthetic.initMouseEvent(type,
+ bubbles, // canBubble
+ true, // cancelable
+ window,
+ count, // detail: click count
+ touch.screenX,
+ touch.screenY,
+ touch.clientX,
+ touch.clientY,
+ false, // ctrlKey: we don't have one
+ false, // altKey: we don't have one
+ false, // shiftKey: we don't have one
+ false, // metaKey: we don't have one
+ 0, // we're simulating the left button
+ relatedTarget || null);
+
+ try {
+ return target.dispatchEvent(synthetic);
+ }
+ catch (e) {
+ console.warn('Exception calling dispatchEvent', type, e);
+ return true;
+ }
+ }
+}());
+
+var MouseEventShim = {
+ // It is a known gecko bug that synthetic events have timestamps measured
+ // in microseconds while regular events have timestamps measured in
+ // milliseconds. This utility function returns a the timestamp converted
+ // to milliseconds, if necessary.
+ getEventTimestamp: function(e) {
+ if (e.isTrusted) // XXX: Are real events always trusted?
+ return e.timeStamp;
+ else
+ return e.timeStamp / 1000;
+ },
+
+ // Set this to false if you don't care about mouseover/out events
+ // and don't want the target of mousemove events to follow the touch
+ trackMouseMoves: true,
+
+ // Call this function from a mousedown event handler if you want to guarantee
+ // that the mousemove and mouseup events will go to the same element
+ // as the mousedown even if they leave the bounds of the element. This is
+ // like setting trackMouseMoves to false for just one drag. It is a
+ // substitute for event.target.setCapture(true)
+ setCapture: function(target) {
+ this.capturing = true; // Will be set back to false on mouseup
+ if (target)
+ this.captureTarget = target;
+ },
+
+ capturing: false,
+
+ // Keep these in sync with ui.dragThresholdX and ui.dragThresholdY prefs.
+ // If a touch ever moves more than this many pixels from its starting point
+ // then we will not synthesize a click event when the touch ends.
+ dragThresholdX: 25,
+ dragThresholdY: 25
+};
diff --git a/shared/js/notification_helper.js b/shared/js/notification_helper.js
new file mode 100644
index 0000000..384fbfc
--- /dev/null
+++ b/shared/js/notification_helper.js
@@ -0,0 +1,68 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+/**
+ * Keeping a reference on all active notifications to avoid weird GC issues.
+ * See https://bugzilla.mozilla.org/show_bug.cgi?id=755402
+ */
+
+var NotificationHelper = {
+ _referencesArray: [],
+
+ getIconURI: function nc_getIconURI(app, entryPoint) {
+ var icons = app.manifest.icons;
+
+ if (entryPoint) {
+ icons = app.manifest.entry_points[entryPoint].icons;
+ }
+
+ if (!icons)
+ return null;
+
+ var sizes = Object.keys(icons).map(function parse(str) {
+ return parseInt(str, 10);
+ });
+ sizes.sort(function(x, y) { return y - x; });
+
+ var HVGA = document.documentElement.clientWidth < 480;
+ var index = sizes[HVGA ? sizes.length - 1 : 0];
+ return app.installOrigin + icons[index];
+ },
+
+ send: function nc_send(title, body, icon, clickCB, closeCB) {
+ if (!('mozNotification' in navigator))
+ return;
+
+ var notification = navigator.mozNotification.createNotification(title,
+ body, icon);
+
+ notification.onclick = (function() {
+ if (clickCB)
+ clickCB();
+
+ this._forget(notification);
+ }).bind(this);
+
+ notification.onclose = (function() {
+ if (closeCB)
+ closeCB();
+
+ this._forget(notification);
+ }).bind(this);
+
+ notification.show();
+ this._keep(notification);
+ },
+
+ _keep: function nc_keep(notification) {
+ this._referencesArray.push(notification);
+ },
+ _forget: function nc_forget(notification) {
+ this._referencesArray.splice(
+ this._referencesArray.indexOf(notification), 1
+ );
+ }
+};
+
diff --git a/shared/js/phoneNumberJS/PhoneNumber.js b/shared/js/phoneNumberJS/PhoneNumber.js
new file mode 100644
index 0000000..16f9e80
--- /dev/null
+++ b/shared/js/phoneNumberJS/PhoneNumber.js
@@ -0,0 +1,335 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+var PhoneNumber = (function (dataBase) {
+ // Use strict in our context only - users might not want it
+ 'use strict';
+
+ const UNICODE_DIGITS = /[\uFF10-\uFF19\u0660-\u0669\u06F0-\u06F9]/g;
+ const ALPHA_CHARS = /[a-zA-Z]/g;
+ const NON_ALPHA_CHARS = /[^a-zA-Z]/g;
+ const NON_DIALABLE_CHARS = /[^+\*\d]/g;
+ const PLUS_CHARS = /^[+\uFF0B]+/;
+ const BACKSLASH = /\\/g;
+ const COMMACOMMA = /,,/g;
+ const COMMABRACKET = /,]/g;
+ const SPLIT_FIRST_GROUP = /^(\d+)(.*)$/;
+
+ // Format of the string encoded meta data. If the name contains "^" or "$"
+ // we will generate a regular expression from the value, with those special
+ // characters as prefix/suffix.
+ const META_DATA_ENCODING = ["region",
+ "^internationalPrefix",
+ "nationalPrefix",
+ "^nationalPrefixForParsing",
+ "nationalPrefixTransformRule",
+ "nationalPrefixFormattingRule",
+ "^possiblePattern$",
+ "^nationalPattern$",
+ "formats"];
+
+ const FORMAT_ENCODING = ["^pattern$",
+ "nationalFormat",
+ "^leadingDigits",
+ "nationalPrefixFormattingRule",
+ "internationalFormat"];
+
+ var regionCache = Object.create(null);
+
+ // Parse an array of strings into a convenient object. We store meta
+ // data as arrays since thats much more compact than JSON.
+ function ParseArray(array, encoding, obj) {
+ for (var n = 0; n < encoding.length; ++n) {
+ var value = array[n];
+ if (!value)
+ continue;
+ var field = encoding[n];
+ var fieldAlpha = field.replace(NON_ALPHA_CHARS, "");
+ if (field != fieldAlpha)
+ value = new RegExp(field.replace(fieldAlpha, value));
+ obj[fieldAlpha] = value;
+ }
+ return obj;
+ }
+
+ // Parse string encoded meta data into a convenient object
+ // representation.
+ function ParseMetaData(countryCode, md) {
+ var array = JSON.parse(md.replace(BACKSLASH, "\\\\").replace(COMMACOMMA, ', null,').replace(COMMACOMMA, ', null,').replace(COMMABRACKET, ', null]'));
+ md = ParseArray(array,
+ META_DATA_ENCODING,
+ { countryCode: countryCode });
+ regionCache[md.region] = md;
+ return md;
+ }
+
+ // Parse string encoded format data into a convenient object
+ // representation.
+ function ParseFormat(md) {
+ var formats = md.formats;
+ // Bail if we already parsed the format definitions.
+ if (!(Array.isArray(formats[0])))
+ return;
+ for (var n = 0; n < formats.length; ++n) {
+ formats[n] = ParseArray(formats[n],
+ FORMAT_ENCODING,
+ {});
+ }
+ }
+
+ // Search for the meta data associated with a region identifier ("US") in
+ // our database, which is indexed by country code ("1"). Since we have
+ // to walk the entire database for this, we cache the result of the lookup
+ // for future reference.
+ function FindMetaDataForRegion(region) {
+ // Check in the region cache first. This will find all entries we have
+ // already resolved (parsed from a string encoding).
+ var md = regionCache[region];
+ if (md)
+ return md;
+ for (var countryCode in dataBase) {
+ var entry = dataBase[countryCode];
+ // Each entry is a string encoded object of the form '["US..', or
+ // an array of strings. We don't want to parse the string here
+ // to save memory, so we just substring the region identifier
+ // and compare it. For arrays, we compare against all region
+ // identifiers with that country code. We skip entries that are
+ // of type object, because they were already resolved (parsed into
+ // an object), and their country code should have been in the cache.
+ if (Array.isArray(entry)) {
+ for (var n = 0; n < entry.length; ++n) {
+ if (typeof entry[n] == "string" && entry[n].substr(2,2) == region)
+ return entry[n] = ParseMetaData(countryCode, entry[n]);
+ }
+ continue;
+ }
+ if (typeof entry == "string" && entry.substr(2,2) == region)
+ return dataBase[countryCode] = ParseMetaData(countryCode, entry);
+ }
+ }
+
+ // Format a national number for a given region. The boolean flag "intl"
+ // indicates whether we want the national or international format.
+ function FormatNumber(regionMetaData, number, intl) {
+ // We lazily parse the format description in the meta data for the region,
+ // so make sure to parse it now if we haven't already done so.
+ ParseFormat(regionMetaData);
+ var formats = regionMetaData.formats;
+ for (var n = 0; n < formats.length; ++n) {
+ var format = formats[n];
+ // The leading digits field is optional. If we don't have it, just
+ // use the matching pattern to qualify numbers.
+ if (format.leadingDigits && !format.leadingDigits.test(number))
+ continue;
+ if (!format.pattern.test(number))
+ continue;
+ if (intl) {
+ // If there is no international format, just fall back to the national
+ // format.
+ var internationalFormat = format.internationalFormat;
+ if (!internationalFormat)
+ internationalFormat = format.nationalFormat;
+ // Some regions have numbers that can't be dialed from outside the
+ // country, indicated by "NA" for the international format of that
+ // number format pattern.
+ if (internationalFormat == "NA")
+ return null;
+ // Prepend "+" and the country code.
+ number = "+" + regionMetaData.countryCode + " " +
+ number.replace(format.pattern, internationalFormat);
+ } else {
+ number = number.replace(format.pattern, format.nationalFormat);
+ // The region has a national prefix formatting rule, and it can be overwritten
+ // by each actual number format rule.
+ var nationalPrefixFormattingRule = regionMetaData.nationalPrefixFormattingRule;
+ if (format.nationalPrefixFormattingRule)
+ nationalPrefixFormattingRule = format.nationalPrefixFormattingRule;
+ if (nationalPrefixFormattingRule) {
+ // The prefix formatting rule contains two magic markers, "$NP" and "$FG".
+ // "$NP" will be replaced by the national prefix, and "$FG" with the
+ // first group of numbers.
+ var match = number.match(SPLIT_FIRST_GROUP);
+ var firstGroup = match[1];
+ var rest = match[2];
+ var prefix = nationalPrefixFormattingRule;
+ prefix = prefix.replace("$NP", regionMetaData.nationalPrefix);
+ prefix = prefix.replace("$FG", firstGroup);
+ number = prefix + rest;
+ }
+ }
+ return (number == "NA") ? null : number;
+ }
+ return null;
+ }
+
+ function NationalNumber(regionMetaData, number) {
+ this.region = regionMetaData.region;
+ this.regionMetaData = regionMetaData;
+ this.nationalNumber = number;
+ }
+
+ // NationalNumber represents the result of parsing a phone number. We have
+ // three getters on the prototype that format the number in national and
+ // international format. Once called, the getters put a direct property
+ // onto the object, caching the result.
+ NationalNumber.prototype = {
+ // +1 949-726-2896
+ get internationalFormat() {
+ var value = FormatNumber(this.regionMetaData, this.nationalNumber, true);
+ Object.defineProperty(this, "internationalFormat", { value: value, enumerable: true });
+ return value;
+ },
+ // (949) 726-2896
+ get nationalFormat() {
+ var value = FormatNumber(this.regionMetaData, this.nationalNumber, false);
+ Object.defineProperty(this, "nationalFormat", { value: value, enumerable: true });
+ return value;
+ },
+ // +19497262896
+ get internationalNumber() {
+ var value = this.internationalFormat.replace(NON_DIALABLE_CHARS, "");
+ Object.defineProperty(this, "nationalNumber", { value: value, enumerable: true });
+ return value;
+ }
+ };
+
+ // Normalize a number by converting unicode numbers and symbols to their
+ // ASCII equivalents and removing all non-dialable characters.
+ function NormalizeNumber(number) {
+ number = number.replace(UNICODE_DIGITS,
+ function (ch) {
+ return String.fromCharCode(48 + (ch.charCodeAt(0) & 0xf));
+ });
+ number = number.replace(ALPHA_CHARS,
+ function (ch) {
+ return (ch.toLowerCase().charCodeAt(0) - 97)/3+2 | 0;
+ });
+ number = number.replace(PLUS_CHARS, "+");
+ number = number.replace(NON_DIALABLE_CHARS, "");
+ return number;
+ }
+
+ // Check whether the number is valid for the given region.
+ function IsValidNumber(number, md) {
+ return md.possiblePattern.test(number);
+ }
+
+ // Check whether the number is a valid national number for the given region.
+ function IsNationalNumber(number, md) {
+ return IsValidNumber(number, md) && md.nationalPattern.test(number);
+ }
+
+ // Determine the country code a number starts with, or return null if
+ // its not a valid country code.
+ function ParseCountryCode(number) {
+ for (var n = 1; n <= 3; ++n) {
+ var cc = number.substr(0,n);
+ if (dataBase[cc])
+ return cc;
+ }
+ return null;
+ }
+
+ // Parse an international number that starts with the country code. Return
+ // null if the number is not a valid international number.
+ function ParseInternationalNumber(number) {
+ var ret;
+
+ // Parse and strip the country code.
+ var countryCode = ParseCountryCode(number);
+ if (!countryCode)
+ return null;
+ number = number.substr(countryCode.length);
+
+ // Lookup the meta data for the region (or regions) and if the rest of
+ // the number parses for that region, return the parsed number.
+ var entry = dataBase[countryCode];
+ if (Array.isArray(entry)) {
+ for (var n = 0; n < entry.length; ++n) {
+ if (typeof entry[n] == "string")
+ entry[n] = ParseMetaData(countryCode, entry[n]);
+ if (ret = ParseNationalNumber(number, entry[n]))
+ return ret;
+ }
+ return null;
+ }
+ if (typeof entry == "string")
+ entry = dataBase[countryCode] = ParseMetaData(countryCode, entry);
+ return ParseNationalNumber(number, entry);
+ }
+
+ // Parse a national number for a specific region. Return null if the
+ // number is not a valid national number (it might still be a possible
+ // number for parts of that region).
+ function ParseNationalNumber(number, md) {
+ if (!md.possiblePattern.test(number) ||
+ !md.nationalPattern.test(number)) {
+ return null;
+ }
+ // Success.
+ return new NationalNumber(md, number);
+ }
+
+ // Parse a number and transform it into the national format, removing any
+ // international dial prefixes and country codes.
+ function ParseNumber(number, defaultRegion) {
+ var ret;
+
+ // Remove formating characters and whitespace.
+ number = NormalizeNumber(number);
+
+ // Detect and strip leading '+'.
+ if (number[0] === '+')
+ return ParseInternationalNumber(number.replace(PLUS_CHARS, ""));
+
+ // Lookup the meta data for the given region.
+ var md = FindMetaDataForRegion(defaultRegion.toUpperCase());
+
+ // See if the number starts with an international prefix, and if the
+ // number resulting from stripping the code is valid, then remove the
+ // prefix and flag the number as international.
+ if (md.internationalPrefix.test(number)) {
+ var possibleNumber = number.replace(md.internationalPrefix, "");
+ if (ret = ParseInternationalNumber(possibleNumber))
+ return ret;
+ }
+
+ // This is not an international number. See if its a national one for
+ // the current region. National numbers can start with the national
+ // prefix, or without.
+ if (md.nationalPrefixForParsing) {
+ // Some regions have specific national prefix parse rules. Apply those.
+ var withoutPrefix = number.replace(md.nationalPrefixForParsing,
+ md.nationalPrefixTransformRule);
+ if (ret = ParseNationalNumber(withoutPrefix, md))
+ return ret;
+ } else {
+ // If there is no specific national prefix rule, just strip off the
+ // national prefix from the beginning of the number (if there is one).
+ var nationalPrefix = md.nationalPrefix;
+ if (nationalPrefix && number.indexOf(nationalPrefix) == 0 &&
+ (ret = ParseNationalNumber(number.substr(nationalPrefix.length), md))) {
+ return ret;
+ }
+ }
+ if (ret = ParseNationalNumber(number, md))
+ return ret;
+
+ // If the number matches the possible numbers of the current region,
+ // return it as a possible number.
+ if (md.possiblePattern.test(number))
+ return new NationalNumber(md, number);
+
+ // Now lets see if maybe its an international number after all, but
+ // without '+' or the international prefix.
+ if (ret = ParseInternationalNumber(number))
+ return ret;
+
+ // We couldn't parse the number at all.
+ return null;
+ }
+
+ return {
+ Parse: ParseNumber
+ };
+})(PHONE_NUMBER_META_DATA);
diff --git a/shared/js/phoneNumberJS/PhoneNumberMetaData.js b/shared/js/phoneNumberJS/PhoneNumberMetaData.js
new file mode 100644
index 0000000..a901a78
--- /dev/null
+++ b/shared/js/phoneNumberJS/PhoneNumberMetaData.js
@@ -0,0 +1,218 @@
+/* Automatically generated. Do not edit. */
+const PHONE_NUMBER_META_DATA = {
+"46": '["SE","00","0",,,"$NP$FG","\\d{5,10}","[1-9]\\d{6,9}",[["(8)(\\d{2,3})(\\d{2,3})(\\d{2})","$1-$2 $3 $4","8",,"$1 $2 $3 $4"],["([1-69]\\d)(\\d{2,3})(\\d{2})(\\d{2})","$1-$2 $3 $4","1[013689]|2[0136]|3[1356]|4[0246]|54|6[03]|90",,"$1 $2 $3 $4"],["([1-69]\\d)(\\d{3})(\\d{2})","$1-$2 $3","1[13689]|2[136]|3[1356]|4[0246]|54|6[03]|90",,"$1 $2 $3"],["(\\d{3})(\\d{2})(\\d{2})(\\d{2})","$1-$2 $3 $4","1[2457]|2[2457-9]|3[0247-9]|4[1357-9]|5[0-35-9]|6[124-9]|9(?:[125-8]|3[0-5]|4[0-3])",,"$1 $2 $3 $4"],["(\\d{3})(\\d{2,3})(\\d{2})","$1-$2 $3","1[2457]|2[2457-9]|3[0247-9]|4[1357-9]|5[0-35-9]|6[124-9]|9(?:[125-8]|3[0-5]|4[0-3])",,"$1 $2 $3"],["(7\\d)(\\d{3})(\\d{2})(\\d{2})","$1-$2 $3 $4","7",,"$1 $2 $3 $4"],["(20)(\\d{2,3})(\\d{2})","$1-$2 $3","20",,"$1 $2 $3"],["(9[034]\\d)(\\d{2})(\\d{2})(\\d{3})","$1-$2 $3 $4","9[034]",,"$1 $2 $3 $4"]]]',
+"299": '["GL","00",,,,,"\\d{6}","[1-689]\\d{5}",[["(\\d{2})(\\d{2})(\\d{2})","$1 $2 $3",,,]]]',
+"385": '["HR","00","0",,,"$NP$FG","\\d{6,12}","[1-7]\\d{5,8}|[89]\\d{6,11}",[["(1)(\\d{4})(\\d{3})","$1 $2 $3","1",,],["(6[09])(\\d{4})(\\d{3})","$1 $2 $3","6[09]",,],["(62)(\\d{3})(\\d{3,4})","$1 $2 $3","62",,],["([2-5]\\d)(\\d{3})(\\d{3})","$1 $2 $3","[2-5]",,],["(9\\d)(\\d{3})(\\d{3,4})","$1 $2 $3","9",,],["(9\\d)(\\d{4})(\\d{4})","$1 $2 $3","9",,],["(9\\d)(\\d{3,4})(\\d{3})(\\d{3})","$1 $2 $3 $4","9",,],["(\\d{2})(\\d{2})(\\d{2,3})","$1 $2 $3","6[145]|7",,],["(\\d{2})(\\d{3,4})(\\d{3})","$1 $2 $3","6[145]|7",,],["(80[01])(\\d{2})(\\d{2,3})","$1 $2 $3","8",,],["(80[01])(\\d{3,4})(\\d{3})","$1 $2 $3","8",,]]]',
+"670": '["TL","00",,,,,"\\d{7,8}","[2-489]\\d{6}|7\\d{6,7}",[["(\\d{3})(\\d{4})","$1 $2","[2-489]",,],["(\\d{4})(\\d{4})","$1 $2","7",,]]]',
+"258": '["MZ","00",,,,,"\\d{8,9}","[28]\\d{7,8}",[["([28]\\d)(\\d{3})(\\d{3,4})","$1 $2 $3","2|8[246]",,],["(80\\d)(\\d{3})(\\d{3})","$1 $2 $3","80",,]]]',
+"359": '["BG","00","0",,,"$NP$FG","\\d{5,9}","[23567]\\d{5,7}|[489]\\d{6,8}",[["(2)(\\d{5})","$1 $2","29",,],["(2)(\\d{3})(\\d{3,4})","$1 $2 $3","2",,],["(\\d{3})(\\d{4})","$1 $2","43[124-7]|70[1-9]",,],["(\\d{3})(\\d{3})(\\d{2})","$1 $2 $3","43[124-7]|70[1-9]",,],["(\\d{3})(\\d{2})(\\d{3})","$1 $2 $3","[78]00",,],["(\\d{2})(\\d{3})(\\d{2,3})","$1 $2 $3","[356]|7[1-9]|8[1-6]|9[1-7]",,],["(\\d{2})(\\d{3})(\\d{3,4})","$1 $2 $3","48|8[7-9]|9[08]",,]]]',
+"682": '["CK","00",,,,,"\\d{5}","[2-57]\\d{4}",[["(\\d{2})(\\d{3})","$1 $2",,,]]]',
+"852": '["HK","00",,,,,"\\d{5,11}","[235-7]\\d{7}|8\\d{7,8}|9\\d{4,10}",[["(\\d{4})(\\d{4})","$1 $2","[235-7]|[89](?:0[1-9]|[1-9])",,],["(800)(\\d{3})(\\d{3})","$1 $2 $3","800",,],["(900)(\\d{2})(\\d{3})(\\d{3})","$1 $2 $3 $4","900",,],["(900)(\\d{2,5})","$1 $2","900",,]]]',
+"998": '["UZ","810","8",,,"$NP $FG","\\d{7,9}","[679]\\d{8}",[["([679]\\d)(\\d{3})(\\d{2})(\\d{2})","$1 $2 $3 $4",,,]]]',
+"291": '["ER","00","0",,,"$NP$FG","\\d{6,7}","[178]\\d{6}",[["(\\d)(\\d{3})(\\d{3})","$1 $2 $3",,,]]]',
+"95": '["MM","00","0",,,"$NP$FG","\\d{5,10}","[124-8]\\d{5,7}|9(?:[25689]|4\\d{1,2}|7\\d)\\d{6}",[["(1)(\\d{3})(\\d{3})","$1 $2 $3","1",,],["(1)(3)(33\\d)(\\d{3})","$1 $2 $3 $4","133",,],["(2)(\\d{2})(\\d{3})","$1 $2 $3","2",,],["(\\d{2})(\\d{3})(\\d{3})","$1 $2 $3","67|81",,],["(\\d{2})(\\d{2})(\\d{3})","$1 $2 $3","[4-8]",,],["(9)(\\d{3})(\\d{4,5})","$1 $2 $3","9(?:[25-9]|4[1349])",,],["(9)(4\\d{4})(\\d{4})","$1 $2 $3","94[0256]",,]]]',
+"266": '["LS","00",,,,,"\\d{8}","[2568]\\d{7}",[["(\\d{4})(\\d{4})","$1 $2",,,]]]',
+"245": '["GW","00",,,,,"\\d{7}","[3567]\\d{6}",[["(\\d{3})(\\d{4})","$1 $2",,,]]]',
+"374": '["AM","00","0",,,"($NP$FG)","\\d{5,8}","[1-9]\\d{7}",[["(\\d{2})(\\d{6})","$1 $2","1|47",,],["(\\d{2})(\\d{6})","$1 $2","[5-7]|9[1-9]","$NP$FG",],["(\\d{3})(\\d{5})","$1 $2","[23]",,],["(\\d{3})(\\d{2})(\\d{3})","$1 $2 $3","8|90","$NP $FG",]]]',
+"379": '["VA","00",,,,,"\\d{10}","06\\d{8}",[["(06)(\\d{4})(\\d{4})","$1 $2 $3",,,]]]',
+"61": ['["AU","(?:14(?:1[14]|34|4[17]|[56]6|7[47]|88))?001[14-689]","0",,,,"\\d{6,10}","[1-578]\\d{5,9}",[["([2378])(\\d{4})(\\d{4})","$1 $2 $3","[2378]","($NP$FG)",],["(\\d{3})(\\d{3})(\\d{3})","$1 $2 $3","[45]|14","$NP$FG",],["(16)(\\d{3})(\\d{2,4})","$1 $2 $3","16","$NP$FG",],["(1[389]\\d{2})(\\d{3})(\\d{3})","$1 $2 $3","1(?:[38]0|90)","$FG",],["(180)(2\\d{3})","$1 $2","180","$FG",],["(19\\d)(\\d{3})","$1 $2","19[13]","$FG",],["(19\\d{2})(\\d{4})","$1 $2","19[67]","$FG",],["(13)(\\d{2})(\\d{2})","$1 $2 $3","13[1-9]","$FG",]]]','["CC","(?:14(?:1[14]|34|4[17]|[56]6|7[47]|88))?001[14-689]","0",,,,"\\d{6,10}","[1458]\\d{5,9}",]','["CX","(?:14(?:1[14]|34|4[17]|[56]6|7[47]|88))?001[14-689]","0",,,,"\\d{6,10}","[1458]\\d{5,9}",]'],
+"500": '["FK","00",,,,,"\\d{5}","[2-7]\\d{4}",]',
+"261": '["MG","00","0",,,"$NP$FG","\\d{7,9}","[23]\\d{8}",[["([23]\\d)(\\d{2})(\\d{3})(\\d{2})","$1 $2 $3 $4",,,]]]',
+"92": '["PK","00","0",,,"($NP$FG)","\\d{6,12}","1\\d{8}|[2-8]\\d{5,11}|9(?:[013-9]\\d{4,9}|2\\d(?:111\\d{6}|\\d{3,7}))",[["(\\d{2})(111)(\\d{3})(\\d{3})","$1 $2 $3 $4","(?:2[125]|4[0-246-9]|5[1-35-7]|6[1-8]|7[14]|8[16]|91)1",,],["(\\d{3})(111)(\\d{3})(\\d{3})","$1 $2 $3 $4","2[349]|45|54|60|72|8[2-5]|9[2-9]",,],["(\\d{2})(\\d{7,8})","$1 $2","(?:2[125]|4[0-246-9]|5[1-35-7]|6[1-8]|7[14]|8[16]|91)[2-9]",,],["(\\d{3})(\\d{6,7})","$1 $2","2[349]|45|54|60|72|8[2-5]|9[2-9]",,],["(3\\d{2})(\\d{7})","$1 $2","3","$NP$FG",],["([15]\\d{3})(\\d{5,6})","$1 $2","58[12]|1",,],["(586\\d{2})(\\d{5})","$1 $2","586",,],["([89]00)(\\d{3})(\\d{2})","$1 $2 $3","[89]00","$NP$FG",]]]',
+"234": '["NG","009","0",,,"$NP$FG","\\d{5,14}","[1-69]\\d{5,8}|[78]\\d{5,13}",[["([129])(\\d{3})(\\d{3,4})","$1 $2 $3","[129]",,],["([3-8]\\d)(\\d{3})(\\d{2,3})","$1 $2 $3","[3-6]|7(?:[1-79]|0[1-9])|8[2-9]",,],["([78]\\d{2})(\\d{3})(\\d{3,4})","$1 $2 $3","70|8[01]",,],["([78]00)(\\d{4})(\\d{4,5})","$1 $2 $3","[78]00",,],["([78]00)(\\d{5})(\\d{5,6})","$1 $2 $3","[78]00",,],["(78)(\\d{2})(\\d{3})","$1 $2 $3","78",,]]]',
+"350": '["GI","00",,,,,"\\d{8}","[2568]\\d{7}",]',
+"45": '["DK","00",,,,,"\\d{8}","[2-9]\\d{7}",[["(\\d{2})(\\d{2})(\\d{2})(\\d{2})","$1 $2 $3 $4",,,]]]',
+"963": '["SY","00","0",,,"$NP$FG","\\d{6,9}","[1-59]\\d{7,8}",[["(\\d{2})(\\d{3})(\\d{3,4})","$1 $2 $3","[1-5]",,],["(9\\d{2})(\\d{3})(\\d{3})","$1 $2 $3","9",,]]]',
+"226": '["BF","00",,,,,"\\d{8}","[24-7]\\d{7}",[["(\\d{2})(\\d{2})(\\d{2})(\\d{2})","$1 $2 $3 $4",,,]]]',
+"974": '["QA","00",,,,,"\\d{7,8}","[2-8]\\d{6,7}",[["([28]\\d{2})(\\d{4})","$1 $2","[28]",,],["([3-7]\\d{3})(\\d{4})","$1 $2","[3-7]",,]]]',
+"218": '["LY","00","0",,,"$NP$FG","\\d{7,9}","[25679]\\d{8}",[["([25679]\\d)(\\d{7})","$1-$2",,,]]]',
+"51": '["PE","19(?:1[124]|77|90)00","0",,,"($NP$FG)","\\d{6,9}","[14-9]\\d{7,8}",[["(1)(\\d{7})","$1 $2","1",,],["([4-8]\\d)(\\d{6})","$1 $2","[4-7]|8[2-4]",,],["(\\d{3})(\\d{5})","$1 $2","80",,],["(9\\d{2})(\\d{3})(\\d{3})","$1 $2 $3","9","$FG",]]]',
+"62": '["ID","0(?:0[1789]|10(?:00|1[67]))","0",,,"$NP$FG","\\d{5,11}","[1-9]\\d{6,10}",[["(\\d{2})(\\d{7,8})","$1 $2","2[124]|[36]1","($NP$FG)",],["(\\d{3})(\\d{5,7})","$1 $2","[4579]|2[035-9]|[36][02-9]","($NP$FG)",],["(8\\d{2})(\\d{3,4})(\\d{3,4})","$1-$2-$3","8[1-35-9]",,],["(177)(\\d{6,8})","$1 $2","1",,],["(800)(\\d{5,7})","$1 $2","800",,],["(809)(\\d)(\\d{3})(\\d{3})","$1 $2 $3 $4","809",,]]]',
+"298": '["FO","00",,"(10(?:01|[12]0|88))",,,"\\d{6}","[2-9]\\d{5}",[["(\\d{6})","$1",,,]]]',
+"381": '["RS","00","0",,,"$NP$FG","\\d{5,12}","[126-9]\\d{4,11}|3(?:[0-79]\\d{3,10}|8[2-9]\\d{2,9})",[["([23]\\d{2})(\\d{4,9})","$1 $2","(?:2[389]|39)0",,],["([1-3]\\d)(\\d{5,10})","$1 $2","1|2(?:[0-24-7]|[389][1-9])|3(?:[0-8]|9[1-9])",,],["(6\\d)(\\d{6,8})","$1 $2","6",,],["([89]\\d{2})(\\d{3,9})","$1 $2","[89]",,],["(7[26])(\\d{4,9})","$1 $2","7[26]",,],["(7[08]\\d)(\\d{4,9})","$1 $2","7[08]",,]]]',
+"975": '["BT","00",,,,,"\\d{6,8}","[1-8]\\d{6,7}",[["([17]7)(\\d{2})(\\d{2})(\\d{2})","$1 $2 $3 $4","1|77",,],["([2-8])(\\d{3})(\\d{3})","$1 $2 $3","[2-68]|7[246]",,]]]',
+"34": '["ES","00",,,,,"\\d{9}","[5-9]\\d{8}",[["([5-9]\\d{2})(\\d{2})(\\d{2})(\\d{2})","$1 $2 $3 $4",,,]]]',
+"881": '["001",,,,,,"\\d{9}","[67]\\d{8}",[["(\\d)(\\d{3})(\\d{5})","$1 $2 $3","[67]",,]]]',
+"855": '["KH","00[14-9]","0",,,,"\\d{6,10}","[1-9]\\d{7,9}",[["(\\d{2})(\\d{3})(\\d{3,4})","$1 $2 $3","1\\d[1-9]|[2-9]","$NP$FG",],["(1[89]00)(\\d{3})(\\d{3})","$1 $2 $3","1[89]0",,]]]',
+"420": '["CZ","00",,,,,"\\d{9,12}","[2-8]\\d{8}|9\\d{8,11}",[["([2-9]\\d{2})(\\d{3})(\\d{3})","$1 $2 $3","[2-8]|9[015-7]",,],["(96\\d)(\\d{3})(\\d{3})(\\d{3})","$1 $2 $3 $4","96",,],["(9\\d)(\\d{3})(\\d{3})(\\d{3})","$1 $2 $3 $4","9[36]",,]]]',
+"216": '["TN","00",,,,,"\\d{8}","[2-57-9]\\d{7}",[["(\\d{2})(\\d{3})(\\d{3})","$1 $2 $3",,,]]]',
+"673": '["BN","00",,,,,"\\d{7}","[2-578]\\d{6}",[["([2-578]\\d{2})(\\d{4})","$1 $2",,,]]]',
+"290": '["SH","00",,,,,"\\d{4}","[2-9]\\d{3}",]',
+"882": '["001",,,,,,"\\d{7,12}","[13]\\d{6,11}",[["(\\d{2})(\\d{4})(\\d{3})","$1 $2 $3","3[23]",,],["(\\d{2})(\\d{5})","$1 $2","16|342",,],["(\\d{2})(\\d{4})(\\d{4})","$1 $2 $3","34[57]",,],["(\\d{3})(\\d{4})(\\d{4})","$1 $2 $3","348",,],["(\\d{2})(\\d{2})(\\d{4})","$1 $2 $3","1",,],["(\\d{2})(\\d{3,4})(\\d{4})","$1 $2 $3","16",,],["(\\d{2})(\\d{4,5})(\\d{5})","$1 $2 $3","16",,]]]',
+"267": '["BW","00",,,,,"\\d{7,8}","[2-79]\\d{6,7}",[["(\\d{3})(\\d{4})","$1 $2","[2-6]",,],["(7\\d)(\\d{3})(\\d{3})","$1 $2 $3","7",,],["(90)(\\d{5})","$1 $2","9",,]]]',
+"94": '["LK","00","0",,,"$NP$FG","\\d{7,9}","[1-9]\\d{8}",[["(\\d{2})(\\d{1})(\\d{6})","$1 $2 $3","[1-689]",,],["(\\d{2})(\\d{3})(\\d{4})","$1 $2 $3","7",,]]]',
+"356": '["MT","00",,,,,"\\d{8}","[2579]\\d{7}",[["(\\d{4})(\\d{4})","$1 $2",,,]]]',
+"375": '["BY","810","8","80?",,,"\\d{7,11}","[1-4]\\d{8}|[89]\\d{9,10}",[["([1-4]\\d)(\\d{3})(\\d{4})","$1 $2 $3","[1-4]","$NP 0$FG",],["([89]\\d{2})(\\d{3})(\\d{4})","$1 $2 $3","8[01]|9","$NP $FG",],["(8\\d{2})(\\d{4})(\\d{4})","$1 $2 $3","82","$NP $FG",]]]',
+"690": '["TK","00",,,,,"\\d{4}","[2-5]\\d{3}",]',
+"507": '["PA","00",,,,,"\\d{7,8}","[1-9]\\d{6,7}",[["(\\d{3})(\\d{4})","$1-$2","[1-57-9]",,],["(\\d{4})(\\d{4})","$1-$2","6",,]]]',
+"692": '["MH","011","1",,,,"\\d{7}","[2-6]\\d{6}",[["(\\d{3})(\\d{4})","$1-$2",,,]]]',
+"250": '["RW","00","0",,,,"\\d{8,9}","[027-9]\\d{7,8}",[["(2\\d{2})(\\d{3})(\\d{3})","$1 $2 $3","2","$FG",],["([7-9]\\d{2})(\\d{3})(\\d{3})","$1 $2 $3","[7-9]","$NP$FG",],["(0\\d)(\\d{2})(\\d{2})(\\d{2})","$1 $2 $3 $4","0",,]]]',
+"81": '["JP","010","0",,,"$NP$FG","\\d{7,16}","[1-9]\\d{8,9}|0(?:[36]\\d{7,14}|7\\d{5,7}|8\\d{7})",[["(\\d{3})(\\d{3})(\\d{3})","$1-$2-$3","(?:12|57|99)0",,],["(\\d{3})(\\d{3})(\\d{4})","$1-$2-$3","800",,],["(\\d{3})(\\d{4})","$1-$2","077",,],["(\\d{3})(\\d{2})(\\d{3,4})","$1-$2-$3","077",,],["(\\d{3})(\\d{2})(\\d{4})","$1-$2-$3","088",,],["(\\d{3})(\\d{3})(\\d{3,4})","$1-$2-$3","0(?:37|66)",,],["(\\d{3})(\\d{4})(\\d{4,5})","$1-$2-$3","0(?:37|66)",,],["(\\d{3})(\\d{5})(\\d{5,6})","$1-$2-$3","0(?:37|66)",,],["(\\d{3})(\\d{6})(\\d{6,7})","$1-$2-$3","0(?:37|66)",,],["(\\d{2})(\\d{4})(\\d{4})","$1-$2-$3","[2579]0|80[1-9]",,],["(\\d{4})(\\d)(\\d{4})","$1-$2-$3","1(?:26|3[79]|4[56]|5[4-68]|6[3-5])|5(?:76|97)|499|746|8(?:3[89]|63|47|51)|9(?:49|80|9[16])",,],["(\\d{3})(\\d{2})(\\d{4})","$1-$2-$3","1(?:2[3-6]|3[3-9]|4[2-6]|5[2-8]|[68][2-7]|7[2-689]|9[1-578])|2(?:2[03-689]|3[3-58]|4[0-468]|5[04-8]|6[013-8]|7[06-9]|8[02-57-9]|9[13])|4(?:2[28]|3[689]|6[035-7]|7[05689]|80|9[3-5])|5(?:3[1-36-9]|4[4578]|5[013-8]|6[1-9]|7[2-8]|8[14-7]|9[4-9])|7(?:2[15]|3[5-9]|4[02-9]|6[135-8]|7[0-4689]|9[014-9])|8(?:2[49]|3[3-8]|4[5-8]|5[2-9]|6[35-9]|7[579]|8[03-579]|9[2-8])|9(?:[23]0|4[02-46-9]|5[024-79]|6[4-9]|7[2-47-9]|8[02-7]|9[3-7])",,],["(\\d{2})(\\d{3})(\\d{4})","$1-$2-$3","1|2(?:2[37]|5[5-9]|64|78|8[39]|91)|4(?:2[2689]|64|7[347])|5(?:[2-589]|39)|60|8(?:[46-9]|3[279]|2[124589])|9(?:[235-8]|93)",,],["(\\d{3})(\\d{2})(\\d{4})","$1-$2-$3","2(?:9[14-79]|74|[34]7|[56]9)|82|993",,],["(\\d)(\\d{4})(\\d{4})","$1-$2-$3","3|4(?:2[09]|7[01])|6[1-9]",,],["(\\d{2})(\\d{3})(\\d{4})","$1-$2-$3","[2479][1-9]",,]]]',
+"237": '["CM","00",,,,,"\\d{8}","[237-9]\\d{7}",[["([237-9]\\d)(\\d{2})(\\d{2})(\\d{2})","$1 $2 $3 $4","[2379]|88",,],["(800)(\\d{2})(\\d{3})","$1 $2 $3","80",,]]]',
+"351": '["PT","00",,,,,"\\d{9}","[2-46-9]\\d{8}",[["([2-46-9]\\d{2})(\\d{3})(\\d{3})","$1 $2 $3",,,]]]',
+"246": '["IO","00",,,,,"\\d{7}","3\\d{6}",[["(\\d{3})(\\d{4})","$1 $2",,,]]]',
+"227": '["NE","00",,,,,"\\d{8}","[029]\\d{7}",[["([029]\\d)(\\d{2})(\\d{2})(\\d{2})","$1 $2 $3 $4","[29]|09",,],["(08)(\\d{3})(\\d{3})","$1 $2 $3","08",,]]]',
+"27": '["ZA","00","0",,,"$NP$FG","\\d{5,9}","[1-5]\\d{8}|(?:7\\d{4,8}|8[1-5789]\\d{3,7})|8[06]\\d{7}",[["(860)(\\d{3})(\\d{3})","$1 $2 $3","860",,],["([1-578]\\d)(\\d{3})(\\d{4})","$1 $2 $3","[1-57]|8(?:[0-57-9]|6[1-9])",,],["(\\d{2})(\\d{3,4})","$1 $2","7|8[1-5789]",,],["(\\d{2})(\\d{3})(\\d{2,3})","$1 $2 $3","7|8[1-5789]",,]]]',
+"962": '["JO","00","0",,,"$NP$FG","\\d{7,9}","[235-9]\\d{7,8}",[["(\\d)(\\d{3})(\\d{4})","$1 $2 $3","[2356]|87","($NP$FG)",],["(7)(\\d{4})(\\d{4})","$1 $2 $3","7[457-9]",,],["(\\d{3})(\\d{5,6})","$1 $2","70|8[0158]|9",,]]]',
+"387": '["BA","00","0",,,"$NP$FG","\\d{6,9}","[3-9]\\d{7,8}",[["(\\d{2})(\\d{3})(\\d{3})","$1 $2-$3","[3-5]",,],["(\\d{2})(\\d{3})(\\d{3})","$1 $2 $3","6[1-356]|[7-9]",,],["(\\d{2})(\\d{2})(\\d{2})(\\d{3})","$1 $2 $3 $4","6[047]",,]]]',
+"33": '["FR","[04579]0","0",,,"$NP$FG","\\d{4}(?:\\d{5})?","[124-9]\\d{8}|3\\d{3}(?:\\d{5})?",[["([1-79])(\\d{2})(\\d{2})(\\d{2})(\\d{2})","$1 $2 $3 $4 $5","[1-79]",,],["(8\\d{2})(\\d{2})(\\d{2})(\\d{2})","$1 $2 $3 $4","8","$NP $FG",]]]',
+"972": '["IL","0(?:0|1[2-9])","0",,,"$FG","\\d{4,10}","[17]\\d{6,9}|[2-589]\\d{3}(?:\\d{3,6})?|6\\d{3}",[["([2-489])(\\d{3})(\\d{4})","$1-$2-$3","[2-489]","$NP$FG",],["([57]\\d)(\\d{3})(\\d{4})","$1-$2-$3","[57]","$NP$FG",],["(1)([7-9]\\d{2})(\\d{3})(\\d{3})","$1-$2-$3-$4","1[7-9]",,],["(1255)(\\d{3})","$1-$2","125",,],["(1200)(\\d{3})(\\d{3})","$1-$2-$3","120",,],["(1212)(\\d{2})(\\d{2})","$1-$2-$3","121",,],["(1599)(\\d{6})","$1-$2","15",,],["(\\d{4})","*$1","[2-689]",,]]]',
+"248": '["SC","0[0-2]",,,,,"\\d{6,7}","[24689]\\d{5,6}",[["(\\d{3})(\\d{3})","$1 $2","[89]",,],["(\\d)(\\d{3})(\\d{3})","$1 $2 $3","[246]",,]]]',
+"297": '["AW","00",,,,,"\\d{7}","[25-9]\\d{6}",[["(\\d{3})(\\d{4})","$1 $2",,,]]]',
+"421": '["SK","00","0",,,"$NP$FG","\\d{9}","[2-689]\\d{8}",[["(2)(\\d{3})(\\d{3})(\\d{2})","$1/$2 $3 $4","2",,],["([3-5]\\d)(\\d{3})(\\d{2})(\\d{2})","$1/$2 $3 $4","[3-5]",,],["([689]\\d{2})(\\d{3})(\\d{3})","$1 $2 $3","[689]",,]]]',
+"672": '["NF","00",,,,,"\\d{5,6}","[13]\\d{5}",[["(\\d{2})(\\d{4})","$1 $2","1",,],["(\\d)(\\d{5})","$1 $2","3",,]]]',
+"870": '["001",,,,,,"\\d{9}","[35-7]\\d{8}",[["(\\d{3})(\\d{3})(\\d{3})","$1 $2 $3",,,]]]',
+"883": '["001",,,,,,"\\d{9}(?:\\d{3})?","51\\d{7}(?:\\d{3})?",[["(\\d{3})(\\d{3})(\\d{3})","$1 $2 $3",,,],["(\\d{3})(\\d{3})(\\d{3})(\\d{3})","$1 $2 $3 $4",,,]]]',
+"264": '["NA","00","0",,,"$NP$FG","\\d{8,9}","[68]\\d{7,8}",[["(8\\d)(\\d{3})(\\d{4})","$1 $2 $3","8[1235]",,],["(6\\d)(\\d{2,3})(\\d{4})","$1 $2 $3","6",,],["(88)(\\d{3})(\\d{3})","$1 $2 $3","88",,],["(870)(\\d{3})(\\d{3})","$1 $2 $3","870",,]]]',
+"878": '["001",,,,,,"\\d{12}","1\\d{11}",[["(\\d{2})(\\d{5})(\\d{5})","$1 $2 $3",,,]]]',
+"239": '["ST","00",,,,,"\\d{7}","[29]\\d{6}",[["(\\d{3})(\\d{4})","$1 $2",,,]]]',
+"357": '["CY","00",,,,,"\\d{8}","[257-9]\\d{7}",[["(\\d{2})(\\d{6})","$1 $2",,,]]]',
+"240": '["GQ","00",,,,,"\\d{9}","[23589]\\d{8}",[["(\\d{3})(\\d{3})(\\d{3})","$1 $2 $3","[235]",,],["(\\d{3})(\\d{6})","$1 $2","[89]",,]]]',
+"506": '["CR","00",,"(19(?:0[0-2468]|19|66|77))",,,"\\d{8,10}","[24-9]\\d{7,9}",[["(\\d{4})(\\d{4})","$1 $2","[24-7]|8[3-9]",,],["(\\d{3})(\\d{3})(\\d{4})","$1-$2-$3","[89]0",,]]]',
+"86": '["CN","(1[1279]\\d{3})?00","0","(1[1279]\\d{3})|0",,,"\\d{4,12}","[1-79]\\d{7,11}|8[0-357-9]\\d{6,9}",[["(80\\d{2})(\\d{4})","$1 $2","80[2678]","$NP$FG",],["([48]00)(\\d{3})(\\d{4})","$1 $2 $3","[48]00",,],["(\\d{3,4})(\\d{4})","$1 $2","[2-9]",,"NA"],["(21)(\\d{4})(\\d{4,6})","$1 $2 $3","21","$NP$FG",],["([12]\\d)(\\d{4})(\\d{4})","$1 $2 $3","10[1-9]|2[02-9]","$NP$FG",],["(\\d{3})(\\d{4})(\\d{4})","$1 $2 $3","3(?:11|7[179])|4(?:[15]1|3[12])|5(?:1|2[37]|3[12]|7[13-79]|9[15])|7(?:31|5[457]|6[09]|91)|898","$NP$FG",],["(\\d{3})(\\d{3})(\\d{4})","$1 $2 $3","3(?:1[02-9]|35|49|5|7[02-68]|9[1-68])|4(?:1[02-9]|2[179]|[35][2-9]|6[4789]|7\\d|8[23])|5(?:3[03-9]|4[36]|5|6[1-6]|7[028]|80|9[2-46-9])|6(?:3[1-5]|6[0238]|9[12])|7(?:01|[1579]|2[248]|3[04-9]|4[3-6]|6[2368])|8(?:1[236-8]|2[5-7]|[37]|5[1-9]|8[3678]|9[1-7])|9(?:0[1-3689]|1[1-79]|[379]|4[13]|5[1-5])","$NP$FG",],["(1[3-58]\\d)(\\d{4})(\\d{4})","$1 $2 $3","1[3-58]",,],["(10800)(\\d{3})(\\d{4})","$1 $2 $3","108",,]]]',
+"257": '["BI","00",,,,,"\\d{8}","[27]\\d{7}",[["([27]\\d)(\\d{2})(\\d{2})(\\d{2})","$1 $2 $3 $4",,,]]]',
+"683": '["NU","00",,,,,"\\d{4}","[1-5]\\d{3}",]',
+"43": '["AT","00","0",,,"$NP$FG","\\d{3,13}","[1-9]\\d{3,12}",[["(1)(\\d{3,12})","$1 $2","1",,],["(5\\d)(\\d{3,5})","$1 $2","5[079]",,],["(5\\d)(\\d{3})(\\d{3,4})","$1 $2 $3","5[079]",,],["(5\\d)(\\d{4})(\\d{4,7})","$1 $2 $3","5[079]",,],["(\\d{3})(\\d{3,10})","$1 $2","316|46|51|732|6(?:44|5[0-3579]|[6-9])|7(?:1|[28]0)|[89]",,],["(\\d{4})(\\d{3,9})","$1 $2","2|3(?:1[1-578]|[3-8])|4[2378]|5[2-6]|6(?:[12]|4[1-35-9]|5[468])|7(?:2[1-8]|35|4[1-8]|[57-9])",,]]]',
+"247": '["AC","00",,,,,"\\d{4}","[2-467]\\d{3}",]',
+"675": '["PG","00",,,,,"\\d{7,8}","[1-9]\\d{6,7}",[["(\\d{3})(\\d{4})","$1 $2","[1-689]",,],["(7[1-36]\\d)(\\d{2})(\\d{3})","$1 $2 $3","7[1-36]",,]]]',
+"376": '["AD","00",,,,,"\\d{6,8}","(?:[346-9]|180)\\d{5}",[["(\\d{3})(\\d{3})","$1 $2","[346-9]",,],["(180[02])(\\d{4})","$1 $2","1",,]]]',
+"63": '["PH","00","0",,,,"\\d{7,13}","[2-9]\\d{7,9}|1800\\d{7,9}",[["(2)(\\d{3})(\\d{4})","$1 $2 $3","2","($NP$FG)",],["(\\d{4})(\\d{5})","$1 $2","3(?:23|39|46)|4(?:2[3-6]|[35]9|4[26]|76)|5(?:22|44)|642|8(?:62|8[245])","($NP$FG)",],["(\\d{5})(\\d{4})","$1 $2","346|4(?:27|9[35])|883","($NP$FG)",],["([3-8]\\d)(\\d{3})(\\d{4})","$1 $2 $3","[3-8]","($NP$FG)",],["(9\\d{2})(\\d{3})(\\d{4})","$1 $2 $3","9","$NP$FG",],["(1800)(\\d{3})(\\d{4})","$1 $2 $3","1",,],["(1800)(\\d{1,2})(\\d{3})(\\d{4})","$1 $2 $3 $4","1",,]]]',
+"236": '["CF","00",,,,,"\\d{8}","[278]\\d{7}",[["(\\d{2})(\\d{2})(\\d{2})(\\d{2})","$1 $2 $3 $4",,,]]]',
+"590": ['["GP","00","0",,,"$NP$FG","\\d{9}","[56]\\d{8}",[["([56]90)(\\d{2})(\\d{4})","$1 $2-$3",,,]]]','["BL","00","0",,,,"\\d{9}","[56]\\d{8}",]','["MF","00","0",,,,"\\d{9}","[56]\\d{8}",]'],
+"53": '["CU","119","0",,,"($NP$FG)","\\d{4,8}","[2-57]\\d{5,7}",[["(\\d)(\\d{6,7})","$1 $2","7",,],["(\\d{2})(\\d{4,6})","$1 $2","[2-4]",,],["(\\d)(\\d{7})","$1 $2","5","$NP$FG",]]]',
+"64": '["NZ","0(?:0|161)","0",,,"$NP$FG","\\d{7,11}","6[235-9]\\d{6}|[2-57-9]\\d{7,10}",[["([34679])(\\d{3})(\\d{4})","$1-$2 $3","[3467]|9[1-9]",,],["(24099)(\\d{3})","$1 $2","240",,],["(\\d{2})(\\d{3})(\\d{3})","$1 $2 $3","21",,],["(\\d{2})(\\d{3})(\\d{3,4})","$1 $2 $3","2(?:1[1-9]|[69]|7[0-35-9])|86",,],["(2\\d)(\\d{3,4})(\\d{4})","$1 $2 $3","2[028]",,],["(\\d{3})(\\d{3})(\\d{3,4})","$1 $2 $3","2(?:10|74)|5|[89]0",,]]]',
+"965": '["KW","00",,,,,"\\d{7,8}","[12569]\\d{6,7}",[["(\\d{4})(\\d{3,4})","$1 $2","[1269]",,],["(5[015]\\d)(\\d{5})","$1 $2","5",,]]]',
+"224": '["GN","00",,,,,"\\d{8,9}","[23567]\\d{7,8}",[["(\\d{2})(\\d{2})(\\d{2})(\\d{2})","$1 $2 $3 $4","[23567]",,],["(\\d{3})(\\d{2})(\\d{2})(\\d{2})","$1 $2 $3 $4","62",,]]]',
+"973": '["BH","00",,,,,"\\d{8}","[136-9]\\d{7}",[["(\\d{4})(\\d{4})","$1 $2",,,]]]',
+"32": '["BE","00","0",,,"$NP$FG","\\d{8,9}","[1-9]\\d{7,8}",[["(4[6-9]\\d)(\\d{2})(\\d{2})(\\d{2})","$1 $2 $3 $4","4[6-9]",,],["([2-49])(\\d{3})(\\d{2})(\\d{2})","$1 $2 $3 $4","[23]|[49][23]",,],["([15-8]\\d)(\\d{2})(\\d{2})(\\d{2})","$1 $2 $3 $4","[156]|7[0178]|8(?:0[1-9]|[1-79])",,],["([89]\\d{2})(\\d{2})(\\d{3})","$1 $2 $3","(?:80|9)0",,]]]',
+"249": '["SD","00","0",,,"$NP$FG","\\d{9}","[19]\\d{8}",[["(\\d{2})(\\d{3})(\\d{4})","$1 $2 $3",,,]]]',
+"678": '["VU","00",,,,,"\\d{5,7}","[2-57-9]\\d{4,6}",[["(\\d{3})(\\d{4})","$1 $2","[579]",,]]]',
+"52": '["MX","0[09]","01","0[12]|04[45](\\d{10})","1$1","$NP $FG","\\d{7,11}","[1-9]\\d{9,10}",[["([358]\\d)(\\d{4})(\\d{4})","$1 $2 $3","33|55|81",,],["(\\d{3})(\\d{3})(\\d{4})","$1 $2 $3","[2467]|3[12457-9]|5[89]|8[02-9]|9[0-35-9]",,],["(1)([358]\\d)(\\d{4})(\\d{4})","044 $2 $3 $4","1(?:33|55|81)","$FG","$1 $2 $3 $4"],["(1)(\\d{3})(\\d{3})(\\d{4})","044 $2 $3 $4","1(?:[2467]|3[12457-9]|5[89]|8[2-9]|9[1-35-9])","$FG","$1 $2 $3 $4"]]]',
+"968": '["OM","00",,,,,"\\d{7,9}","(?:2[2-6]|5|9[1-9])\\d{6}|800\\d{5,6}",[["(2\\d)(\\d{6})","$1 $2","2",,],["(9\\d{3})(\\d{4})","$1 $2","9",,],["([58]00)(\\d{4,6})","$1 $2","[58]",,]]]',
+"599": ['["CW","00",,,,,"\\d{7,8}","[169]\\d{6,7}",[["(\\d{3})(\\d{4})","$1 $2","[13-7]",,],["(9)(\\d{3})(\\d{4})","$1 $2 $3","9",,]]]','["BQ","00",,,,,"\\d{7}","[347]\\d{6}",]'],
+"800": '["001",,,,,,"\\d{8}","\\d{8}",[["(\\d{4})(\\d{4})","$1 $2",,,]]]',
+"386": '["SI","00","0",,,"$NP$FG","\\d{5,8}","[1-7]\\d{6,7}|[89]\\d{4,7}",[["(\\d)(\\d{3})(\\d{2})(\\d{2})","$1 $2 $3 $4","[12]|3[4-8]|4[24-8]|5[2-8]|7[3-8]","($NP$FG)",],["([3-7]\\d)(\\d{3})(\\d{3})","$1 $2 $3","[37][01]|4[019]|51|6",,],["([89][09])(\\d{3,6})","$1 $2","[89][09]",,],["([58]\\d{2})(\\d{5})","$1 $2","59|8[1-3]",,]]]',
+"679": '["FJ","0(?:0|52)",,,,,"\\d{7}(?:\\d{4})?","[36-9]\\d{6}|0\\d{10}",[["(\\d{3})(\\d{4})","$1 $2","[36-9]",,],["(\\d{4})(\\d{3})(\\d{4})","$1 $2 $3","0",,]]]',
+"238": '["CV","0",,,,,"\\d{7}","[259]\\d{6}",[["(\\d{3})(\\d{2})(\\d{2})","$1 $2 $3",,,]]]',
+"691": '["FM","00",,,,,"\\d{7}","[39]\\d{6}",[["(\\d{3})(\\d{4})","$1 $2",,,]]]',
+"262": ['["RE","00","0",,,"$NP$FG","\\d{9}","[268]\\d{8}",[["([268]\\d{2})(\\d{2})(\\d{2})(\\d{2})","$1 $2 $3 $4",,,]]]','["YT","00","0",,,"$NP$FG","\\d{9}","[268]\\d{8}",]'],
+"241": '["GA","00","0",,,,"\\d{7,8}","[01]\\d{6,7}",[["(1)(\\d{2})(\\d{2})(\\d{2})","$1 $2 $3 $4","1","$NP$FG",],["(0\\d)(\\d{2})(\\d{2})(\\d{2})","$1 $2 $3 $4","0",,]]]',
+"370": '["LT","00","8",,,"($NP-$FG)","\\d{8}","[3-9]\\d{7}",[["([34]\\d)(\\d{6})","$1 $2","37|4(?:1|5[45]|6[2-4])",,],["([3-6]\\d{2})(\\d{5})","$1 $2","3[148]|4(?:[24]|6[09])|528|6",,],["([7-9]\\d{2})(\\d{2})(\\d{3})","$1 $2 $3","[7-9]","$NP $FG",],["(5)(2\\d{2})(\\d{4})","$1 $2 $3","52[0-79]",,]]]',
+"256": '["UG","00[057]","0",,,"$NP$FG","\\d{5,9}","\\d{9}",[["(\\d{3})(\\d{6})","$1 $2","[7-9]|20(?:[013-5]|2[5-9])|4(?:6[45]|[7-9])",,],["(\\d{2})(\\d{7})","$1 $2","3|4(?:[1-5]|6[0-36-9])",,],["(2024)(\\d{5})","$1 $2","2024",,]]]',
+"677": '["SB","0[01]",,,,,"\\d{5,7}","[1-9]\\d{4,6}",[["(\\d{3})(\\d{4})","$1 $2","[7-9]",,]]]',
+"377": '["MC","00","0",,,"$NP$FG","\\d{8,9}","[4689]\\d{7,8}",[["(\\d{2})(\\d{2})(\\d{2})(\\d{2})","$1 $2 $3 $4","[89]","$FG",],["(\\d{2})(\\d{3})(\\d{3})","$1 $2 $3","4",,],["(6)(\\d{2})(\\d{2})(\\d{2})(\\d{2})","$1 $2 $3 $4 $5","6",,]]]',
+"382": '["ME","00","0",,,"$NP$FG","\\d{6,9}","[2-9]\\d{7,8}",[["(\\d{2})(\\d{3})(\\d{3})","$1 $2 $3","[2-57-9]|6[3789]",,],["(67)(9)(\\d{3})(\\d{3})","$1 $2 $3 $4","679",,]]]',
+"231": '["LR","00","0",,,"$NP$FG","\\d{7,9}","(?:[29]\\d|[4-6]|7\\d{1,2}|[38]\\d{2})\\d{6}",[["([279]\\d)(\\d{3})(\\d{3})","$1 $2 $3","[279]",,],["(7\\d{2})(\\d{3})(\\d{3})","$1 $2 $3","7",,],["([4-6])(\\d{3})(\\d{3})","$1 $2 $3","[4-6]",,],["(\\d{2})(\\d{3})(\\d{4})","$1 $2 $3","[38]",,]]]',
+"591": '["BO","00(1\\d)?","0","0(1\\d)?",,,"\\d{7,8}","[23467]\\d{7}",[["([234])(\\d{7})","$1 $2","[234]",,],["([67]\\d{7})","$1","[67]",,]]]',
+"808": '["001",,,,,,"\\d{8}","\\d{8}",[["(\\d{4})(\\d{4})","$1 $2",,,]]]',
+"964": '["IQ","00","0",,,"$NP$FG","\\d{6,10}","[1-7]\\d{7,9}",[["(1)(\\d{3})(\\d{4})","$1 $2 $3","1",,],["([2-6]\\d)(\\d{3})(\\d{3,4})","$1 $2 $3","[2-6]",,],["(7\\d{2})(\\d{3})(\\d{4})","$1 $2 $3","7",,]]]',
+"225": '["CI","00",,,,,"\\d{8}","[02-6]\\d{7}",[["(\\d{2})(\\d{2})(\\d{2})(\\d{2})","$1 $2 $3 $4",,,]]]',
+"992": '["TJ","810","8",,,"($NP) $FG","\\d{3,9}","[3-59]\\d{8}",[["([349]\\d{2})(\\d{2})(\\d{4})","$1 $2 $3","[34]7|91[78]",,],["([459]\\d)(\\d{3})(\\d{4})","$1 $2 $3","4[48]|5|9(?:1[59]|[0235-9])",,],["(331700)(\\d)(\\d{2})","$1 $2 $3","331",,],["(\\d{4})(\\d)(\\d{4})","$1 $2 $3","3[1-5]",,]]]',
+"55": '["BR","00(?:1[45]|2[135]|[34]1|43)","0","0(?:(1[245]|2[135]|[34]1)(\\d{10,11}))?","$2",,"\\d{8,11}","[1-46-9]\\d{7,10}|5\\d{8,9}",[["(\\d{2})(\\d{5})(\\d{4})","$1 $2-$3","119","($FG)",],["(\\d{2})(\\d{4})(\\d{4})","$1 $2-$3","[1-9][1-9]","($FG)",],["([34]00\\d)(\\d{4})","$1-$2","[34]00",,],["([3589]00)(\\d{2,3})(\\d{4})","$1 $2 $3","[3589]00","$NP$FG",]]]',
+"674": '["NR","00",,,,,"\\d{7}","[458]\\d{6}",[["(\\d{3})(\\d{4})","$1 $2",,,]]]',
+"967": '["YE","00","0",,,"$NP$FG","\\d{6,9}","[1-7]\\d{6,8}",[["([1-7])(\\d{3})(\\d{3,4})","$1 $2 $3","[1-6]|7[24-68]",,],["(7\\d{2})(\\d{3})(\\d{3})","$1 $2 $3","7[0137]",,]]]',
+"49": '["DE","00","0",,,"$NP$FG","\\d{2,15}","[1-35-9]\\d{3,14}|4(?:[0-8]\\d{4,12}|9(?:[0-37]\\d|4[1-8]|5\\d{1,2}|6[1-8]\\d?)\\d{2,7})",[["(\\d{2})(\\d{4,11})","$1 $2","3[02]|40|[68]9",,],["(\\d{3})(\\d{3,11})","$1 $2","2(?:\\d1|0[2389]|1[24]|28|34)|3(?:[3-9][15]|40)|[4-8][1-9]1|9(?:06|[1-9]1)",,],["(\\d{4})(\\d{2,11})","$1 $2","[24-6]|[7-9](?:\\d[1-9]|[1-9]\\d)|3(?:[3569][02-46-9]|4[2-4679]|7[2-467]|8[2-46-8])",,],["(\\d{5})(\\d{1,10})","$1 $2","3",,],["(1\\d{2})(\\d{7,8})","$1 $2","1[5-7]",,],["(177)(99)(\\d{7,8})","$1 $2 $3","177",,],["(8\\d{2})(\\d{7,10})","$1 $2","800",,],["(\\d{3})(\\d)(\\d{4,10})","$1 $2 $3","(?:18|90)0",,],["(1\\d{2})(\\d{5,11})","$1 $2","181",,],["(18\\d{3})(\\d{6})","$1 $2","185",,],["(18\\d{2})(\\d{7})","$1 $2","18[68]",,],["(18\\d)(\\d{8})","$1 $2","18[2-579]",,],["(700)(\\d{4})(\\d{4})","$1 $2 $3","700",,]]]',
+"31": '["NL","00","0",,,"$NP$FG","\\d{5,10}","1\\d{4,8}|[2-7]\\d{8}|[89]\\d{6,9}",[["([1-578]\\d)(\\d{3})(\\d{4})","$1 $2 $3","1[035]|2[0346]|3[03568]|4[0356]|5[0358]|7|8[458]",,],["([1-5]\\d{2})(\\d{3})(\\d{3})","$1 $2 $3","1[16-8]|2[259]|3[124]|4[17-9]|5[124679]",,],["(6)(\\d{8})","$1 $2","6[0-57-9]",,],["(66)(\\d{7})","$1 $2","66",,],["(14)(\\d{3,4})","$1 $2","14","$FG",],["([89]0\\d)(\\d{4,7})","$1 $2","80|9",,]]]',
+"970": '["PS","00","0",,,"$NP$FG","\\d{4,10}","[24589]\\d{7,8}|1(?:[78]\\d{8}|[49]\\d{2,3})",[["([2489])(2\\d{2})(\\d{4})","$1 $2 $3","[2489]",,],["(5[69]\\d)(\\d{3})(\\d{3})","$1 $2 $3","5",,],["(1[78]00)(\\d{3})(\\d{3})","$1 $2 $3","1[78]","$FG",]]]',
+"58": '["VE","00","0","(1\\d{2})|0",,"$NP$FG","\\d{7,10}","[24589]\\d{9}",[["(\\d{3})(\\d{7})","$1-$2",,,]]]',
+"856": '["LA","00","0",,,"$NP$FG","\\d{6,10}","[2-8]\\d{7,9}",[["(20)(\\d{2})(\\d{3})(\\d{3})","$1 $2 $3 $4","20",,],["([2-8]\\d)(\\d{3})(\\d{3})","$1 $2 $3","2[13]|[3-8]",,]]]',
+"354": '["IS","00",,,,,"\\d{7,9}","[4-9]\\d{6}|38\\d{7}",[["(\\d{3})(\\d{4})","$1 $2","[4-9]",,],["(3\\d{2})(\\d{3})(\\d{3})","$1 $2 $3","3",,]]]',
+"242": '["CG","00",,,,,"\\d{9}","[028]\\d{8}",[["(\\d{2})(\\d{3})(\\d{4})","$1 $2 $3","[02]",,],["(\\d)(\\d{4})(\\d{4})","$1 $2 $3","8",,]]]',
+"423": '["LI","00","0",,,,"\\d{7,9}","6\\d{8}|[23789]\\d{6}",[["(\\d{3})(\\d{2})(\\d{2})","$1 $2 $3","[23]|7[3-57-9]|87",,],["(6\\d)(\\d{3})(\\d{3})","$1 $2 $3","6",,],["(6[567]\\d)(\\d{3})(\\d{3})","$1 $2 $3","6[567]",,],["(69)(7\\d{2})(\\d{4})","$1 $2 $3","697",,],["([7-9]0\\d)(\\d{2})(\\d{2})","$1 $2 $3","[7-9]0",,],["([89]0\\d)(\\d{2})(\\d{2})(\\d{2})","$1 $2 $3 $4","[89]0","$NP$FG",]]]',
+"213": '["DZ","00","0",,,"$NP$FG","\\d{8,9}","(?:[1-4]|[5-9]\\d)\\d{7}",[["([1-4]\\d)(\\d{2})(\\d{2})(\\d{2})","$1 $2 $3 $4","[1-4]",,],["([5-8]\\d{2})(\\d{2})(\\d{2})(\\d{2})","$1 $2 $3 $4","[5-8]",,],["(9\\d)(\\d{3})(\\d{2})(\\d{2})","$1 $2 $3 $4","9",,]]]',
+"371": '["LV","00",,,,,"\\d{8}","[2689]\\d{7}",[["([2689]\\d)(\\d{3})(\\d{3})","$1 $2 $3",,,]]]',
+"503": '["SV","00",,,,,"\\d{7,8}|\\d{11}","[267]\\d{7}|[89]\\d{6}(?:\\d{4})?",[["(\\d{4})(\\d{4})","$1 $2","[267]",,],["(\\d{3})(\\d{4})","$1 $2","[89]",,],["(\\d{3})(\\d{4})(\\d{4})","$1 $2 $3","[89]",,]]]',
+"685": '["WS","0",,,,,"\\d{5,7}","[2-8]\\d{4,6}",[["(8\\d{2})(\\d{3,4})","$1 $2","8",,],["(7\\d)(\\d{5})","$1 $2","7",,]]]',
+"880": '["BD","00[12]?","0",,,"$NP$FG","\\d{6,10}","[2-79]\\d{5,9}|1\\d{9}|8[0-7]\\d{4,8}",[["(2)(\\d{7})","$1 $2","2",,],["(\\d{2})(\\d{4,6})","$1 $2","[3-79]1",,],["(\\d{3})(\\d{3,7})","$1 $2","[3-79][2-9]|8",,],["(\\d{4})(\\d{6})","$1 $2","1",,]]]',
+"265": '["MW","00","0",,,"$NP$FG","\\d{7,9}","(?:1(?:\\d{2})?|[2789]\\d{2})\\d{6}",[["(\\d)(\\d{3})(\\d{3})","$1 $2 $3","1",,],["(2\\d{2})(\\d{3})(\\d{3})","$1 $2 $3","2",,],["(\\d{3})(\\d{2})(\\d{2})(\\d{2})","$1 $2 $3 $4","[1789]",,]]]',
+"65": '["SG","0[0-3][0-9]",,,,,"\\d{8,11}","[36]\\d{7}|[17-9]\\d{7,10}",[["([3689]\\d{3})(\\d{4})","$1 $2","[369]|8[1-9]",,],["(1[89]00)(\\d{3})(\\d{4})","$1 $2 $3","1[89]",,],["(7000)(\\d{4})(\\d{3})","$1 $2 $3","70",,],["(800)(\\d{3})(\\d{4})","$1 $2 $3","80",,]]]',
+"504": '["HN","00",,,,,"\\d{8}","[237-9]\\d{7}",[["(\\d{4})(\\d{4})","$1-$2",,,]]]',
+"688": '["TV","00",,,,,"\\d{5,6}","[29]\\d{4,5}",]',
+"84": '["VN","00","0",,,"$NP$FG","\\d{7,10}","[17]\\d{6,9}|[2-69]\\d{7,9}|8\\d{6,8}",[["([17]99)(\\d{4})","$1 $2","[17]99",,],["([48])(\\d{4})(\\d{4})","$1 $2 $3","[48]",,],["([235-7]\\d)(\\d{4})(\\d{3})","$1 $2 $3","2[025-79]|3[0136-9]|5[2-9]|6[0-46-8]|7[02-79]",,],["(80)(\\d{5})","$1 $2","80",,],["(69\\d)(\\d{4,5})","$1 $2","69",,],["([235-7]\\d{2})(\\d{4})(\\d{3})","$1 $2 $3","2[1348]|3[25]|5[01]|65|7[18]",,],["(9\\d)(\\d{3})(\\d{2})(\\d{2})","$1 $2 $3 $4","9",,],["(1[2689]\\d)(\\d{3})(\\d{4})","$1 $2 $3","1(?:[26]|8[68]|99)",,],["(1[89]00)(\\d{4,6})","$1 $2","1[89]0","$FG",]]]',
+"255": '["TZ","00[056]","0",,,"$NP$FG","\\d{7,9}","\\d{9}",[["([24]\\d)(\\d{3})(\\d{4})","$1 $2 $3","[24]",,],["([67]\\d{2})(\\d{3})(\\d{3})","$1 $2 $3","[67]",,],["([89]\\d{2})(\\d{2})(\\d{4})","$1 $2 $3","[89]",,]]]',
+"222": '["MR","00",,,,,"\\d{8}","[2-48]\\d{7}",[["([2-48]\\d)(\\d{2})(\\d{2})(\\d{2})","$1 $2 $3 $4",,,]]]',
+"230": '["MU","0(?:[2-7]0|33)",,,,,"\\d{7}","[2-9]\\d{6}",[["([2-9]\\d{2})(\\d{4})","$1 $2",,,]]]',
+"592": '["GY","001",,,,,"\\d{7}","[2-4679]\\d{6}",[["(\\d{3})(\\d{4})","$1 $2",,,]]]',
+"41": '["CH","00","0",,,"$NP$FG","\\d{9}(?:\\d{3})?","[2-9]\\d{8}|860\\d{9}",[["([2-9]\\d)(\\d{3})(\\d{2})(\\d{2})","$1 $2 $3 $4","[2-7]|[89]1",,],["([89]\\d{2})(\\d{3})(\\d{3})","$1 $2 $3","8[047]|90",,],["(\\d{3})(\\d{2})(\\d{3})(\\d{2})(\\d{2})","$1 $2 $3 $4 $5","860",,]]]',
+"39": '["IT","00",,,,,"\\d{6,11}","[01589]\\d{5,10}|3(?:[12457-9]\\d{8}|[36]\\d{7,9})",[["(\\d{2})(\\d{3,4})(\\d{4})","$1 $2 $3","0[26]|55",,],["(0[26])(\\d{4})(\\d{5})","$1 $2 $3","0[26]",,],["(0[26])(\\d{4,6})","$1 $2","0[26]",,],["(0\\d{2})(\\d{3,4})(\\d{4})","$1 $2 $3","0[13-57-9][0159]",,],["(\\d{3})(\\d{3,6})","$1 $2","0[13-57-9][0159]|8(?:03|4[17]|9[245])",,],["(0\\d{3})(\\d{3})(\\d{4})","$1 $2 $3","0[13-57-9][2-46-8]",,],["(0\\d{3})(\\d{2,6})","$1 $2","0[13-57-9][2-46-8]",,],["(\\d{3})(\\d{3})(\\d{3,4})","$1 $2 $3","[13]|8(?:00|4[08]|9[59])",,],["(\\d{4})(\\d{4})","$1 $2","894",,],["(\\d{3})(\\d{4})(\\d{4})","$1 $2 $3","3",,]]]',
+"993": '["TM","810","8",,,"($NP $FG)","\\d{8}","[1-6]\\d{7}",[["(\\d{2})(\\d{2})(\\d{2})(\\d{2})","$1 $2-$3-$4","12",,],["(\\d{2})(\\d{6})","$1 $2","6","$NP $FG",],["(\\d{3})(\\d)(\\d{2})(\\d{2})","$1 $2-$3-$4","13|[2-5]",,]]]',
+"888": '["001",,,,,,"\\d{11}","\\d{11}",[["(\\d{3})(\\d{3})(\\d{5})","$1 $2 $3",,,]]]',
+"353": '["IE","00","0",,,"($NP$FG)","\\d{5,10}","[124-9]\\d{6,9}",[["(1)(\\d{3,4})(\\d{4})","$1 $2 $3","1",,],["(\\d{2})(\\d{5})","$1 $2","2[2-9]|4[347]|5[2-58]|6[2-47-9]|9[3-9]",,],["(\\d{3})(\\d{5})","$1 $2","40[24]|50[45]",,],["(48)(\\d{4})(\\d{4})","$1 $2 $3","48",,],["(818)(\\d{3})(\\d{3})","$1 $2 $3","81",,],["(\\d{2})(\\d{3})(\\d{3,4})","$1 $2 $3","[24-69]|7[14]",,],["([78]\\d)(\\d{3,4})(\\d{4})","$1 $2 $3","76|8[35-9]","$NP$FG",],["(700)(\\d{3})(\\d{3})","$1 $2 $3","70","$NP$FG",],["(\\d{4})(\\d{3})(\\d{3})","$1 $2 $3","1(?:8[059]|5)","$FG",]]]',
+"966": '["SA","00","0",,,"$NP$FG","\\d{7,10}","(?:[1-467]|92)\\d{7}|5\\d{8}|8\\d{9}",[["([1-467])(\\d{3})(\\d{4})","$1 $2 $3","[1-467]",,],["(5\\d)(\\d{3})(\\d{4})","$1 $2 $3","5",,],["(9200)(\\d{5})","$1 $2","9","$FG",],["(800)(\\d{3})(\\d{4})","$1 $2 $3","80","$FG",],["(8111)(\\d{3})(\\d{3})","$1 $2 $3","81",,]]]',
+"380": '["UA","00","0",,,"$NP$FG","\\d{5,9}","[3-689]\\d{8}",[["([3-69]\\d)(\\d{3})(\\d{4})","$1 $2 $3","39|4(?:[45][0-5]|87)|5(?:0|6[37]|7[37])|6[36-8]|9[1-9]",,],["([3-689]\\d{2})(\\d{3})(\\d{3})","$1 $2 $3","3[1-8]2|4[1378]2|5(?:[12457]2|6[24])|6(?:[49]2|[12][29]|5[24])|8|90",,],["([3-6]\\d{3})(\\d{5})","$1 $2","3(?:5[013-9]|[1-46-8])|4(?:[137][013-9]|6|[45][6-9]|8[4-6])|5(?:[1245][013-9]|6[0135-9]|3|7[4-6])|6(?:[49][013-9]|5[0135-9]|[12][13-8])",,]]]',
+"98": '["IR","00","0",,,"$NP$FG","\\d{4,10}","[2-6]\\d{4,9}|9(?:[1-4]\\d{8}|9\\d{2,8})|[178]\\d{9}",[["(21)(\\d{3,5})","$1 $2","21",,],["(21)(\\d{3})(\\d{3,4})","$1 $2 $3","21",,],["(21)(\\d{4})(\\d{4})","$1 $2 $3","21",,],["(\\d{3})(\\d{3})(\\d{3,4})","$1 $2 $3","[13-9]|2[02-9]",,]]]',
+"971": '["AE","00","0",,,"$NP$FG","\\d{5,12}","[2-79]\\d{7,8}|800\\d{2,9}",[["([2-4679])(\\d{3})(\\d{4})","$1 $2 $3","[2-4679][2-8]",,],["(5[0256])(\\d{3})(\\d{4})","$1 $2 $3","5",,],["([4679]00)(\\d)(\\d{5})","$1 $2 $3","[4679]0","$FG",],["(800)(\\d{2,9})","$1 $2","8","$FG",]]]',
+"30": '["GR","00",,,,,"\\d{10}","[26-9]\\d{9}",[["([27]\\d)(\\d{4})(\\d{4})","$1 $2 $3","21|7",,],["(\\d{3})(\\d{3})(\\d{4})","$1 $2 $3","2[2-9]1|[689]",,],["(2\\d{3})(\\d{6})","$1 $2","2[2-9][02-9]",,]]]',
+"228": '["TG","00",,,,,"\\d{8}","[29]\\d{7}",[["(\\d{2})(\\d{2})(\\d{2})(\\d{2})","$1 $2 $3 $4",,,]]]',
+"48": '["PL","00",,,,,"\\d{6,9}","[1-58]\\d{6,8}|9\\d{8}|[67]\\d{5,8}",[["(\\d{2})(\\d{3})(\\d{2})(\\d{2})","$1 $2 $3 $4","[124]|3[2-4]|5[24-689]|6[1-3578]|7[14-7]|8[1-79]|9[145]",,],["(\\d{2})(\\d{4,6})","$1 $2","[124]|3[2-4]|5[24-689]|6[1-3578]|7[14-7]|8[1-7]",,],["(\\d{3})(\\d{3})(\\d{3})","$1 $2 $3","39|5[013]|6[0469]|7[0289]|8[08]",,],["(\\d{3})(\\d{2})(\\d{2,3})","$1 $2 $3","64",,],["(\\d{3})(\\d{3})","$1 $2","64",,]]]',
+"886": '["TW","0(?:0[25679]|19)","0",,,"$NP$FG","\\d{8,9}","[2-9]\\d{7,8}",[["([2-8])(\\d{3,4})(\\d{4})","$1 $2 $3","[2-7]|8[1-9]",,],["([89]\\d{2})(\\d{3})(\\d{3})","$1 $2 $3","80|9",,]]]',
+"212": '["MA","00","0",,,"$NP$FG","\\d{9}","[5689]\\d{8}",[["([56]\\d{2})(\\d{6})","$1-$2","5(?:2[015-7]|3[0-4])|6",,],["([58]\\d{3})(\\d{5})","$1-$2","5(?:2[2-489]|3[5-9])|892",,],["(5\\d{4})(\\d{4})","$1-$2","5(?:29|38)",,],["(8[09])(\\d{7})","$1-$2","8(?:0|9[013-9])",,]]]',
+"372": '["EE","00",,,,,"\\d{4,10}","1\\d{3,4}|[3-9]\\d{6,7}|800\\d{6,7}",[["([3-79]\\d{2})(\\d{4})","$1 $2","[369]|4[3-8]|5(?:[0-2]|5[0-478]|6[45])|7[1-9]",,],["(70)(\\d{2})(\\d{4})","$1 $2 $3","70",,],["(8000)(\\d{3})(\\d{3})","$1 $2 $3","800",,],["([458]\\d{3})(\\d{3,4})","$1 $2","40|5|8(?:00|[1-5])",,]]]',
+"598": '["UY","0(?:1[3-9]\\d|0)","0",,,,"\\d{7,8}","[2489]\\d{6,7}",[["(\\d{4})(\\d{4})","$1 $2","[24]",,],["(\\d{2})(\\d{3})(\\d{3})","$1 $2 $3","9[1-9]","$NP$FG",],["(\\d{3})(\\d{4})","$1 $2","[89]0","$NP$FG",]]]',
+"502": '["GT","00",,,,,"\\d{8}(?:\\d{3})?","[2-7]\\d{7}|1[89]\\d{9}",[["(\\d{4})(\\d{4})","$1 $2","[2-7]",,],["(\\d{4})(\\d{3})(\\d{4})","$1 $2 $3","1",,]]]',
+"82": '["KR","00(?:[124-68]|[37]\\d{2})","0","0(8[1-46-8]|85\\d{2})?",,"$NP$FG","\\d{4,10}","[1-7]\\d{3,9}|8\\d{8}",[["(\\d{2})(\\d{4})(\\d{4})","$1-$2-$3","1(?:0|1[19]|[69]9|5[458])|[57]0",,],["(\\d{2})(\\d{3,4})(\\d{4})","$1-$2-$3","1(?:[169][2-8]|[78]|5[1-4])|[68]0|[3-6][1-9][2-9]",,],["(\\d{3})(\\d)(\\d{4})","$1-$2-$3","131",,],["(\\d{3})(\\d{2})(\\d{4})","$1-$2-$3","131",,],["(\\d{3})(\\d{3})(\\d{4})","$1-$2-$3","13[2-9]",,],["(\\d{2})(\\d{2})(\\d{3})(\\d{4})","$1-$2-$3-$4","30",,],["(\\d)(\\d{3,4})(\\d{4})","$1-$2-$3","2[2-9]",,],["(\\d)(\\d{3,4})","$1-$2","21[0-46-9]",,],["(\\d{2})(\\d{3,4})","$1-$2","[3-6][1-9]1",,],["(\\d{4})(\\d{4})","$1-$2","1(?:5[46-9]|6[04678])","$FG",]]]',
+"253": '["DJ","00",,,,,"\\d{8}","[27]\\d{7}",[["(\\d{2})(\\d{2})(\\d{2})(\\d{2})","$1 $2 $3 $4",,,]]]',
+"91": '["IN","00","0",,,"$NP$FG","\\d{6,13}","1\\d{7,12}|[2-9]\\d{9,10}",[["(\\d{2})(\\d{2})(\\d{6})","$1 $2 $3","7(?:2[0579]|3[057-9]|4[0-389]|5[024-9]|6[0-35-9]|7[03469]|8[0-4679])|8(?:0[01589]|1[0-479]|2[236-9]|3[0-57-9]|[45]|6[0245789]|7[1-69]|8[0124-9]|9[02-9])|9",,],["(\\d{2})(\\d{4})(\\d{4})","$1 $2 $3","11|2[02]|33|4[04]|79|80[2-46]",,],["(\\d{3})(\\d{3})(\\d{4})","$1 $2 $3","1(?:2[0-249]|3[0-25]|4[145]|[569][14]|7[1257]|8[1346]|[68][1-9])|2(?:1[257]|3[013]|4[01]|5[0137]|6[0158]|78|8[1568]|9[14])|3(?:26|4[1-3]|5[34]|6[01489]|7[02-46]|8[159])|4(?:1[36]|2[1-47]|3[15]|5[12]|6[126-9]|7[0-24-9]|8[013-57]|9[014-7])|5(?:[136][25]|22|4[28]|5[12]|[78]1|9[15])|6(?:12|[2345]1|57|6[13]|7[14]|80)",,],["(\\d{3})(\\d{3})(\\d{4})","$1 $2 $3","7(?:12|2[14]|3[134]|4[47]|5[15]|[67]1|88)",,],["(\\d{3})(\\d{3})(\\d{4})","$1 $2 $3","8(?:16|2[014]|3[126]|6[136]|7[078]|8[34]|91)",,],["(\\d{4})(\\d{3})(\\d{3})","$1 $2 $3","1(?:[2-579]|[68][1-9])|[2-8]",,],["(1600)(\\d{2})(\\d{4})","$1 $2 $3","160","$FG",],["(1800)(\\d{4,5})","$1 $2","180","$FG",],["(18[06]0)(\\d{2,4})(\\d{4})","$1 $2 $3","18[06]","$FG",],["(\\d{4})(\\d{3})(\\d{4})(\\d{2})","$1 $2 $3 $4","18[06]","$FG",]]]',
+"389": '["MK","00","0",,,"$NP$FG","\\d{8}","[2-578]\\d{7}",[["(2)(\\d{3})(\\d{4})","$1 $2 $3","2",,],["([347]\\d)(\\d{3})(\\d{3})","$1 $2 $3","[347]",,],["([58]\\d{2})(\\d)(\\d{2})(\\d{2})","$1 $2 $3 $4","[58]",,]]]',
+"1": ['["US","011","1",,,,"\\d{7}(?:\\d{3})?","[2-9]\\d{9}",[["(\\d{3})(\\d{4})","$1-$2",,,"NA"],["(\\d{3})(\\d{3})(\\d{4})","($1) $2-$3",,,"$1-$2-$3"]]]','["AI","011","1",,,,"\\d{7}(?:\\d{3})?","[2589]\\d{9}",]','["AS","011","1",,,,"\\d{7}(?:\\d{3})?","[5689]\\d{9}",]','["BB","011","1",,,,"\\d{7}(?:\\d{3})?","[2589]\\d{9}",]','["BM","011","1",,,,"\\d{7}(?:\\d{3})?","[4589]\\d{9}",]','["BS","011","1",,,,"\\d{7}(?:\\d{3})?","[2589]\\d{9}",]','["CA","011","1",,,,"\\d{7}(?:\\d{3})?","[2-9]\\d{9}|3\\d{6}",]','["DM","011","1",,,,"\\d{7}(?:\\d{3})?","[57-9]\\d{9}",]','["DO","011","1",,,,"\\d{7}(?:\\d{3})?","[589]\\d{9}",]','["GD","011","1",,,,"\\d{7}(?:\\d{3})?","[4589]\\d{9}",]','["GU","011","1",,,,"\\d{7}(?:\\d{3})?","[5689]\\d{9}",]','["JM","011","1",,,,"\\d{7}(?:\\d{3})?","[589]\\d{9}",]','["KN","011","1",,,,"\\d{7}(?:\\d{3})?","[589]\\d{9}",]','["KY","011","1",,,,"\\d{7}(?:\\d{3})?","[3589]\\d{9}",]','["LC","011","1",,,,"\\d{7}(?:\\d{3})?","[5789]\\d{9}",]','["MP","011","1",,,,"\\d{7}(?:\\d{3})?","[5689]\\d{9}",]','["MS","011","1",,,,"\\d{7}(?:\\d{3})?","[5689]\\d{9}",]','["PR","011","1",,,,"\\d{7}(?:\\d{3})?","[5789]\\d{9}",]','["SX","011","1",,,,"\\d{7}(?:\\d{3})?","[5789]\\d{9}",]','["TC","011","1",,,,"\\d{7}(?:\\d{3})?","[5689]\\d{9}",]','["TT","011","1",,,,"\\d{7}(?:\\d{3})?","[589]\\d{9}",]','["AG","011","1",,,,"\\d{7}(?:\\d{3})?","[2589]\\d{9}",]','["VC","011","1",,,,"\\d{7}(?:\\d{3})?","[5789]\\d{9}",]','["VG","011","1",,,,"\\d{7}(?:\\d{3})?","[2589]\\d{9}",]','["VI","011","1",,,,"\\d{7}(?:\\d{3})?","[3589]\\d{9}",]'],
+"60": '["MY","00","0",,,,"\\d{6,10}","[13-9]\\d{7,9}",[["([4-79])(\\d{3})(\\d{4})","$1-$2 $3","[4-79]","$NP$FG",],["(3)(\\d{4})(\\d{4})","$1-$2 $3","3","$NP$FG",],["([18]\\d)(\\d{3})(\\d{3,4})","$1-$2 $3","1[02-46-9][1-9]|8","$NP$FG",],["(1)([36-8]00)(\\d{2})(\\d{4})","$1-$2-$3-$4","1[36-8]0",,],["(11)(\\d{4})(\\d{4})","$1-$2 $3","11","$NP$FG",],["(154)(\\d{3})(\\d{4})","$1-$2 $3","15","$NP$FG",]]]',
+"355": '["AL","00","0",,,"$NP$FG","\\d{5,9}","[2-57]\\d{7}|6\\d{8}|8\\d{5,7}|9\\d{5}",[["(4)(\\d{3})(\\d{4})","$1 $2 $3","4[0-6]",,],["(6[6-9])(\\d{3})(\\d{4})","$1 $2 $3","6",,],["(\\d{2})(\\d{3})(\\d{3})","$1 $2 $3","[2358][2-5]|4[7-9]",,],["(\\d{3})(\\d{3,5})","$1 $2","[235][16-9]|8[016-9]|[79]",,]]]',
+"254": '["KE","000","0",,,"$NP$FG","\\d{5,10}","20\\d{6,7}|[4-9]\\d{6,9}",[["(\\d{2})(\\d{4,7})","$1 $2","[24-6]",,],["(\\d{3})(\\d{6,7})","$1 $2","7",,],["(\\d{3})(\\d{3})(\\d{3,4})","$1 $2 $3","[89]",,]]]',
+"223": '["ML","00",,,,,"\\d{8}","[246-8]\\d{7}",[["([246-8]\\d)(\\d{2})(\\d{2})(\\d{2})","$1 $2 $3 $4",,,]]]',
+"686": '["KI","00",,"0",,,"\\d{5}","[2-689]\\d{4}",]',
+"994": '["AZ","00","0",,,"($NP$FG)","\\d{7,9}","[1-9]\\d{8}",[["(\\d{2})(\\d{3})(\\d{2})(\\d{2})","$1 $2 $3 $4","(?:1[28]|2(?:[45]2|[0-36])|365)",,],["(\\d{2})(\\d{3})(\\d{2})(\\d{2})","$1 $2 $3 $4","[4-8]","$NP$FG",],["(\\d{3})(\\d{2})(\\d{2})(\\d{2})","$1 $2 $3 $4","9","$NP$FG",]]]',
+"979": '["001",,,,,,"\\d{9}","\\d{9}",[["(\\d)(\\d{4})(\\d{4})","$1 $2 $3",,,]]]',
+"66": '["TH","00","0",,,"$NP$FG","\\d{4}|\\d{8,10}","[2-9]\\d{7,8}|1\\d{3}(?:\\d{6})?",[["(2)(\\d{3})(\\d{4})","$1 $2 $3","2",,],["([3-9]\\d)(\\d{3})(\\d{3,4})","$1 $2 $3","[3-9]",,],["(1[89]00)(\\d{3})(\\d{3})","$1 $2 $3","1","$FG",]]]',
+"233": '["GH","00","0",,,"$NP$FG","\\d{7,9}","[235]\\d{8}|8\\d{7}",[["(\\d{2})(\\d{3})(\\d{4})","$1 $2 $3","[235]",,],["(\\d{3})(\\d{5})","$1 $2","8",,]]]',
+"593": '["EC","00","0",,,"($NP$FG)","\\d{7,11}","1\\d{9,10}|[2-8]\\d{7}|9\\d{8}",[["(\\d)(\\d{3})(\\d{4})","$1 $2-$3","[247]|[356][2-8]",,"$1-$2-$3"],["(\\d{2})(\\d{3})(\\d{4})","$1 $2 $3","9","$NP$FG",],["(1800)(\\d{3})(\\d{3,4})","$1 $2 $3","1","$FG",]]]',
+"509": '["HT","00",,,,,"\\d{8}","[2-489]\\d{7}",[["(\\d{2})(\\d{2})(\\d{4})","$1 $2 $3",,,]]]',
+"54": '["AR","00","0","0?(?:(11|2(?:2(?:02?|[13]|2[13-79]|4[1-6]|5[2457]|6[124-8]|7[1-4]|8[13-6]|9[1267])|3(?:02?|1[467]|2[03-6]|3[13-8]|[49][2-6]|5[2-8]|[67])|4(?:7[3-578]|9)|6(?:[0136]|2[24-6]|4[6-8]?|5[15-8])|80|9(?:0[1-3]|[19]|2\\d|3[1-6]|4[02568]?|5[2-4]|6[2-46]|72?|8[23]?))|3(?:3(?:2[79]|6|8[2578])|4(?:0[124-9]|[12]|3[5-8]?|4[24-7]|5[4-68]?|6[02-9]|7[126]|8[2379]?|9[1-36-8])|5(?:1|2[1245]|3[237]?|4[1-46-9]|6[2-4]|7[1-6]|8[2-5]?)|6[24]|7(?:1[1568]|2[15]|3[145]|4[13]|5[14-8]|[069]|7[2-57]|8[126])|8(?:[01]|2[15-7]|3[2578]?|4[13-6]|5[4-8]?|6[1-357-9]|7[36-8]?|8[5-8]?|9[124])))15)?","9$1","$NP$FG","\\d{6,11}","[1-368]\\d{9}|9\\d{10}",[["([68]\\d{2})(\\d{3})(\\d{4})","$1-$2-$3","[68]",,],["(9)(11)(\\d{4})(\\d{4})","$2 15-$3-$4","911",,"$1 $2 $3-$4"],["(9)(\\d{3})(\\d{3})(\\d{4})","$2 15-$3-$4","9(?:2[234689]|3[3-8])",,"$1 $2 $3-$4"],["(9)(\\d{4})(\\d{3})(\\d{3})","$2 15-$3-$4","93[58]",,],["(9)(\\d{4})(\\d{2})(\\d{4})","$2 15-$3-$4","9[23]",,"$1 $2 $3-$4"],["(11)(\\d{4})(\\d{4})","$1 $2-$3","1",,],["(\\d{3})(\\d{3})(\\d{4})","$1 $2-$3","2(?:2[013]|3[067]|49|6[01346]|80|9[147-9])|3(?:36|4[12358]|5[138]|6[24]|7[069]|8[013578])",,],["(\\d{4})(\\d{3})(\\d{3})","$1 $2-$3","3(?:53|8[78])",,],["(\\d{4})(\\d{2})(\\d{4})","$1 $2-$3","[23]",,]]]',
+"57": '["CO","00[579]|#555|#999","0","0([3579]|4(?:44|56))?",,,"\\d{7,11}","(?:[13]\\d{0,3}|[24-8])\\d{7}",[["(\\d)(\\d{7})","$1 $2","1(?:8[2-9]|9[0-3]|[2-7])|[24-8]","($FG)",],["(\\d{3})(\\d{7})","$1 $2","3",,],["(1)(\\d{3})(\\d{7})","$1-$2-$3","1(?:80|9[04])","$NP$FG","$1 $2 $3"]]]',
+"597": '["SR","00",,,,,"\\d{6,7}","[2-8]\\d{5,6}",[["(\\d{3})(\\d{3})","$1-$2","[2-4]|5[2-58]",,],["(\\d{2})(\\d{2})(\\d{2})","$1-$2-$3","56",,],["(\\d{3})(\\d{4})","$1-$2","[6-8]",,]]]',
+"676": '["TO","00",,,,,"\\d{5,7}","[02-8]\\d{4,6}",[["(\\d{2})(\\d{3})","$1-$2","[1-6]|7[0-4]|8[05]",,],["(\\d{3})(\\d{4})","$1 $2","7[5-9]|8[7-9]",,],["(\\d{4})(\\d{3})","$1 $2","0",,]]]',
+"505": '["NI","00",,,,,"\\d{8}","[128]\\d{7}",[["(\\d{4})(\\d{4})","$1 $2",,,]]]',
+"850": '["KP","00|99","0",,,"$NP$FG","\\d{6,8}|\\d{10}","1\\d{9}|[28]\\d{7}",[["(\\d{3})(\\d{3})(\\d{4})","$1 $2 $3","1",,],["(\\d)(\\d{3})(\\d{4})","$1 $2 $3","2",,],["(\\d{2})(\\d{3})(\\d{3})","$1 $2 $3","8",,]]]',
+"7": ['["RU","810","8",,,"$NP ($FG)","\\d{10}","[3489]\\d{9}",[["(\\d{3})(\\d{2})(\\d{2})","$1-$2-$3","[1-79]","$FG","NA"],["([3489]\\d{2})(\\d{3})(\\d{2})(\\d{2})","$1 $2-$3-$4","[34689]",,],["(7\\d{2})(\\d{3})(\\d{4})","$1 $2 $3","7",,]]]','["KZ","810","8",,,,"\\d{10}","(?:33\\d|7\\d{2}|80[09])\\d{7}",]'],
+"268": '["SZ","00",,,,,"\\d{8}","[027]\\d{7}",[["(\\d{4})(\\d{4})","$1 $2","[027]",,]]]',
+"501": '["BZ","00",,,,,"\\d{7}(?:\\d{4})?","[2-8]\\d{6}|0\\d{10}",[["(\\d{3})(\\d{4})","$1-$2","[2-8]",,],["(0)(800)(\\d{4})(\\d{3})","$1-$2-$3-$4","0",,]]]',
+"252": '["SO","00","0",,,,"\\d{7,9}","[1-79]\\d{6,8}",[["(\\d)(\\d{6})","$1 $2","2[0-79]|[13-5]",,],["(\\d)(\\d{7})","$1 $2","24|[67]",,],["(\\d{2})(\\d{5,7})","$1 $2","15|28|6[178]|9",,],["(69\\d)(\\d{6})","$1 $2","69",,]]]',
+"229": '["BJ","00",,,,,"\\d{4,8}","[2689]\\d{7}|7\\d{3}",[["(\\d{2})(\\d{2})(\\d{2})(\\d{2})","$1 $2 $3 $4",,,]]]',
+"680": '["PW","01[12]",,,,,"\\d{7}","[2-8]\\d{6}",[["(\\d{3})(\\d{4})","$1 $2",,,]]]',
+"263": '["ZW","00","0",,,"$NP$FG","\\d{3,10}","2(?:[012457-9]\\d{3,8}|6\\d{3,6})|[13-79]\\d{4,8}|86\\d{8}",[["([49])(\\d{3})(\\d{2,5})","$1 $2 $3","4|9[2-9]",,],["([179]\\d)(\\d{3})(\\d{3,4})","$1 $2 $3","[19]1|7",,],["(86\\d{2})(\\d{3})(\\d{3})","$1 $2 $3","86[24]",,],["([1-356]\\d)(\\d{3,5})","$1 $2","1[3-9]|2(?:[1-469]|0[0-35-9]|[45][0-79])|3(?:0[0-79]|1[0-689]|[24-69]|3[0-69])|5(?:[02-46-9]|[15][0-69])|6(?:[0145]|[29][0-79]|3[0-689]|[68][0-69])",,],["([1-356]\\d)(\\d{3})(\\d{3})","$1 $2 $3","1[3-9]|2(?:[1-469]|0[0-35-9]|[45][0-79])|3(?:0[0-79]|1[0-689]|[24-69]|3[0-69])|5(?:[02-46-9]|[15][0-69])|6(?:[0145]|[29][0-79]|3[0-689]|[68][0-69])",,],["([2356]\\d{2})(\\d{3,5})","$1 $2","2(?:[278]|0[45]|48)|3(?:08|17|3[78]|[78])|5[15][78]|6(?:[29]8|37|[68][78])",,],["([2356]\\d{2})(\\d{3})(\\d{3})","$1 $2 $3","2(?:[278]|0[45]|48)|3(?:08|17|3[78]|[78])|5[15][78]|6(?:[29]8|37|[68][78])",,],["([25]\\d{3})(\\d{3,5})","$1 $2","(?:25|54)8",,],["([25]\\d{3})(\\d{3})(\\d{3})","$1 $2 $3","(?:25|54)8",,],["(8\\d{3})(\\d{6})","$1 $2","86[1389]",,]]]',
+"90": '["TR","00","0",,,,"\\d{7,10}","[2-589]\\d{9}|444\\d{4}",[["(\\d{3})(\\d{3})(\\d{4})","$1 $2 $3","[23]|4(?:[0-35-9]|4[0-35-9])","($NP$FG)",],["(\\d{3})(\\d{3})(\\d{4})","$1 $2 $3","[589]","$NP$FG",],["(444)(\\d{1})(\\d{3})","$1 $2 $3","444",,]]]',
+"352": '["LU","00",,"(15(?:0[06]|1[12]|35|4[04]|55|6[26]|77|88|99)\\d)",,,"\\d{4,11}","[24-9]\\d{3,10}|3(?:[0-46-9]\\d{2,9}|5[013-9]\\d{1,8})",[["(\\d{2})(\\d{3})","$1 $2","[2-5]|7[1-9]|[89](?:[1-9]|0[2-9])",,],["(\\d{2})(\\d{2})(\\d{2})","$1 $2 $3","[2-5]|7[1-9]|[89](?:[1-9]|0[2-9])",,],["(\\d{2})(\\d{2})(\\d{3})","$1 $2 $3","20",,],["(\\d{2})(\\d{2})(\\d{2})(\\d{1,2})","$1 $2 $3 $4","2(?:[0367]|4[3-8])",,],["(\\d{2})(\\d{2})(\\d{2})(\\d{3})","$1 $2 $3 $4","20",,],["(\\d{2})(\\d{2})(\\d{2})(\\d{2})(\\d{1,2})","$1 $2 $3 $4 $5","2(?:[0367]|4[3-8])",,],["(\\d{2})(\\d{2})(\\d{2})(\\d{1,4})","$1 $2 $3 $4","2(?:[12589]|4[12])|[3-5]|7[1-9]|[89](?:[1-9]|0[2-9])",,],["(\\d{3})(\\d{2})(\\d{3})","$1 $2 $3","[89]0[01]|70",,],["(\\d{3})(\\d{3})(\\d{3})","$1 $2 $3","6",,]]]',
+"47": ['["NO","00",,,,,"\\d{5}(?:\\d{3})?","0\\d{4}|[2-9]\\d{7}",[["([489]\\d{2})(\\d{2})(\\d{3})","$1 $2 $3","[489]",,],["([235-7]\\d)(\\d{2})(\\d{2})(\\d{2})","$1 $2 $3 $4","[235-7]",,]]]','["SJ","00",,,,,"\\d{5}(?:\\d{3})?","0\\d{4}|[4789]\\d{7}",]'],
+"243": '["CD","00","0",,,"$NP$FG","\\d{7,9}","[1-6]\\d{6}|8\\d{6,8}|9\\d{8}",[["([89]\\d{2})(\\d{3})(\\d{3})","$1 $2 $3","8[0-259]|9",,],["(\\d{2})(\\d{2})(\\d{3})","$1 $2 $3","8[48]",,],["(\\d{2})(\\d{5})","$1 $2","[1-6]",,]]]',
+"220": '["GM","00",,,,,"\\d{7}","[2-9]\\d{6}",[["(\\d{3})(\\d{4})","$1 $2",,,]]]',
+"687": '["NC","00",,,,,"\\d{6}","[2-47-9]\\d{5}",[["(\\d{2})(\\d{2})(\\d{2})","$1.$2.$3",,,]]]',
+"995": '["GE","810","8",,,,"\\d{6,9}","[3458]\\d{8}",[["(\\d{3})(\\d{2})(\\d{2})(\\d{2})","$1 $2 $3 $4","[348]","$NP $FG",],["(\\d{3})(\\d{2})(\\d{2})(\\d{2})","$1 $2 $3 $4","5","$FG",]]]',
+"961": '["LB","00","0",,,,"\\d{7,8}","[13-9]\\d{6,7}",[["(\\d)(\\d{3})(\\d{3})","$1 $2 $3","[13-6]|7(?:[2-579]|62|8[0-7])|[89][2-9]","$NP$FG",],["([7-9]\\d)(\\d{3})(\\d{3})","$1 $2 $3","[89][01]|7(?:[01]|6[013-9]|8[89]|91)",,]]]',
+"40": '["RO","00","0",,,"$NP$FG","\\d{6,9}","2\\d{5,8}|[37-9]\\d{8}",[["([237]\\d)(\\d{3})(\\d{4})","$1 $2 $3","[23]1|7",,],["(21)(\\d{4})","$1 $2","21",,],["(\\d{3})(\\d{3})(\\d{3})","$1 $2 $3","[23][3-7]|[89]",,],["(2\\d{2})(\\d{3})","$1 $2","2[3-6]",,]]]',
+"232": '["SL","00","0",,,"($NP$FG)","\\d{6,8}","[2-578]\\d{7}",[["(\\d{2})(\\d{6})","$1 $2",,,]]]',
+"594": '["GF","00","0",,,"$NP$FG","\\d{9}","[56]\\d{8}",[["(\\d{3})(\\d{2})(\\d{2})(\\d{2})","$1 $2 $3 $4",,,]]]',
+"976": '["MN","001","0",,,"$NP$FG","\\d{6,10}","[12]\\d{7,9}|[57-9]\\d{7}",[["([12]\\d)(\\d{2})(\\d{4})","$1 $2 $3","[12]1",,],["([12]2\\d)(\\d{5,6})","$1 $2","[12]2[1-3]",,],["([12]\\d{3})(\\d{5})","$1 $2","[12](?:27|[3-5])",,],["(\\d{4})(\\d{4})","$1 $2","[57-9]","$FG",],["([12]\\d{4})(\\d{4,5})","$1 $2","[12](?:27|[3-5])",,]]]',
+"20": '["EG","00","0",,,"$NP$FG","\\d{5,10}","1\\d{4,9}|[2456]\\d{8}|3\\d{7}|[89]\\d{8,9}",[["(\\d)(\\d{7,8})","$1 $2","[23]",,],["(\\d{3})(\\d{3})(\\d{4})","$1 $2 $3","1[012]|[89]00",,],["(\\d{2})(\\d{6,7})","$1 $2","1(?:3|5[23])|[4-6]|[89][2-9]",,]]]',
+"689": '["PF","00",,,,,"\\d{6}","[2-9]\\d{5}",[["(\\d{2})(\\d{2})(\\d{2})","$1 $2 $3",,,]]]',
+"56": '["CL","(?:0|1(?:1[0-69]|2[0-57]|5[13-58]|69|7[0167]|8[018]))0","0","0|(1(?:1[0-69]|2[0-57]|5[13-58]|69|7[0167]|8[018]))",,"$NP$FG","\\d{6,11}","(?:[2-9]|600|123)\\d{7,8}",[["(2)(\\d{3})(\\d{4})","$1 $2 $3","2","($FG)",],["(\\d{2})(\\d{2,3})(\\d{4})","$1 $2 $3","[357]|4[1-35]|6[13-57]","($FG)",],["(9)([5-9]\\d{3})(\\d{4})","$1 $2 $3","9",,],["(44)(\\d{3})(\\d{4})","$1 $2 $3","44",,],["([68]00)(\\d{3})(\\d{3,4})","$1 $2 $3","60|8","$FG",],["(600)(\\d{3})(\\d{2})(\\d{3})","$1 $2 $3 $4","60","$FG",],["(1230)(\\d{3})(\\d{4})","$1 $2 $3","1","$FG",]]]',
+"596": '["MQ","00","0",,,"$NP$FG","\\d{9}","[56]\\d{8}",[["(\\d{3})(\\d{2})(\\d{2})(\\d{2})","$1 $2 $3 $4",,,]]]',
+"508": '["PM","00","0",,,"$NP$FG","\\d{6}","[45]\\d{5}",[["([45]\\d)(\\d{2})(\\d{2})","$1 $2 $3",,,]]]',
+"269": '["KM","00",,,,,"\\d{7}","[379]\\d{6}",[["(\\d{3})(\\d{2})(\\d{2})","$1 $2 $3",,,]]]',
+"358": ['["FI","00|99[049]","0",,,"$NP$FG","\\d{5,12}","1\\d{4,11}|[2-9]\\d{4,10}",[["(\\d{3})(\\d{3,7})","$1 $2","(?:[1-3]00|[6-8]0)",,],["(\\d{2})(\\d{4,10})","$1 $2","2[09]|[14]|50|7[135]",,],["(\\d)(\\d{4,11})","$1 $2","[25689][1-8]|3",,]]]','["AX","00|99[049]","0",,,"$NP$FG","\\d{5,12}","[135]\\d{5,9}|[27]\\d{4,9}|4\\d{5,10}|6\\d{7,8}|8\\d{6,9}",]'],
+"251": '["ET","00","0",,,"$NP$FG","\\d{7,9}","[1-59]\\d{8}",[["([1-59]\\d)(\\d{3})(\\d{4})","$1 $2 $3",,,]]]',
+"681": '["WF","00",,,,,"\\d{6}","[5-7]\\d{5}",[["(\\d{2})(\\d{2})(\\d{2})","$1 $2 $3",,,]]]',
+"853": '["MO","00",,,,,"\\d{8}","[268]\\d{7}",[["([268]\\d{3})(\\d{4})","$1 $2",,,]]]',
+"44": ['["GB","00","0",,,"$NP$FG","\\d{4,10}","\\d{7,10}",[["(\\d{2})(\\d{4})(\\d{4})","$1 $2 $3","2|5[56]|7(?:0|6[013-9])",,],["(\\d{3})(\\d{3})(\\d{4})","$1 $2 $3","1(?:1|\\d1)|3|9[018]",,],["(\\d{5})(\\d{4,5})","$1 $2","1(?:38|5[23]|69|76|94)",,],["(1\\d{3})(\\d{5,6})","$1 $2","1",,],["(7\\d{3})(\\d{6})","$1 $2","7(?:[1-5789]|62)",,],["(800)(\\d{4})","$1 $2","800",,],["(845)(46)(4\\d)","$1 $2 $3","845",,],["(8\\d{2})(\\d{3})(\\d{4})","$1 $2 $3","8(?:4[2-5]|7[0-3])",,],["(80\\d)(\\d{3})(\\d{4})","$1 $2 $3","80",,],["([58]00)(\\d{6})","$1 $2","[58]00",,]]]','["GG","00","0",,,"$NP$FG","\\d{6,10}","[135789]\\d{6,9}",]','["IM","00","0",,,"$NP$FG","\\d{6,10}","[135789]\\d{6,9}",]','["JE","00","0",,,"$NP$FG","\\d{6,10}","[135789]\\d{6,9}",]'],
+"244": '["AO","00",,,,,"\\d{9}","[29]\\d{8}",[["(\\d{3})(\\d{3})(\\d{3})","$1 $2 $3",,,]]]',
+"211": '["SS","00","0",,,,"\\d{9}","[19]\\d{8}",[["(\\d{3})(\\d{3})(\\d{3})","$1 $2 $3",,"$NP$FG",]]]',
+"373": '["MD","00","0",,,"$NP$FG","\\d{8}","[235-9]\\d{7}",[["(\\d{2})(\\d{3})(\\d{3})","$1 $2 $3","22|3",,],["([25-7]\\d{2})(\\d{2})(\\d{3})","$1 $2 $3","2[13-79]|[5-7]",,],["([89]\\d{2})(\\d{5})","$1 $2","[89]",,]]]',
+"996": '["KG","00","0",,,"$NP$FG","\\d{5,10}","[35-8]\\d{8,9}",[["(\\d{3})(\\d{3})(\\d{3})","$1 $2 $3","31[25]|[5-7]",,],["(\\d{4})(\\d{5})","$1 $2","3(?:1[36]|[2-9])",,],["(\\d{3})(\\d{3})(\\d)(\\d{3})","$1 $2 $3 $4","8",,]]]',
+"93": '["AF","00","0",,,"$NP$FG","\\d{7,9}","[2-7]\\d{8}",[["([2-7]\\d)(\\d{3})(\\d{4})","$1 $2 $3",,,]]]',
+"260": '["ZM","00","0",,,"$NP$FG","\\d{9}","[289]\\d{8}",[["([29]\\d)(\\d{7})","$1 $2","[29]",,],["(800)(\\d{3})(\\d{3})","$1 $2 $3","8",,]]]',
+"378": '["SM","00",,"(?:0549)?([89]\\d{5})","0549$1",,"\\d{6,10}","[05-7]\\d{7,9}",[["(\\d{2})(\\d{2})(\\d{2})(\\d{2})","$1 $2 $3 $4","[5-7]",,],["(0549)(\\d{6})","$1 $2","0",,"($1) $2"],["(\\d{6})","0549 $1","[89]",,"(0549) $1"]]]',
+"235": '["TD","00|16",,,,,"\\d{8}","[2679]\\d{7}",[["(\\d{2})(\\d{2})(\\d{2})(\\d{2})","$1 $2 $3 $4",,,]]]',
+"960": '["MV","0(?:0|19)",,,,,"\\d{7,10}","[3467]\\d{6}|9(?:00\\d{7}|\\d{6})",[["(\\d{3})(\\d{4})","$1-$2","[3467]|9(?:[1-9]|0[1-9])",,],["(\\d{3})(\\d{3})(\\d{4})","$1 $2 $3","900",,]]]',
+"221": '["SN","00",,,,,"\\d{9}","[37]\\d{8}",[["(\\d{2})(\\d{3})(\\d{2})(\\d{2})","$1 $2 $3 $4",,,]]]',
+"595": '["PY","00","0",,,,"\\d{5,9}","5[0-5]\\d{4,7}|[2-46-9]\\d{5,8}",[["(\\d{2})(\\d{5,7})","$1 $2","(?:[26]1|3[289]|4[124678]|7[123]|8[1236])","($FG)",],["(\\d{3})(\\d{3,6})","$1 $2","[2-9]0","$NP$FG",],["(\\d{3})(\\d{6})","$1 $2","9[1-9]","$NP$FG",],["(\\d{2})(\\d{3})(\\d{4})","$1 $2 $3","8700",,],["(\\d{3})(\\d{4,6})","$1 $2","[2-8][1-9]","($FG)",]]]',
+"977": '["NP","00","0",,,"$NP$FG","\\d{6,10}","[1-8]\\d{7}|9(?:[1-69]\\d{6}|7[2-6]\\d{5,7}|8\\d{8})",[["(1)(\\d{7})","$1-$2","1[2-6]",,],["(\\d{2})(\\d{6})","$1-$2","1[01]|[2-8]|9(?:[1-69]|7[15-9])",,],["(9\\d{2})(\\d{7})","$1-$2","9(?:7[45]|8)",,]]]',
+"36": '["HU","00","06",,,"($FG)","\\d{6,9}","[1-9]\\d{7,8}",[["(1)(\\d{3})(\\d{4})","$1 $2 $3","1",,],["(\\d{2})(\\d{3})(\\d{3,4})","$1 $2 $3","[2-9]",,]]]',
+};
diff --git a/shared/js/phoneNumberJS/mcc_iso3166_table.js b/shared/js/phoneNumberJS/mcc_iso3166_table.js
new file mode 100644
index 0000000..4119856
--- /dev/null
+++ b/shared/js/phoneNumberJS/mcc_iso3166_table.js
@@ -0,0 +1,242 @@
+// MCC(Mobile Country Codes) and country name(ISO3166-1) mapping table.
+// Reference Data from:
+// http://en.wikipedia.org/wiki/List_of_mobile_country_codes
+
+MCC_ISO3166_TABLE = {
+412:'AF',
+276:'AL',
+603:'DZ',
+544:'AS',
+213:'AD',
+631:'AO',
+365:'AI',
+344:'AG',
+722:'AR',
+283:'AM',
+363:'AW',
+505:'AU',
+232:'AT',
+400:'AZ',
+364:'BS',
+426:'BH',
+470:'BD',
+342:'BB',
+257:'BY',
+206:'BE',
+702:'BZ',
+616:'BJ',
+350:'BM',
+402:'BT',
+736:'BO',
+218:'BA',
+652:'BW',
+724:'BR',
+348:'VG',
+528:'BN',
+284:'BG',
+613:'BF',
+642:'BI',
+456:'KH',
+624:'CM',
+302:'CA',
+625:'CV',
+346:'KY',
+623:'CF',
+622:'TD',
+730:'CL',
+460:'CN',
+461:'CN',
+732:'CO',
+654:'KM',
+629:'CG',
+548:'CK',
+712:'CR',
+612:'CI',
+219:'HR',
+368:'CU',
+362:'CW',
+280:'CY',
+230:'CZ',
+630:'CD',
+238:'DK',
+638:'DJ',
+366:'DM',
+370:'DO',
+514:'TL',
+740:'EC',
+602:'EG',
+706:'SV',
+627:'GQ',
+657:'ER',
+248:'EE',
+636:'ET',
+750:'FK',
+288:'FO',
+542:'FJ',
+244:'FI',
+208:'FR',
+742:'GF',
+547:'PF',
+628:'GA',
+607:'GM',
+282:'GE',
+262:'DE',
+620:'GH',
+266:'GI',
+202:'GR',
+290:'GL',
+352:'GD',
+340:'GP',
+535:'GU',
+704:'GT',
+611:'GN',
+632:'GW',
+738:'GY',
+372:'HT',
+708:'HN',
+454:'HK',
+216:'HU',
+274:'IS',
+404:'IN',
+405:'IN',
+406:'IN',
+510:'ID',
+432:'IR',
+418:'IQ',
+272:'IE',
+425:'IL',
+222:'IT',
+338:'JM',
+441:'JP',
+440:'JP',
+416:'JO',
+401:'KZ',
+639:'KE',
+545:'KI',
+467:'KP',
+450:'KR',
+419:'KW',
+437:'KG',
+457:'LA',
+247:'LV',
+415:'LB',
+651:'LS',
+618:'LR',
+606:'LY',
+295:'LI',
+246:'LT',
+270:'LU',
+455:'MO',
+294:'MK',
+646:'MG',
+650:'MW',
+502:'MY',
+472:'MV',
+610:'ML',
+278:'MT',
+551:'MH',
+340:'MQ',
+609:'MR',
+617:'MU',
+334:'MX',
+550:'FM',
+259:'MD',
+212:'MC',
+428:'MN',
+297:'ME',
+354:'MS',
+604:'MA',
+643:'MZ',
+414:'MM',
+649:'NA',
+536:'NR',
+429:'NP',
+204:'NL',
+546:'NC',
+530:'NZ',
+710:'NI',
+614:'NE',
+621:'NG',
+555:'NU',
+534:'MP',
+242:'NO',
+422:'OM',
+410:'PK',
+552:'PW',
+425:'PS',
+714:'PA',
+537:'PG',
+744:'PY',
+716:'PE',
+515:'PH',
+260:'PL',
+268:'PT',
+330:'PR',
+427:'QA',
+647:'RE',
+226:'RO',
+250:'RU',
+635:'RW',
+356:'KN',
+358:'LC',
+308:'PM',
+360:'VC',
+549:'WS',
+292:'SM',
+626:'ST',
+420:'SA',
+608:'SN',
+220:'RS',
+633:'SC',
+619:'SL',
+525:'SG',
+231:'SK',
+293:'SI',
+540:'SB',
+637:'SO',
+655:'ZA',
+214:'ES',
+413:'LK',
+634:'SD',
+746:'SR',
+653:'SZ',
+240:'SE',
+228:'CH',
+417:'SY',
+466:'TW',
+436:'TJ',
+640:'TZ',
+520:'TH',
+615:'TG',
+539:'TO',
+374:'TT',
+605:'TN',
+286:'TR',
+438:'TM',
+376:'TC',
+641:'UG',
+255:'UA',
+424:'AE',
+430:'AE',
+431:'AE',
+235:'GB',
+234:'GB',
+310:'US',
+311:'US',
+312:'US',
+313:'US',
+314:'US',
+315:'US',
+316:'US',
+332:'VI',
+748:'UY',
+434:'UZ',
+541:'VU',
+225:'VA',
+734:'VE',
+452:'VN',
+543:'WF',
+421:'YE',
+645:'ZM',
+648:'ZW'
+}
diff --git a/shared/js/settings_listener.js b/shared/js/settings_listener.js
new file mode 100644
index 0000000..272729c
--- /dev/null
+++ b/shared/js/settings_listener.js
@@ -0,0 +1,69 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+var SettingsListener = {
+ /* Timer to remove the lock. */
+ _timer: null,
+
+ /* lock stores here */
+ _lock: null,
+
+ /**
+ * getSettingsLock: create a lock or retrieve one that we saved.
+ * mozSettings.createLock() is expensive and lock should be reused
+ * whenever possible.
+ */
+
+ getSettingsLock: function sl_getSettingsLock() {
+ // Each time there is a getSettingsLock call, we pospone the removal
+ clearTimeout(this._timer);
+ this._timer = setTimeout((function removeLock() {
+ this._lock = null;
+ }).bind(this), 0);
+
+ // If there is a lock present we return that
+ if (this._lock) {
+ return this._lock;
+ }
+
+ // If there isn't we return one.
+ var settings = window.navigator.mozSettings;
+
+ return (this._lock = settings.createLock());
+ },
+
+ observe: function sl_observe(name, defaultValue, callback) {
+ var settings = window.navigator.mozSettings;
+ if (!settings) {
+ window.setTimeout(function() { callback(defaultValue); });
+ return;
+ }
+
+ var req;
+ try {
+ req = this.getSettingsLock().get(name);
+ } catch (e) {
+ // It is possible (but rare) for getSettingsLock() to return
+ // a SettingsLock object that is no longer valid.
+ // Until https://bugzilla.mozilla.org/show_bug.cgi?id=793239
+ // is fixed, we just catch the resulting exception and try
+ // again with a fresh lock
+ console.warn('Stale lock in settings_listener.js.',
+ 'See https://bugzilla.mozilla.org/show_bug.cgi?id=793239');
+ this._lock = null;
+ req = this.getSettingsLock().get(name);
+ }
+
+ req.addEventListener('success', (function onsuccess() {
+ callback(typeof(req.result[name]) != 'undefined' ?
+ req.result[name] : defaultValue);
+ }));
+
+ settings.addObserver(name, function settingChanged(evt) {
+ callback(evt.settingValue);
+ });
+ }
+};
+
diff --git a/shared/js/simple_phone_matcher.js b/shared/js/simple_phone_matcher.js
new file mode 100644
index 0000000..249c551
--- /dev/null
+++ b/shared/js/simple_phone_matcher.js
@@ -0,0 +1,366 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+/**
+ * A simple lib to match internaional phone number with local
+ * and user formatted phone numbers.
+ *
+ * Adding this feature to gecko is discussed here:
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=743363
+ *
+ */
+
+var SimplePhoneMatcher = {
+ mcc: '724', // Assuming a Brazilian mcc by default, can be changed.
+
+ // Used to remove all the formatting from a phone number.
+ sanitizedNumber: function spm_sanitizedNumber(number) {
+ var join = this._formattingChars.join('|\\');
+ var regexp = new RegExp('(\\' + join + ')', 'g');
+ return number.replace(regexp, '');
+ },
+
+ // Generate variants of a phone number (with prefix, without...).
+ // The variants are sorted from the shortest to the longest.
+ generateVariants: function spm_generateVariants(number) {
+ var sanitizedNumber = this.sanitizedNumber(number);
+
+ var variants = [];
+
+ variants = variants.concat(this._internationalPrefixes(sanitizedNumber),
+ this._trunkPrefixes(sanitizedNumber),
+ this._carrierPrefixes(sanitizedNumber),
+ this._areaPrefixes(sanitizedNumber));
+
+ return variants.sort(function shortestFirst(a, b) {
+ return a.length > b.length;
+ });
+ },
+
+ // Find the best (ie longest) match between the variants for a number
+ // and matches.
+ // |matches| is an array of arrays
+ // This way we can easily go trough the results of a mozContacts request:
+ // array (contacts) of arrays (phone numbers).
+ // => {
+ // bestMatchIndex: i,
+ // localIndex: j
+ // }
+ // ie. bestMatchIndex will be the index in the contact arrays, localIndex
+ // the index in the phone numbers array of this contact
+ bestMatch: function spm_bestMatchIndex(variants, matches) {
+ var bestMatchIndex = null;
+ var bestLocalIndex = null;
+ var bestMatchLength = 0;
+
+ matches.forEach(function(match, matchIndex) {
+ match.forEach(function(number, localIndex) {
+ var sanitizedNumber = this.sanitizedNumber(number);
+
+ variants.forEach(function match(variant) {
+ if (variant.indexOf(sanitizedNumber) !== -1 ||
+ sanitizedNumber.indexOf(variant) !== -1) {
+ var length = sanitizedNumber.length;
+
+ if (length > bestMatchLength) {
+ bestMatchLength = length;
+ bestMatchIndex = matchIndex;
+ bestLocalIndex = localIndex;
+ }
+ }
+ });
+ }, this);
+ }, this);
+
+ return {
+ bestMatchIndex: bestMatchIndex,
+ localIndex: bestLocalIndex
+ };
+ },
+
+ _formattingChars: ['\s', '-', '.', '(', ')'],
+
+ _mccWith00Prefix: ['208', '214', '234', '235', '724'],
+ _mccWith011Prefix: ['310', '311', '312', '313', '314', '315', '316'],
+
+ // https://en.wikipedia.org/wiki/Country_calling_code
+ // https://en.wikipedia.org/wiki/Trunk_code
+ // This is an array of objects composed by { p: <prefix>, t: <true if it can be trunk> }
+ _countryPrefixes: [
+ // North American Numbering Plan countries and territories
+ // US, CA, AG, AI, AS, BB, BM, BS, DM, DO, GD, GU, JM, KN, KY,
+ // LC, MP, MS, PR, SX, TC, TT, VC, VG, VI
+ {p: '1', t: false},
+
+ // BS BB AI
+ {p: '1242', t: false}, {p: '1246', t: false}, {p: '1264', t: false},
+ // AG VG VI KY BM
+ {p: '1268', t: false}, {p: '1284', t: false}, {p: '1340', t: false}, {p: '1345', t: false}, {p: '1441', t: false},
+ // GD TC MS MP GU
+ {p: '1473', t: false}, {p: '1649', t: false}, {p: '1664', t: false}, {p: '1670', t: false}, {p: '1671', t: false},
+ // AS SX LC DM VC
+ {p: '1684', t: false}, {p: '1721', t: false}, {p: '1758', t: false}, {p: '1767', t: false}, {p: '1784', t: false},
+ // PR DO DO DO TT
+ {p: '1787', t: false}, {p: '1809', t: false}, {p: '1829', t: false}, {p: '1849', t: false}, {p: '1868', t: false},
+ // KN JM PR
+ {p: '1869', t: false}, {p: '1876', t: false}, {p: '1939', t: false},
+
+ // EG -- SS
+ {p: '20', t: false}, {p: '210', t: false}, {p: '211', t: false},
+ // MA,EH DZ -- -- TN
+ {p: '212', t: false}, {p: '213', t: false}, {p: '214', t: false}, {p: '215', t: false}, {p: '216', t: false},
+ // -- LY -- GM SN
+ {p: '217', t: false}, {p: '218', t: false}, {p: '219', t: false}, {p: '220', t: false}, {p: '221', t: false},
+ // MR ML GN CI BF
+ {p: '222', t: false}, {p: '223', t: false}, {p: '224', t: false}, {p: '225', t: false}, {p: '226', t: false},
+ // NE TG BJ MU LR
+ {p: '227', t: false}, {p: '228', t: false}, {p: '229', t: false}, {p: '230', t: false}, {p: '231', t: false},
+ // SL GH NG TD CF
+ {p: '232', t: false}, {p: '233', t: false}, {p: '234', t: false}, {p: '235', t: false}, {p: '236', t: false},
+ // CM CV ST GQ GA
+ {p: '237', t: false}, {p: '238', t: false}, {p: '239', t: false}, {p: '240', t: false}, {p: '241', t: false},
+ // CG CD AO GW IO
+ {p: '242', t: false}, {p: '243', t: false}, {p: '244', t: false}, {p: '245', t: false}, {p: '246', t: false},
+ // AC SC SD RW ET
+ {p: '247', t: false}, {p: '248', t: false}, {p: '249', t: false}, {p: '250', t: false}, {p: '251', t: false},
+ // SO,QS DJ KE TZ UG
+ {p: '252', t: false}, {p: '253', t: false}, {p: '254', t: false}, {p: '255', t: false}, {p: '256', t: false},
+ // BI MZ -- ZM MG
+ {p: '257', t: false}, {p: '258', t: false}, {p: '259', t: false}, {p: '260', t: false}, {p: '261', t: false},
+ // RE,YT ZW NA MW LS
+ {p: '262', t: false}, {p: '263', t: false}, {p: '264', t: false}, {p: '265', t: false}, {p: '266', t: false},
+ // BW SZ KM ZA --
+ {p: '267', t: false}, {p: '268', t: false}, {p: '269', t: false}, {p: '27', t: false}, {p: '28', t: false},
+ // SH,TA ER -- -- --
+ {p: '290', t: false}, {p: '291', t: false}, {p: '292', t: false}, {p: '293', t: false}, {p: '294', t: false},
+ // -- -- AW FO GL
+ {p: '295', t: false}, {p: '296', t: false}, {p: '297', t: false}, {p: '298', t: false}, {p: '299', t: false},
+
+ // GR NL BE
+ {p: '30', t: false}, {p: '31', t: true}, {p: '32', t: true},
+ // FR ES GI PT LU
+ {p: '33', t: true}, {p: '34', t: false}, {p: '350', t: false}, {p: '351', t: true}, {p: '352', t: false},
+ // IE IS AL MT CY
+ {p: '353', t: false}, {p: '354', t: false}, {p: '355', t: true}, {p: '356', t: false}, {p: '357', t: false},
+ // FI,AX BG HU LT LV
+ {p: '358', t: false}, {p: '359', t: true}, {p: '36', t: true}, {p: '370', t: true}, {p: '371', t: false},
+ // EE MD AM,QN BY AD
+ {p: '372', t: false}, {p: '373', t: true}, {p: '374', t: false}, {p: '375', t: true}, {p: '376', t: false},
+ // MC SM VA UA RS
+ {p: '377', t: false}, {p: '378', t: false}, {p: '379', t: false}, {p: '380', t: true}, {p: '381', t: true},
+ // ME -- -- HR SI
+ {p: '382', t: true}, {p: '383', t: false}, {p: '384', t: false}, {p: '385', t: true}, {p: '386', t: false},
+ // BA EU MK IT,VA
+ {p: '387', t: true}, {p: '388', t: false}, {p: '389', t: true}, {p: '39', t: false},
+
+ // RO CH CZ
+ {p: '40', t: true}, {p: '41', t: true}, {p: '420', t: false},
+ // SK -- LI -- --
+ {p: '421', t: false}, {p: '422', t: false}, {p: '423', t: false}, {p: '424', t: false}, {p: '425', t: false},
+ // -- -- -- -- AT
+ {p: '426', t: false}, {p: '427', t: false}, {p: '428', t: false}, {p: '429', t: false}, {p: '43', t: true},
+ // GB/UK,GG,IM,JE DK SE NO,SJ PL
+ {p: '44', t: true}, {p: '45', t: false}, {p: '46', t: true}, {p: '47', t: false}, {p: '48', t: true},
+ // DE
+ {p: '49', t: true},
+
+ // FK BZ GT
+ {p: '500', t: false}, {p: '501', t: false}, {p: '502', t: false},
+ // SV HN NI CR PA
+ {p: '503', t: false}, {p: '504', t: false}, {p: '505', t: false}, {p: '506', t: false}, {p: '507', t: false},
+ // PM HT PE MX CU
+ {p: '508', t: false}, {p: '509', t: false}, {p: '51', t: true}, {p: '52', t: false}, {p: '53', t: false},
+ // AR BR CL CO VE
+ {p: '54', t: true}, {p: '55', t: true}, {p: '56', t: false}, {p: '57', t: false}, {p: '58', t: false},
+ // GP,BL,MF BO GY EC GF
+ {p: '590', t: false}, {p: '591', t: true}, {p: '592', t: false}, {p: '593', t: false}, {p: '594', t: false},
+ // PY MQ SR UY BQ,CW
+ {p: '595', t: false}, {p: '596', t: false}, {p: '597', t: false}, {p: '598', t: false}, {p: '599', t: true},
+
+ // MY AU,CX,CC ID
+ {p: '60', t: true}, {p: '61', t: true}, {p: '62', t: true},
+ // PH NZ SG TH TL
+ {p: '63', t: true}, {p: '64', t: false}, {p: '65', t: true}, {p: '66', t: true}, {p: '670', t: false},
+ // -- NF,AQ BN NR PG
+ {p: '671', t: false}, {p: '672', t: false}, {p: '673', t: true}, {p:'674', t: false}, {p: '675', t: false},
+ // TO SB VU FJ PW
+ {p: '676', t: false}, {p: '677', t: false}, {p: '678', t: false}, {p:'679', t: false}, {p: '680', t: false},
+ // WF CK NU -- WS
+ {p: '681', t: false}, {p: '682', t: false}, {p: '683', t: false}, {p:'684', t: false}, {p: '685', t: false},
+ // KI NC TV PF TK
+ {p: '686', t: false}, {p: '687', t: false}, {p: '688', t: false}, {p:'689', t: false}, {p: '690', t: false},
+ // FM MH -- -- --
+ {p: '691', t: false}, {p: '692', t: false}, {p: '693', t: false}, {p: '694', t: false}, {p: '695', t: false},
+ // -- -- -- -- --
+ {p: '696', t: false}, {p: '697', t: false}, {p: '698', t: false}, {p: '699', t: false}, {p: '699', t: false},
+
+ // RU,KZ
+ {p: '7', t: true},
+
+ // KZ KZ Abkhazia Abkhazia
+ {p: '76', t: true}, {p: '77', t: true}, {p: '7840', t: false}, {p: '7940', t: false},
+
+ // XT -- --
+ {p: '800', t: false}, {p: '801', t: false}, {p: '802', t: false},
+ // -- -- -- -- --
+ {p: '803', t: false}, {p: '804', t: false}, {p: '805', t: false}, {p: '806', t: false}, {p: '807', t: false},
+ // XS -- JP KR --
+ {p: '808', t: false}, {p: '809', t: false}, {p: '81', t: false}, {p: '82', t: true}, {p: '83', t: false},
+ // VN KP -- HK MO
+ {p: '84', t: true}, {p: '850', t: true}, {p: '851', t: false}, {p: '852', t: false}, {p: '853', t: false},
+ // -- KH LA -- --
+ {p: '854', t: false}, {p: '855', t: true}, {p: '856', t: true}, {p: '857', t: false}, {p: '858', t: false},
+ // -- CN XN -- --
+ {p: '859', t: false}, {p: '86', t: true}, {p: '870', t: false}, {p: '871', t: false}, {p: '872', t: false},
+ // -- -- -- -- --
+ {p: '873', t: false}, {p: '874', t: false}, {p: '875', t: false}, {p: '876', t: false}, {p: '877', t: false},
+ // XP -- BD XG XV
+ {p: '878', t: false}, {p: '879', t: false}, {p: '880', t: true}, {p: '881', t: false}, {p: '882', t: false},
+ // XV -- -- TW --
+ {p: '883', t: false}, {p: '884', t: false}, {p: '885', t: false}, {p: '886', t: true}, {p: '887', t: false},
+ // XD -- --
+ {p: '888', t: false}, {p: '889', t: false}, {p: '89', t: false},
+
+ // TR,QY IN PK
+ {p: '90', t: false}, {p: '91', t: true}, {p: '92', t: true},
+ // AF LK MM MV LB
+ {p: '93', t: true}, {p: '94', t: false}, {p: '95', t: true}, {p: '960', t: false}, {p: '961', t: false},
+ // JO SY IQ KW SA
+ {p: '962', t: false}, {p: '963', t: false}, {p: '964', t: false}, {p: '965', t: false}, {p: '966', t: false},
+ // YE OM -- PS AE
+ {p: '967', t: false}, {p: '968', t: false}, {p: '969', t: false}, {p: '970', t: false}, {p: '971', t: false},
+ // IL BH QA BT MN
+ {p: '972', t: false}, {p: '973', t: false}, {p: '974', t: false}, {p: '975', t: true}, {p: '976', t: true},
+ // NP -- XR IR --
+ {p: '977', t: true}, {p: '978', t: false}, {p: '979', t: false}, {p: '98', t: false}, {p: '990', t: false},
+ // XC TJ TM AZ GE
+ {p: '991', t: false}, {p: '992', t: false}, {p: '993', t: true}, {p: '994', t: true}, {p: '995', t: true},
+ // KG -- UZ --
+ {p: '996', t: false}, {p: '997', t: false}, {p: '998', t: true}, {p: '999', t: false}
+ ],
+ _trunkCodes: ['0'],
+
+ // https://en.wikipedia.org/wiki/List_of_dialling_codes_in_Brazil
+ // https://en.wikipedia.org/wiki/Telephone_numbers_in_the_United_Kingdom
+ // https://en.wikipedia.org/wiki/Telephone_numbering_plan
+ // country code -> length of the area code
+ _areaCodeSwipe: {
+ '55': 2,
+ '44': 3,
+ '1': 3
+ },
+
+ _internationalPrefixes: function spm_internatialPrefixes(number) {
+ var variants = [number];
+
+ var internationalPrefix = '';
+ if (this._mccWith00Prefix.indexOf(this.mcc) !== -1) {
+ internationalPrefix = '00';
+ }
+ if (this._mccWith011Prefix.indexOf(this.mcc) !== -1) {
+ internationalPrefix = '011';
+ }
+
+ var plusRegexp = new RegExp('^\\+');
+ if (number.match(plusRegexp)) {
+ variants.push(number.replace(plusRegexp, internationalPrefix));
+ }
+
+ var ipRegexp = new RegExp('^' + internationalPrefix);
+ if (number.match(ipRegexp)) {
+ variants.push(number.replace(ipRegexp, '+'));
+ }
+
+ return variants;
+ },
+
+ _trunkPrefixes: function spm_trunkPrefixes(number) {
+ var variants = [];
+
+ var prefixesWithTrunk0 = [];
+ var prefixesWithNoTrunk = [];
+ this._countryPrefixes.forEach(function(prefix) {
+ if (prefix.t) {
+ prefixesWithTrunk0.push(prefix.p);
+ } else {
+ prefixesWithNoTrunk.push(prefix.p);
+ }
+ });
+
+ var trunk0Join = prefixesWithTrunk0.join('|')
+ var trunk0Regexp = new RegExp('^\\+(' + trunk0Join + ')');
+ this._internationalPrefixes(number).some(function match(variant) {
+ var match = variant.match(trunk0Regexp);
+
+ if (match) {
+ variants.push(variant.replace(trunk0Regexp, '0'));
+ variants.push(variant.replace(trunk0Regexp, ''));
+ }
+
+ return match;
+ });
+
+ var noTrunkJoin = prefixesWithNoTrunk.join('|');
+ var noTrunkRegexp = new RegExp('^\\+(' + noTrunkJoin + ')');
+ this._internationalPrefixes(number).some(function match(variant) {
+ var match = variant.match(noTrunkRegexp);
+
+ if (match) {
+ variants.push(variant.replace(noTrunkRegexp, ''));
+ }
+
+ return match;
+ });
+
+ // If the number has a trunk prefix already we need a variant without it
+ var withTrunkRegexp = new RegExp('^(' + this._trunkCodes.join('|') + ')');
+ if (number.match(withTrunkRegexp)) {
+ variants.push(number.replace(withTrunkRegexp, ''));
+ }
+
+ return variants;
+ },
+
+ _areaPrefixes: function spm_areaPrefixes(number) {
+ var variants = [];
+
+ Object.keys(this._areaCodeSwipe).forEach(function(country) {
+ var re = new RegExp('^\\+' + country);
+
+ this._internationalPrefixes(number).some(function match(variant) {
+ var match = variant.match(re);
+
+ if (match) {
+ var afterArea = 1 + country.length + this._areaCodeSwipe[country];
+ variants.push(variant.substring(afterArea));
+ }
+
+ return match;
+ }, this);
+ }, this);
+
+ return variants;
+ },
+
+ // http://thebrazilbusiness.com/article/telephone-system-in-brazil
+ _carrierPrefixes: function spm_carrierPrefix(number) {
+ if (this.mcc != '724') {
+ return [];
+ }
+
+ var variants = [];
+ var withTrunk = new RegExp('^0');
+
+ // A number with carrier prefix will have a trunk code and at
+ // lest 13 digits
+ if (number.length >= 13 && number.match(withTrunk)) {
+ var afterCarrier = 3;
+ variants.push(number.substring(afterCarrier));
+ }
+
+ return variants;
+ }
+};
+
diff --git a/shared/js/tz_select.js b/shared/js/tz_select.js
new file mode 100644
index 0000000..720df43
--- /dev/null
+++ b/shared/js/tz_select.js
@@ -0,0 +1,155 @@
+/* -*- Mode: js; js-indent-level: 2; indent-tabs-mode: nil -*- */
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+function tzSelect(regionSelector, citySelector, onchange) {
+ var TIMEZONE_FILE = '/shared/resources/tz.json';
+
+
+ /**
+ * Activate a timezone selector UI
+ */
+
+ function newTZSelector(onchangeTZ, currentID) {
+ var gRegion = currentID.replace(/\/.*/, '');
+ var gCity = currentID.replace(/.*?\//, '');
+ var gTZ = null;
+
+ function loadTZ(callback) {
+ var xhr = new XMLHttpRequest();
+ xhr.open('GET', TIMEZONE_FILE, true);
+ xhr.responseType = 'json';
+ xhr.onreadystatechange = function() {
+ if (xhr.readyState == 4) {
+ if (xhr.status == 200 || xhr.status === 0) {
+ gTZ = xhr.response;
+ }
+ callback();
+ }
+ };
+ xhr.send();
+ }
+
+ function fillSelectElement(selector, options) {
+ selector.innerHTML = '';
+ options.sort(function(a, b) {
+ return (a.text > b.text);
+ });
+ for (var i = 0; i < options.length; i++) {
+ var option = document.createElement('option');
+ option.textContent = options[i].text;
+ option.selected = options[i].selected;
+ option.value = options[i].value;
+ selector.appendChild(option);
+ }
+ }
+
+ function getSelectedText(selector) {
+ var options = selector.querySelectorAll('option');
+ return options[selector.selectedIndex].textContent;
+ }
+
+ function fillRegions() {
+ var _ = navigator.mozL10n.get;
+ var options = [];
+ for (var c in gTZ) {
+ options.push({
+ text: _('tzRegion-' + c) || c,
+ value: c,
+ selected: (c == gRegion)
+ });
+ }
+ fillSelectElement(regionSelector, options);
+ fillCities();
+ }
+
+ function fillCities() {
+ gRegion = regionSelector.value;
+ var list = gTZ[gRegion];
+ var options = [];
+ for (var i = 0; i < list.length; i++) {
+ options.push({
+ text: list[i].name || list[i].city.replace(/_/g, ' '),
+ value: i,
+ selected: (list[i].city == gCity)
+ });
+ }
+ fillSelectElement(citySelector, options);
+ setTimezone();
+ }
+
+ function setTimezone() {
+ var res = gTZ[gRegion][citySelector.value];
+ gCity = res.city;
+ var offset = res.offset.split(',');
+ onchangeTZ({
+ id: res.id || gRegion + '/' + res.city,
+ region: getSelectedText(regionSelector),
+ city: getSelectedText(citySelector),
+ cc: res.cc,
+ utcOffset: offset[0],
+ dstOffset: offset[1]
+ });
+ }
+
+ regionSelector.onchange = fillCities;
+ citySelector.onchange = setTimezone;
+ loadTZ(fillRegions);
+ }
+
+
+ /**
+ * Monitor time.timezone changes
+ */
+
+ function newTZObserver() {
+ var settings = window.navigator.mozSettings;
+ if (!settings)
+ return;
+
+ settings.addObserver('time.timezone', function(event) {
+ setTimezoneDescription(event.settingValue);
+ });
+
+ var reqTimezone = settings.createLock().get('time.timezone');
+ reqTimezone.onsuccess = function dt_getStatusSuccess() {
+ var lastMozSettingValue = reqTimezone.result['time.timezone'];
+ if (!lastMozSettingValue) {
+ lastMozSettingValue = 'Pacific/Pago_Pago';
+ }
+
+ setTimezoneDescription(lastMozSettingValue);
+
+ // initialize the timezone selector with the initial TZ setting
+ newTZSelector(function updateTZ(tz) {
+ var req = settings.createLock().set({ 'time.timezone': tz.id });
+ if (onchange) {
+ req.onsuccess = function updateTZ_callback() {
+ // Wait until the timezone is actually set
+ // before calling the callback.
+ window.addEventListener('moztimechange', function timeChanged() {
+ window.removeEventListener('moztimechange', timeChanged);
+ onchange(tz);
+ });
+ }
+ }
+ }, lastMozSettingValue);
+
+ console.log('Initial TZ value: ' + lastMozSettingValue);
+ };
+
+ function setTimezoneDescription(timezoneID) {
+ regionSelector.value = timezoneID.replace(/\/.*/, '');
+ citySelector.value = timezoneID.replace(/.*?\//, '');
+ }
+ }
+
+
+ /**
+ * Startup -- make sure webL10n is ready before using tzSelect()
+ */
+
+ newTZObserver();
+}
+
diff --git a/shared/js/visibility_monitor.js b/shared/js/visibility_monitor.js
new file mode 100644
index 0000000..5a8ba1f
--- /dev/null
+++ b/shared/js/visibility_monitor.js
@@ -0,0 +1,494 @@
+/*
+ * visibility_monitor.js
+ *
+ * Given a scrolling container element (with overflow-y: scroll set,
+ * e.g.), monitorChildVisibility() listens for scroll events in order to
+ * determine which child elements are visible within the element and
+ * which are not (assuming that the element itself is visible).
+ *
+ * When a child scrolls onscreen, it is passed to the onscreen callback.
+ *
+ * When a child scrolls offscreen, it is passed to the offscreen callback.
+ *
+ * This class also listens for DOM modification events so that it can handle
+ * children being added to or removed from the scrolling element. It also
+ * handles resize events.
+ *
+ * Note that this class only pays attention to the direct children of
+ * the container element, not all ancestors.
+ *
+ * When you insert a new child into the container, you should create it in
+ * its offscreen state. If it is inserted offscreen nothing will happen.
+ * If you insert it onscreen, it will immediately be passed to the onscreen
+ * callback function
+ *
+ * The scrollmargin argument specifies a number of pixels. Elements
+ * that are within this many pixels of being onscreen are considered
+ * onscreen.
+ *
+ * By specifing proper onscreen and offscreen functions you can use this
+ * class to (for example) remove the background-image style of elements
+ * that are not visible, allowing gecko to free up image memory.
+ * In that sense, this class can be used to workaround
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=689623
+ *
+ * The return value of this function is an object that has a stop() method.
+ * calling the stop method stops visiblity monitoring. If you want to restart
+ * call monitorChildVisiblity() again.
+ *
+ * monitorChildVisiblity() makes the following assumptions. If your program
+ * violates them, the function may not work correctly:
+ *
+ * Child elements of the container element flow left to right and
+ * top to bottom. I.e. the nextSibling of a child element never has a
+ * smaller clientTop value. They are not absolutely positioned and don't
+ * move on their own.
+ *
+ * The children of the container element are themselves all elements;
+ * there are no text nodes or comments cluttering things up.
+ *
+ * Children don't change size, either spontaneously or in response to
+ * onscreen and offscreen callbacks. Don't set display:none on an element
+ * when it goes offscreen, for example.
+ *
+ * Children aren't added or removed to the container while the container
+ * or any of its ancestors is hidden with display:none or is removed from
+ * the tree. The mutation observer that responds to additions and deletions
+ * needs the container and its children to have valid layout data in order
+ * to figure out what is onscreen and what is offscreen. Use visiblity:hidden
+ * instead of display:none if you need to add or remove children while
+ * the container is hidden.
+ *
+ * DocumentFragments are not used to add multiple children at once to
+ * the container, and multiple children are not deleted at once by
+ * setting innerHTML or innerText to ''.
+ *
+ * The container element only changes size when there is a resize event
+ * on the window.
+ */
+'use strict';
+
+function monitorChildVisibility(container, scrollmargin,
+ onscreenCallback, offscreenCallback)
+{
+ // The onscreen region is represented by these two elements
+ var firstOnscreen = null, lastOnscreen = null;
+
+ // This is the last onscreen region that we have notified the client about
+ var firstNotifiedOnscreen = null, lastNotifiedOnscreen = null;
+
+ // The timer used by deferCallbacks()
+ var pendingCallbacks = null;
+
+ // Update the onscreen region whenever we scroll
+ container.addEventListener('scroll', scrollHandler);
+
+ // Update the onscreen region when the window changes size
+ window.addEventListener('resize', resizeHandler);
+
+ // Update the onscreen region when children are added or removed
+ var observer = new MutationObserver(mutationHandler);
+ observer.observe(container, { childList: true });
+
+ // Now determine the initial onscreen region
+ adjustBounds();
+
+ // Call the onscreenCallback for the initial onscreen elements
+ callCallbacks();
+
+ // Return an object that allows the caller to stop monitoring
+ return {
+ stop: function stop() {
+ // Unregister our event handlers and stop the mutation observer.
+ container.removeEventListener('scroll', scrollHandler);
+ window.removeEventListener('resize', resizeHandler);
+ observer.disconnect();
+ }
+ };
+
+ // Adjust the onscreen element range and synchronously call onscreen
+ // and offscreen callbacks as needed.
+ function resizeHandler() {
+ // If we are triggered with 0 height, ignore the event. If this happens
+ // we don't have any layout data and we'll end up thinking that all
+ // of the children are onscreen. Better to do nothing at all here and
+ // just wait until the container becomes visible again.
+ if (container.clientHeight === 0) {
+ return;
+ }
+ adjustBounds();
+ callCallbacks();
+ }
+
+ // This gets called when children are added or removed from the container.
+ // Adding and removing nodes can change the position of other elements
+ // so changes may extend beyond just the ones added or removed
+ function mutationHandler(mutations) {
+ // Ignore any mutations while we are not displayed because
+ // none of our calculations will be right
+ if (container.clientHeight === 0) {
+ return;
+ }
+
+ // If there are any pending callbacks, call them now before handling
+ // the mutations so that we start off in sync, with the onscreen range
+ // equal to the notified range.
+ if (pendingCallbacks)
+ callCallbacks();
+
+ for (var i = 0; i < mutations.length; i++) {
+ var mutation = mutations[i];
+ if (mutation.addedNodes) {
+ for (var j = 0; j < mutation.addedNodes.length; j++) {
+ var child = mutation.addedNodes[j];
+ if (child.nodeType === Node.ELEMENT_NODE)
+ childAdded(child);
+ }
+ }
+
+ if (mutation.removedNodes) {
+ for (var j = 0; j < mutation.removedNodes.length; j++) {
+ var child = mutation.removedNodes[j];
+ if (child.nodeType === Node.ELEMENT_NODE)
+ childRemoved(child,
+ mutation.previousSibling,
+ mutation.nextSibling);
+ }
+ }
+ }
+ }
+
+ // If the new child is onscreen, call the onscreen callback for it.
+ // Adjust the onscreen element range and synchronously call
+ // onscreen and offscreen callbacks as needed.
+ function childAdded(child) {
+ // If the added child is after the last onscreen child, and we're
+ // not filling in the first page of content then this insertion
+ // doesn't affect us at all.
+ if (lastOnscreen &&
+ after(child, lastOnscreen) &&
+ child.offsetTop > container.clientHeight + scrollmargin)
+ return;
+
+ // Otherwise, if this is the first element added or if it is after
+ // the first onscreen element, then it is onscreen and we need to
+ // call the onscreen callback for it.
+ if (!firstOnscreen || after(child, firstOnscreen)) {
+ // Invoke the onscreen callback for this child
+ try {
+ onscreenCallback(child);
+ }
+ catch (e) {
+ console.warn('monitorChildVisiblity: Exception in onscreenCallback:',
+ e, e.stack);
+ }
+ }
+
+ // Now adjust the first and last onscreen element and
+ // send a synchronous notification
+ adjustBounds();
+ callCallbacks();
+ }
+
+ // If the removed element was after the last onscreen element just return.
+ // Otherwise adjust the onscreen element range and synchronously call
+ // onscreen and offscreen callbacks as needed. Note, however that there
+ // are some special cases when the last element is deleted or when the
+ // first or last onscreen element is deleted.
+ function childRemoved(child, previous, next) {
+ // If there aren't any elements left revert back to initial state
+ if (container.firstElementChild === null) {
+ firstOnscreen = lastOnscreen = null;
+ firstNotifiedOnscreen = lastNotifiedOnscreen = null;
+ }
+ else {
+ // If the removed child was after the last onscreen child, then
+ // this removal doesn't affect us at all.
+ if (previous !== null && after(previous, lastOnscreen))
+ return;
+
+ // If the first onscreen element was the one removed
+ // use the next or previous element as a starting point instead.
+ // We know that there is at least one element left, so one of these
+ // two must be defined.
+ if (child === firstOnscreen) {
+ firstOnscreen = firstNotifiedOnscreen = next || previous;
+ }
+
+ // And similarly for the last onscreen element
+ if (child === lastOnscreen) {
+ lastOnscreen = lastNotifiedOnscreen = previous || next;
+ }
+
+ // Find the new bounds after the deletion
+ adjustBounds();
+ }
+
+ // Synchronously call the callbacks
+ callCallbacks();
+ }
+
+ // Adjust the onscreen element range and asynchronously call
+ // onscreen and offscreen callbacks as needed. We do this
+ // asynchronously so that if we get lots of scroll events in
+ // rapid succession and can't keep up, we can skip some of
+ // the notifications.
+ function scrollHandler() {
+ // Ignore scrolls while we are not displayed because
+ // none of our calculations will be right
+ if (container.clientHeight === 0) {
+ return;
+ }
+
+ // Adjust the first and last onscreen element
+ adjustBounds();
+
+ // We may get a lot of scroll events in quick succession, so
+ // don't call the callbacks synchronously. Instead defer so that
+ // we can handle any other queued scroll events.
+ deferCallbacks();
+ }
+
+ // Return true if node a is before node b and false otherwise
+ function before(a, b) {
+ return !!(a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING);
+ }
+
+ // Return true if node a is after node b and false otherwise
+ function after(a, b) {
+ return !!(a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_PRECEDING);
+ }
+
+ // This function recomputes the range of onscreen elements. Normally it
+ // just needs to do small amounts of nextElementSibling
+ // or previousElementSibling iteration to find the range. But it can also
+ // start from an unknown state and search the entire container to find
+ // the range of child elements that are onscreen.
+ function adjustBounds() {
+ // If the container has no children, the bounds are null
+ if (container.firstElementChild === null) {
+ firstOnscreen = lastOnscreen = null;
+ return;
+ }
+
+ // Compute the visible region of the screen, including scroll margin
+ var scrollTop = container.scrollTop;
+ var screenTop = scrollTop - scrollmargin;
+ var screenBottom = scrollTop + container.clientHeight + scrollmargin;
+
+ // This utility function returns ON if the child is onscreen,
+ // BEFORE if it offscreen before the visible elements and AFTER if
+ // it is offscreen aafter the visible elements
+ var BEFORE = -1, ON = 0, AFTER = 1;
+ function position(child) {
+ var childTop = child.offsetTop;
+ var childBottom = childTop + child.offsetHeight;
+ if (childBottom < screenTop)
+ return BEFORE;
+ if (childTop > screenBottom)
+ return AFTER;
+ return ON;
+ }
+
+ // If we don't have a first onscreen element yet, start with the first.
+ if (!firstOnscreen)
+ firstOnscreen = container.firstElementChild;
+
+ // Check the position of the top
+ var toppos = position(firstOnscreen);
+
+ // If the first element is onscreen, see if there are earlier ones
+ if (toppos === ON) {
+ var prev = firstOnscreen.previousElementSibling;
+ while (prev && position(prev) === ON) {
+ firstOnscreen = prev;
+ prev = prev.previousElementSibling;
+ }
+ }
+ else if (toppos === BEFORE) {
+ // The screen is below us, so find the next element that is visible.
+ var e = firstOnscreen.nextElementSibling;
+ while (e && position(e) !== ON) {
+ e = e.nextElementSibling;
+ }
+ firstOnscreen = e;
+ }
+ else {
+ // We've scrolled a lot or things have moved so much that the
+ // entire visible region is now above the first element.
+ // So scan backwards to find the new lastOnscreen and firstOnscreen
+ // elements. Note that if we get here, we can return since we
+ // will have updated both bounds
+
+ // Loop until we find an onscreen element
+ lastOnscreen = firstOnscreen.previousElementSibling;
+ while (lastOnscreen && position(lastOnscreen) !== ON)
+ lastOnscreen = lastOnscreen.previousElementSibling;
+
+ // Now loop from there to find the first onscreen element
+ firstOnscreen = lastOnscreen;
+ prev = firstOnscreen.previousElementSibling;
+ while (prev && position(prev) === ON) {
+ firstOnscreen = prev;
+ prev = prev.previousElementSibling;
+ }
+ return;
+ }
+
+ // Now make the same adjustment on the bottom of the onscreen region
+ // If we don't have a lastOnscreen value to start with, use the newly
+ // computed firstOnscreen value.
+ if (lastOnscreen === null)
+ lastOnscreen = firstOnscreen;
+
+ var bottompos = position(lastOnscreen);
+ if (bottompos === ON) {
+ // If the last element is onscreen, see if there are more below it.
+ var next = lastOnscreen.nextElementSibling;
+ while (next && position(next) === ON) {
+ lastOnscreen = next;
+ next = next.nextElementSibling;
+ }
+ }
+ else if (bottompos === AFTER) {
+ // the last element is now below the visible part of the screen
+ lastOnscreen = lastOnscreen.previousElementSibling;
+ while (position(lastOnscreen) !== ON)
+ lastOnscreen = lastOnscreen.previousElementSibling;
+ }
+ else {
+ // First and last are now both above the visible portion of the screen
+ // So loop down to find their new positions
+ firstOnscreen = lastOnscreen.nextElementSibling;
+ while (firstOnscreen && position(firstOnscreen) !== ON) {
+ firstOnscreen = firstOnscreen.nextElementSibling;
+ }
+
+ lastOnscreen = firstOnscreen;
+ var next = lastOnscreen.nextElementSibling;
+ while (next && position(next) === ON) {
+ lastOnscreen = next;
+ next = next.nextElementSibling;
+ }
+ }
+ }
+
+ // Call the callCallbacks() function after any pending events are processed
+ // We use this for asynchronous notification after scroll events.
+ function deferCallbacks() {
+ if (pendingCallbacks) {
+ // XXX: or we could just return here, which would defer for less time.
+ clearTimeout(pendingCallbacks);
+ }
+ pendingCallbacks = setTimeout(callCallbacks, 0);
+ }
+
+ // Synchronously call the callbacks to notify the client of the new set
+ // of onscreen elements. This only calls the onscreen and offscreen
+ // callbacks for elements that have come onscreen or gone offscreen since
+ // the last time it was called.
+ function callCallbacks() {
+ // If there is a pending call to this function (or if this was the pending
+ // call) clear it now, since we are sending the callbacks
+ if (pendingCallbacks) {
+ clearTimeout(pendingCallbacks);
+ pendingCallbacks = null;
+ }
+
+ // Call the onscreen callback for element from and its siblings
+ // up to, but not including to.
+ function onscreen(from, to) {
+ var e = from;
+ while (e && e !== to) {
+ try {
+ onscreenCallback(e);
+ }
+ catch (ex) {
+ console.warn('monitorChildVisibility: Exception in onscreenCallback:',
+ ex, ex.stack);
+ }
+ e = e.nextElementSibling;
+ }
+ }
+
+ // Call the offscreen callback for element from and its siblings
+ // up to, but not including to.
+ function offscreen(from, to) {
+ var e = from;
+ while (e && e !== to) {
+ try {
+ offscreenCallback(e);
+ }
+ catch (ex) {
+ console.warn('monitorChildVisibility: ' +
+ 'Exception in offscreenCallback:',
+ ex, ex.stack);
+ }
+ e = e.nextElementSibling;
+ }
+ }
+
+ // If the two ranges are the same, return immediately
+ if (firstOnscreen === firstNotifiedOnscreen &&
+ lastOnscreen === lastNotifiedOnscreen)
+ return;
+
+ // If the last notified range is null, then we just add the new range
+ if (firstNotifiedOnscreen === null) {
+ onscreen(firstOnscreen, lastOnscreen.nextElementSibling);
+ }
+
+ // If the new range is null, this means elements have been removed.
+ // We don't need to call offscreen for elements that are not in the
+ // container anymore, so we don't do anything in this case
+ else if (firstOnscreen === null) {
+ // Nothing to do here
+ }
+
+ // If the new range and the old range are disjoint, call the onscreen
+ // callback for the new range first and then call the offscreen callback
+ // for the old.
+ else if (before(lastOnscreen, firstNotifiedOnscreen) ||
+ after(firstOnscreen, lastNotifiedOnscreen)) {
+ // Mark the new ones onscreen
+ onscreen(firstOnscreen, lastOnscreen.nextElementSibling);
+
+ // Mark the old range offscreen
+ offscreen(firstNotifiedOnscreen,
+ lastNotifiedOnscreen.nextElementSibling);
+ }
+
+ // Otherwise if new elements are visible at the top, call those callbacks
+ // If new elements are visible at the bottom, call those.
+ // If elements have gone offscreen at the top, call those callbacks
+ // If elements have gone offscreen at the bottom, call those.
+ else {
+ // Are there new onscreen elements at the top?
+ if (before(firstOnscreen, firstNotifiedOnscreen)) {
+ onscreen(firstOnscreen, firstNotifiedOnscreen);
+ }
+
+ // Are there new onscreen elements at the bottom?
+ if (after(lastOnscreen, lastNotifiedOnscreen)) {
+ onscreen(lastNotifiedOnscreen.nextElementSibling,
+ lastOnscreen.nextElementSibling);
+ }
+
+ // Have elements gone offscreen at the top?
+ if (after(firstOnscreen, firstNotifiedOnscreen)) {
+ offscreen(firstNotifiedOnscreen, firstOnscreen);
+ }
+
+ // Have elements gone offscreen at the bottom?
+ if (before(lastOnscreen, lastNotifiedOnscreen)) {
+ offscreen(lastOnscreen.nextElementSibling,
+ lastNotifiedOnscreen.nextElementSibling);
+ }
+ }
+
+ // Now the notified onscreen range is in sync with the actual
+ // onscreen range.
+ firstNotifiedOnscreen = firstOnscreen;
+ lastNotifiedOnscreen = lastOnscreen;
+ }
+}