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