Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(75)

Unified Diff: sky/framework/sky-element/sky-binder.sky

Issue 821353003: Switch to SkyBinder (Closed) Base URL: git@github.com:domokit/mojo.git@master
Patch Set: Created 5 years, 12 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « sky/framework/sky-element/TemplateBinding.sky ('k') | sky/framework/sky-element/sky-element.sky » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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>
« no previous file with comments | « sky/framework/sky-element/TemplateBinding.sky ('k') | sky/framework/sky-element/sky-element.sky » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698