439 lines
14 KiB
JavaScript
439 lines
14 KiB
JavaScript
/**
|
|
* 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 && arguments.length > 0) {
|
|
callback = (typeof arguments[0] === 'function') ? arguments[0] : arguments[1];
|
|
options = (typeof arguments[0] === 'function') ? {} : arguments[0];
|
|
} else {
|
|
throw 'Incorrect # of ctor args!'
|
|
}
|
|
// TODO perhaps allow for pub/sub instead?
|
|
if (typeof callback !== 'function') throw 'No callback was provided';
|
|
|
|
// 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) {
|
|
for (var i = 0, l = Lawnchair.adapters.length; i < l; i++) {
|
|
if (Lawnchair.adapters[i].adapter === options.adapter) {
|
|
adapter = Lawnchair.adapters[i].valid() ? Lawnchair.adapters[i] : undefined;
|
|
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(index, store) {
|
|
if (typeof window==='undefined') {
|
|
window = { top: { } }; // node/optimizer compatibility
|
|
}
|
|
|
|
var data = window.top.name ? JSON.parse(window.top.name) : {}
|
|
|
|
return {
|
|
|
|
valid: function () {
|
|
return typeof window.top.name != 'undefined'
|
|
},
|
|
|
|
init: function (options, callback) {
|
|
data[this.name] = data[this.name] || {index:[],store:{}}
|
|
index = data[this.name].index
|
|
store = data[this.name].store
|
|
this.fn(this.name, callback).call(this, this)
|
|
},
|
|
|
|
keys: function (callback) {
|
|
this.fn('keys', callback).call(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
|
|
index.push(key)
|
|
}
|
|
store[key] = obj
|
|
window.top.name = JSON.stringify(data) // TODO wow, this is the only diff from the memory adapter
|
|
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(store[keyOrArray[i]])
|
|
}
|
|
} else {
|
|
r = 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, !!(store[key]))
|
|
return this
|
|
},
|
|
|
|
all: function (cb) {
|
|
var r = []
|
|
for (var i = 0, l = index.length; i < l; i++) {
|
|
var obj = store[index[i]]
|
|
obj.key = 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++) {
|
|
delete store[del[i]]
|
|
index.splice(this.indexOf(index, del[i]), 1)
|
|
}
|
|
window.top.name = JSON.stringify(data)
|
|
if (cb) this.lambda(cb).call(this)
|
|
return this
|
|
},
|
|
|
|
nuke: function (cb) {
|
|
store = {}
|
|
index = []
|
|
window.top.name = JSON.stringify(data)
|
|
if (cb) this.lambda(cb).call(this)
|
|
return this
|
|
}
|
|
}
|
|
/////
|
|
})())
|
|
/**
|
|
* dom storage adapter
|
|
* ===
|
|
* - originally authored by Joseph Pecoraro
|
|
*
|
|
*/
|
|
//
|
|
// TODO does it make sense to be chainable all over the place?
|
|
// chainable: nuke, remove, all, get, save, all
|
|
// not chainable: valid, keys
|
|
//
|
|
Lawnchair.adapter('dom', (function() {
|
|
var storage = window.localStorage
|
|
// the indexer is an encapsulation of the helpers needed to keep an ordered index of the keys
|
|
var indexer = function(name) {
|
|
return {
|
|
// the key
|
|
key: name + '._index_',
|
|
// returns the index
|
|
all: function() {
|
|
var a = storage.getItem(this.key)
|
|
if (a) {
|
|
a = JSON.parse(a)
|
|
}
|
|
if (a === null) storage.setItem(this.key, JSON.stringify([])) // lazy init
|
|
return JSON.parse(storage.getItem(this.key))
|
|
},
|
|
// adds a key to the index
|
|
add: function (key) {
|
|
var a = this.all()
|
|
a.push(key)
|
|
storage.setItem(this.key, JSON.stringify(a))
|
|
},
|
|
// deletes a key from the index
|
|
del: function (key) {
|
|
var a = this.all(), r = []
|
|
// FIXME this is crazy inefficient but I'm in a strata meeting and half concentrating
|
|
for (var i = 0, l = a.length; i < l; i++) {
|
|
if (a[i] != key) r.push(a[i])
|
|
}
|
|
storage.setItem(this.key, JSON.stringify(r))
|
|
},
|
|
// returns index for a key
|
|
find: function (key) {
|
|
var a = this.all()
|
|
for (var i = 0, l = a.length; i < l; i++) {
|
|
if (key === a[i]) return i
|
|
}
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
// adapter api
|
|
return {
|
|
|
|
// ensure we are in an env with localStorage
|
|
valid: function () {
|
|
return !!storage && function() {
|
|
// in mobile safari if safe browsing is enabled, window.storage
|
|
// is defined but setItem calls throw exceptions.
|
|
var success = true
|
|
var value = Math.random()
|
|
try {
|
|
storage.setItem(value, value)
|
|
} catch (e) {
|
|
success = false
|
|
}
|
|
storage.removeItem(value)
|
|
return success
|
|
}()
|
|
},
|
|
|
|
init: function (options, callback) {
|
|
this.indexer = indexer(this.name)
|
|
if (callback) this.fn(this.name, callback).call(this, this)
|
|
},
|
|
|
|
save: function (obj, callback) {
|
|
var key = obj.key ? this.name + '.' + obj.key : this.name + '.' + this.uuid()
|
|
// if the key is not in the index push it on
|
|
if (this.indexer.find(key) === false) this.indexer.add(key)
|
|
// now we kil the key and use it in the store colleciton
|
|
delete obj.key;
|
|
storage.setItem(key, JSON.stringify(obj))
|
|
obj.key = key.slice(this.name.length + 1)
|
|
if (callback) {
|
|
this.lambda(callback).call(this, obj)
|
|
}
|
|
return this
|
|
},
|
|
|
|
batch: function (ary, callback) {
|
|
var saved = []
|
|
// not particularily efficient but this is more for sqlite situations
|
|
for (var i = 0, l = ary.length; i < l; i++) {
|
|
this.save(ary[i], function(r){
|
|
saved.push(r)
|
|
})
|
|
}
|
|
if (callback) this.lambda(callback).call(this, saved)
|
|
return this
|
|
},
|
|
|
|
// accepts [options], callback
|
|
keys: function(callback) {
|
|
if (callback) {
|
|
var name = this.name
|
|
, keys = this.indexer.all().map(function(r){ return r.replace(name + '.', '') })
|
|
this.fn('keys', callback).call(this, keys)
|
|
}
|
|
return this // TODO options for limit/offset, return promise
|
|
},
|
|
|
|
get: function (key, callback) {
|
|
if (this.isArray(key)) {
|
|
var r = []
|
|
for (var i = 0, l = key.length; i < l; i++) {
|
|
var k = this.name + '.' + key[i]
|
|
var obj = storage.getItem(k)
|
|
if (obj) {
|
|
obj = JSON.parse(obj)
|
|
obj.key = key[i]
|
|
r.push(obj)
|
|
}
|
|
}
|
|
if (callback) this.lambda(callback).call(this, r)
|
|
} else {
|
|
var k = this.name + '.' + key
|
|
var obj = storage.getItem(k)
|
|
if (obj) {
|
|
obj = JSON.parse(obj)
|
|
obj.key = key
|
|
}
|
|
if (callback) this.lambda(callback).call(this, obj)
|
|
}
|
|
return this
|
|
},
|
|
|
|
exists: function (key, cb) {
|
|
var exists = this.indexer.find(this.name+'.'+key) === false ? false : true ;
|
|
this.lambda(cb).call(this, exists);
|
|
return this;
|
|
},
|
|
// NOTE adapters cannot set this.__results but plugins do
|
|
// this probably should be reviewed
|
|
all: function (callback) {
|
|
var idx = this.indexer.all()
|
|
, r = []
|
|
, o
|
|
, k
|
|
for (var i = 0, l = idx.length; i < l; i++) {
|
|
k = idx[i] //v
|
|
o = JSON.parse(storage.getItem(k))
|
|
o.key = k.replace(this.name + '.', '')
|
|
r.push(o)
|
|
}
|
|
if (callback) this.fn(this.name, callback).call(this, r)
|
|
return this
|
|
},
|
|
|
|
remove: function (keyOrObj, callback) {
|
|
var key = this.name + '.' + ((keyOrObj.key) ? keyOrObj.key : keyOrObj)
|
|
this.indexer.del(key)
|
|
storage.removeItem(key)
|
|
if (callback) this.lambda(callback).call(this)
|
|
return this
|
|
},
|
|
|
|
nuke: function (callback) {
|
|
this.all(function(r) {
|
|
for (var i = 0, l = r.length; i < l; i++) {
|
|
this.remove(r[i]);
|
|
}
|
|
if (callback) this.lambda(callback).call(this)
|
|
})
|
|
return this
|
|
}
|
|
}})());
|