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