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

Side by Side Diff: sky/framework/sky-element/observe.sky

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

Powered by Google App Engine
This is Rietveld 408576698