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