diff --git a/bower.json b/bower.json index f7a8bff..4857b9e 100644 --- a/bower.json +++ b/bower.json @@ -15,10 +15,13 @@ "text": "requirejs-text#~2.0.14", "jquery": "~2.2.0", "backbone": "~1.2.3", + "backbone.localStorage": "1.1.16", "lodash": "~4.0.1", "requirejs": "~2.1.22", "underscore": "~1.8.3", "modernizr": "~3.3.1", - "zepto": "~1.1.6" + "zepto": "~1.1.6", + "localforage": "~1.3.3", + "localforage-backbone": "~0.6.2" } } diff --git a/bower_components/localforage-backbone/dist/localforage.backbone.js b/bower_components/localforage-backbone/dist/localforage.backbone.js new file mode 100644 index 0000000..da1b24e --- /dev/null +++ b/bower_components/localforage-backbone/dist/localforage.backbone.js @@ -0,0 +1,207 @@ +/*! + localForage Backbone Adapter + Version 0.6.2 + https://github.com/mozilla/localforage-backbone + (c) 2014 Mozilla, Apache License 2.0 +*/ +// backbone.localforage allows users of Backbone.js to store their collections +// entirely offline with no communication to a REST server. It uses whatever +// driver localForage is set to use to store the data (IndexedDB, WebSQL, or +// localStorage, depending on availability). This allows apps on Chrome, +// Firefox, IE, and Safari to use async, offline storage, which is cool. +// +// The basics of how to use this library is that it lets you override the +// `sync` method on your collections and models to use localForage. So +// +// var MyModel = Backbone.Model.extend({}) +// var MyCollection = Backbone.Collection.extend({ +// model: MyModel +// }); +// +// becomes +// +// var MyModel = Backbone.Collection.extend({ +// sync: Backbone.localforage.sync('ModelNamespace') +// }); +// var MyCollection = Backbone.Collection.extend({ +// model: MyModel, +// sync: Backbone.localforage.sync('MyCollection') +// }); +// +// Inspiration for this file comes from a few backbone.localstorage +// implementations. +(function(root, factory) { + if (typeof define === 'function' && define.amd) { + define(['localforage', 'backbone', 'underscore'], factory); + } else if (typeof module !== 'undefined' && module.exports) { + var localforage = require('localforage'); + var Backbone = require('backbone'); + var _ = require('underscore'); + module.exports = factory(localforage, Backbone, _); + } else { + factory(root.localforage, root.Backbone, root._); + } +}(this, function(localforage, Backbone, _) { + function S4() { + // jshint -W016 + return ((1 + Math.random()) * 65536 | 0).toString(16).substring(1); + // jshint +W016 + } + + function guid() { + return S4() + S4() + '-' + S4() + '-' + S4() + '-' + S4() + '-' + S4() + S4() + S4(); + } + + function updateCollectionReferences(collection, callback, err, data) { + // If this model has a collection, keep the collection in = + // sync as well. + if (collection) { + // Create an array of `model.collection` models' ids. + var collectionData = collection.map(function(model) { + return collection.model.prototype.sync._localforageNamespace + '/' + model.id; + }); + + // Bind `data` to `callback` to call after + // `model.collection` models' ids are persisted. + callback = callback ? _.partial(callback, err, data) : undefined; + + if (!collection.sync.localforageKey) { + localforageKey(collection); + } + + // Persist `model.collection` models' ids. + localforage.setItem(collection.sync.localforageKey, collectionData, callback); + } + } + + function localforageKey(model) { + // If `this` is a `Backbone.Collection` it means + // `Backbone.Collection#fetch` has been called. + if (model instanceof Backbone.Collection) { + model.sync.localforageKey = model.sync._localforageNamespace; + } else { // `this` is a `Backbone.Model` if not a `Backbone.Collection`. + // Generate an id if one is not set yet. + if (!model.id) { + model[model.idAttribute] = model.attributes[model.idAttribute] = guid(); + } + + model.sync.localforageKey = model.sync._localforageNamespace + '/' + model.id; + } + } + + // For now, we aren't complicated: just set a property off Backbone to + // serve as our export point. + Backbone.localforage = { + localforageInstance: localforage, + + sync: function(name) { + var self = this; + var sync = function(method, model, options) { + localforageKey(model); + + switch (method) { + case 'read': + return model.id ? self.find(model, options) : self.findAll(model, options); + case 'create': + return self.create(model, options); + case 'update': + return self.update(model, options); + case 'delete': + return self.destroy(model, options); + } + }; + + // This needs to be exposed for later usage, but it's private to + // the adapter. + sync._localforageNamespace = name; + + // expose function used to create the localeForage key + // this enable to have the key set before sync is called + sync._localeForageKeyFn = localforageKey; + + return sync; + }, + + save: function(model, callback) { + localforage.setItem(model.sync.localforageKey, model.toJSON(), function(err, data) { + // keep the collection in sync + if (model.collection) { + updateCollectionReferences(model.collection, callback, err, data); + } else if (callback) { + callback(data); + } + }); + }, + + create: function(model, callbacks) { + // We always have an ID available by this point, so we just call + // the update method. + return this.update(model, callbacks); + }, + + update: function(model, callbacks) { + this.save(model, function(data) { + if (callbacks.success) { + callbacks.success(data); + } + }); + }, + + find: function(model, callbacks) { + localforage.getItem(model.sync.localforageKey, function(err, data) { + if (!err && !_.isEmpty(data)) { + if (callbacks.success) { + callbacks.success(data); + } + } else if (callbacks.error) { + callbacks.error(); + } + }); + }, + + // Only used by `Backbone.Collection#sync`. + findAll: function(collection, callbacks) { + localforage.getItem(collection.sync.localforageKey, function(err, data) { + if (!err && data && data.length) { + var done = function() { + if (callbacks.success) { + callbacks.success(data); + } + }; + + // Only execute `done` after getting all of the + // collection's models. + done = _.after(data.length, done); + + var onModel = function(i, err, model) { + data[i] = model; + done(); + }; + + for (var i = 0; i < data.length; ++i) { + localforage.getItem(data[i], _.partial(onModel, i)); + } + } else { + data = []; + if (callbacks.success) { + callbacks.success(data); + } + } + }); + }, + + destroy: function(model, callbacks) { + var collection = model.collection; + localforage.removeItem(model.sync.localforageKey, function() { + // keep the collection in sync + if (collection) { + updateCollectionReferences(collection, callbacks.success, null, model.toJSON()); + } else if (callbacks.success) { + callbacks.success(model.toJSON()); + } + }); + } + }; + + return Backbone.localforage; +})); diff --git a/bower_components/localforage/dist/localforage.js b/bower_components/localforage/dist/localforage.js new file mode 100644 index 0000000..094bf21 --- /dev/null +++ b/bower_components/localforage/dist/localforage.js @@ -0,0 +1,2858 @@ +/*! + localForage -- Offline Storage, Improved + Version 1.3.3 + https://mozilla.github.io/localForage + (c) 2013-2015 Mozilla, Apache License 2.0 +*/ +(function() { +var define, requireModule, require, requirejs; + +(function() { + var registry = {}, seen = {}; + + define = function(name, deps, callback) { + registry[name] = { deps: deps, callback: callback }; + }; + + requirejs = require = requireModule = function(name) { + requirejs._eak_seen = registry; + + if (seen[name]) { return seen[name]; } + seen[name] = {}; + + if (!registry[name]) { + throw new Error("Could not find module " + name); + } + + var mod = registry[name], + deps = mod.deps, + callback = mod.callback, + reified = [], + exports; + + for (var i=0, l=deps.length; i dbInfo.db.version; + + if (isDowngrade) { + // If the version is not the default one + // then warn for impossible downgrade. + if (dbInfo.version !== defaultVersion) { + globalObject.console.warn('The database "' + dbInfo.name + '"' + ' can\'t be downgraded from version ' + dbInfo.db.version + ' to version ' + dbInfo.version + '.'); + } + // Align the versions to prevent errors. + dbInfo.version = dbInfo.db.version; + } + + if (isUpgrade || isNewStore) { + // If the store is new then increment the version (if needed). + // This will trigger an "upgradeneeded" event which is required + // for creating a store. + if (isNewStore) { + var incVersion = dbInfo.db.version + 1; + if (incVersion > dbInfo.version) { + dbInfo.version = incVersion; + } + } + + return true; + } + + return false; + } + + function getItem(key, callback) { + var self = this; + + // Cast the key to a string, as that's all we can set as a key. + if (typeof key !== 'string') { + globalObject.console.warn(key + ' used as a key, but it is not a string.'); + key = String(key); + } + + var promise = new Promise(function (resolve, reject) { + self.ready().then(function () { + var dbInfo = self._dbInfo; + var store = dbInfo.db.transaction(dbInfo.storeName, 'readonly').objectStore(dbInfo.storeName); + var req = store.get(key); + + req.onsuccess = function () { + var value = req.result; + if (value === undefined) { + value = null; + } + if (_isEncodedBlob(value)) { + value = _decodeBlob(value); + } + resolve(value); + }; + + req.onerror = function () { + reject(req.error); + }; + })['catch'](reject); + }); + + executeCallback(promise, callback); + return promise; + } + + // Iterate over all items stored in database. + function iterate(iterator, callback) { + var self = this; + + var promise = new Promise(function (resolve, reject) { + self.ready().then(function () { + var dbInfo = self._dbInfo; + var store = dbInfo.db.transaction(dbInfo.storeName, 'readonly').objectStore(dbInfo.storeName); + + var req = store.openCursor(); + var iterationNumber = 1; + + req.onsuccess = function () { + var cursor = req.result; + + if (cursor) { + var value = cursor.value; + if (_isEncodedBlob(value)) { + value = _decodeBlob(value); + } + var result = iterator(value, cursor.key, iterationNumber++); + + if (result !== void 0) { + resolve(result); + } else { + cursor['continue'](); + } + } else { + resolve(); + } + }; + + req.onerror = function () { + reject(req.error); + }; + })['catch'](reject); + }); + + executeCallback(promise, callback); + + return promise; + } + + function setItem(key, value, callback) { + var self = this; + + // Cast the key to a string, as that's all we can set as a key. + if (typeof key !== 'string') { + globalObject.console.warn(key + ' used as a key, but it is not a string.'); + key = String(key); + } + + var promise = new Promise(function (resolve, reject) { + var dbInfo; + self.ready().then(function () { + dbInfo = self._dbInfo; + if (value instanceof Blob) { + return _checkBlobSupport(dbInfo.db).then(function (blobSupport) { + if (blobSupport) { + return value; + } + return _encodeBlob(value); + }); + } + return value; + }).then(function (value) { + var transaction = dbInfo.db.transaction(dbInfo.storeName, 'readwrite'); + var store = transaction.objectStore(dbInfo.storeName); + + // The reason we don't _save_ null is because IE 10 does + // not support saving the `null` type in IndexedDB. How + // ironic, given the bug below! + // See: https://github.com/mozilla/localForage/issues/161 + if (value === null) { + value = undefined; + } + + transaction.oncomplete = function () { + // Cast to undefined so the value passed to + // callback/promise is the same as what one would get out + // of `getItem()` later. This leads to some weirdness + // (setItem('foo', undefined) will return `null`), but + // it's not my fault localStorage is our baseline and that + // it's weird. + if (value === undefined) { + value = null; + } + + resolve(value); + }; + transaction.onabort = transaction.onerror = function () { + var err = req.error ? req.error : req.transaction.error; + reject(err); + }; + + var req = store.put(value, key); + })['catch'](reject); + }); + + executeCallback(promise, callback); + return promise; + } + + function removeItem(key, callback) { + var self = this; + + // Cast the key to a string, as that's all we can set as a key. + if (typeof key !== 'string') { + globalObject.console.warn(key + ' used as a key, but it is not a string.'); + key = String(key); + } + + var promise = new Promise(function (resolve, reject) { + self.ready().then(function () { + var dbInfo = self._dbInfo; + var transaction = dbInfo.db.transaction(dbInfo.storeName, 'readwrite'); + var store = transaction.objectStore(dbInfo.storeName); + + // We use a Grunt task to make this safe for IE and some + // versions of Android (including those used by Cordova). + // Normally IE won't like `.delete()` and will insist on + // using `['delete']()`, but we have a build step that + // fixes this for us now. + var req = store['delete'](key); + transaction.oncomplete = function () { + resolve(); + }; + + transaction.onerror = function () { + reject(req.error); + }; + + // The request will be also be aborted if we've exceeded our storage + // space. + transaction.onabort = function () { + var err = req.error ? req.error : req.transaction.error; + reject(err); + }; + })['catch'](reject); + }); + + executeCallback(promise, callback); + return promise; + } + + function clear(callback) { + var self = this; + + var promise = new Promise(function (resolve, reject) { + self.ready().then(function () { + var dbInfo = self._dbInfo; + var transaction = dbInfo.db.transaction(dbInfo.storeName, 'readwrite'); + var store = transaction.objectStore(dbInfo.storeName); + var req = store.clear(); + + transaction.oncomplete = function () { + resolve(); + }; + + transaction.onabort = transaction.onerror = function () { + var err = req.error ? req.error : req.transaction.error; + reject(err); + }; + })['catch'](reject); + }); + + executeCallback(promise, callback); + return promise; + } + + function length(callback) { + var self = this; + + var promise = new Promise(function (resolve, reject) { + self.ready().then(function () { + var dbInfo = self._dbInfo; + var store = dbInfo.db.transaction(dbInfo.storeName, 'readonly').objectStore(dbInfo.storeName); + var req = store.count(); + + req.onsuccess = function () { + resolve(req.result); + }; + + req.onerror = function () { + reject(req.error); + }; + })['catch'](reject); + }); + + executeCallback(promise, callback); + return promise; + } + + function key(n, callback) { + var self = this; + + var promise = new Promise(function (resolve, reject) { + if (n < 0) { + resolve(null); + + return; + } + + self.ready().then(function () { + var dbInfo = self._dbInfo; + var store = dbInfo.db.transaction(dbInfo.storeName, 'readonly').objectStore(dbInfo.storeName); + + var advanced = false; + var req = store.openCursor(); + req.onsuccess = function () { + var cursor = req.result; + if (!cursor) { + // this means there weren't enough keys + resolve(null); + + return; + } + + if (n === 0) { + // We have the first key, return it if that's what they + // wanted. + resolve(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. + resolve(cursor.key); + } + } + }; + + req.onerror = function () { + reject(req.error); + }; + })['catch'](reject); + }); + + executeCallback(promise, callback); + return promise; + } + + function keys(callback) { + var self = this; + + var promise = new Promise(function (resolve, reject) { + self.ready().then(function () { + var dbInfo = self._dbInfo; + var store = dbInfo.db.transaction(dbInfo.storeName, 'readonly').objectStore(dbInfo.storeName); + + var req = store.openCursor(); + var keys = []; + + req.onsuccess = function () { + var cursor = req.result; + + if (!cursor) { + resolve(keys); + return; + } + + keys.push(cursor.key); + cursor['continue'](); + }; + + req.onerror = function () { + reject(req.error); + }; + })['catch'](reject); + }); + + executeCallback(promise, callback); + return promise; + } + + function executeCallback(promise, callback) { + if (callback) { + promise.then(function (result) { + callback(null, result); + }, function (error) { + callback(error); + }); + } + } + + var asyncStorage = { + _driver: 'asyncStorage', + _initStorage: _initStorage, + iterate: iterate, + getItem: getItem, + setItem: setItem, + removeItem: removeItem, + clear: clear, + length: length, + key: key, + keys: keys + }; + + return asyncStorage; + })(typeof window !== 'undefined' ? window : self); + exports['default'] = asyncStorage; + module.exports = exports['default']; + +/***/ }, +/* 2 */ +/***/ function(module, exports, __webpack_require__) { + + // If IndexedDB isn't available, we'll fall back to localStorage. + // Note that this will have considerable performance and storage + // side-effects (all data will be serialized on save and only data that + // can be converted to a string via `JSON.stringify()` will be saved). + 'use strict'; + + exports.__esModule = true; + var localStorageWrapper = (function (globalObject) { + 'use strict'; + + var localStorage = null; + + // If the app is running inside a Google Chrome packaged webapp, or some + // other context where localStorage isn't available, we don't use + // localStorage. This feature detection is preferred over the old + // `if (window.chrome && window.chrome.runtime)` code. + // See: https://github.com/mozilla/localForage/issues/68 + try { + // If localStorage isn't available, we get outta here! + // This should be inside a try catch + if (!globalObject.localStorage || !('setItem' in globalObject.localStorage)) { + return; + } + // Initialize localStorage and create a variable to use throughout + // the code. + localStorage = globalObject.localStorage; + } catch (e) { + return; + } + + // Config the localStorage backend, using options set in the config. + function _initStorage(options) { + var self = this; + var dbInfo = {}; + if (options) { + for (var i in options) { + dbInfo[i] = options[i]; + } + } + + dbInfo.keyPrefix = dbInfo.name + '/'; + + if (dbInfo.storeName !== self._defaultConfig.storeName) { + dbInfo.keyPrefix += dbInfo.storeName + '/'; + } + + self._dbInfo = dbInfo; + + return new Promise(function (resolve, reject) { + resolve(__webpack_require__(3)); + }).then(function (lib) { + dbInfo.serializer = lib; + return Promise.resolve(); + }); + } + + // Remove all keys from the datastore, effectively destroying all data in + // the app's key/value store! + function clear(callback) { + var self = this; + var promise = self.ready().then(function () { + var keyPrefix = self._dbInfo.keyPrefix; + + for (var i = localStorage.length - 1; i >= 0; i--) { + var key = localStorage.key(i); + + if (key.indexOf(keyPrefix) === 0) { + localStorage.removeItem(key); + } + } + }); + + executeCallback(promise, callback); + return promise; + } + + // Retrieve an item from the store. Unlike the original async_storage + // library in Gaia, we don't modify return values at all. If a key's value + // is `undefined`, we pass that value to the callback function. + function getItem(key, callback) { + var self = this; + + // Cast the key to a string, as that's all we can set as a key. + if (typeof key !== 'string') { + globalObject.console.warn(key + ' used as a key, but it is not a string.'); + key = String(key); + } + + var promise = self.ready().then(function () { + var dbInfo = self._dbInfo; + var result = localStorage.getItem(dbInfo.keyPrefix + key); + + // If a result was found, parse it from the serialized + // string into a JS object. If result isn't truthy, the key + // is likely undefined and we'll pass it straight to the + // callback. + if (result) { + result = dbInfo.serializer.deserialize(result); + } + + return result; + }); + + executeCallback(promise, callback); + return promise; + } + + // Iterate over all items in the store. + function iterate(iterator, callback) { + var self = this; + + var promise = self.ready().then(function () { + var dbInfo = self._dbInfo; + var keyPrefix = dbInfo.keyPrefix; + var keyPrefixLength = keyPrefix.length; + var length = localStorage.length; + + // We use a dedicated iterator instead of the `i` variable below + // so other keys we fetch in localStorage aren't counted in + // the `iterationNumber` argument passed to the `iterate()` + // callback. + // + // See: github.com/mozilla/localForage/pull/435#discussion_r38061530 + var iterationNumber = 1; + + for (var i = 0; i < length; i++) { + var key = localStorage.key(i); + if (key.indexOf(keyPrefix) !== 0) { + continue; + } + var value = localStorage.getItem(key); + + // If a result was found, parse it from the serialized + // string into a JS object. If result isn't truthy, the + // key is likely undefined and we'll pass it straight + // to the iterator. + if (value) { + value = dbInfo.serializer.deserialize(value); + } + + value = iterator(value, key.substring(keyPrefixLength), iterationNumber++); + + if (value !== void 0) { + return value; + } + } + }); + + executeCallback(promise, callback); + return promise; + } + + // Same as localStorage's key() method, except takes a callback. + function key(n, callback) { + var self = this; + var promise = self.ready().then(function () { + var dbInfo = self._dbInfo; + var result; + try { + result = localStorage.key(n); + } catch (error) { + result = null; + } + + // Remove the prefix from the key, if a key is found. + if (result) { + result = result.substring(dbInfo.keyPrefix.length); + } + + return result; + }); + + executeCallback(promise, callback); + return promise; + } + + function keys(callback) { + var self = this; + var promise = self.ready().then(function () { + var dbInfo = self._dbInfo; + var length = localStorage.length; + var keys = []; + + for (var i = 0; i < length; i++) { + if (localStorage.key(i).indexOf(dbInfo.keyPrefix) === 0) { + keys.push(localStorage.key(i).substring(dbInfo.keyPrefix.length)); + } + } + + return keys; + }); + + executeCallback(promise, callback); + return promise; + } + + // Supply the number of keys in the datastore to the callback function. + function length(callback) { + var self = this; + var promise = self.keys().then(function (keys) { + return keys.length; + }); + + executeCallback(promise, callback); + return promise; + } + + // Remove an item from the store, nice and simple. + function removeItem(key, callback) { + var self = this; + + // Cast the key to a string, as that's all we can set as a key. + if (typeof key !== 'string') { + globalObject.console.warn(key + ' used as a key, but it is not a string.'); + key = String(key); + } + + var promise = self.ready().then(function () { + var dbInfo = self._dbInfo; + localStorage.removeItem(dbInfo.keyPrefix + key); + }); + + executeCallback(promise, callback); + return promise; + } + + // Set a key's value and run an optional callback once the value is set. + // Unlike Gaia's implementation, the callback function is passed the value, + // in case you want to operate on that value only after you're sure it + // saved, or something like that. + function setItem(key, value, callback) { + var self = this; + + // Cast the key to a string, as that's all we can set as a key. + if (typeof key !== 'string') { + globalObject.console.warn(key + ' used as a key, but it is not a string.'); + key = String(key); + } + + var promise = self.ready().then(function () { + // Convert undefined values to null. + // https://github.com/mozilla/localForage/pull/42 + if (value === undefined) { + value = null; + } + + // Save the original value to pass to the callback. + var originalValue = value; + + return new Promise(function (resolve, reject) { + var dbInfo = self._dbInfo; + dbInfo.serializer.serialize(value, function (value, error) { + if (error) { + reject(error); + } else { + try { + localStorage.setItem(dbInfo.keyPrefix + key, value); + resolve(originalValue); + } catch (e) { + // localStorage capacity exceeded. + // TODO: Make this a specific error/event. + if (e.name === 'QuotaExceededError' || e.name === 'NS_ERROR_DOM_QUOTA_REACHED') { + reject(e); + } + reject(e); + } + } + }); + }); + }); + + executeCallback(promise, callback); + return promise; + } + + function executeCallback(promise, callback) { + if (callback) { + promise.then(function (result) { + callback(null, result); + }, function (error) { + callback(error); + }); + } + } + + var localStorageWrapper = { + _driver: 'localStorageWrapper', + _initStorage: _initStorage, + // Default API, from Gaia/localStorage. + iterate: iterate, + getItem: getItem, + setItem: setItem, + removeItem: removeItem, + clear: clear, + length: length, + key: key, + keys: keys + }; + + return localStorageWrapper; + })(typeof window !== 'undefined' ? window : self); + exports['default'] = localStorageWrapper; + module.exports = exports['default']; + +/***/ }, +/* 3 */ +/***/ function(module, exports) { + + 'use strict'; + + exports.__esModule = true; + var localforageSerializer = (function (globalObject) { + 'use strict'; + + // Sadly, the best way to save binary data in WebSQL/localStorage is serializing + // it to Base64, so this is how we store it to prevent very strange errors with less + // verbose ways of binary <-> string data storage. + var BASE_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; + + var BLOB_TYPE_PREFIX = '~~local_forage_type~'; + var BLOB_TYPE_PREFIX_REGEX = /^~~local_forage_type~([^~]+)~/; + + var SERIALIZED_MARKER = '__lfsc__:'; + var SERIALIZED_MARKER_LENGTH = SERIALIZED_MARKER.length; + + // OMG the serializations! + var TYPE_ARRAYBUFFER = 'arbf'; + var TYPE_BLOB = 'blob'; + var TYPE_INT8ARRAY = 'si08'; + var TYPE_UINT8ARRAY = 'ui08'; + var TYPE_UINT8CLAMPEDARRAY = 'uic8'; + var TYPE_INT16ARRAY = 'si16'; + var TYPE_INT32ARRAY = 'si32'; + var TYPE_UINT16ARRAY = 'ur16'; + var TYPE_UINT32ARRAY = 'ui32'; + var TYPE_FLOAT32ARRAY = 'fl32'; + var TYPE_FLOAT64ARRAY = 'fl64'; + var TYPE_SERIALIZED_MARKER_LENGTH = SERIALIZED_MARKER_LENGTH + TYPE_ARRAYBUFFER.length; + + // Abstracts constructing a Blob object, so it also works in older + // browsers that don't support the native Blob constructor. (i.e. + // old QtWebKit versions, at least). + function _createBlob(parts, properties) { + parts = parts || []; + properties = properties || {}; + + try { + return new Blob(parts, properties); + } catch (err) { + if (err.name !== 'TypeError') { + throw err; + } + + var BlobBuilder = globalObject.BlobBuilder || globalObject.MSBlobBuilder || globalObject.MozBlobBuilder || globalObject.WebKitBlobBuilder; + + var builder = new BlobBuilder(); + for (var i = 0; i < parts.length; i += 1) { + builder.append(parts[i]); + } + + return builder.getBlob(properties.type); + } + } + + // Serialize a value, afterwards executing a callback (which usually + // instructs the `setItem()` callback/promise to be executed). This is how + // we store binary data with localStorage. + function serialize(value, callback) { + var valueString = ''; + if (value) { + valueString = value.toString(); + } + + // Cannot use `value instanceof ArrayBuffer` or such here, as these + // checks fail when running the tests using casper.js... + // + // TODO: See why those tests fail and use a better solution. + if (value && (value.toString() === '[object ArrayBuffer]' || value.buffer && value.buffer.toString() === '[object ArrayBuffer]')) { + // Convert binary arrays to a string and prefix the string with + // a special marker. + var buffer; + var marker = SERIALIZED_MARKER; + + if (value instanceof ArrayBuffer) { + buffer = value; + marker += TYPE_ARRAYBUFFER; + } else { + buffer = value.buffer; + + if (valueString === '[object Int8Array]') { + marker += TYPE_INT8ARRAY; + } else if (valueString === '[object Uint8Array]') { + marker += TYPE_UINT8ARRAY; + } else if (valueString === '[object Uint8ClampedArray]') { + marker += TYPE_UINT8CLAMPEDARRAY; + } else if (valueString === '[object Int16Array]') { + marker += TYPE_INT16ARRAY; + } else if (valueString === '[object Uint16Array]') { + marker += TYPE_UINT16ARRAY; + } else if (valueString === '[object Int32Array]') { + marker += TYPE_INT32ARRAY; + } else if (valueString === '[object Uint32Array]') { + marker += TYPE_UINT32ARRAY; + } else if (valueString === '[object Float32Array]') { + marker += TYPE_FLOAT32ARRAY; + } else if (valueString === '[object Float64Array]') { + marker += TYPE_FLOAT64ARRAY; + } else { + callback(new Error('Failed to get type for BinaryArray')); + } + } + + callback(marker + bufferToString(buffer)); + } else if (valueString === '[object Blob]') { + // Conver the blob to a binaryArray and then to a string. + var fileReader = new FileReader(); + + fileReader.onload = function () { + // Backwards-compatible prefix for the blob type. + var str = BLOB_TYPE_PREFIX + value.type + '~' + bufferToString(this.result); + + callback(SERIALIZED_MARKER + TYPE_BLOB + str); + }; + + fileReader.readAsArrayBuffer(value); + } else { + try { + callback(JSON.stringify(value)); + } catch (e) { + console.error("Couldn't convert value into a JSON string: ", value); + + callback(null, e); + } + } + } + + // Deserialize data we've inserted into a value column/field. We place + // special markers into our strings to mark them as encoded; this isn't + // as nice as a meta field, but it's the only sane thing we can do whilst + // keeping localStorage support intact. + // + // Oftentimes this will just deserialize JSON content, but if we have a + // special marker (SERIALIZED_MARKER, defined above), we will extract + // some kind of arraybuffer/binary data/typed array out of the string. + function deserialize(value) { + // If we haven't marked this string as being specially serialized (i.e. + // something other than serialized JSON), we can just return it and be + // done with it. + if (value.substring(0, SERIALIZED_MARKER_LENGTH) !== SERIALIZED_MARKER) { + return JSON.parse(value); + } + + // The following code deals with deserializing some kind of Blob or + // TypedArray. First we separate out the type of data we're dealing + // with from the data itself. + var serializedString = value.substring(TYPE_SERIALIZED_MARKER_LENGTH); + var type = value.substring(SERIALIZED_MARKER_LENGTH, TYPE_SERIALIZED_MARKER_LENGTH); + + var blobType; + // Backwards-compatible blob type serialization strategy. + // DBs created with older versions of localForage will simply not have the blob type. + if (type === TYPE_BLOB && BLOB_TYPE_PREFIX_REGEX.test(serializedString)) { + var matcher = serializedString.match(BLOB_TYPE_PREFIX_REGEX); + blobType = matcher[1]; + serializedString = serializedString.substring(matcher[0].length); + } + var buffer = stringToBuffer(serializedString); + + // Return the right type based on the code/type set during + // serialization. + switch (type) { + case TYPE_ARRAYBUFFER: + return buffer; + case TYPE_BLOB: + return _createBlob([buffer], { type: blobType }); + case TYPE_INT8ARRAY: + return new Int8Array(buffer); + case TYPE_UINT8ARRAY: + return new Uint8Array(buffer); + case TYPE_UINT8CLAMPEDARRAY: + return new Uint8ClampedArray(buffer); + case TYPE_INT16ARRAY: + return new Int16Array(buffer); + case TYPE_UINT16ARRAY: + return new Uint16Array(buffer); + case TYPE_INT32ARRAY: + return new Int32Array(buffer); + case TYPE_UINT32ARRAY: + return new Uint32Array(buffer); + case TYPE_FLOAT32ARRAY: + return new Float32Array(buffer); + case TYPE_FLOAT64ARRAY: + return new Float64Array(buffer); + default: + throw new Error('Unkown type: ' + type); + } + } + + function stringToBuffer(serializedString) { + // Fill the string into a ArrayBuffer. + var bufferLength = serializedString.length * 0.75; + var len = serializedString.length; + var i; + var p = 0; + var encoded1, encoded2, encoded3, encoded4; + + if (serializedString[serializedString.length - 1] === '=') { + bufferLength--; + if (serializedString[serializedString.length - 2] === '=') { + bufferLength--; + } + } + + var buffer = new ArrayBuffer(bufferLength); + var bytes = new Uint8Array(buffer); + + for (i = 0; i < len; i += 4) { + encoded1 = BASE_CHARS.indexOf(serializedString[i]); + encoded2 = BASE_CHARS.indexOf(serializedString[i + 1]); + encoded3 = BASE_CHARS.indexOf(serializedString[i + 2]); + encoded4 = BASE_CHARS.indexOf(serializedString[i + 3]); + + /*jslint bitwise: true */ + bytes[p++] = encoded1 << 2 | encoded2 >> 4; + bytes[p++] = (encoded2 & 15) << 4 | encoded3 >> 2; + bytes[p++] = (encoded3 & 3) << 6 | encoded4 & 63; + } + return buffer; + } + + // Converts a buffer to a string to store, serialized, in the backend + // storage library. + function bufferToString(buffer) { + // base64-arraybuffer + var bytes = new Uint8Array(buffer); + var base64String = ''; + var i; + + for (i = 0; i < bytes.length; i += 3) { + /*jslint bitwise: true */ + base64String += BASE_CHARS[bytes[i] >> 2]; + base64String += BASE_CHARS[(bytes[i] & 3) << 4 | bytes[i + 1] >> 4]; + base64String += BASE_CHARS[(bytes[i + 1] & 15) << 2 | bytes[i + 2] >> 6]; + base64String += BASE_CHARS[bytes[i + 2] & 63]; + } + + if (bytes.length % 3 === 2) { + base64String = base64String.substring(0, base64String.length - 1) + '='; + } else if (bytes.length % 3 === 1) { + base64String = base64String.substring(0, base64String.length - 2) + '=='; + } + + return base64String; + } + + var localforageSerializer = { + serialize: serialize, + deserialize: deserialize, + stringToBuffer: stringToBuffer, + bufferToString: bufferToString + }; + + return localforageSerializer; + })(typeof window !== 'undefined' ? window : self); + exports['default'] = localforageSerializer; + module.exports = exports['default']; + +/***/ }, +/* 4 */ +/***/ function(module, exports, __webpack_require__) { + + /* + * Includes code from: + * + * base64-arraybuffer + * https://github.com/niklasvh/base64-arraybuffer + * + * Copyright (c) 2012 Niklas von Hertzen + * Licensed under the MIT license. + */ + 'use strict'; + + exports.__esModule = true; + var webSQLStorage = (function (globalObject) { + 'use strict'; + + var openDatabase = globalObject.openDatabase; + + // If WebSQL methods aren't available, we can stop now. + if (!openDatabase) { + return; + } + + // Open the WebSQL database (automatically creates one if one didn't + // previously exist), using any options set in the config. + function _initStorage(options) { + var self = this; + var dbInfo = { + db: null + }; + + if (options) { + for (var i in options) { + dbInfo[i] = typeof options[i] !== 'string' ? options[i].toString() : options[i]; + } + } + + var dbInfoPromise = new Promise(function (resolve, reject) { + // Open the database; the openDatabase API will automatically + // create it for us if it doesn't exist. + try { + dbInfo.db = openDatabase(dbInfo.name, String(dbInfo.version), dbInfo.description, dbInfo.size); + } catch (e) { + return self.setDriver(self.LOCALSTORAGE).then(function () { + return self._initStorage(options); + }).then(resolve)['catch'](reject); + } + + // Create our key/value table if it doesn't exist. + dbInfo.db.transaction(function (t) { + t.executeSql('CREATE TABLE IF NOT EXISTS ' + dbInfo.storeName + ' (id INTEGER PRIMARY KEY, key unique, value)', [], function () { + self._dbInfo = dbInfo; + resolve(); + }, function (t, error) { + reject(error); + }); + }); + }); + + return new Promise(function (resolve, reject) { + resolve(__webpack_require__(3)); + }).then(function (lib) { + dbInfo.serializer = lib; + return dbInfoPromise; + }); + } + + function getItem(key, callback) { + var self = this; + + // Cast the key to a string, as that's all we can set as a key. + if (typeof key !== 'string') { + globalObject.console.warn(key + ' used as a key, but it is not a string.'); + key = String(key); + } + + var promise = new Promise(function (resolve, reject) { + self.ready().then(function () { + var dbInfo = self._dbInfo; + dbInfo.db.transaction(function (t) { + t.executeSql('SELECT * FROM ' + dbInfo.storeName + ' WHERE key = ? LIMIT 1', [key], function (t, results) { + var result = results.rows.length ? results.rows.item(0).value : null; + + // Check to see if this is serialized content we need to + // unpack. + if (result) { + result = dbInfo.serializer.deserialize(result); + } + + resolve(result); + }, function (t, error) { + + reject(error); + }); + }); + })['catch'](reject); + }); + + executeCallback(promise, callback); + return promise; + } + + function iterate(iterator, callback) { + var self = this; + + var promise = new Promise(function (resolve, reject) { + self.ready().then(function () { + var dbInfo = self._dbInfo; + + dbInfo.db.transaction(function (t) { + t.executeSql('SELECT * FROM ' + dbInfo.storeName, [], function (t, results) { + var rows = results.rows; + var length = rows.length; + + for (var i = 0; i < length; i++) { + var item = rows.item(i); + var result = item.value; + + // Check to see if this is serialized content + // we need to unpack. + if (result) { + result = dbInfo.serializer.deserialize(result); + } + + result = iterator(result, item.key, i + 1); + + // void(0) prevents problems with redefinition + // of `undefined`. + if (result !== void 0) { + resolve(result); + return; + } + } + + resolve(); + }, function (t, error) { + reject(error); + }); + }); + })['catch'](reject); + }); + + executeCallback(promise, callback); + return promise; + } + + function setItem(key, value, callback) { + var self = this; + + // Cast the key to a string, as that's all we can set as a key. + if (typeof key !== 'string') { + globalObject.console.warn(key + ' used as a key, but it is not a string.'); + key = String(key); + } + + var promise = new Promise(function (resolve, reject) { + self.ready().then(function () { + // The localStorage API doesn't return undefined values in an + // "expected" way, so undefined is always cast to null in all + // drivers. See: https://github.com/mozilla/localForage/pull/42 + if (value === undefined) { + value = null; + } + + // Save the original value to pass to the callback. + var originalValue = value; + + var dbInfo = self._dbInfo; + dbInfo.serializer.serialize(value, function (value, error) { + if (error) { + reject(error); + } else { + dbInfo.db.transaction(function (t) { + t.executeSql('INSERT OR REPLACE INTO ' + dbInfo.storeName + ' (key, value) VALUES (?, ?)', [key, value], function () { + resolve(originalValue); + }, function (t, error) { + reject(error); + }); + }, function (sqlError) { + // The transaction failed; check + // to see if it's a quota error. + if (sqlError.code === sqlError.QUOTA_ERR) { + // We reject the callback outright for now, but + // it's worth trying to re-run the transaction. + // Even if the user accepts the prompt to use + // more storage on Safari, this error will + // be called. + // + // TODO: Try to re-run the transaction. + reject(sqlError); + } + }); + } + }); + })['catch'](reject); + }); + + executeCallback(promise, callback); + return promise; + } + + function removeItem(key, callback) { + var self = this; + + // Cast the key to a string, as that's all we can set as a key. + if (typeof key !== 'string') { + globalObject.console.warn(key + ' used as a key, but it is not a string.'); + key = String(key); + } + + var promise = new Promise(function (resolve, reject) { + self.ready().then(function () { + var dbInfo = self._dbInfo; + dbInfo.db.transaction(function (t) { + t.executeSql('DELETE FROM ' + dbInfo.storeName + ' WHERE key = ?', [key], function () { + resolve(); + }, function (t, error) { + + reject(error); + }); + }); + })['catch'](reject); + }); + + executeCallback(promise, callback); + return promise; + } + + // Deletes every item in the table. + // TODO: Find out if this resets the AUTO_INCREMENT number. + function clear(callback) { + var self = this; + + var promise = new Promise(function (resolve, reject) { + self.ready().then(function () { + var dbInfo = self._dbInfo; + dbInfo.db.transaction(function (t) { + t.executeSql('DELETE FROM ' + dbInfo.storeName, [], function () { + resolve(); + }, function (t, error) { + reject(error); + }); + }); + })['catch'](reject); + }); + + executeCallback(promise, callback); + return promise; + } + + // Does a simple `COUNT(key)` to get the number of items stored in + // localForage. + function length(callback) { + var self = this; + + var promise = new Promise(function (resolve, reject) { + self.ready().then(function () { + var dbInfo = self._dbInfo; + dbInfo.db.transaction(function (t) { + // Ahhh, SQL makes this one soooooo easy. + t.executeSql('SELECT COUNT(key) as c FROM ' + dbInfo.storeName, [], function (t, results) { + var result = results.rows.item(0).c; + + resolve(result); + }, function (t, error) { + + reject(error); + }); + }); + })['catch'](reject); + }); + + executeCallback(promise, callback); + return promise; + } + + // Return the key located at key index X; essentially gets the key from a + // `WHERE id = ?`. This is the most efficient way I can think to implement + // this rarely-used (in my experience) part of the API, but it can seem + // inconsistent, because we do `INSERT OR REPLACE INTO` on `setItem()`, so + // the ID of each key will change every time it's updated. Perhaps a stored + // procedure for the `setItem()` SQL would solve this problem? + // TODO: Don't change ID on `setItem()`. + function key(n, callback) { + var self = this; + + var promise = new Promise(function (resolve, reject) { + self.ready().then(function () { + var dbInfo = self._dbInfo; + dbInfo.db.transaction(function (t) { + t.executeSql('SELECT key FROM ' + dbInfo.storeName + ' WHERE id = ? LIMIT 1', [n + 1], function (t, results) { + var result = results.rows.length ? results.rows.item(0).key : null; + resolve(result); + }, function (t, error) { + reject(error); + }); + }); + })['catch'](reject); + }); + + executeCallback(promise, callback); + return promise; + } + + function keys(callback) { + var self = this; + + var promise = new Promise(function (resolve, reject) { + self.ready().then(function () { + var dbInfo = self._dbInfo; + dbInfo.db.transaction(function (t) { + t.executeSql('SELECT key FROM ' + dbInfo.storeName, [], function (t, results) { + var keys = []; + + for (var i = 0; i < results.rows.length; i++) { + keys.push(results.rows.item(i).key); + } + + resolve(keys); + }, function (t, error) { + + reject(error); + }); + }); + })['catch'](reject); + }); + + executeCallback(promise, callback); + return promise; + } + + function executeCallback(promise, callback) { + if (callback) { + promise.then(function (result) { + callback(null, result); + }, function (error) { + callback(error); + }); + } + } + + var webSQLStorage = { + _driver: 'webSQLStorage', + _initStorage: _initStorage, + iterate: iterate, + getItem: getItem, + setItem: setItem, + removeItem: removeItem, + clear: clear, + length: length, + key: key, + keys: keys + }; + + return webSQLStorage; + })(typeof window !== 'undefined' ? window : self); + exports['default'] = webSQLStorage; + module.exports = exports['default']; + +/***/ } +/******/ ]) +}); +; \ No newline at end of file diff --git a/index.html b/index.html index 3bd47d7..116828e 100644 --- a/index.html +++ b/index.html @@ -31,20 +31,29 @@