| <!-- |
| // Copyright 2014 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| --> |
| <import src="observe.sky" as="observe" /> |
| <import src="element-registry.sky" as="registry" /> |
| |
| <script> |
| var stagingDocument = new Document(); |
| |
| class TemplateInstance { |
| constructor() { |
| this.bindings = []; |
| this.terminator = null; |
| this.fragment = stagingDocument.createDocumentFragment(); |
| Object.preventExtensions(this); |
| } |
| close() { |
| var bindings = this.bindings; |
| for (var i = 0; i < bindings.length; i++) { |
| bindings[i].close(); |
| } |
| } |
| } |
| |
| var emptyInstance = new TemplateInstance(); |
| var directiveCache = new WeakMap(); |
| |
| function createInstance(template, model) { |
| var content = template.content; |
| if (!content.firstChild) |
| return emptyInstance; |
| |
| var directives = directiveCache.get(content); |
| if (!directives) { |
| directives = new NodeDirectives(content); |
| directiveCache.set(content, directives); |
| } |
| |
| var instance = new TemplateInstance(); |
| |
| var length = directives.children.length; |
| for (var i = 0; i < length; ++i) { |
| var clone = directives.children[i].createBoundClone(instance.fragment, |
| model, instance.bindings); |
| |
| // The terminator of the instance is the clone of the last child of the |
| // content. If the last child is an active template, it may produce |
| // instances as a result of production, so simply collecting the last |
| // child of the instance after it has finished producing may be wrong. |
| if (i == length - 1) |
| instance.terminator = clone; |
| } |
| |
| return instance; |
| } |
| |
| function sanitizeValue(value) { |
| return value == null ? '' : value; |
| } |
| |
| function updateText(node, value) { |
| node.data = sanitizeValue(value); |
| } |
| |
| function updateAttribute(element, name, value) { |
| element.setAttribute(name, sanitizeValue(value)); |
| } |
| |
| class BindingExpression { |
| constructor(prefix, path) { |
| this.prefix = prefix; |
| this.path = observe.Path.get(path); |
| Object.preventExtensions(this); |
| } |
| } |
| |
| class PropertyDirective { |
| constructor(name) { |
| this.name = name; |
| this.expressions = []; |
| this.suffix = ""; |
| Object.preventExtensions(this); |
| } |
| createObserver(model) { |
| var expressions = this.expressions; |
| var suffix = this.suffix; |
| |
| if (expressions.length == 1 && expressions[0].prefix == "" && suffix == "") |
| return new observe.PathObserver(model, expressions[0].path); |
| |
| var observer = new observe.CompoundObserver(); |
| |
| for (var i = 0; i < expressions.length; ++i) |
| observer.addPath(model, expressions[i].path); |
| |
| return new observe.ObserverTransform(observer, function(values) { |
| var buffer = ""; |
| for (var i = 0; i < values.length; ++i) { |
| buffer += expressions[i].prefix; |
| buffer += values[i]; |
| } |
| buffer += suffix; |
| return buffer; |
| }); |
| } |
| bindProperty(node, model) { |
| var name = this.name; |
| var observable = this.createObserver(model); |
| if (node instanceof Text) { |
| updateText(node, observable.open(function(value) { |
| return updateText(node, value); |
| })); |
| } else if (name == 'style' || name == 'class') { |
| updateAttribute(node, name, observable.open(function(value) { |
| updateAttribute(node, name, value); |
| })); |
| } else { |
| node[name] = observable.open(function(value) { |
| node[name] = value; |
| }); |
| } |
| if (typeof node.addPropertyBinding == 'function') |
| node.addPropertyBinding(this.name, observable); |
| return observable; |
| } |
| } |
| |
| function parsePropertyDirective(value, property) { |
| if (!value || !value.length) |
| return; |
| |
| var result; |
| var offset = 0; |
| var firstIndex = 0; |
| var lastIndex = 0; |
| |
| while (offset < value.length) { |
| firstIndex = value.indexOf('{{', offset); |
| if (firstIndex == -1) |
| break; |
| lastIndex = value.indexOf('}}', firstIndex + 2); |
| if (lastIndex == -1) |
| lastIndex = value.length; |
| var prefix = value.substring(offset, firstIndex); |
| var path = value.substring(firstIndex + 2, lastIndex); |
| offset = lastIndex + 2; |
| if (!result) |
| result = new PropertyDirective(property); |
| result.expressions.push(new BindingExpression(prefix, path)); |
| } |
| |
| if (result && offset < value.length) |
| result.suffix = value.substring(offset); |
| |
| return result; |
| } |
| |
| function parseAttributeDirectives(element, directives) { |
| var attributes = element.getAttributes(); |
| var tagName = element.tagName; |
| |
| for (var i = 0; i < attributes.length; i++) { |
| var attr = attributes[i]; |
| var name = attr.name; |
| var value = attr.value; |
| |
| if (name.startsWith('on-')) { |
| directives.eventHandlers.push(name.substring(3)); |
| continue; |
| } |
| |
| if (!registry.checkAttribute(tagName, name)) { |
| console.error('Element "'+ tagName + |
| '" has unknown attribute "' + name + '".'); |
| } |
| |
| var property = parsePropertyDirective(value, name); |
| if (!property) |
| continue; |
| |
| directives.properties.push(property); |
| } |
| } |
| |
| function eventHandlerCallback(event) { |
| var element = event.currentTarget; |
| var method = element.getAttribute('on-' + event.type); |
| var scope = element.ownerScope; |
| var host = scope.host; |
| var handler = host && host[method]; |
| if (handler instanceof Function) |
| return handler.call(host, event); |
| } |
| |
| class NodeDirectives { |
| constructor(node) { |
| this.eventHandlers = []; |
| this.children = []; |
| this.properties = []; |
| this.node = node; |
| Object.preventExtensions(this); |
| |
| if (node instanceof Element) { |
| parseAttributeDirectives(node, this); |
| } else if (node instanceof Text) { |
| var property = parsePropertyDirective(node.data, 'textContent'); |
| if (property) |
| this.properties.push(property); |
| } |
| |
| for (var child = node.firstChild; child; child = child.nextSibling) { |
| this.children.push(new NodeDirectives(child)); |
| } |
| } |
| findProperty(name) { |
| for (var i = 0; i < this.properties.length; ++i) { |
| if (this.properties[i].name === name) |
| return this.properties[i]; |
| } |
| return null; |
| } |
| createBoundClone(parent, model, bindings) { |
| // TODO(esprehn): In sky instead of needing to use a staging docuemnt per |
| // custom element registry we're going to need to use the current module's |
| // registry. |
| var clone = stagingDocument.importNode(this.node, false); |
| |
| for (var i = 0; i < this.eventHandlers.length; ++i) { |
| clone.addEventListener(this.eventHandlers[i], eventHandlerCallback); |
| } |
| |
| var clone = parent.appendChild(clone); |
| |
| for (var i = 0; i < this.children.length; ++i) { |
| this.children[i].createBoundClone(clone, model, bindings); |
| } |
| |
| for (var i = 0; i < this.properties.length; ++i) { |
| bindings.push(this.properties[i].bindProperty(clone, model)); |
| } |
| |
| if (clone instanceof HTMLTemplateElement) { |
| var iterator = new TemplateIterator(clone); |
| iterator.updateDependencies(this, model); |
| bindings.push(iterator); |
| } |
| |
| return clone; |
| } |
| } |
| |
| var iterators = new WeakMap(); |
| |
| class TemplateIterator { |
| constructor(element) { |
| this.closed = false; |
| this.template = element; |
| this.contentTemplate = null; |
| this.instances = []; |
| this.hasRepeat = false; |
| this.ifObserver = null; |
| this.valueObserver = null; |
| this.iteratedValue = []; |
| this.presentValue = null; |
| this.arrayObserver = null; |
| Object.preventExtensions(this); |
| iterators.set(element, this); |
| } |
| |
| updateDependencies(directives, model) { |
| this.contentTemplate = directives.node; |
| |
| var ifValue = true; |
| var ifProperty = directives.findProperty('if'); |
| if (ifProperty) { |
| this.ifObserver = ifProperty.createObserver(model); |
| ifValue = this.ifObserver.open(this.updateIfValue, this); |
| } |
| |
| var repeatProperty = directives.findProperty('repeat'); |
| if (repeatProperty) { |
| this.hasRepeat = true; |
| this.valueObserver = repeatProperty.createObserver(model); |
| } else { |
| var path = observe.Path.get(""); |
| this.valueObserver = new observe.PathObserver(model, path); |
| } |
| |
| var value = this.valueObserver.open(this.updateIteratedValue, this); |
| this.updateValue(ifValue ? value : null); |
| } |
| |
| getUpdatedValue() { |
| return this.valueObserver.discardChanges(); |
| } |
| |
| updateIfValue(ifValue) { |
| if (!ifValue) { |
| this.valueChanged(); |
| return; |
| } |
| |
| this.updateValue(this.getUpdatedValue()); |
| } |
| |
| updateIteratedValue(value) { |
| if (this.ifObserver) { |
| var ifValue = this.ifObserver.discardChanges(); |
| if (!ifValue) { |
| this.valueChanged(); |
| return; |
| } |
| } |
| |
| this.updateValue(value); |
| } |
| |
| updateValue(value) { |
| if (!this.hasRepeat) |
| value = [value]; |
| var observe = this.hasRepeat && Array.isArray(value); |
| this.valueChanged(value, observe); |
| } |
| |
| valueChanged(value, observeValue) { |
| if (!Array.isArray(value)) |
| value = []; |
| |
| if (value === this.iteratedValue) |
| return; |
| |
| this.unobserve(); |
| this.presentValue = value; |
| if (observeValue) { |
| this.arrayObserver = new observe.ArrayObserver(this.presentValue); |
| this.arrayObserver.open(this.handleSplices, this); |
| } |
| |
| this.handleSplices(observe.ArrayObserver.calculateSplices(this.presentValue, |
| this.iteratedValue)); |
| } |
| |
| getLastInstanceNode(index) { |
| if (index == -1) |
| return this.template; |
| var instance = this.instances[index]; |
| var terminator = instance.terminator; |
| if (!terminator) |
| return this.getLastInstanceNode(index - 1); |
| |
| if (!(terminator instanceof Element) || this.template === terminator) { |
| return terminator; |
| } |
| |
| var subtemplateIterator = iterators.get(terminator); |
| if (!subtemplateIterator) |
| return terminator; |
| |
| return subtemplateIterator.getLastTemplateNode(); |
| } |
| |
| getLastTemplateNode() { |
| return this.getLastInstanceNode(this.instances.length - 1); |
| } |
| |
| insertInstanceAt(index, instance) { |
| var previousInstanceLast = this.getLastInstanceNode(index - 1); |
| var parent = this.template.parentNode; |
| this.instances.splice(index, 0, instance); |
| parent.insertBefore(instance.fragment, previousInstanceLast.nextSibling); |
| } |
| |
| extractInstanceAt(index) { |
| var previousInstanceLast = this.getLastInstanceNode(index - 1); |
| var lastNode = this.getLastInstanceNode(index); |
| var parent = this.template.parentNode; |
| var instance = this.instances.splice(index, 1)[0]; |
| |
| while (lastNode !== previousInstanceLast) { |
| var node = previousInstanceLast.nextSibling; |
| if (node == lastNode) |
| lastNode = previousInstanceLast; |
| |
| instance.fragment.appendChild(parent.removeChild(node)); |
| } |
| |
| return instance; |
| } |
| |
| handleSplices(splices) { |
| if (this.closed || !splices.length) |
| return; |
| |
| var template = this.template; |
| |
| if (!template.parentNode) { |
| this.close(); |
| return; |
| } |
| |
| observe.ArrayObserver.applySplices(this.iteratedValue, this.presentValue, |
| splices); |
| |
| // Instance Removals |
| var instanceCache = new Map; |
| var removeDelta = 0; |
| for (var i = 0; i < splices.length; i++) { |
| var splice = splices[i]; |
| var removed = splice.removed; |
| for (var j = 0; j < removed.length; j++) { |
| var model = removed[j]; |
| var instance = this.extractInstanceAt(splice.index + removeDelta); |
| if (instance !== emptyInstance) { |
| instanceCache.set(model, instance); |
| } |
| } |
| |
| removeDelta -= splice.addedCount; |
| } |
| |
| // Instance Insertions |
| for (var i = 0; i < splices.length; i++) { |
| var splice = splices[i]; |
| var addIndex = splice.index; |
| for (; addIndex < splice.index + splice.addedCount; addIndex++) { |
| var model = this.iteratedValue[addIndex]; |
| var instance = instanceCache.get(model); |
| if (instance) { |
| instanceCache.delete(model); |
| } else { |
| if (model === undefined || model === null) { |
| instance = emptyInstance; |
| } else { |
| instance = createInstance(this.contentTemplate, model); |
| } |
| } |
| |
| this.insertInstanceAt(addIndex, instance); |
| } |
| } |
| |
| instanceCache.forEach(function(instance) { |
| instance.close(); |
| }); |
| } |
| |
| unobserve() { |
| if (!this.arrayObserver) |
| return; |
| |
| this.arrayObserver.close(); |
| this.arrayObserver = null; |
| } |
| |
| close() { |
| if (this.closed) |
| return; |
| this.unobserve(); |
| for (var i = 0; i < this.instances.length; i++) { |
| this.instances[i].close(); |
| } |
| |
| this.instances.length = 0; |
| |
| if (this.ifObserver) |
| this.ifObserver.close(); |
| if (this.valueObserver) |
| this.valueObserver.close(); |
| |
| iterators.delete(this.template); |
| this.closed = true; |
| } |
| } |
| |
| module.exports = { |
| createInstance: createInstance, |
| }; |
| </script> |