| <!-- | 
 | // 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> |