| 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
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..a67861de4d7c72d3419f2c3b6af55b2ae86ec093
|
| --- /dev/null
|
| +++ b/sky/framework/sky-element/sky-binder.sky
|
| @@ -0,0 +1,471 @@
|
| +<!--
|
| +// 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" />
|
| +
|
| +<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;
|
| + });
|
| + }
|
| + 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();
|
| +
|
| + 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;
|
| + }
|
| +
|
| + 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.ifBinding)
|
| + this.ifBinding.close();
|
| + if (this.binding)
|
| + this.binding.close();
|
| +
|
| + iterators.delete(this.template);
|
| + this.closed = true;
|
| + }
|
| +}
|
| +
|
| +module.exports = {
|
| + createInstance: createInstance,
|
| +};
|
| +</script>
|
|
|