/*! * jStore - Persistent Client-Side Storage * * Copyright (c) 2009 Eric Garside (http://eric.garside.name) * * Dual licensed under: * MIT: http://www.opensource.org/licenses/mit-license.php * GPLv3: http://www.opensource.org/licenses/gpl-3.0.html */ /*! * jQuery JSON Plugin * version: 1.0 (2008-04-17) * * This document is licensed as free software under the terms of the * MIT License: http://www.opensource.org/licenses/mit-license.php * * Brantley Harris technically wrote this plugin, but it is based somewhat * on the JSON.org website's http://www.json.org/json2.js, which proclaims: * "NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.", a sentiment that * I uphold. I really just cleaned it up. * * It is also based heavily on MochiKit's serializeJSON, which is * copywrited 2005 by Bob Ippolito. */ (function($) { function toIntegersAtLease(n) // Format integers to have at least two digits. { return n < 10 ? '0' + n : n; } Date.prototype.toJSON = function(date) // Yes, it polutes the Date namespace, but we'll allow it here, as // it's damned usefull. { return this.getUTCFullYear() + '-' + toIntegersAtLease(this.getUTCMonth()) + '-' + toIntegersAtLease(this.getUTCDate()); }; var escapeable = /["\\\x00-\x1f\x7f-\x9f]/g; var meta = { // table of character substitutions '\b': '\\b', '\t': '\\t', '\n': '\\n', '\f': '\\f', '\r': '\\r', '"' : '\\"', '\\': '\\\\' }; $.quoteString = function(string) // Places quotes around a string, inteligently. // If the string contains no control characters, no quote characters, and no // backslash characters, then we can safely slap some quotes around it. // Otherwise we must also replace the offending characters with safe escape // sequences. { if (escapeable.test(string)) { return '"' + string.replace(escapeable, function (a) { var c = meta[a]; if (typeof c === 'string') { return c; } c = a.charCodeAt(); return '\\u00' + Math.floor(c / 16).toString(16) + (c % 16).toString(16); }) + '"'; } return '"' + string + '"'; }; $.toJSON = function(o, compact) { var type = typeof(o); if (type == "undefined") return "undefined"; else if (type == "number" || type == "boolean") return o + ""; else if (o === null) return "null"; // Is it a string? if (type == "string") { return $.quoteString(o); } // Does it have a .toJSON function? if (type == "object" && typeof o.toJSON == "function") return o.toJSON(compact); // Is it an array? if (type != "function" && typeof(o.length) == "number") { var ret = []; for (var i = 0; i < o.length; i++) { ret.push( $.toJSON(o[i], compact) ); } if (compact) return "[" + ret.join(",") + "]"; else return "[" + ret.join(", ") + "]"; } // If it's a function, we have to warn somebody! if (type == "function") { throw new TypeError("Unable to convert object of type 'function' to json."); } // It's probably an object, then. var ret = []; for (var k in o) { var name; type = typeof(k); if (type == "number") name = '"' + k + '"'; else if (type == "string") name = $.quoteString(k); else continue; //skip non-string or number keys var val = $.toJSON(o[k], compact); if (typeof(val) != "string") { // skip non-serializable values continue; } if (compact) ret.push(name + ":" + val); else ret.push(name + ": " + val); } return "{" + ret.join(", ") + "}"; }; $.compactJSON = function(o) { return $.toJSON(o, true); }; $.evalJSON = function(src) // Evals JSON that we know to be safe. { try { return eval("(" + src + ")"); } catch(e) { return src; } }; $.secureEvalJSON = function(src) // Evals JSON in a way that is *more* secure. { var filtered = src; filtered = filtered.replace(/\\["\\\/bfnrtu]/g, '@'); filtered = filtered.replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']'); filtered = filtered.replace(/(?:^|:|,)(?:\s*\[)+/g, ''); if (/^[\],:{}\s]*$/.test(filtered)) return eval("(" + src + ")"); else throw new SyntaxError("Error parsing JSON, source is not valid."); }; })(jQuery); /** * Javascript Class Framework * * Copyright (c) 2008 John Resig (http://ejohn.org/blog/simple-javascript-inheritance/) * Inspired by base2 and Prototype */ (function(){ var initializing = false, fnTest = /xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/; // The base Class implementation (does nothing) this.Class = function(){}; // Create a new Class that inherits from this class Class.extend = function(prop) { var _super = this.prototype; // Instantiate a base class (but only create the instance, // don't run the init constructor) initializing = true; var prototype = new this(); initializing = false; // Copy the properties over onto the new prototype for (var name in prop) { // Check if we're overwriting an existing function prototype[name] = typeof prop[name] == "function" && typeof _super[name] == "function" && fnTest.test(prop[name]) ? (function(name, fn){ return function() { var tmp = this._super; // Add a new ._super() method that is the same method // but on the super-class this._super = _super[name]; // The method only need to be bound temporarily, so we // remove it when we're done executing var ret = fn.apply(this, arguments); this._super = tmp; return ret; }; })(name, prop[name]) : prop[name]; } // The dummy class constructor function Class() { // All construction is actually done in the init method if ( !initializing && this.init ) this.init.apply(this, arguments); } // Populate our constructed prototype object Class.prototype = prototype; // Enforce the constructor to be what we expect Class.constructor = Class; // And make this class extendable Class.extend = arguments.callee; return Class; }; })(); /*! * jStore Delegate Framework * Copyright (c) 2009 Eric Garside (http://eric.garside.name) */ (function($){ this.jStoreDelegate = Class.extend({ init: function(parent){ // The Object this delgate operates for this.parent = parent; // Container for callbacks to dispatch. // eventType => [ callback, callback, ... ] this.callbacks = {}; }, bind: function(event, callback){ if ( !$.isFunction(callback) ) return this; if ( !this.callbacks[ event ] ) this.callbacks[ event ] = []; this.callbacks[ event ].push(callback); return this; }, trigger: function(){ var parent = this.parent, args = [].slice.call(arguments), event = args.shift(), handlers = this.callbacks[ event ]; if ( !handlers ) return false; $.each(handlers, function(){ this.apply(parent, args) }); return this; } }); })(jQuery); /** * jStore-jQuery Interface * Copyright (c) 2009 Eric Garside (http://eric.garside.name) */ (function($){ var rxJson; try { rxJson = new RegExp('^("(\\\\.|[^"\\\\\\n\\r])*?"|[,:{}\\[\\]0-9.\\-+Eaeflnr-u \\n\\r\\t])+?$') } catch (e) { rxJson = /^(true|false|null|\[.*\]|\{.*\}|".*"|\d+|\d+\.\d+)$/ } // Setup the jStore namespace in jQuery for options storage $.jStore = {}; // Seed the object $.extend($.jStore, { EngineOrder: [], // Engines should put their availability tests within jStore.Availability Availability: {}, // Defined engines should enter themselves into the jStore.Engines Engines: {}, // Instanciated engines should exist within jStore.Instances Instances: {}, // The current engine to use for storage CurrentEngine: null, // Provide global settings for overwriting defaults: { project: null, engine: null, autoload: true, flash: 'jStore.Flash.html' }, // Boolean for ready state handling isReady: false, // Boolean for flash ready state handling isFlashReady: false, // An event delegate delegate: new jStoreDelegate($.jStore) .bind('jStore-ready', function(engine){ $.jStore.isReady = true; if ($.jStore.defaults.autoload) engine.connect(); }) .bind('flash-ready', function(){ $.jStore.isFlashReady = true; }), // Enable ready callback for jStore ready: function(callback){ if ($.jStore.isReady) callback.apply($.jStore, [$.jStore.CurrentEngine]); else $.jStore.delegate.bind('jStore-ready', callback); }, // Enable failure callback registration for jStore fail: function(callback){ $.jStore.delegate.bind('jStore-failure', callback); }, // Enable ready callback for Flash flashReady: function(callback){ if ($.jStore.isFlashReady) callback.apply($.jStore, [$.jStore.CurrentEngine]); else $.jStore.delegate.bind('flash-ready', callback); }, // Enable and test an engine use: function(engine, project, identifier){ project = project || $.jStore.defaults.project || location.hostname.replace(/\./g, '-') || 'unknown'; var e = $.jStore.Engines[engine.toLowerCase()] || null, name = (identifier ? identifier + '.' : '') + project + '.' + engine; if ( !e ) throw 'JSTORE_ENGINE_UNDEFINED'; // Instanciate the engine e = new e(project, name); // Prevent against naming conflicts if ($.jStore.Instances[name]) throw 'JSTORE_JRI_CONFLICT'; // Test the engine if (e.isAvailable()){ $.jStore.Instances[name] = e; // The Easy Way if (!$.jStore.CurrentEngine){ $.jStore.CurrentEngine = e; } $.jStore.delegate.trigger('jStore-ready', e); } else { if (!e.autoload) // Not available throw 'JSTORE_ENGINE_UNAVILABLE'; else { // The hard way e.included(function(){ if (this.isAvailable()) { // Worked out $.jStore.Instances[name] = this; // If there is no current engine, use this one if (!$.jStore.CurrentEngine){ $.jStore.CurrentEngine = this; } $.jStore.delegate.trigger('jStore-ready', this); } else $.jStore.delegate.trigger('jStore-failure', this); }).include(); } } }, // Set the current storage engine setCurrentEngine: function(name){ if (!$.jStore.Instances.length ) // If no instances exist, attempt to load one return $.jStore.FindEngine(); if (!name && $.jStore.Instances.length >= 1) { // If no name is specified, use the first engine $.jStore.delegate.trigger('jStore-ready', $.jStore.Instances[0]); return $.jStore.CurrentEngine = $.jStore.Instances[0]; } if (name && $.jStore.Instances[name]) { // If a name is specified and exists, use it $.jStore.delegate.trigger('jStore-ready', $.jStore.Instances[name]); return $.jStore.CurrentEngine = $.jStore.Instances[name]; } throw 'JSTORE_JRI_NO_MATCH'; }, // Test all possible engines for straightforward useability FindEngine: function(){ $.each($.jStore.EngineOrder, function(k){ if ($.jStore.Availability[this]()){ // Find the first, easiest option and use it. $.jStore.use(this, $.jStore.defaults.project, 'default'); return false; } }) }, // Provide a way for users to call for auto-loading load: function(){ if ($.jStore.defaults.engine) return $.jStore.use($.jStore.defaults.engine, $.jStore.defaults.project, 'default'); // Attempt to find a valid engine, and catch any exceptions if we can't try { $.jStore.FindEngine(); } catch (e) {} }, // Parse a value as JSON before its stored. safeStore: function(value){ switch (typeof value){ case 'object': case 'function': return $.compactJSON(value); case 'number': case 'boolean': case 'string': case 'xml': return value; case 'undefined': default: return ''; } }, // Restores JSON'd values before returning safeResurrect: function(value){ return rxJson.test(value) ? $.evalJSON(value) : value; }, // Provide a simple interface for storing/getting values store: function(key, value){ if (!$.jStore.CurrentEngine) return false; if ( !value ) // Executing a get command return $.jStore.CurrentEngine.get(key); // Executing a set command return $.jStore.CurrentEngine.set(key, value); }, // Provide a simple interface for removing values remove: function(key){ if (!$.jStore.CurrentEngine) return false; return $.jStore.CurrentEngine.rem(key); }, // Alias access for reading get: function(key){ return $.jStore.store(key); }, // Alias access for setting set: function(key, value){ return $.jStore.store(key, value); } }) // Extend the jQuery funcitonal object $.extend($.fn, { // Provide a chainable interface for storing values/getting a value at the end of a chain store: function(key, value){ if (!$.jStore.CurrentEngine) return this; var result = $.jStore.store(key, value); return !value ? result : this; }, // Provide a chainable interface for removing values removeStore: function(key){ $.jStore.remove(key); return this; }, // Alias access for reading at the end of a chain. getStore: function(key){ return $.jStore.store(key); }, // Alias access for setting on a chanin. setStore: function(key, value){ $.jStore.store(key, value); return this; } }) })(jQuery); /** * jStore Engine Core * Copyright (c) 2009 Eric Garside (http://eric.garside.name) */ (function($){ this.StorageEngine = Class.extend({ init: function(project, name){ // Configure the project name this.project = project; // The JRI name given by the manager this.jri = name; // Cache the data so we can work synchronously this.data = {}; // The maximum limit of the storage engine this.limit = -1; // Third party script includes this.includes = []; // Create an event delegate for users to subscribe to event triggers this.delegate = new jStoreDelegate(this) .bind('engine-ready', function(){ this.isReady = true; }) .bind('engine-included', function(){ this.hasIncluded = true; }); // If enabled, the manager will check availability, then run include(), then check again this.autoload = false; // This should be changed by the engines, if they have required includes // When set, we're ready to transact data this.isReady = false; // When the includer is finished, it will set this to true this.hasIncluded = false; }, // Performs all necessary script includes include: function(){ var self = this, total = this.includes.length, count = 0; $.each(this.includes, function(){ $.ajax({type: 'get', url: this, dataType: 'script', cache: true, success: function(){ count++; if (count == total) self.delegate.trigger('engine-included'); } }) }); }, // This should be overloaded with an actual functionality presence check isAvailable: function(){ return false; }, // All get/set/rem functions across the engines should add this to the // first line of those functions to prevent accessing the engine while unstable. interruptAccess: function(){ if (!this.isReady) throw 'JSTORE_ENGINE_NOT_READY'; }, /** Event Subscription Shortcuts **/ ready: function(callback){ if (this.isReady) callback.apply(this); else this.delegate.bind('engine-ready', callback); return this; }, included: function(callback){ if (this.hasIncluded) callback.apply(this); else this.delegate.bind('engine-included', callback); return this; }, /** Cache Data Access **/ get: function(key){ this.interruptAccess(); return this.data[key] || null; }, set: function(key, value){ this.interruptAccess(); this.data[key] = value; return value; }, rem: function(key){ this.interruptAccess(); var beforeDelete = this.data[key]; this.data[key] = null; return beforeDelete; } }); })(jQuery); /*! * jStore DOM Storage Engine * Copyright (c) 2009 Eric Garside (http://eric.garside.name) */ (function($){ // Set up a static test function for this instance var sessionAvailability = $.jStore.Availability.session = function(){ return !!window.sessionStorage; }, localAvailability = $.jStore.Availability.local = function(){ return !!(window.localStorage || window.globalStorage); }; this.jStoreDom = StorageEngine.extend({ init: function(project, name){ // Call the parental init object this._super(project, name); // The type of storage engine this.type = 'DOM'; // Set the Database limit this.limit = 5 * 1024 * 1024; }, connect: function(){ // Fire our delegate to indicate we're ready for data transactions this.delegate.trigger('engine-ready'); }, get: function(key){ this.interruptAccess(); var out = this.db.getItem(key); // Gecko's getItem returns {value: 'the value'}, WebKit returns 'the value' return $.jStore.safeResurrect( (out && out.value ? out.value : out) ); }, set: function(key, value){ this.interruptAccess(); this.db.setItem(key,$.jStore.safeStore(value)); return value; }, rem: function(key){ this.interruptAccess(); var out = this.get(key); this.db.removeItem(key); return out } }) this.jStoreLocal = jStoreDom.extend({ connect: function(){ // Gecko uses a non-standard globalStorage[ www.example.com ] DOM access object for persistant storage. this.db = !window.globalStorage ? window.localStorage : window.globalStorage[location.hostname]; this._super(); }, isAvailable: localAvailability }) this.jStoreSession = jStoreDom.extend({ connect: function(){ this.db = sessionStorage; this._super(); }, isAvailable: sessionAvailability }) $.jStore.Engines.local = jStoreLocal; $.jStore.Engines.session = jStoreSession; // Store the ordering preference $.jStore.EngineOrder[ 1 ] = 'local'; })(jQuery); /*! * jStore Flash Storage Engine * Copyright (c) 2009 Eric Garside (http://eric.garside.name) * jStore.swf Copyright (c) 2008 Daniel Bulli (http://www.nuff-respec.com) */ (function($){ // Set up a static test function for this instance var avilability = $.jStore.Availability.flash = function(){ return !!($.jStore.hasFlash('8.0.0')); } this.jStoreFlash = StorageEngine.extend({ init: function(project, name){ // Call the parental init object this._super(project, name); // The type of storage engine this.type = 'Flash'; // Bind our flashReady function to the jStore Delegate var self = this; $.jStore.flashReady(function(){ self.flashReady() }); }, connect: function(){ var name = 'jstore-flash-embed-' + this.project; // To make Flash Storage work on IE, we have to load up an iFrame // which contains an HTML page that embeds the object using an // object tag wrapping an embed tag. Of course, this is unnecessary for // all browsers except for IE, which, to my knowledge, is the only browser // in existance where you need to complicate your code to fix bugs. Goddamnit. :( $(document.body) .append(''); }, flashReady: function(e){ var iFrame = $('#jStoreFlashFrame')[0]; // IE if (iFrame.Document && $.isFunction(iFrame.Document['jStoreFlash'].f_get_cookie)) this.db = iFrame.Document['jStoreFlash']; // Safari && Firefox else if (iFrame.contentWindow && iFrame.contentWindow.document){ var doc = iFrame.contentWindow.document; // Safari if ($.isFunction($('object', $(doc))[0].f_get_cookie)) this.db = $('object', $(doc))[0]; // Firefox else if ($.isFunction($('embed', $(doc))[0].f_get_cookie)) this.db = $('embed', $(doc))[0]; } // We're ready to process data if (this.db) this.delegate.trigger('engine-ready'); }, isAvailable: avilability, get: function(key){ this.interruptAccess(); var out = this.db.f_get_cookie(key); return out == 'null' ? null : $.jStore.safeResurrect(out); }, set: function(key, value){ this.interruptAccess(); this.db.f_set_cookie(key, $.jStore.safeStore(value)); return value; }, rem: function(key){ this.interruptAccess(); var beforeDelete = this.get(key); this.db.f_delete_cookie(key); return beforeDelete; } }) $.jStore.Engines.flash = jStoreFlash; // Store the ordering preference $.jStore.EngineOrder[ 2 ] = 'flash'; /** * Flash Detection functions copied from the jQuery Flash Plugin * Copyright (c) 2006 Luke Lutman (http://jquery.lukelutman.com/plugins/flash) * Dual licensed under the MIT and GPL licenses. * http://www.opensource.org/licenses/mit-license.php * http://www.opensource.org/licenses/gpl-license.php */ $.jStore.hasFlash = function(version){ var pv = $.jStore.flashVersion().match(/\d+/g), rv = version.match(/\d+/g); for(var i = 0; i < 3; i++) { pv[i] = parseInt(pv[i] || 0); rv[i] = parseInt(rv[i] || 0); // player is less than required if(pv[i] < rv[i]) return false; // player is greater than required if(pv[i] > rv[i]) return true; } // major version, minor version and revision match exactly return true; } $.jStore.flashVersion = function(){ // ie try { try { // avoid fp6 minor version lookup issues // see: http://blog.deconcept.com/2006/01/11/getvariable-setvariable-crash-internet-explorer-flash-6/ var axo = new ActiveXObject('ShockwaveFlash.ShockwaveFlash.6'); try { axo.AllowScriptAccess = 'always'; } catch(e) { return '6,0,0'; } } catch(e) { return new ActiveXObject('ShockwaveFlash.ShockwaveFlash').GetVariable('$version').replace(/\D+/g, ',').match(/^,?(.+),?$/)[1]; } // other browsers } catch(e) { try { if(navigator.mimeTypes["application/x-shockwave-flash"].enabledPlugin){ return (navigator.plugins["Shockwave Flash 2.0"] || navigator.plugins["Shockwave Flash"]).description.replace(/\D+/g, ",").match(/^,?(.+),?$/)[1]; } } catch(e) {} } return '0,0,0'; } })(jQuery); // Callback fired when ExternalInterface is established function flash_ready(){ $.jStore.delegate.trigger('flash-ready'); } /*! * jStore Google Gears Storage Engine * Copyright (c) 2009 Eric Garside (http://eric.garside.name) */ (function($){ // Set up a static test function for this instance var avilability = $.jStore.Availability.gears = function(){ return !!(window.google && window.google.gears) } this.jStoreGears = StorageEngine.extend({ init: function(project, name){ // Call the parental init object this._super(project, name); // The type of storage engine this.type = 'Google Gears'; // Add required third-party scripts this.includes.push('http://code.google.com/apis/gears/gears_init.js'); // Allow Autoloading on fail this.autoload = true; }, connect: function(){ // Create our database connection var db = this.db = google.gears.factory.create('beta.database'); db.open( 'jstore-' + this.project ); db.execute( 'CREATE TABLE IF NOT EXISTS jstore (k TEXT UNIQUE NOT NULL PRIMARY KEY, v TEXT NOT NULL)' ); // Cache the data from the table this.updateCache(); }, updateCache: function(){ // Read the database into our cache object var result = this.db.execute( 'SELECT k,v FROM jstore' ); while (result.isValidRow()){ this.data[result.field(0)] = $.jStore.safeResurrect( result.field(1) ); result.next(); } result.close(); // Fire our delegate to indicate we're ready for data transactions this.delegate.trigger('engine-ready'); }, isAvailable: avilability, set: function(key, value){ this.interruptAccess(); // Update the database var db = this.db; db.execute( 'BEGIN' ); db.execute( 'INSERT OR REPLACE INTO jstore(k, v) VALUES (?, ?)', [key,$.jStore.safeStore(value)] ); db.execute( 'COMMIT' ); return this._super(key, value); }, rem: function(key){ this.interruptAccess(); // Update the database var db = this.db; db.execute( 'BEGIN' ); db.execute( 'DELETE FROM jstore WHERE k = ?', [key] ); db.execute( 'COMMIT' ); return this._super(key); } }) $.jStore.Engines.gears = jStoreGears; // Store the ordering preference $.jStore.EngineOrder[ 3 ] = 'gears'; })(jQuery); /*! * jStore HTML5 Specification Storage Engine * Copyright (c) 2009 Eric Garside (http://eric.garside.name) */ (function($){ // Set up a static test function for this instance var avilability = $.jStore.Availability.html5 = function(){ return !!window.openDatabase } this.jStoreHtml5 = StorageEngine.extend({ init: function(project, name){ // Call the parental init object this._super(project, name); // The type of storage engine this.type = 'HTML5'; // Set the Database limit this.limit = 1024 * 200; }, connect: function(){ // Create our database connection var db = this.db = openDatabase('jstore-' + this.project, '1.0', this.project, this.limit); if (!db) throw 'JSTORE_ENGINE_HTML5_NODB'; db.transaction(function(db){ db.executeSql( 'CREATE TABLE IF NOT EXISTS jstore (k TEXT UNIQUE NOT NULL PRIMARY KEY, v TEXT NOT NULL)' ); }); // Cache the data from the table this.updateCache(); }, updateCache: function(){ var self = this; // Read the database into our cache object this.db.transaction(function(db){ db.executeSql( 'SELECT k,v FROM jstore', [], function(db, result){ var rows = result.rows, i = 0, row; for (; i < rows.length; ++i){ row = rows.item(i); self.data[row.k] = $.jStore.safeResurrect( row.v ); } // Fire our delegate to indicate we're ready for data transactions self.delegate.trigger('engine-ready'); }); }); }, isAvailable: avilability, set: function(key, value){ this.interruptAccess(); // Update the database this.db.transaction(function(db){ db.executeSql( 'INSERT OR REPLACE INTO jstore(k, v) VALUES (?, ?)', [key,$.jStore.safeStore(value)]); }); return this._super(key, value); }, rem: function(key){ this.interruptAccess(); // Update the database this.db.transaction(function(db){ db.executeSql( 'DELETE FROM jstore WHERE k = ?', [key] ) }) return this._super(key); } }) $.jStore.Engines.html5 = jStoreHtml5; // Store the ordering preference $.jStore.EngineOrder[ 0 ] = 'html5'; })(jQuery); /*!* * jStore IE Storage Engine * Copyright (c) 2009 Eric Garside (http://eric.garside.name) */ (function($){ // Set up a static test function for this instance var avilability = $.jStore.Availability.ie = function(){ return !!window.ActiveXObject; } this.jStoreIE = StorageEngine.extend({ init: function(project, name){ // Call the parental init object this._super(project, name); // The type of storage engine this.type = 'IE'; // Allow Autoloading on fail this.limit = 64 * 1024; }, connect: function(){ // Create a hidden div to store attributes in this.db = $('') .appendTo(document.body).get(0); // Fire our delegate to indicate we're ready for data transactions this.delegate.trigger('engine-ready'); }, isAvailable: avilability, get: function(key){ this.interruptAccess(); this.db.load(this.project); return $.jStore.safeResurrect( this.db.getAttribute(key) ); }, set: function(key, value){ this.interruptAccess(); this.db.setAttribute(key, $.jStore.safeStore(value)); this.db.save(this.project); return value; }, rem: function(key){ this.interruptAccess(); var beforeDelete = this.get(key); this.db.removeAttribute(key); this.db.save(this.project); return beforeDelete; } }) $.jStore.Engines.ie = jStoreIE; // Store the ordering preference $.jStore.EngineOrder[ 4 ] = 'ie'; })(jQuery);