diff options
author | Daniel Narvaez <dwnarvaez@gmail.com> | 2013-02-06 14:30:36 (GMT) |
---|---|---|
committer | Daniel Narvaez <dwnarvaez@gmail.com> | 2013-02-06 14:30:36 (GMT) |
commit | f5bac2f1e1a51b83d215a07f9d5d87db337873f3 (patch) | |
tree | 15c5f594b1e00c6272552cc5544a1bc757713e34 /shared/js | |
parent | c301ead6bb9c201801400964427b517dafb6a03c (diff) |
Get the build to work
Diffstat (limited to 'shared/js')
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; + } +} |