| Index: pkg/template_binding/lib/src/template_iterator.dart
|
| diff --git a/pkg/template_binding/lib/src/template_iterator.dart b/pkg/template_binding/lib/src/template_iterator.dart
|
| index 981adf580577822c707f5849da7877436ca00e95..878e2e7cc1581535581baa2970bc1ed46f802aa6 100644
|
| --- a/pkg/template_binding/lib/src/template_iterator.dart
|
| +++ b/pkg/template_binding/lib/src/template_iterator.dart
|
| @@ -17,32 +17,23 @@ part of template_binding;
|
| // See: https://github.com/polymer/TemplateBinding/issues/59
|
| bool _toBoolean(value) => null != value && false != value;
|
|
|
| -Node _createDeepCloneAndDecorateTemplates(Node node, BindingDelegate delegate) {
|
| - var clone = node.clone(false); // Shallow clone.
|
| - if (isSemanticTemplate(clone)) {
|
| - TemplateBindExtension.decorate(clone, node);
|
| - if (delegate != null) {
|
| - templateBindFallback(clone)._bindingDelegate = delegate;
|
| - }
|
| +List _getBindings(Node node, BindingDelegate delegate) {
|
| + if (node is Element) {
|
| + return _parseAttributeBindings(node, delegate);
|
| }
|
|
|
| - for (var c = node.firstChild; c != null; c = c.nextNode) {
|
| - clone.append(_createDeepCloneAndDecorateTemplates(c, delegate));
|
| + if (node is Text) {
|
| + var tokens = _parseMustaches(node.text, 'text', node, delegate);
|
| + if (tokens != null) return ['text', tokens];
|
| }
|
| - return clone;
|
| +
|
| + return null;
|
| }
|
|
|
| void _addBindings(Node node, model, [BindingDelegate delegate]) {
|
| - List bindings = null;
|
| - if (node is Element) {
|
| - bindings = _parseAttributeBindings(node);
|
| - } else if (node is Text) {
|
| - var tokens = _parseMustacheTokens(node.text);
|
| - if (tokens != null) bindings = ['text', tokens];
|
| - }
|
| -
|
| + var bindings = _getBindings(node, delegate);
|
| if (bindings != null) {
|
| - _processBindings(bindings, node, model, delegate);
|
| + _processBindings(bindings, node, model);
|
| }
|
|
|
| for (var c = node.firstChild; c != null; c = c.nextNode) {
|
| @@ -50,23 +41,34 @@ void _addBindings(Node node, model, [BindingDelegate delegate]) {
|
| }
|
| }
|
|
|
| -List _parseAttributeBindings(Element element) {
|
| +
|
| +List _parseAttributeBindings(Element element, BindingDelegate delegate) {
|
| var bindings = null;
|
| var ifFound = false;
|
| var bindFound = false;
|
| var isTemplateNode = isSemanticTemplate(element);
|
|
|
| element.attributes.forEach((name, value) {
|
| + // Allow bindings expressed in attributes to be prefixed with underbars.
|
| + // We do this to allow correct semantics for browsers that don't implement
|
| + // <template> where certain attributes might trigger side-effects -- and
|
| + // for IE which sanitizes certain attributes, disallowing mustache
|
| + // replacements in their text.
|
| + while (name[0] == '_') {
|
| + name = name.substring(1);
|
| + }
|
| +
|
| if (isTemplateNode) {
|
| if (name == 'if') {
|
| ifFound = true;
|
| + if (value == '') value = '{{}}'; // Accept 'naked' if.
|
| } else if (name == 'bind' || name == 'repeat') {
|
| bindFound = true;
|
| - if (value == '') value = '{{}}';
|
| + if (value == '') value = '{{}}'; // Accept 'naked' bind & repeat.
|
| }
|
| }
|
|
|
| - var tokens = _parseMustacheTokens(value);
|
| + var tokens = _parseMustaches(value, name, element, delegate);
|
| if (tokens != null) {
|
| if (bindings == null) bindings = [];
|
| bindings..add(name)..add(tokens);
|
| @@ -76,90 +78,70 @@ List _parseAttributeBindings(Element element) {
|
| // Treat <template if> as <template bind if>
|
| if (ifFound && !bindFound) {
|
| if (bindings == null) bindings = [];
|
| - bindings..add('bind')..add(_parseMustacheTokens('{{}}'));
|
| + bindings..add('bind')
|
| + ..add(_parseMustaches('{{}}', 'bind', element, delegate));
|
| }
|
|
|
| return bindings;
|
| }
|
|
|
| void _processBindings(List bindings, Node node, model,
|
| - BindingDelegate delegate) {
|
| + [List<NodeBinding> bound]) {
|
|
|
| for (var i = 0; i < bindings.length; i += 2) {
|
| - _setupBinding(node, bindings[i], bindings[i + 1], model, delegate);
|
| - }
|
| -}
|
| -
|
| -void _setupBinding(Node node, String name, List tokens, model,
|
| - BindingDelegate delegate) {
|
| -
|
| - if (_isSimpleBinding(tokens)) {
|
| - _bindOrDelegate(node, name, model, tokens[1], delegate);
|
| - return;
|
| - }
|
| -
|
| - // TODO(jmesserly): MDV caches the closure on the tokens, but I'm not sure
|
| - // why they do that instead of just caching the entire CompoundBinding object
|
| - // and unbindAll then bind to the new model.
|
| - var replacementBinding = new CompoundBinding()
|
| - ..scheduled = true
|
| - ..combinator = (values) {
|
| - var newValue = new StringBuffer();
|
| -
|
| - for (var i = 0, text = true; i < tokens.length; i++, text = !text) {
|
| - if (text) {
|
| - newValue.write(tokens[i]);
|
| - } else {
|
| - var value = values[i];
|
| - if (value != null) {
|
| - newValue.write(value);
|
| + var name = bindings[i];
|
| + var tokens = bindings[i + 1];
|
| + var bindingModel = model;
|
| + var bindingPath = tokens.tokens[1];
|
| + if (tokens.hasOnePath) {
|
| + var delegateFn = tokens.tokens[2];
|
| + if (delegateFn != null) {
|
| + var delegateBinding = delegateFn(model, node);
|
| + if (delegateBinding != null) {
|
| + bindingModel = delegateBinding;
|
| + bindingPath = 'value';
|
| }
|
| }
|
| - }
|
| -
|
| - return newValue.toString();
|
| - };
|
|
|
| - for (var i = 1; i < tokens.length; i += 2) {
|
| - // TODO(jmesserly): not sure if this index is correct. See my comment here:
|
| - // https://github.com/Polymer/mdv/commit/f1af6fe683fd06eed2a7a7849f01c227db12cda3#L0L1035
|
| - _bindOrDelegate(replacementBinding, i, model, tokens[i], delegate);
|
| - }
|
| -
|
| - replacementBinding.resolve();
|
| -
|
| - nodeBind(node).bind(name, replacementBinding, 'value');
|
| -}
|
| + if (!tokens.isSimplePath) {
|
| + bindingModel = new PathObserver(bindingModel, bindingPath,
|
| + computeValue: tokens.combinator);
|
| + bindingPath = 'value';
|
| + }
|
| + } else {
|
| + var observer = new CompoundPathObserver(computeValue: tokens.combinator);
|
| + for (var j = 1; j < tokens.tokens.length; j += 3) {
|
| + var subModel = model;
|
| + var subPath = tokens.tokens[j];
|
| + var delegateFn = tokens.tokens[j + 1];
|
| + var delegateBinding = delegateFn != null ?
|
| + delegateFn(subModel, node) : null;
|
| +
|
| + if (delegateBinding != null) {
|
| + subModel = delegateBinding;
|
| + subPath = 'value';
|
| + }
|
|
|
| -void _bindOrDelegate(node, name, model, String path,
|
| - BindingDelegate delegate) {
|
| + observer.addPath(subModel, subPath);
|
| + }
|
|
|
| - if (delegate != null) {
|
| - var delegateBinding = delegate.getBinding(model, path, name, node);
|
| - if (delegateBinding != null) {
|
| - model = delegateBinding;
|
| - path = 'value';
|
| + observer.start();
|
| + bindingModel = observer;
|
| + bindingPath = 'value';
|
| }
|
| - }
|
|
|
| - if (node is CompoundBinding) {
|
| - node.bind(name, model, path);
|
| - } else {
|
| - nodeBind(node).bind(name, model, path);
|
| + var binding = nodeBind(node).bind(name, bindingModel, bindingPath);
|
| + if (bound != null) bound.add(binding);
|
| }
|
| }
|
|
|
| -/** True if and only if [tokens] is of the form `['', path, '']`. */
|
| -bool _isSimpleBinding(List<String> tokens) =>
|
| - tokens.length == 3 && tokens[0].isEmpty && tokens[2].isEmpty;
|
| -
|
| /**
|
| * Parses {{ mustache }} bindings.
|
| *
|
| - * Returns null if there are no matches. Otherwise returns
|
| - * [TEXT, (PATH, TEXT)+] if there is at least one mustache.
|
| + * Returns null if there are no matches. Otherwise returns the parsed tokens.
|
| */
|
| -List<String> _parseMustacheTokens(String s) {
|
| +_MustacheTokens _parseMustaches(String s, String name, Node node,
|
| + BindingDelegate delegate) {
|
| if (s.isEmpty) return null;
|
|
|
| var tokens = null;
|
| @@ -172,18 +154,63 @@ List<String> _parseMustacheTokens(String s) {
|
| if (endIndex < 0) {
|
| if (tokens == null) return null;
|
|
|
| - tokens.add(s.substring(lastIndex));
|
| + tokens.add(s.substring(lastIndex)); // TEXT
|
| break;
|
| }
|
|
|
| - if (tokens == null) tokens = <String>[];
|
| + if (tokens == null) tokens = [];
|
| tokens.add(s.substring(lastIndex, startIndex)); // TEXT
|
| - tokens.add(s.substring(startIndex + 2, endIndex).trim()); // PATH
|
| + var pathString = s.substring(startIndex + 2, endIndex).trim();
|
| + tokens.add(pathString); // PATH
|
| + var delegateFn = delegate == null ? null :
|
| + delegate.prepareBinding(pathString, name, node);
|
| + tokens.add(delegateFn);
|
| +
|
| lastIndex = endIndex + 2;
|
| }
|
|
|
| if (lastIndex == length) tokens.add('');
|
| - return tokens;
|
| +
|
| + return new _MustacheTokens(tokens);
|
| +}
|
| +
|
| +class _MustacheTokens {
|
| + bool get hasOnePath => tokens.length == 4;
|
| + bool get isSimplePath => hasOnePath && tokens[0] == '' && tokens[3] == '';
|
| +
|
| + /** [TEXT, (PATH, TEXT, DELEGATE_FN)+] if there is at least one mustache. */
|
| + // TODO(jmesserly): clean up the type here?
|
| + final List tokens;
|
| +
|
| + // Dart note: I think this is cached in JavaScript to avoid an extra
|
| + // allocation per template instance. Seems reasonable, so we do the same.
|
| + Function _combinator;
|
| + Function get combinator => _combinator;
|
| +
|
| + _MustacheTokens(this.tokens) {
|
| + // Should be: [TEXT, (PATH, TEXT, DELEGATE_FN)+].
|
| + assert((tokens.length + 2) % 3 == 0);
|
| +
|
| + _combinator = hasOnePath ? _singleCombinator : _listCombinator;
|
| + }
|
| +
|
| + // Dart note: split "combinator" into the single/list variants, so the
|
| + // argument can be typed.
|
| + String _singleCombinator(Object value) {
|
| + if (value == null) value = '';
|
| + return '${tokens[0]}$value${tokens[3]}';
|
| + }
|
| +
|
| + String _listCombinator(List<Object> values) {
|
| + var newValue = new StringBuffer(tokens[0]);
|
| + for (var i = 1; i < tokens.length; i += 3) {
|
| + var value = values[(i - 1) ~/ 3];
|
| + if (value != null) newValue.write(value);
|
| + newValue.write(tokens[i + 2]);
|
| + }
|
| +
|
| + return newValue.toString();
|
| + }
|
| }
|
|
|
| void _addTemplateInstanceRecord(fragment, model) {
|
| @@ -201,106 +228,150 @@ void _addTemplateInstanceRecord(fragment, model) {
|
| }
|
| }
|
|
|
| -
|
| class _TemplateIterator {
|
| - final Element _templateElement;
|
| - final List<Node> terminators = [];
|
| - CompoundBinding inputs;
|
| + final TemplateBindExtension _templateExt;
|
| +
|
| + /**
|
| + * Flattened array of tuples:
|
| + * <instanceTerminatorNode, [bindingsSetupByInstance]>
|
| + */
|
| + final List terminators = [];
|
| List iteratedValue;
|
| bool closed = false;
|
| + bool depsChanging = false;
|
|
|
| - StreamSubscription _sub;
|
| + bool hasRepeat = false, hasBind = false, hasIf = false;
|
| + Object repeatModel, bindModel, ifModel;
|
| + String repeatPath, bindPath, ifPath;
|
|
|
| - _TemplateIterator(this._templateElement) {
|
| - inputs = new CompoundBinding(resolveInputs);
|
| - }
|
| + StreamSubscription _valueSub, _arraySub;
|
|
|
| - resolveInputs(Map values) {
|
| - if (closed) return;
|
| + bool _initPrepareFunctions = false;
|
| + PrepareInstanceModelFunction _instanceModelFn;
|
| + PrepareInstancePositionChangedFunction _instancePositionChangedFn;
|
| +
|
| + _TemplateIterator(this._templateExt);
|
| +
|
| + Element get _templateElement => _templateExt._node;
|
| +
|
| + resolve() {
|
| + depsChanging = false;
|
| +
|
| + if (_valueSub != null) {
|
| + _valueSub.cancel();
|
| + _valueSub = null;
|
| + }
|
|
|
| - if (values.containsKey('if') && !_toBoolean(values['if'])) {
|
| - valueChanged(null);
|
| - } else if (values.containsKey('repeat')) {
|
| - valueChanged(values['repeat']);
|
| - } else if (values.containsKey('bind') || values.containsKey('if')) {
|
| - valueChanged([values['bind']]);
|
| + if (!hasRepeat && !hasBind) {
|
| + _valueChanged(null);
|
| + return;
|
| + }
|
| +
|
| + final model = hasRepeat ? repeatModel : bindModel;
|
| + final path = hasRepeat ? repeatPath : bindPath;
|
| +
|
| + var valueObserver;
|
| + if (!hasIf) {
|
| + valueObserver = new PathObserver(model, path,
|
| + computeValue: hasRepeat ? null : (x) => [x]);
|
| } else {
|
| - valueChanged(null);
|
| + // TODO(jmesserly): I'm not sure if closing over this is necessary for
|
| + // correctness. It does seem useful if the valueObserver gets fired after
|
| + // hasRepeat has changed, due to async nature of things.
|
| + final isRepeat = hasRepeat;
|
| +
|
| + valueFn(List values) {
|
| + var modelValue = values[0];
|
| + var ifValue = values[1];
|
| + if (!_toBoolean(ifValue)) return null;
|
| + return isRepeat ? modelValue : [ modelValue ];
|
| + }
|
| +
|
| + valueObserver = new CompoundPathObserver(computeValue: valueFn)
|
| + ..addPath(model, path)
|
| + ..addPath(ifModel, ifPath)
|
| + ..start();
|
| }
|
| - // We don't return a value to the CompoundBinding; instead we skip a hop and
|
| - // call valueChanged directly.
|
| - return null;
|
| - }
|
|
|
| - void valueChanged(value) {
|
| - if (value is! List) value = null;
|
| + _valueSub = valueObserver.changes.listen(
|
| + (r) => _valueChanged(r.last.newValue));
|
| + _valueChanged(valueObserver.value);
|
| + }
|
|
|
| + void _valueChanged(newValue) {
|
| var oldValue = iteratedValue;
|
| unobserve();
|
| - iteratedValue = value;
|
|
|
| - if (iteratedValue is Observable) {
|
| - _sub = (iteratedValue as Observable).changes.listen(_handleChanges);
|
| + if (newValue is List) {
|
| + iteratedValue = newValue;
|
| + } else if (newValue is Iterable) {
|
| + // Dart note: we support Iterable by calling toList.
|
| + // But we need to be careful to observe the original iterator if it
|
| + // supports that.
|
| + iteratedValue = (newValue as Iterable).toList();
|
| + } else {
|
| + iteratedValue = null;
|
| + }
|
| +
|
| + if (iteratedValue != null && newValue is Observable) {
|
| + _arraySub = (newValue as Observable).changes.listen(
|
| + _handleSplices);
|
| }
|
|
|
| var splices = calculateSplices(
|
| iteratedValue != null ? iteratedValue : [],
|
| oldValue != null ? oldValue : []);
|
|
|
| - if (splices.length > 0) _handleChanges(splices);
|
| -
|
| - if (inputs.length == 0) {
|
| - close();
|
| - templateBindFallback(_templateElement)._templateIterator = null;
|
| - }
|
| + if (splices.isNotEmpty) _handleSplices(splices);
|
| }
|
|
|
| Node getTerminatorAt(int index) {
|
| if (index == -1) return _templateElement;
|
| - var terminator = terminators[index];
|
| - if (isSemanticTemplate(terminator) &&
|
| - !identical(terminator, _templateElement)) {
|
| - var subIterator = templateBindFallback(terminator)._templateIterator;
|
| - if (subIterator != null) {
|
| - return subIterator.getTerminatorAt(subIterator.terminators.length - 1);
|
| - }
|
| + var terminator = terminators[index * 2];
|
| + if (!isSemanticTemplate(terminator) ||
|
| + identical(terminator, _templateElement)) {
|
| + return terminator;
|
| }
|
|
|
| - return terminator;
|
| + var subIter = templateBindFallback(terminator)._iterator;
|
| + if (subIter == null) return terminator;
|
| +
|
| + return subIter.getTerminatorAt(subIter.terminators.length ~/ 2 - 1);
|
| }
|
|
|
| + // TODO(rafaelw): If we inserting sequences of instances we can probably
|
| + // avoid lots of calls to getTerminatorAt(), or cache its result.
|
| void insertInstanceAt(int index, DocumentFragment fragment,
|
| - List<Node> instanceNodes) {
|
| + List<Node> instanceNodes, List<NodeBinding> bound) {
|
|
|
| var previousTerminator = getTerminatorAt(index - 1);
|
| var terminator = null;
|
| if (fragment != null) {
|
| terminator = fragment.lastChild;
|
| - } else if (instanceNodes.length > 0) {
|
| + } else if (instanceNodes != null && instanceNodes.isNotEmpty) {
|
| terminator = instanceNodes.last;
|
| }
|
| if (terminator == null) terminator = previousTerminator;
|
|
|
| - terminators.insert(index, terminator);
|
| -
|
| + terminators.insertAll(index * 2, [terminator, bound]);
|
| var parent = _templateElement.parentNode;
|
| var insertBeforeNode = previousTerminator.nextNode;
|
|
|
| if (fragment != null) {
|
| parent.insertBefore(fragment, insertBeforeNode);
|
| - return;
|
| - }
|
| -
|
| - for (var node in instanceNodes) {
|
| - parent.insertBefore(node, insertBeforeNode);
|
| + } else if (instanceNodes != null) {
|
| + for (var node in instanceNodes) {
|
| + parent.insertBefore(node, insertBeforeNode);
|
| + }
|
| }
|
| }
|
|
|
| - List<Node> extractInstanceAt(int index) {
|
| + _BoundNodes extractInstanceAt(int index) {
|
| var instanceNodes = <Node>[];
|
| var previousTerminator = getTerminatorAt(index - 1);
|
| var terminator = getTerminatorAt(index);
|
| - terminators.removeAt(index);
|
| + var bound = terminators[index * 2 + 1];
|
| + terminators.removeRange(index * 2, index * 2 + 2);
|
|
|
| var parent = _templateElement.parentNode;
|
| while (terminator != previousTerminator) {
|
| @@ -309,45 +380,41 @@ class _TemplateIterator {
|
| node.remove();
|
| instanceNodes.add(node);
|
| }
|
| - return instanceNodes;
|
| + return new _BoundNodes(instanceNodes, bound);
|
| }
|
|
|
| - getInstanceModel(model, BindingDelegate delegate) {
|
| - if (delegate != null) {
|
| - return delegate.getInstanceModel(_templateElement, model);
|
| - }
|
| - return model;
|
| - }
|
| -
|
| - DocumentFragment getInstanceFragment(model, BindingDelegate delegate) {
|
| - return templateBind(_templateElement).createInstance(model, delegate);
|
| - }
|
| -
|
| - void _handleChanges(Iterable<ChangeRecord> splices) {
|
| + void _handleSplices(Iterable<ChangeRecord> splices) {
|
| if (closed) return;
|
|
|
| splices = splices.where((s) => s is ListChangeRecord);
|
|
|
| - var template = _templateElement;
|
| - var delegate = templateBind(template).bindingDelegate;
|
| + final template = _templateElement;
|
| + final delegate = _templateExt._self.bindingDelegate;
|
|
|
| if (template.parentNode == null || template.ownerDocument.window == null) {
|
| close();
|
| - // TODO(jmesserly): MDV calls templateIteratorTable.delete(this) here,
|
| - // but I think that's a no-op because only nodes are used as keys.
|
| - // See https://github.com/Polymer/mdv/pull/114.
|
| return;
|
| }
|
|
|
| - var instanceCache = new HashMap(equals: identical);
|
| + // Dart note: the JavaScript code relies on the distinction between null
|
| + // and undefined to track whether the functions are prepared. We use a bool.
|
| + if (!_initPrepareFunctions) {
|
| + _initPrepareFunctions = true;
|
| + if (delegate != null) {
|
| + _instanceModelFn = delegate.prepareInstanceModel(template);
|
| + _instancePositionChangedFn =
|
| + delegate.prepareInstancePositionChanged(template);
|
| + }
|
| + }
|
| +
|
| + var instanceCache = new HashMap<Object, _BoundNodes>(equals: identical);
|
| var removeDelta = 0;
|
| for (var splice in splices) {
|
| for (int i = 0; i < splice.removedCount; i++) {
|
| - var instanceNodes = extractInstanceAt(splice.index + removeDelta);
|
| - if (instanceNodes.length == 0) continue;
|
| - var model = nodeBindFallback(instanceNodes.first)
|
| - ._templateInstance.model;
|
| - instanceCache[model] = instanceNodes;
|
| + var instance = extractInstanceAt(splice.index + removeDelta);
|
| + if (instance.nodes.length == 0) continue;
|
| + var model = nodeBind(instance.nodes.first).templateInstance.model;
|
| + instanceCache[model] = instance;
|
| }
|
|
|
| removeDelta -= splice.addedCount;
|
| @@ -360,51 +427,109 @@ class _TemplateIterator {
|
|
|
| var model = iteratedValue[addIndex];
|
| var fragment = null;
|
| - var instanceNodes = instanceCache.remove(model);
|
| - if (instanceNodes == null) {
|
| - var actualModel = getInstanceModel(model, delegate);
|
| - fragment = getInstanceFragment(actualModel, delegate);
|
| + var instance = instanceCache.remove(model);
|
| + List bound;
|
| + List instanceNodes = null;
|
| + if (instance != null && instance.nodes.isNotEmpty) {
|
| + bound = instance.bound;
|
| + instanceNodes = instance.nodes;
|
| + } else {
|
| + bound = [];
|
| + if (_instanceModelFn != null) {
|
| + model = _instanceModelFn(model);
|
| + }
|
| + if (model != null) {
|
| + fragment = _templateExt.createInstance(model, delegate, bound);
|
| + }
|
| }
|
|
|
| - insertInstanceAt(addIndex, fragment, instanceNodes);
|
| + insertInstanceAt(addIndex, fragment, instanceNodes, bound);
|
| }
|
| }
|
|
|
| - for (var instanceNodes in instanceCache.values) {
|
| - instanceNodes.forEach(_unbindAllRecursively);
|
| + for (var instance in instanceCache.values) {
|
| + closeInstanceBindings(instance.bound);
|
| }
|
| +
|
| + if (_instancePositionChangedFn != null) reportInstancesMoved(splices);
|
| + }
|
| +
|
| + void reportInstanceMoved(int index) {
|
| + var previousTerminator = getTerminatorAt(index - 1);
|
| + var terminator = getTerminatorAt(index);
|
| + if (identical(previousTerminator, terminator)) {
|
| + return; // instance has zero nodes.
|
| + }
|
| +
|
| + // We must use the first node of the instance, because any subsequent
|
| + // nodes may have been generated by sub-templates.
|
| + // TODO(rafaelw): This is brittle WRT instance mutation -- e.g. if the
|
| + // first node was removed by script.
|
| + var instance = nodeBind(previousTerminator.nextNode).templateInstance;
|
| + _instancePositionChangedFn(instance, index);
|
| + }
|
| +
|
| + void reportInstancesMoved(Iterable<ChangeRecord> splices) {
|
| + var index = 0;
|
| + var offset = 0;
|
| + for (ListChangeRecord splice in splices) {
|
| + if (offset != 0) {
|
| + while (index < splice.index) {
|
| + reportInstanceMoved(index);
|
| + index++;
|
| + }
|
| + } else {
|
| + index = splice.index;
|
| + }
|
| +
|
| + while (index < splice.index + splice.addedCount) {
|
| + reportInstanceMoved(index);
|
| + index++;
|
| + }
|
| +
|
| + offset += splice.addedCount - splice.removedCount;
|
| + }
|
| +
|
| + if (offset == 0) return;
|
| +
|
| + var length = terminators.length ~/ 2;
|
| + while (index < length) {
|
| + reportInstanceMoved(index);
|
| + index++;
|
| + }
|
| + }
|
| +
|
| + void closeInstanceBindings(List<NodeBinding> bound) {
|
| + for (var binding in bound) binding.close();
|
| }
|
|
|
| void unobserve() {
|
| - if (_sub == null) return;
|
| - _sub.cancel();
|
| - _sub = null;
|
| + if (_arraySub == null) return;
|
| + _arraySub.cancel();
|
| + _arraySub = null;
|
| }
|
|
|
| void close() {
|
| if (closed) return;
|
|
|
| unobserve();
|
| - inputs.close();
|
| - terminators.clear();
|
| - closed = true;
|
| - }
|
| -
|
| - static void _unbindAllRecursively(Node node) {
|
| - var nodeExt = nodeBindFallback(node);
|
| - nodeExt._templateInstance = null;
|
| - if (isSemanticTemplate(node)) {
|
| - // Make sure we stop observing when we remove an element.
|
| - var templateIterator = nodeExt._templateIterator;
|
| - if (templateIterator != null) {
|
| - templateIterator.close();
|
| - nodeExt._templateIterator = null;
|
| - }
|
| + for (var i = 1; i < terminators.length; i += 2) {
|
| + closeInstanceBindings(terminators[i]);
|
| }
|
|
|
| - nodeBind(node).unbindAll();
|
| - for (var c = node.firstChild; c != null; c = c.nextNode) {
|
| - _unbindAllRecursively(c);
|
| + terminators.clear();
|
| + if (_valueSub != null) {
|
| + _valueSub.cancel();
|
| + _valueSub = null;
|
| }
|
| + _templateExt._iterator = null;
|
| + closed = true;
|
| }
|
| }
|
| +
|
| +// Dart note: the JavaScript version just puts an expando on the array.
|
| +class _BoundNodes {
|
| + final List<Node> nodes;
|
| + final List<NodeBinding> bound;
|
| + _BoundNodes(this.nodes, this.bound);
|
| +}
|
|
|