592 lines
21 KiB
JavaScript
592 lines
21 KiB
JavaScript
/**
|
|
* angular-locker
|
|
*
|
|
* A simple & configurable abstraction for local/session storage in angular projects.
|
|
*
|
|
* @link https://github.com/tymondesigns/angular-locker
|
|
* @author Sean Tymon @tymondesigns
|
|
* @license MIT License, http://www.opensource.org/licenses/MIT
|
|
*/
|
|
|
|
(function (root, factory) {
|
|
if (typeof define === 'function' && define.amd) {
|
|
define(function () {
|
|
return factory(root.angular);
|
|
});
|
|
} else if (typeof exports === 'object') {
|
|
module.exports = factory(root.angular || (window && window.angular));
|
|
} else {
|
|
factory(root.angular);
|
|
}
|
|
})(this, function (angular) {
|
|
|
|
'use strict';
|
|
|
|
angular.module('angular-locker', [])
|
|
|
|
.provider('locker', function () {
|
|
|
|
/**
|
|
* If value is a function then execute, otherwise return
|
|
*
|
|
* @param {Mixed} value
|
|
* @param {Mixed} param
|
|
* @return {Mixed}
|
|
*/
|
|
var _value = function (value, param) {
|
|
return angular.isFunction(value) ? value(param) : value;
|
|
};
|
|
|
|
/**
|
|
* Determine whether a value is defined and not null
|
|
*
|
|
* @param {Mixed} value
|
|
* @return {Boolean}
|
|
*/
|
|
var _defined = function (value) {
|
|
return angular.isDefined(value) && value !== null;
|
|
};
|
|
|
|
/**
|
|
* Trigger an error
|
|
*
|
|
* @param {String} msg
|
|
* @return {void}
|
|
*/
|
|
var _error = function (msg) {
|
|
throw new Error('[angular-locker] ' + msg);
|
|
};
|
|
|
|
/**
|
|
* Set the defaults
|
|
*
|
|
* @type {Object}
|
|
*/
|
|
var defaults = {
|
|
driver: 'local',
|
|
namespace: 'locker',
|
|
eventsEnabled: true,
|
|
separator: '.',
|
|
extend: {}
|
|
};
|
|
|
|
return {
|
|
|
|
/**
|
|
* Allow the defaults to be specified via the `lockerProvider`
|
|
*
|
|
* @param {Object} value
|
|
*/
|
|
defaults: function (value) {
|
|
if (! _defined(value)) return defaults;
|
|
|
|
angular.forEach(value, function (val, key) {
|
|
if (defaults.hasOwnProperty(key)) defaults[key] = val;
|
|
});
|
|
},
|
|
|
|
/**
|
|
* The locker service
|
|
*/
|
|
$get: ['$window', '$rootScope', '$parse', function ($window, $rootScope, $parse) {
|
|
|
|
/**
|
|
* Define the Locker class
|
|
*
|
|
* @param {Object} options
|
|
*/
|
|
function Locker (options) {
|
|
|
|
/**
|
|
* The config options
|
|
*
|
|
* @type {Object}
|
|
*/
|
|
this._options = options;
|
|
|
|
/**
|
|
* Out of the box drivers
|
|
*
|
|
* @type {Object}
|
|
*/
|
|
this._registeredDrivers = angular.extend({
|
|
local: $window.localStorage,
|
|
session: $window.sessionStorage
|
|
}, options.extend);
|
|
|
|
/**
|
|
* Get the Storage instance from the key
|
|
*
|
|
* @param {String} driver
|
|
* @return {Storage}
|
|
*/
|
|
this._resolveDriver = function (driver) {
|
|
if (! this._registeredDrivers.hasOwnProperty(driver)) {
|
|
_error('The driver "' + driver + '" was not found.');
|
|
}
|
|
|
|
return this._registeredDrivers[driver];
|
|
};
|
|
|
|
/**
|
|
* The driver instance
|
|
*
|
|
* @type {Storage}
|
|
*/
|
|
this._driver = this._resolveDriver(options.driver);
|
|
|
|
/**
|
|
* The namespace value
|
|
*
|
|
* @type {String}
|
|
*/
|
|
this._namespace = options.namespace;
|
|
|
|
/**
|
|
* Separates the namespace from the keys
|
|
*
|
|
* @type {String}
|
|
*/
|
|
this._separator = options.separator;
|
|
|
|
/**
|
|
* Store the watchers here so we can un-register them later
|
|
*
|
|
* @type {Object}
|
|
*/
|
|
this._watchers = {};
|
|
|
|
/**
|
|
* Check browser support
|
|
*
|
|
* @see https://github.com/Modernizr/Modernizr/blob/master/feature-detects/storage/localstorage.js#L38-L47
|
|
* @param {String} driver
|
|
* @return {Boolean}
|
|
*/
|
|
this._checkSupport = function (driver) {
|
|
if (! _defined(this._supported)) {
|
|
var l = 'l';
|
|
try {
|
|
this._resolveDriver(driver || 'local').setItem(l, l);
|
|
this._resolveDriver(driver || 'local').removeItem(l);
|
|
this._supported = true;
|
|
} catch (e) {
|
|
this._supported = false;
|
|
}
|
|
}
|
|
|
|
return this._supported;
|
|
};
|
|
|
|
/**
|
|
* Build the storage key from the namspace
|
|
*
|
|
* @param {String} key
|
|
* @return {String}
|
|
*/
|
|
this._getPrefix = function (key) {
|
|
if (! this._namespace) return key;
|
|
|
|
return this._namespace + this._separator + key;
|
|
};
|
|
|
|
/**
|
|
* Try to encode value as json, or just return the value upon failure
|
|
*
|
|
* @param {Mixed} value
|
|
* @return {Mixed}
|
|
*/
|
|
this._serialize = function (value) {
|
|
try {
|
|
return angular.toJson(value);
|
|
} catch (e) {
|
|
return value;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Try to parse value as json, if it fails then it probably isn't json so just return it
|
|
*
|
|
* @param {String} value
|
|
* @return {Object|String}
|
|
*/
|
|
this._unserialize = function (value) {
|
|
try {
|
|
return angular.fromJson(value);
|
|
} catch (e) {
|
|
return value;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Trigger an event
|
|
*
|
|
* @param {String} name
|
|
* @param {Object} payload
|
|
* @return {void}
|
|
*/
|
|
this._event = function (name, payload) {
|
|
if (this._options.eventsEnabled) {
|
|
$rootScope.$emit(name, angular.extend(payload, {
|
|
driver: this._options.driver,
|
|
namespace: this._namespace,
|
|
}));
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Add to storage
|
|
*
|
|
* @param {String} key
|
|
* @param {Mixed} value
|
|
*/
|
|
this._setItem = function (key, value) {
|
|
if (! this._checkSupport()) _error('The browser does not support localStorage');
|
|
|
|
try {
|
|
var oldVal = this._getItem(key);
|
|
this._driver.setItem(this._getPrefix(key), this._serialize(value));
|
|
if (this._exists(key) && ! angular.equals(oldVal, value)) {
|
|
this._event('locker.item.updated', { key: key, oldValue: oldVal, newValue: value });
|
|
} else {
|
|
this._event('locker.item.added', { key: key, value: value });
|
|
}
|
|
} catch (e) {
|
|
if (['QUOTA_EXCEEDED_ERR', 'NS_ERROR_DOM_QUOTA_REACHED', 'QuotaExceededError'].indexOf(e.name) !== -1) {
|
|
_error('The browser storage quota has been exceeded');
|
|
} else {
|
|
_error('Could not add item with key "' + key + '"');
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Get from storage
|
|
*
|
|
* @param {String} key
|
|
* @return {Mixed}
|
|
*/
|
|
this._getItem = function (key) {
|
|
if (! this._checkSupport()) _error('The browser does not support localStorage');
|
|
|
|
return this._unserialize(this._driver.getItem(this._getPrefix(key)));
|
|
};
|
|
|
|
/**
|
|
* Exists in storage
|
|
*
|
|
* @param {String} key
|
|
* @return {Boolean}
|
|
*/
|
|
this._exists = function (key) {
|
|
if (! this._checkSupport()) _error('The browser does not support localStorage');
|
|
|
|
return this._driver.hasOwnProperty(this._getPrefix(_value(key)));
|
|
};
|
|
|
|
/**
|
|
* Remove from storage
|
|
*
|
|
* @param {String} key
|
|
* @return {Boolean}
|
|
*/
|
|
this._removeItem = function (key) {
|
|
if (! this._checkSupport()) _error('The browser does not support localStorage');
|
|
|
|
if (! this._exists(key)) return false;
|
|
|
|
this._driver.removeItem(this._getPrefix(key));
|
|
this._event('locker.item.forgotten', { key: key });
|
|
|
|
return true;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Define the public api
|
|
*
|
|
* @type {Object}
|
|
*/
|
|
Locker.prototype = {
|
|
|
|
/**
|
|
* Add a new item to storage (even if it already exists)
|
|
*
|
|
* @param {Mixed} key
|
|
* @param {Mixed} value
|
|
* @param {Mixed} def
|
|
* @return {self}
|
|
*/
|
|
put: function (key, value, def) {
|
|
if (! _defined(key)) return false;
|
|
key = _value(key);
|
|
|
|
if (angular.isObject(key)) {
|
|
angular.forEach(key, function (value, key) {
|
|
this._setItem(key, _defined(value) ? value : def);
|
|
}, this);
|
|
} else {
|
|
if (! _defined(value)) return false;
|
|
var val = this._getItem(key);
|
|
this._setItem(key, _value(value, _defined(val) ? val : def));
|
|
}
|
|
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Add an item to storage if it doesn't already exist
|
|
*
|
|
* @param {Mixed} key
|
|
* @param {Mixed} value
|
|
* @param {Mixed} def
|
|
* @return {Boolean}
|
|
*/
|
|
add: function (key, value, def) {
|
|
if (! this.has(key)) {
|
|
this.put(key, value, def);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
},
|
|
|
|
/**
|
|
* Retrieve the specified item from storage
|
|
*
|
|
* @param {String|Array} key
|
|
* @param {Mixed} def
|
|
* @return {Mixed}
|
|
*/
|
|
get: function (key, def) {
|
|
if (angular.isArray(key)) {
|
|
var items = {};
|
|
angular.forEach(key, function (k) {
|
|
if (this.has(k)) items[k] = this._getItem(k);
|
|
}, this);
|
|
|
|
return items;
|
|
}
|
|
|
|
if (! this.has(key)) return arguments.length === 2 ? def : void 0;
|
|
|
|
return this._getItem(key);
|
|
},
|
|
|
|
/**
|
|
* Determine whether the item exists in storage
|
|
*
|
|
* @param {String|Function} key
|
|
* @return {Boolean}
|
|
*/
|
|
has: function (key) {
|
|
return this._exists(key);
|
|
},
|
|
|
|
/**
|
|
* Remove specified item(s) from storage
|
|
*
|
|
* @param {Mixed} key
|
|
* @return {Object}
|
|
*/
|
|
forget: function (key) {
|
|
key = _value(key);
|
|
|
|
if (angular.isArray(key)) {
|
|
key.map(this._removeItem, this);
|
|
} else {
|
|
this._removeItem(key);
|
|
}
|
|
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Retrieve the specified item from storage and then remove it
|
|
*
|
|
* @param {String|Array} key
|
|
* @param {Mixed} def
|
|
* @return {Mixed}
|
|
*/
|
|
pull: function (key, def) {
|
|
var value = this.get(key, def);
|
|
this.forget(key);
|
|
|
|
return value;
|
|
},
|
|
|
|
/**
|
|
* Return all items in storage within the current namespace/driver
|
|
*
|
|
* @return {Object}
|
|
*/
|
|
all: function () {
|
|
var items = {};
|
|
angular.forEach(this._driver, function (value, key) {
|
|
if (this._namespace) {
|
|
var prefix = this._namespace + this._separator;
|
|
if (key.indexOf(prefix) === 0) key = key.substring(prefix.length);
|
|
}
|
|
if (this.has(key)) items[key] = this.get(key);
|
|
}, this);
|
|
|
|
return items;
|
|
},
|
|
|
|
/**
|
|
* Get the storage keys as an array
|
|
*
|
|
* @return {Array}
|
|
*/
|
|
keys: function () {
|
|
return Object.keys(this.all());
|
|
},
|
|
|
|
/**
|
|
* Remove all items set within the current namespace/driver
|
|
*
|
|
* @return {self}
|
|
*/
|
|
clean: function () {
|
|
return this.forget(this.keys());
|
|
},
|
|
|
|
/**
|
|
* Empty the current storage driver completely. careful now.
|
|
*
|
|
* @return {self}
|
|
*/
|
|
empty: function () {
|
|
this._driver.clear();
|
|
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Get the total number of items within the current namespace
|
|
*
|
|
* @return {Integer}
|
|
*/
|
|
count: function () {
|
|
return this.keys().length;
|
|
},
|
|
|
|
/**
|
|
* Bind a storage key to a $scope property
|
|
*
|
|
* @param {Object} $scope
|
|
* @param {String} key
|
|
* @param {Mixed} def
|
|
* @return {self}
|
|
*/
|
|
bind: function ($scope, key, def) {
|
|
if (! _defined( $scope.$eval(key) )) {
|
|
$parse(key).assign($scope, this.get(key, def));
|
|
if (! this.has(key)) this.put(key, def);
|
|
}
|
|
|
|
var self = this;
|
|
this._watchers[key + $scope.$id] = $scope.$watch(key, function (newVal) {
|
|
self.put(key, newVal);
|
|
}, angular.isObject($scope[key]));
|
|
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Unbind a storage key from a $scope property
|
|
*
|
|
* @param {Object} $scope
|
|
* @param {String} key
|
|
* @return {self}
|
|
*/
|
|
unbind: function ($scope, key) {
|
|
$parse(key).assign($scope, void 0);
|
|
this.forget(key);
|
|
|
|
var watchId = key + $scope.$id;
|
|
|
|
if (this._watchers[watchId]) {
|
|
// execute the de-registration function
|
|
this._watchers[watchId]();
|
|
delete this._watchers[watchId];
|
|
}
|
|
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Set the storage driver on a new instance to enable overriding defaults
|
|
*
|
|
* @param {String} driver
|
|
* @return {self}
|
|
*/
|
|
driver: function (driver) {
|
|
// no need to create a new instance if the driver is the same
|
|
if (driver === this._options.driver) return this;
|
|
|
|
return this.instance(angular.extend(this._options, { driver: driver }));
|
|
},
|
|
|
|
/**
|
|
* Get the currently set driver
|
|
*
|
|
* @return {Storage}
|
|
*/
|
|
getDriver: function () {
|
|
return this._driver;
|
|
},
|
|
|
|
/**
|
|
* Set the namespace on a new instance to enable overriding defaults
|
|
*
|
|
* @param {String} namespace
|
|
* @return {self}
|
|
*/
|
|
namespace: function (namespace) {
|
|
// no need to create a new instance if the namespace is the same
|
|
if (namespace === this._namespace) return this;
|
|
|
|
return this.instance(angular.extend(this._options, { namespace: namespace }));
|
|
},
|
|
|
|
/**
|
|
* Get the currently set namespace
|
|
*
|
|
* @return {String}
|
|
*/
|
|
getNamespace: function () {
|
|
return this._namespace;
|
|
},
|
|
|
|
/**
|
|
* Check browser support
|
|
*
|
|
* @see https://github.com/Modernizr/Modernizr/blob/master/feature-detects/storage/localstorage.js#L38-L47
|
|
* @param {String} driver
|
|
* @return {Boolean}
|
|
*/
|
|
supported: function (driver) {
|
|
return this._checkSupport(driver);
|
|
},
|
|
|
|
/**
|
|
* Get a new instance of Locker
|
|
*
|
|
* @param {Object} options
|
|
* @return {Locker}
|
|
*/
|
|
instance: function (options) {
|
|
return new Locker(options);
|
|
}
|
|
};
|
|
|
|
// return the default instance
|
|
return new Locker(defaults);
|
|
}]
|
|
};
|
|
|
|
});
|
|
|
|
});
|