Index: sky/framework/sky-element/sky-binder.sky |
diff --git a/sky/framework/sky-element/sky-binder.sky b/sky/framework/sky-element/sky-binder.sky |
deleted file mode 100644 |
index bfef1ab86162231ac2ec85065e80eb031a9d4b5e..0000000000000000000000000000000000000000 |
--- a/sky/framework/sky-element/sky-binder.sky |
+++ /dev/null |
@@ -1,498 +0,0 @@ |
-<!-- |
-// 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) |
- directives.properties.push(property); |
- } |
-} |
- |
-function createCloneSource(element, properties) { |
- if (!properties.length) |
- return element; |
- |
- // Leave attributes alone on template so you can see the if/repeat statements |
- // in the inspector. |
- if (element instanceof HTMLTemplateElement) |
- return element; |
- |
- var result = element.cloneNode(false); |
- |
- for (var i = 0; i < properties.length; ++i) { |
- result.removeAttribute(properties[i].name); |
- } |
- |
- return result; |
-} |
- |
-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; |
- this.cloneSourceNode = node; |
- Object.preventExtensions(this); |
- |
- if (node instanceof Element) { |
- parseAttributeDirectives(node, this); |
- this.cloneSourceNode = createCloneSource(node, this.properties); |
- } 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.cloneSourceNode, false); |
- |
- for (var i = 0; i < this.eventHandlers.length; ++i) { |
- clone.addEventListener(this.eventHandlers[i], eventHandlerCallback); |
- } |
- |
- for (var i = 0; i < this.properties.length; ++i) { |
- bindings.push(this.properties[i].bindProperty(clone, model)); |
- } |
- |
- parent.appendChild(clone); |
- |
- for (var i = 0; i < this.children.length; ++i) { |
- this.children[i].createBoundClone(clone, model, bindings); |
- } |
- |
- 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> |