|  | <!-- | 
|  | // Copyright 2014 The Chromium Authors. All rights reserved. | 
|  | // Use of this source code is governed by a BSD-style license that can be | 
|  | // found in the LICENSE file. | 
|  | --> | 
|  |  | 
|  | <script> | 
|  | 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); | 
|  | } | 
|  |  | 
|  | function areSameValue(left, right) { | 
|  | if (left === right) | 
|  | return left !== 0 || 1 / left === 1 / right; | 
|  | if (Number.isNaN(left) && Number.isNaN(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++; | 
|  | } | 
|  |  | 
|  | return cycles > 0; | 
|  | } | 
|  |  | 
|  | function runEOM(fn) { | 
|  | return Promise.resolve().then(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.'); | 
|  |  | 
|  | this.callback_ = callback; | 
|  | this.target_ = target; | 
|  | this.connect_(); | 
|  | this.state_ = OPENED; | 
|  | return this.value_; | 
|  | }, | 
|  |  | 
|  | close: function() { | 
|  | if (this.state_ != OPENED) | 
|  | return; | 
|  |  | 
|  | 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_; | 
|  | } | 
|  | } | 
|  |  | 
|  | function ArrayObserver(array) { | 
|  | if (!Array.isArray(array)) | 
|  | throw Error('Provided object is not an Array'); | 
|  | Observer.call(this); | 
|  | this.value_ = array; | 
|  | this.oldObject_ = undefined; | 
|  | } | 
|  |  | 
|  | ArrayObserver.prototype = createObject({ | 
|  |  | 
|  | __proto__: Observer.prototype, | 
|  |  | 
|  | connect_: function(callback, target) { | 
|  | this.directObserver_ = getObservedObject(this, this.value_, | 
|  | true /* arrayObserve */); | 
|  | }, | 
|  |  | 
|  | copyObject: function(arr) { | 
|  | return arr.slice(); | 
|  | }, | 
|  |  | 
|  | check_: function(changeRecords) { | 
|  | var splices; | 
|  | if (!changeRecords) | 
|  | return false; | 
|  | splices = projectArraySplices(this.value_, changeRecords); | 
|  |  | 
|  | if (!splices || !splices.length) | 
|  | return false; | 
|  |  | 
|  | this.report_([splices]); | 
|  | return true; | 
|  | }, | 
|  |  | 
|  | disconnect_: function() { | 
|  | this.directObserver_.close(); | 
|  | this.directObserver_ = undefined; | 
|  | }, | 
|  |  | 
|  | deliver: function() { | 
|  | if (this.state_ != OPENED) | 
|  | return; | 
|  |  | 
|  | this.directObserver_.deliver(false); | 
|  | }, | 
|  |  | 
|  | discardChanges: function() { | 
|  | if (this.directObserver_) | 
|  | this.directObserver_.deliver(true); | 
|  | else | 
|  | this.oldObject_ = this.copyObject(this.value_); | 
|  |  | 
|  | return this.value_; | 
|  | } | 
|  | }); | 
|  |  | 
|  | 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() { | 
|  | 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() { | 
|  | 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; | 
|  | } | 
|  | } | 
|  |  | 
|  | 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; | 
|  | } | 
|  |  | 
|  | ArrayObserver.calculateSplices = function(current, previous) { | 
|  | return arraySplice.calculateSplices(current, previous); | 
|  | }; | 
|  |  | 
|  | module.exports = { | 
|  | Observer: Observer, | 
|  | runEOM_: runEOM, | 
|  | observerSentinel_: observerSentinel, | 
|  | PathObserver: PathObserver, | 
|  | ArrayObserver: ArrayObserver, | 
|  | ArraySplice: ArraySplice, | 
|  | CompoundObserver: CompoundObserver, | 
|  | Path: Path, | 
|  | ObserverTransform: ObserverTransform | 
|  | } | 
|  | </script> |