Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(120)

Side by Side Diff: packages/template_binding/lib/js/observe.js

Issue 2989763002: Update charted to 0.4.8 and roll (Closed)
Patch Set: Removed Cutch from list of reviewers Created 3 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(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);
OLDNEW
« no previous file with comments | « packages/template_binding/lib/js/node_bind.js ('k') | packages/template_binding/lib/src/binding_delegate.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698