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

Side by Side Diff: src/site/samples/polymer_intl/example/packages/template_binding/js/observe.js

Issue 1387723002: Updating the samples page to reflect the examples that have been retired. (Closed) Base URL: https://github.com/dart-lang/www.dartlang.org.git@master
Patch Set: Created 5 years, 2 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[0]);
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 var eomObj = { pingPong: true };
515 var eomRunScheduled = false;
516
517 Object.observe(eomObj, function() {
518 runEOMTasks();
519 eomRunScheduled = false;
520 });
521
522 return function(fn) {
523 eomTasks.push(fn);
524 if (!eomRunScheduled) {
525 eomRunScheduled = true;
526 eomObj.pingPong = !eomObj.pingPong;
527 }
528 };
529 })() :
530 (function() {
531 return function(fn) {
532 eomTasks.push(fn);
533 };
534 })();
535
536 var observedObjectCache = [];
537
538 function newObservedObject() {
539 var observer;
540 var object;
541 var discardRecords = false;
542 var first = true;
543
544 function callback(records) {
545 if (observer && observer.state_ === OPENED && !discardRecords)
546 observer.check_(records);
547 }
548
549 return {
550 open: function(obs) {
551 if (observer)
552 throw Error('ObservedObject in use');
553
554 if (!first)
555 Object.deliverChangeRecords(callback);
556
557 observer = obs;
558 first = false;
559 },
560 observe: function(obj, arrayObserve) {
561 object = obj;
562 if (arrayObserve)
563 Array.observe(object, callback);
564 else
565 Object.observe(object, callback);
566 },
567 deliver: function(discard) {
568 discardRecords = discard;
569 Object.deliverChangeRecords(callback);
570 discardRecords = false;
571 },
572 close: function() {
573 observer = undefined;
574 Object.unobserve(object, callback);
575 observedObjectCache.push(this);
576 }
577 };
578 }
579
580 /*
581 * The observedSet abstraction is a perf optimization which reduces the total
582 * number of Object.observe observations of a set of objects. The idea is that
583 * groups of Observers will have some object dependencies in common and this
584 * observed set ensures that each object in the transitive closure of
585 * dependencies is only observed once. The observedSet acts as a write barrier
586 * such that whenever any change comes through, all Observers are checked for
587 * changed values.
588 *
589 * Note that this optimization is explicitly moving work from setup-time to
590 * change-time.
591 *
592 * TODO(rafaelw): Implement "garbage collection". In order to move work off
593 * the critical path, when Observers are closed, their observed objects are
594 * not Object.unobserve(d). As a result, it's possible that if the observedSet
595 * is kept open, but some Observers have been closed, it could cause "leaks"
596 * (prevent otherwise collectable objects from being collected). At some
597 * point, we should implement incremental "gc" which keeps a list of
598 * observedSets which may need clean-up and does small amounts of cleanup on a
599 * timeout until all is clean.
600 */
601
602 function getObservedObject(observer, object, arrayObserve) {
603 var dir = observedObjectCache.pop() || newObservedObject();
604 dir.open(observer);
605 dir.observe(object, arrayObserve);
606 return dir;
607 }
608
609 var observedSetCache = [];
610
611 function newObservedSet() {
612 var observerCount = 0;
613 var observers = [];
614 var objects = [];
615 var rootObj;
616 var rootObjProps;
617
618 function observe(obj, prop) {
619 if (!obj)
620 return;
621
622 if (obj === rootObj)
623 rootObjProps[prop] = true;
624
625 if (objects.indexOf(obj) < 0) {
626 objects.push(obj);
627 Object.observe(obj, callback);
628 }
629
630 observe(Object.getPrototypeOf(obj), prop);
631 }
632
633 function allRootObjNonObservedProps(recs) {
634 for (var i = 0; i < recs.length; i++) {
635 var rec = recs[i];
636 if (rec.object !== rootObj ||
637 rootObjProps[rec.name] ||
638 rec.type === 'setPrototype') {
639 return false;
640 }
641 }
642 return true;
643 }
644
645 function callback(recs) {
646 if (allRootObjNonObservedProps(recs))
647 return;
648
649 var observer;
650 for (var i = 0; i < observers.length; i++) {
651 observer = observers[i];
652 if (observer.state_ == OPENED) {
653 observer.iterateObjects_(observe);
654 }
655 }
656
657 for (var i = 0; i < observers.length; i++) {
658 observer = observers[i];
659 if (observer.state_ == OPENED) {
660 observer.check_();
661 }
662 }
663 }
664
665 var record = {
666 object: undefined,
667 objects: objects,
668 open: function(obs, object) {
669 if (!rootObj) {
670 rootObj = object;
671 rootObjProps = {};
672 }
673
674 observers.push(obs);
675 observerCount++;
676 obs.iterateObjects_(observe);
677 },
678 close: function(obs) {
679 observerCount--;
680 if (observerCount > 0) {
681 return;
682 }
683
684 for (var i = 0; i < objects.length; i++) {
685 Object.unobserve(objects[i], callback);
686 Observer.unobservedCount++;
687 }
688
689 observers.length = 0;
690 objects.length = 0;
691 rootObj = undefined;
692 rootObjProps = undefined;
693 observedSetCache.push(this);
694 }
695 };
696
697 return record;
698 }
699
700 var lastObservedSet;
701
702 function getObservedSet(observer, obj) {
703 if (!lastObservedSet || lastObservedSet.object !== obj) {
704 lastObservedSet = observedSetCache.pop() || newObservedSet();
705 lastObservedSet.object = obj;
706 }
707 lastObservedSet.open(observer, obj);
708 return lastObservedSet;
709 }
710
711 var UNOPENED = 0;
712 var OPENED = 1;
713 var CLOSED = 2;
714 var RESETTING = 3;
715
716 var nextObserverId = 1;
717
718 function Observer() {
719 this.state_ = UNOPENED;
720 this.callback_ = undefined;
721 this.target_ = undefined; // TODO(rafaelw): Should be WeakRef
722 this.directObserver_ = undefined;
723 this.value_ = undefined;
724 this.id_ = nextObserverId++;
725 }
726
727 Observer.prototype = {
728 open: function(callback, target) {
729 if (this.state_ != UNOPENED)
730 throw Error('Observer has already been opened.');
731
732 addToAll(this);
733 this.callback_ = callback;
734 this.target_ = target;
735 this.connect_();
736 this.state_ = OPENED;
737 return this.value_;
738 },
739
740 close: function() {
741 if (this.state_ != OPENED)
742 return;
743
744 removeFromAll(this);
745 this.disconnect_();
746 this.value_ = undefined;
747 this.callback_ = undefined;
748 this.target_ = undefined;
749 this.state_ = CLOSED;
750 },
751
752 deliver: function() {
753 if (this.state_ != OPENED)
754 return;
755
756 dirtyCheck(this);
757 },
758
759 report_: function(changes) {
760 try {
761 this.callback_.apply(this.target_, changes);
762 } catch (ex) {
763 Observer._errorThrownDuringCallback = true;
764 console.error('Exception caught during observer callback: ' +
765 (ex.stack || ex));
766 }
767 },
768
769 discardChanges: function() {
770 this.check_(undefined, true);
771 return this.value_;
772 }
773 }
774
775 var collectObservers = !hasObserve;
776 var allObservers;
777 Observer._allObserversCount = 0;
778
779 if (collectObservers) {
780 allObservers = [];
781 }
782
783 function addToAll(observer) {
784 Observer._allObserversCount++;
785 if (!collectObservers)
786 return;
787
788 allObservers.push(observer);
789 }
790
791 function removeFromAll(observer) {
792 Observer._allObserversCount--;
793 }
794
795 var runningMicrotaskCheckpoint = false;
796
797 global.Platform = global.Platform || {};
798
799 global.Platform.performMicrotaskCheckpoint = function() {
800 if (runningMicrotaskCheckpoint)
801 return;
802
803 if (!collectObservers)
804 return;
805
806 runningMicrotaskCheckpoint = true;
807
808 var cycles = 0;
809 var anyChanged, toCheck;
810
811 do {
812 cycles++;
813 toCheck = allObservers;
814 allObservers = [];
815 anyChanged = false;
816
817 for (var i = 0; i < toCheck.length; i++) {
818 var observer = toCheck[i];
819 if (observer.state_ != OPENED)
820 continue;
821
822 if (observer.check_())
823 anyChanged = true;
824
825 allObservers.push(observer);
826 }
827 if (runEOMTasks())
828 anyChanged = true;
829 } while (cycles < MAX_DIRTY_CHECK_CYCLES && anyChanged);
830
831 if (testingExposeCycleCount)
832 global.dirtyCheckCycleCount = cycles;
833
834 runningMicrotaskCheckpoint = false;
835 };
836
837 if (collectObservers) {
838 global.Platform.clearObservers = function() {
839 allObservers = [];
840 };
841 }
842
843 function ObjectObserver(object) {
844 Observer.call(this);
845 this.value_ = object;
846 this.oldObject_ = undefined;
847 }
848
849 ObjectObserver.prototype = createObject({
850 __proto__: Observer.prototype,
851
852 arrayObserve: false,
853
854 connect_: function(callback, target) {
855 if (hasObserve) {
856 this.directObserver_ = getObservedObject(this, this.value_,
857 this.arrayObserve);
858 } else {
859 this.oldObject_ = this.copyObject(this.value_);
860 }
861
862 },
863
864 copyObject: function(object) {
865 var copy = Array.isArray(object) ? [] : {};
866 for (var prop in object) {
867 copy[prop] = object[prop];
868 };
869 if (Array.isArray(object))
870 copy.length = object.length;
871 return copy;
872 },
873
874 check_: function(changeRecords, skipChanges) {
875 var diff;
876 var oldValues;
877 if (hasObserve) {
878 if (!changeRecords)
879 return false;
880
881 oldValues = {};
882 diff = diffObjectFromChangeRecords(this.value_, changeRecords,
883 oldValues);
884 } else {
885 oldValues = this.oldObject_;
886 diff = diffObjectFromOldObject(this.value_, this.oldObject_);
887 }
888
889 if (diffIsEmpty(diff))
890 return false;
891
892 if (!hasObserve)
893 this.oldObject_ = this.copyObject(this.value_);
894
895 this.report_([
896 diff.added || {},
897 diff.removed || {},
898 diff.changed || {},
899 function(property) {
900 return oldValues[property];
901 }
902 ]);
903
904 return true;
905 },
906
907 disconnect_: function() {
908 if (hasObserve) {
909 this.directObserver_.close();
910 this.directObserver_ = undefined;
911 } else {
912 this.oldObject_ = undefined;
913 }
914 },
915
916 deliver: function() {
917 if (this.state_ != OPENED)
918 return;
919
920 if (hasObserve)
921 this.directObserver_.deliver(false);
922 else
923 dirtyCheck(this);
924 },
925
926 discardChanges: function() {
927 if (this.directObserver_)
928 this.directObserver_.deliver(true);
929 else
930 this.oldObject_ = this.copyObject(this.value_);
931
932 return this.value_;
933 }
934 });
935
936 function ArrayObserver(array) {
937 if (!Array.isArray(array))
938 throw Error('Provided object is not an Array');
939 ObjectObserver.call(this, array);
940 }
941
942 ArrayObserver.prototype = createObject({
943
944 __proto__: ObjectObserver.prototype,
945
946 arrayObserve: true,
947
948 copyObject: function(arr) {
949 return arr.slice();
950 },
951
952 check_: function(changeRecords) {
953 var splices;
954 if (hasObserve) {
955 if (!changeRecords)
956 return false;
957 splices = projectArraySplices(this.value_, changeRecords);
958 } else {
959 splices = calcSplices(this.value_, 0, this.value_.length,
960 this.oldObject_, 0, this.oldObject_.length);
961 }
962
963 if (!splices || !splices.length)
964 return false;
965
966 if (!hasObserve)
967 this.oldObject_ = this.copyObject(this.value_);
968
969 this.report_([splices]);
970 return true;
971 }
972 });
973
974 ArrayObserver.applySplices = function(previous, current, splices) {
975 splices.forEach(function(splice) {
976 var spliceArgs = [splice.index, splice.removed.length];
977 var addIndex = splice.index;
978 while (addIndex < splice.index + splice.addedCount) {
979 spliceArgs.push(current[addIndex]);
980 addIndex++;
981 }
982
983 Array.prototype.splice.apply(previous, spliceArgs);
984 });
985 };
986
987 function PathObserver(object, path) {
988 Observer.call(this);
989
990 this.object_ = object;
991 this.path_ = getPath(path);
992 this.directObserver_ = undefined;
993 }
994
995 PathObserver.prototype = createObject({
996 __proto__: Observer.prototype,
997
998 get path() {
999 return this.path_;
1000 },
1001
1002 connect_: function() {
1003 if (hasObserve)
1004 this.directObserver_ = getObservedSet(this, this.object_);
1005
1006 this.check_(undefined, true);
1007 },
1008
1009 disconnect_: function() {
1010 this.value_ = undefined;
1011
1012 if (this.directObserver_) {
1013 this.directObserver_.close(this);
1014 this.directObserver_ = undefined;
1015 }
1016 },
1017
1018 iterateObjects_: function(observe) {
1019 this.path_.iterateObjects(this.object_, observe);
1020 },
1021
1022 check_: function(changeRecords, skipChanges) {
1023 var oldValue = this.value_;
1024 this.value_ = this.path_.getValueFrom(this.object_);
1025 if (skipChanges || areSameValue(this.value_, oldValue))
1026 return false;
1027
1028 this.report_([this.value_, oldValue, this]);
1029 return true;
1030 },
1031
1032 setValue: function(newValue) {
1033 if (this.path_)
1034 this.path_.setValueFrom(this.object_, newValue);
1035 }
1036 });
1037
1038 function CompoundObserver(reportChangesOnOpen) {
1039 Observer.call(this);
1040
1041 this.reportChangesOnOpen_ = reportChangesOnOpen;
1042 this.value_ = [];
1043 this.directObserver_ = undefined;
1044 this.observed_ = [];
1045 }
1046
1047 var observerSentinel = {};
1048
1049 CompoundObserver.prototype = createObject({
1050 __proto__: Observer.prototype,
1051
1052 connect_: function() {
1053 if (hasObserve) {
1054 var object;
1055 var needsDirectObserver = false;
1056 for (var i = 0; i < this.observed_.length; i += 2) {
1057 object = this.observed_[i]
1058 if (object !== observerSentinel) {
1059 needsDirectObserver = true;
1060 break;
1061 }
1062 }
1063
1064 if (needsDirectObserver)
1065 this.directObserver_ = getObservedSet(this, object);
1066 }
1067
1068 this.check_(undefined, !this.reportChangesOnOpen_);
1069 },
1070
1071 disconnect_: function() {
1072 for (var i = 0; i < this.observed_.length; i += 2) {
1073 if (this.observed_[i] === observerSentinel)
1074 this.observed_[i + 1].close();
1075 }
1076 this.observed_.length = 0;
1077 this.value_.length = 0;
1078
1079 if (this.directObserver_) {
1080 this.directObserver_.close(this);
1081 this.directObserver_ = undefined;
1082 }
1083 },
1084
1085 addPath: function(object, path) {
1086 if (this.state_ != UNOPENED && this.state_ != RESETTING)
1087 throw Error('Cannot add paths once started.');
1088
1089 var path = getPath(path);
1090 this.observed_.push(object, path);
1091 if (!this.reportChangesOnOpen_)
1092 return;
1093 var index = this.observed_.length / 2 - 1;
1094 this.value_[index] = path.getValueFrom(object);
1095 },
1096
1097 addObserver: function(observer) {
1098 if (this.state_ != UNOPENED && this.state_ != RESETTING)
1099 throw Error('Cannot add observers once started.');
1100
1101 this.observed_.push(observerSentinel, observer);
1102 if (!this.reportChangesOnOpen_)
1103 return;
1104 var index = this.observed_.length / 2 - 1;
1105 this.value_[index] = observer.open(this.deliver, this);
1106 },
1107
1108 startReset: function() {
1109 if (this.state_ != OPENED)
1110 throw Error('Can only reset while open');
1111
1112 this.state_ = RESETTING;
1113 this.disconnect_();
1114 },
1115
1116 finishReset: function() {
1117 if (this.state_ != RESETTING)
1118 throw Error('Can only finishReset after startReset');
1119 this.state_ = OPENED;
1120 this.connect_();
1121
1122 return this.value_;
1123 },
1124
1125 iterateObjects_: function(observe) {
1126 var object;
1127 for (var i = 0; i < this.observed_.length; i += 2) {
1128 object = this.observed_[i]
1129 if (object !== observerSentinel)
1130 this.observed_[i + 1].iterateObjects(object, observe)
1131 }
1132 },
1133
1134 check_: function(changeRecords, skipChanges) {
1135 var oldValues;
1136 for (var i = 0; i < this.observed_.length; i += 2) {
1137 var object = this.observed_[i];
1138 var path = this.observed_[i+1];
1139 var value;
1140 if (object === observerSentinel) {
1141 var observable = path;
1142 value = this.state_ === UNOPENED ?
1143 observable.open(this.deliver, this) :
1144 observable.discardChanges();
1145 } else {
1146 value = path.getValueFrom(object);
1147 }
1148
1149 if (skipChanges) {
1150 this.value_[i / 2] = value;
1151 continue;
1152 }
1153
1154 if (areSameValue(value, this.value_[i / 2]))
1155 continue;
1156
1157 oldValues = oldValues || [];
1158 oldValues[i / 2] = this.value_[i / 2];
1159 this.value_[i / 2] = value;
1160 }
1161
1162 if (!oldValues)
1163 return false;
1164
1165 // TODO(rafaelw): Having observed_ as the third callback arg here is
1166 // pretty lame API. Fix.
1167 this.report_([this.value_, oldValues, this.observed_]);
1168 return true;
1169 }
1170 });
1171
1172 function identFn(value) { return value; }
1173
1174 function ObserverTransform(observable, getValueFn, setValueFn,
1175 dontPassThroughSet) {
1176 this.callback_ = undefined;
1177 this.target_ = undefined;
1178 this.value_ = undefined;
1179 this.observable_ = observable;
1180 this.getValueFn_ = getValueFn || identFn;
1181 this.setValueFn_ = setValueFn || identFn;
1182 // TODO(rafaelw): This is a temporary hack. PolymerExpressions needs this
1183 // at the moment because of a bug in it's dependency tracking.
1184 this.dontPassThroughSet_ = dontPassThroughSet;
1185 }
1186
1187 ObserverTransform.prototype = {
1188 open: function(callback, target) {
1189 this.callback_ = callback;
1190 this.target_ = target;
1191 this.value_ =
1192 this.getValueFn_(this.observable_.open(this.observedCallback_, this));
1193 return this.value_;
1194 },
1195
1196 observedCallback_: function(value) {
1197 value = this.getValueFn_(value);
1198 if (areSameValue(value, this.value_))
1199 return;
1200 var oldValue = this.value_;
1201 this.value_ = value;
1202 this.callback_.call(this.target_, this.value_, oldValue);
1203 },
1204
1205 discardChanges: function() {
1206 this.value_ = this.getValueFn_(this.observable_.discardChanges());
1207 return this.value_;
1208 },
1209
1210 deliver: function() {
1211 return this.observable_.deliver();
1212 },
1213
1214 setValue: function(value) {
1215 value = this.setValueFn_(value);
1216 if (!this.dontPassThroughSet_ && this.observable_.setValue)
1217 return this.observable_.setValue(value);
1218 },
1219
1220 close: function() {
1221 if (this.observable_)
1222 this.observable_.close();
1223 this.callback_ = undefined;
1224 this.target_ = undefined;
1225 this.observable_ = undefined;
1226 this.value_ = undefined;
1227 this.getValueFn_ = undefined;
1228 this.setValueFn_ = undefined;
1229 }
1230 }
1231
1232 var expectedRecordTypes = {
1233 add: true,
1234 update: true,
1235 delete: true
1236 };
1237
1238 function diffObjectFromChangeRecords(object, changeRecords, oldValues) {
1239 var added = {};
1240 var removed = {};
1241
1242 for (var i = 0; i < changeRecords.length; i++) {
1243 var record = changeRecords[i];
1244 if (!expectedRecordTypes[record.type]) {
1245 console.error('Unknown changeRecord type: ' + record.type);
1246 console.error(record);
1247 continue;
1248 }
1249
1250 if (!(record.name in oldValues))
1251 oldValues[record.name] = record.oldValue;
1252
1253 if (record.type == 'update')
1254 continue;
1255
1256 if (record.type == 'add') {
1257 if (record.name in removed)
1258 delete removed[record.name];
1259 else
1260 added[record.name] = true;
1261
1262 continue;
1263 }
1264
1265 // type = 'delete'
1266 if (record.name in added) {
1267 delete added[record.name];
1268 delete oldValues[record.name];
1269 } else {
1270 removed[record.name] = true;
1271 }
1272 }
1273
1274 for (var prop in added)
1275 added[prop] = object[prop];
1276
1277 for (var prop in removed)
1278 removed[prop] = undefined;
1279
1280 var changed = {};
1281 for (var prop in oldValues) {
1282 if (prop in added || prop in removed)
1283 continue;
1284
1285 var newValue = object[prop];
1286 if (oldValues[prop] !== newValue)
1287 changed[prop] = newValue;
1288 }
1289
1290 return {
1291 added: added,
1292 removed: removed,
1293 changed: changed
1294 };
1295 }
1296
1297 function newSplice(index, removed, addedCount) {
1298 return {
1299 index: index,
1300 removed: removed,
1301 addedCount: addedCount
1302 };
1303 }
1304
1305 var EDIT_LEAVE = 0;
1306 var EDIT_UPDATE = 1;
1307 var EDIT_ADD = 2;
1308 var EDIT_DELETE = 3;
1309
1310 function ArraySplice() {}
1311
1312 ArraySplice.prototype = {
1313
1314 // Note: This function is *based* on the computation of the Levenshtein
1315 // "edit" distance. The one change is that "updates" are treated as two
1316 // edits - not one. With Array splices, an update is really a delete
1317 // followed by an add. By retaining this, we optimize for "keeping" the
1318 // maximum array items in the original array. For example:
1319 //
1320 // 'xxxx123' -> '123yyyy'
1321 //
1322 // With 1-edit updates, the shortest path would be just to update all seven
1323 // characters. With 2-edit updates, we delete 4, leave 3, and add 4. This
1324 // leaves the substring '123' intact.
1325 calcEditDistances: function(current, currentStart, currentEnd,
1326 old, oldStart, oldEnd) {
1327 // "Deletion" columns
1328 var rowCount = oldEnd - oldStart + 1;
1329 var columnCount = currentEnd - currentStart + 1;
1330 var distances = new Array(rowCount);
1331
1332 // "Addition" rows. Initialize null column.
1333 for (var i = 0; i < rowCount; i++) {
1334 distances[i] = new Array(columnCount);
1335 distances[i][0] = i;
1336 }
1337
1338 // Initialize null row
1339 for (var j = 0; j < columnCount; j++)
1340 distances[0][j] = j;
1341
1342 for (var i = 1; i < rowCount; i++) {
1343 for (var j = 1; j < columnCount; j++) {
1344 if (this.equals(current[currentStart + j - 1], old[oldStart + i - 1]))
1345 distances[i][j] = distances[i - 1][j - 1];
1346 else {
1347 var north = distances[i - 1][j] + 1;
1348 var west = distances[i][j - 1] + 1;
1349 distances[i][j] = north < west ? north : west;
1350 }
1351 }
1352 }
1353
1354 return distances;
1355 },
1356
1357 // This starts at the final weight, and walks "backward" by finding
1358 // the minimum previous weight recursively until the origin of the weight
1359 // matrix.
1360 spliceOperationsFromEditDistances: function(distances) {
1361 var i = distances.length - 1;
1362 var j = distances[0].length - 1;
1363 var current = distances[i][j];
1364 var edits = [];
1365 while (i > 0 || j > 0) {
1366 if (i == 0) {
1367 edits.push(EDIT_ADD);
1368 j--;
1369 continue;
1370 }
1371 if (j == 0) {
1372 edits.push(EDIT_DELETE);
1373 i--;
1374 continue;
1375 }
1376 var northWest = distances[i - 1][j - 1];
1377 var west = distances[i - 1][j];
1378 var north = distances[i][j - 1];
1379
1380 var min;
1381 if (west < north)
1382 min = west < northWest ? west : northWest;
1383 else
1384 min = north < northWest ? north : northWest;
1385
1386 if (min == northWest) {
1387 if (northWest == current) {
1388 edits.push(EDIT_LEAVE);
1389 } else {
1390 edits.push(EDIT_UPDATE);
1391 current = northWest;
1392 }
1393 i--;
1394 j--;
1395 } else if (min == west) {
1396 edits.push(EDIT_DELETE);
1397 i--;
1398 current = west;
1399 } else {
1400 edits.push(EDIT_ADD);
1401 j--;
1402 current = north;
1403 }
1404 }
1405
1406 edits.reverse();
1407 return edits;
1408 },
1409
1410 /**
1411 * Splice Projection functions:
1412 *
1413 * A splice map is a representation of how a previous array of items
1414 * was transformed into a new array of items. Conceptually it is a list of
1415 * tuples of
1416 *
1417 * <index, removed, addedCount>
1418 *
1419 * which are kept in ascending index order of. The tuple represents that at
1420 * the |index|, |removed| sequence of items were removed, and counting forwa rd
1421 * from |index|, |addedCount| items were added.
1422 */
1423
1424 /**
1425 * Lacking individual splice mutation information, the minimal set of
1426 * splices can be synthesized given the previous state and final state of an
1427 * array. The basic approach is to calculate the edit distance matrix and
1428 * choose the shortest path through it.
1429 *
1430 * Complexity: O(l * p)
1431 * l: The length of the current array
1432 * p: The length of the old array
1433 */
1434 calcSplices: function(current, currentStart, currentEnd,
1435 old, oldStart, oldEnd) {
1436 var prefixCount = 0;
1437 var suffixCount = 0;
1438
1439 var minLength = Math.min(currentEnd - currentStart, oldEnd - oldStart);
1440 if (currentStart == 0 && oldStart == 0)
1441 prefixCount = this.sharedPrefix(current, old, minLength);
1442
1443 if (currentEnd == current.length && oldEnd == old.length)
1444 suffixCount = this.sharedSuffix(current, old, minLength - prefixCount);
1445
1446 currentStart += prefixCount;
1447 oldStart += prefixCount;
1448 currentEnd -= suffixCount;
1449 oldEnd -= suffixCount;
1450
1451 if (currentEnd - currentStart == 0 && oldEnd - oldStart == 0)
1452 return [];
1453
1454 if (currentStart == currentEnd) {
1455 var splice = newSplice(currentStart, [], 0);
1456 while (oldStart < oldEnd)
1457 splice.removed.push(old[oldStart++]);
1458
1459 return [ splice ];
1460 } else if (oldStart == oldEnd)
1461 return [ newSplice(currentStart, [], currentEnd - currentStart) ];
1462
1463 var ops = this.spliceOperationsFromEditDistances(
1464 this.calcEditDistances(current, currentStart, currentEnd,
1465 old, oldStart, oldEnd));
1466
1467 var splice = undefined;
1468 var splices = [];
1469 var index = currentStart;
1470 var oldIndex = oldStart;
1471 for (var i = 0; i < ops.length; i++) {
1472 switch(ops[i]) {
1473 case EDIT_LEAVE:
1474 if (splice) {
1475 splices.push(splice);
1476 splice = undefined;
1477 }
1478
1479 index++;
1480 oldIndex++;
1481 break;
1482 case EDIT_UPDATE:
1483 if (!splice)
1484 splice = newSplice(index, [], 0);
1485
1486 splice.addedCount++;
1487 index++;
1488
1489 splice.removed.push(old[oldIndex]);
1490 oldIndex++;
1491 break;
1492 case EDIT_ADD:
1493 if (!splice)
1494 splice = newSplice(index, [], 0);
1495
1496 splice.addedCount++;
1497 index++;
1498 break;
1499 case EDIT_DELETE:
1500 if (!splice)
1501 splice = newSplice(index, [], 0);
1502
1503 splice.removed.push(old[oldIndex]);
1504 oldIndex++;
1505 break;
1506 }
1507 }
1508
1509 if (splice) {
1510 splices.push(splice);
1511 }
1512 return splices;
1513 },
1514
1515 sharedPrefix: function(current, old, searchLength) {
1516 for (var i = 0; i < searchLength; i++)
1517 if (!this.equals(current[i], old[i]))
1518 return i;
1519 return searchLength;
1520 },
1521
1522 sharedSuffix: function(current, old, searchLength) {
1523 var index1 = current.length;
1524 var index2 = old.length;
1525 var count = 0;
1526 while (count < searchLength && this.equals(current[--index1], old[--index2 ]))
1527 count++;
1528
1529 return count;
1530 },
1531
1532 calculateSplices: function(current, previous) {
1533 return this.calcSplices(current, 0, current.length, previous, 0,
1534 previous.length);
1535 },
1536
1537 equals: function(currentValue, previousValue) {
1538 return currentValue === previousValue;
1539 }
1540 };
1541
1542 var arraySplice = new ArraySplice();
1543
1544 function calcSplices(current, currentStart, currentEnd,
1545 old, oldStart, oldEnd) {
1546 return arraySplice.calcSplices(current, currentStart, currentEnd,
1547 old, oldStart, oldEnd);
1548 }
1549
1550 function intersect(start1, end1, start2, end2) {
1551 // Disjoint
1552 if (end1 < start2 || end2 < start1)
1553 return -1;
1554
1555 // Adjacent
1556 if (end1 == start2 || end2 == start1)
1557 return 0;
1558
1559 // Non-zero intersect, span1 first
1560 if (start1 < start2) {
1561 if (end1 < end2)
1562 return end1 - start2; // Overlap
1563 else
1564 return end2 - start2; // Contained
1565 } else {
1566 // Non-zero intersect, span2 first
1567 if (end2 < end1)
1568 return end2 - start1; // Overlap
1569 else
1570 return end1 - start1; // Contained
1571 }
1572 }
1573
1574 function mergeSplice(splices, index, removed, addedCount) {
1575
1576 var splice = newSplice(index, removed, addedCount);
1577
1578 var inserted = false;
1579 var insertionOffset = 0;
1580
1581 for (var i = 0; i < splices.length; i++) {
1582 var current = splices[i];
1583 current.index += insertionOffset;
1584
1585 if (inserted)
1586 continue;
1587
1588 var intersectCount = intersect(splice.index,
1589 splice.index + splice.removed.length,
1590 current.index,
1591 current.index + current.addedCount);
1592
1593 if (intersectCount >= 0) {
1594 // Merge the two splices
1595
1596 splices.splice(i, 1);
1597 i--;
1598
1599 insertionOffset -= current.addedCount - current.removed.length;
1600
1601 splice.addedCount += current.addedCount - intersectCount;
1602 var deleteCount = splice.removed.length +
1603 current.removed.length - intersectCount;
1604
1605 if (!splice.addedCount && !deleteCount) {
1606 // merged splice is a noop. discard.
1607 inserted = true;
1608 } else {
1609 var removed = current.removed;
1610
1611 if (splice.index < current.index) {
1612 // some prefix of splice.removed is prepended to current.removed.
1613 var prepend = splice.removed.slice(0, current.index - splice.index);
1614 Array.prototype.push.apply(prepend, removed);
1615 removed = prepend;
1616 }
1617
1618 if (splice.index + splice.removed.length > current.index + current.add edCount) {
1619 // some suffix of splice.removed is appended to current.removed.
1620 var append = splice.removed.slice(current.index + current.addedCount - splice.index);
1621 Array.prototype.push.apply(removed, append);
1622 }
1623
1624 splice.removed = removed;
1625 if (current.index < splice.index) {
1626 splice.index = current.index;
1627 }
1628 }
1629 } else if (splice.index < current.index) {
1630 // Insert splice here.
1631
1632 inserted = true;
1633
1634 splices.splice(i, 0, splice);
1635 i++;
1636
1637 var offset = splice.addedCount - splice.removed.length
1638 current.index += offset;
1639 insertionOffset += offset;
1640 }
1641 }
1642
1643 if (!inserted)
1644 splices.push(splice);
1645 }
1646
1647 function createInitialSplices(array, changeRecords) {
1648 var splices = [];
1649
1650 for (var i = 0; i < changeRecords.length; i++) {
1651 var record = changeRecords[i];
1652 switch(record.type) {
1653 case 'splice':
1654 mergeSplice(splices, record.index, record.removed.slice(), record.adde dCount);
1655 break;
1656 case 'add':
1657 case 'update':
1658 case 'delete':
1659 if (!isIndex(record.name))
1660 continue;
1661 var index = toNumber(record.name);
1662 if (index < 0)
1663 continue;
1664 mergeSplice(splices, index, [record.oldValue], 1);
1665 break;
1666 default:
1667 console.error('Unexpected record type: ' + JSON.stringify(record));
1668 break;
1669 }
1670 }
1671
1672 return splices;
1673 }
1674
1675 function projectArraySplices(array, changeRecords) {
1676 var splices = [];
1677
1678 createInitialSplices(array, changeRecords).forEach(function(splice) {
1679 if (splice.addedCount == 1 && splice.removed.length == 1) {
1680 if (splice.removed[0] !== array[splice.index])
1681 splices.push(splice);
1682
1683 return
1684 };
1685
1686 splices = splices.concat(calcSplices(array, splice.index, splice.index + s plice.addedCount,
1687 splice.removed, 0, splice.removed.len gth));
1688 });
1689
1690 return splices;
1691 }
1692
1693 global.Observer = Observer;
1694 global.Observer.runEOM_ = runEOM;
1695 global.Observer.observerSentinel_ = observerSentinel; // for testing.
1696 global.Observer.hasObjectObserve = hasObserve;
1697 global.ArrayObserver = ArrayObserver;
1698 global.ArrayObserver.calculateSplices = function(current, previous) {
1699 return arraySplice.calculateSplices(current, previous);
1700 };
1701
1702 global.ArraySplice = ArraySplice;
1703 global.ObjectObserver = ObjectObserver;
1704 global.PathObserver = PathObserver;
1705 global.CompoundObserver = CompoundObserver;
1706 global.Path = Path;
1707 global.ObserverTransform = ObserverTransform;
1708 })(typeof global !== 'undefined' && global && typeof module !== 'undefined' && m odule ? global : this || window);
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698