blob: 5a57d724832a57b716b8344038e2df3730988d4b [file] [log] [blame]
<!--
// 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>