(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);