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

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

Issue 821353003: Switch to SkyBinder (Closed) Base URL: git@github.com:domokit/mojo.git@master
Patch Set: Created 5 years, 11 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 <import src="observe.sky" as="observe" />
7
8 <script>
9 var stagingDocument = new Document();
10
11 class TemplateInstance {
12 constructor() {
13 this.bindings = [];
14 this.terminator = null;
15 this.fragment = stagingDocument.createDocumentFragment();
16 Object.preventExtensions(this);
17 }
18 close() {
19 var bindings = this.bindings;
20 for (var i = 0; i < bindings.length; i++) {
21 bindings[i].close();
22 }
23 }
24 }
25
26 var emptyInstance = new TemplateInstance();
27 var directiveCache = new WeakMap();
28
29 function createInstance(template, model) {
30 var content = template.content;
31 if (!content.firstChild)
32 return emptyInstance;
33
34 var directives = directiveCache.get(content);
35 if (!directives) {
36 directives = new NodeDirectives(content);
37 directiveCache.set(content, directives);
38 }
39
40 var instance = new TemplateInstance();
41
42 var length = directives.children.length;
43 for (var i = 0; i < length; ++i) {
44 var clone = directives.children[i].createBoundClone(instance.fragment,
45 model, instance.bindings);
46
47 // The terminator of the instance is the clone of the last child of the
48 // content. If the last child is an active template, it may produce
49 // instances as a result of production, so simply collecting the last
50 // child of the instance after it has finished producing may be wrong.
51 if (i == length - 1)
52 instance.terminator = clone;
53 }
54
55 return instance;
56 }
57
58 function sanitizeValue(value) {
59 return value == null ? '' : value;
60 }
61
62 function updateText(node, value) {
63 node.data = sanitizeValue(value);
64 }
65
66 function updateAttribute(element, name, value) {
67 element.setAttribute(name, sanitizeValue(value));
68 }
69
70 class BindingExpression {
71 constructor(prefix, path) {
72 this.prefix = prefix;
73 this.path = observe.Path.get(path);
74 Object.preventExtensions(this);
75 }
76 }
77
78 class PropertyDirective {
79 constructor(name) {
80 this.name = name;
81 this.expressions = [];
82 this.suffix = "";
83 Object.preventExtensions(this);
84 }
85 createObserver(model) {
86 var expressions = this.expressions;
87 var suffix = this.suffix;
88
89 if (expressions.length == 1 && expressions[0].prefix == "" && suffix == "")
90 return new observe.PathObserver(model, expressions[0].path);
91
92 var observer = new observe.CompoundObserver();
93
94 for (var i = 0; i < expressions.length; ++i)
95 observer.addPath(model, expressions[i].path);
96
97 return new observe.ObserverTransform(observer, function(values) {
98 var buffer = "";
99 for (var i = 0; i < values.length; ++i) {
100 buffer += expressions[i].prefix;
101 buffer += values[i];
102 }
103 buffer += suffix;
104 return buffer;
105 });
106 }
107 bindProperty(node, model) {
108 var name = this.name;
109 var observable = this.createObserver(model);
110 if (node instanceof Text) {
111 updateText(node, observable.open(function(value) {
112 return updateText(node, value);
113 }));
114 } else if (name == 'style' || name == 'class') {
115 updateAttribute(node, name, observable.open(function(value) {
116 updateAttribute(node, name, value);
117 }));
118 } else {
119 node[name] = observable.open(function(value) {
120 node[name] = value;
121 });
122 }
123 return observable;
124 }
125 }
126
127 function parsePropertyDirective(value, property) {
128 if (!value || !value.length)
129 return;
130
131 var result;
132 var offset = 0;
133 var firstIndex = 0;
134 var lastIndex = 0;
135
136 while (offset < value.length) {
137 firstIndex = value.indexOf('{{', offset);
138 if (firstIndex == -1)
139 break;
140 lastIndex = value.indexOf('}}', firstIndex + 2);
141 if (lastIndex == -1)
142 lastIndex = value.length;
143 var prefix = value.substring(offset, firstIndex);
144 var path = value.substring(firstIndex + 2, lastIndex);
145 offset = lastIndex + 2;
146 if (!result)
147 result = new PropertyDirective(property);
148 result.expressions.push(new BindingExpression(prefix, path));
149 }
150
151 if (result && offset < value.length)
152 result.suffix = value.substring(offset);
153
154 return result;
155 }
156
157 function parseAttributeDirectives(element, directives) {
158 var attributes = element.getAttributes();
159
160 for (var i = 0; i < attributes.length; i++) {
161 var attr = attributes[i];
162 var name = attr.name;
163 var value = attr.value;
164
165 if (name.startsWith('on-')) {
166 directives.eventHandlers.push(name.substring(3));
167 continue;
168 }
169
170 var property = parsePropertyDirective(value, name);
171 if (!property)
172 continue;
173
174 directives.properties.push(property);
175 }
176 }
177
178 function eventHandlerCallback(event) {
179 var element = event.currentTarget;
180 var method = element.getAttribute('on-' + event.type);
181 var scope = element.ownerScope;
182 var host = scope.host;
183 var handler = host && host[method];
184 if (handler instanceof Function)
185 return handler.call(host, event);
186 }
187
188 class NodeDirectives {
189 constructor(node) {
190 this.eventHandlers = [];
191 this.children = [];
192 this.properties = [];
193 this.node = node;
194 Object.preventExtensions(this);
195
196 if (node instanceof Element) {
197 parseAttributeDirectives(node, this);
198 } else if (node instanceof Text) {
199 var property = parsePropertyDirective(node.data, 'textContent');
200 if (property)
201 this.properties.push(property);
202 }
203
204 for (var child = node.firstChild; child; child = child.nextSibling) {
205 this.children.push(new NodeDirectives(child));
206 }
207 }
208 findProperty(name) {
209 for (var i = 0; i < this.properties.length; ++i) {
210 if (this.properties[i].name === name)
211 return this.properties[i];
212 }
213 return null;
214 }
215 createBoundClone(parent, model, bindings) {
216 // TODO(esprehn): In sky instead of needing to use a staging docuemnt per
217 // custom element registry we're going to need to use the current module's
218 // registry.
219 var clone = stagingDocument.importNode(this.node, false);
220
221 for (var i = 0; i < this.eventHandlers.length; ++i) {
222 clone.addEventListener(this.eventHandlers[i], eventHandlerCallback);
223 }
224
225 var clone = parent.appendChild(clone);
226
227 for (var i = 0; i < this.children.length; ++i) {
228 this.children[i].createBoundClone(clone, model, bindings);
229 }
230
231 for (var i = 0; i < this.properties.length; ++i) {
232 bindings.push(this.properties[i].bindProperty(clone, model));
233 }
234
235 if (clone instanceof HTMLTemplateElement) {
236 var iterator = new TemplateIterator(clone);
237 iterator.updateDependencies(this, model);
238 bindings.push(iterator);
239 }
240
241 return clone;
242 }
243 }
244
245 var iterators = new WeakMap();
246
247 class TemplateIterator {
248 constructor(element) {
249 this.closed = false;
250 this.template = element;
251 this.contentTemplate = null;
252 this.instances = [];
253 this.hasRepeat = false;
254 this.ifObserver = null;
255 this.valueObserver = null;
256 this.iteratedValue = [];
257 this.presentValue = null;
258 this.arrayObserver = null;
259 Object.preventExtensions(this);
260 iterators.set(element, this);
261 }
262
263 updateDependencies(directives, model) {
264 this.contentTemplate = directives.node;
265
266 var ifValue = true;
267 var ifProperty = directives.findProperty('if');
268 if (ifProperty) {
269 this.ifObserver = ifProperty.createObserver(model);
270 ifValue = this.ifObserver.open(this.updateIfValue, this);
271 }
272
273 var repeatProperty = directives.findProperty('repeat');
274 if (repeatProperty) {
275 this.hasRepeat = true;
276 this.valueObserver = repeatProperty.createObserver(model);
277 } else {
278 var path = observe.Path.get("");
279 this.valueObserver = new observe.PathObserver(model, path);
280 }
281
282 var value = this.valueObserver.open(this.updateIteratedValue, this);
283 this.updateValue(ifValue ? value : null);
284 }
285
286 getUpdatedValue() {
287 return this.valueObserver.discardChanges();
288 }
289
290 updateIfValue(ifValue) {
291 if (!ifValue) {
292 this.valueChanged();
293 return;
294 }
295
296 this.updateValue(this.getUpdatedValue());
297 }
298
299 updateIteratedValue(value) {
300 if (this.ifObserver) {
301 var ifValue = this.ifObserver.discardChanges();
302 if (!ifValue) {
303 this.valueChanged();
304 return;
305 }
306 }
307
308 this.updateValue(value);
309 }
310
311 updateValue(value) {
312 if (!this.hasRepeat)
313 value = [value];
314 var observe = this.hasRepeat && Array.isArray(value);
315 this.valueChanged(value, observe);
316 }
317
318 valueChanged(value, observeValue) {
319 if (!Array.isArray(value))
320 value = [];
321
322 if (value === this.iteratedValue)
323 return;
324
325 this.unobserve();
326 this.presentValue = value;
327 if (observeValue) {
328 this.arrayObserver = new observe.ArrayObserver(this.presentValue);
329 this.arrayObserver.open(this.handleSplices, this);
330 }
331
332 this.handleSplices(observe.ArrayObserver.calculateSplices(this.presentValue,
333 this.iteratedValue));
334 }
335
336 getLastInstanceNode(index) {
337 if (index == -1)
338 return this.template;
339 var instance = this.instances[index];
340 var terminator = instance.terminator;
341 if (!terminator)
342 return this.getLastInstanceNode(index - 1);
343
344 if (!(terminator instanceof Element) || this.template === terminator) {
345 return terminator;
346 }
347
348 var subtemplateIterator = iterators.get(terminator);
349 if (!subtemplateIterator)
350 return terminator;
351
352 return subtemplateIterator.getLastTemplateNode();
353 }
354
355 getLastTemplateNode() {
356 return this.getLastInstanceNode(this.instances.length - 1);
357 }
358
359 insertInstanceAt(index, instance) {
360 var previousInstanceLast = this.getLastInstanceNode(index - 1);
361 var parent = this.template.parentNode;
362 this.instances.splice(index, 0, instance);
363 parent.insertBefore(instance.fragment, previousInstanceLast.nextSibling);
364 }
365
366 extractInstanceAt(index) {
367 var previousInstanceLast = this.getLastInstanceNode(index - 1);
368 var lastNode = this.getLastInstanceNode(index);
369 var parent = this.template.parentNode;
370 var instance = this.instances.splice(index, 1)[0];
371
372 while (lastNode !== previousInstanceLast) {
373 var node = previousInstanceLast.nextSibling;
374 if (node == lastNode)
375 lastNode = previousInstanceLast;
376
377 instance.fragment.appendChild(parent.removeChild(node));
378 }
379
380 return instance;
381 }
382
383 handleSplices(splices) {
384 if (this.closed || !splices.length)
385 return;
386
387 var template = this.template;
388
389 if (!template.parentNode) {
390 this.close();
391 return;
392 }
393
394 observe.ArrayObserver.applySplices(this.iteratedValue, this.presentValue,
395 splices);
396
397 // Instance Removals
398 var instanceCache = new Map;
399 var removeDelta = 0;
400 for (var i = 0; i < splices.length; i++) {
401 var splice = splices[i];
402 var removed = splice.removed;
403 for (var j = 0; j < removed.length; j++) {
404 var model = removed[j];
405 var instance = this.extractInstanceAt(splice.index + removeDelta);
406 if (instance !== emptyInstance) {
407 instanceCache.set(model, instance);
408 }
409 }
410
411 removeDelta -= splice.addedCount;
412 }
413
414 // Instance Insertions
415 for (var i = 0; i < splices.length; i++) {
416 var splice = splices[i];
417 var addIndex = splice.index;
418 for (; addIndex < splice.index + splice.addedCount; addIndex++) {
419 var model = this.iteratedValue[addIndex];
420 var instance = instanceCache.get(model);
421 if (instance) {
422 instanceCache.delete(model);
423 } else {
424 if (model === undefined || model === null) {
425 instance = emptyInstance;
426 } else {
427 instance = createInstance(this.contentTemplate, model);
428 }
429 }
430
431 this.insertInstanceAt(addIndex, instance);
432 }
433 }
434
435 instanceCache.forEach(function(instance) {
436 instance.close();
437 });
438 }
439
440 unobserve() {
441 if (!this.arrayObserver)
442 return;
443
444 this.arrayObserver.close();
445 this.arrayObserver = null;
446 }
447
448 close() {
449 if (this.closed)
450 return;
451 this.unobserve();
452 for (var i = 0; i < this.instances.length; i++) {
453 this.instances[i].close();
454 }
455
456 this.instances.length = 0;
457
458 if (this.ifBinding)
459 this.ifBinding.close();
460 if (this.binding)
461 this.binding.close();
462
463 iterators.delete(this.template);
464 this.closed = true;
465 }
466 }
467
468 module.exports = {
469 createInstance: createInstance,
470 };
471 </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