Index: runtime/bin/vmservice/observatory/deployed/web/packages/template_binding/js/observe.js |
diff --git a/runtime/bin/vmservice/observatory/deployed/web/packages/template_binding/js/observe.js b/runtime/bin/vmservice/observatory/deployed/web/packages/template_binding/js/observe.js |
new file mode 100644 |
index 0000000000000000000000000000000000000000..6cd2ccb4b10ab4d301b9a551da71704081d1a636 |
--- /dev/null |
+++ b/runtime/bin/vmservice/observatory/deployed/web/packages/template_binding/js/observe.js |
@@ -0,0 +1,1711 @@ |
+/* |
+ * Copyright (c) 2014 The Polymer Project Authors. All rights reserved. |
+ * This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt |
+ * The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt |
+ * The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt |
+ * Code distributed by Google as part of the polymer project is also |
+ * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt |
+ */ |
+ |
+(function(global) { |
+ 'use strict'; |
+ |
+ var testingExposeCycleCount = global.testingExposeCycleCount; |
+ |
+ // Detect and do basic sanity checking on Object/Array.observe. |
+ function detectObjectObserve() { |
+ if (typeof Object.observe !== 'function' || |
+ typeof Array.observe !== 'function') { |
+ return false; |
+ } |
+ |
+ var records = []; |
+ |
+ function callback(recs) { |
+ records = recs; |
+ } |
+ |
+ var test = {}; |
+ var arr = []; |
+ Object.observe(test, callback); |
+ Array.observe(arr, callback); |
+ test.id = 1; |
+ test.id = 2; |
+ delete test.id; |
+ arr.push(1, 2); |
+ arr.length = 0; |
+ |
+ Object.deliverChangeRecords(callback); |
+ if (records.length !== 5) |
+ return false; |
+ |
+ if (records[0].type != 'add' || |
+ records[1].type != 'update' || |
+ records[2].type != 'delete' || |
+ records[3].type != 'splice' || |
+ records[4].type != 'splice') { |
+ return false; |
+ } |
+ |
+ Object.unobserve(test, callback); |
+ Array.unobserve(arr, callback); |
+ |
+ return true; |
+ } |
+ |
+ var hasObserve = detectObjectObserve(); |
+ |
+ function detectEval() { |
+ // Don't test for eval if we're running in a Chrome App environment. |
+ // We check for APIs set that only exist in a Chrome App context. |
+ if (typeof chrome !== 'undefined' && chrome.app && chrome.app.runtime) { |
+ return false; |
+ } |
+ |
+ // Firefox OS Apps do not allow eval. This feature detection is very hacky |
+ // but even if some other platform adds support for this function this code |
+ // will continue to work. |
+ if (typeof navigator != 'undefined' && navigator.getDeviceStorage) { |
+ return false; |
+ } |
+ |
+ try { |
+ var f = new Function('', 'return true;'); |
+ return f(); |
+ } catch (ex) { |
+ return false; |
+ } |
+ } |
+ |
+ var hasEval = detectEval(); |
+ |
+ function isIndex(s) { |
+ return +s === s >>> 0 && s !== ''; |
+ } |
+ |
+ function toNumber(s) { |
+ return +s; |
+ } |
+ |
+ function isObject(obj) { |
+ return obj === Object(obj); |
+ } |
+ |
+ var numberIsNaN = global.Number.isNaN || function(value) { |
+ return typeof value === 'number' && global.isNaN(value); |
+ } |
+ |
+ function areSameValue(left, right) { |
+ if (left === right) |
+ return left !== 0 || 1 / left === 1 / right; |
+ if (numberIsNaN(left) && numberIsNaN(right)) |
+ return true; |
+ |
+ return left !== left && right !== right; |
+ } |
+ |
+ var createObject = ('__proto__' in {}) ? |
+ function(obj) { return obj; } : |
+ function(obj) { |
+ var proto = obj.__proto__; |
+ if (!proto) |
+ return obj; |
+ var newObject = Object.create(proto); |
+ Object.getOwnPropertyNames(obj).forEach(function(name) { |
+ Object.defineProperty(newObject, name, |
+ Object.getOwnPropertyDescriptor(obj, name)); |
+ }); |
+ return newObject; |
+ }; |
+ |
+ var identStart = '[\$_a-zA-Z]'; |
+ var identPart = '[\$_a-zA-Z0-9]'; |
+ var identRegExp = new RegExp('^' + identStart + '+' + identPart + '*' + '$'); |
+ |
+ function getPathCharType(char) { |
+ if (char === undefined) |
+ return 'eof'; |
+ |
+ var code = char.charCodeAt(0); |
+ |
+ switch(code) { |
+ case 0x5B: // [ |
+ case 0x5D: // ] |
+ case 0x2E: // . |
+ case 0x22: // " |
+ case 0x27: // ' |
+ case 0x30: // 0 |
+ return char; |
+ |
+ case 0x5F: // _ |
+ case 0x24: // $ |
+ return 'ident'; |
+ |
+ case 0x20: // Space |
+ case 0x09: // Tab |
+ case 0x0A: // Newline |
+ case 0x0D: // Return |
+ case 0xA0: // No-break space |
+ case 0xFEFF: // Byte Order Mark |
+ case 0x2028: // Line Separator |
+ case 0x2029: // Paragraph Separator |
+ return 'ws'; |
+ } |
+ |
+ // a-z, A-Z |
+ if ((0x61 <= code && code <= 0x7A) || (0x41 <= code && code <= 0x5A)) |
+ return 'ident'; |
+ |
+ // 1-9 |
+ if (0x31 <= code && code <= 0x39) |
+ return 'number'; |
+ |
+ return 'else'; |
+ } |
+ |
+ var pathStateMachine = { |
+ 'beforePath': { |
+ 'ws': ['beforePath'], |
+ 'ident': ['inIdent', 'append'], |
+ '[': ['beforeElement'], |
+ 'eof': ['afterPath'] |
+ }, |
+ |
+ 'inPath': { |
+ 'ws': ['inPath'], |
+ '.': ['beforeIdent'], |
+ '[': ['beforeElement'], |
+ 'eof': ['afterPath'] |
+ }, |
+ |
+ 'beforeIdent': { |
+ 'ws': ['beforeIdent'], |
+ 'ident': ['inIdent', 'append'] |
+ }, |
+ |
+ 'inIdent': { |
+ 'ident': ['inIdent', 'append'], |
+ '0': ['inIdent', 'append'], |
+ 'number': ['inIdent', 'append'], |
+ 'ws': ['inPath', 'push'], |
+ '.': ['beforeIdent', 'push'], |
+ '[': ['beforeElement', 'push'], |
+ 'eof': ['afterPath', 'push'] |
+ }, |
+ |
+ 'beforeElement': { |
+ 'ws': ['beforeElement'], |
+ '0': ['afterZero', 'append'], |
+ 'number': ['inIndex', 'append'], |
+ "'": ['inSingleQuote', 'append', ''], |
+ '"': ['inDoubleQuote', 'append', ''] |
+ }, |
+ |
+ 'afterZero': { |
+ 'ws': ['afterElement', 'push'], |
+ ']': ['inPath', 'push'] |
+ }, |
+ |
+ 'inIndex': { |
+ '0': ['inIndex', 'append'], |
+ 'number': ['inIndex', 'append'], |
+ 'ws': ['afterElement'], |
+ ']': ['inPath', 'push'] |
+ }, |
+ |
+ 'inSingleQuote': { |
+ "'": ['afterElement'], |
+ 'eof': ['error'], |
+ 'else': ['inSingleQuote', 'append'] |
+ }, |
+ |
+ 'inDoubleQuote': { |
+ '"': ['afterElement'], |
+ 'eof': ['error'], |
+ 'else': ['inDoubleQuote', 'append'] |
+ }, |
+ |
+ 'afterElement': { |
+ 'ws': ['afterElement'], |
+ ']': ['inPath', 'push'] |
+ } |
+ } |
+ |
+ function noop() {} |
+ |
+ function parsePath(path) { |
+ var keys = []; |
+ var index = -1; |
+ var c, newChar, key, type, transition, action, typeMap, mode = 'beforePath'; |
+ |
+ var actions = { |
+ push: function() { |
+ if (key === undefined) |
+ return; |
+ |
+ keys.push(key); |
+ key = undefined; |
+ }, |
+ |
+ append: function() { |
+ if (key === undefined) |
+ key = newChar |
+ else |
+ key += newChar; |
+ } |
+ }; |
+ |
+ function maybeUnescapeQuote() { |
+ if (index >= path.length) |
+ return; |
+ |
+ var nextChar = path[index + 1]; |
+ if ((mode == 'inSingleQuote' && nextChar == "'") || |
+ (mode == 'inDoubleQuote' && nextChar == '"')) { |
+ index++; |
+ newChar = nextChar; |
+ actions.append(); |
+ return true; |
+ } |
+ } |
+ |
+ while (mode) { |
+ index++; |
+ c = path[index]; |
+ |
+ if (c == '\\' && maybeUnescapeQuote(mode)) |
+ continue; |
+ |
+ type = getPathCharType(c); |
+ typeMap = pathStateMachine[mode]; |
+ transition = typeMap[type] || typeMap['else'] || 'error'; |
+ |
+ if (transition == 'error') |
+ return; // parse error; |
+ |
+ mode = transition[0]; |
+ action = actions[transition[1]] || noop; |
+ newChar = transition[2] === undefined ? c : transition[2]; |
+ action(); |
+ |
+ if (mode === 'afterPath') { |
+ return keys; |
+ } |
+ } |
+ |
+ return; // parse error |
+ } |
+ |
+ function isIdent(s) { |
+ return identRegExp.test(s); |
+ } |
+ |
+ var constructorIsPrivate = {}; |
+ |
+ function Path(parts, privateToken) { |
+ if (privateToken !== constructorIsPrivate) |
+ throw Error('Use Path.get to retrieve path objects'); |
+ |
+ for (var i = 0; i < parts.length; i++) { |
+ this.push(String(parts[i])); |
+ } |
+ |
+ if (hasEval && this.length) { |
+ this.getValueFrom = this.compiledGetValueFromFn(); |
+ } |
+ } |
+ |
+ // TODO(rafaelw): Make simple LRU cache |
+ var pathCache = {}; |
+ |
+ function getPath(pathString) { |
+ if (pathString instanceof Path) |
+ return pathString; |
+ |
+ if (pathString == null || pathString.length == 0) |
+ pathString = ''; |
+ |
+ if (typeof pathString != 'string') { |
+ if (isIndex(pathString.length)) { |
+ // Constructed with array-like (pre-parsed) keys |
+ return new Path(pathString, constructorIsPrivate); |
+ } |
+ |
+ pathString = String(pathString); |
+ } |
+ |
+ var path = pathCache[pathString]; |
+ if (path) |
+ return path; |
+ |
+ var parts = parsePath(pathString); |
+ if (!parts) |
+ return invalidPath; |
+ |
+ var path = new Path(parts, constructorIsPrivate); |
+ pathCache[pathString] = path; |
+ return path; |
+ } |
+ |
+ Path.get = getPath; |
+ |
+ function formatAccessor(key) { |
+ if (isIndex(key)) { |
+ return '[' + key + ']'; |
+ } else { |
+ return '["' + key.replace(/"/g, '\\"') + '"]'; |
+ } |
+ } |
+ |
+ Path.prototype = createObject({ |
+ __proto__: [], |
+ valid: true, |
+ |
+ toString: function() { |
+ var pathString = ''; |
+ for (var i = 0; i < this.length; i++) { |
+ var key = this[i]; |
+ if (isIdent(key)) { |
+ pathString += i ? '.' + key : key; |
+ } else { |
+ pathString += formatAccessor(key); |
+ } |
+ } |
+ |
+ return pathString; |
+ }, |
+ |
+ getValueFrom: function(obj, directObserver) { |
+ for (var i = 0; i < this.length; i++) { |
+ if (obj == null) |
+ return; |
+ obj = obj[this[i]]; |
+ } |
+ return obj; |
+ }, |
+ |
+ iterateObjects: function(obj, observe) { |
+ for (var i = 0; i < this.length; i++) { |
+ if (i) |
+ obj = obj[this[i - 1]]; |
+ if (!isObject(obj)) |
+ return; |
+ observe(obj, this[i]); |
+ } |
+ }, |
+ |
+ compiledGetValueFromFn: function() { |
+ var str = ''; |
+ var pathString = 'obj'; |
+ str += 'if (obj != null'; |
+ var i = 0; |
+ var key; |
+ for (; i < (this.length - 1); i++) { |
+ key = this[i]; |
+ pathString += isIdent(key) ? '.' + key : formatAccessor(key); |
+ str += ' &&\n ' + pathString + ' != null'; |
+ } |
+ str += ')\n'; |
+ |
+ var key = this[i]; |
+ pathString += isIdent(key) ? '.' + key : formatAccessor(key); |
+ |
+ str += ' return ' + pathString + ';\nelse\n return undefined;'; |
+ return new Function('obj', str); |
+ }, |
+ |
+ setValueFrom: function(obj, value) { |
+ if (!this.length) |
+ return false; |
+ |
+ for (var i = 0; i < this.length - 1; i++) { |
+ if (!isObject(obj)) |
+ return false; |
+ obj = obj[this[i]]; |
+ } |
+ |
+ if (!isObject(obj)) |
+ return false; |
+ |
+ obj[this[i]] = value; |
+ return true; |
+ } |
+ }); |
+ |
+ var invalidPath = new Path('', constructorIsPrivate); |
+ invalidPath.valid = false; |
+ invalidPath.getValueFrom = invalidPath.setValueFrom = function() {}; |
+ |
+ var MAX_DIRTY_CHECK_CYCLES = 1000; |
+ |
+ function dirtyCheck(observer) { |
+ var cycles = 0; |
+ while (cycles < MAX_DIRTY_CHECK_CYCLES && observer.check_()) { |
+ cycles++; |
+ } |
+ if (testingExposeCycleCount) |
+ global.dirtyCheckCycleCount = cycles; |
+ |
+ return cycles > 0; |
+ } |
+ |
+ function objectIsEmpty(object) { |
+ for (var prop in object) |
+ return false; |
+ return true; |
+ } |
+ |
+ function diffIsEmpty(diff) { |
+ return objectIsEmpty(diff.added) && |
+ objectIsEmpty(diff.removed) && |
+ objectIsEmpty(diff.changed); |
+ } |
+ |
+ function diffObjectFromOldObject(object, oldObject) { |
+ var added = {}; |
+ var removed = {}; |
+ var changed = {}; |
+ |
+ for (var prop in oldObject) { |
+ var newValue = object[prop]; |
+ |
+ if (newValue !== undefined && newValue === oldObject[prop]) |
+ continue; |
+ |
+ if (!(prop in object)) { |
+ removed[prop] = undefined; |
+ continue; |
+ } |
+ |
+ if (newValue !== oldObject[prop]) |
+ changed[prop] = newValue; |
+ } |
+ |
+ for (var prop in object) { |
+ if (prop in oldObject) |
+ continue; |
+ |
+ added[prop] = object[prop]; |
+ } |
+ |
+ if (Array.isArray(object) && object.length !== oldObject.length) |
+ changed.length = object.length; |
+ |
+ return { |
+ added: added, |
+ removed: removed, |
+ changed: changed |
+ }; |
+ } |
+ |
+ var eomTasks = []; |
+ function runEOMTasks() { |
+ if (!eomTasks.length) |
+ return false; |
+ |
+ for (var i = 0; i < eomTasks.length; i++) { |
+ eomTasks[i](); |
+ } |
+ eomTasks.length = 0; |
+ return true; |
+ } |
+ |
+ var runEOM = hasObserve ? (function(){ |
+ return function(fn) { |
+ return Promise.resolve().then(fn); |
+ } |
+ })() : |
+ (function() { |
+ return function(fn) { |
+ eomTasks.push(fn); |
+ }; |
+ })(); |
+ |
+ var observedObjectCache = []; |
+ |
+ function newObservedObject() { |
+ var observer; |
+ var object; |
+ var discardRecords = false; |
+ var first = true; |
+ |
+ function callback(records) { |
+ if (observer && observer.state_ === OPENED && !discardRecords) |
+ observer.check_(records); |
+ } |
+ |
+ return { |
+ open: function(obs) { |
+ if (observer) |
+ throw Error('ObservedObject in use'); |
+ |
+ if (!first) |
+ Object.deliverChangeRecords(callback); |
+ |
+ observer = obs; |
+ first = false; |
+ }, |
+ observe: function(obj, arrayObserve) { |
+ object = obj; |
+ if (arrayObserve) |
+ Array.observe(object, callback); |
+ else |
+ Object.observe(object, callback); |
+ }, |
+ deliver: function(discard) { |
+ discardRecords = discard; |
+ Object.deliverChangeRecords(callback); |
+ discardRecords = false; |
+ }, |
+ close: function() { |
+ observer = undefined; |
+ Object.unobserve(object, callback); |
+ observedObjectCache.push(this); |
+ } |
+ }; |
+ } |
+ |
+ /* |
+ * The observedSet abstraction is a perf optimization which reduces the total |
+ * number of Object.observe observations of a set of objects. The idea is that |
+ * groups of Observers will have some object dependencies in common and this |
+ * observed set ensures that each object in the transitive closure of |
+ * dependencies is only observed once. The observedSet acts as a write barrier |
+ * such that whenever any change comes through, all Observers are checked for |
+ * changed values. |
+ * |
+ * Note that this optimization is explicitly moving work from setup-time to |
+ * change-time. |
+ * |
+ * TODO(rafaelw): Implement "garbage collection". In order to move work off |
+ * the critical path, when Observers are closed, their observed objects are |
+ * not Object.unobserve(d). As a result, it's possible that if the observedSet |
+ * is kept open, but some Observers have been closed, it could cause "leaks" |
+ * (prevent otherwise collectable objects from being collected). At some |
+ * point, we should implement incremental "gc" which keeps a list of |
+ * observedSets which may need clean-up and does small amounts of cleanup on a |
+ * timeout until all is clean. |
+ */ |
+ |
+ function getObservedObject(observer, object, arrayObserve) { |
+ var dir = observedObjectCache.pop() || newObservedObject(); |
+ dir.open(observer); |
+ dir.observe(object, arrayObserve); |
+ return dir; |
+ } |
+ |
+ var observedSetCache = []; |
+ |
+ function newObservedSet() { |
+ var observerCount = 0; |
+ var observers = []; |
+ var objects = []; |
+ var rootObj; |
+ var rootObjProps; |
+ |
+ function observe(obj, prop) { |
+ if (!obj) |
+ return; |
+ |
+ if (obj === rootObj) |
+ rootObjProps[prop] = true; |
+ |
+ if (objects.indexOf(obj) < 0) { |
+ objects.push(obj); |
+ Object.observe(obj, callback); |
+ } |
+ |
+ observe(Object.getPrototypeOf(obj), prop); |
+ } |
+ |
+ function allRootObjNonObservedProps(recs) { |
+ for (var i = 0; i < recs.length; i++) { |
+ var rec = recs[i]; |
+ if (rec.object !== rootObj || |
+ rootObjProps[rec.name] || |
+ rec.type === 'setPrototype') { |
+ return false; |
+ } |
+ } |
+ return true; |
+ } |
+ |
+ function callback(recs) { |
+ if (allRootObjNonObservedProps(recs)) |
+ return; |
+ |
+ var observer; |
+ for (var i = 0; i < observers.length; i++) { |
+ observer = observers[i]; |
+ if (observer.state_ == OPENED) { |
+ observer.iterateObjects_(observe); |
+ } |
+ } |
+ |
+ for (var i = 0; i < observers.length; i++) { |
+ observer = observers[i]; |
+ if (observer.state_ == OPENED) { |
+ observer.check_(); |
+ } |
+ } |
+ } |
+ |
+ var record = { |
+ objects: objects, |
+ get rootObject() { return rootObj; }, |
+ set rootObject(value) { |
+ rootObj = value; |
+ rootObjProps = {}; |
+ }, |
+ open: function(obs, object) { |
+ observers.push(obs); |
+ observerCount++; |
+ obs.iterateObjects_(observe); |
+ }, |
+ close: function(obs) { |
+ observerCount--; |
+ if (observerCount > 0) { |
+ return; |
+ } |
+ |
+ for (var i = 0; i < objects.length; i++) { |
+ Object.unobserve(objects[i], callback); |
+ Observer.unobservedCount++; |
+ } |
+ |
+ observers.length = 0; |
+ objects.length = 0; |
+ rootObj = undefined; |
+ rootObjProps = undefined; |
+ observedSetCache.push(this); |
+ if (lastObservedSet === this) |
+ lastObservedSet = null; |
+ }, |
+ }; |
+ |
+ return record; |
+ } |
+ |
+ var lastObservedSet; |
+ |
+ function getObservedSet(observer, obj) { |
+ if (!lastObservedSet || lastObservedSet.rootObject !== obj) { |
+ lastObservedSet = observedSetCache.pop() || newObservedSet(); |
+ lastObservedSet.rootObject = obj; |
+ } |
+ lastObservedSet.open(observer, obj); |
+ return lastObservedSet; |
+ } |
+ |
+ var UNOPENED = 0; |
+ var OPENED = 1; |
+ var CLOSED = 2; |
+ var RESETTING = 3; |
+ |
+ var nextObserverId = 1; |
+ |
+ function Observer() { |
+ this.state_ = UNOPENED; |
+ this.callback_ = undefined; |
+ this.target_ = undefined; // TODO(rafaelw): Should be WeakRef |
+ this.directObserver_ = undefined; |
+ this.value_ = undefined; |
+ this.id_ = nextObserverId++; |
+ } |
+ |
+ Observer.prototype = { |
+ open: function(callback, target) { |
+ if (this.state_ != UNOPENED) |
+ throw Error('Observer has already been opened.'); |
+ |
+ addToAll(this); |
+ this.callback_ = callback; |
+ this.target_ = target; |
+ this.connect_(); |
+ this.state_ = OPENED; |
+ return this.value_; |
+ }, |
+ |
+ close: function() { |
+ if (this.state_ != OPENED) |
+ return; |
+ |
+ removeFromAll(this); |
+ this.disconnect_(); |
+ this.value_ = undefined; |
+ this.callback_ = undefined; |
+ this.target_ = undefined; |
+ this.state_ = CLOSED; |
+ }, |
+ |
+ deliver: function() { |
+ if (this.state_ != OPENED) |
+ return; |
+ |
+ dirtyCheck(this); |
+ }, |
+ |
+ report_: function(changes) { |
+ try { |
+ this.callback_.apply(this.target_, changes); |
+ } catch (ex) { |
+ Observer._errorThrownDuringCallback = true; |
+ console.error('Exception caught during observer callback: ' + |
+ (ex.stack || ex)); |
+ } |
+ }, |
+ |
+ discardChanges: function() { |
+ this.check_(undefined, true); |
+ return this.value_; |
+ } |
+ } |
+ |
+ var collectObservers = !hasObserve; |
+ var allObservers; |
+ Observer._allObserversCount = 0; |
+ |
+ if (collectObservers) { |
+ allObservers = []; |
+ } |
+ |
+ function addToAll(observer) { |
+ Observer._allObserversCount++; |
+ if (!collectObservers) |
+ return; |
+ |
+ allObservers.push(observer); |
+ } |
+ |
+ function removeFromAll(observer) { |
+ Observer._allObserversCount--; |
+ } |
+ |
+ var runningMicrotaskCheckpoint = false; |
+ |
+ global.Platform = global.Platform || {}; |
+ |
+ global.Platform.performMicrotaskCheckpoint = function() { |
+ if (runningMicrotaskCheckpoint) |
+ return; |
+ |
+ if (!collectObservers) |
+ return; |
+ |
+ runningMicrotaskCheckpoint = true; |
+ |
+ var cycles = 0; |
+ var anyChanged, toCheck; |
+ |
+ do { |
+ cycles++; |
+ toCheck = allObservers; |
+ allObservers = []; |
+ anyChanged = false; |
+ |
+ for (var i = 0; i < toCheck.length; i++) { |
+ var observer = toCheck[i]; |
+ if (observer.state_ != OPENED) |
+ continue; |
+ |
+ if (observer.check_()) |
+ anyChanged = true; |
+ |
+ allObservers.push(observer); |
+ } |
+ if (runEOMTasks()) |
+ anyChanged = true; |
+ } while (cycles < MAX_DIRTY_CHECK_CYCLES && anyChanged); |
+ |
+ if (testingExposeCycleCount) |
+ global.dirtyCheckCycleCount = cycles; |
+ |
+ runningMicrotaskCheckpoint = false; |
+ }; |
+ |
+ if (collectObservers) { |
+ global.Platform.clearObservers = function() { |
+ allObservers = []; |
+ }; |
+ } |
+ |
+ function ObjectObserver(object) { |
+ Observer.call(this); |
+ this.value_ = object; |
+ this.oldObject_ = undefined; |
+ } |
+ |
+ ObjectObserver.prototype = createObject({ |
+ __proto__: Observer.prototype, |
+ |
+ arrayObserve: false, |
+ |
+ connect_: function(callback, target) { |
+ if (hasObserve) { |
+ this.directObserver_ = getObservedObject(this, this.value_, |
+ this.arrayObserve); |
+ } else { |
+ this.oldObject_ = this.copyObject(this.value_); |
+ } |
+ |
+ }, |
+ |
+ copyObject: function(object) { |
+ var copy = Array.isArray(object) ? [] : {}; |
+ for (var prop in object) { |
+ copy[prop] = object[prop]; |
+ }; |
+ if (Array.isArray(object)) |
+ copy.length = object.length; |
+ return copy; |
+ }, |
+ |
+ check_: function(changeRecords, skipChanges) { |
+ var diff; |
+ var oldValues; |
+ if (hasObserve) { |
+ if (!changeRecords) |
+ return false; |
+ |
+ oldValues = {}; |
+ diff = diffObjectFromChangeRecords(this.value_, changeRecords, |
+ oldValues); |
+ } else { |
+ oldValues = this.oldObject_; |
+ diff = diffObjectFromOldObject(this.value_, this.oldObject_); |
+ } |
+ |
+ if (diffIsEmpty(diff)) |
+ return false; |
+ |
+ if (!hasObserve) |
+ this.oldObject_ = this.copyObject(this.value_); |
+ |
+ this.report_([ |
+ diff.added || {}, |
+ diff.removed || {}, |
+ diff.changed || {}, |
+ function(property) { |
+ return oldValues[property]; |
+ } |
+ ]); |
+ |
+ return true; |
+ }, |
+ |
+ disconnect_: function() { |
+ if (hasObserve) { |
+ this.directObserver_.close(); |
+ this.directObserver_ = undefined; |
+ } else { |
+ this.oldObject_ = undefined; |
+ } |
+ }, |
+ |
+ deliver: function() { |
+ if (this.state_ != OPENED) |
+ return; |
+ |
+ if (hasObserve) |
+ this.directObserver_.deliver(false); |
+ else |
+ dirtyCheck(this); |
+ }, |
+ |
+ discardChanges: function() { |
+ if (this.directObserver_) |
+ this.directObserver_.deliver(true); |
+ else |
+ this.oldObject_ = this.copyObject(this.value_); |
+ |
+ return this.value_; |
+ } |
+ }); |
+ |
+ function ArrayObserver(array) { |
+ if (!Array.isArray(array)) |
+ throw Error('Provided object is not an Array'); |
+ ObjectObserver.call(this, array); |
+ } |
+ |
+ ArrayObserver.prototype = createObject({ |
+ |
+ __proto__: ObjectObserver.prototype, |
+ |
+ arrayObserve: true, |
+ |
+ copyObject: function(arr) { |
+ return arr.slice(); |
+ }, |
+ |
+ check_: function(changeRecords) { |
+ var splices; |
+ if (hasObserve) { |
+ if (!changeRecords) |
+ return false; |
+ splices = projectArraySplices(this.value_, changeRecords); |
+ } else { |
+ splices = calcSplices(this.value_, 0, this.value_.length, |
+ this.oldObject_, 0, this.oldObject_.length); |
+ } |
+ |
+ if (!splices || !splices.length) |
+ return false; |
+ |
+ if (!hasObserve) |
+ this.oldObject_ = this.copyObject(this.value_); |
+ |
+ this.report_([splices]); |
+ return true; |
+ } |
+ }); |
+ |
+ ArrayObserver.applySplices = function(previous, current, splices) { |
+ splices.forEach(function(splice) { |
+ var spliceArgs = [splice.index, splice.removed.length]; |
+ var addIndex = splice.index; |
+ while (addIndex < splice.index + splice.addedCount) { |
+ spliceArgs.push(current[addIndex]); |
+ addIndex++; |
+ } |
+ |
+ Array.prototype.splice.apply(previous, spliceArgs); |
+ }); |
+ }; |
+ |
+ function PathObserver(object, path) { |
+ Observer.call(this); |
+ |
+ this.object_ = object; |
+ this.path_ = getPath(path); |
+ this.directObserver_ = undefined; |
+ } |
+ |
+ PathObserver.prototype = createObject({ |
+ __proto__: Observer.prototype, |
+ |
+ get path() { |
+ return this.path_; |
+ }, |
+ |
+ connect_: function() { |
+ if (hasObserve) |
+ this.directObserver_ = getObservedSet(this, this.object_); |
+ |
+ this.check_(undefined, true); |
+ }, |
+ |
+ disconnect_: function() { |
+ this.value_ = undefined; |
+ |
+ if (this.directObserver_) { |
+ this.directObserver_.close(this); |
+ this.directObserver_ = undefined; |
+ } |
+ }, |
+ |
+ iterateObjects_: function(observe) { |
+ this.path_.iterateObjects(this.object_, observe); |
+ }, |
+ |
+ check_: function(changeRecords, skipChanges) { |
+ var oldValue = this.value_; |
+ this.value_ = this.path_.getValueFrom(this.object_); |
+ if (skipChanges || areSameValue(this.value_, oldValue)) |
+ return false; |
+ |
+ this.report_([this.value_, oldValue, this]); |
+ return true; |
+ }, |
+ |
+ setValue: function(newValue) { |
+ if (this.path_) |
+ this.path_.setValueFrom(this.object_, newValue); |
+ } |
+ }); |
+ |
+ function CompoundObserver(reportChangesOnOpen) { |
+ Observer.call(this); |
+ |
+ this.reportChangesOnOpen_ = reportChangesOnOpen; |
+ this.value_ = []; |
+ this.directObserver_ = undefined; |
+ this.observed_ = []; |
+ } |
+ |
+ var observerSentinel = {}; |
+ |
+ CompoundObserver.prototype = createObject({ |
+ __proto__: Observer.prototype, |
+ |
+ connect_: function() { |
+ if (hasObserve) { |
+ var object; |
+ var needsDirectObserver = false; |
+ for (var i = 0; i < this.observed_.length; i += 2) { |
+ object = this.observed_[i] |
+ if (object !== observerSentinel) { |
+ needsDirectObserver = true; |
+ break; |
+ } |
+ } |
+ |
+ if (needsDirectObserver) |
+ this.directObserver_ = getObservedSet(this, object); |
+ } |
+ |
+ this.check_(undefined, !this.reportChangesOnOpen_); |
+ }, |
+ |
+ disconnect_: function() { |
+ for (var i = 0; i < this.observed_.length; i += 2) { |
+ if (this.observed_[i] === observerSentinel) |
+ this.observed_[i + 1].close(); |
+ } |
+ this.observed_.length = 0; |
+ this.value_.length = 0; |
+ |
+ if (this.directObserver_) { |
+ this.directObserver_.close(this); |
+ this.directObserver_ = undefined; |
+ } |
+ }, |
+ |
+ addPath: function(object, path) { |
+ if (this.state_ != UNOPENED && this.state_ != RESETTING) |
+ throw Error('Cannot add paths once started.'); |
+ |
+ var path = getPath(path); |
+ this.observed_.push(object, path); |
+ if (!this.reportChangesOnOpen_) |
+ return; |
+ var index = this.observed_.length / 2 - 1; |
+ this.value_[index] = path.getValueFrom(object); |
+ }, |
+ |
+ addObserver: function(observer) { |
+ if (this.state_ != UNOPENED && this.state_ != RESETTING) |
+ throw Error('Cannot add observers once started.'); |
+ |
+ this.observed_.push(observerSentinel, observer); |
+ if (!this.reportChangesOnOpen_) |
+ return; |
+ var index = this.observed_.length / 2 - 1; |
+ this.value_[index] = observer.open(this.deliver, this); |
+ }, |
+ |
+ startReset: function() { |
+ if (this.state_ != OPENED) |
+ throw Error('Can only reset while open'); |
+ |
+ this.state_ = RESETTING; |
+ this.disconnect_(); |
+ }, |
+ |
+ finishReset: function() { |
+ if (this.state_ != RESETTING) |
+ throw Error('Can only finishReset after startReset'); |
+ this.state_ = OPENED; |
+ this.connect_(); |
+ |
+ return this.value_; |
+ }, |
+ |
+ iterateObjects_: function(observe) { |
+ var object; |
+ for (var i = 0; i < this.observed_.length; i += 2) { |
+ object = this.observed_[i] |
+ if (object !== observerSentinel) |
+ this.observed_[i + 1].iterateObjects(object, observe) |
+ } |
+ }, |
+ |
+ check_: function(changeRecords, skipChanges) { |
+ var oldValues; |
+ for (var i = 0; i < this.observed_.length; i += 2) { |
+ var object = this.observed_[i]; |
+ var path = this.observed_[i+1]; |
+ var value; |
+ if (object === observerSentinel) { |
+ var observable = path; |
+ value = this.state_ === UNOPENED ? |
+ observable.open(this.deliver, this) : |
+ observable.discardChanges(); |
+ } else { |
+ value = path.getValueFrom(object); |
+ } |
+ |
+ if (skipChanges) { |
+ this.value_[i / 2] = value; |
+ continue; |
+ } |
+ |
+ if (areSameValue(value, this.value_[i / 2])) |
+ continue; |
+ |
+ oldValues = oldValues || []; |
+ oldValues[i / 2] = this.value_[i / 2]; |
+ this.value_[i / 2] = value; |
+ } |
+ |
+ if (!oldValues) |
+ return false; |
+ |
+ // TODO(rafaelw): Having observed_ as the third callback arg here is |
+ // pretty lame API. Fix. |
+ this.report_([this.value_, oldValues, this.observed_]); |
+ return true; |
+ } |
+ }); |
+ |
+ function identFn(value) { return value; } |
+ |
+ function ObserverTransform(observable, getValueFn, setValueFn, |
+ dontPassThroughSet) { |
+ this.callback_ = undefined; |
+ this.target_ = undefined; |
+ this.value_ = undefined; |
+ this.observable_ = observable; |
+ this.getValueFn_ = getValueFn || identFn; |
+ this.setValueFn_ = setValueFn || identFn; |
+ // TODO(rafaelw): This is a temporary hack. PolymerExpressions needs this |
+ // at the moment because of a bug in it's dependency tracking. |
+ this.dontPassThroughSet_ = dontPassThroughSet; |
+ } |
+ |
+ ObserverTransform.prototype = { |
+ open: function(callback, target) { |
+ this.callback_ = callback; |
+ this.target_ = target; |
+ this.value_ = |
+ this.getValueFn_(this.observable_.open(this.observedCallback_, this)); |
+ return this.value_; |
+ }, |
+ |
+ observedCallback_: function(value) { |
+ value = this.getValueFn_(value); |
+ if (areSameValue(value, this.value_)) |
+ return; |
+ var oldValue = this.value_; |
+ this.value_ = value; |
+ this.callback_.call(this.target_, this.value_, oldValue); |
+ }, |
+ |
+ discardChanges: function() { |
+ this.value_ = this.getValueFn_(this.observable_.discardChanges()); |
+ return this.value_; |
+ }, |
+ |
+ deliver: function() { |
+ return this.observable_.deliver(); |
+ }, |
+ |
+ setValue: function(value) { |
+ value = this.setValueFn_(value); |
+ if (!this.dontPassThroughSet_ && this.observable_.setValue) |
+ return this.observable_.setValue(value); |
+ }, |
+ |
+ close: function() { |
+ if (this.observable_) |
+ this.observable_.close(); |
+ this.callback_ = undefined; |
+ this.target_ = undefined; |
+ this.observable_ = undefined; |
+ this.value_ = undefined; |
+ this.getValueFn_ = undefined; |
+ this.setValueFn_ = undefined; |
+ } |
+ } |
+ |
+ var expectedRecordTypes = { |
+ add: true, |
+ update: true, |
+ delete: true |
+ }; |
+ |
+ function diffObjectFromChangeRecords(object, changeRecords, oldValues) { |
+ var added = {}; |
+ var removed = {}; |
+ |
+ for (var i = 0; i < changeRecords.length; i++) { |
+ var record = changeRecords[i]; |
+ if (!expectedRecordTypes[record.type]) { |
+ console.error('Unknown changeRecord type: ' + record.type); |
+ console.error(record); |
+ continue; |
+ } |
+ |
+ if (!(record.name in oldValues)) |
+ oldValues[record.name] = record.oldValue; |
+ |
+ if (record.type == 'update') |
+ continue; |
+ |
+ if (record.type == 'add') { |
+ if (record.name in removed) |
+ delete removed[record.name]; |
+ else |
+ added[record.name] = true; |
+ |
+ continue; |
+ } |
+ |
+ // type = 'delete' |
+ if (record.name in added) { |
+ delete added[record.name]; |
+ delete oldValues[record.name]; |
+ } else { |
+ removed[record.name] = true; |
+ } |
+ } |
+ |
+ for (var prop in added) |
+ added[prop] = object[prop]; |
+ |
+ for (var prop in removed) |
+ removed[prop] = undefined; |
+ |
+ var changed = {}; |
+ for (var prop in oldValues) { |
+ if (prop in added || prop in removed) |
+ continue; |
+ |
+ var newValue = object[prop]; |
+ if (oldValues[prop] !== newValue) |
+ changed[prop] = newValue; |
+ } |
+ |
+ return { |
+ added: added, |
+ removed: removed, |
+ changed: changed |
+ }; |
+ } |
+ |
+ function newSplice(index, removed, addedCount) { |
+ return { |
+ index: index, |
+ removed: removed, |
+ addedCount: addedCount |
+ }; |
+ } |
+ |
+ var EDIT_LEAVE = 0; |
+ var EDIT_UPDATE = 1; |
+ var EDIT_ADD = 2; |
+ var EDIT_DELETE = 3; |
+ |
+ function ArraySplice() {} |
+ |
+ ArraySplice.prototype = { |
+ |
+ // Note: This function is *based* on the computation of the Levenshtein |
+ // "edit" distance. The one change is that "updates" are treated as two |
+ // edits - not one. With Array splices, an update is really a delete |
+ // followed by an add. By retaining this, we optimize for "keeping" the |
+ // maximum array items in the original array. For example: |
+ // |
+ // 'xxxx123' -> '123yyyy' |
+ // |
+ // With 1-edit updates, the shortest path would be just to update all seven |
+ // characters. With 2-edit updates, we delete 4, leave 3, and add 4. This |
+ // leaves the substring '123' intact. |
+ calcEditDistances: function(current, currentStart, currentEnd, |
+ old, oldStart, oldEnd) { |
+ // "Deletion" columns |
+ var rowCount = oldEnd - oldStart + 1; |
+ var columnCount = currentEnd - currentStart + 1; |
+ var distances = new Array(rowCount); |
+ |
+ // "Addition" rows. Initialize null column. |
+ for (var i = 0; i < rowCount; i++) { |
+ distances[i] = new Array(columnCount); |
+ distances[i][0] = i; |
+ } |
+ |
+ // Initialize null row |
+ for (var j = 0; j < columnCount; j++) |
+ distances[0][j] = j; |
+ |
+ for (var i = 1; i < rowCount; i++) { |
+ for (var j = 1; j < columnCount; j++) { |
+ if (this.equals(current[currentStart + j - 1], old[oldStart + i - 1])) |
+ distances[i][j] = distances[i - 1][j - 1]; |
+ else { |
+ var north = distances[i - 1][j] + 1; |
+ var west = distances[i][j - 1] + 1; |
+ distances[i][j] = north < west ? north : west; |
+ } |
+ } |
+ } |
+ |
+ return distances; |
+ }, |
+ |
+ // This starts at the final weight, and walks "backward" by finding |
+ // the minimum previous weight recursively until the origin of the weight |
+ // matrix. |
+ spliceOperationsFromEditDistances: function(distances) { |
+ var i = distances.length - 1; |
+ var j = distances[0].length - 1; |
+ var current = distances[i][j]; |
+ var edits = []; |
+ while (i > 0 || j > 0) { |
+ if (i == 0) { |
+ edits.push(EDIT_ADD); |
+ j--; |
+ continue; |
+ } |
+ if (j == 0) { |
+ edits.push(EDIT_DELETE); |
+ i--; |
+ continue; |
+ } |
+ var northWest = distances[i - 1][j - 1]; |
+ var west = distances[i - 1][j]; |
+ var north = distances[i][j - 1]; |
+ |
+ var min; |
+ if (west < north) |
+ min = west < northWest ? west : northWest; |
+ else |
+ min = north < northWest ? north : northWest; |
+ |
+ if (min == northWest) { |
+ if (northWest == current) { |
+ edits.push(EDIT_LEAVE); |
+ } else { |
+ edits.push(EDIT_UPDATE); |
+ current = northWest; |
+ } |
+ i--; |
+ j--; |
+ } else if (min == west) { |
+ edits.push(EDIT_DELETE); |
+ i--; |
+ current = west; |
+ } else { |
+ edits.push(EDIT_ADD); |
+ j--; |
+ current = north; |
+ } |
+ } |
+ |
+ edits.reverse(); |
+ return edits; |
+ }, |
+ |
+ /** |
+ * Splice Projection functions: |
+ * |
+ * A splice map is a representation of how a previous array of items |
+ * was transformed into a new array of items. Conceptually it is a list of |
+ * tuples of |
+ * |
+ * <index, removed, addedCount> |
+ * |
+ * which are kept in ascending index order of. The tuple represents that at |
+ * the |index|, |removed| sequence of items were removed, and counting forward |
+ * from |index|, |addedCount| items were added. |
+ */ |
+ |
+ /** |
+ * Lacking individual splice mutation information, the minimal set of |
+ * splices can be synthesized given the previous state and final state of an |
+ * array. The basic approach is to calculate the edit distance matrix and |
+ * choose the shortest path through it. |
+ * |
+ * Complexity: O(l * p) |
+ * l: The length of the current array |
+ * p: The length of the old array |
+ */ |
+ calcSplices: function(current, currentStart, currentEnd, |
+ old, oldStart, oldEnd) { |
+ var prefixCount = 0; |
+ var suffixCount = 0; |
+ |
+ var minLength = Math.min(currentEnd - currentStart, oldEnd - oldStart); |
+ if (currentStart == 0 && oldStart == 0) |
+ prefixCount = this.sharedPrefix(current, old, minLength); |
+ |
+ if (currentEnd == current.length && oldEnd == old.length) |
+ suffixCount = this.sharedSuffix(current, old, minLength - prefixCount); |
+ |
+ currentStart += prefixCount; |
+ oldStart += prefixCount; |
+ currentEnd -= suffixCount; |
+ oldEnd -= suffixCount; |
+ |
+ if (currentEnd - currentStart == 0 && oldEnd - oldStart == 0) |
+ return []; |
+ |
+ if (currentStart == currentEnd) { |
+ var splice = newSplice(currentStart, [], 0); |
+ while (oldStart < oldEnd) |
+ splice.removed.push(old[oldStart++]); |
+ |
+ return [ splice ]; |
+ } else if (oldStart == oldEnd) |
+ return [ newSplice(currentStart, [], currentEnd - currentStart) ]; |
+ |
+ var ops = this.spliceOperationsFromEditDistances( |
+ this.calcEditDistances(current, currentStart, currentEnd, |
+ old, oldStart, oldEnd)); |
+ |
+ var splice = undefined; |
+ var splices = []; |
+ var index = currentStart; |
+ var oldIndex = oldStart; |
+ for (var i = 0; i < ops.length; i++) { |
+ switch(ops[i]) { |
+ case EDIT_LEAVE: |
+ if (splice) { |
+ splices.push(splice); |
+ splice = undefined; |
+ } |
+ |
+ index++; |
+ oldIndex++; |
+ break; |
+ case EDIT_UPDATE: |
+ if (!splice) |
+ splice = newSplice(index, [], 0); |
+ |
+ splice.addedCount++; |
+ index++; |
+ |
+ splice.removed.push(old[oldIndex]); |
+ oldIndex++; |
+ break; |
+ case EDIT_ADD: |
+ if (!splice) |
+ splice = newSplice(index, [], 0); |
+ |
+ splice.addedCount++; |
+ index++; |
+ break; |
+ case EDIT_DELETE: |
+ if (!splice) |
+ splice = newSplice(index, [], 0); |
+ |
+ splice.removed.push(old[oldIndex]); |
+ oldIndex++; |
+ break; |
+ } |
+ } |
+ |
+ if (splice) { |
+ splices.push(splice); |
+ } |
+ return splices; |
+ }, |
+ |
+ sharedPrefix: function(current, old, searchLength) { |
+ for (var i = 0; i < searchLength; i++) |
+ if (!this.equals(current[i], old[i])) |
+ return i; |
+ return searchLength; |
+ }, |
+ |
+ sharedSuffix: function(current, old, searchLength) { |
+ var index1 = current.length; |
+ var index2 = old.length; |
+ var count = 0; |
+ while (count < searchLength && this.equals(current[--index1], old[--index2])) |
+ count++; |
+ |
+ return count; |
+ }, |
+ |
+ calculateSplices: function(current, previous) { |
+ return this.calcSplices(current, 0, current.length, previous, 0, |
+ previous.length); |
+ }, |
+ |
+ equals: function(currentValue, previousValue) { |
+ return currentValue === previousValue; |
+ } |
+ }; |
+ |
+ var arraySplice = new ArraySplice(); |
+ |
+ function calcSplices(current, currentStart, currentEnd, |
+ old, oldStart, oldEnd) { |
+ return arraySplice.calcSplices(current, currentStart, currentEnd, |
+ old, oldStart, oldEnd); |
+ } |
+ |
+ function intersect(start1, end1, start2, end2) { |
+ // Disjoint |
+ if (end1 < start2 || end2 < start1) |
+ return -1; |
+ |
+ // Adjacent |
+ if (end1 == start2 || end2 == start1) |
+ return 0; |
+ |
+ // Non-zero intersect, span1 first |
+ if (start1 < start2) { |
+ if (end1 < end2) |
+ return end1 - start2; // Overlap |
+ else |
+ return end2 - start2; // Contained |
+ } else { |
+ // Non-zero intersect, span2 first |
+ if (end2 < end1) |
+ return end2 - start1; // Overlap |
+ else |
+ return end1 - start1; // Contained |
+ } |
+ } |
+ |
+ function mergeSplice(splices, index, removed, addedCount) { |
+ |
+ var splice = newSplice(index, removed, addedCount); |
+ |
+ var inserted = false; |
+ var insertionOffset = 0; |
+ |
+ for (var i = 0; i < splices.length; i++) { |
+ var current = splices[i]; |
+ current.index += insertionOffset; |
+ |
+ if (inserted) |
+ continue; |
+ |
+ var intersectCount = intersect(splice.index, |
+ splice.index + splice.removed.length, |
+ current.index, |
+ current.index + current.addedCount); |
+ |
+ if (intersectCount >= 0) { |
+ // Merge the two splices |
+ |
+ splices.splice(i, 1); |
+ i--; |
+ |
+ insertionOffset -= current.addedCount - current.removed.length; |
+ |
+ splice.addedCount += current.addedCount - intersectCount; |
+ var deleteCount = splice.removed.length + |
+ current.removed.length - intersectCount; |
+ |
+ if (!splice.addedCount && !deleteCount) { |
+ // merged splice is a noop. discard. |
+ inserted = true; |
+ } else { |
+ var removed = current.removed; |
+ |
+ if (splice.index < current.index) { |
+ // some prefix of splice.removed is prepended to current.removed. |
+ var prepend = splice.removed.slice(0, current.index - splice.index); |
+ Array.prototype.push.apply(prepend, removed); |
+ removed = prepend; |
+ } |
+ |
+ if (splice.index + splice.removed.length > current.index + current.addedCount) { |
+ // some suffix of splice.removed is appended to current.removed. |
+ var append = splice.removed.slice(current.index + current.addedCount - splice.index); |
+ Array.prototype.push.apply(removed, append); |
+ } |
+ |
+ splice.removed = removed; |
+ if (current.index < splice.index) { |
+ splice.index = current.index; |
+ } |
+ } |
+ } else if (splice.index < current.index) { |
+ // Insert splice here. |
+ |
+ inserted = true; |
+ |
+ splices.splice(i, 0, splice); |
+ i++; |
+ |
+ var offset = splice.addedCount - splice.removed.length |
+ current.index += offset; |
+ insertionOffset += offset; |
+ } |
+ } |
+ |
+ if (!inserted) |
+ splices.push(splice); |
+ } |
+ |
+ function createInitialSplices(array, changeRecords) { |
+ var splices = []; |
+ |
+ for (var i = 0; i < changeRecords.length; i++) { |
+ var record = changeRecords[i]; |
+ switch(record.type) { |
+ case 'splice': |
+ mergeSplice(splices, record.index, record.removed.slice(), record.addedCount); |
+ break; |
+ case 'add': |
+ case 'update': |
+ case 'delete': |
+ if (!isIndex(record.name)) |
+ continue; |
+ var index = toNumber(record.name); |
+ if (index < 0) |
+ continue; |
+ mergeSplice(splices, index, [record.oldValue], 1); |
+ break; |
+ default: |
+ console.error('Unexpected record type: ' + JSON.stringify(record)); |
+ break; |
+ } |
+ } |
+ |
+ return splices; |
+ } |
+ |
+ function projectArraySplices(array, changeRecords) { |
+ var splices = []; |
+ |
+ createInitialSplices(array, changeRecords).forEach(function(splice) { |
+ if (splice.addedCount == 1 && splice.removed.length == 1) { |
+ if (splice.removed[0] !== array[splice.index]) |
+ splices.push(splice); |
+ |
+ return |
+ }; |
+ |
+ splices = splices.concat(calcSplices(array, splice.index, splice.index + splice.addedCount, |
+ splice.removed, 0, splice.removed.length)); |
+ }); |
+ |
+ return splices; |
+ } |
+ |
+ // Export the observe-js object for **Node.js**, with |
+ // backwards-compatibility for the old `require()` API. If we're in |
+ // the browser, export as a global object. |
+ |
+ var expose = global; |
+ |
+ if (typeof exports !== 'undefined') { |
+ if (typeof module !== 'undefined' && module.exports) { |
+ expose = exports = module.exports; |
+ } |
+ expose = exports; |
+ } |
+ |
+ expose.Observer = Observer; |
+ expose.Observer.runEOM_ = runEOM; |
+ expose.Observer.observerSentinel_ = observerSentinel; // for testing. |
+ expose.Observer.hasObjectObserve = hasObserve; |
+ expose.ArrayObserver = ArrayObserver; |
+ expose.ArrayObserver.calculateSplices = function(current, previous) { |
+ return arraySplice.calculateSplices(current, previous); |
+ }; |
+ |
+ expose.ArraySplice = ArraySplice; |
+ expose.ObjectObserver = ObjectObserver; |
+ expose.PathObserver = PathObserver; |
+ expose.CompoundObserver = CompoundObserver; |
+ expose.Path = Path; |
+ expose.ObserverTransform = ObserverTransform; |
+ |
+})(typeof global !== 'undefined' && global && typeof module !== 'undefined' && module ? global : this || window); |