// // Copyright (c) 2008 Paul Duncan (paul@pablotron.org) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // /** * Persist - top-level namespace for Persist library. */ Persist = (function() { var VERSION = '0.1.0', P, B, esc, init, empty, ec; // easycookie 0.2.1 (pre-minified) // (see http://pablotron.org/software/easy_cookie/) ec = (function(){var EPOCH='Thu, 01-Jan-1970 00:00:01 GMT',RATIO=1000*60*60*24,KEYS=['expires','path','domain'],esc=escape,un=unescape,doc=document,me;var get_now=function(){var r=new Date();r.setTime(r.getTime());return r;} var cookify=function(c_key,c_val){var i,key,val,r=[],opt=(arguments.length>2)?arguments[2]:{};r.push(esc(c_key)+'='+esc(c_val));for(i=0;i2)?arguments[2]:{},now=get_now(),expire_at,cfg={};if(opt.expires){opt.expires*=RATIO;cfg.expires=new Date(now.getTime()+opt.expires);cfg.expires=cfg.expires.toGMTString();} var keys=['path','domain','secure'];for(i=0;i | ; , db.open(esc(this.name)); // create table db.execute(C.sql.create).close(); }, get: function(key, fn, scope) { var r, sql = C.sql.get; // if callback isn't defined, then return if (!fn) return; // begin transaction this.transaction(function (t) { // exec query r = t.execute(sql, [key]); // call callback if (r.isValidRow()) fn.call(scope || this, true, r.field(0)); else fn.call(scope || this, false, null); // close result set r.close(); }); }, set: function(key, val, fn, scope) { var rm_sql = C.sql.remove, sql = C.sql.set, r; // begin set transaction this.transaction(function(t) { // exec remove query t.execute(rm_sql, [key]).close(); // exec set query t.execute(sql, [key, val]).close(); // run callback if (fn) fn.call(scope || this, true, val); }); }, // begin remove transaction remove: function(key, fn, scope) { var get_sql = C.sql.get; sql = C.sql.remove, r, val; this.transaction(function(t) { // if a callback was defined, then get the old // value before removing it if (fn) { // exec get query r = t.execute(get_sql, [key]); if (r.isValidRow()) { // key exists, get value val = r.field(0); // exec remove query t.execute(sql, [key]).close(); // exec callback fn.call(scope || this, true, val); } else { // key does not exist, exec callback fn.call(scope || this, false, null); } // close result set r.close(); } else { // no callback was defined, so just remove the // data without checking the old value // exec remove query t.execute(sql, [key]).close(); } }); } } }, // whatwg db backend (webkit, Safari 3.1+) // (src: whatwg and http://webkit.org/misc/DatabaseExample.html) whatwg_db: { size: 200 * 1024, test: function() { var name = 'PersistJS Test', desc = 'Persistent database test.'; // test for openDatabase if (!window.openDatabase) return false; // make sure openDatabase works // XXX: will this leak a db handle and/or waste space? if (!window.openDatabase(name, C.sql.version, desc, B.whatwg_db.size)) return false; return true; }, methods: { transaction: function(fn) { if (!this.db_created) { var sql = C.sql.create; this.db.transaction(function(t) { // create table t.executeSql(sql, [], function() { this.db_created = true; }); }, empty); // trap exception } this.db.transaction(fn); }, init: function() { var desc, size; // init description and size desc = this.o.about || "Persistent storage for " + this.name; size = this.o.size || B.whatwg_db.size; // create database handle this.db = openDatabase(this.name, C.sql.version, desc, size); }, get: function(key, fn, scope) { var sql = C.sql.get; // if callback isn't defined, then return if (!fn) return; // get callback scope scope = scope || this; // begin transaction this.transaction(function (t) { t.executeSql(sql, [key], function(t, r) { if (r.rows.length > 0) fn.call(scope, true, r.rows.item(0)['v']); else fn.call(scope, false, null); }); }); }, set: function(key, val, fn, scope) { var rm_sql = C.sql.remove, sql = C.sql.set; // begin set transaction this.transaction(function(t) { // exec remove query t.executeSql(rm_sql, [key], function() { // exec set query t.executeSql(sql, [key, val], function(t, r) { // run callback if (fn) fn.call(scope || this, true, val); }); }); }); return val; }, // begin remove transaction remove: function(key, fn, scope) { var get_sql = C.sql.get; sql = C.sql.remove; this.transaction(function(t) { // if a callback was defined, then get the old // value before removing it if (fn) { // exec get query t.executeSql(get_sql, [key], function(t, r) { if (r.rows.length > 0) { // key exists, get value var val = r.rows.item(0)['v']; // exec remove query t.executeSql(sql, [key], function(t, r) { // exec callback fn.call(scope || this, true, val); }); } else { // key does not exist, exec callback fn.call(scope || this, false, null); } }); } else { // no callback was defined, so just remove the // data without checking the old value // exec remove query t.executeSql(sql, [key]); } }); } } }, // globalstorage backend (globalStorage, FF2+, IE8+) // (src: http://developer.mozilla.org/en/docs/DOM:Storage#globalStorage) // // TODO: test to see if IE8 uses object literal semantics or // getItem/setItem/removeItem semantics globalstorage: { // (5 meg limit, src: http://ejohn.org/blog/dom-storage-answers/) size: 5 * 1024 * 1024, test: function() { return window.globalStorage ? true : false; }, methods: { key: function(key) { return esc(this.name) + esc(key); }, init: function() { this.store = globalStorage[this.o.domain]; }, get: function(key, fn, scope) { // expand key key = this.key(key); if (fn) fn.call(scope || this, true, this.store.getItem(key)); }, set: function(key, val, fn, scope) { // expand key key = this.key(key); // set value this.store.setItem(key, val); if (fn) fn.call(scope || this, true, val); }, remove: function(key, fn, scope) { var val; // expand key key = this.key(key); // get value val = this.store[key]; // delete value this.store.removeItem(key); if (fn) fn.call(scope || this, (val !== null), val); } } }, // localstorage backend (globalStorage, FF2+, IE8+) // (src: http://www.whatwg.org/specs/web-apps/current-work/#the-localstorage) localstorage: { // (unknown?) size: -1, test: function() { return window.localStorage ? true : false; }, methods: { key: function(key) { return esc(this.name) + esc(key); }, init: function() { this.store = localStorage; }, get: function(key, fn, scope) { // expand key key = this.key(key); if (fn) fn.call(scope || this, true, this.store.getItem(key)); }, set: function(key, val, fn, scope) { // expand key key = this.key(key); // set value this.store.setItem(key, val); if (fn) fn.call(scope || this, true, val); }, remove: function(key, fn, scope) { var val; // expand key key = this.key(key); // get value val = this.getItem(key); // delete value this.store.removeItem(key); if (fn) fn.call(scope || this, (val !== null), val); } } }, // IE backend ie: { prefix: '_persist_data-', // style: 'display:none; behavior:url(#default#userdata);', // 64k limit size: 64 * 1024, test: function() { // make sure we're dealing with IE // (src: http://javariet.dk/shared/browser_dom.htm) return window.ActiveXObject ? true : false; }, make_userdata: function(id) { var el = document.createElement('div'); // set element properties el.id = id; el.style.display = 'none'; el.addBehavior('#default#userData'); // append element to body document.body.appendChild(el); // return element return el; }, methods: { init: function() { var id = B.ie.prefix + esc(this.name); // save element this.el = B.ie.make_userdata(id); // load data if (this.o.defer) this.load(); }, get: function(key, fn, scope) { var val; // expand key key = esc(key); // load data if (!this.o.defer) this.load(); // get value val = this.el.getAttribute(key); // call fn if (fn) fn.call(scope || this, val ? true : false, val); }, set: function(key, val, fn, scope) { // expand key key = esc(key); // set attribute this.el.setAttribute(key, val); // save data if (!this.o.defer) this.save(); // call fn if (fn) fn.call(scope || this, true, val); }, load: function() { this.el.load(esc(this.name)); }, save: function() { this.el.save(esc(this.name)); } } }, // cookie backend // uses easycookie: http://pablotron.org/software/easy_cookie/ cookie: { delim: ':', // 4k limit (low-ball this limit to handle browser weirdness, and // so we don't hose session cookies) size: 4000, test: function() { // XXX: use easycookie to test if cookies are enabled return P.Cookie.enabled ? true : false; }, methods: { key: function(key) { return this.name + B.cookie.delim + key; }, get: function(key, val, fn, scope) { // expand key key = this.key(key); // get value val = ec.get(key); // call fn if (fn) fn.call(scope || this, val != null, val); }, set: function(key, val, fn, scope) { // expand key key = this.key(key); // save value ec.set(key, val, this.o); // call fn if (fn) fn.call(scope || this, true, val); }, remove: function(key, val, fn, scope) { var val; // expand key key = this.key(key); // remove cookie val = ec.remove(key) // call fn if (fn) fn.call(scope || this, val != null, val); } } }, // flash backend (requires flash 8 or newer) // http://kb.adobe.com/selfservice/viewContent.do?externalId=tn_16194&sliceId=1 // http://livedocs.adobe.com/flash/8/main/wwhelp/wwhimpl/common/html/wwhelp.htm?context=LiveDocs_Parts&file=00002200.html flash: { test: function() { // TODO: better flash detection if (!window.SWFObject || !deconcept || !deconcept.SWFObjectUtil) return false; // get the major version var major = deconcept.SWFObjectUtil.getPlayerVersion().major; // check flash version (require 8.0 or newer) return (major >= 8) ? true : false; }, methods: { init: function() { if (!B.flash.el) { var o, key, el, cfg = C.flash; // create wrapper element el = document.createElement('div'); el.id = cfg.div_id; // FIXME: hide flash element // el.style.display = 'none'; // append element to body document.body.appendChild(el); // create new swf object o = new SWFObject(this.o.swf_path || cfg.path, cfg.id, cfg.size.w, cfg.size.h, '8'); // set parameters for (key in cfg.args) o.addVariable(key, cfg.args[key]); // write flash object o.write(el); // save flash element B.flash.el = document.getElementById(cfg.id); } // use singleton flash element this.el = B.flash.el; }, get: function(key, fn, scope) { var val; // escape key key = esc(key); // get value val = this.el.get(this.name, key); // call handler if (fn) fn.call(scope || this, val !== null, val); }, set: function(key, val, fn, scope) { var old_val; // escape key key = esc(key); // set value old_val = this.el.set(this.name, key, val); // call handler if (fn) fn.call(scope || this, true, val); }, remove: function(key, fn, scope) { var val; // get key key = esc(key); // remove old value val = this.el.remove(this.name, key); // call handler if (fn) fn.call(scope || this, true, val); } } } }; // init function var init = function() { var i, l, b, key, fns = C.methods, keys = C.search_order; // set all functions to the empty function for (i = 0, l = fns.length; i < l; i++) P.Store.prototype[fns[i]] = empty; // clear type and size P.type = null; P.size = -1; // loop over all backends and test for each one for (i = 0, l = keys.length; !P.type && i < l; i++) { b = B[keys[i]]; // test for backend if (b.test()) { // found backend, save type and size P.type = keys[i]; P.size = b.size; // extend store prototype with backend methods for (key in b.methods) P.Store.prototype[key] = b.methods[key]; } } // mark library as initialized P._init = true; }; // create top-level namespace P = { // version of persist library VERSION: VERSION, // backend type and size limit type: null, size: 0, // expose init function // init: init, add: function(o) { // add to backend hash B[o.id] = o; // add backend to front of search order C.search_order = [o.id].concat(C.search_order); // re-initialize library init(); }, remove: function(id) { var ofs = C.search_order.indexOf(id); if (ofs < 0) return; // remove from search order C.search_order.splice(ofs, 1); // delete from lut delete B[id]; // re-initialize library init(); }, // expose easycookie API Cookie: ec, // store API Store: function(name, o) { // verify name if (!C.name_re.exec(name)) throw new Error("Invalid name"); // XXX: should we lazy-load type? // if (!P._init) // init(); if (!P.type) throw new Error("No suitable storage found"); o = o || {}; this.name = name; // get domain (XXX: does this localdomain fix work?) o.domain = o.domain || location.hostname || 'localhost.localdomain'; this.o = o; // expires in 2 years o.expires = o.expires || 365 * 2; // set path to root o.path = o.path || '/'; // call init function this.init(); } }; // init persist init(); // return top-level namespace return P; })();