OLD | NEW |
(Empty) | |
| 1 /* |
| 2 * Copyright (c) 2014 The Polymer Project Authors. All rights reserved. |
| 3 * This code may only be used under the BSD style license found at http://polyme
r.github.io/LICENSE.txt |
| 4 * The complete set of authors may be found at http://polymer.github.io/AUTHORS.
txt |
| 5 * The complete set of contributors may be found at http://polymer.github.io/CON
TRIBUTORS.txt |
| 6 * Code distributed by Google as part of the polymer project is also |
| 7 * subject to an additional IP rights grant found at http://polymer.github.io/PA
TENTS.txt |
| 8 */ |
| 9 |
| 10 (function(global) { |
| 11 'use strict'; |
| 12 |
| 13 var testingExposeCycleCount = global.testingExposeCycleCount; |
| 14 |
| 15 // Detect and do basic sanity checking on Object/Array.observe. |
| 16 function detectObjectObserve() { |
| 17 if (typeof Object.observe !== 'function' || |
| 18 typeof Array.observe !== 'function') { |
| 19 return false; |
| 20 } |
| 21 |
| 22 var records = []; |
| 23 |
| 24 function callback(recs) { |
| 25 records = recs; |
| 26 } |
| 27 |
| 28 var test = {}; |
| 29 var arr = []; |
| 30 Object.observe(test, callback); |
| 31 Array.observe(arr, callback); |
| 32 test.id = 1; |
| 33 test.id = 2; |
| 34 delete test.id; |
| 35 arr.push(1, 2); |
| 36 arr.length = 0; |
| 37 |
| 38 Object.deliverChangeRecords(callback); |
| 39 if (records.length !== 5) |
| 40 return false; |
| 41 |
| 42 if (records[0].type != 'add' || |
| 43 records[1].type != 'update' || |
| 44 records[2].type != 'delete' || |
| 45 records[3].type != 'splice' || |
| 46 records[4].type != 'splice') { |
| 47 return false; |
| 48 } |
| 49 |
| 50 Object.unobserve(test, callback); |
| 51 Array.unobserve(arr, callback); |
| 52 |
| 53 return true; |
| 54 } |
| 55 |
| 56 var hasObserve = detectObjectObserve(); |
| 57 |
| 58 function detectEval() { |
| 59 // Don't test for eval if we're running in a Chrome App environment. |
| 60 // We check for APIs set that only exist in a Chrome App context. |
| 61 if (typeof chrome !== 'undefined' && chrome.app && chrome.app.runtime) { |
| 62 return false; |
| 63 } |
| 64 |
| 65 // Firefox OS Apps do not allow eval. This feature detection is very hacky |
| 66 // but even if some other platform adds support for this function this code |
| 67 // will continue to work. |
| 68 if (typeof navigator != 'undefined' && navigator.getDeviceStorage) { |
| 69 return false; |
| 70 } |
| 71 |
| 72 try { |
| 73 var f = new Function('', 'return true;'); |
| 74 return f(); |
| 75 } catch (ex) { |
| 76 return false; |
| 77 } |
| 78 } |
| 79 |
| 80 var hasEval = detectEval(); |
| 81 |
| 82 function isIndex(s) { |
| 83 return +s === s >>> 0 && s !== ''; |
| 84 } |
| 85 |
| 86 function toNumber(s) { |
| 87 return +s; |
| 88 } |
| 89 |
| 90 function isObject(obj) { |
| 91 return obj === Object(obj); |
| 92 } |
| 93 |
| 94 var numberIsNaN = global.Number.isNaN || function(value) { |
| 95 return typeof value === 'number' && global.isNaN(value); |
| 96 } |
| 97 |
| 98 function areSameValue(left, right) { |
| 99 if (left === right) |
| 100 return left !== 0 || 1 / left === 1 / right; |
| 101 if (numberIsNaN(left) && numberIsNaN(right)) |
| 102 return true; |
| 103 |
| 104 return left !== left && right !== right; |
| 105 } |
| 106 |
| 107 var createObject = ('__proto__' in {}) ? |
| 108 function(obj) { return obj; } : |
| 109 function(obj) { |
| 110 var proto = obj.__proto__; |
| 111 if (!proto) |
| 112 return obj; |
| 113 var newObject = Object.create(proto); |
| 114 Object.getOwnPropertyNames(obj).forEach(function(name) { |
| 115 Object.defineProperty(newObject, name, |
| 116 Object.getOwnPropertyDescriptor(obj, name)); |
| 117 }); |
| 118 return newObject; |
| 119 }; |
| 120 |
| 121 var identStart = '[\$_a-zA-Z]'; |
| 122 var identPart = '[\$_a-zA-Z0-9]'; |
| 123 var identRegExp = new RegExp('^' + identStart + '+' + identPart + '*' + '$'); |
| 124 |
| 125 function getPathCharType(char) { |
| 126 if (char === undefined) |
| 127 return 'eof'; |
| 128 |
| 129 var code = char.charCodeAt(0); |
| 130 |
| 131 switch(code) { |
| 132 case 0x5B: // [ |
| 133 case 0x5D: // ] |
| 134 case 0x2E: // . |
| 135 case 0x22: // " |
| 136 case 0x27: // ' |
| 137 case 0x30: // 0 |
| 138 return char; |
| 139 |
| 140 case 0x5F: // _ |
| 141 case 0x24: // $ |
| 142 return 'ident'; |
| 143 |
| 144 case 0x20: // Space |
| 145 case 0x09: // Tab |
| 146 case 0x0A: // Newline |
| 147 case 0x0D: // Return |
| 148 case 0xA0: // No-break space |
| 149 case 0xFEFF: // Byte Order Mark |
| 150 case 0x2028: // Line Separator |
| 151 case 0x2029: // Paragraph Separator |
| 152 return 'ws'; |
| 153 } |
| 154 |
| 155 // a-z, A-Z |
| 156 if ((0x61 <= code && code <= 0x7A) || (0x41 <= code && code <= 0x5A)) |
| 157 return 'ident'; |
| 158 |
| 159 // 1-9 |
| 160 if (0x31 <= code && code <= 0x39) |
| 161 return 'number'; |
| 162 |
| 163 return 'else'; |
| 164 } |
| 165 |
| 166 var pathStateMachine = { |
| 167 'beforePath': { |
| 168 'ws': ['beforePath'], |
| 169 'ident': ['inIdent', 'append'], |
| 170 '[': ['beforeElement'], |
| 171 'eof': ['afterPath'] |
| 172 }, |
| 173 |
| 174 'inPath': { |
| 175 'ws': ['inPath'], |
| 176 '.': ['beforeIdent'], |
| 177 '[': ['beforeElement'], |
| 178 'eof': ['afterPath'] |
| 179 }, |
| 180 |
| 181 'beforeIdent': { |
| 182 'ws': ['beforeIdent'], |
| 183 'ident': ['inIdent', 'append'] |
| 184 }, |
| 185 |
| 186 'inIdent': { |
| 187 'ident': ['inIdent', 'append'], |
| 188 '0': ['inIdent', 'append'], |
| 189 'number': ['inIdent', 'append'], |
| 190 'ws': ['inPath', 'push'], |
| 191 '.': ['beforeIdent', 'push'], |
| 192 '[': ['beforeElement', 'push'], |
| 193 'eof': ['afterPath', 'push'] |
| 194 }, |
| 195 |
| 196 'beforeElement': { |
| 197 'ws': ['beforeElement'], |
| 198 '0': ['afterZero', 'append'], |
| 199 'number': ['inIndex', 'append'], |
| 200 "'": ['inSingleQuote', 'append', ''], |
| 201 '"': ['inDoubleQuote', 'append', ''] |
| 202 }, |
| 203 |
| 204 'afterZero': { |
| 205 'ws': ['afterElement', 'push'], |
| 206 ']': ['inPath', 'push'] |
| 207 }, |
| 208 |
| 209 'inIndex': { |
| 210 '0': ['inIndex', 'append'], |
| 211 'number': ['inIndex', 'append'], |
| 212 'ws': ['afterElement'], |
| 213 ']': ['inPath', 'push'] |
| 214 }, |
| 215 |
| 216 'inSingleQuote': { |
| 217 "'": ['afterElement'], |
| 218 'eof': ['error'], |
| 219 'else': ['inSingleQuote', 'append'] |
| 220 }, |
| 221 |
| 222 'inDoubleQuote': { |
| 223 '"': ['afterElement'], |
| 224 'eof': ['error'], |
| 225 'else': ['inDoubleQuote', 'append'] |
| 226 }, |
| 227 |
| 228 'afterElement': { |
| 229 'ws': ['afterElement'], |
| 230 ']': ['inPath', 'push'] |
| 231 } |
| 232 } |
| 233 |
| 234 function noop() {} |
| 235 |
| 236 function parsePath(path) { |
| 237 var keys = []; |
| 238 var index = -1; |
| 239 var c, newChar, key, type, transition, action, typeMap, mode = 'beforePath'; |
| 240 |
| 241 var actions = { |
| 242 push: function() { |
| 243 if (key === undefined) |
| 244 return; |
| 245 |
| 246 keys.push(key); |
| 247 key = undefined; |
| 248 }, |
| 249 |
| 250 append: function() { |
| 251 if (key === undefined) |
| 252 key = newChar |
| 253 else |
| 254 key += newChar; |
| 255 } |
| 256 }; |
| 257 |
| 258 function maybeUnescapeQuote() { |
| 259 if (index >= path.length) |
| 260 return; |
| 261 |
| 262 var nextChar = path[index + 1]; |
| 263 if ((mode == 'inSingleQuote' && nextChar == "'") || |
| 264 (mode == 'inDoubleQuote' && nextChar == '"')) { |
| 265 index++; |
| 266 newChar = nextChar; |
| 267 actions.append(); |
| 268 return true; |
| 269 } |
| 270 } |
| 271 |
| 272 while (mode) { |
| 273 index++; |
| 274 c = path[index]; |
| 275 |
| 276 if (c == '\\' && maybeUnescapeQuote(mode)) |
| 277 continue; |
| 278 |
| 279 type = getPathCharType(c); |
| 280 typeMap = pathStateMachine[mode]; |
| 281 transition = typeMap[type] || typeMap['else'] || 'error'; |
| 282 |
| 283 if (transition == 'error') |
| 284 return; // parse error; |
| 285 |
| 286 mode = transition[0]; |
| 287 action = actions[transition[1]] || noop; |
| 288 newChar = transition[2] === undefined ? c : transition[2]; |
| 289 action(); |
| 290 |
| 291 if (mode === 'afterPath') { |
| 292 return keys; |
| 293 } |
| 294 } |
| 295 |
| 296 return; // parse error |
| 297 } |
| 298 |
| 299 function isIdent(s) { |
| 300 return identRegExp.test(s); |
| 301 } |
| 302 |
| 303 var constructorIsPrivate = {}; |
| 304 |
| 305 function Path(parts, privateToken) { |
| 306 if (privateToken !== constructorIsPrivate) |
| 307 throw Error('Use Path.get to retrieve path objects'); |
| 308 |
| 309 for (var i = 0; i < parts.length; i++) { |
| 310 this.push(String(parts[i])); |
| 311 } |
| 312 |
| 313 if (hasEval && this.length) { |
| 314 this.getValueFrom = this.compiledGetValueFromFn(); |
| 315 } |
| 316 } |
| 317 |
| 318 // TODO(rafaelw): Make simple LRU cache |
| 319 var pathCache = {}; |
| 320 |
| 321 function getPath(pathString) { |
| 322 if (pathString instanceof Path) |
| 323 return pathString; |
| 324 |
| 325 if (pathString == null || pathString.length == 0) |
| 326 pathString = ''; |
| 327 |
| 328 if (typeof pathString != 'string') { |
| 329 if (isIndex(pathString.length)) { |
| 330 // Constructed with array-like (pre-parsed) keys |
| 331 return new Path(pathString, constructorIsPrivate); |
| 332 } |
| 333 |
| 334 pathString = String(pathString); |
| 335 } |
| 336 |
| 337 var path = pathCache[pathString]; |
| 338 if (path) |
| 339 return path; |
| 340 |
| 341 var parts = parsePath(pathString); |
| 342 if (!parts) |
| 343 return invalidPath; |
| 344 |
| 345 var path = new Path(parts, constructorIsPrivate); |
| 346 pathCache[pathString] = path; |
| 347 return path; |
| 348 } |
| 349 |
| 350 Path.get = getPath; |
| 351 |
| 352 function formatAccessor(key) { |
| 353 if (isIndex(key)) { |
| 354 return '[' + key + ']'; |
| 355 } else { |
| 356 return '["' + key.replace(/"/g, '\\"') + '"]'; |
| 357 } |
| 358 } |
| 359 |
| 360 Path.prototype = createObject({ |
| 361 __proto__: [], |
| 362 valid: true, |
| 363 |
| 364 toString: function() { |
| 365 var pathString = ''; |
| 366 for (var i = 0; i < this.length; i++) { |
| 367 var key = this[i]; |
| 368 if (isIdent(key)) { |
| 369 pathString += i ? '.' + key : key; |
| 370 } else { |
| 371 pathString += formatAccessor(key); |
| 372 } |
| 373 } |
| 374 |
| 375 return pathString; |
| 376 }, |
| 377 |
| 378 getValueFrom: function(obj, directObserver) { |
| 379 for (var i = 0; i < this.length; i++) { |
| 380 if (obj == null) |
| 381 return; |
| 382 obj = obj[this[i]]; |
| 383 } |
| 384 return obj; |
| 385 }, |
| 386 |
| 387 iterateObjects: function(obj, observe) { |
| 388 for (var i = 0; i < this.length; i++) { |
| 389 if (i) |
| 390 obj = obj[this[i - 1]]; |
| 391 if (!isObject(obj)) |
| 392 return; |
| 393 observe(obj, this[i]); |
| 394 } |
| 395 }, |
| 396 |
| 397 compiledGetValueFromFn: function() { |
| 398 var str = ''; |
| 399 var pathString = 'obj'; |
| 400 str += 'if (obj != null'; |
| 401 var i = 0; |
| 402 var key; |
| 403 for (; i < (this.length - 1); i++) { |
| 404 key = this[i]; |
| 405 pathString += isIdent(key) ? '.' + key : formatAccessor(key); |
| 406 str += ' &&\n ' + pathString + ' != null'; |
| 407 } |
| 408 str += ')\n'; |
| 409 |
| 410 var key = this[i]; |
| 411 pathString += isIdent(key) ? '.' + key : formatAccessor(key); |
| 412 |
| 413 str += ' return ' + pathString + ';\nelse\n return undefined;'; |
| 414 return new Function('obj', str); |
| 415 }, |
| 416 |
| 417 setValueFrom: function(obj, value) { |
| 418 if (!this.length) |
| 419 return false; |
| 420 |
| 421 for (var i = 0; i < this.length - 1; i++) { |
| 422 if (!isObject(obj)) |
| 423 return false; |
| 424 obj = obj[this[i]]; |
| 425 } |
| 426 |
| 427 if (!isObject(obj)) |
| 428 return false; |
| 429 |
| 430 obj[this[i]] = value; |
| 431 return true; |
| 432 } |
| 433 }); |
| 434 |
| 435 var invalidPath = new Path('', constructorIsPrivate); |
| 436 invalidPath.valid = false; |
| 437 invalidPath.getValueFrom = invalidPath.setValueFrom = function() {}; |
| 438 |
| 439 var MAX_DIRTY_CHECK_CYCLES = 1000; |
| 440 |
| 441 function dirtyCheck(observer) { |
| 442 var cycles = 0; |
| 443 while (cycles < MAX_DIRTY_CHECK_CYCLES && observer.check_()) { |
| 444 cycles++; |
| 445 } |
| 446 if (testingExposeCycleCount) |
| 447 global.dirtyCheckCycleCount = cycles; |
| 448 |
| 449 return cycles > 0; |
| 450 } |
| 451 |
| 452 function objectIsEmpty(object) { |
| 453 for (var prop in object) |
| 454 return false; |
| 455 return true; |
| 456 } |
| 457 |
| 458 function diffIsEmpty(diff) { |
| 459 return objectIsEmpty(diff.added) && |
| 460 objectIsEmpty(diff.removed) && |
| 461 objectIsEmpty(diff.changed); |
| 462 } |
| 463 |
| 464 function diffObjectFromOldObject(object, oldObject) { |
| 465 var added = {}; |
| 466 var removed = {}; |
| 467 var changed = {}; |
| 468 |
| 469 for (var prop in oldObject) { |
| 470 var newValue = object[prop]; |
| 471 |
| 472 if (newValue !== undefined && newValue === oldObject[prop]) |
| 473 continue; |
| 474 |
| 475 if (!(prop in object)) { |
| 476 removed[prop] = undefined; |
| 477 continue; |
| 478 } |
| 479 |
| 480 if (newValue !== oldObject[prop]) |
| 481 changed[prop] = newValue; |
| 482 } |
| 483 |
| 484 for (var prop in object) { |
| 485 if (prop in oldObject) |
| 486 continue; |
| 487 |
| 488 added[prop] = object[prop]; |
| 489 } |
| 490 |
| 491 if (Array.isArray(object) && object.length !== oldObject.length) |
| 492 changed.length = object.length; |
| 493 |
| 494 return { |
| 495 added: added, |
| 496 removed: removed, |
| 497 changed: changed |
| 498 }; |
| 499 } |
| 500 |
| 501 var eomTasks = []; |
| 502 function runEOMTasks() { |
| 503 if (!eomTasks.length) |
| 504 return false; |
| 505 |
| 506 for (var i = 0; i < eomTasks.length; i++) { |
| 507 eomTasks[i](); |
| 508 } |
| 509 eomTasks.length = 0; |
| 510 return true; |
| 511 } |
| 512 |
| 513 var runEOM = hasObserve ? (function(){ |
| 514 return function(fn) { |
| 515 return Promise.resolve().then(fn); |
| 516 } |
| 517 })() : |
| 518 (function() { |
| 519 return function(fn) { |
| 520 eomTasks.push(fn); |
| 521 }; |
| 522 })(); |
| 523 |
| 524 var observedObjectCache = []; |
| 525 |
| 526 function newObservedObject() { |
| 527 var observer; |
| 528 var object; |
| 529 var discardRecords = false; |
| 530 var first = true; |
| 531 |
| 532 function callback(records) { |
| 533 if (observer && observer.state_ === OPENED && !discardRecords) |
| 534 observer.check_(records); |
| 535 } |
| 536 |
| 537 return { |
| 538 open: function(obs) { |
| 539 if (observer) |
| 540 throw Error('ObservedObject in use'); |
| 541 |
| 542 if (!first) |
| 543 Object.deliverChangeRecords(callback); |
| 544 |
| 545 observer = obs; |
| 546 first = false; |
| 547 }, |
| 548 observe: function(obj, arrayObserve) { |
| 549 object = obj; |
| 550 if (arrayObserve) |
| 551 Array.observe(object, callback); |
| 552 else |
| 553 Object.observe(object, callback); |
| 554 }, |
| 555 deliver: function(discard) { |
| 556 discardRecords = discard; |
| 557 Object.deliverChangeRecords(callback); |
| 558 discardRecords = false; |
| 559 }, |
| 560 close: function() { |
| 561 observer = undefined; |
| 562 Object.unobserve(object, callback); |
| 563 observedObjectCache.push(this); |
| 564 } |
| 565 }; |
| 566 } |
| 567 |
| 568 /* |
| 569 * The observedSet abstraction is a perf optimization which reduces the total |
| 570 * number of Object.observe observations of a set of objects. The idea is that |
| 571 * groups of Observers will have some object dependencies in common and this |
| 572 * observed set ensures that each object in the transitive closure of |
| 573 * dependencies is only observed once. The observedSet acts as a write barrier |
| 574 * such that whenever any change comes through, all Observers are checked for |
| 575 * changed values. |
| 576 * |
| 577 * Note that this optimization is explicitly moving work from setup-time to |
| 578 * change-time. |
| 579 * |
| 580 * TODO(rafaelw): Implement "garbage collection". In order to move work off |
| 581 * the critical path, when Observers are closed, their observed objects are |
| 582 * not Object.unobserve(d). As a result, it's possible that if the observedSet |
| 583 * is kept open, but some Observers have been closed, it could cause "leaks" |
| 584 * (prevent otherwise collectable objects from being collected). At some |
| 585 * point, we should implement incremental "gc" which keeps a list of |
| 586 * observedSets which may need clean-up and does small amounts of cleanup on a |
| 587 * timeout until all is clean. |
| 588 */ |
| 589 |
| 590 function getObservedObject(observer, object, arrayObserve) { |
| 591 var dir = observedObjectCache.pop() || newObservedObject(); |
| 592 dir.open(observer); |
| 593 dir.observe(object, arrayObserve); |
| 594 return dir; |
| 595 } |
| 596 |
| 597 var observedSetCache = []; |
| 598 |
| 599 function newObservedSet() { |
| 600 var observerCount = 0; |
| 601 var observers = []; |
| 602 var objects = []; |
| 603 var rootObj; |
| 604 var rootObjProps; |
| 605 |
| 606 function observe(obj, prop) { |
| 607 if (!obj) |
| 608 return; |
| 609 |
| 610 if (obj === rootObj) |
| 611 rootObjProps[prop] = true; |
| 612 |
| 613 if (objects.indexOf(obj) < 0) { |
| 614 objects.push(obj); |
| 615 Object.observe(obj, callback); |
| 616 } |
| 617 |
| 618 observe(Object.getPrototypeOf(obj), prop); |
| 619 } |
| 620 |
| 621 function allRootObjNonObservedProps(recs) { |
| 622 for (var i = 0; i < recs.length; i++) { |
| 623 var rec = recs[i]; |
| 624 if (rec.object !== rootObj || |
| 625 rootObjProps[rec.name] || |
| 626 rec.type === 'setPrototype') { |
| 627 return false; |
| 628 } |
| 629 } |
| 630 return true; |
| 631 } |
| 632 |
| 633 function callback(recs) { |
| 634 if (allRootObjNonObservedProps(recs)) |
| 635 return; |
| 636 |
| 637 var observer; |
| 638 for (var i = 0; i < observers.length; i++) { |
| 639 observer = observers[i]; |
| 640 if (observer.state_ == OPENED) { |
| 641 observer.iterateObjects_(observe); |
| 642 } |
| 643 } |
| 644 |
| 645 for (var i = 0; i < observers.length; i++) { |
| 646 observer = observers[i]; |
| 647 if (observer.state_ == OPENED) { |
| 648 observer.check_(); |
| 649 } |
| 650 } |
| 651 } |
| 652 |
| 653 var record = { |
| 654 objects: objects, |
| 655 get rootObject() { return rootObj; }, |
| 656 set rootObject(value) { |
| 657 rootObj = value; |
| 658 rootObjProps = {}; |
| 659 }, |
| 660 open: function(obs, object) { |
| 661 observers.push(obs); |
| 662 observerCount++; |
| 663 obs.iterateObjects_(observe); |
| 664 }, |
| 665 close: function(obs) { |
| 666 observerCount--; |
| 667 if (observerCount > 0) { |
| 668 return; |
| 669 } |
| 670 |
| 671 for (var i = 0; i < objects.length; i++) { |
| 672 Object.unobserve(objects[i], callback); |
| 673 Observer.unobservedCount++; |
| 674 } |
| 675 |
| 676 observers.length = 0; |
| 677 objects.length = 0; |
| 678 rootObj = undefined; |
| 679 rootObjProps = undefined; |
| 680 observedSetCache.push(this); |
| 681 if (lastObservedSet === this) |
| 682 lastObservedSet = null; |
| 683 }, |
| 684 }; |
| 685 |
| 686 return record; |
| 687 } |
| 688 |
| 689 var lastObservedSet; |
| 690 |
| 691 function getObservedSet(observer, obj) { |
| 692 if (!lastObservedSet || lastObservedSet.rootObject !== obj) { |
| 693 lastObservedSet = observedSetCache.pop() || newObservedSet(); |
| 694 lastObservedSet.rootObject = obj; |
| 695 } |
| 696 lastObservedSet.open(observer, obj); |
| 697 return lastObservedSet; |
| 698 } |
| 699 |
| 700 var UNOPENED = 0; |
| 701 var OPENED = 1; |
| 702 var CLOSED = 2; |
| 703 var RESETTING = 3; |
| 704 |
| 705 var nextObserverId = 1; |
| 706 |
| 707 function Observer() { |
| 708 this.state_ = UNOPENED; |
| 709 this.callback_ = undefined; |
| 710 this.target_ = undefined; // TODO(rafaelw): Should be WeakRef |
| 711 this.directObserver_ = undefined; |
| 712 this.value_ = undefined; |
| 713 this.id_ = nextObserverId++; |
| 714 } |
| 715 |
| 716 Observer.prototype = { |
| 717 open: function(callback, target) { |
| 718 if (this.state_ != UNOPENED) |
| 719 throw Error('Observer has already been opened.'); |
| 720 |
| 721 addToAll(this); |
| 722 this.callback_ = callback; |
| 723 this.target_ = target; |
| 724 this.connect_(); |
| 725 this.state_ = OPENED; |
| 726 return this.value_; |
| 727 }, |
| 728 |
| 729 close: function() { |
| 730 if (this.state_ != OPENED) |
| 731 return; |
| 732 |
| 733 removeFromAll(this); |
| 734 this.disconnect_(); |
| 735 this.value_ = undefined; |
| 736 this.callback_ = undefined; |
| 737 this.target_ = undefined; |
| 738 this.state_ = CLOSED; |
| 739 }, |
| 740 |
| 741 deliver: function() { |
| 742 if (this.state_ != OPENED) |
| 743 return; |
| 744 |
| 745 dirtyCheck(this); |
| 746 }, |
| 747 |
| 748 report_: function(changes) { |
| 749 try { |
| 750 this.callback_.apply(this.target_, changes); |
| 751 } catch (ex) { |
| 752 Observer._errorThrownDuringCallback = true; |
| 753 console.error('Exception caught during observer callback: ' + |
| 754 (ex.stack || ex)); |
| 755 } |
| 756 }, |
| 757 |
| 758 discardChanges: function() { |
| 759 this.check_(undefined, true); |
| 760 return this.value_; |
| 761 } |
| 762 } |
| 763 |
| 764 var collectObservers = !hasObserve; |
| 765 var allObservers; |
| 766 Observer._allObserversCount = 0; |
| 767 |
| 768 if (collectObservers) { |
| 769 allObservers = []; |
| 770 } |
| 771 |
| 772 function addToAll(observer) { |
| 773 Observer._allObserversCount++; |
| 774 if (!collectObservers) |
| 775 return; |
| 776 |
| 777 allObservers.push(observer); |
| 778 } |
| 779 |
| 780 function removeFromAll(observer) { |
| 781 Observer._allObserversCount--; |
| 782 } |
| 783 |
| 784 var runningMicrotaskCheckpoint = false; |
| 785 |
| 786 global.Platform = global.Platform || {}; |
| 787 |
| 788 global.Platform.performMicrotaskCheckpoint = function() { |
| 789 if (runningMicrotaskCheckpoint) |
| 790 return; |
| 791 |
| 792 if (!collectObservers) |
| 793 return; |
| 794 |
| 795 runningMicrotaskCheckpoint = true; |
| 796 |
| 797 var cycles = 0; |
| 798 var anyChanged, toCheck; |
| 799 |
| 800 do { |
| 801 cycles++; |
| 802 toCheck = allObservers; |
| 803 allObservers = []; |
| 804 anyChanged = false; |
| 805 |
| 806 for (var i = 0; i < toCheck.length; i++) { |
| 807 var observer = toCheck[i]; |
| 808 if (observer.state_ != OPENED) |
| 809 continue; |
| 810 |
| 811 if (observer.check_()) |
| 812 anyChanged = true; |
| 813 |
| 814 allObservers.push(observer); |
| 815 } |
| 816 if (runEOMTasks()) |
| 817 anyChanged = true; |
| 818 } while (cycles < MAX_DIRTY_CHECK_CYCLES && anyChanged); |
| 819 |
| 820 if (testingExposeCycleCount) |
| 821 global.dirtyCheckCycleCount = cycles; |
| 822 |
| 823 runningMicrotaskCheckpoint = false; |
| 824 }; |
| 825 |
| 826 if (collectObservers) { |
| 827 global.Platform.clearObservers = function() { |
| 828 allObservers = []; |
| 829 }; |
| 830 } |
| 831 |
| 832 function ObjectObserver(object) { |
| 833 Observer.call(this); |
| 834 this.value_ = object; |
| 835 this.oldObject_ = undefined; |
| 836 } |
| 837 |
| 838 ObjectObserver.prototype = createObject({ |
| 839 __proto__: Observer.prototype, |
| 840 |
| 841 arrayObserve: false, |
| 842 |
| 843 connect_: function(callback, target) { |
| 844 if (hasObserve) { |
| 845 this.directObserver_ = getObservedObject(this, this.value_, |
| 846 this.arrayObserve); |
| 847 } else { |
| 848 this.oldObject_ = this.copyObject(this.value_); |
| 849 } |
| 850 |
| 851 }, |
| 852 |
| 853 copyObject: function(object) { |
| 854 var copy = Array.isArray(object) ? [] : {}; |
| 855 for (var prop in object) { |
| 856 copy[prop] = object[prop]; |
| 857 }; |
| 858 if (Array.isArray(object)) |
| 859 copy.length = object.length; |
| 860 return copy; |
| 861 }, |
| 862 |
| 863 check_: function(changeRecords, skipChanges) { |
| 864 var diff; |
| 865 var oldValues; |
| 866 if (hasObserve) { |
| 867 if (!changeRecords) |
| 868 return false; |
| 869 |
| 870 oldValues = {}; |
| 871 diff = diffObjectFromChangeRecords(this.value_, changeRecords, |
| 872 oldValues); |
| 873 } else { |
| 874 oldValues = this.oldObject_; |
| 875 diff = diffObjectFromOldObject(this.value_, this.oldObject_); |
| 876 } |
| 877 |
| 878 if (diffIsEmpty(diff)) |
| 879 return false; |
| 880 |
| 881 if (!hasObserve) |
| 882 this.oldObject_ = this.copyObject(this.value_); |
| 883 |
| 884 this.report_([ |
| 885 diff.added || {}, |
| 886 diff.removed || {}, |
| 887 diff.changed || {}, |
| 888 function(property) { |
| 889 return oldValues[property]; |
| 890 } |
| 891 ]); |
| 892 |
| 893 return true; |
| 894 }, |
| 895 |
| 896 disconnect_: function() { |
| 897 if (hasObserve) { |
| 898 this.directObserver_.close(); |
| 899 this.directObserver_ = undefined; |
| 900 } else { |
| 901 this.oldObject_ = undefined; |
| 902 } |
| 903 }, |
| 904 |
| 905 deliver: function() { |
| 906 if (this.state_ != OPENED) |
| 907 return; |
| 908 |
| 909 if (hasObserve) |
| 910 this.directObserver_.deliver(false); |
| 911 else |
| 912 dirtyCheck(this); |
| 913 }, |
| 914 |
| 915 discardChanges: function() { |
| 916 if (this.directObserver_) |
| 917 this.directObserver_.deliver(true); |
| 918 else |
| 919 this.oldObject_ = this.copyObject(this.value_); |
| 920 |
| 921 return this.value_; |
| 922 } |
| 923 }); |
| 924 |
| 925 function ArrayObserver(array) { |
| 926 if (!Array.isArray(array)) |
| 927 throw Error('Provided object is not an Array'); |
| 928 ObjectObserver.call(this, array); |
| 929 } |
| 930 |
| 931 ArrayObserver.prototype = createObject({ |
| 932 |
| 933 __proto__: ObjectObserver.prototype, |
| 934 |
| 935 arrayObserve: true, |
| 936 |
| 937 copyObject: function(arr) { |
| 938 return arr.slice(); |
| 939 }, |
| 940 |
| 941 check_: function(changeRecords) { |
| 942 var splices; |
| 943 if (hasObserve) { |
| 944 if (!changeRecords) |
| 945 return false; |
| 946 splices = projectArraySplices(this.value_, changeRecords); |
| 947 } else { |
| 948 splices = calcSplices(this.value_, 0, this.value_.length, |
| 949 this.oldObject_, 0, this.oldObject_.length); |
| 950 } |
| 951 |
| 952 if (!splices || !splices.length) |
| 953 return false; |
| 954 |
| 955 if (!hasObserve) |
| 956 this.oldObject_ = this.copyObject(this.value_); |
| 957 |
| 958 this.report_([splices]); |
| 959 return true; |
| 960 } |
| 961 }); |
| 962 |
| 963 ArrayObserver.applySplices = function(previous, current, splices) { |
| 964 splices.forEach(function(splice) { |
| 965 var spliceArgs = [splice.index, splice.removed.length]; |
| 966 var addIndex = splice.index; |
| 967 while (addIndex < splice.index + splice.addedCount) { |
| 968 spliceArgs.push(current[addIndex]); |
| 969 addIndex++; |
| 970 } |
| 971 |
| 972 Array.prototype.splice.apply(previous, spliceArgs); |
| 973 }); |
| 974 }; |
| 975 |
| 976 function PathObserver(object, path) { |
| 977 Observer.call(this); |
| 978 |
| 979 this.object_ = object; |
| 980 this.path_ = getPath(path); |
| 981 this.directObserver_ = undefined; |
| 982 } |
| 983 |
| 984 PathObserver.prototype = createObject({ |
| 985 __proto__: Observer.prototype, |
| 986 |
| 987 get path() { |
| 988 return this.path_; |
| 989 }, |
| 990 |
| 991 connect_: function() { |
| 992 if (hasObserve) |
| 993 this.directObserver_ = getObservedSet(this, this.object_); |
| 994 |
| 995 this.check_(undefined, true); |
| 996 }, |
| 997 |
| 998 disconnect_: function() { |
| 999 this.value_ = undefined; |
| 1000 |
| 1001 if (this.directObserver_) { |
| 1002 this.directObserver_.close(this); |
| 1003 this.directObserver_ = undefined; |
| 1004 } |
| 1005 }, |
| 1006 |
| 1007 iterateObjects_: function(observe) { |
| 1008 this.path_.iterateObjects(this.object_, observe); |
| 1009 }, |
| 1010 |
| 1011 check_: function(changeRecords, skipChanges) { |
| 1012 var oldValue = this.value_; |
| 1013 this.value_ = this.path_.getValueFrom(this.object_); |
| 1014 if (skipChanges || areSameValue(this.value_, oldValue)) |
| 1015 return false; |
| 1016 |
| 1017 this.report_([this.value_, oldValue, this]); |
| 1018 return true; |
| 1019 }, |
| 1020 |
| 1021 setValue: function(newValue) { |
| 1022 if (this.path_) |
| 1023 this.path_.setValueFrom(this.object_, newValue); |
| 1024 } |
| 1025 }); |
| 1026 |
| 1027 function CompoundObserver(reportChangesOnOpen) { |
| 1028 Observer.call(this); |
| 1029 |
| 1030 this.reportChangesOnOpen_ = reportChangesOnOpen; |
| 1031 this.value_ = []; |
| 1032 this.directObserver_ = undefined; |
| 1033 this.observed_ = []; |
| 1034 } |
| 1035 |
| 1036 var observerSentinel = {}; |
| 1037 |
| 1038 CompoundObserver.prototype = createObject({ |
| 1039 __proto__: Observer.prototype, |
| 1040 |
| 1041 connect_: function() { |
| 1042 if (hasObserve) { |
| 1043 var object; |
| 1044 var needsDirectObserver = false; |
| 1045 for (var i = 0; i < this.observed_.length; i += 2) { |
| 1046 object = this.observed_[i] |
| 1047 if (object !== observerSentinel) { |
| 1048 needsDirectObserver = true; |
| 1049 break; |
| 1050 } |
| 1051 } |
| 1052 |
| 1053 if (needsDirectObserver) |
| 1054 this.directObserver_ = getObservedSet(this, object); |
| 1055 } |
| 1056 |
| 1057 this.check_(undefined, !this.reportChangesOnOpen_); |
| 1058 }, |
| 1059 |
| 1060 disconnect_: function() { |
| 1061 for (var i = 0; i < this.observed_.length; i += 2) { |
| 1062 if (this.observed_[i] === observerSentinel) |
| 1063 this.observed_[i + 1].close(); |
| 1064 } |
| 1065 this.observed_.length = 0; |
| 1066 this.value_.length = 0; |
| 1067 |
| 1068 if (this.directObserver_) { |
| 1069 this.directObserver_.close(this); |
| 1070 this.directObserver_ = undefined; |
| 1071 } |
| 1072 }, |
| 1073 |
| 1074 addPath: function(object, path) { |
| 1075 if (this.state_ != UNOPENED && this.state_ != RESETTING) |
| 1076 throw Error('Cannot add paths once started.'); |
| 1077 |
| 1078 var path = getPath(path); |
| 1079 this.observed_.push(object, path); |
| 1080 if (!this.reportChangesOnOpen_) |
| 1081 return; |
| 1082 var index = this.observed_.length / 2 - 1; |
| 1083 this.value_[index] = path.getValueFrom(object); |
| 1084 }, |
| 1085 |
| 1086 addObserver: function(observer) { |
| 1087 if (this.state_ != UNOPENED && this.state_ != RESETTING) |
| 1088 throw Error('Cannot add observers once started.'); |
| 1089 |
| 1090 this.observed_.push(observerSentinel, observer); |
| 1091 if (!this.reportChangesOnOpen_) |
| 1092 return; |
| 1093 var index = this.observed_.length / 2 - 1; |
| 1094 this.value_[index] = observer.open(this.deliver, this); |
| 1095 }, |
| 1096 |
| 1097 startReset: function() { |
| 1098 if (this.state_ != OPENED) |
| 1099 throw Error('Can only reset while open'); |
| 1100 |
| 1101 this.state_ = RESETTING; |
| 1102 this.disconnect_(); |
| 1103 }, |
| 1104 |
| 1105 finishReset: function() { |
| 1106 if (this.state_ != RESETTING) |
| 1107 throw Error('Can only finishReset after startReset'); |
| 1108 this.state_ = OPENED; |
| 1109 this.connect_(); |
| 1110 |
| 1111 return this.value_; |
| 1112 }, |
| 1113 |
| 1114 iterateObjects_: function(observe) { |
| 1115 var object; |
| 1116 for (var i = 0; i < this.observed_.length; i += 2) { |
| 1117 object = this.observed_[i] |
| 1118 if (object !== observerSentinel) |
| 1119 this.observed_[i + 1].iterateObjects(object, observe) |
| 1120 } |
| 1121 }, |
| 1122 |
| 1123 check_: function(changeRecords, skipChanges) { |
| 1124 var oldValues; |
| 1125 for (var i = 0; i < this.observed_.length; i += 2) { |
| 1126 var object = this.observed_[i]; |
| 1127 var path = this.observed_[i+1]; |
| 1128 var value; |
| 1129 if (object === observerSentinel) { |
| 1130 var observable = path; |
| 1131 value = this.state_ === UNOPENED ? |
| 1132 observable.open(this.deliver, this) : |
| 1133 observable.discardChanges(); |
| 1134 } else { |
| 1135 value = path.getValueFrom(object); |
| 1136 } |
| 1137 |
| 1138 if (skipChanges) { |
| 1139 this.value_[i / 2] = value; |
| 1140 continue; |
| 1141 } |
| 1142 |
| 1143 if (areSameValue(value, this.value_[i / 2])) |
| 1144 continue; |
| 1145 |
| 1146 oldValues = oldValues || []; |
| 1147 oldValues[i / 2] = this.value_[i / 2]; |
| 1148 this.value_[i / 2] = value; |
| 1149 } |
| 1150 |
| 1151 if (!oldValues) |
| 1152 return false; |
| 1153 |
| 1154 // TODO(rafaelw): Having observed_ as the third callback arg here is |
| 1155 // pretty lame API. Fix. |
| 1156 this.report_([this.value_, oldValues, this.observed_]); |
| 1157 return true; |
| 1158 } |
| 1159 }); |
| 1160 |
| 1161 function identFn(value) { return value; } |
| 1162 |
| 1163 function ObserverTransform(observable, getValueFn, setValueFn, |
| 1164 dontPassThroughSet) { |
| 1165 this.callback_ = undefined; |
| 1166 this.target_ = undefined; |
| 1167 this.value_ = undefined; |
| 1168 this.observable_ = observable; |
| 1169 this.getValueFn_ = getValueFn || identFn; |
| 1170 this.setValueFn_ = setValueFn || identFn; |
| 1171 // TODO(rafaelw): This is a temporary hack. PolymerExpressions needs this |
| 1172 // at the moment because of a bug in it's dependency tracking. |
| 1173 this.dontPassThroughSet_ = dontPassThroughSet; |
| 1174 } |
| 1175 |
| 1176 ObserverTransform.prototype = { |
| 1177 open: function(callback, target) { |
| 1178 this.callback_ = callback; |
| 1179 this.target_ = target; |
| 1180 this.value_ = |
| 1181 this.getValueFn_(this.observable_.open(this.observedCallback_, this)); |
| 1182 return this.value_; |
| 1183 }, |
| 1184 |
| 1185 observedCallback_: function(value) { |
| 1186 value = this.getValueFn_(value); |
| 1187 if (areSameValue(value, this.value_)) |
| 1188 return; |
| 1189 var oldValue = this.value_; |
| 1190 this.value_ = value; |
| 1191 this.callback_.call(this.target_, this.value_, oldValue); |
| 1192 }, |
| 1193 |
| 1194 discardChanges: function() { |
| 1195 this.value_ = this.getValueFn_(this.observable_.discardChanges()); |
| 1196 return this.value_; |
| 1197 }, |
| 1198 |
| 1199 deliver: function() { |
| 1200 return this.observable_.deliver(); |
| 1201 }, |
| 1202 |
| 1203 setValue: function(value) { |
| 1204 value = this.setValueFn_(value); |
| 1205 if (!this.dontPassThroughSet_ && this.observable_.setValue) |
| 1206 return this.observable_.setValue(value); |
| 1207 }, |
| 1208 |
| 1209 close: function() { |
| 1210 if (this.observable_) |
| 1211 this.observable_.close(); |
| 1212 this.callback_ = undefined; |
| 1213 this.target_ = undefined; |
| 1214 this.observable_ = undefined; |
| 1215 this.value_ = undefined; |
| 1216 this.getValueFn_ = undefined; |
| 1217 this.setValueFn_ = undefined; |
| 1218 } |
| 1219 } |
| 1220 |
| 1221 var expectedRecordTypes = { |
| 1222 add: true, |
| 1223 update: true, |
| 1224 delete: true |
| 1225 }; |
| 1226 |
| 1227 function diffObjectFromChangeRecords(object, changeRecords, oldValues) { |
| 1228 var added = {}; |
| 1229 var removed = {}; |
| 1230 |
| 1231 for (var i = 0; i < changeRecords.length; i++) { |
| 1232 var record = changeRecords[i]; |
| 1233 if (!expectedRecordTypes[record.type]) { |
| 1234 console.error('Unknown changeRecord type: ' + record.type); |
| 1235 console.error(record); |
| 1236 continue; |
| 1237 } |
| 1238 |
| 1239 if (!(record.name in oldValues)) |
| 1240 oldValues[record.name] = record.oldValue; |
| 1241 |
| 1242 if (record.type == 'update') |
| 1243 continue; |
| 1244 |
| 1245 if (record.type == 'add') { |
| 1246 if (record.name in removed) |
| 1247 delete removed[record.name]; |
| 1248 else |
| 1249 added[record.name] = true; |
| 1250 |
| 1251 continue; |
| 1252 } |
| 1253 |
| 1254 // type = 'delete' |
| 1255 if (record.name in added) { |
| 1256 delete added[record.name]; |
| 1257 delete oldValues[record.name]; |
| 1258 } else { |
| 1259 removed[record.name] = true; |
| 1260 } |
| 1261 } |
| 1262 |
| 1263 for (var prop in added) |
| 1264 added[prop] = object[prop]; |
| 1265 |
| 1266 for (var prop in removed) |
| 1267 removed[prop] = undefined; |
| 1268 |
| 1269 var changed = {}; |
| 1270 for (var prop in oldValues) { |
| 1271 if (prop in added || prop in removed) |
| 1272 continue; |
| 1273 |
| 1274 var newValue = object[prop]; |
| 1275 if (oldValues[prop] !== newValue) |
| 1276 changed[prop] = newValue; |
| 1277 } |
| 1278 |
| 1279 return { |
| 1280 added: added, |
| 1281 removed: removed, |
| 1282 changed: changed |
| 1283 }; |
| 1284 } |
| 1285 |
| 1286 function newSplice(index, removed, addedCount) { |
| 1287 return { |
| 1288 index: index, |
| 1289 removed: removed, |
| 1290 addedCount: addedCount |
| 1291 }; |
| 1292 } |
| 1293 |
| 1294 var EDIT_LEAVE = 0; |
| 1295 var EDIT_UPDATE = 1; |
| 1296 var EDIT_ADD = 2; |
| 1297 var EDIT_DELETE = 3; |
| 1298 |
| 1299 function ArraySplice() {} |
| 1300 |
| 1301 ArraySplice.prototype = { |
| 1302 |
| 1303 // Note: This function is *based* on the computation of the Levenshtein |
| 1304 // "edit" distance. The one change is that "updates" are treated as two |
| 1305 // edits - not one. With Array splices, an update is really a delete |
| 1306 // followed by an add. By retaining this, we optimize for "keeping" the |
| 1307 // maximum array items in the original array. For example: |
| 1308 // |
| 1309 // 'xxxx123' -> '123yyyy' |
| 1310 // |
| 1311 // With 1-edit updates, the shortest path would be just to update all seven |
| 1312 // characters. With 2-edit updates, we delete 4, leave 3, and add 4. This |
| 1313 // leaves the substring '123' intact. |
| 1314 calcEditDistances: function(current, currentStart, currentEnd, |
| 1315 old, oldStart, oldEnd) { |
| 1316 // "Deletion" columns |
| 1317 var rowCount = oldEnd - oldStart + 1; |
| 1318 var columnCount = currentEnd - currentStart + 1; |
| 1319 var distances = new Array(rowCount); |
| 1320 |
| 1321 // "Addition" rows. Initialize null column. |
| 1322 for (var i = 0; i < rowCount; i++) { |
| 1323 distances[i] = new Array(columnCount); |
| 1324 distances[i][0] = i; |
| 1325 } |
| 1326 |
| 1327 // Initialize null row |
| 1328 for (var j = 0; j < columnCount; j++) |
| 1329 distances[0][j] = j; |
| 1330 |
| 1331 for (var i = 1; i < rowCount; i++) { |
| 1332 for (var j = 1; j < columnCount; j++) { |
| 1333 if (this.equals(current[currentStart + j - 1], old[oldStart + i - 1])) |
| 1334 distances[i][j] = distances[i - 1][j - 1]; |
| 1335 else { |
| 1336 var north = distances[i - 1][j] + 1; |
| 1337 var west = distances[i][j - 1] + 1; |
| 1338 distances[i][j] = north < west ? north : west; |
| 1339 } |
| 1340 } |
| 1341 } |
| 1342 |
| 1343 return distances; |
| 1344 }, |
| 1345 |
| 1346 // This starts at the final weight, and walks "backward" by finding |
| 1347 // the minimum previous weight recursively until the origin of the weight |
| 1348 // matrix. |
| 1349 spliceOperationsFromEditDistances: function(distances) { |
| 1350 var i = distances.length - 1; |
| 1351 var j = distances[0].length - 1; |
| 1352 var current = distances[i][j]; |
| 1353 var edits = []; |
| 1354 while (i > 0 || j > 0) { |
| 1355 if (i == 0) { |
| 1356 edits.push(EDIT_ADD); |
| 1357 j--; |
| 1358 continue; |
| 1359 } |
| 1360 if (j == 0) { |
| 1361 edits.push(EDIT_DELETE); |
| 1362 i--; |
| 1363 continue; |
| 1364 } |
| 1365 var northWest = distances[i - 1][j - 1]; |
| 1366 var west = distances[i - 1][j]; |
| 1367 var north = distances[i][j - 1]; |
| 1368 |
| 1369 var min; |
| 1370 if (west < north) |
| 1371 min = west < northWest ? west : northWest; |
| 1372 else |
| 1373 min = north < northWest ? north : northWest; |
| 1374 |
| 1375 if (min == northWest) { |
| 1376 if (northWest == current) { |
| 1377 edits.push(EDIT_LEAVE); |
| 1378 } else { |
| 1379 edits.push(EDIT_UPDATE); |
| 1380 current = northWest; |
| 1381 } |
| 1382 i--; |
| 1383 j--; |
| 1384 } else if (min == west) { |
| 1385 edits.push(EDIT_DELETE); |
| 1386 i--; |
| 1387 current = west; |
| 1388 } else { |
| 1389 edits.push(EDIT_ADD); |
| 1390 j--; |
| 1391 current = north; |
| 1392 } |
| 1393 } |
| 1394 |
| 1395 edits.reverse(); |
| 1396 return edits; |
| 1397 }, |
| 1398 |
| 1399 /** |
| 1400 * Splice Projection functions: |
| 1401 * |
| 1402 * A splice map is a representation of how a previous array of items |
| 1403 * was transformed into a new array of items. Conceptually it is a list of |
| 1404 * tuples of |
| 1405 * |
| 1406 * <index, removed, addedCount> |
| 1407 * |
| 1408 * which are kept in ascending index order of. The tuple represents that at |
| 1409 * the |index|, |removed| sequence of items were removed, and counting forwa
rd |
| 1410 * from |index|, |addedCount| items were added. |
| 1411 */ |
| 1412 |
| 1413 /** |
| 1414 * Lacking individual splice mutation information, the minimal set of |
| 1415 * splices can be synthesized given the previous state and final state of an |
| 1416 * array. The basic approach is to calculate the edit distance matrix and |
| 1417 * choose the shortest path through it. |
| 1418 * |
| 1419 * Complexity: O(l * p) |
| 1420 * l: The length of the current array |
| 1421 * p: The length of the old array |
| 1422 */ |
| 1423 calcSplices: function(current, currentStart, currentEnd, |
| 1424 old, oldStart, oldEnd) { |
| 1425 var prefixCount = 0; |
| 1426 var suffixCount = 0; |
| 1427 |
| 1428 var minLength = Math.min(currentEnd - currentStart, oldEnd - oldStart); |
| 1429 if (currentStart == 0 && oldStart == 0) |
| 1430 prefixCount = this.sharedPrefix(current, old, minLength); |
| 1431 |
| 1432 if (currentEnd == current.length && oldEnd == old.length) |
| 1433 suffixCount = this.sharedSuffix(current, old, minLength - prefixCount); |
| 1434 |
| 1435 currentStart += prefixCount; |
| 1436 oldStart += prefixCount; |
| 1437 currentEnd -= suffixCount; |
| 1438 oldEnd -= suffixCount; |
| 1439 |
| 1440 if (currentEnd - currentStart == 0 && oldEnd - oldStart == 0) |
| 1441 return []; |
| 1442 |
| 1443 if (currentStart == currentEnd) { |
| 1444 var splice = newSplice(currentStart, [], 0); |
| 1445 while (oldStart < oldEnd) |
| 1446 splice.removed.push(old[oldStart++]); |
| 1447 |
| 1448 return [ splice ]; |
| 1449 } else if (oldStart == oldEnd) |
| 1450 return [ newSplice(currentStart, [], currentEnd - currentStart) ]; |
| 1451 |
| 1452 var ops = this.spliceOperationsFromEditDistances( |
| 1453 this.calcEditDistances(current, currentStart, currentEnd, |
| 1454 old, oldStart, oldEnd)); |
| 1455 |
| 1456 var splice = undefined; |
| 1457 var splices = []; |
| 1458 var index = currentStart; |
| 1459 var oldIndex = oldStart; |
| 1460 for (var i = 0; i < ops.length; i++) { |
| 1461 switch(ops[i]) { |
| 1462 case EDIT_LEAVE: |
| 1463 if (splice) { |
| 1464 splices.push(splice); |
| 1465 splice = undefined; |
| 1466 } |
| 1467 |
| 1468 index++; |
| 1469 oldIndex++; |
| 1470 break; |
| 1471 case EDIT_UPDATE: |
| 1472 if (!splice) |
| 1473 splice = newSplice(index, [], 0); |
| 1474 |
| 1475 splice.addedCount++; |
| 1476 index++; |
| 1477 |
| 1478 splice.removed.push(old[oldIndex]); |
| 1479 oldIndex++; |
| 1480 break; |
| 1481 case EDIT_ADD: |
| 1482 if (!splice) |
| 1483 splice = newSplice(index, [], 0); |
| 1484 |
| 1485 splice.addedCount++; |
| 1486 index++; |
| 1487 break; |
| 1488 case EDIT_DELETE: |
| 1489 if (!splice) |
| 1490 splice = newSplice(index, [], 0); |
| 1491 |
| 1492 splice.removed.push(old[oldIndex]); |
| 1493 oldIndex++; |
| 1494 break; |
| 1495 } |
| 1496 } |
| 1497 |
| 1498 if (splice) { |
| 1499 splices.push(splice); |
| 1500 } |
| 1501 return splices; |
| 1502 }, |
| 1503 |
| 1504 sharedPrefix: function(current, old, searchLength) { |
| 1505 for (var i = 0; i < searchLength; i++) |
| 1506 if (!this.equals(current[i], old[i])) |
| 1507 return i; |
| 1508 return searchLength; |
| 1509 }, |
| 1510 |
| 1511 sharedSuffix: function(current, old, searchLength) { |
| 1512 var index1 = current.length; |
| 1513 var index2 = old.length; |
| 1514 var count = 0; |
| 1515 while (count < searchLength && this.equals(current[--index1], old[--index2
])) |
| 1516 count++; |
| 1517 |
| 1518 return count; |
| 1519 }, |
| 1520 |
| 1521 calculateSplices: function(current, previous) { |
| 1522 return this.calcSplices(current, 0, current.length, previous, 0, |
| 1523 previous.length); |
| 1524 }, |
| 1525 |
| 1526 equals: function(currentValue, previousValue) { |
| 1527 return currentValue === previousValue; |
| 1528 } |
| 1529 }; |
| 1530 |
| 1531 var arraySplice = new ArraySplice(); |
| 1532 |
| 1533 function calcSplices(current, currentStart, currentEnd, |
| 1534 old, oldStart, oldEnd) { |
| 1535 return arraySplice.calcSplices(current, currentStart, currentEnd, |
| 1536 old, oldStart, oldEnd); |
| 1537 } |
| 1538 |
| 1539 function intersect(start1, end1, start2, end2) { |
| 1540 // Disjoint |
| 1541 if (end1 < start2 || end2 < start1) |
| 1542 return -1; |
| 1543 |
| 1544 // Adjacent |
| 1545 if (end1 == start2 || end2 == start1) |
| 1546 return 0; |
| 1547 |
| 1548 // Non-zero intersect, span1 first |
| 1549 if (start1 < start2) { |
| 1550 if (end1 < end2) |
| 1551 return end1 - start2; // Overlap |
| 1552 else |
| 1553 return end2 - start2; // Contained |
| 1554 } else { |
| 1555 // Non-zero intersect, span2 first |
| 1556 if (end2 < end1) |
| 1557 return end2 - start1; // Overlap |
| 1558 else |
| 1559 return end1 - start1; // Contained |
| 1560 } |
| 1561 } |
| 1562 |
| 1563 function mergeSplice(splices, index, removed, addedCount) { |
| 1564 |
| 1565 var splice = newSplice(index, removed, addedCount); |
| 1566 |
| 1567 var inserted = false; |
| 1568 var insertionOffset = 0; |
| 1569 |
| 1570 for (var i = 0; i < splices.length; i++) { |
| 1571 var current = splices[i]; |
| 1572 current.index += insertionOffset; |
| 1573 |
| 1574 if (inserted) |
| 1575 continue; |
| 1576 |
| 1577 var intersectCount = intersect(splice.index, |
| 1578 splice.index + splice.removed.length, |
| 1579 current.index, |
| 1580 current.index + current.addedCount); |
| 1581 |
| 1582 if (intersectCount >= 0) { |
| 1583 // Merge the two splices |
| 1584 |
| 1585 splices.splice(i, 1); |
| 1586 i--; |
| 1587 |
| 1588 insertionOffset -= current.addedCount - current.removed.length; |
| 1589 |
| 1590 splice.addedCount += current.addedCount - intersectCount; |
| 1591 var deleteCount = splice.removed.length + |
| 1592 current.removed.length - intersectCount; |
| 1593 |
| 1594 if (!splice.addedCount && !deleteCount) { |
| 1595 // merged splice is a noop. discard. |
| 1596 inserted = true; |
| 1597 } else { |
| 1598 var removed = current.removed; |
| 1599 |
| 1600 if (splice.index < current.index) { |
| 1601 // some prefix of splice.removed is prepended to current.removed. |
| 1602 var prepend = splice.removed.slice(0, current.index - splice.index); |
| 1603 Array.prototype.push.apply(prepend, removed); |
| 1604 removed = prepend; |
| 1605 } |
| 1606 |
| 1607 if (splice.index + splice.removed.length > current.index + current.add
edCount) { |
| 1608 // some suffix of splice.removed is appended to current.removed. |
| 1609 var append = splice.removed.slice(current.index + current.addedCount
- splice.index); |
| 1610 Array.prototype.push.apply(removed, append); |
| 1611 } |
| 1612 |
| 1613 splice.removed = removed; |
| 1614 if (current.index < splice.index) { |
| 1615 splice.index = current.index; |
| 1616 } |
| 1617 } |
| 1618 } else if (splice.index < current.index) { |
| 1619 // Insert splice here. |
| 1620 |
| 1621 inserted = true; |
| 1622 |
| 1623 splices.splice(i, 0, splice); |
| 1624 i++; |
| 1625 |
| 1626 var offset = splice.addedCount - splice.removed.length |
| 1627 current.index += offset; |
| 1628 insertionOffset += offset; |
| 1629 } |
| 1630 } |
| 1631 |
| 1632 if (!inserted) |
| 1633 splices.push(splice); |
| 1634 } |
| 1635 |
| 1636 function createInitialSplices(array, changeRecords) { |
| 1637 var splices = []; |
| 1638 |
| 1639 for (var i = 0; i < changeRecords.length; i++) { |
| 1640 var record = changeRecords[i]; |
| 1641 switch(record.type) { |
| 1642 case 'splice': |
| 1643 mergeSplice(splices, record.index, record.removed.slice(), record.adde
dCount); |
| 1644 break; |
| 1645 case 'add': |
| 1646 case 'update': |
| 1647 case 'delete': |
| 1648 if (!isIndex(record.name)) |
| 1649 continue; |
| 1650 var index = toNumber(record.name); |
| 1651 if (index < 0) |
| 1652 continue; |
| 1653 mergeSplice(splices, index, [record.oldValue], 1); |
| 1654 break; |
| 1655 default: |
| 1656 console.error('Unexpected record type: ' + JSON.stringify(record)); |
| 1657 break; |
| 1658 } |
| 1659 } |
| 1660 |
| 1661 return splices; |
| 1662 } |
| 1663 |
| 1664 function projectArraySplices(array, changeRecords) { |
| 1665 var splices = []; |
| 1666 |
| 1667 createInitialSplices(array, changeRecords).forEach(function(splice) { |
| 1668 if (splice.addedCount == 1 && splice.removed.length == 1) { |
| 1669 if (splice.removed[0] !== array[splice.index]) |
| 1670 splices.push(splice); |
| 1671 |
| 1672 return |
| 1673 }; |
| 1674 |
| 1675 splices = splices.concat(calcSplices(array, splice.index, splice.index + s
plice.addedCount, |
| 1676 splice.removed, 0, splice.removed.len
gth)); |
| 1677 }); |
| 1678 |
| 1679 return splices; |
| 1680 } |
| 1681 |
| 1682 // Export the observe-js object for **Node.js**, with |
| 1683 // backwards-compatibility for the old `require()` API. If we're in |
| 1684 // the browser, export as a global object. |
| 1685 |
| 1686 var expose = global; |
| 1687 |
| 1688 if (typeof exports !== 'undefined') { |
| 1689 if (typeof module !== 'undefined' && module.exports) { |
| 1690 expose = exports = module.exports; |
| 1691 } |
| 1692 expose = exports; |
| 1693 } |
| 1694 |
| 1695 expose.Observer = Observer; |
| 1696 expose.Observer.runEOM_ = runEOM; |
| 1697 expose.Observer.observerSentinel_ = observerSentinel; // for testing. |
| 1698 expose.Observer.hasObjectObserve = hasObserve; |
| 1699 expose.ArrayObserver = ArrayObserver; |
| 1700 expose.ArrayObserver.calculateSplices = function(current, previous) { |
| 1701 return arraySplice.calculateSplices(current, previous); |
| 1702 }; |
| 1703 |
| 1704 expose.ArraySplice = ArraySplice; |
| 1705 expose.ObjectObserver = ObjectObserver; |
| 1706 expose.PathObserver = PathObserver; |
| 1707 expose.CompoundObserver = CompoundObserver; |
| 1708 expose.Path = Path; |
| 1709 expose.ObserverTransform = ObserverTransform; |
| 1710 |
| 1711 })(typeof global !== 'undefined' && global && typeof module !== 'undefined' && m
odule ? global : this || window); |
OLD | NEW |