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

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

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

Powered by Google App Engine
This is Rietveld 408576698