Comparing version 1.1.0 to 1.2.0
{ | ||
"name": "persisto", | ||
"description": "Persist Javascript objects to localStorage and remote servers.", | ||
"version": "1.1.0", | ||
"version": "1.2.0", | ||
"main": [ | ||
@@ -6,0 +6,0 @@ "dist/persisto.js" |
@@ -1,14 +0,16 @@ | ||
# 1.1.1-0 / Unreleased | ||
* | ||
# 1.2.1-0 / Unreleased | ||
# 1.2.0 / 2019-09-07 | ||
* [ADD] AMD wrapper and module support (`persisto = require('persisto')`). | ||
* [CHANGE] Apply and enforce 'prettier' codestyle | ||
# 1.1.0 / 2016-08-14 | ||
* [CHANGE] rename `.init` option to `.defaults` | ||
* [CHANGE] rename `.debug` option to `.debugLevel` | ||
* [CHANGE] Rename `.init` option to `.defaults`. | ||
* [CHANGE] Rename `.debug` option to `.debugLevel`. | ||
# 1.0.0 / 2016-08-11 | ||
* [ADD] .set() creates intermediate objects if missing | ||
* [ADD] .writeToForm() and .readFromForm() accept inputs with nested names | ||
* [FIX] .remove() supports dot notation | ||
* [ADD] .set() creates intermediate objects if missing. | ||
* [ADD] .writeToForm() and .readFromForm() accept inputs with nested names. | ||
* [FIX] .remove() supports dot notation. | ||
# 0.0.1 / 2016-04-10 | ||
* Initial release | ||
* Initial release. |
@@ -1,2 +0,2 @@ | ||
Copyright 2016 Martin Wendt, | ||
Copyright 2016-2017 Martin Wendt, | ||
http://wwWendt.de/ | ||
@@ -3,0 +3,0 @@ |
/*! | ||
* persisto.js | ||
* | ||
* Persistent Javascript objects and web forms using Web Storage. | ||
* Persistent JavaScript objects and web forms using Web Storage. | ||
* | ||
* Copyright (c) 2016, Martin Wendt (http://wwWendt.de) | ||
* Copyright (c) 2016-2017, Martin Wendt (http://wwWendt.de) | ||
* Released under the MIT license | ||
* | ||
* @version 1.1.0 | ||
* @date 2016-08-14T10:00 | ||
* @version 1.2.0 | ||
* @date 2019-09-07T13:47:35Z | ||
*/ | ||
;(function($, window, document, undefined) { | ||
(function(factory) { | ||
if (typeof define === "function" && define.amd) { | ||
// AMD. Register as an anonymous module. | ||
define(["jquery"], factory); | ||
} else if (typeof module === "object" && module.exports) { | ||
// Node/CommonJS | ||
module.exports = factory(require("jquery")); | ||
} else { | ||
// Browser globals | ||
factory(jQuery); | ||
} | ||
})(function($) { | ||
"use strict"; | ||
/*globals console */ | ||
/******************************************************************************* | ||
* Private functions and variables | ||
*/ | ||
"use strict"; | ||
var MAX_INT = 9007199254740991, | ||
// Allow mangling of some global names: | ||
console = window.console, | ||
error = $.error; | ||
/******************************************************************************* | ||
* Private functions and variables | ||
*/ | ||
/** | ||
* A persistent plain object or array. | ||
*/ | ||
window.PersistentObject = function(namespace, opts) { | ||
var prevValue, | ||
self = this, | ||
dfd = $.Deferred(), | ||
stamp = Date.now(); // disable warning 'PersistentObject is not defined' | ||
var MAX_INT = 9007199254740991; | ||
/* Return current time stamp. */ | ||
function _getNow() { | ||
return new Date().getTime(); | ||
} | ||
/** | ||
* A persistent plain object or array. | ||
*/ | ||
window.PersistentObject = function(namespace, opts) { | ||
var prevValue, | ||
self = this, | ||
dfd = $.Deferred(), | ||
stamp = _getNow(); | ||
/* jshint ignore:start */ // disable warning 'PersistentObject is not defined' | ||
if ( !(this instanceof PersistentObject) ) { $.error("Must use 'new' keyword"); } | ||
/* jshint ignore:start */ if (!(this instanceof PersistentObject)) { | ||
error("Must use 'new' keyword"); | ||
} | ||
/* jshint ignore:end */ | ||
if ( typeof namespace !== "string" ) { | ||
$.error(this + ": Missing required argument: namespace"); | ||
} | ||
if (typeof namespace !== "string") { | ||
error(this + ": Missing required argument: namespace"); | ||
} | ||
this.opts = $.extend({ | ||
remote: null, // URL for GET/PUT, ajax options, or callback | ||
defaults: {}, // default value if no data is found in localStorage | ||
commitDelay: 500, // commit changes after 0.5 seconds of inactivity | ||
createParents: true, // set() creates missing intermediate parent objects for children | ||
maxCommitDelay: 3000, // commit changes max. 3 seconds after first change | ||
pushDelay: 5000, // push commits after 5 seconds of inactivity | ||
maxPushDelay: 30000, // push commits max. 30 seconds after first change | ||
storage: window.localStorage, | ||
// Default debugLevel is set to 1 by `grunt build`: | ||
debugLevel: 1, // 0:quiet, 1:normal, 2:verbose | ||
// Events | ||
change: $.noop, | ||
commit: $.noop, | ||
conflict: $.noop, | ||
error: $.noop, | ||
pull: $.noop, | ||
push: $.noop, | ||
update: $.noop | ||
}, opts); | ||
this._checkTimer = null; | ||
this.namespace = namespace; | ||
this.storage = this.opts.storage; | ||
this._data = this.opts.defaults; | ||
this.opts = $.extend( | ||
{ | ||
remote: null, // URL for GET/PUT, ajax options, or callback | ||
defaults: {}, // default value if no data is found in localStorage | ||
commitDelay: 500, // commit changes after 0.5 seconds of inactivity | ||
createParents: true, // set() creates missing intermediate parent objects for children | ||
maxCommitDelay: 3000, // commit changes max. 3 seconds after first change | ||
pushDelay: 5000, // push commits after 5 seconds of inactivity | ||
maxPushDelay: 30000, // push commits max. 30 seconds after first change | ||
storage: window.localStorage, | ||
// Default debugLevel is set to 1 by `grunt build`: | ||
debugLevel: 1, // 0:quiet, 1:normal, 2:verbose | ||
// Events | ||
change: $.noop, | ||
commit: $.noop, | ||
conflict: $.noop, | ||
error: $.noop, | ||
pull: $.noop, | ||
push: $.noop, | ||
update: $.noop, | ||
}, | ||
opts | ||
); | ||
this._checkTimer = null; | ||
this.namespace = namespace; | ||
this.storage = this.opts.storage; | ||
this._data = this.opts.defaults; | ||
this.offline = undefined; | ||
this.phase = null; | ||
this.uncommittedSince = null; | ||
this.unpushedSince = null; | ||
this.lastUpdate = 0; | ||
this.lastPull = 0; | ||
this.commitCount = 0; | ||
this.pushCount = 0; | ||
this.lastModified = stamp; | ||
this.offline = undefined; | ||
this.phase = null; | ||
this.uncommittedSince = null; | ||
this.unpushedSince = null; | ||
this.lastUpdate = 0; | ||
this.lastPull = 0; | ||
this.commitCount = 0; | ||
this.pushCount = 0; | ||
this.lastModified = stamp; | ||
this.ready = dfd.promise; | ||
this.ready = dfd.promise; | ||
// _data contains the default value. Now load from persistent storage if any | ||
prevValue = this.storage ? this.storage.getItem(this.namespace) : null; | ||
// _data contains the default value. Now load from persistent storage if any | ||
prevValue = this.storage ? this.storage.getItem(this.namespace) : null; | ||
if ( this.opts.remote ) { | ||
// Try to pull, then resolve | ||
this.pull().done(function() { | ||
// self.debug("init from remote", this._data); | ||
self.offline = false; | ||
dfd.resolve(); | ||
}).fail(function() { | ||
self.offline = true; | ||
if ( prevValue != null ) { | ||
console.warn(self + ": could not init from remote; falling back to storage."); | ||
// self._data = JSON.parse(prevValue); | ||
self._data = $.extend({}, self.opts.defaults, JSON.parse(prevValue)); | ||
} else { | ||
console.warn(self + ": could not init from remote; falling back default."); | ||
} | ||
dfd.resolve(); | ||
}); | ||
} else if ( prevValue != null ) { | ||
this.update(); | ||
// We still extend from opts.defaults, in case some fields where missing | ||
this._data = $.extend({}, this.opts.defaults, this._data); | ||
// this.debug("init from storage", this._data); | ||
dfd.resolve(); | ||
// this.lastUpdate = stamp; | ||
// this.setDirty(); | ||
} else { | ||
// this.debug("init to default", this._data); | ||
dfd.resolve(); | ||
} | ||
}; | ||
if (this.opts.remote) { | ||
// Try to pull, then resolve | ||
this.pull() | ||
.done(function() { | ||
// self.debug("init from remote", this._data); | ||
self.offline = false; | ||
dfd.resolve(); | ||
}) | ||
.fail(function() { | ||
self.offline = true; | ||
if (prevValue != null) { | ||
console.warn( | ||
self + ": could not init from remote; falling back to storage." | ||
); | ||
// self._data = JSON.parse(prevValue); | ||
self._data = $.extend( | ||
{}, | ||
self.opts.defaults, | ||
JSON.parse(prevValue) | ||
); | ||
} else { | ||
console.warn( | ||
self + ": could not init from remote; falling back default." | ||
); | ||
} | ||
dfd.resolve(); | ||
}); | ||
} else if (prevValue != null) { | ||
this.update(); | ||
// We still extend from opts.defaults, in case some fields where missing | ||
this._data = $.extend({}, this.opts.defaults, this._data); | ||
// this.debug("init from storage", this._data); | ||
dfd.resolve(); | ||
// this.lastUpdate = stamp; | ||
// this.setDirty(); | ||
} else { | ||
// this.debug("init to default", this._data); | ||
dfd.resolve(); | ||
} | ||
}; | ||
window.PersistentObject.prototype = { | ||
/** @type {string} */ | ||
version: "1.1.0", // Set to semver by 'grunt release' | ||
window.PersistentObject.prototype = { | ||
/** @type {string} */ | ||
version: "1.2.0", // Set to semver by 'grunt release' | ||
/* Trigger commit/push according to current settings. */ | ||
_invalidate: function(hint, deferredCall) { | ||
var self = this, | ||
prevChange = this.lastModified, | ||
now = _getNow(), | ||
nextCommit = 0, | ||
nextPush = 0, | ||
nextCheck = 0; | ||
/* Trigger commit/push according to current settings. */ | ||
_invalidate: function(hint, deferredCall) { | ||
var self = this, | ||
prevChange = this.lastModified, | ||
now = Date.now(), | ||
nextCommit = 0, | ||
nextPush = 0, | ||
nextCheck = 0; | ||
if ( this._checkTimer ) { | ||
clearTimeout(this._checkTimer); | ||
this._checkTimer = null; | ||
} | ||
if (this._checkTimer) { | ||
clearTimeout(this._checkTimer); | ||
this._checkTimer = null; | ||
} | ||
if ( !deferredCall ) { | ||
// this.debug("_invalidate(" + hint + ")"); | ||
this.lastModified = now; | ||
if ( !this.uncommittedSince ) { this.uncommittedSince = now; } | ||
if ( !this.unpushedSince ) { this.unpushedSince = now; } | ||
this.opts.change(hint); | ||
} else { | ||
this.debug("_invalidate() recursive"); | ||
} | ||
if (!deferredCall) { | ||
// this.debug("_invalidate(" + hint + ")"); | ||
this.lastModified = now; | ||
if (!this.uncommittedSince) { | ||
this.uncommittedSince = now; | ||
} | ||
if (!this.unpushedSince) { | ||
this.unpushedSince = now; | ||
} | ||
this.opts.change(hint); | ||
} else { | ||
this.debug("_invalidate() recursive"); | ||
} | ||
if ( this.storage ) { | ||
// If we came here by a deferred timer (or delay is 0), commit | ||
// immedialtely | ||
if ( ((now - prevChange) >= this.opts.commitDelay) || | ||
((now - this.uncommittedSince) >= this.opts.maxCommitDelay) ) { | ||
if (this.storage) { | ||
// If we came here by a deferred timer (or delay is 0), commit | ||
// immedialtely | ||
if ( | ||
now - prevChange >= this.opts.commitDelay || | ||
now - this.uncommittedSince >= this.opts.maxCommitDelay | ||
) { | ||
this.debug( | ||
"_invalidate(): force commit", | ||
now - prevChange >= this.opts.commitDelay, | ||
now - this.uncommittedSince >= this.opts.maxCommitDelay | ||
); | ||
this.commit(); | ||
} else { | ||
// otherwise schedule next check | ||
nextCommit = Math.min( | ||
now + this.opts.commitDelay + 1, | ||
this.uncommittedSince + this.opts.maxCommitDelay + 1 | ||
); | ||
} | ||
} | ||
this.debug("_invalidate(): force commit", | ||
((now - prevChange) >= this.opts.commitDelay), | ||
((now - this.uncommittedSince) >= this.opts.maxCommitDelay)); | ||
this.commit(); | ||
} else { | ||
// otherwise schedule next check | ||
nextCommit = Math.min( | ||
now + this.opts.commitDelay + 1, | ||
this.uncommittedSince + this.opts.maxCommitDelay + 1 ); | ||
} | ||
} | ||
if (this.opts.remote) { | ||
if ( | ||
now - prevChange >= this.opts.pushDelay || | ||
now - this.unpushedSince >= this.opts.maxPushDelay | ||
) { | ||
this.debug( | ||
"_invalidate(): force push", | ||
now - prevChange >= this.opts.pushDelay, | ||
now - this.unpushedSince >= this.opts.maxPushDelay | ||
); | ||
this.push(); | ||
} else { | ||
nextPush = Math.min( | ||
now + this.opts.pushDelay + 1, | ||
this.unpushedSince + this.opts.maxPushDelay + 1 | ||
); | ||
} | ||
} | ||
if (nextCommit || nextPush) { | ||
nextCheck = Math.min(nextCommit || MAX_INT, nextPush || MAX_INT); | ||
// this.debug("Defer update:", nextCheck - now) | ||
this.debug( | ||
"_invalidate(" + hint + ") defer by " + (nextCheck - now) + "ms" | ||
); | ||
this._checkTimer = setTimeout(function() { | ||
self._checkTimer = null; // no need to call clearTimeout in the handler... | ||
self._invalidate.call(self, null, true); | ||
}, nextCheck - now); | ||
} | ||
}, | ||
/* Load data from localStorage. */ | ||
_update: function(objData) { | ||
if (this.uncommittedSince) { | ||
console.warn("Updating an uncommitted object."); | ||
if (this.conflict(objData, this._data) === false) { | ||
return; | ||
} | ||
} | ||
this._data = objData; | ||
// this.dirty = false; | ||
this.lastUpdate = Date.now(); | ||
}, | ||
/* Return readable string representation for this instance. */ | ||
toString: function() { | ||
return "PersistentObject('" + this.namespace + "')"; | ||
}, | ||
/* Log to console if opts.debugLevel >= 2 */ | ||
debug: function() { | ||
if (this.opts.debugLevel >= 2) { | ||
Array.prototype.unshift.call(arguments, this.toString()); | ||
console.log.apply(console, arguments); | ||
} | ||
}, | ||
/* Log to console if opts.debugLevel >= 1 */ | ||
log: function() { | ||
if (this.opts.debugLevel >= 1) { | ||
Array.prototype.unshift.call(arguments, this.toString()); | ||
console.log.apply(console, arguments); | ||
} | ||
}, | ||
/** Return true if there are uncommited or unpushed modifications. */ | ||
isDirty: function() { | ||
return !!( | ||
(this.storage && this.uncommittedSince) || | ||
(this.opts.remote && this.unpushedSince) | ||
); | ||
}, | ||
/** Return true if initial pull has completed. */ | ||
isReady: function() { | ||
return this.ready.state !== "pending"; | ||
}, | ||
/** Access object property (`key` supports dot notation). */ | ||
get: function(key) { | ||
var i, | ||
cur = this._data, | ||
parts = ("" + key) // convert to string | ||
.replace(/\[(\w+)\]/g, ".$1") // convert indexes to properties | ||
.replace(/^\./, "") // strip a leading dot | ||
.split("."); | ||
if ( this.opts.remote ) { | ||
if ( ((now - prevChange) >= this.opts.pushDelay) || | ||
((now - this.unpushedSince) >= this.opts.maxPushDelay) ) { | ||
// NOTE: this is slower (tested on Safari): | ||
// return key.split(".").reduce(function(prev, curr) { | ||
// return prev[curr]; | ||
// }, this._data); | ||
this.debug("_invalidate(): force push", ((now - prevChange) >= this.opts.pushDelay), | ||
((now - this.unpushedSince) >= this.opts.maxPushDelay)); | ||
this.push(); | ||
} else { | ||
nextPush = Math.min( | ||
now + this.opts.pushDelay + 1, | ||
this.unpushedSince + this.opts.maxPushDelay + 1 ); | ||
} | ||
} | ||
if ( nextCommit || nextPush ) { | ||
nextCheck = Math.min( | ||
nextCommit || MAX_INT, | ||
nextPush || MAX_INT); | ||
// this.debug("Defer update:", nextCheck - now) | ||
this.debug("_invalidate(" + hint + ") defer by " + (nextCheck - now) + "ms" ); | ||
this._checkTimer = setTimeout(function() { | ||
self._checkTimer = null; // no need to call clearTimeout in the handler... | ||
self._invalidate.call(self, null, true); | ||
}, nextCheck - now); | ||
} | ||
}, | ||
/* Load data from localStorage. */ | ||
_update: function(objData) { | ||
if ( this.uncommittedSince ) { | ||
console.warn("Updating an uncommitted object."); | ||
if ( this.conflict(objData, this._data) === false ) { | ||
return; | ||
} | ||
} | ||
this._data = objData; | ||
// this.dirty = false; | ||
this.lastUpdate = _getNow(); | ||
}, | ||
/* Return readable string representation for this instance. */ | ||
toString: function() { | ||
return "PersistentObject('" + this.namespace + "')"; | ||
}, | ||
/* Log to console if opts.debugLevel >= 2 */ | ||
debug: function() { | ||
if ( this.opts.debugLevel >= 2 ) { | ||
Array.prototype.unshift.call(arguments, this.toString()); | ||
console.log.apply(window.console, arguments); | ||
} | ||
}, | ||
/* Log to console if opts.debugLevel >= 1 */ | ||
log: function() { | ||
if ( this.opts.debugLevel >= 1 ) { | ||
Array.prototype.unshift.call(arguments, this.toString()); | ||
console.log.apply(window.console, arguments); | ||
} | ||
}, | ||
/** Return true if there are uncommited or unpushed modifications. */ | ||
isDirty: function() { | ||
return !!( | ||
(this.storage && this.uncommittedSince) || | ||
(this.opts.remote && this.unpushedSince)); | ||
}, | ||
/** Return true if initial pull has completed. */ | ||
isReady: function() { | ||
return this.ready.state !== "pending"; | ||
}, | ||
/** Access object property (`key` supports dot notation). */ | ||
get: function(key) { | ||
var i, | ||
cur = this._data, | ||
parts = ("" + key) // convert to string | ||
.replace(/\[(\w+)\]/g, ".$1") // convert indexes to properties | ||
.replace(/^\./, "") // strip a leading dot | ||
.split("."); | ||
for (i = 0; i < parts.length; i++) { | ||
cur = cur[parts[i]]; | ||
if (cur === undefined && i < parts.length - 1) { | ||
error( | ||
this + | ||
": Property '" + | ||
key + | ||
"' could not be accessed because parent '" + | ||
parts.slice(0, i + 1).join(".") + | ||
"' does not exist" | ||
); | ||
} | ||
} | ||
return cur; | ||
}, | ||
/* Modify object property and set the `dirty` flag (`key` supports dot notation). */ | ||
_setOrRemove: function(key, value, remove) { | ||
var i, | ||
parent, | ||
cur = this._data, | ||
parts = ("" + key) // convert to string | ||
.replace(/\[(\w+)\]/g, ".$1") // convert indexes to properties | ||
.replace(/^\./, "") // strip a leading dot | ||
.split("."), | ||
lastPart = parts.pop(); | ||
// NOTE: this is slower (tested on Safari): | ||
// return key.split(".").reduce(function(prev, curr) { | ||
// return prev[curr]; | ||
// }, this._data); | ||
for (i = 0; i < parts.length; i++) { | ||
parent = cur; | ||
cur = parent[parts[i]]; | ||
// Create intermediate parent objects properties if required | ||
if (cur === undefined) { | ||
if (this.opts.createParents) { | ||
this.debug("Creating intermediate parent '" + parts[i] + "'"); | ||
cur = parent[parts[i]] = {}; | ||
} else { | ||
error( | ||
this + | ||
": Property '" + | ||
key + | ||
"' could not be set because parent '" + | ||
parts.slice(0, i + 1).join(".") + | ||
"' does not exist" | ||
); | ||
} | ||
} | ||
} | ||
if (cur[lastPart] !== value) { | ||
if (remove === true) { | ||
delete cur[lastPart]; | ||
this._invalidate("remove"); | ||
} else { | ||
cur[lastPart] = value; | ||
this._invalidate("set"); | ||
} | ||
} | ||
}, | ||
/** Modify object property and set the `dirty` flag (`key` supports dot notation). */ | ||
set: function(key, value) { | ||
return this._setOrRemove(key, value, false); | ||
}, | ||
/** Delete object property and set the `dirty` flag (`key` supports dot notation). */ | ||
remove: function(key) { | ||
return this._setOrRemove(key, undefined, true); | ||
}, | ||
/** Replace data object with a new instance. */ | ||
reset: function(obj) { | ||
this._data = obj || {}; | ||
this._invalidate("reset"); | ||
}, | ||
/** Flag object as modified, so that commit / push will be scheduled. */ | ||
setDirty: function(flag) { | ||
if (flag === false) { | ||
} else { | ||
this._invalidate("explicit"); | ||
} | ||
}, | ||
/** Load data from localStorage. */ | ||
update: function() { | ||
if (this.phase) { | ||
error( | ||
this + ": Trying to update while '" + this.phase + "' is pending." | ||
); | ||
} | ||
if (this.opts.debugLevel >= 2 && console.time) { | ||
console.time(this + ".update"); | ||
} | ||
var data = this.storage.getItem(this.namespace); | ||
data = JSON.parse(data); | ||
this._update(data); | ||
if (this.opts.debugLevel >= 2 && console.time) { | ||
console.timeEnd(this + ".update"); | ||
} | ||
}, | ||
/** Write data to localStorage. */ | ||
commit: function() { | ||
var data; | ||
if (this.phase) { | ||
error( | ||
this + ": Trying to commit while '" + this.phase + "' is pending." | ||
); | ||
} | ||
if (this.opts.debugLevel >= 2 && console.time) { | ||
console.time(this + ".commit"); | ||
} | ||
// try { data = JSON.stringify(this._data); } catch(e) { } | ||
data = JSON.stringify(this._data); | ||
this.storage.setItem(this.namespace, data); | ||
// this.dirty = false; | ||
this.uncommittedSince = null; | ||
this.commitCount += 1; | ||
// this.lastCommit = Date.now(); | ||
if (this.opts.debugLevel >= 2 && console.time) { | ||
console.timeEnd(this + ".commit"); | ||
} | ||
return data; | ||
}, | ||
/** Download, then update data from the cloud. */ | ||
pull: function() { | ||
var self = this; | ||
for (i = 0; i < parts.length; i++) { | ||
cur = cur[parts[i]]; | ||
if ( cur === undefined && i < (parts.length - 1) ) { | ||
$.error(this + ": Property '" + key + "' could not be accessed because parent '" + | ||
parts.slice(0, i + 1).join(".") + "' does not exist"); | ||
} | ||
} | ||
return cur; | ||
}, | ||
/* Modify object property and set the `dirty` flag (`key` supports dot notation). */ | ||
_setOrRemove: function(key, value, remove) { | ||
var i, parent, | ||
cur = this._data, | ||
parts = ("" + key) // convert to string | ||
.replace(/\[(\w+)\]/g, ".$1") // convert indexes to properties | ||
.replace(/^\./, "") // strip a leading dot | ||
.split("."), | ||
lastPart = parts.pop(); | ||
if (this.phase) { | ||
error(this + ": Trying to pull while '" + this.phase + "' is pending."); | ||
} | ||
if (this.opts.debugLevel >= 2 && console.time) { | ||
console.time(this + ".pull"); | ||
} | ||
this.phase = "pull"; | ||
// return $.get(this.opts.remote, function(objData) { | ||
return $.ajax({ | ||
type: "GET", | ||
url: this.opts.remote, | ||
}) | ||
.done(function(objData) { | ||
var strData = objData; | ||
if ($.isArray(objData) || $.isPlainObject(objData)) { | ||
strData = JSON.stringify(objData); | ||
} else { | ||
objData = JSON.parse(objData); | ||
} | ||
self.storage.setItem(self.namespace, strData); | ||
self._update(objData); | ||
self.lastPull = Date.now(); | ||
}) | ||
.fail(function() { | ||
self.opts.error(arguments); | ||
}) | ||
.always(function() { | ||
self.phase = null; | ||
if (self.opts.debugLevel >= 2 && console.time) { | ||
console.timeEnd(self + ".pull"); | ||
} | ||
}); | ||
}, | ||
/** Commit, then upload data to the cloud. */ | ||
push: function() { | ||
var self = this, | ||
data = this.commit(); | ||
for (i = 0; i < parts.length; i++) { | ||
parent = cur; | ||
cur = parent[parts[i]]; | ||
// Create intermediate parent objects properties if required | ||
if ( cur === undefined ) { | ||
if ( this.opts.createParents ) { | ||
this.debug("Creating intermediate parent '" + parts[i] + "'"); | ||
cur = parent[parts[i]] = {}; | ||
} else { | ||
$.error(this + ": Property '" + key + "' could not be set because parent '" + | ||
parts.slice(0, i + 1).join(".") + "' does not exist"); | ||
} | ||
} | ||
} | ||
if ( cur[lastPart] !== value ) { | ||
if ( remove === true ) { | ||
delete cur[lastPart]; | ||
this._invalidate("remove"); | ||
} else { | ||
cur[lastPart] = value; | ||
this._invalidate("set"); | ||
} | ||
} | ||
}, | ||
/** Modify object property and set the `dirty` flag (`key` supports dot notation). */ | ||
set: function(key, value) { | ||
return this._setOrRemove(key, value, false); | ||
}, | ||
/** Delete object property and set the `dirty` flag (`key` supports dot notation). */ | ||
remove: function(key) { | ||
return this._setOrRemove(key, undefined, true); | ||
}, | ||
/** Replace data object with a new instance. */ | ||
reset: function(obj) { | ||
this._data = obj || {}; | ||
this._invalidate("reset"); | ||
}, | ||
/** Flag object as modified, so that commit / push will be scheduled. */ | ||
setDirty: function(flag) { | ||
if ( flag === false ) { | ||
} else { | ||
this._invalidate("explicit"); | ||
} | ||
}, | ||
/** Load data from localStorage. */ | ||
update: function() { | ||
if ( this.phase ) { | ||
$.error(this + ": Trying to update while '" + this.phase + "' is pending."); | ||
} | ||
if ( this.opts.debugLevel >= 2 && console.time ) { console.time(this + ".update"); } | ||
var data = this.storage.getItem(this.namespace); | ||
data = JSON.parse(data); | ||
this._update(data); | ||
if ( this.opts.debugLevel >= 2 && console.time ) { console.timeEnd(this + ".update"); } | ||
}, | ||
/** Write data to localStorage. */ | ||
commit: function() { | ||
var data; | ||
if ( this.phase ) { | ||
$.error(this + ": Trying to commit while '" + this.phase + "' is pending."); | ||
} | ||
if ( this.opts.debugLevel >= 2 && console.time ) { console.time(this + ".commit"); } | ||
// try { data = JSON.stringify(this._data); } catch(e) { } | ||
data = JSON.stringify(this._data); | ||
this.storage.setItem(this.namespace, data); | ||
// this.dirty = false; | ||
this.uncommittedSince = null; | ||
this.commitCount += 1; | ||
// this.lastCommit = _getNow(); | ||
if ( this.opts.debugLevel >= 2 && console.time ) { console.timeEnd(this + ".commit"); } | ||
return data; | ||
}, | ||
/** Download, then update data from the cloud. */ | ||
pull: function() { | ||
var self = this; | ||
if (this.phase) { | ||
error(this + ": Trying to push while '" + this.phase + "' is pending."); | ||
} | ||
if (this.opts.debugLevel >= 2 && console.time) { | ||
console.time(self + ".push"); | ||
} | ||
this.phase = "push"; | ||
if (!this.opts.remote) { | ||
error(this + ": Missing remote option"); | ||
} | ||
return $.ajax({ | ||
type: "PUT", | ||
url: this.opts.remote, | ||
data: data, | ||
}) | ||
.done(function() { | ||
// console.log("PUT", arguments); | ||
// self.lastPush = Date.now(); | ||
self.unpushedSince = null; | ||
self.pushCount += 1; | ||
}) | ||
.fail(function() { | ||
self.opts.error(arguments); | ||
}) | ||
.always(function() { | ||
self.phase = null; | ||
if (self.opts.debugLevel >= 2 && console.time) { | ||
console.timeEnd(self + ".push"); | ||
} | ||
}); | ||
}, | ||
/** Read data properties from form input elements with the same name. | ||
* Supports elements of input (type: text, radio, checkbox), textarea, | ||
* and select. | ||
*/ | ||
readFromForm: function(form, options) { | ||
var self = this, | ||
$form = $(form), | ||
opts = $.extend( | ||
{ | ||
addNew: false, | ||
coerce: true, // convert single checkboxes to bool (instead value) | ||
trim: true, | ||
}, | ||
options | ||
); | ||
if ( this.phase ) { | ||
$.error(this + ": Trying to pull while '" + this.phase + "' is pending."); | ||
} | ||
if ( this.opts.debugLevel >= 2 && console.time ) { console.time(this + ".pull"); } | ||
this.phase = "pull"; | ||
// return $.get(this.opts.remote, function(objData) { | ||
return $.ajax({ | ||
type: "GET", | ||
url: this.opts.remote | ||
}).done(function(objData) { | ||
var strData = objData; | ||
if ( $.isArray(objData) || $.isPlainObject(objData) ) { | ||
strData = JSON.stringify(objData); | ||
} else { | ||
objData = JSON.parse(objData); | ||
} | ||
self.storage.setItem(self.namespace, strData); | ||
self._update(objData); | ||
self.lastPull = _getNow(); | ||
}).fail(function() { | ||
self.opts.error(arguments); | ||
}).always(function() { | ||
self.phase = null; | ||
if ( self.opts.debugLevel >= 2 && console.time ) { console.timeEnd(self + ".pull"); } | ||
}); | ||
}, | ||
/** Commit, then upload data to the cloud. */ | ||
push: function() { | ||
var self = this, | ||
data = this.commit(); | ||
if (opts.addNew) { | ||
$form.find("[name]").each(function() { | ||
var name = $(this).attr("name"); | ||
if (self._data[name] === undefined) { | ||
self.debug("readFromForm: add field '" + name + "'"); | ||
self._data[name] = null; | ||
} | ||
}); | ||
} | ||
$.each(this._data, function(k, v) { | ||
var val, | ||
$input = $form.find("[name='" + k + "']"), | ||
type = $input.attr("type"); | ||
if ( this.phase ) { | ||
$.error(this + ": Trying to push while '" + this.phase + "' is pending."); | ||
} | ||
if ( this.opts.debugLevel >= 2 && console.time ) { console.time(self + ".push"); } | ||
this.phase = "push"; | ||
if ( !this.opts.remote ) { $.error(this + ": Missing remote option"); } | ||
return $.ajax({ | ||
type: "PUT", | ||
url: this.opts.remote, | ||
data: data | ||
}).done(function() { | ||
// console.log("PUT", arguments); | ||
// self.lastPush = _getNow(); | ||
self.unpushedSince = null; | ||
self.pushCount += 1; | ||
}).fail(function() { | ||
self.opts.error(arguments); | ||
}).always(function() { | ||
self.phase = null; | ||
if ( self.opts.debugLevel >= 2 && console.time ) { console.timeEnd(self + ".push"); } | ||
}); | ||
}, | ||
/** Read data properties from form input elements with the same name. | ||
* Supports elements of input (type: text, radio, checkbox), textarea, | ||
* and select. | ||
*/ | ||
readFromForm: function(form, options) { | ||
var self = this, | ||
$form = $(form), | ||
opts = $.extend({ | ||
addNew: false, | ||
coerce: true, // convert single checkboxes to bool (instead value) | ||
trim: true | ||
}, options); | ||
if (!$input.length) { | ||
self.debug("readFromForm: field not found: '" + k + "'"); | ||
return; | ||
} | ||
if (type === "radio") { | ||
val = $input.filter(":checked").val(); | ||
} else if (type === "checkbox" && $input.length === 1) { | ||
val = !!$input.filter(":checked").length; | ||
} else if (type === "checkbox" && $input.length > 1) { | ||
val = []; | ||
$input.filter(":checked").each(function() { | ||
val.push($(this).val()); | ||
}); | ||
} else { | ||
val = $input.val(); | ||
if (opts.trim && typeof val === "string") { | ||
val = $.trim(val); | ||
} | ||
} | ||
// console.log("readFromForm: val(" + k + "): '" + val + "'"); | ||
self.set(k, val); | ||
}); | ||
// console.log("readFromForm: '" + this + "'", this._data); | ||
}, | ||
/** Write data to form elements with the same name. | ||
*/ | ||
writeToForm: function(form, options) { | ||
var $form = $(form); | ||
if ( opts.addNew ) { | ||
$form.find("[name]").each(function() { | ||
var name = $(this).attr("name"); | ||
if ( self._data[name] === undefined ) { | ||
self.debug("readFromForm: add field '" + name + "'"); | ||
self._data[name] = null; | ||
} | ||
}); | ||
} | ||
$.each(this._data, function(k, v) { | ||
var val, | ||
$input = $form.find("[name='" + k + "']"), | ||
type = $input.attr("type"); | ||
$.each(this._data, function(k, v) { | ||
var $input = $form.find("[name='" + k + "']"), | ||
type = $input.attr("type"); | ||
if ( !$input.length ) { | ||
self.debug("readFromForm: field not found: '" + k + "'"); | ||
return; | ||
} | ||
if ( type === "radio") { | ||
val = $input.filter(":checked").val(); | ||
} else if ( type === "checkbox" && $input.length === 1 ) { | ||
val = !!$input.filter(":checked").length; | ||
} else if ( type === "checkbox" && $input.length > 1 ) { | ||
val = []; | ||
$input.filter(":checked").each(function() { | ||
val.push($(this).val()); | ||
}); | ||
} else { | ||
val = $input.val(); | ||
if ( opts.trim && typeof val === "string" ) { val = $.trim(val); } | ||
} | ||
// console.log("readFromForm: val(" + k + "): '" + val + "'"); | ||
self.set(k, val); | ||
}); | ||
// console.log("readFromForm: '" + this + "'", this._data); | ||
}, | ||
/** Write data to form elements with the same name. | ||
*/ | ||
writeToForm: function(form, options) { | ||
var $form = $(form); | ||
$.each(this._data, function(k, v) { | ||
var $input = $form.find("[name='" + k + "']"), | ||
type = $input.attr("type"); | ||
if ( $input.length ) { | ||
if ( type === "radio" ) { | ||
$input.filter("[value='" + v + "']").prop("checked", true); | ||
} else if ( type === "checkbox" ) { | ||
if ( $input.length === 1 ) { | ||
$input.prop("checked", !!v); | ||
} else { | ||
// multi-value checkbox | ||
$input.each(function() { | ||
$(this).prop("checked", $.isArray(v) ? | ||
$.inArray(this.value, v) >= 0 : this.value === v); | ||
}); | ||
} | ||
} else if ( $input.is("select") ) { | ||
// listbox | ||
$input.find("option").each(function() { | ||
$(this).prop("selected", $.isArray(v) ? | ||
$.inArray(this.value, v) >= 0 : this.value === v); | ||
}); | ||
} else { | ||
$input.val(v); | ||
} | ||
} | ||
}); | ||
} | ||
}; | ||
// ----------------------------------------------------------------------------- | ||
}(jQuery, window, document)); | ||
if ($input.length) { | ||
if (type === "radio") { | ||
$input.filter("[value='" + v + "']").prop("checked", true); | ||
} else if (type === "checkbox") { | ||
if ($input.length === 1) { | ||
$input.prop("checked", !!v); | ||
} else { | ||
// multi-value checkbox | ||
$input.each(function() { | ||
$(this).prop( | ||
"checked", | ||
$.isArray(v) | ||
? $.inArray(this.value, v) >= 0 | ||
: this.value === v | ||
); | ||
}); | ||
} | ||
} else if ($input.is("select")) { | ||
// listbox | ||
$input.find("option").each(function() { | ||
$(this).prop( | ||
"selected", | ||
$.isArray(v) ? $.inArray(this.value, v) >= 0 : this.value === v | ||
); | ||
}); | ||
} else { | ||
$input.val(v); | ||
} | ||
} | ||
}); | ||
}, | ||
}; | ||
// ----------------------------------------------------------------------------- | ||
// Value returned by `require('persisto')` | ||
return window.PersistentObject; | ||
}); // End of closure |
@@ -1,4 +0,4 @@ | ||
/*! Persistent Javascript objects and web forms using Web Storage. - v1.1.0 - 2016-08-14 | https://github.com/mar10/persisto | Copyright (c) 2016 Martin Wendt; Licensed MIT */ | ||
/*! Persistent JavaScript objects and web forms using Web Storage. - v1.2.0 - 2019-09-07 | https://github.com/mar10/persisto | Copyright (c) 2019 Martin Wendt; Licensed MIT */ | ||
!function(a,b,c,d){"use strict";function e(){return(new Date).getTime()}var f=9007199254740991;b.PersistentObject=function(c,f){var g,h=this,i=a.Deferred(),j=e();this instanceof PersistentObject||a.error("Must use 'new' keyword"),"string"!=typeof c&&a.error(this+": Missing required argument: namespace"),this.opts=a.extend({remote:null,defaults:{},commitDelay:500,createParents:!0,maxCommitDelay:3e3,pushDelay:5e3,maxPushDelay:3e4,storage:b.localStorage,debugLevel: 1,change:a.noop,commit:a.noop,conflict:a.noop,error:a.noop,pull:a.noop,push:a.noop,update:a.noop},f),this._checkTimer=null,this.namespace=c,this.storage=this.opts.storage,this._data=this.opts.defaults,this.offline=d,this.phase=null,this.uncommittedSince=null,this.unpushedSince=null,this.lastUpdate=0,this.lastPull=0,this.commitCount=0,this.pushCount=0,this.lastModified=j,this.ready=i.promise,g=this.storage?this.storage.getItem(this.namespace):null,this.opts.remote?this.pull().done(function(){h.offline=!1,i.resolve()}).fail(function(){h.offline=!0,null!=g?(console.warn(h+": could not init from remote; falling back to storage."),h._data=a.extend({},h.opts.defaults,JSON.parse(g))):console.warn(h+": could not init from remote; falling back default."),i.resolve()}):null!=g?(this.update(),this._data=a.extend({},this.opts.defaults,this._data),i.resolve()):i.resolve()},b.PersistentObject.prototype={version:"1.1.0",_invalidate:function(a,b){var c=this,d=this.lastModified,g=e(),h=0,i=0,j=0;this._checkTimer&&(clearTimeout(this._checkTimer),this._checkTimer=null),b?this.debug("_invalidate() recursive"):(this.lastModified=g,this.uncommittedSince||(this.uncommittedSince=g),this.unpushedSince||(this.unpushedSince=g),this.opts.change(a)),this.storage&&(g-d>=this.opts.commitDelay||g-this.uncommittedSince>=this.opts.maxCommitDelay?(this.debug("_invalidate(): force commit",g-d>=this.opts.commitDelay,g-this.uncommittedSince>=this.opts.maxCommitDelay),this.commit()):h=Math.min(g+this.opts.commitDelay+1,this.uncommittedSince+this.opts.maxCommitDelay+1)),this.opts.remote&&(g-d>=this.opts.pushDelay||g-this.unpushedSince>=this.opts.maxPushDelay?(this.debug("_invalidate(): force push",g-d>=this.opts.pushDelay,g-this.unpushedSince>=this.opts.maxPushDelay),this.push()):i=Math.min(g+this.opts.pushDelay+1,this.unpushedSince+this.opts.maxPushDelay+1)),(h||i)&&(j=Math.min(h||f,i||f),this.debug("_invalidate("+a+") defer by "+(j-g)+"ms"),this._checkTimer=setTimeout(function(){c._checkTimer=null,c._invalidate.call(c,null,!0)},j-g))},_update:function(a){this.uncommittedSince&&(console.warn("Updating an uncommitted object."),this.conflict(a,this._data)===!1)||(this._data=a,this.lastUpdate=e())},toString:function(){return"PersistentObject('"+this.namespace+"')"},debug:function(){this.opts.debugLevel>=2&&(Array.prototype.unshift.call(arguments,this.toString()),console.log.apply(b.console,arguments))},log:function(){this.opts.debugLevel>=1&&(Array.prototype.unshift.call(arguments,this.toString()),console.log.apply(b.console,arguments))},isDirty:function(){return!!(this.storage&&this.uncommittedSince||this.opts.remote&&this.unpushedSince)},isReady:function(){return"pending"!==this.ready.state},get:function(b){var c,e=this._data,f=(""+b).replace(/\[(\w+)\]/g,".$1").replace(/^\./,"").split(".");for(c=0;c<f.length;c++)e=e[f[c]],e===d&&c<f.length-1&&a.error(this+": Property '"+b+"' could not be accessed because parent '"+f.slice(0,c+1).join(".")+"' does not exist");return e},_setOrRemove:function(b,c,e){var f,g,h=this._data,i=(""+b).replace(/\[(\w+)\]/g,".$1").replace(/^\./,"").split("."),j=i.pop();for(f=0;f<i.length;f++)g=h,h=g[i[f]],h===d&&(this.opts.createParents?(this.debug("Creating intermediate parent '"+i[f]+"'"),h=g[i[f]]={}):a.error(this+": Property '"+b+"' could not be set because parent '"+i.slice(0,f+1).join(".")+"' does not exist"));h[j]!==c&&(e===!0?(delete h[j],this._invalidate("remove")):(h[j]=c,this._invalidate("set")))},set:function(a,b){return this._setOrRemove(a,b,!1)},remove:function(a){return this._setOrRemove(a,d,!0)},reset:function(a){this._data=a||{},this._invalidate("reset")},setDirty:function(a){a===!1||this._invalidate("explicit")},update:function(){this.phase&&a.error(this+": Trying to update while '"+this.phase+"' is pending."),this.opts.debugLevel>=2&&console.time&&console.time(this+".update");var b=this.storage.getItem(this.namespace);b=JSON.parse(b),this._update(b),this.opts.debugLevel>=2&&console.time&&console.timeEnd(this+".update")},commit:function(){var b;return this.phase&&a.error(this+": Trying to commit while '"+this.phase+"' is pending."),this.opts.debugLevel>=2&&console.time&&console.time(this+".commit"),b=JSON.stringify(this._data),this.storage.setItem(this.namespace,b),this.uncommittedSince=null,this.commitCount+=1,this.opts.debugLevel>=2&&console.time&&console.timeEnd(this+".commit"),b},pull:function(){var b=this;return this.phase&&a.error(this+": Trying to pull while '"+this.phase+"' is pending."),this.opts.debugLevel>=2&&console.time&&console.time(this+".pull"),this.phase="pull",a.ajax({type:"GET",url:this.opts.remote}).done(function(c){var d=c;a.isArray(c)||a.isPlainObject(c)?d=JSON.stringify(c):c=JSON.parse(c),b.storage.setItem(b.namespace,d),b._update(c),b.lastPull=e()}).fail(function(){b.opts.error(arguments)}).always(function(){b.phase=null,b.opts.debugLevel>=2&&console.time&&console.timeEnd(b+".pull")})},push:function(){var b=this,c=this.commit();return this.phase&&a.error(this+": Trying to push while '"+this.phase+"' is pending."),this.opts.debugLevel>=2&&console.time&&console.time(b+".push"),this.phase="push",this.opts.remote||a.error(this+": Missing remote option"),a.ajax({type:"PUT",url:this.opts.remote,data:c}).done(function(){b.unpushedSince=null,b.pushCount+=1}).fail(function(){b.opts.error(arguments)}).always(function(){b.phase=null,b.opts.debugLevel>=2&&console.time&&console.timeEnd(b+".push")})},readFromForm:function(b,c){var e=this,f=a(b),g=a.extend({addNew:!1,coerce:!0,trim:!0},c);g.addNew&&f.find("[name]").each(function(){var b=a(this).attr("name");e._data[b]===d&&(e.debug("readFromForm: add field '"+b+"'"),e._data[b]=null)}),a.each(this._data,function(b,c){var d,h=f.find("[name='"+b+"']"),i=h.attr("type");return h.length?("radio"===i?d=h.filter(":checked").val():"checkbox"===i&&1===h.length?d=!!h.filter(":checked").length:"checkbox"===i&&h.length>1?(d=[],h.filter(":checked").each(function(){d.push(a(this).val())})):(d=h.val(),g.trim&&"string"==typeof d&&(d=a.trim(d))),void e.set(b,d)):void e.debug("readFromForm: field not found: '"+b+"'")})},writeToForm:function(b,c){var d=a(b);a.each(this._data,function(b,c){var e=d.find("[name='"+b+"']"),f=e.attr("type");e.length&&("radio"===f?e.filter("[value='"+c+"']").prop("checked",!0):"checkbox"===f?1===e.length?e.prop("checked",!!c):e.each(function(){a(this).prop("checked",a.isArray(c)?a.inArray(this.value,c)>=0:this.value===c)}):e.is("select")?e.find("option").each(function(){a(this).prop("selected",a.isArray(c)?a.inArray(this.value,c)>=0:this.value===c)}):e.val(c))})}}}(jQuery,window,document); | ||
!function(a){"function"==typeof define&&define.amd?define(["jquery"],a):"object"==typeof module&&module.exports?module.exports=a(require("jquery")):a(jQuery)}(function(a){"use strict";var b=9007199254740991,c=window.console,d=a.error;return window.PersistentObject=function(b,e){var f,g=this,h=a.Deferred(),i=Date.now();this instanceof PersistentObject||d("Must use 'new' keyword"),"string"!=typeof b&&d(this+": Missing required argument: namespace"),this.opts=a.extend({remote:null,defaults:{},commitDelay:500,createParents:!0,maxCommitDelay:3e3,pushDelay:5e3,maxPushDelay:3e4,storage:window.localStorage,debugLevel: 1,change:a.noop,commit:a.noop,conflict:a.noop,error:a.noop,pull:a.noop,push:a.noop,update:a.noop},e),this._checkTimer=null,this.namespace=b,this.storage=this.opts.storage,this._data=this.opts.defaults,this.offline=void 0,this.phase=null,this.uncommittedSince=null,this.unpushedSince=null,this.lastUpdate=0,this.lastPull=0,this.commitCount=0,this.pushCount=0,this.lastModified=i,this.ready=h.promise,f=this.storage?this.storage.getItem(this.namespace):null,this.opts.remote?this.pull().done(function(){g.offline=!1,h.resolve()}).fail(function(){g.offline=!0,null!=f?(c.warn(g+": could not init from remote; falling back to storage."),g._data=a.extend({},g.opts.defaults,JSON.parse(f))):c.warn(g+": could not init from remote; falling back default."),h.resolve()}):null!=f?(this.update(),this._data=a.extend({},this.opts.defaults,this._data),h.resolve()):h.resolve()},window.PersistentObject.prototype={version:"1.2.0",_invalidate:function(a,c){var d=this,e=this.lastModified,f=Date.now(),g=0,h=0,i=0;this._checkTimer&&(clearTimeout(this._checkTimer),this._checkTimer=null),c?this.debug("_invalidate() recursive"):(this.lastModified=f,this.uncommittedSince||(this.uncommittedSince=f),this.unpushedSince||(this.unpushedSince=f),this.opts.change(a)),this.storage&&(f-e>=this.opts.commitDelay||f-this.uncommittedSince>=this.opts.maxCommitDelay?(this.debug("_invalidate(): force commit",f-e>=this.opts.commitDelay,f-this.uncommittedSince>=this.opts.maxCommitDelay),this.commit()):g=Math.min(f+this.opts.commitDelay+1,this.uncommittedSince+this.opts.maxCommitDelay+1)),this.opts.remote&&(f-e>=this.opts.pushDelay||f-this.unpushedSince>=this.opts.maxPushDelay?(this.debug("_invalidate(): force push",f-e>=this.opts.pushDelay,f-this.unpushedSince>=this.opts.maxPushDelay),this.push()):h=Math.min(f+this.opts.pushDelay+1,this.unpushedSince+this.opts.maxPushDelay+1)),(g||h)&&(i=Math.min(g||b,h||b),this.debug("_invalidate("+a+") defer by "+(i-f)+"ms"),this._checkTimer=setTimeout(function(){d._checkTimer=null,d._invalidate.call(d,null,!0)},i-f))},_update:function(a){this.uncommittedSince&&(c.warn("Updating an uncommitted object."),this.conflict(a,this._data)===!1)||(this._data=a,this.lastUpdate=Date.now())},toString:function(){return"PersistentObject('"+this.namespace+"')"},debug:function(){this.opts.debugLevel>=2&&(Array.prototype.unshift.call(arguments,this.toString()),c.log.apply(c,arguments))},log:function(){this.opts.debugLevel>=1&&(Array.prototype.unshift.call(arguments,this.toString()),c.log.apply(c,arguments))},isDirty:function(){return!!(this.storage&&this.uncommittedSince||this.opts.remote&&this.unpushedSince)},isReady:function(){return"pending"!==this.ready.state},get:function(a){var b,c=this._data,e=(""+a).replace(/\[(\w+)\]/g,".$1").replace(/^\./,"").split(".");for(b=0;b<e.length;b++)c=c[e[b]],void 0===c&&b<e.length-1&&d(this+": Property '"+a+"' could not be accessed because parent '"+e.slice(0,b+1).join(".")+"' does not exist");return c},_setOrRemove:function(a,b,c){var e,f,g=this._data,h=(""+a).replace(/\[(\w+)\]/g,".$1").replace(/^\./,"").split("."),i=h.pop();for(e=0;e<h.length;e++)f=g,g=f[h[e]],void 0===g&&(this.opts.createParents?(this.debug("Creating intermediate parent '"+h[e]+"'"),g=f[h[e]]={}):d(this+": Property '"+a+"' could not be set because parent '"+h.slice(0,e+1).join(".")+"' does not exist"));g[i]!==b&&(c===!0?(delete g[i],this._invalidate("remove")):(g[i]=b,this._invalidate("set")))},set:function(a,b){return this._setOrRemove(a,b,!1)},remove:function(a){return this._setOrRemove(a,void 0,!0)},reset:function(a){this._data=a||{},this._invalidate("reset")},setDirty:function(a){a===!1||this._invalidate("explicit")},update:function(){this.phase&&d(this+": Trying to update while '"+this.phase+"' is pending."),this.opts.debugLevel>=2&&c.time&&c.time(this+".update");var a=this.storage.getItem(this.namespace);a=JSON.parse(a),this._update(a),this.opts.debugLevel>=2&&c.time&&c.timeEnd(this+".update")},commit:function(){var a;return this.phase&&d(this+": Trying to commit while '"+this.phase+"' is pending."),this.opts.debugLevel>=2&&c.time&&c.time(this+".commit"),a=JSON.stringify(this._data),this.storage.setItem(this.namespace,a),this.uncommittedSince=null,this.commitCount+=1,this.opts.debugLevel>=2&&c.time&&c.timeEnd(this+".commit"),a},pull:function(){var b=this;return this.phase&&d(this+": Trying to pull while '"+this.phase+"' is pending."),this.opts.debugLevel>=2&&c.time&&c.time(this+".pull"),this.phase="pull",a.ajax({type:"GET",url:this.opts.remote}).done(function(c){var d=c;a.isArray(c)||a.isPlainObject(c)?d=JSON.stringify(c):c=JSON.parse(c),b.storage.setItem(b.namespace,d),b._update(c),b.lastPull=Date.now()}).fail(function(){b.opts.error(arguments)}).always(function(){b.phase=null,b.opts.debugLevel>=2&&c.time&&c.timeEnd(b+".pull")})},push:function(){var b=this,e=this.commit();return this.phase&&d(this+": Trying to push while '"+this.phase+"' is pending."),this.opts.debugLevel>=2&&c.time&&c.time(b+".push"),this.phase="push",this.opts.remote||d(this+": Missing remote option"),a.ajax({type:"PUT",url:this.opts.remote,data:e}).done(function(){b.unpushedSince=null,b.pushCount+=1}).fail(function(){b.opts.error(arguments)}).always(function(){b.phase=null,b.opts.debugLevel>=2&&c.time&&c.timeEnd(b+".push")})},readFromForm:function(b,c){var d=this,e=a(b),f=a.extend({addNew:!1,coerce:!0,trim:!0},c);f.addNew&&e.find("[name]").each(function(){var b=a(this).attr("name");void 0===d._data[b]&&(d.debug("readFromForm: add field '"+b+"'"),d._data[b]=null)}),a.each(this._data,function(b,c){var g,h=e.find("[name='"+b+"']"),i=h.attr("type");return h.length?("radio"===i?g=h.filter(":checked").val():"checkbox"===i&&1===h.length?g=!!h.filter(":checked").length:"checkbox"===i&&h.length>1?(g=[],h.filter(":checked").each(function(){g.push(a(this).val())})):(g=h.val(),f.trim&&"string"==typeof g&&(g=a.trim(g))),void d.set(b,g)):void d.debug("readFromForm: field not found: '"+b+"'")})},writeToForm:function(b,c){var d=a(b);a.each(this._data,function(b,c){var e=d.find("[name='"+b+"']"),f=e.attr("type");e.length&&("radio"===f?e.filter("[value='"+c+"']").prop("checked",!0):"checkbox"===f?1===e.length?e.prop("checked",!!c):e.each(function(){a(this).prop("checked",a.isArray(c)?a.inArray(this.value,c)>=0:this.value===c)}):e.is("select")?e.find("option").each(function(){a(this).prop("selected",a.isArray(c)?a.inArray(this.value,c)>=0:this.value===c)}):e.val(c))})}},window.PersistentObject}); | ||
//# sourceMappingURL=persisto.min.js.map |
@@ -1,2 +0,2 @@ | ||
Copyright 2016 Martin Wendt, | ||
Copyright 2016-2017 Martin Wendt, | ||
http://wwWendt.de/ | ||
@@ -3,0 +3,0 @@ |
{ | ||
"name": "persisto", | ||
"title": "Persistent Javascript objects and web forms using Web Storage.", | ||
"description": "Persist Javascript objects to localStorage and remote servers.", | ||
"title": "Persistent JavaScript objects and web forms using Web Storage.", | ||
"description": "Persist JavaScript objects to localStorage and remote servers.", | ||
"filename": "dist/persisto.min.js", | ||
"main": "dist/persisto.js", | ||
"version": "1.1.0", | ||
"version": "1.2.0", | ||
"homepage": "https://github.com/mar10/persisto", | ||
@@ -37,16 +37,20 @@ "author": { | ||
"devDependencies": { | ||
"grunt": "~0.4.5", | ||
"eslint": "^5.12.0", | ||
"eslint-config-prettier": "^3.4.0", | ||
"eslint-plugin-prettier": "^3.0.1", | ||
"grunt": "^1.0.4", | ||
"grunt-contrib-clean": "^1.0.0", | ||
"grunt-contrib-compress": "^1.0.0", | ||
"grunt-contrib-concat": "^0.5.1", | ||
"grunt-contrib-connect": "^0.11.2", | ||
"grunt-contrib-copy": "^0.8.2", | ||
"grunt-contrib-concat": "^1.0.0", | ||
"grunt-contrib-connect": "^1.0.0", | ||
"grunt-contrib-copy": "^1.0.0", | ||
"grunt-contrib-jshint": "^1.0.0", | ||
"grunt-contrib-qunit": "^1.0.1", | ||
"grunt-contrib-uglify": "^0.11.1", | ||
"grunt-contrib-watch": "^0.6.1", | ||
"grunt-jscs": "^2.7.0", | ||
"grunt-saucelabs": "^8.1.1", | ||
"grunt-text-replace": "~0.3.7", | ||
"grunt-yabs": "^0.5.0" | ||
"grunt-contrib-uglify": "^1.0.0", | ||
"grunt-contrib-watch": "^1.1.0", | ||
"grunt-eslint": "^21.0.0", | ||
"grunt-saucelabs": "^9.0.1", | ||
"grunt-text-replace": "^0.4.0", | ||
"grunt-yabs": "^1.1.4", | ||
"prettier": "^1.15.3" | ||
}, | ||
@@ -53,0 +57,0 @@ "scripts": { |
@@ -1,8 +0,13 @@ | ||
# persisto [![GitHub version](https://badge.fury.io/gh/mar10%2Fpersisto.svg)](https://github.com/mar10/persisto/releases/latest) [![Build Status](https://travis-ci.org/mar10/persisto.svg?branch=master)](https://travis-ci.org/mar10/persisto) | ||
# persisto | ||
[![GitHub version](https://img.shields.io/github/release/mar10/persisto.svg)](https://github.com/mar10/persisto/releases/latest) | ||
[![Build Status](https://travis-ci.org/mar10/persisto.svg?branch=master)](https://travis-ci.org/mar10/persisto) | ||
[![npm](https://img.shields.io/npm/dm/persisto.svg)](https://www.npmjs.com/package/persisto) | ||
[![](https://data.jsdelivr.com/v1/package/npm/persisto/badge)](https://www.jsdelivr.com/package/npm/persisto) | ||
[![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier) | ||
> Persistent Javascript objects and web forms using Web Storage. | ||
> Persistent JavaScript objects and web forms using Web Storage. | ||
Features | ||
- Persist Javascript objects (`{...}`) to | ||
- Persist JavaScript objects (`{...}`) to | ||
[`localStorage` / `sessionStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API).<br> | ||
@@ -14,3 +19,3 @@ Use the `get()`/`set()` API for direct (even nested) access, hiding the need | ||
(deferred writing appears to be 10-15 times faster) and remote backends. | ||
- Make Javascript objects editable in HTML forms.<br> | ||
- Make JavaScript objects editable in HTML forms.<br> | ||
Common use case: maintain persistent client settings and let users edit them. | ||
@@ -33,7 +38,8 @@ - Optionally synchronize the data with a remote endpoint | ||
[Download the latest persisto.js](https://github.com/mar10/persisto/releases) | ||
or include directly [from CDN](https://www.jsdelivr.com/projects/persisto): | ||
[Download the latest persisto.js](https://github.com/mar10/persisto/releases) | ||
or include directly from CDN: [![](https://data.jsdelivr.com/v1/package/npm/persisto/badge)](https://www.jsdelivr.com/package/npm/persisto) or | ||
[UNPKG](https://unpkg.com/persisto@latest/dist/persisto.min.js): | ||
```html | ||
<script src="//cdn.jsdelivr.net/persisto/1/persisto.min.js"></script> | ||
<script src="//cdn.jsdelivr.net/npm/persisto@1/dist/persisto.min.js"></script> | ||
``` | ||
@@ -51,3 +57,3 @@ | ||
`store` now contains the data that was stored in `localStorage.mySettings` if | ||
`store` now contains the data that was stored in `localStorage.mySettings` if | ||
present. Otherwise, `store` is initialized to the default values that we | ||
@@ -109,3 +115,3 @@ passed with the `.defaults` option. | ||
<label>Nickname:<input name="nickname" type="text" value="" /></label><br> | ||
<label>Theme: | ||
<label>Theme: | ||
<fieldset> | ||
@@ -122,3 +128,3 @@ <label> <input name="theme" type="radio" value="default" /> Standard </label><br> | ||
Note also that only fields are synchronized, that already existed in the storage | ||
data. Use the `addNew` option if *all* form fields should be evaluated and create | ||
data. Use the `addNew` option if *all* form fields should be evaluated and create | ||
new properties in the store object: | ||
@@ -136,6 +142,6 @@ | ||
- Any `PersistentObject` instance is stored as one monolythic JSON string.<br> | ||
*Persisto* deferres and collates these updates, but modifying a single | ||
*Persisto* deferres and collates these updates, but modifying a single | ||
property of a large data object still comes with some overhead.<br> | ||
Splitting data into several `PersistentObject`s may remedy the problem.<br> | ||
But if your data model is more like a table with hundredth's of rows, a | ||
But if your data model is more like a table with hundredth's of rows, a | ||
responsive database backend may be a better choice. | ||
@@ -151,3 +157,3 @@ | ||
Arrays are only a special form of plain Javascript objects, so we can store and | ||
Arrays are only a special form of plain JavaScript objects, so we can store and | ||
access them as top level type like this: | ||
@@ -179,6 +185,6 @@ | ||
### Performance and Direct Access | ||
### Performance and Direct Access | ||
In general, performance costs of `set()` and `get()` calls should be | ||
neglectable, compared to the resulting synchronization times, but in some cases | ||
In general, performance costs of `set()` and `get()` calls should be | ||
neglectable, compared to the resulting synchronization times, but in some cases | ||
direct access of the internal data object may be preferred.<br> | ||
@@ -206,5 +212,5 @@ In this case modifications must be signalled by a call to `setDirty()`. | ||
// Page reload would prevent the delayed commit from happen, so we force | ||
// Page reload would prevent the delayed commit from happen, so we force | ||
// synchronization: | ||
commit(); | ||
store.commit(); | ||
@@ -232,3 +238,3 @@ location.reload(); | ||
store.ready | ||
).done(function(){ | ||
@@ -253,8 +259,9 @@ // Page was loaded and and store has pulled the data from the remote endpoint... | ||
<dd> | ||
Type: <code>int</code>, | ||
Type: <code>int</code>, | ||
default: <code>500</code> milliseconds<br> | ||
Commit changes after 0.5 seconds of inactivity.<br> | ||
This means, after each change, we wait 0.5 more seconds for additional changes | ||
to come in, before the actual commit is executed.<br> | ||
The total delay (first change until actual commit) is limited `maxCommitDelay`.<br> | ||
Commit cached changes to localStorage after 0.5 seconds of inactivity.<br> | ||
After each change, we wait 0.5 more seconds for additional changes to come | ||
in, before the actual commit is executed.<br> | ||
The maximum delay (first change until actual commit) is limited by | ||
`maxCommitDelay`.<br> | ||
Set to <code>0</code> to force synchronous mode. | ||
@@ -264,3 +271,3 @@ </dd> | ||
<dd> | ||
Type: <code>int</code>, | ||
Type: <code>int</code>, | ||
default: <code>1</code><br> | ||
@@ -271,3 +278,3 @@ Verbosity level: 0:quiet, 1:normal, 2:verbose. | ||
<dd> | ||
Type: <code>object</code>, | ||
Type: <code>object</code>, | ||
default: <code>{}</code><br> | ||
@@ -278,17 +285,20 @@ Default value if no data is found in localStorage. | ||
<dd> | ||
Type: <code>int</code>, | ||
Type: <code>int</code>, | ||
default: <code>3000</code> milliseconds<br> | ||
Commit changes max. 3 seconds after first change. | ||
Commit every change max. 3 seconds after it occurred. | ||
</dd> | ||
<dt>maxPushDelay</dt> | ||
<dd> | ||
Type: <code>int</code>, | ||
Type: <code>int</code>, | ||
default: <code>30000</code> milliseconds<br> | ||
Push commits to remote max. 30 seconds after first change. | ||
Push every commit to remote max. 30 seconds after it occurred. | ||
</dd> | ||
<dt>pushDelay</dt> | ||
<dd> | ||
Type: <code>int</code>, | ||
Type: <code>int</code>, | ||
default: <code>5000</code> milliseconds<br> | ||
Push commits to remote after 5 seconds of inactivity. | ||
Push commits to remote after 5 seconds of inactivity.<br> | ||
After each change, we wait 5 more seconds for additional changes to come | ||
in, before the actual push is executed.<br> | ||
The maximum delay (first change until actual push) is limited by `maxPushDelay`.<br> | ||
Set to <code>0</code> to force synchronous mode. | ||
@@ -298,3 +308,3 @@ </dd> | ||
<dd> | ||
Type: <code>string</code>, | ||
Type: <code>string</code>, | ||
default: <code>null</code><br> | ||
@@ -305,3 +315,3 @@ URL for GET/PUT request. Pass `null` to disable remote synchronization. | ||
<dd> | ||
Type: <code>object</code>, | ||
Type: <code>object</code>, | ||
default: <code>window.localStorage</code><br> | ||
@@ -430,2 +440,2 @@ Instance of [Web Storage](https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API).<br> | ||
</dl> | ||
--> | ||
--> |
/*! | ||
* persisto.js | ||
* | ||
* Persistent Javascript objects and web forms using Web Storage. | ||
* Persistent JavaScript objects and web forms using Web Storage. | ||
* | ||
* Copyright (c) 2016, Martin Wendt (http://wwWendt.de) | ||
* Copyright (c) 2016-2017, Martin Wendt (http://wwWendt.de) | ||
* Released under the MIT license | ||
@@ -13,468 +13,553 @@ * | ||
;(function($, window, document, undefined) { | ||
(function(factory) { | ||
if (typeof define === "function" && define.amd) { | ||
// AMD. Register as an anonymous module. | ||
define(["jquery"], factory); | ||
} else if (typeof module === "object" && module.exports) { | ||
// Node/CommonJS | ||
module.exports = factory(require("jquery")); | ||
} else { | ||
// Browser globals | ||
factory(jQuery); | ||
} | ||
})(function($) { | ||
"use strict"; | ||
/*globals console */ | ||
/******************************************************************************* | ||
* Private functions and variables | ||
*/ | ||
"use strict"; | ||
var MAX_INT = 9007199254740991, | ||
// Allow mangling of some global names: | ||
console = window.console, | ||
error = $.error; | ||
/******************************************************************************* | ||
* Private functions and variables | ||
*/ | ||
/** | ||
* A persistent plain object or array. | ||
*/ | ||
window.PersistentObject = function(namespace, opts) { | ||
var prevValue, | ||
self = this, | ||
dfd = $.Deferred(), | ||
stamp = Date.now(); // disable warning 'PersistentObject is not defined' | ||
var MAX_INT = 9007199254740991; | ||
/* Return current time stamp. */ | ||
function _getNow() { | ||
return new Date().getTime(); | ||
} | ||
/** | ||
* A persistent plain object or array. | ||
*/ | ||
window.PersistentObject = function(namespace, opts) { | ||
var prevValue, | ||
self = this, | ||
dfd = $.Deferred(), | ||
stamp = _getNow(); | ||
/* jshint ignore:start */ // disable warning 'PersistentObject is not defined' | ||
if ( !(this instanceof PersistentObject) ) { $.error("Must use 'new' keyword"); } | ||
/* jshint ignore:start */ if (!(this instanceof PersistentObject)) { | ||
error("Must use 'new' keyword"); | ||
} | ||
/* jshint ignore:end */ | ||
if ( typeof namespace !== "string" ) { | ||
$.error(this + ": Missing required argument: namespace"); | ||
} | ||
if (typeof namespace !== "string") { | ||
error(this + ": Missing required argument: namespace"); | ||
} | ||
this.opts = $.extend({ | ||
remote: null, // URL for GET/PUT, ajax options, or callback | ||
defaults: {}, // default value if no data is found in localStorage | ||
commitDelay: 500, // commit changes after 0.5 seconds of inactivity | ||
createParents: true, // set() creates missing intermediate parent objects for children | ||
maxCommitDelay: 3000, // commit changes max. 3 seconds after first change | ||
pushDelay: 5000, // push commits after 5 seconds of inactivity | ||
maxPushDelay: 30000, // push commits max. 30 seconds after first change | ||
storage: window.localStorage, | ||
// Default debugLevel is set to 1 by `grunt build`: | ||
debugLevel: 2, // 0:quiet, 1:normal, 2:verbose | ||
// Events | ||
change: $.noop, | ||
commit: $.noop, | ||
conflict: $.noop, | ||
error: $.noop, | ||
pull: $.noop, | ||
push: $.noop, | ||
update: $.noop | ||
}, opts); | ||
this._checkTimer = null; | ||
this.namespace = namespace; | ||
this.storage = this.opts.storage; | ||
this._data = this.opts.defaults; | ||
this.opts = $.extend( | ||
{ | ||
remote: null, // URL for GET/PUT, ajax options, or callback | ||
defaults: {}, // default value if no data is found in localStorage | ||
commitDelay: 500, // commit changes after 0.5 seconds of inactivity | ||
createParents: true, // set() creates missing intermediate parent objects for children | ||
maxCommitDelay: 3000, // commit changes max. 3 seconds after first change | ||
pushDelay: 5000, // push commits after 5 seconds of inactivity | ||
maxPushDelay: 30000, // push commits max. 30 seconds after first change | ||
storage: window.localStorage, | ||
// Default debugLevel is set to 1 by `grunt build`: | ||
debugLevel: 2, // 0:quiet, 1:normal, 2:verbose | ||
// Events | ||
change: $.noop, | ||
commit: $.noop, | ||
conflict: $.noop, | ||
error: $.noop, | ||
pull: $.noop, | ||
push: $.noop, | ||
update: $.noop, | ||
}, | ||
opts | ||
); | ||
this._checkTimer = null; | ||
this.namespace = namespace; | ||
this.storage = this.opts.storage; | ||
this._data = this.opts.defaults; | ||
this.offline = undefined; | ||
this.phase = null; | ||
this.uncommittedSince = null; | ||
this.unpushedSince = null; | ||
this.lastUpdate = 0; | ||
this.lastPull = 0; | ||
this.commitCount = 0; | ||
this.pushCount = 0; | ||
this.lastModified = stamp; | ||
this.offline = undefined; | ||
this.phase = null; | ||
this.uncommittedSince = null; | ||
this.unpushedSince = null; | ||
this.lastUpdate = 0; | ||
this.lastPull = 0; | ||
this.commitCount = 0; | ||
this.pushCount = 0; | ||
this.lastModified = stamp; | ||
this.ready = dfd.promise; | ||
this.ready = dfd.promise; | ||
// _data contains the default value. Now load from persistent storage if any | ||
prevValue = this.storage ? this.storage.getItem(this.namespace) : null; | ||
// _data contains the default value. Now load from persistent storage if any | ||
prevValue = this.storage ? this.storage.getItem(this.namespace) : null; | ||
if ( this.opts.remote ) { | ||
// Try to pull, then resolve | ||
this.pull().done(function() { | ||
// self.debug("init from remote", this._data); | ||
self.offline = false; | ||
dfd.resolve(); | ||
}).fail(function() { | ||
self.offline = true; | ||
if ( prevValue != null ) { | ||
console.warn(self + ": could not init from remote; falling back to storage."); | ||
// self._data = JSON.parse(prevValue); | ||
self._data = $.extend({}, self.opts.defaults, JSON.parse(prevValue)); | ||
} else { | ||
console.warn(self + ": could not init from remote; falling back default."); | ||
} | ||
dfd.resolve(); | ||
}); | ||
} else if ( prevValue != null ) { | ||
this.update(); | ||
// We still extend from opts.defaults, in case some fields where missing | ||
this._data = $.extend({}, this.opts.defaults, this._data); | ||
// this.debug("init from storage", this._data); | ||
dfd.resolve(); | ||
// this.lastUpdate = stamp; | ||
// this.setDirty(); | ||
} else { | ||
// this.debug("init to default", this._data); | ||
dfd.resolve(); | ||
} | ||
}; | ||
if (this.opts.remote) { | ||
// Try to pull, then resolve | ||
this.pull() | ||
.done(function() { | ||
// self.debug("init from remote", this._data); | ||
self.offline = false; | ||
dfd.resolve(); | ||
}) | ||
.fail(function() { | ||
self.offline = true; | ||
if (prevValue != null) { | ||
console.warn( | ||
self + ": could not init from remote; falling back to storage." | ||
); | ||
// self._data = JSON.parse(prevValue); | ||
self._data = $.extend( | ||
{}, | ||
self.opts.defaults, | ||
JSON.parse(prevValue) | ||
); | ||
} else { | ||
console.warn( | ||
self + ": could not init from remote; falling back default." | ||
); | ||
} | ||
dfd.resolve(); | ||
}); | ||
} else if (prevValue != null) { | ||
this.update(); | ||
// We still extend from opts.defaults, in case some fields where missing | ||
this._data = $.extend({}, this.opts.defaults, this._data); | ||
// this.debug("init from storage", this._data); | ||
dfd.resolve(); | ||
// this.lastUpdate = stamp; | ||
// this.setDirty(); | ||
} else { | ||
// this.debug("init to default", this._data); | ||
dfd.resolve(); | ||
} | ||
}; | ||
window.PersistentObject.prototype = { | ||
/** @type {string} */ | ||
version: "@VERSION", // Set to semver by 'grunt release' | ||
window.PersistentObject.prototype = { | ||
/** @type {string} */ | ||
version: "@VERSION", // Set to semver by 'grunt release' | ||
/* Trigger commit/push according to current settings. */ | ||
_invalidate: function(hint, deferredCall) { | ||
var self = this, | ||
prevChange = this.lastModified, | ||
now = _getNow(), | ||
nextCommit = 0, | ||
nextPush = 0, | ||
nextCheck = 0; | ||
/* Trigger commit/push according to current settings. */ | ||
_invalidate: function(hint, deferredCall) { | ||
var self = this, | ||
prevChange = this.lastModified, | ||
now = Date.now(), | ||
nextCommit = 0, | ||
nextPush = 0, | ||
nextCheck = 0; | ||
if ( this._checkTimer ) { | ||
clearTimeout(this._checkTimer); | ||
this._checkTimer = null; | ||
} | ||
if (this._checkTimer) { | ||
clearTimeout(this._checkTimer); | ||
this._checkTimer = null; | ||
} | ||
if ( !deferredCall ) { | ||
// this.debug("_invalidate(" + hint + ")"); | ||
this.lastModified = now; | ||
if ( !this.uncommittedSince ) { this.uncommittedSince = now; } | ||
if ( !this.unpushedSince ) { this.unpushedSince = now; } | ||
this.opts.change(hint); | ||
} else { | ||
this.debug("_invalidate() recursive"); | ||
} | ||
if (!deferredCall) { | ||
// this.debug("_invalidate(" + hint + ")"); | ||
this.lastModified = now; | ||
if (!this.uncommittedSince) { | ||
this.uncommittedSince = now; | ||
} | ||
if (!this.unpushedSince) { | ||
this.unpushedSince = now; | ||
} | ||
this.opts.change(hint); | ||
} else { | ||
this.debug("_invalidate() recursive"); | ||
} | ||
if ( this.storage ) { | ||
// If we came here by a deferred timer (or delay is 0), commit | ||
// immedialtely | ||
if ( ((now - prevChange) >= this.opts.commitDelay) || | ||
((now - this.uncommittedSince) >= this.opts.maxCommitDelay) ) { | ||
if (this.storage) { | ||
// If we came here by a deferred timer (or delay is 0), commit | ||
// immedialtely | ||
if ( | ||
now - prevChange >= this.opts.commitDelay || | ||
now - this.uncommittedSince >= this.opts.maxCommitDelay | ||
) { | ||
this.debug( | ||
"_invalidate(): force commit", | ||
now - prevChange >= this.opts.commitDelay, | ||
now - this.uncommittedSince >= this.opts.maxCommitDelay | ||
); | ||
this.commit(); | ||
} else { | ||
// otherwise schedule next check | ||
nextCommit = Math.min( | ||
now + this.opts.commitDelay + 1, | ||
this.uncommittedSince + this.opts.maxCommitDelay + 1 | ||
); | ||
} | ||
} | ||
this.debug("_invalidate(): force commit", | ||
((now - prevChange) >= this.opts.commitDelay), | ||
((now - this.uncommittedSince) >= this.opts.maxCommitDelay)); | ||
this.commit(); | ||
} else { | ||
// otherwise schedule next check | ||
nextCommit = Math.min( | ||
now + this.opts.commitDelay + 1, | ||
this.uncommittedSince + this.opts.maxCommitDelay + 1 ); | ||
} | ||
} | ||
if (this.opts.remote) { | ||
if ( | ||
now - prevChange >= this.opts.pushDelay || | ||
now - this.unpushedSince >= this.opts.maxPushDelay | ||
) { | ||
this.debug( | ||
"_invalidate(): force push", | ||
now - prevChange >= this.opts.pushDelay, | ||
now - this.unpushedSince >= this.opts.maxPushDelay | ||
); | ||
this.push(); | ||
} else { | ||
nextPush = Math.min( | ||
now + this.opts.pushDelay + 1, | ||
this.unpushedSince + this.opts.maxPushDelay + 1 | ||
); | ||
} | ||
} | ||
if (nextCommit || nextPush) { | ||
nextCheck = Math.min(nextCommit || MAX_INT, nextPush || MAX_INT); | ||
// this.debug("Defer update:", nextCheck - now) | ||
this.debug( | ||
"_invalidate(" + hint + ") defer by " + (nextCheck - now) + "ms" | ||
); | ||
this._checkTimer = setTimeout(function() { | ||
self._checkTimer = null; // no need to call clearTimeout in the handler... | ||
self._invalidate.call(self, null, true); | ||
}, nextCheck - now); | ||
} | ||
}, | ||
/* Load data from localStorage. */ | ||
_update: function(objData) { | ||
if (this.uncommittedSince) { | ||
console.warn("Updating an uncommitted object."); | ||
if (this.conflict(objData, this._data) === false) { | ||
return; | ||
} | ||
} | ||
this._data = objData; | ||
// this.dirty = false; | ||
this.lastUpdate = Date.now(); | ||
}, | ||
/* Return readable string representation for this instance. */ | ||
toString: function() { | ||
return "PersistentObject('" + this.namespace + "')"; | ||
}, | ||
/* Log to console if opts.debugLevel >= 2 */ | ||
debug: function() { | ||
if (this.opts.debugLevel >= 2) { | ||
Array.prototype.unshift.call(arguments, this.toString()); | ||
console.log.apply(console, arguments); | ||
} | ||
}, | ||
/* Log to console if opts.debugLevel >= 1 */ | ||
log: function() { | ||
if (this.opts.debugLevel >= 1) { | ||
Array.prototype.unshift.call(arguments, this.toString()); | ||
console.log.apply(console, arguments); | ||
} | ||
}, | ||
/** Return true if there are uncommited or unpushed modifications. */ | ||
isDirty: function() { | ||
return !!( | ||
(this.storage && this.uncommittedSince) || | ||
(this.opts.remote && this.unpushedSince) | ||
); | ||
}, | ||
/** Return true if initial pull has completed. */ | ||
isReady: function() { | ||
return this.ready.state !== "pending"; | ||
}, | ||
/** Access object property (`key` supports dot notation). */ | ||
get: function(key) { | ||
var i, | ||
cur = this._data, | ||
parts = ("" + key) // convert to string | ||
.replace(/\[(\w+)\]/g, ".$1") // convert indexes to properties | ||
.replace(/^\./, "") // strip a leading dot | ||
.split("."); | ||
if ( this.opts.remote ) { | ||
if ( ((now - prevChange) >= this.opts.pushDelay) || | ||
((now - this.unpushedSince) >= this.opts.maxPushDelay) ) { | ||
// NOTE: this is slower (tested on Safari): | ||
// return key.split(".").reduce(function(prev, curr) { | ||
// return prev[curr]; | ||
// }, this._data); | ||
this.debug("_invalidate(): force push", ((now - prevChange) >= this.opts.pushDelay), | ||
((now - this.unpushedSince) >= this.opts.maxPushDelay)); | ||
this.push(); | ||
} else { | ||
nextPush = Math.min( | ||
now + this.opts.pushDelay + 1, | ||
this.unpushedSince + this.opts.maxPushDelay + 1 ); | ||
} | ||
} | ||
if ( nextCommit || nextPush ) { | ||
nextCheck = Math.min( | ||
nextCommit || MAX_INT, | ||
nextPush || MAX_INT); | ||
// this.debug("Defer update:", nextCheck - now) | ||
this.debug("_invalidate(" + hint + ") defer by " + (nextCheck - now) + "ms" ); | ||
this._checkTimer = setTimeout(function() { | ||
self._checkTimer = null; // no need to call clearTimeout in the handler... | ||
self._invalidate.call(self, null, true); | ||
}, nextCheck - now); | ||
} | ||
}, | ||
/* Load data from localStorage. */ | ||
_update: function(objData) { | ||
if ( this.uncommittedSince ) { | ||
console.warn("Updating an uncommitted object."); | ||
if ( this.conflict(objData, this._data) === false ) { | ||
return; | ||
} | ||
} | ||
this._data = objData; | ||
// this.dirty = false; | ||
this.lastUpdate = _getNow(); | ||
}, | ||
/* Return readable string representation for this instance. */ | ||
toString: function() { | ||
return "PersistentObject('" + this.namespace + "')"; | ||
}, | ||
/* Log to console if opts.debugLevel >= 2 */ | ||
debug: function() { | ||
if ( this.opts.debugLevel >= 2 ) { | ||
Array.prototype.unshift.call(arguments, this.toString()); | ||
console.log.apply(window.console, arguments); | ||
} | ||
}, | ||
/* Log to console if opts.debugLevel >= 1 */ | ||
log: function() { | ||
if ( this.opts.debugLevel >= 1 ) { | ||
Array.prototype.unshift.call(arguments, this.toString()); | ||
console.log.apply(window.console, arguments); | ||
} | ||
}, | ||
/** Return true if there are uncommited or unpushed modifications. */ | ||
isDirty: function() { | ||
return !!( | ||
(this.storage && this.uncommittedSince) || | ||
(this.opts.remote && this.unpushedSince)); | ||
}, | ||
/** Return true if initial pull has completed. */ | ||
isReady: function() { | ||
return this.ready.state !== "pending"; | ||
}, | ||
/** Access object property (`key` supports dot notation). */ | ||
get: function(key) { | ||
var i, | ||
cur = this._data, | ||
parts = ("" + key) // convert to string | ||
.replace(/\[(\w+)\]/g, ".$1") // convert indexes to properties | ||
.replace(/^\./, "") // strip a leading dot | ||
.split("."); | ||
for (i = 0; i < parts.length; i++) { | ||
cur = cur[parts[i]]; | ||
if (cur === undefined && i < parts.length - 1) { | ||
error( | ||
this + | ||
": Property '" + | ||
key + | ||
"' could not be accessed because parent '" + | ||
parts.slice(0, i + 1).join(".") + | ||
"' does not exist" | ||
); | ||
} | ||
} | ||
return cur; | ||
}, | ||
/* Modify object property and set the `dirty` flag (`key` supports dot notation). */ | ||
_setOrRemove: function(key, value, remove) { | ||
var i, | ||
parent, | ||
cur = this._data, | ||
parts = ("" + key) // convert to string | ||
.replace(/\[(\w+)\]/g, ".$1") // convert indexes to properties | ||
.replace(/^\./, "") // strip a leading dot | ||
.split("."), | ||
lastPart = parts.pop(); | ||
// NOTE: this is slower (tested on Safari): | ||
// return key.split(".").reduce(function(prev, curr) { | ||
// return prev[curr]; | ||
// }, this._data); | ||
for (i = 0; i < parts.length; i++) { | ||
parent = cur; | ||
cur = parent[parts[i]]; | ||
// Create intermediate parent objects properties if required | ||
if (cur === undefined) { | ||
if (this.opts.createParents) { | ||
this.debug("Creating intermediate parent '" + parts[i] + "'"); | ||
cur = parent[parts[i]] = {}; | ||
} else { | ||
error( | ||
this + | ||
": Property '" + | ||
key + | ||
"' could not be set because parent '" + | ||
parts.slice(0, i + 1).join(".") + | ||
"' does not exist" | ||
); | ||
} | ||
} | ||
} | ||
if (cur[lastPart] !== value) { | ||
if (remove === true) { | ||
delete cur[lastPart]; | ||
this._invalidate("remove"); | ||
} else { | ||
cur[lastPart] = value; | ||
this._invalidate("set"); | ||
} | ||
} | ||
}, | ||
/** Modify object property and set the `dirty` flag (`key` supports dot notation). */ | ||
set: function(key, value) { | ||
return this._setOrRemove(key, value, false); | ||
}, | ||
/** Delete object property and set the `dirty` flag (`key` supports dot notation). */ | ||
remove: function(key) { | ||
return this._setOrRemove(key, undefined, true); | ||
}, | ||
/** Replace data object with a new instance. */ | ||
reset: function(obj) { | ||
this._data = obj || {}; | ||
this._invalidate("reset"); | ||
}, | ||
/** Flag object as modified, so that commit / push will be scheduled. */ | ||
setDirty: function(flag) { | ||
if (flag === false) { | ||
} else { | ||
this._invalidate("explicit"); | ||
} | ||
}, | ||
/** Load data from localStorage. */ | ||
update: function() { | ||
if (this.phase) { | ||
error( | ||
this + ": Trying to update while '" + this.phase + "' is pending." | ||
); | ||
} | ||
if (this.opts.debugLevel >= 2 && console.time) { | ||
console.time(this + ".update"); | ||
} | ||
var data = this.storage.getItem(this.namespace); | ||
data = JSON.parse(data); | ||
this._update(data); | ||
if (this.opts.debugLevel >= 2 && console.time) { | ||
console.timeEnd(this + ".update"); | ||
} | ||
}, | ||
/** Write data to localStorage. */ | ||
commit: function() { | ||
var data; | ||
if (this.phase) { | ||
error( | ||
this + ": Trying to commit while '" + this.phase + "' is pending." | ||
); | ||
} | ||
if (this.opts.debugLevel >= 2 && console.time) { | ||
console.time(this + ".commit"); | ||
} | ||
// try { data = JSON.stringify(this._data); } catch(e) { } | ||
data = JSON.stringify(this._data); | ||
this.storage.setItem(this.namespace, data); | ||
// this.dirty = false; | ||
this.uncommittedSince = null; | ||
this.commitCount += 1; | ||
// this.lastCommit = Date.now(); | ||
if (this.opts.debugLevel >= 2 && console.time) { | ||
console.timeEnd(this + ".commit"); | ||
} | ||
return data; | ||
}, | ||
/** Download, then update data from the cloud. */ | ||
pull: function() { | ||
var self = this; | ||
for (i = 0; i < parts.length; i++) { | ||
cur = cur[parts[i]]; | ||
if ( cur === undefined && i < (parts.length - 1) ) { | ||
$.error(this + ": Property '" + key + "' could not be accessed because parent '" + | ||
parts.slice(0, i + 1).join(".") + "' does not exist"); | ||
} | ||
} | ||
return cur; | ||
}, | ||
/* Modify object property and set the `dirty` flag (`key` supports dot notation). */ | ||
_setOrRemove: function(key, value, remove) { | ||
var i, parent, | ||
cur = this._data, | ||
parts = ("" + key) // convert to string | ||
.replace(/\[(\w+)\]/g, ".$1") // convert indexes to properties | ||
.replace(/^\./, "") // strip a leading dot | ||
.split("."), | ||
lastPart = parts.pop(); | ||
if (this.phase) { | ||
error(this + ": Trying to pull while '" + this.phase + "' is pending."); | ||
} | ||
if (this.opts.debugLevel >= 2 && console.time) { | ||
console.time(this + ".pull"); | ||
} | ||
this.phase = "pull"; | ||
// return $.get(this.opts.remote, function(objData) { | ||
return $.ajax({ | ||
type: "GET", | ||
url: this.opts.remote, | ||
}) | ||
.done(function(objData) { | ||
var strData = objData; | ||
if ($.isArray(objData) || $.isPlainObject(objData)) { | ||
strData = JSON.stringify(objData); | ||
} else { | ||
objData = JSON.parse(objData); | ||
} | ||
self.storage.setItem(self.namespace, strData); | ||
self._update(objData); | ||
self.lastPull = Date.now(); | ||
}) | ||
.fail(function() { | ||
self.opts.error(arguments); | ||
}) | ||
.always(function() { | ||
self.phase = null; | ||
if (self.opts.debugLevel >= 2 && console.time) { | ||
console.timeEnd(self + ".pull"); | ||
} | ||
}); | ||
}, | ||
/** Commit, then upload data to the cloud. */ | ||
push: function() { | ||
var self = this, | ||
data = this.commit(); | ||
for (i = 0; i < parts.length; i++) { | ||
parent = cur; | ||
cur = parent[parts[i]]; | ||
// Create intermediate parent objects properties if required | ||
if ( cur === undefined ) { | ||
if ( this.opts.createParents ) { | ||
this.debug("Creating intermediate parent '" + parts[i] + "'"); | ||
cur = parent[parts[i]] = {}; | ||
} else { | ||
$.error(this + ": Property '" + key + "' could not be set because parent '" + | ||
parts.slice(0, i + 1).join(".") + "' does not exist"); | ||
} | ||
} | ||
} | ||
if ( cur[lastPart] !== value ) { | ||
if ( remove === true ) { | ||
delete cur[lastPart]; | ||
this._invalidate("remove"); | ||
} else { | ||
cur[lastPart] = value; | ||
this._invalidate("set"); | ||
} | ||
} | ||
}, | ||
/** Modify object property and set the `dirty` flag (`key` supports dot notation). */ | ||
set: function(key, value) { | ||
return this._setOrRemove(key, value, false); | ||
}, | ||
/** Delete object property and set the `dirty` flag (`key` supports dot notation). */ | ||
remove: function(key) { | ||
return this._setOrRemove(key, undefined, true); | ||
}, | ||
/** Replace data object with a new instance. */ | ||
reset: function(obj) { | ||
this._data = obj || {}; | ||
this._invalidate("reset"); | ||
}, | ||
/** Flag object as modified, so that commit / push will be scheduled. */ | ||
setDirty: function(flag) { | ||
if ( flag === false ) { | ||
} else { | ||
this._invalidate("explicit"); | ||
} | ||
}, | ||
/** Load data from localStorage. */ | ||
update: function() { | ||
if ( this.phase ) { | ||
$.error(this + ": Trying to update while '" + this.phase + "' is pending."); | ||
} | ||
if ( this.opts.debugLevel >= 2 && console.time ) { console.time(this + ".update"); } | ||
var data = this.storage.getItem(this.namespace); | ||
data = JSON.parse(data); | ||
this._update(data); | ||
if ( this.opts.debugLevel >= 2 && console.time ) { console.timeEnd(this + ".update"); } | ||
}, | ||
/** Write data to localStorage. */ | ||
commit: function() { | ||
var data; | ||
if ( this.phase ) { | ||
$.error(this + ": Trying to commit while '" + this.phase + "' is pending."); | ||
} | ||
if ( this.opts.debugLevel >= 2 && console.time ) { console.time(this + ".commit"); } | ||
// try { data = JSON.stringify(this._data); } catch(e) { } | ||
data = JSON.stringify(this._data); | ||
this.storage.setItem(this.namespace, data); | ||
// this.dirty = false; | ||
this.uncommittedSince = null; | ||
this.commitCount += 1; | ||
// this.lastCommit = _getNow(); | ||
if ( this.opts.debugLevel >= 2 && console.time ) { console.timeEnd(this + ".commit"); } | ||
return data; | ||
}, | ||
/** Download, then update data from the cloud. */ | ||
pull: function() { | ||
var self = this; | ||
if (this.phase) { | ||
error(this + ": Trying to push while '" + this.phase + "' is pending."); | ||
} | ||
if (this.opts.debugLevel >= 2 && console.time) { | ||
console.time(self + ".push"); | ||
} | ||
this.phase = "push"; | ||
if (!this.opts.remote) { | ||
error(this + ": Missing remote option"); | ||
} | ||
return $.ajax({ | ||
type: "PUT", | ||
url: this.opts.remote, | ||
data: data, | ||
}) | ||
.done(function() { | ||
// console.log("PUT", arguments); | ||
// self.lastPush = Date.now(); | ||
self.unpushedSince = null; | ||
self.pushCount += 1; | ||
}) | ||
.fail(function() { | ||
self.opts.error(arguments); | ||
}) | ||
.always(function() { | ||
self.phase = null; | ||
if (self.opts.debugLevel >= 2 && console.time) { | ||
console.timeEnd(self + ".push"); | ||
} | ||
}); | ||
}, | ||
/** Read data properties from form input elements with the same name. | ||
* Supports elements of input (type: text, radio, checkbox), textarea, | ||
* and select. | ||
*/ | ||
readFromForm: function(form, options) { | ||
var self = this, | ||
$form = $(form), | ||
opts = $.extend( | ||
{ | ||
addNew: false, | ||
coerce: true, // convert single checkboxes to bool (instead value) | ||
trim: true, | ||
}, | ||
options | ||
); | ||
if ( this.phase ) { | ||
$.error(this + ": Trying to pull while '" + this.phase + "' is pending."); | ||
} | ||
if ( this.opts.debugLevel >= 2 && console.time ) { console.time(this + ".pull"); } | ||
this.phase = "pull"; | ||
// return $.get(this.opts.remote, function(objData) { | ||
return $.ajax({ | ||
type: "GET", | ||
url: this.opts.remote | ||
}).done(function(objData) { | ||
var strData = objData; | ||
if ( $.isArray(objData) || $.isPlainObject(objData) ) { | ||
strData = JSON.stringify(objData); | ||
} else { | ||
objData = JSON.parse(objData); | ||
} | ||
self.storage.setItem(self.namespace, strData); | ||
self._update(objData); | ||
self.lastPull = _getNow(); | ||
}).fail(function() { | ||
self.opts.error(arguments); | ||
}).always(function() { | ||
self.phase = null; | ||
if ( self.opts.debugLevel >= 2 && console.time ) { console.timeEnd(self + ".pull"); } | ||
}); | ||
}, | ||
/** Commit, then upload data to the cloud. */ | ||
push: function() { | ||
var self = this, | ||
data = this.commit(); | ||
if (opts.addNew) { | ||
$form.find("[name]").each(function() { | ||
var name = $(this).attr("name"); | ||
if (self._data[name] === undefined) { | ||
self.debug("readFromForm: add field '" + name + "'"); | ||
self._data[name] = null; | ||
} | ||
}); | ||
} | ||
$.each(this._data, function(k, v) { | ||
var val, | ||
$input = $form.find("[name='" + k + "']"), | ||
type = $input.attr("type"); | ||
if ( this.phase ) { | ||
$.error(this + ": Trying to push while '" + this.phase + "' is pending."); | ||
} | ||
if ( this.opts.debugLevel >= 2 && console.time ) { console.time(self + ".push"); } | ||
this.phase = "push"; | ||
if ( !this.opts.remote ) { $.error(this + ": Missing remote option"); } | ||
return $.ajax({ | ||
type: "PUT", | ||
url: this.opts.remote, | ||
data: data | ||
}).done(function() { | ||
// console.log("PUT", arguments); | ||
// self.lastPush = _getNow(); | ||
self.unpushedSince = null; | ||
self.pushCount += 1; | ||
}).fail(function() { | ||
self.opts.error(arguments); | ||
}).always(function() { | ||
self.phase = null; | ||
if ( self.opts.debugLevel >= 2 && console.time ) { console.timeEnd(self + ".push"); } | ||
}); | ||
}, | ||
/** Read data properties from form input elements with the same name. | ||
* Supports elements of input (type: text, radio, checkbox), textarea, | ||
* and select. | ||
*/ | ||
readFromForm: function(form, options) { | ||
var self = this, | ||
$form = $(form), | ||
opts = $.extend({ | ||
addNew: false, | ||
coerce: true, // convert single checkboxes to bool (instead value) | ||
trim: true | ||
}, options); | ||
if (!$input.length) { | ||
self.debug("readFromForm: field not found: '" + k + "'"); | ||
return; | ||
} | ||
if (type === "radio") { | ||
val = $input.filter(":checked").val(); | ||
} else if (type === "checkbox" && $input.length === 1) { | ||
val = !!$input.filter(":checked").length; | ||
} else if (type === "checkbox" && $input.length > 1) { | ||
val = []; | ||
$input.filter(":checked").each(function() { | ||
val.push($(this).val()); | ||
}); | ||
} else { | ||
val = $input.val(); | ||
if (opts.trim && typeof val === "string") { | ||
val = $.trim(val); | ||
} | ||
} | ||
// console.log("readFromForm: val(" + k + "): '" + val + "'"); | ||
self.set(k, val); | ||
}); | ||
// console.log("readFromForm: '" + this + "'", this._data); | ||
}, | ||
/** Write data to form elements with the same name. | ||
*/ | ||
writeToForm: function(form, options) { | ||
var $form = $(form); | ||
if ( opts.addNew ) { | ||
$form.find("[name]").each(function() { | ||
var name = $(this).attr("name"); | ||
if ( self._data[name] === undefined ) { | ||
self.debug("readFromForm: add field '" + name + "'"); | ||
self._data[name] = null; | ||
} | ||
}); | ||
} | ||
$.each(this._data, function(k, v) { | ||
var val, | ||
$input = $form.find("[name='" + k + "']"), | ||
type = $input.attr("type"); | ||
$.each(this._data, function(k, v) { | ||
var $input = $form.find("[name='" + k + "']"), | ||
type = $input.attr("type"); | ||
if ( !$input.length ) { | ||
self.debug("readFromForm: field not found: '" + k + "'"); | ||
return; | ||
} | ||
if ( type === "radio") { | ||
val = $input.filter(":checked").val(); | ||
} else if ( type === "checkbox" && $input.length === 1 ) { | ||
val = !!$input.filter(":checked").length; | ||
} else if ( type === "checkbox" && $input.length > 1 ) { | ||
val = []; | ||
$input.filter(":checked").each(function() { | ||
val.push($(this).val()); | ||
}); | ||
} else { | ||
val = $input.val(); | ||
if ( opts.trim && typeof val === "string" ) { val = $.trim(val); } | ||
} | ||
// console.log("readFromForm: val(" + k + "): '" + val + "'"); | ||
self.set(k, val); | ||
}); | ||
// console.log("readFromForm: '" + this + "'", this._data); | ||
}, | ||
/** Write data to form elements with the same name. | ||
*/ | ||
writeToForm: function(form, options) { | ||
var $form = $(form); | ||
$.each(this._data, function(k, v) { | ||
var $input = $form.find("[name='" + k + "']"), | ||
type = $input.attr("type"); | ||
if ( $input.length ) { | ||
if ( type === "radio" ) { | ||
$input.filter("[value='" + v + "']").prop("checked", true); | ||
} else if ( type === "checkbox" ) { | ||
if ( $input.length === 1 ) { | ||
$input.prop("checked", !!v); | ||
} else { | ||
// multi-value checkbox | ||
$input.each(function() { | ||
$(this).prop("checked", $.isArray(v) ? | ||
$.inArray(this.value, v) >= 0 : this.value === v); | ||
}); | ||
} | ||
} else if ( $input.is("select") ) { | ||
// listbox | ||
$input.find("option").each(function() { | ||
$(this).prop("selected", $.isArray(v) ? | ||
$.inArray(this.value, v) >= 0 : this.value === v); | ||
}); | ||
} else { | ||
$input.val(v); | ||
} | ||
} | ||
}); | ||
} | ||
}; | ||
// ----------------------------------------------------------------------------- | ||
}(jQuery, window, document)); | ||
if ($input.length) { | ||
if (type === "radio") { | ||
$input.filter("[value='" + v + "']").prop("checked", true); | ||
} else if (type === "checkbox") { | ||
if ($input.length === 1) { | ||
$input.prop("checked", !!v); | ||
} else { | ||
// multi-value checkbox | ||
$input.each(function() { | ||
$(this).prop( | ||
"checked", | ||
$.isArray(v) | ||
? $.inArray(this.value, v) >= 0 | ||
: this.value === v | ||
); | ||
}); | ||
} | ||
} else if ($input.is("select")) { | ||
// listbox | ||
$input.find("option").each(function() { | ||
$(this).prop( | ||
"selected", | ||
$.isArray(v) ? $.inArray(this.value, v) >= 0 : this.value === v | ||
); | ||
}); | ||
} else { | ||
$input.val(v); | ||
} | ||
} | ||
}); | ||
}, | ||
}; | ||
// ----------------------------------------------------------------------------- | ||
// Value returned by `require('persisto')` | ||
return window.PersistentObject; | ||
}); // End of closure |
@@ -72,5 +72,5 @@ ;(function($, window, document, undefined) { | ||
store.remove("bar.qux"); | ||
assert.strictEqual( store.get("bar.qux"), undefined, "reset('bar.qux')" ); | ||
assert.strictEqual( store.get("bar.qux"), undefined, "remove('bar.qux')" ); | ||
store.remove("bar"); | ||
assert.strictEqual( store.get("bar"), undefined, "reset('bar')" ); | ||
assert.strictEqual( store.get("bar"), undefined, "remove('bar')" ); | ||
@@ -77,0 +77,0 @@ store.set("bar.undefined1.test", "testval"); |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Minified code
QualityThis package contains minified code. This may be harmless in some cases where minified code is included in packaged libraries, however packages on npm should not minify code.
Found 1 instance in 1 package
174364
26
1795
425
18
1