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

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

Issue 698653002: Add initial SkyElement & city-list example (Closed) Base URL: https://github.com/domokit/mojo.git@master
Patch Set: ws Created 6 years, 2 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/examples/city-list/index.sky ('k') | sky/framework/sky-element/observe.sky » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: sky/framework/sky-element/TemplateBinding.sky
diff --git a/sky/framework/sky-element/TemplateBinding.sky b/sky/framework/sky-element/TemplateBinding.sky
new file mode 100644
index 0000000000000000000000000000000000000000..292c71f5a9b438f7cc1d8a9565a1447c2314cfcd
--- /dev/null
+++ b/sky/framework/sky-element/TemplateBinding.sky
@@ -0,0 +1,1003 @@
+<!--
+// 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.
+-->
+<link rel="import" href="observe.sky" as="observe" />
+
+<script>
+Node.prototype.bind = function(name, observable, oneTime) {
+ var self = this;
+
+ if (oneTime) {
+ this[name] = observable;
+ return;
+ }
+
+ observable.open(function(value) {
+ self[name] = value;
+ });
+
+ return observable;
+};
+
+function sanitizeValue(value) {
+ return value == null ? '' : value;
+}
+
+function updateText(node, value) {
+ node.data = sanitizeValue(value);
+}
+
+function textBinding(node) {
+ return function(value) {
+ return updateText(node, value);
+ };
+}
+
+Text.prototype.bind = function(name, value, oneTime) {
+ if (name !== 'textContent')
+ return Node.prototype.bind.call(this, name, value, oneTime);
+
+ if (oneTime)
+ return updateText(this, value);
+
+ var observable = value;
+ updateText(this, observable.open(textBinding(this)));
+ return observable;
+}
+
+function updateAttribute(el, name, value) {
+ el.setAttribute(name, sanitizeValue(value));
+}
+
+function attributeBinding(el, name) {
+ return function(value) {
+ updateAttribute(el, name, value);
+ };
+}
+
+Element.prototype.bind = function(name, value, oneTime) {
+ if (name !== 'style' && name !== 'class')
+ return Node.prototype.bind.call(this, name, value, oneTime);
+
+ if (oneTime)
+ return updateAttribute(this, name, value);
+
+ var observable = value;
+ updateAttribute(this, name, observable.open(attributeBinding(this, name)));
+ return observable;
+}
+
+function getFragmentRoot(node) {
+ var p;
+ while (p = node.parentNode) {
+ node = p;
+ }
+
+ return node;
+}
+
+function searchRefId(node, id) {
+ if (!id)
+ return;
+
+ var ref;
+ var selector = '#' + id;
+ while (!ref) {
+ node = getFragmentRoot(node);
+
+ if (node.protoContent_)
+ ref = node.protoContent_.querySelector(selector);
+ else if (node.getElementById)
+ ref = node.getElementById(id);
+
+ if (ref || !node.templateCreator_)
+ break
+
+ node = node.templateCreator_;
+ }
+
+ return ref;
+}
+
+function getInstanceRoot(node) {
+ while (node.parentNode) {
+ node = node.parentNode;
+ }
+ return node.templateCreator_ ? node : null;
+}
+
+var BIND = 'bind';
+var REPEAT = 'repeat';
+var IF = 'if';
+
+var templateAttributeDirectives = {
+ 'template': true,
+ 'repeat': true,
+ 'bind': true,
+ 'ref': true
+};
+
+function isTemplate(el) {
+ if (el.isTemplate_ === undefined)
+ el.isTemplate_ = el.tagName == 'TEMPLATE';
+
+ return el.isTemplate_;
+}
+
+function mixin(to, from) {
+ Object.getOwnPropertyNames(from).forEach(function(name) {
+ Object.defineProperty(to, name,
+ Object.getOwnPropertyDescriptor(from, name));
+ });
+}
+
+function getTemplateStagingDocument(template) {
+ if (!template.stagingDocument_) {
+ var owner = template.ownerDocument;
+ if (!owner.stagingDocument_) {
+ owner.stagingDocument_ = owner.implementation.createDocument('');
+ owner.stagingDocument_.isStagingDocument = true;
+ owner.stagingDocument_.stagingDocument_ = owner.stagingDocument_;
+ }
+
+ template.stagingDocument_ = owner.stagingDocument_;
+ }
+
+ return template.stagingDocument_;
+}
+
+var templateObserver;
+if (typeof MutationObserver == 'function') {
+ templateObserver = new MutationObserver(function(records) {
+ for (var i = 0; i < records.length; i++) {
+ records[i].target.refChanged_();
+ }
+ });
+}
+
+var contentDescriptor = {
+ get: function() {
+ return this.content_;
+ },
+ enumerable: true,
+ configurable: true
+};
+
+function ensureSetModelScheduled(template) {
+ if (!template.setModelFn_) {
+ template.setModelFn_ = function() {
+ template.setModelFnScheduled_ = false;
+ var map = getBindings(template,
+ template.delegate_ && template.delegate_.prepareBinding);
+ processBindings(template, map, template.model_);
+ };
+ }
+
+ if (!template.setModelFnScheduled_) {
+ template.setModelFnScheduled_ = true;
+ Observer.runEOM_(template.setModelFn_);
+ }
+}
+
+mixin(HTMLTemplateElement.prototype, {
+ bind: function(name, value, oneTime) {
+ if (name != 'ref')
+ return Element.prototype.bind.call(this, name, value, oneTime);
+
+ var self = this;
+ var ref = oneTime ? value : value.open(function(ref) {
+ self.setAttribute('ref', ref);
+ self.refChanged_();
+ });
+
+ this.setAttribute('ref', ref);
+ this.refChanged_();
+ if (oneTime)
+ return;
+
+ if (!this.bindings_) {
+ this.bindings_ = { ref: value };
+ } else {
+ this.bindings_.ref = value;
+ }
+
+ return value;
+ },
+
+ processBindingDirectives_: function(directives) {
+ if (this.iterator_)
+ this.iterator_.closeDeps();
+
+ if (!directives.if && !directives.bind && !directives.repeat) {
+ if (this.iterator_) {
+ this.iterator_.close();
+ this.iterator_ = undefined;
+ }
+
+ return;
+ }
+
+ if (!this.iterator_) {
+ this.iterator_ = new TemplateIterator(this);
+ }
+
+ this.iterator_.updateDependencies(directives, this.model_);
+
+ if (templateObserver) {
+ templateObserver.observe(this, { attributes: true,
+ attributeFilter: ['ref'] });
+ }
+
+ return this.iterator_;
+ },
+
+ createInstance: function(model, bindingDelegate, delegate_) {
+ if (bindingDelegate)
+ delegate_ = this.newDelegate_(bindingDelegate);
+ else if (!delegate_)
+ delegate_ = this.delegate_;
+
+ if (!this.refContent_)
+ this.refContent_ = this.ref_.content;
+ var content = this.refContent_;
+ if (content.firstChild === null)
+ return emptyInstance;
+
+ var map = getInstanceBindingMap(content, delegate_);
+ var stagingDocument = getTemplateStagingDocument(this);
+ var instance = stagingDocument.createDocumentFragment();
+ instance.templateCreator_ = this;
+ instance.protoContent_ = content;
+ instance.bindings_ = [];
+ instance.terminator_ = null;
+ var instanceRecord = instance.templateInstance_ = {
+ firstNode: null,
+ lastNode: null,
+ model: model
+ };
+
+ var i = 0;
+ var collectTerminator = false;
+ for (var child = content.firstChild; child; child = child.nextSibling) {
+ // 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 (child.nextSibling === null)
+ collectTerminator = true;
+
+ var clone = cloneAndBindInstance(child, instance, stagingDocument,
+ map.children[i++],
+ model,
+ delegate_,
+ instance.bindings_);
+ clone.templateInstance_ = instanceRecord;
+ if (collectTerminator)
+ instance.terminator_ = clone;
+ }
+
+ instanceRecord.firstNode = instance.firstChild;
+ instanceRecord.lastNode = instance.lastChild;
+ instance.templateCreator_ = undefined;
+ instance.protoContent_ = undefined;
+ return instance;
+ },
+
+ get model() {
+ return this.model_;
+ },
+
+ set model(model) {
+ this.model_ = model;
+ ensureSetModelScheduled(this);
+ },
+
+ get bindingDelegate() {
+ return this.delegate_ && this.delegate_.raw;
+ },
+
+ refChanged_: function() {
+ if (!this.iterator_ || this.refContent_ === this.ref_.content)
+ return;
+
+ this.refContent_ = undefined;
+ this.iterator_.valueChanged();
+ this.iterator_.updateIteratedValue(this.iterator_.getUpdatedValue());
+ },
+
+ clear: function() {
+ this.model_ = undefined;
+ this.delegate_ = undefined;
+ if (this.bindings_ && this.bindings_.ref)
+ this.bindings_.ref.close()
+ this.refContent_ = undefined;
+ if (!this.iterator_)
+ return;
+ this.iterator_.valueChanged();
+ this.iterator_.close()
+ this.iterator_ = undefined;
+ },
+
+ setDelegate_: function(delegate) {
+ this.delegate_ = delegate;
+ this.bindingMap_ = undefined;
+ if (this.iterator_) {
+ this.iterator_.instancePositionChangedFn_ = undefined;
+ this.iterator_.instanceModelFn_ = undefined;
+ }
+ },
+
+ newDelegate_: function(bindingDelegate) {
+ if (!bindingDelegate)
+ return;
+
+ function delegateFn(name) {
+ var fn = bindingDelegate && bindingDelegate[name];
+ if (typeof fn != 'function')
+ return;
+
+ return function() {
+ return fn.apply(bindingDelegate, arguments);
+ };
+ }
+
+ return {
+ bindingMaps: {},
+ raw: bindingDelegate,
+ prepareBinding: delegateFn('prepareBinding'),
+ prepareInstanceModel: delegateFn('prepareInstanceModel'),
+ prepareInstancePositionChanged:
+ delegateFn('prepareInstancePositionChanged')
+ };
+ },
+
+ set bindingDelegate(bindingDelegate) {
+ if (this.delegate_) {
+ throw Error('Template must be cleared before a new bindingDelegate ' +
+ 'can be assigned');
+ }
+
+ this.setDelegate_(this.newDelegate_(bindingDelegate));
+ },
+
+ get ref_() {
+ var ref = searchRefId(this, this.getAttribute('ref'));
+ if (!ref)
+ ref = this.instanceRef_;
+
+ if (!ref)
+ return this;
+
+ var nextRef = ref.ref_;
+ return nextRef ? nextRef : ref;
+ }
+});
+
+// Returns
+// a) undefined if there are no mustaches.
+// b) [TEXT, (ONE_TIME?, PATH, DELEGATE_FN, TEXT)+] if there is at least
+// one mustache.
+function parseMustaches(s, name, node, prepareBindingFn) {
+ if (!s || !s.length)
+ return;
+
+ var tokens;
+ var length = s.length;
+ var startIndex = 0, lastIndex = 0, endIndex = 0;
+ var onlyOneTime = true;
+ while (lastIndex < length) {
+ var startIndex = s.indexOf('{{', lastIndex);
+ var oneTimeStart = s.indexOf('[[', lastIndex);
+ var oneTime = false;
+ var terminator = '}}';
+
+ if (oneTimeStart >= 0 &&
+ (startIndex < 0 || oneTimeStart < startIndex)) {
+ startIndex = oneTimeStart;
+ oneTime = true;
+ terminator = ']]';
+ }
+
+ endIndex = startIndex < 0 ? -1 : s.indexOf(terminator, startIndex + 2);
+
+ if (endIndex < 0) {
+ if (!tokens)
+ return;
+
+ tokens.push(s.slice(lastIndex)); // TEXT
+ break;
+ }
+
+ tokens = tokens || [];
+ tokens.push(s.slice(lastIndex, startIndex)); // TEXT
+ var pathString = s.slice(startIndex + 2, endIndex).trim();
+ tokens.push(oneTime); // ONE_TIME?
+ onlyOneTime = onlyOneTime && oneTime;
+ var delegateFn = prepareBindingFn &&
+ prepareBindingFn(pathString, name, node);
+ // Don't try to parse the expression if there's a prepareBinding function
+ if (delegateFn == null) {
+ tokens.push(observe.Path.get(pathString)); // PATH
+ } else {
+ tokens.push(null);
+ }
+ tokens.push(delegateFn); // DELEGATE_FN
+ lastIndex = endIndex + 2;
+ }
+
+ if (lastIndex === length)
+ tokens.push(''); // TEXT
+
+ tokens.hasOnePath = tokens.length === 5;
+ tokens.isSimplePath = tokens.hasOnePath &&
+ tokens[0] == '' &&
+ tokens[4] == '';
+ tokens.onlyOneTime = onlyOneTime;
+
+ tokens.combinator = function(values) {
+ var newValue = tokens[0];
+
+ for (var i = 1; i < tokens.length; i += 4) {
+ var value = tokens.hasOnePath ? values : values[(i - 1) / 4];
+ if (value !== undefined)
+ newValue += value;
+ newValue += tokens[i + 3];
+ }
+
+ return newValue;
+ }
+
+ return tokens;
+};
+
+function processOneTimeBinding(name, tokens, node, model) {
+ if (tokens.hasOnePath) {
+ var delegateFn = tokens[3];
+ var value = delegateFn ? delegateFn(model, node, true) :
+ tokens[2].getValueFrom(model);
+ return tokens.isSimplePath ? value : tokens.combinator(value);
+ }
+
+ var values = [];
+ for (var i = 1; i < tokens.length; i += 4) {
+ var delegateFn = tokens[i + 2];
+ values[(i - 1) / 4] = delegateFn ? delegateFn(model, node) :
+ tokens[i + 1].getValueFrom(model);
+ }
+
+ return tokens.combinator(values);
+}
+
+function processSinglePathBinding(name, tokens, node, model) {
+ var delegateFn = tokens[3];
+ var observer = delegateFn ? delegateFn(model, node, false) :
+ new observe.PathObserver(model, tokens[2]);
+
+ return tokens.isSimplePath ? observer :
+ new observe.ObserverTransform(observer, tokens.combinator);
+}
+
+function processBinding(name, tokens, node, model) {
+ if (tokens.onlyOneTime)
+ return processOneTimeBinding(name, tokens, node, model);
+
+ if (tokens.hasOnePath)
+ return processSinglePathBinding(name, tokens, node, model);
+
+ var observer = new observe.CompoundObserver();
+
+ for (var i = 1; i < tokens.length; i += 4) {
+ var oneTime = tokens[i];
+ var delegateFn = tokens[i + 2];
+
+ if (delegateFn) {
+ var value = delegateFn(model, node, oneTime);
+ if (oneTime)
+ observer.addPath(value)
+ else
+ observer.addObserver(value);
+ continue;
+ }
+
+ var path = tokens[i + 1];
+ if (oneTime)
+ observer.addPath(path.getValueFrom(model))
+ else
+ observer.addPath(model, path);
+ }
+
+ return new observe.ObserverTransform(observer, tokens.combinator);
+}
+
+function processBindings(node, bindings, model, instanceBindings) {
+ for (var i = 0; i < bindings.length; i += 2) {
+ var name = bindings[i]
+ var tokens = bindings[i + 1];
+ var value = processBinding(name, tokens, node, model);
+ var binding = node.bind(name, value, tokens.onlyOneTime);
+ if (binding && instanceBindings)
+ instanceBindings.push(binding);
+ }
+
+ if (!bindings.isTemplate)
+ return;
+
+ node.model_ = model;
+ var iter = node.processBindingDirectives_(bindings);
+ if (instanceBindings && iter)
+ instanceBindings.push(iter);
+}
+
+function parseWithDefault(el, name, prepareBindingFn) {
+ var v = el.getAttribute(name);
+ return parseMustaches(v == '' ? '{{}}' : v, name, el, prepareBindingFn);
+}
+
+function parseAttributeBindings(element, prepareBindingFn) {
+ var bindings = [];
+ var ifFound = false;
+ var bindFound = false;
+
+ for (var i = 0; i < element.attributes.length; i++) {
+ var attr = element.attributes[i];
+ var name = attr.name;
+ var value = attr.value;
+
+ if (isTemplate(element) &&
+ (name === IF || name === BIND || name === REPEAT)) {
+ continue;
+ }
+
+ var tokens = parseMustaches(value, name, element,
+ prepareBindingFn);
+ if (!tokens)
+ continue;
+
+ bindings.push(name, tokens);
+ }
+
+ if (isTemplate(element)) {
+ bindings.isTemplate = true;
+ bindings.if = parseWithDefault(element, IF, prepareBindingFn);
+ bindings.bind = parseWithDefault(element, BIND, prepareBindingFn);
+ bindings.repeat = parseWithDefault(element, REPEAT, prepareBindingFn);
+
+ if (bindings.if && !bindings.bind && !bindings.repeat)
+ bindings.bind = parseMustaches('{{}}', BIND, element, prepareBindingFn);
+ }
+
+ return bindings;
+}
+
+function getBindings(node, prepareBindingFn) {
+ if (node instanceof Element) {
+ return parseAttributeBindings(node, prepareBindingFn);
+ }
+
+ if (node instanceof Text) {
+ var tokens = parseMustaches(node.data, 'textContent', node,
+ prepareBindingFn);
+ if (tokens)
+ return ['textContent', tokens];
+ }
+
+ return [];
+}
+
+function cloneAndBindInstance(node, parent, stagingDocument, bindings, model,
+ delegate,
+ instanceBindings,
+ instanceRecord) {
+ var clone = parent.appendChild(stagingDocument.importNode(node, false));
+
+ var i = 0;
+ for (var child = node.firstChild; child; child = child.nextSibling) {
+ cloneAndBindInstance(child, clone, stagingDocument,
+ bindings.children[i++],
+ model,
+ delegate,
+ instanceBindings);
+ }
+
+ if (bindings.isTemplate) {
+ clone.instanceRef_ = node;
+
+ if (delegate)
+ clone.setDelegate_(delegate);
+ }
+
+ processBindings(clone, bindings, model, instanceBindings);
+ return clone;
+}
+
+function createInstanceBindingMap(node, prepareBindingFn) {
+ var map = getBindings(node, prepareBindingFn);
+ map.children = {};
+ var index = 0;
+ for (var child = node.firstChild; child; child = child.nextSibling) {
+ map.children[index++] = createInstanceBindingMap(child, prepareBindingFn);
+ }
+
+ return map;
+}
+
+var contentUidCounter = 1;
+
+// TODO(rafaelw): Setup a MutationObserver on content which clears the id
+// so that bindingMaps regenerate when the template.content changes.
+function getContentUid(content) {
+ var id = content.id_;
+ if (!id)
+ id = content.id_ = contentUidCounter++;
+ return id;
+}
+
+// Each delegate is associated with a set of bindingMaps, one for each
+// content which may be used by a template. The intent is that each binding
+// delegate gets the opportunity to prepare the instance (via the prepare*
+// delegate calls) once across all uses.
+// TODO(rafaelw): Separate out the parse map from the binding map. In the
+// current implementation, if two delegates need a binding map for the same
+// content, the second will have to reparse.
+function getInstanceBindingMap(content, delegate_) {
+ var contentId = getContentUid(content);
+ if (delegate_) {
+ var map = delegate_.bindingMaps[contentId];
+ if (!map) {
+ map = delegate_.bindingMaps[contentId] =
+ createInstanceBindingMap(content, delegate_.prepareBinding) || [];
+ }
+ return map;
+ }
+
+ var map = content.bindingMap_;
+ if (!map) {
+ map = content.bindingMap_ =
+ createInstanceBindingMap(content, undefined) || [];
+ }
+ return map;
+}
+
+Object.defineProperty(Node.prototype, 'templateInstance', {
+ get: function() {
+ var instance = this.templateInstance_;
+ return instance ? instance :
+ (this.parentNode ? this.parentNode.templateInstance : undefined);
+ }
+});
+
+var emptyInstance = document.createDocumentFragment();
+emptyInstance.bindings_ = [];
+emptyInstance.terminator_ = null;
+
+function TemplateIterator(templateElement) {
+ this.closed = false;
+ this.templateElement_ = templateElement;
+ this.instances = [];
+ this.deps = undefined;
+ this.iteratedValue = [];
+ this.presentValue = undefined;
+ this.arrayObserver = undefined;
+}
+
+TemplateIterator.prototype = {
+ closeDeps: function() {
+ var deps = this.deps;
+ if (deps) {
+ if (deps.ifOneTime === false)
+ deps.ifValue.close();
+ if (deps.oneTime === false)
+ deps.value.close();
+ }
+ },
+
+ updateDependencies: function(directives, model) {
+ this.closeDeps();
+
+ var deps = this.deps = {};
+ var template = this.templateElement_;
+
+ var ifValue = true;
+ if (directives.if) {
+ deps.hasIf = true;
+ deps.ifOneTime = directives.if.onlyOneTime;
+ deps.ifValue = processBinding(IF, directives.if, template, model);
+
+ ifValue = deps.ifValue;
+
+ // oneTime if & predicate is false. nothing else to do.
+ if (deps.ifOneTime && !ifValue) {
+ this.valueChanged();
+ return;
+ }
+
+ if (!deps.ifOneTime)
+ ifValue = ifValue.open(this.updateIfValue, this);
+ }
+
+ if (directives.repeat) {
+ deps.repeat = true;
+ deps.oneTime = directives.repeat.onlyOneTime;
+ deps.value = processBinding(REPEAT, directives.repeat, template, model);
+ } else {
+ deps.repeat = false;
+ deps.oneTime = directives.bind.onlyOneTime;
+ deps.value = processBinding(BIND, directives.bind, template, model);
+ }
+
+ var value = deps.value;
+ if (!deps.oneTime)
+ value = value.open(this.updateIteratedValue, this);
+
+ if (!ifValue) {
+ this.valueChanged();
+ return;
+ }
+
+ this.updateValue(value);
+ },
+
+ /**
+ * Gets the updated value of the bind/repeat. This can potentially call
+ * user code (if a bindingDelegate is set up) so we try to avoid it if we
+ * already have the value in hand (from Observer.open).
+ */
+ getUpdatedValue: function() {
+ var value = this.deps.value;
+ if (!this.deps.oneTime)
+ value = value.discardChanges();
+ return value;
+ },
+
+ updateIfValue: function(ifValue) {
+ if (!ifValue) {
+ this.valueChanged();
+ return;
+ }
+
+ this.updateValue(this.getUpdatedValue());
+ },
+
+ updateIteratedValue: function(value) {
+ if (this.deps.hasIf) {
+ var ifValue = this.deps.ifValue;
+ if (!this.deps.ifOneTime)
+ ifValue = ifValue.discardChanges();
+ if (!ifValue) {
+ this.valueChanged();
+ return;
+ }
+ }
+
+ this.updateValue(value);
+ },
+
+ updateValue: function(value) {
+ if (!this.deps.repeat)
+ value = [value];
+ var observe = this.deps.repeat &&
+ !this.deps.oneTime &&
+ Array.isArray(value);
+ this.valueChanged(value, observe);
+ },
+
+ valueChanged: function(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: function(index) {
+ if (index == -1)
+ return this.templateElement_;
+ var instance = this.instances[index];
+ var terminator = instance.terminator_;
+ if (!terminator)
+ return this.getLastInstanceNode(index - 1);
+
+ if (terminator.nodeType !== Node.ELEMENT_NODE ||
+ this.templateElement_ === terminator) {
+ return terminator;
+ }
+
+ var subtemplateIterator = terminator.iterator_;
+ if (!subtemplateIterator)
+ return terminator;
+
+ return subtemplateIterator.getLastTemplateNode();
+ },
+
+ getLastTemplateNode: function() {
+ return this.getLastInstanceNode(this.instances.length - 1);
+ },
+
+ insertInstanceAt: function(index, fragment) {
+ var previousInstanceLast = this.getLastInstanceNode(index - 1);
+ var parent = this.templateElement_.parentNode;
+ this.instances.splice(index, 0, fragment);
+
+ parent.insertBefore(fragment, previousInstanceLast.nextSibling);
+ },
+
+ extractInstanceAt: function(index) {
+ var previousInstanceLast = this.getLastInstanceNode(index - 1);
+ var lastNode = this.getLastInstanceNode(index);
+ var parent = this.templateElement_.parentNode;
+ var instance = this.instances.splice(index, 1)[0];
+
+ while (lastNode !== previousInstanceLast) {
+ var node = previousInstanceLast.nextSibling;
+ if (node == lastNode)
+ lastNode = previousInstanceLast;
+
+ instance.appendChild(parent.removeChild(node));
+ }
+
+ return instance;
+ },
+
+ getDelegateFn: function(fn) {
+ fn = fn && fn(this.templateElement_);
+ return typeof fn === 'function' ? fn : null;
+ },
+
+ handleSplices: function(splices) {
+ if (this.closed || !splices.length)
+ return;
+
+ var template = this.templateElement_;
+
+ if (!template.parentNode) {
+ this.close();
+ return;
+ }
+
+ observe.ArrayObserver.applySplices(this.iteratedValue, this.presentValue,
+ splices);
+
+ var delegate = template.delegate_;
+ if (this.instanceModelFn_ === undefined) {
+ this.instanceModelFn_ =
+ this.getDelegateFn(delegate && delegate.prepareInstanceModel);
+ }
+
+ if (this.instancePositionChangedFn_ === undefined) {
+ this.instancePositionChangedFn_ =
+ this.getDelegateFn(delegate &&
+ delegate.prepareInstancePositionChanged);
+ }
+
+ // 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 (this.instanceModelFn_) {
+ model = this.instanceModelFn_(model);
+ }
+
+ if (model === undefined) {
+ instance = emptyInstance;
+ } else {
+ instance = template.createInstance(model, undefined, delegate);
+ }
+ }
+
+ this.insertInstanceAt(addIndex, instance);
+ }
+ }
+
+ instanceCache.forEach(function(instance) {
+ this.closeInstanceBindings(instance);
+ }, this);
+
+ if (this.instancePositionChangedFn_)
+ this.reportInstancesMoved(splices);
+ },
+
+ reportInstanceMoved: function(index) {
+ var instance = this.instances[index];
+ if (instance === emptyInstance)
+ return;
+
+ this.instancePositionChangedFn_(instance.templateInstance_, index);
+ },
+
+ reportInstancesMoved: function(splices) {
+ var index = 0;
+ var offset = 0;
+ for (var i = 0; i < splices.length; i++) {
+ var splice = splices[i];
+ if (offset != 0) {
+ while (index < splice.index) {
+ this.reportInstanceMoved(index);
+ index++;
+ }
+ } else {
+ index = splice.index;
+ }
+
+ while (index < splice.index + splice.addedCount) {
+ this.reportInstanceMoved(index);
+ index++;
+ }
+
+ offset += splice.addedCount - splice.removed.length;
+ }
+
+ if (offset == 0)
+ return;
+
+ var length = this.instances.length;
+ while (index < length) {
+ this.reportInstanceMoved(index);
+ index++;
+ }
+ },
+
+ closeInstanceBindings: function(instance) {
+ var bindings = instance.bindings_;
+ for (var i = 0; i < bindings.length; i++) {
+ bindings[i].close();
+ }
+ },
+
+ unobserve: function() {
+ if (!this.arrayObserver)
+ return;
+
+ this.arrayObserver.close();
+ this.arrayObserver = undefined;
+ },
+
+ close: function() {
+ if (this.closed)
+ return;
+ this.unobserve();
+ for (var i = 0; i < this.instances.length; i++) {
+ this.closeInstanceBindings(this.instances[i]);
+ }
+
+ this.instances.length = 0;
+ this.closeDeps();
+ this.templateElement_.iterator_ = undefined;
+ this.closed = true;
+ }
+};
+</script>
« no previous file with comments | « sky/examples/city-list/index.sky ('k') | sky/framework/sky-element/observe.sky » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698