/** * Lawnchair! * --- * clientside json store * */ var Lawnchair = function (options, callback) { // ensure Lawnchair was called as a constructor if (!(this instanceof Lawnchair)) return new Lawnchair(options, callback); // lawnchair requires json if (!JSON) throw 'JSON unavailable! Include http://www.json.org/json2.js to fix.' // options are optional; callback is not if (arguments.length <= 2) { callback = (typeof arguments[0] === 'function') ? arguments[0] : arguments[1]; options = (typeof arguments[0] === 'function') ? {} : arguments[0] || {}; } else { throw 'Incorrect # of ctor args!' } // default configuration this.record = options.record || 'record' // default for records this.name = options.name || 'records' // default name for underlying store // mixin first valid adapter var adapter // if the adapter is passed in we try to load that only if (options.adapter) { // the argument passed should be an array of prefered adapters // if it is not, we convert it if(typeof(options.adapter) === 'string'){ options.adapter = [options.adapter]; } // iterates over the array of passed adapters for(var j = 0, k = options.adapter.length; j < k; j++){ // itirates over the array of available adapters for (var i = Lawnchair.adapters.length-1; i >= 0; i--) { if (Lawnchair.adapters[i].adapter === options.adapter[j]) { adapter = Lawnchair.adapters[i].valid() ? Lawnchair.adapters[i] : undefined; if (adapter) break } } if (adapter) break } // otherwise find the first valid adapter for this env } else { for (var i = 0, l = Lawnchair.adapters.length; i < l; i++) { adapter = Lawnchair.adapters[i].valid() ? Lawnchair.adapters[i] : undefined if (adapter) break } } // we have failed if (!adapter) throw 'No valid adapter.' // yay! mixin the adapter for (var j in adapter) this[j] = adapter[j] // call init for each mixed in plugin for (var i = 0, l = Lawnchair.plugins.length; i < l; i++) Lawnchair.plugins[i].call(this) // init the adapter this.init(options, callback) } Lawnchair.adapters = [] /** * queues an adapter for mixin * === * - ensures an adapter conforms to a specific interface * */ Lawnchair.adapter = function (id, obj) { // add the adapter id to the adapter obj // ugly here for a cleaner dsl for implementing adapters obj['adapter'] = id // methods required to implement a lawnchair adapter var implementing = 'adapter valid init keys save batch get exists all remove nuke'.split(' ') , indexOf = this.prototype.indexOf // mix in the adapter for (var i in obj) { if (indexOf(implementing, i) === -1) throw 'Invalid adapter! Nonstandard method: ' + i } // if we made it this far the adapter interface is valid // insert the new adapter as the preferred adapter Lawnchair.adapters.splice(0,0,obj) } Lawnchair.plugins = [] /** * generic shallow extension for plugins * === * - if an init method is found it registers it to be called when the lawnchair is inited * - yes we could use hasOwnProp but nobody here is an asshole */ Lawnchair.plugin = function (obj) { for (var i in obj) i === 'init' ? Lawnchair.plugins.push(obj[i]) : this.prototype[i] = obj[i] } /** * helpers * */ Lawnchair.prototype = { isArray: Array.isArray || function(o) { return Object.prototype.toString.call(o) === '[object Array]' }, /** * this code exists for ie8... for more background see: * http://www.flickr.com/photos/westcoastlogic/5955365742/in/photostream */ indexOf: function(ary, item, i, l) { if (ary.indexOf) return ary.indexOf(item) for (i = 0, l = ary.length; i < l; i++) if (ary[i] === item) return i return -1 }, // awesome shorthand callbacks as strings. this is shameless theft from dojo. lambda: function (callback) { return this.fn(this.record, callback) }, // first stab at named parameters for terse callbacks; dojo: first != best // ;D fn: function (name, callback) { return typeof callback == 'string' ? new Function(name, callback) : callback }, // returns a unique identifier (by way of Backbone.localStorage.js) // TODO investigate smaller UUIDs to cut on storage cost uuid: function () { var S4 = function () { return (((1+Math.random())*0x10000)|0).toString(16).substring(1); } return (S4()+S4()+"-"+S4()+"-"+S4()+"-"+S4()+"-"+S4()+S4()+S4()); }, // a classic iterator each: function (callback) { var cb = this.lambda(callback) // iterate from chain if (this.__results) { for (var i = 0, l = this.__results.length; i < l; i++) cb.call(this, this.__results[i], i) } // otherwise iterate the entire collection else { this.all(function(r) { for (var i = 0, l = r.length; i < l; i++) cb.call(this, r[i], i) }) } return this } // -- }; // window.name code courtesy Remy Sharp: http://24ways.org/2009/breaking-out-the-edges-of-the-browser Lawnchair.adapter('window-name', (function() { if (typeof window==='undefined') { window = { top: { } }; // node/optimizer compatibility } // edited from the original here by elsigh // Some sites store JSON data in window.top.name, but some folks (twitter on iPad) // put simple strings in there - we should make sure not to cause a SyntaxError. var data = {} try { data = JSON.parse(window.top.name) } catch (e) {} return { valid: function () { return typeof window.top.name != 'undefined' }, init: function (options, callback) { data[this.name] = data[this.name] || {index:[],store:{}} this.index = data[this.name].index this.store = data[this.name].store this.fn(this.name, callback).call(this, this) return this }, keys: function (callback) { this.fn('keys', callback).call(this, this.index) return this }, save: function (obj, cb) { // data[key] = value + ''; // force to string // window.top.name = JSON.stringify(data); var key = obj.key || this.uuid() this.exists(key, function(exists) { if (!exists) { if (obj.key) delete obj.key this.index.push(key) } this.store[key] = obj try { window.top.name = JSON.stringify(data) // TODO wow, this is the only diff from the memory adapter } catch(e) { // restore index/store to previous value before JSON exception if (!exists) { this.index.pop(); delete this.store[key]; } throw e; } if (cb) { obj.key = key this.lambda(cb).call(this, obj) } }) return this }, batch: function (objs, cb) { var r = [] for (var i = 0, l = objs.length; i < l; i++) { this.save(objs[i], function(record) { r.push(record) }) } if (cb) this.lambda(cb).call(this, r) return this }, get: function (keyOrArray, cb) { var r; if (this.isArray(keyOrArray)) { r = [] for (var i = 0, l = keyOrArray.length; i < l; i++) { r.push(this.store[keyOrArray[i]]) } } else { r = this.store[keyOrArray] if (r) r.key = keyOrArray } if (cb) this.lambda(cb).call(this, r) return this }, exists: function (key, cb) { this.lambda(cb).call(this, !!(this.store[key])) return this }, all: function (cb) { var r = [] for (var i = 0, l = this.index.length; i < l; i++) { var obj = this.store[this.index[i]] obj.key = this.index[i] r.push(obj) } this.fn(this.name, cb).call(this, r) return this }, remove: function (keyOrArray, cb) { var del = this.isArray(keyOrArray) ? keyOrArray : [keyOrArray] for (var i = 0, l = del.length; i < l; i++) { var key = del[i].key ? del[i].key : del[i] var where = this.indexOf(this.index, key) if (where < 0) continue /* key not present */ delete this.store[key] this.index.splice(where, 1) } window.top.name = JSON.stringify(data) if (cb) this.lambda(cb).call(this) return this }, nuke: function (cb) { this.store = data[this.name].store = {} this.index = data[this.name].index = [] window.top.name = JSON.stringify(data) if (cb) this.lambda(cb).call(this) return this } } ///// })())