215 lines
6.0 KiB
JavaScript
215 lines
6.0 KiB
JavaScript
|
(function() {
|
||
|
// Public sightglass interface.
|
||
|
function sightglass(obj, keypath, callback, options) {
|
||
|
return new Observer(obj, keypath, callback, options)
|
||
|
}
|
||
|
|
||
|
// Batteries not included.
|
||
|
sightglass.adapters = {}
|
||
|
|
||
|
// Constructs a new keypath observer and kicks things off.
|
||
|
function Observer(obj, keypath, callback, options) {
|
||
|
this.options = options || {}
|
||
|
this.options.adapters = this.options.adapters || {}
|
||
|
this.obj = obj
|
||
|
this.keypath = keypath
|
||
|
this.callback = callback
|
||
|
this.objectPath = []
|
||
|
this.update = this.update.bind(this)
|
||
|
this.parse()
|
||
|
|
||
|
if (isObject(this.target = this.realize())) {
|
||
|
this.set(true, this.key, this.target, this.callback)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Tokenizes the provided keypath string into interface + path tokens for the
|
||
|
// observer to work with.
|
||
|
Observer.tokenize = function(keypath, interfaces, root) {
|
||
|
var tokens = []
|
||
|
var current = {i: root, path: ''}
|
||
|
var index, chr
|
||
|
|
||
|
for (index = 0; index < keypath.length; index++) {
|
||
|
chr = keypath.charAt(index)
|
||
|
|
||
|
if (!!~interfaces.indexOf(chr)) {
|
||
|
tokens.push(current)
|
||
|
current = {i: chr, path: ''}
|
||
|
} else {
|
||
|
current.path += chr
|
||
|
}
|
||
|
}
|
||
|
|
||
|
tokens.push(current)
|
||
|
return tokens
|
||
|
}
|
||
|
|
||
|
// Parses the keypath using the interfaces defined on the view. Sets variables
|
||
|
// for the tokenized keypath as well as the end key.
|
||
|
Observer.prototype.parse = function() {
|
||
|
var interfaces = this.interfaces()
|
||
|
var root, path
|
||
|
|
||
|
if (!interfaces.length) {
|
||
|
error('Must define at least one adapter interface.')
|
||
|
}
|
||
|
|
||
|
if (!!~interfaces.indexOf(this.keypath[0])) {
|
||
|
root = this.keypath[0]
|
||
|
path = this.keypath.substr(1)
|
||
|
} else {
|
||
|
if (typeof (root = this.options.root || sightglass.root) === 'undefined') {
|
||
|
error('Must define a default root adapter.')
|
||
|
}
|
||
|
|
||
|
path = this.keypath
|
||
|
}
|
||
|
|
||
|
this.tokens = Observer.tokenize(path, interfaces, root)
|
||
|
this.key = this.tokens.pop()
|
||
|
}
|
||
|
|
||
|
// Realizes the full keypath, attaching observers for every key and correcting
|
||
|
// old observers to any changed objects in the keypath.
|
||
|
Observer.prototype.realize = function() {
|
||
|
var current = this.obj
|
||
|
var unreached = false
|
||
|
var prev
|
||
|
|
||
|
this.tokens.forEach(function(token, index) {
|
||
|
if (isObject(current)) {
|
||
|
if (typeof this.objectPath[index] !== 'undefined') {
|
||
|
if (current !== (prev = this.objectPath[index])) {
|
||
|
this.set(false, token, prev, this.update)
|
||
|
this.set(true, token, current, this.update)
|
||
|
this.objectPath[index] = current
|
||
|
}
|
||
|
} else {
|
||
|
this.set(true, token, current, this.update)
|
||
|
this.objectPath[index] = current
|
||
|
}
|
||
|
|
||
|
current = this.get(token, current)
|
||
|
} else {
|
||
|
if (unreached === false) {
|
||
|
unreached = index
|
||
|
}
|
||
|
|
||
|
if (prev = this.objectPath[index]) {
|
||
|
this.set(false, token, prev, this.update)
|
||
|
}
|
||
|
}
|
||
|
}, this)
|
||
|
|
||
|
if (unreached !== false) {
|
||
|
this.objectPath.splice(unreached)
|
||
|
}
|
||
|
|
||
|
return current
|
||
|
}
|
||
|
|
||
|
// Updates the keypath. This is called when any intermediary key is changed.
|
||
|
Observer.prototype.update = function() {
|
||
|
var next, oldValue
|
||
|
|
||
|
if ((next = this.realize()) !== this.target) {
|
||
|
if (isObject(this.target)) {
|
||
|
this.set(false, this.key, this.target, this.callback)
|
||
|
}
|
||
|
|
||
|
if (isObject(next)) {
|
||
|
this.set(true, this.key, next, this.callback)
|
||
|
}
|
||
|
|
||
|
oldValue = this.value()
|
||
|
this.target = next
|
||
|
|
||
|
// Always call callback if value is a function. If not a function, call callback only if value changed
|
||
|
if (this.value() instanceof Function || this.value() !== oldValue) this.callback()
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Reads the current end value of the observed keypath. Returns undefined if
|
||
|
// the full keypath is unreachable.
|
||
|
Observer.prototype.value = function() {
|
||
|
if (isObject(this.target)) {
|
||
|
return this.get(this.key, this.target)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Sets the current end value of the observed keypath. Calling setValue when
|
||
|
// the full keypath is unreachable is a no-op.
|
||
|
Observer.prototype.setValue = function(value) {
|
||
|
if (isObject(this.target)) {
|
||
|
this.adapter(this.key).set(this.target, this.key.path, value)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Gets the provided key on an object.
|
||
|
Observer.prototype.get = function(key, obj) {
|
||
|
return this.adapter(key).get(obj, key.path)
|
||
|
}
|
||
|
|
||
|
// Observes or unobserves a callback on the object using the provided key.
|
||
|
Observer.prototype.set = function(active, key, obj, callback) {
|
||
|
var action = active ? 'observe' : 'unobserve'
|
||
|
this.adapter(key)[action](obj, key.path, callback)
|
||
|
}
|
||
|
|
||
|
// Returns an array of all unique adapter interfaces available.
|
||
|
Observer.prototype.interfaces = function() {
|
||
|
var interfaces = Object.keys(this.options.adapters)
|
||
|
|
||
|
Object.keys(sightglass.adapters).forEach(function(i) {
|
||
|
if (!~interfaces.indexOf(i)) {
|
||
|
interfaces.push(i)
|
||
|
}
|
||
|
})
|
||
|
|
||
|
return interfaces
|
||
|
}
|
||
|
|
||
|
// Convenience function to grab the adapter for a specific key.
|
||
|
Observer.prototype.adapter = function(key) {
|
||
|
return this.options.adapters[key.i] ||
|
||
|
sightglass.adapters[key.i]
|
||
|
}
|
||
|
|
||
|
// Unobserves the entire keypath.
|
||
|
Observer.prototype.unobserve = function() {
|
||
|
var obj
|
||
|
|
||
|
this.tokens.forEach(function(token, index) {
|
||
|
if (obj = this.objectPath[index]) {
|
||
|
this.set(false, token, obj, this.update)
|
||
|
}
|
||
|
}, this)
|
||
|
|
||
|
if (isObject(this.target)) {
|
||
|
this.set(false, this.key, this.target, this.callback)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Check if a value is an object than can be observed.
|
||
|
function isObject(obj) {
|
||
|
return typeof obj === 'object' && obj !== null
|
||
|
}
|
||
|
|
||
|
// Error thrower.
|
||
|
function error(message) {
|
||
|
throw new Error('[sightglass] ' + message)
|
||
|
}
|
||
|
|
||
|
// Export module for Node and the browser.
|
||
|
if (typeof module !== 'undefined' && module.exports) {
|
||
|
module.exports = sightglass
|
||
|
} else if (typeof define === 'function' && define.amd) {
|
||
|
define([], function() {
|
||
|
return this.sightglass = sightglass
|
||
|
})
|
||
|
} else {
|
||
|
this.sightglass = sightglass
|
||
|
}
|
||
|
}).call(this);
|