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