mail/src/lib/lawnchair/lawnchair-git.js

294 lines
9.5 KiB
JavaScript
Raw Normal View History

/**
* 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
}
}
/////
})())