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

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

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

Powered by Google App Engine
This is Rietveld 408576698