| 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 setValue: function(newValue) { | |
| 605 }, | |
| 606 | |
| 607 close: function() { | |
| 608 if (this.state_ != OPENED) | |
| 609 return; | |
| 610 | |
| 611 this.disconnect_(); | |
| 612 this.value_ = undefined; | |
| 613 this.callback_ = undefined; | |
| 614 this.target_ = undefined; | |
| 615 this.state_ = CLOSED; | |
| 616 }, | |
| 617 | |
| 618 deliver: function() { | |
| 619 if (this.state_ != OPENED) | |
| 620 return; | |
| 621 | |
| 622 dirtyCheck(this); | |
| 623 }, | |
| 624 | |
| 625 report_: function(changes) { | |
| 626 try { | |
| 627 this.callback_.apply(this.target_, changes); | |
| 628 } catch (ex) { | |
| 629 Observer._errorThrownDuringCallback = true; | |
| 630 console.error('Exception caught during observer callback: ' + | |
| 631 (ex.stack || ex)); | |
| 632 } | |
| 633 }, | |
| 634 | |
| 635 discardChanges: function() { | |
| 636 this.check_(undefined, true); | |
| 637 return this.value_; | |
| 638 } | |
| 639 } | |
| 640 | |
| 641 function ArrayObserver(array) { | |
| 642 if (!Array.isArray(array)) | |
| 643 throw Error('Provided object is not an Array'); | |
| 644 Observer.call(this); | |
| 645 this.value_ = array; | |
| 646 this.oldObject_ = undefined; | |
| 647 } | |
| 648 | |
| 649 ArrayObserver.prototype = createObject({ | |
| 650 | |
| 651 __proto__: Observer.prototype, | |
| 652 | |
| 653 connect_: function(callback, target) { | |
| 654 this.directObserver_ = getObservedObject(this, this.value_, | |
| 655 true /* arrayObserve */); | |
| 656 }, | |
| 657 | |
| 658 copyObject: function(arr) { | |
| 659 return arr.slice(); | |
| 660 }, | |
| 661 | |
| 662 check_: function(changeRecords) { | |
| 663 var splices; | |
| 664 if (!changeRecords) | |
| 665 return false; | |
| 666 splices = projectArraySplices(this.value_, changeRecords); | |
| 667 | |
| 668 if (!splices || !splices.length) | |
| 669 return false; | |
| 670 | |
| 671 this.report_([splices]); | |
| 672 return true; | |
| 673 }, | |
| 674 | |
| 675 disconnect_: function() { | |
| 676 this.directObserver_.close(); | |
| 677 this.directObserver_ = undefined; | |
| 678 }, | |
| 679 | |
| 680 deliver: function() { | |
| 681 if (this.state_ != OPENED) | |
| 682 return; | |
| 683 | |
| 684 this.directObserver_.deliver(false); | |
| 685 }, | |
| 686 | |
| 687 discardChanges: function() { | |
| 688 if (this.directObserver_) | |
| 689 this.directObserver_.deliver(true); | |
| 690 else | |
| 691 this.oldObject_ = this.copyObject(this.value_); | |
| 692 | |
| 693 return this.value_; | |
| 694 } | |
| 695 }); | |
| 696 | |
| 697 ArrayObserver.applySplices = function(previous, current, splices) { | |
| 698 splices.forEach(function(splice) { | |
| 699 var spliceArgs = [splice.index, splice.removed.length]; | |
| 700 var addIndex = splice.index; | |
| 701 while (addIndex < splice.index + splice.addedCount) { | |
| 702 spliceArgs.push(current[addIndex]); | |
| 703 addIndex++; | |
| 704 } | |
| 705 | |
| 706 Array.prototype.splice.apply(previous, spliceArgs); | |
| 707 }); | |
| 708 }; | |
| 709 | |
| 710 function PathObserver(object, path) { | |
| 711 Observer.call(this); | |
| 712 | |
| 713 this.object_ = object; | |
| 714 this.path_ = getPath(path); | |
| 715 this.directObserver_ = undefined; | |
| 716 } | |
| 717 | |
| 718 PathObserver.prototype = createObject({ | |
| 719 __proto__: Observer.prototype, | |
| 720 | |
| 721 get path() { | |
| 722 return this.path_; | |
| 723 }, | |
| 724 | |
| 725 connect_: function() { | |
| 726 this.directObserver_ = getObservedSet(this, this.object_); | |
| 727 this.check_(undefined, true); | |
| 728 }, | |
| 729 | |
| 730 disconnect_: function() { | |
| 731 this.value_ = undefined; | |
| 732 | |
| 733 if (this.directObserver_) { | |
| 734 this.directObserver_.close(this); | |
| 735 this.directObserver_ = undefined; | |
| 736 } | |
| 737 }, | |
| 738 | |
| 739 iterateObjects_: function(observe) { | |
| 740 this.path_.iterateObjects(this.object_, observe); | |
| 741 }, | |
| 742 | |
| 743 check_: function(changeRecords, skipChanges) { | |
| 744 var oldValue = this.value_; | |
| 745 this.value_ = this.path_.getValueFrom(this.object_); | |
| 746 if (skipChanges || areSameValue(this.value_, oldValue)) | |
| 747 return false; | |
| 748 | |
| 749 this.report_([this.value_, oldValue, this]); | |
| 750 return true; | |
| 751 }, | |
| 752 | |
| 753 setValue: function(newValue) { | |
| 754 if (this.path_) | |
| 755 this.path_.setValueFrom(this.object_, newValue); | |
| 756 } | |
| 757 }); | |
| 758 | |
| 759 function CompoundObserver(reportChangesOnOpen) { | |
| 760 Observer.call(this); | |
| 761 | |
| 762 this.reportChangesOnOpen_ = reportChangesOnOpen; | |
| 763 this.value_ = []; | |
| 764 this.directObserver_ = undefined; | |
| 765 this.observed_ = []; | |
| 766 } | |
| 767 | |
| 768 var observerSentinel = {}; | |
| 769 | |
| 770 CompoundObserver.prototype = createObject({ | |
| 771 __proto__: Observer.prototype, | |
| 772 | |
| 773 connect_: function() { | |
| 774 var object; | |
| 775 var needsDirectObserver = false; | |
| 776 for (var i = 0; i < this.observed_.length; i += 2) { | |
| 777 object = this.observed_[i] | |
| 778 if (object !== observerSentinel) { | |
| 779 needsDirectObserver = true; | |
| 780 break; | |
| 781 } | |
| 782 } | |
| 783 | |
| 784 if (needsDirectObserver) | |
| 785 this.directObserver_ = getObservedSet(this, object); | |
| 786 | |
| 787 this.check_(undefined, !this.reportChangesOnOpen_); | |
| 788 }, | |
| 789 | |
| 790 disconnect_: function() { | |
| 791 for (var i = 0; i < this.observed_.length; i += 2) { | |
| 792 if (this.observed_[i] === observerSentinel) | |
| 793 this.observed_[i + 1].close(); | |
| 794 } | |
| 795 this.observed_.length = 0; | |
| 796 this.value_.length = 0; | |
| 797 | |
| 798 if (this.directObserver_) { | |
| 799 this.directObserver_.close(this); | |
| 800 this.directObserver_ = undefined; | |
| 801 } | |
| 802 }, | |
| 803 | |
| 804 addPath: function(object, path) { | |
| 805 if (this.state_ != UNOPENED && this.state_ != RESETTING) | |
| 806 throw Error('Cannot add paths once started.'); | |
| 807 | |
| 808 var path = getPath(path); | |
| 809 this.observed_.push(object, path); | |
| 810 if (!this.reportChangesOnOpen_) | |
| 811 return; | |
| 812 var index = this.observed_.length / 2 - 1; | |
| 813 this.value_[index] = path.getValueFrom(object); | |
| 814 }, | |
| 815 | |
| 816 addObserver: function(observer) { | |
| 817 if (this.state_ != UNOPENED && this.state_ != RESETTING) | |
| 818 throw Error('Cannot add observers once started.'); | |
| 819 | |
| 820 this.observed_.push(observerSentinel, observer); | |
| 821 if (!this.reportChangesOnOpen_) | |
| 822 return; | |
| 823 var index = this.observed_.length / 2 - 1; | |
| 824 this.value_[index] = observer.open(this.deliver, this); | |
| 825 }, | |
| 826 | |
| 827 startReset: function() { | |
| 828 if (this.state_ != OPENED) | |
| 829 throw Error('Can only reset while open'); | |
| 830 | |
| 831 this.state_ = RESETTING; | |
| 832 this.disconnect_(); | |
| 833 }, | |
| 834 | |
| 835 finishReset: function() { | |
| 836 if (this.state_ != RESETTING) | |
| 837 throw Error('Can only finishReset after startReset'); | |
| 838 this.state_ = OPENED; | |
| 839 this.connect_(); | |
| 840 | |
| 841 return this.value_; | |
| 842 }, | |
| 843 | |
| 844 iterateObjects_: function(observe) { | |
| 845 var object; | |
| 846 for (var i = 0; i < this.observed_.length; i += 2) { | |
| 847 object = this.observed_[i] | |
| 848 if (object !== observerSentinel) | |
| 849 this.observed_[i + 1].iterateObjects(object, observe) | |
| 850 } | |
| 851 }, | |
| 852 | |
| 853 check_: function(changeRecords, skipChanges) { | |
| 854 var oldValues; | |
| 855 for (var i = 0; i < this.observed_.length; i += 2) { | |
| 856 var object = this.observed_[i]; | |
| 857 var path = this.observed_[i+1]; | |
| 858 var value; | |
| 859 if (object === observerSentinel) { | |
| 860 var observable = path; | |
| 861 value = this.state_ === UNOPENED ? | |
| 862 observable.open(this.deliver, this) : | |
| 863 observable.discardChanges(); | |
| 864 } else { | |
| 865 value = path.getValueFrom(object); | |
| 866 } | |
| 867 | |
| 868 if (skipChanges) { | |
| 869 this.value_[i / 2] = value; | |
| 870 continue; | |
| 871 } | |
| 872 | |
| 873 if (areSameValue(value, this.value_[i / 2])) | |
| 874 continue; | |
| 875 | |
| 876 oldValues = oldValues || []; | |
| 877 oldValues[i / 2] = this.value_[i / 2]; | |
| 878 this.value_[i / 2] = value; | |
| 879 } | |
| 880 | |
| 881 if (!oldValues) | |
| 882 return false; | |
| 883 | |
| 884 // TODO(rafaelw): Having observed_ as the third callback arg here is | |
| 885 // pretty lame API. Fix. | |
| 886 this.report_([this.value_, oldValues, this.observed_]); | |
| 887 return true; | |
| 888 } | |
| 889 }); | |
| 890 | |
| 891 function identFn(value) { return value; } | |
| 892 | |
| 893 function ObserverTransform(observable, getValueFn) { | |
| 894 this.callback_ = undefined; | |
| 895 this.target_ = undefined; | |
| 896 this.value_ = undefined; | |
| 897 this.observable_ = observable; | |
| 898 this.getValueFn_ = getValueFn || identFn; | |
| 899 } | |
| 900 | |
| 901 ObserverTransform.prototype = { | |
| 902 open: function(callback, target) { | |
| 903 this.callback_ = callback; | |
| 904 this.target_ = target; | |
| 905 this.value_ = | |
| 906 this.getValueFn_(this.observable_.open(this.observedCallback_, this)); | |
| 907 return this.value_; | |
| 908 }, | |
| 909 | |
| 910 observedCallback_: function(value) { | |
| 911 value = this.getValueFn_(value); | |
| 912 if (areSameValue(value, this.value_)) | |
| 913 return; | |
| 914 var oldValue = this.value_; | |
| 915 this.value_ = value; | |
| 916 this.callback_.call(this.target_, this.value_, oldValue); | |
| 917 }, | |
| 918 | |
| 919 setValue: function(oldValue) { | |
| 920 }, | |
| 921 | |
| 922 discardChanges: function() { | |
| 923 this.value_ = this.getValueFn_(this.observable_.discardChanges()); | |
| 924 return this.value_; | |
| 925 }, | |
| 926 | |
| 927 deliver: function() { | |
| 928 return this.observable_.deliver(); | |
| 929 }, | |
| 930 | |
| 931 close: function() { | |
| 932 if (this.observable_) | |
| 933 this.observable_.close(); | |
| 934 this.callback_ = undefined; | |
| 935 this.target_ = undefined; | |
| 936 this.observable_ = undefined; | |
| 937 this.value_ = undefined; | |
| 938 this.getValueFn_ = undefined; | |
| 939 } | |
| 940 } | |
| 941 | |
| 942 function newSplice(index, removed, addedCount) { | |
| 943 return { | |
| 944 index: index, | |
| 945 removed: removed, | |
| 946 addedCount: addedCount | |
| 947 }; | |
| 948 } | |
| 949 | |
| 950 var EDIT_LEAVE = 0; | |
| 951 var EDIT_UPDATE = 1; | |
| 952 var EDIT_ADD = 2; | |
| 953 var EDIT_DELETE = 3; | |
| 954 | |
| 955 function ArraySplice() {} | |
| 956 | |
| 957 ArraySplice.prototype = { | |
| 958 | |
| 959 // Note: This function is *based* on the computation of the Levenshtein | |
| 960 // "edit" distance. The one change is that "updates" are treated as two | |
| 961 // edits - not one. With Array splices, an update is really a delete | |
| 962 // followed by an add. By retaining this, we optimize for "keeping" the | |
| 963 // maximum array items in the original array. For example: | |
| 964 // | |
| 965 // 'xxxx123' -> '123yyyy' | |
| 966 // | |
| 967 // With 1-edit updates, the shortest path would be just to update all seven | |
| 968 // characters. With 2-edit updates, we delete 4, leave 3, and add 4. This | |
| 969 // leaves the substring '123' intact. | |
| 970 calcEditDistances: function(current, currentStart, currentEnd, | |
| 971 old, oldStart, oldEnd) { | |
| 972 // "Deletion" columns | |
| 973 var rowCount = oldEnd - oldStart + 1; | |
| 974 var columnCount = currentEnd - currentStart + 1; | |
| 975 var distances = new Array(rowCount); | |
| 976 | |
| 977 // "Addition" rows. Initialize null column. | |
| 978 for (var i = 0; i < rowCount; i++) { | |
| 979 distances[i] = new Array(columnCount); | |
| 980 distances[i][0] = i; | |
| 981 } | |
| 982 | |
| 983 // Initialize null row | |
| 984 for (var j = 0; j < columnCount; j++) | |
| 985 distances[0][j] = j; | |
| 986 | |
| 987 for (var i = 1; i < rowCount; i++) { | |
| 988 for (var j = 1; j < columnCount; j++) { | |
| 989 if (this.equals(current[currentStart + j - 1], old[oldStart + i - 1])) | |
| 990 distances[i][j] = distances[i - 1][j - 1]; | |
| 991 else { | |
| 992 var north = distances[i - 1][j] + 1; | |
| 993 var west = distances[i][j - 1] + 1; | |
| 994 distances[i][j] = north < west ? north : west; | |
| 995 } | |
| 996 } | |
| 997 } | |
| 998 | |
| 999 return distances; | |
| 1000 }, | |
| 1001 | |
| 1002 // This starts at the final weight, and walks "backward" by finding | |
| 1003 // the minimum previous weight recursively until the origin of the weight | |
| 1004 // matrix. | |
| 1005 spliceOperationsFromEditDistances: function(distances) { | |
| 1006 var i = distances.length - 1; | |
| 1007 var j = distances[0].length - 1; | |
| 1008 var current = distances[i][j]; | |
| 1009 var edits = []; | |
| 1010 while (i > 0 || j > 0) { | |
| 1011 if (i == 0) { | |
| 1012 edits.push(EDIT_ADD); | |
| 1013 j--; | |
| 1014 continue; | |
| 1015 } | |
| 1016 if (j == 0) { | |
| 1017 edits.push(EDIT_DELETE); | |
| 1018 i--; | |
| 1019 continue; | |
| 1020 } | |
| 1021 var northWest = distances[i - 1][j - 1]; | |
| 1022 var west = distances[i - 1][j]; | |
| 1023 var north = distances[i][j - 1]; | |
| 1024 | |
| 1025 var min; | |
| 1026 if (west < north) | |
| 1027 min = west < northWest ? west : northWest; | |
| 1028 else | |
| 1029 min = north < northWest ? north : northWest; | |
| 1030 | |
| 1031 if (min == northWest) { | |
| 1032 if (northWest == current) { | |
| 1033 edits.push(EDIT_LEAVE); | |
| 1034 } else { | |
| 1035 edits.push(EDIT_UPDATE); | |
| 1036 current = northWest; | |
| 1037 } | |
| 1038 i--; | |
| 1039 j--; | |
| 1040 } else if (min == west) { | |
| 1041 edits.push(EDIT_DELETE); | |
| 1042 i--; | |
| 1043 current = west; | |
| 1044 } else { | |
| 1045 edits.push(EDIT_ADD); | |
| 1046 j--; | |
| 1047 current = north; | |
| 1048 } | |
| 1049 } | |
| 1050 | |
| 1051 edits.reverse(); | |
| 1052 return edits; | |
| 1053 }, | |
| 1054 | |
| 1055 /** | |
| 1056 * Splice Projection functions: | |
| 1057 * | |
| 1058 * A splice map is a representation of how a previous array of items | |
| 1059 * was transformed into a new array of items. Conceptually it is a list of | |
| 1060 * tuples of | |
| 1061 * | |
| 1062 * <index, removed, addedCount> | |
| 1063 * | |
| 1064 * which are kept in ascending index order of. The tuple represents that at | |
| 1065 * the |index|, |removed| sequence of items were removed, and counting forward | |
| 1066 * from |index|, |addedCount| items were added. | |
| 1067 */ | |
| 1068 | |
| 1069 /** | |
| 1070 * Lacking individual splice mutation information, the minimal set of | |
| 1071 * splices can be synthesized given the previous state and final state of an | |
| 1072 * array. The basic approach is to calculate the edit distance matrix and | |
| 1073 * choose the shortest path through it. | |
| 1074 * | |
| 1075 * Complexity: O(l * p) | |
| 1076 * l: The length of the current array | |
| 1077 * p: The length of the old array | |
| 1078 */ | |
| 1079 calcSplices: function(current, currentStart, currentEnd, | |
| 1080 old, oldStart, oldEnd) { | |
| 1081 var prefixCount = 0; | |
| 1082 var suffixCount = 0; | |
| 1083 | |
| 1084 var minLength = Math.min(currentEnd - currentStart, oldEnd - oldStart); | |
| 1085 if (currentStart == 0 && oldStart == 0) | |
| 1086 prefixCount = this.sharedPrefix(current, old, minLength); | |
| 1087 | |
| 1088 if (currentEnd == current.length && oldEnd == old.length) | |
| 1089 suffixCount = this.sharedSuffix(current, old, minLength - prefixCount); | |
| 1090 | |
| 1091 currentStart += prefixCount; | |
| 1092 oldStart += prefixCount; | |
| 1093 currentEnd -= suffixCount; | |
| 1094 oldEnd -= suffixCount; | |
| 1095 | |
| 1096 if (currentEnd - currentStart == 0 && oldEnd - oldStart == 0) | |
| 1097 return []; | |
| 1098 | |
| 1099 if (currentStart == currentEnd) { | |
| 1100 var splice = newSplice(currentStart, [], 0); | |
| 1101 while (oldStart < oldEnd) | |
| 1102 splice.removed.push(old[oldStart++]); | |
| 1103 | |
| 1104 return [ splice ]; | |
| 1105 } else if (oldStart == oldEnd) | |
| 1106 return [ newSplice(currentStart, [], currentEnd - currentStart) ]; | |
| 1107 | |
| 1108 var ops = this.spliceOperationsFromEditDistances( | |
| 1109 this.calcEditDistances(current, currentStart, currentEnd, | |
| 1110 old, oldStart, oldEnd)); | |
| 1111 | |
| 1112 var splice = undefined; | |
| 1113 var splices = []; | |
| 1114 var index = currentStart; | |
| 1115 var oldIndex = oldStart; | |
| 1116 for (var i = 0; i < ops.length; i++) { | |
| 1117 switch(ops[i]) { | |
| 1118 case EDIT_LEAVE: | |
| 1119 if (splice) { | |
| 1120 splices.push(splice); | |
| 1121 splice = undefined; | |
| 1122 } | |
| 1123 | |
| 1124 index++; | |
| 1125 oldIndex++; | |
| 1126 break; | |
| 1127 case EDIT_UPDATE: | |
| 1128 if (!splice) | |
| 1129 splice = newSplice(index, [], 0); | |
| 1130 | |
| 1131 splice.addedCount++; | |
| 1132 index++; | |
| 1133 | |
| 1134 splice.removed.push(old[oldIndex]); | |
| 1135 oldIndex++; | |
| 1136 break; | |
| 1137 case EDIT_ADD: | |
| 1138 if (!splice) | |
| 1139 splice = newSplice(index, [], 0); | |
| 1140 | |
| 1141 splice.addedCount++; | |
| 1142 index++; | |
| 1143 break; | |
| 1144 case EDIT_DELETE: | |
| 1145 if (!splice) | |
| 1146 splice = newSplice(index, [], 0); | |
| 1147 | |
| 1148 splice.removed.push(old[oldIndex]); | |
| 1149 oldIndex++; | |
| 1150 break; | |
| 1151 } | |
| 1152 } | |
| 1153 | |
| 1154 if (splice) { | |
| 1155 splices.push(splice); | |
| 1156 } | |
| 1157 return splices; | |
| 1158 }, | |
| 1159 | |
| 1160 sharedPrefix: function(current, old, searchLength) { | |
| 1161 for (var i = 0; i < searchLength; i++) | |
| 1162 if (!this.equals(current[i], old[i])) | |
| 1163 return i; | |
| 1164 return searchLength; | |
| 1165 }, | |
| 1166 | |
| 1167 sharedSuffix: function(current, old, searchLength) { | |
| 1168 var index1 = current.length; | |
| 1169 var index2 = old.length; | |
| 1170 var count = 0; | |
| 1171 while (count < searchLength && | |
| 1172 this.equals(current[--index1], old[--index2])) { | |
| 1173 count++; | |
| 1174 } | |
| 1175 | |
| 1176 return count; | |
| 1177 }, | |
| 1178 | |
| 1179 calculateSplices: function(current, previous) { | |
| 1180 return this.calcSplices(current, 0, current.length, previous, 0, | |
| 1181 previous.length); | |
| 1182 }, | |
| 1183 | |
| 1184 equals: function(currentValue, previousValue) { | |
| 1185 return currentValue === previousValue; | |
| 1186 } | |
| 1187 }; | |
| 1188 | |
| 1189 var arraySplice = new ArraySplice(); | |
| 1190 | |
| 1191 function calcSplices(current, currentStart, currentEnd, | |
| 1192 old, oldStart, oldEnd) { | |
| 1193 return arraySplice.calcSplices(current, currentStart, currentEnd, | |
| 1194 old, oldStart, oldEnd); | |
| 1195 } | |
| 1196 | |
| 1197 function intersect(start1, end1, start2, end2) { | |
| 1198 // Disjoint | |
| 1199 if (end1 < start2 || end2 < start1) | |
| 1200 return -1; | |
| 1201 | |
| 1202 // Adjacent | |
| 1203 if (end1 == start2 || end2 == start1) | |
| 1204 return 0; | |
| 1205 | |
| 1206 // Non-zero intersect, span1 first | |
| 1207 if (start1 < start2) { | |
| 1208 if (end1 < end2) | |
| 1209 return end1 - start2; // Overlap | |
| 1210 else | |
| 1211 return end2 - start2; // Contained | |
| 1212 } else { | |
| 1213 // Non-zero intersect, span2 first | |
| 1214 if (end2 < end1) | |
| 1215 return end2 - start1; // Overlap | |
| 1216 else | |
| 1217 return end1 - start1; // Contained | |
| 1218 } | |
| 1219 } | |
| 1220 | |
| 1221 function mergeSplice(splices, index, removed, addedCount) { | |
| 1222 | |
| 1223 var splice = newSplice(index, removed, addedCount); | |
| 1224 | |
| 1225 var inserted = false; | |
| 1226 var insertionOffset = 0; | |
| 1227 | |
| 1228 for (var i = 0; i < splices.length; i++) { | |
| 1229 var current = splices[i]; | |
| 1230 current.index += insertionOffset; | |
| 1231 | |
| 1232 if (inserted) | |
| 1233 continue; | |
| 1234 | |
| 1235 var intersectCount = intersect(splice.index, | |
| 1236 splice.index + splice.removed.length, | |
| 1237 current.index, | |
| 1238 current.index + current.addedCount); | |
| 1239 | |
| 1240 if (intersectCount >= 0) { | |
| 1241 // Merge the two splices | |
| 1242 | |
| 1243 splices.splice(i, 1); | |
| 1244 i--; | |
| 1245 | |
| 1246 insertionOffset -= current.addedCount - current.removed.length; | |
| 1247 | |
| 1248 splice.addedCount += current.addedCount - intersectCount; | |
| 1249 var deleteCount = splice.removed.length + | |
| 1250 current.removed.length - intersectCount; | |
| 1251 | |
| 1252 if (!splice.addedCount && !deleteCount) { | |
| 1253 // merged splice is a noop. discard. | |
| 1254 inserted = true; | |
| 1255 } else { | |
| 1256 var removed = current.removed; | |
| 1257 | |
| 1258 if (splice.index < current.index) { | |
| 1259 // some prefix of splice.removed is prepended to current.removed. | |
| 1260 var prepend = splice.removed.slice(0, current.index - splice.index); | |
| 1261 Array.prototype.push.apply(prepend, removed); | |
| 1262 removed = prepend; | |
| 1263 } | |
| 1264 | |
| 1265 if (splice.index + splice.removed.length > | |
| 1266 current.index + current.addedCount) { | |
| 1267 // some suffix of splice.removed is appended to current.removed. | |
| 1268 var append = splice.removed.slice( | |
| 1269 current.index + current.addedCount - splice.index); | |
| 1270 Array.prototype.push.apply(removed, append); | |
| 1271 } | |
| 1272 | |
| 1273 splice.removed = removed; | |
| 1274 if (current.index < splice.index) { | |
| 1275 splice.index = current.index; | |
| 1276 } | |
| 1277 } | |
| 1278 } else if (splice.index < current.index) { | |
| 1279 // Insert splice here. | |
| 1280 | |
| 1281 inserted = true; | |
| 1282 | |
| 1283 splices.splice(i, 0, splice); | |
| 1284 i++; | |
| 1285 | |
| 1286 var offset = splice.addedCount - splice.removed.length | |
| 1287 current.index += offset; | |
| 1288 insertionOffset += offset; | |
| 1289 } | |
| 1290 } | |
| 1291 | |
| 1292 if (!inserted) | |
| 1293 splices.push(splice); | |
| 1294 } | |
| 1295 | |
| 1296 function createInitialSplices(array, changeRecords) { | |
| 1297 var splices = []; | |
| 1298 | |
| 1299 for (var i = 0; i < changeRecords.length; i++) { | |
| 1300 var record = changeRecords[i]; | |
| 1301 switch(record.type) { | |
| 1302 case 'splice': | |
| 1303 mergeSplice(splices, record.index, record.removed.slice(), | |
| 1304 record.addedCount); | |
| 1305 break; | |
| 1306 case 'add': | |
| 1307 case 'update': | |
| 1308 case 'delete': | |
| 1309 if (!isIndex(record.name)) | |
| 1310 continue; | |
| 1311 var index = toNumber(record.name); | |
| 1312 if (index < 0) | |
| 1313 continue; | |
| 1314 mergeSplice(splices, index, [record.oldValue], 1); | |
| 1315 break; | |
| 1316 default: | |
| 1317 console.error('Unexpected record type: ' + JSON.stringify(record)); | |
| 1318 break; | |
| 1319 } | |
| 1320 } | |
| 1321 | |
| 1322 return splices; | |
| 1323 } | |
| 1324 | |
| 1325 function projectArraySplices(array, changeRecords) { | |
| 1326 var splices = []; | |
| 1327 | |
| 1328 createInitialSplices(array, changeRecords).forEach(function(splice) { | |
| 1329 if (splice.addedCount == 1 && splice.removed.length == 1) { | |
| 1330 if (splice.removed[0] !== array[splice.index]) | |
| 1331 splices.push(splice); | |
| 1332 | |
| 1333 return | |
| 1334 }; | |
| 1335 | |
| 1336 splices = splices.concat(calcSplices(array, splice.index, | |
| 1337 splice.index + splice.addedCount, | |
| 1338 splice.removed, | |
| 1339 0, | |
| 1340 splice.removed.length)); | |
| 1341 }); | |
| 1342 | |
| 1343 return splices; | |
| 1344 } | |
| 1345 | |
| 1346 ArrayObserver.calculateSplices = function(current, previous) { | |
| 1347 return arraySplice.calculateSplices(current, previous); | |
| 1348 }; | |
| 1349 | |
| 1350 module.exports = { | |
| 1351 Observer: Observer, | |
| 1352 runEOM_: runEOM, | |
| 1353 observerSentinel_: observerSentinel, | |
| 1354 PathObserver: PathObserver, | |
| 1355 ArrayObserver: ArrayObserver, | |
| 1356 ArraySplice: ArraySplice, | |
| 1357 CompoundObserver: CompoundObserver, | |
| 1358 Path: Path, | |
| 1359 ObserverTransform: ObserverTransform | |
| 1360 } | |
| 1361 </script> | |
| OLD | NEW |