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

Unified Diff: sdk/lib/html/dartium/html_dartium.dart

Issue 14732003: Implement Model-Driven-Views spec for Dart (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: try upload again Created 7 years, 7 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
Index: sdk/lib/html/dartium/html_dartium.dart
diff --git a/sdk/lib/html/dartium/html_dartium.dart b/sdk/lib/html/dartium/html_dartium.dart
index a88ce1fd3e7f8db7839a75d33c281ac380856a65..e549cb6acde4e335620102c0ab365e8f48665f29 100644
--- a/sdk/lib/html/dartium/html_dartium.dart
+++ b/sdk/lib/html/dartium/html_dartium.dart
@@ -3,13 +3,14 @@ library dart.dom.html;
import 'dart:async';
import 'dart:collection';
-import 'dart:_collection-dev';
+import 'dart:_collection-dev' hide Symbol;
import 'dart:html_common';
import 'dart:indexed_db';
import 'dart:isolate';
import 'dart:json' as json;
import 'dart:math';
import 'dart:nativewrappers';
+import 'dart:mdv_observe_impl';
import 'dart:typed_data';
import 'dart:web_gl' as gl;
import 'dart:web_sql';
@@ -7043,7 +7044,7 @@ class DocumentFragment extends Node {
e.innerHtml = value;
// Copy list first since we don't want liveness during iteration.
- List nodes = new List.from(e.nodes);
+ List nodes = new List.from(e.nodes, growable: false);
this.nodes.addAll(nodes);
}
@@ -7461,7 +7462,7 @@ class _ChildrenElementList extends ListBase<Element> {
}
}
-/**
+/**
* An immutable list containing HTML elements. This list contains some
* additional methods for ease of CSS manipulation on a group of elements.
*/
@@ -7796,6 +7797,251 @@ abstract class Element extends Node implements ElementTraversal {
}
+ @Creates('Null')
+ Map<String, StreamSubscription> _attributeBindings;
+
+ // TODO(jmesserly): I'm concerned about adding these to every element.
+ // Conceptually all of these belong on TemplateElement. They are here to
+ // support browsers that don't have <template> yet.
+ // However even in the polyfill they're restricted to certain tags
+ // (see [isTemplate]). So we can probably convert it to a (public) mixin, and
+ // only mix it in to the elements that need it.
+ var _model;
+
+ _TemplateIterator _templateIterator;
+
+ Element _templateInstanceRef;
+
+ // Note: only used if `this is! TemplateElement`
+ DocumentFragment _templateContent;
+
+ bool _templateIsDecorated;
+
+ // TODO(jmesserly): should path be optional, and default to empty path?
+ // It is used that way in at least one path in JS TemplateElement tests
+ // (see "BindImperative" test in original JS code).
+ @Experimental
+ void bind(String name, model, String path) {
+ _bindElement(this, name, model, path);
+ }
+
+ // TODO(jmesserly): this is static to work around http://dartbug.com/10166
+ // Similar issue for unbind/unbindAll below.
+ static void _bindElement(Element self, String name, model, String path) {
+ if (self._bindTemplate(name, model, path)) return;
+
+ if (self._attributeBindings == null) {
+ self._attributeBindings = new Map<String, StreamSubscription>();
+ }
+
+ self.attributes.remove(name);
+
+ var changed;
+ if (name.endsWith('?')) {
+ name = name.substring(0, name.length - 1);
+
+ changed = (value) {
+ if (_templateBooleanConversion(value)) {
+ self.attributes[name] = '';
+ } else {
+ self.attributes.remove(name);
+ }
+ };
+ } else {
+ changed = (value) {
+ // TODO(jmesserly): escape value if needed to protect against XSS.
+ // See https://github.com/toolkitchen/mdv/issues/58
+ self.attributes[name] = value == null ? '' : '$value';
+ };
+ }
+
+ self.unbind(name);
+
+ self._attributeBindings[name] =
+ new PathObserver(model, path).bindSync(changed);
+ }
+
+ @Experimental
+ void unbind(String name) {
+ _unbindElement(this, name);
+ }
+
+ static _unbindElement(Element self, String name) {
+ if (self._unbindTemplate(name)) return;
+ if (self._attributeBindings != null) {
+ var binding = self._attributeBindings.remove(name);
+ if (binding != null) binding.cancel();
+ }
+ }
+
+ @Experimental
+ void unbindAll() {
+ _unbindAllElement(this);
+ }
+
+ static void _unbindAllElement(Element self) {
+ self._unbindAllTemplate();
+
+ if (self._attributeBindings != null) {
+ for (var binding in self._attributeBindings.values) {
+ binding.cancel();
+ }
+ self._attributeBindings = null;
+ }
+ }
+
+ // TODO(jmesserly): unlike the JS polyfill, we can't mixin
+ // HTMLTemplateElement at runtime into things that are semantically template
+ // elements. So instead we implement it here with a runtime check.
+ // If the bind succeeds, we return true, otherwise we return false and let
+ // the normal Element.bind logic kick in.
+ bool _bindTemplate(String name, model, String path) {
+ if (isTemplate) {
+ switch (name) {
+ case 'bind':
+ case 'repeat':
+ case 'if':
+ _ensureTemplate();
+ if (_templateIterator == null) {
+ _templateIterator = new _TemplateIterator(this);
+ }
+ _templateIterator.inputs.bind(name, model, path);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ bool _unbindTemplate(String name) {
+ if (isTemplate) {
+ switch (name) {
+ case 'bind':
+ case 'repeat':
+ case 'if':
+ _ensureTemplate();
+ if (_templateIterator != null) {
+ _templateIterator.inputs.unbind(name);
+ }
+ return true;
+ }
+ }
+ return false;
+ }
+
+ void _unbindAllTemplate() {
+ if (isTemplate) {
+ unbind('bind');
+ unbind('repeat');
+ unbind('if');
+ }
+ }
+
+ /**
+ * Gets the template this node refers to.
+ * This is only supported if [isTemplate] is true.
+ */
+ @Experimental
+ Element get ref {
+ _ensureTemplate();
+
+ Element ref = null;
+ var refId = attributes['ref'];
+ if (refId != null) {
+ ref = document.getElementById(refId);
+ }
+
+ return ref != null ? ref : _templateInstanceRef;
+ }
+
+ /**
+ * Gets the content of this template.
+ * This is only supported if [isTemplate] is true.
+ */
+ @Experimental
+ DocumentFragment get content {
+ _ensureTemplate();
+ return _templateContent;
+ }
+
+ /**
+ * Creates an instance of the template.
+ * This is only supported if [isTemplate] is true.
+ */
+ @Experimental
+ DocumentFragment createInstance() {
+ _ensureTemplate();
+
+ var template = ref;
+ if (template == null) template = this;
+
+ var instance = _createDeepCloneAndDecorateTemplates(template.content,
+ attributes['syntax']);
+
+ if (TemplateElement._instanceCreated != null) {
+ TemplateElement._instanceCreated.add(instance);
+ }
+ return instance;
+ }
+
+ /**
+ * The data model which is inherited through the tree.
+ * This is only supported if [isTemplate] is true.
+ *
+ * Setting this will destructive propagate the value to all descendant nodes,
+ * and reinstantiate all of the nodes expanded by this template.
+ *
+ * Currently this does not support propagation through Shadow DOMs.
+ */
+ @Experimental
+ get model => _model;
+
+ @Experimental
+ void set model(value) {
+ _ensureTemplate();
+
+ _model = value;
+ _addBindings(this, model);
+ }
+
+ // TODO(jmesserly): const set would be better
+ static const _TABLE_TAGS = const {
+ 'caption': null,
+ 'col': null,
+ 'colgroup': null,
+ 'tbody': null,
+ 'td': null,
+ 'tfoot': null,
+ 'th': null,
+ 'thead': null,
+ 'tr': null,
+ };
+
+ bool get _isAttributeTemplate => attributes.containsKey('template') &&
+ (localName == 'option' || _TABLE_TAGS.containsKey(localName));
+
+ /**
+ * Returns true if this node is a template.
+ *
+ * A node is a template if [tagName] is TEMPLATE, or the node has the
+ * 'template' attribute and this tag supports attribute form for backwards
+ * compatibility with existing HTML parsers. The nodes that can use attribute
+ * form are table elments (THEAD, TBODY, TFOOT, TH, TR, TD, CAPTION, COLGROUP
+ * and COL) and OPTION.
+ */
+ // TODO(jmesserly): this is not a public MDV API, but it seems like a useful
+ // place to document which tags our polyfill considers to be templates.
+ // Otherwise I'd be repeating it in several other places.
+ // See if we can replace this with a TemplateMixin.
+ @Experimental
+ bool get isTemplate => tagName == 'TEMPLATE' || _isAttributeTemplate;
+
+ void _ensureTemplate() {
+ if (!isTemplate) {
+ throw new UnsupportedError('$this is not a template.');
+ }
+ TemplateElement.decorate(this);
+ }
+
Element.internal() : super.internal();
@DomName('Element.abortEvent')
@@ -8530,6 +8776,7 @@ abstract class Element extends Node implements ElementTraversal {
}
+
final _START_TAG_REGEXP = new RegExp('<(\\w+)');
class _ElementFactoryProvider {
static const _CUSTOM_PARENT_TAG_MAP = const {
@@ -8547,19 +8794,6 @@ class _ElementFactoryProvider {
'track' : 'audio',
};
- // TODO(jmesserly): const set would be better
- static const _TABLE_TAGS = const {
- 'caption': null,
- 'col': null,
- 'colgroup': null,
- 'tbody': null,
- 'td': null,
- 'tfoot': null,
- 'th': null,
- 'thead': null,
- 'tr': null,
- };
-
@DomName('Document.createElement')
static Element createElement_html(String html) {
// TODO(jacobr): this method can be made more robust and performant.
@@ -8573,7 +8807,7 @@ class _ElementFactoryProvider {
final match = _START_TAG_REGEXP.firstMatch(html);
if (match != null) {
tag = match.group(1).toLowerCase();
- if (Device.isIE && _TABLE_TAGS.containsKey(tag)) {
+ if (Device.isIE && Element._TABLE_TAGS.containsKey(tag)) {
return _createTableForIE(html, tag);
}
parentTag = _CUSTOM_PARENT_TAG_MAP[tag];
@@ -10609,6 +10843,10 @@ class HtmlDocument extends Document {
@SupportedBrowser(SupportedBrowser.SAFARI)
@Experimental
String get visibilityState => $dom_webkitVisibilityState;
+
+
+ // Note: used to polyfill <template>
+ Document _templateContentsOwner;
}
// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
@@ -11549,6 +11787,60 @@ class InputElement extends _Element_Merged implements
}
return e;
}
+
+ _ValueBinding _valueBinding;
+
+ _CheckedBinding _checkedBinding;
+
+ @Experimental
+ void bind(String name, model, String path) {
+ switch (name) {
+ case 'value':
+ unbind('value');
+ attributes.remove('value');
+ _valueBinding = new _ValueBinding(this, model, path);
+ break;
+ case 'checked':
+ unbind('checked');
+ attributes.remove('checked');
+ _checkedBinding = new _CheckedBinding(this, model, path);
+ break;
+ default:
+ // TODO(jmesserly): this should be "super" (http://dartbug.com/10166).
+ // Similar issue for unbind/unbindAll below.
+ Element._bindElement(this, name, model, path);
+ break;
+ }
+ }
+
+ @Experimental
+ void unbind(String name) {
+ switch (name) {
+ case 'value':
+ if (_valueBinding != null) {
+ _valueBinding.unbind();
+ _valueBinding = null;
+ }
+ break;
+ case 'checked':
+ if (_checkedBinding != null) {
+ _checkedBinding.unbind();
+ _checkedBinding = null;
+ }
+ break;
+ default:
+ Element._unbindElement(this, name);
+ break;
+ }
+ }
+
+ @Experimental
+ void unbindAll() {
+ unbind('value');
+ unbind('checked');
+ Element._unbindAllElement(this);
+ }
+
InputElement.internal() : super.internal();
@DomName('HTMLInputElement.webkitSpeechChangeEvent')
@@ -15279,7 +15571,7 @@ class _ChildNodeListLazy extends ListBase<Node> {
// time.
Node child = _this.$dom_firstChild;
while (child != null) {
- Node nextChild = child.nextSibling;
+ Node nextChild = child.nextNode;
if (test(child) == removeMatching) {
_this.$dom_removeChild(child);
}
@@ -15404,93 +15696,49 @@ class Node extends EventTarget {
}
}
- // Note that this may either be the locally set model or a cached value
- // of the inherited model. This is cached to minimize model change
- // notifications.
- var _model;
- bool _hasLocalModel;
- Set<StreamController<Node>> _modelChangedStreams;
+ /**
+ * Print out a String representation of this Node.
+ */
+ String toString() => localName == null ?
+ (nodeValue == null ? super.toString() : nodeValue) : localName;
/**
- * The data model which is inherited through the tree.
- *
- * Setting this will propagate the value to all descendant nodes. If the
- * model is not set on this node then it will be inherited from ancestor
- * nodes.
- *
- * Currently this does not support propagation through Shadow DOMs.
- *
- * [clearModel] must be used to remove the model property from this node
- * and have the model inherit from ancestor nodes.
+ * Binds the attribute [name] to the [path] of the [model].
+ * Path is a String of accessors such as `foo.bar.baz`.
*/
@Experimental
- get model {
- // If we have a change handler then we've cached the model locally.
- if (_modelChangedStreams != null && !_modelChangedStreams.isEmpty) {
- return _model;
- }
- // Otherwise start looking up the tree.
- for (var node = this; node != null; node = node.parentNode) {
- if (node._hasLocalModel == true) {
- return node._model;
- }
- }
- return null;
+ void bind(String name, model, String path) {
+ // TODO(jmesserly): should we throw instead?
+ window.console.error('Unhandled binding to Node: '
+ '$this $name $model $path');
}
+ /** Unbinds the attribute [name]. */
@Experimental
- void set model(value) {
- var changed = model != value;
- _model = value;
- _hasLocalModel = true;
- _ModelTreeObserver.initialize();
-
- if (changed) {
- if (_modelChangedStreams != null && !_modelChangedStreams.isEmpty) {
- _modelChangedStreams.toList().forEach((stream) => stream.add(this));
- }
- // Propagate new model to all descendants.
- _ModelTreeObserver.propagateModel(this, value, false);
- }
- }
+ void unbind(String name) {}
- /**
- * Clears the locally set model and makes this model be inherited from parent
- * nodes.
- */
+ /** Unbinds all bound attributes. */
@Experimental
- void clearModel() {
- if (_hasLocalModel == true) {
- _hasLocalModel = false;
+ void unbindAll() {}
- // Propagate new model to all descendants.
- if (parentNode != null) {
- _ModelTreeObserver.propagateModel(this, parentNode.model, false);
- } else {
- _ModelTreeObserver.propagateModel(this, null, false);
- }
- }
- }
+ TemplateInstance _templateInstance;
- /**
- * Get a stream of models, whenever the model changes.
- */
- Stream<Node> get onModelChanged {
- if (_modelChangedStreams == null) {
- _modelChangedStreams = new Set<StreamController<Node>>();
- }
- var controller;
- controller = new StreamController(
- onListen: () { _modelChangedStreams.add(controller); },
- onCancel: () { _modelChangedStreams.remove(controller); });
- return controller.stream;
+ // TODO(arv): Consider storing all "NodeRareData" on a single object?
+ int __instanceTerminatorCount;
+ int get _instanceTerminatorCount {
+ if (__instanceTerminatorCount == null) return 0;
+ return __instanceTerminatorCount;
+ }
+ set _instanceTerminatorCount(int value) {
+ if (value == 0) value = null;
+ __instanceTerminatorCount = value;
}
- /**
- * Print out a String representation of this Node.
- */
- String toString() => localName == null ?
- (nodeValue == null ? super.toString() : nodeValue) : localName;
+ /** Gets the template instance that instantiated this node, if any. */
+ @Experimental
+ TemplateInstance get templateInstance =>
+ _templateInstance != null ? _templateInstance :
+ (parent != null ? parent.templateInstance : null);
Node.internal() : super.internal();
@@ -19820,7 +20068,7 @@ class TableSectionElement extends _Element_Merged {
// WARNING: Do not edit - generated code.
-@DocsEditable
+@Experimental
@DomName('HTMLTemplateElement')
@SupportedBrowser(SupportedBrowser.CHROME)
@Experimental
@@ -19836,8 +20084,115 @@ class TemplateElement extends _Element_Merged {
@DomName('HTMLTemplateElement.content')
@DocsEditable
- DocumentFragment get content native "HTMLTemplateElement_content_Getter";
+ DocumentFragment get $dom_content native "HTMLTemplateElement_content_Getter";
+
+
+ // For real TemplateElement use the actual DOM .content field instead of
+ // our polyfilled expando.
+ @Experimental
+ DocumentFragment get content => $dom_content;
+
+ static StreamController<DocumentFragment> _instanceCreated;
+
+ /**
+ * *Warning*: This is an implementation helper for Model-Driven Views and
+ * should not be used in your code.
+ *
+ * This event is fired whenever a template is instantiated via
+ * [createInstance].
+ */
+ // TODO(rafaelw): This is a hack, and is neccesary for the polyfill
+ // because custom elements are not upgraded during clone()
+ @Experimental
+ static Stream<DocumentFragment> get instanceCreated {
+ if (_instanceCreated == null) {
+ _instanceCreated = new StreamController<DocumentFragment>();
+ }
+ return _instanceCreated.stream;
+ }
+
+ /**
+ * Ensures proper API and content model for template elements.
+ *
+ * [instanceRef] can be used to set the [Element.ref] property of [template],
+ * and use the ref's content will be used as source when createInstance() is
+ * invoked.
+ *
+ * Returns true if this template was just decorated, or false if it was
+ * already decorated.
+ */
+ @Experimental
+ static bool decorate(Element template, [Element instanceRef]) {
+ // == true check because it starts as a null field.
+ if (template._templateIsDecorated == true) return false;
+
+ template._templateIsDecorated = true;
+
+ _injectStylesheet();
+
+ // Create content
+ if (template is! TemplateElement) {
+ var doc = _getTemplateContentsOwner(template.document);
+ template._templateContent = doc.createDocumentFragment();
+ }
+
+ if (instanceRef != null) {
+ template._templateInstanceRef = instanceRef;
+ return true; // content is empty.
+ }
+
+ if (template is TemplateElement) {
+ _bootstrapTemplatesRecursivelyFrom(template.content);
+ } else {
+ _liftNonNativeTemplateChildrenIntoContent(template);
+ }
+
+ return true;
+ }
+
+ /**
+ * This used to decorate recursively all templates from a given node.
+ *
+ * By default [decorate] will be called on templates lazily when certain
+ * properties such as [model] are accessed, but it can be run eagerly to
+ * decorate an entire tree recursively.
+ */
+ // TODO(rafaelw): Review whether this is the right public API.
+ @Experimental
+ static void bootstrap(Node content) {
+ _bootstrapTemplatesRecursivelyFrom(content);
+ }
+
+ static bool _initStyles;
+
+ static void _injectStylesheet() {
+ if (_initStyles == true) return;
+ _initStyles = true;
+
+ var style = new StyleElement();
+ style.text = r'''
+template,
+thead[template],
+tbody[template],
+tfoot[template],
+th[template],
+tr[template],
+td[template],
+caption[template],
+colgroup[template],
+col[template],
+option[template] {
+ display: none;
+}''';
+ document.head.append(style);
+ }
+ /**
+ * A mapping of names to Custom Syntax objects. See [CustomBindingSyntax] for
+ * more information.
+ */
+ @Experimental
+ static Map<String, CustomBindingSyntax> syntax = {};
}
// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
@@ -19870,6 +20225,41 @@ class Text extends CharacterData {
@DocsEditable
Text splitText(int offset) native "Text_splitText_Callback";
+
+ StreamSubscription _textBinding;
+
+ @Experimental
+ void bind(String name, model, String path) {
+ if (name != 'text') {
+ super.bind(name, model, path);
+ return;
+ }
+
+ unbind('text');
+
+ _textBinding = new PathObserver(model, path).bindSync((value) {
+ text = value == null ? '' : '$value';
+ });
+ }
+
+ @Experimental
+ void unbind(String name) {
+ if (name != 'text') {
+ super.unbind(name);
+ return;
+ }
+
+ if (_textBinding == null) return;
+
+ _textBinding.cancel();
+ _textBinding = null;
+ }
+
+ @Experimental
+ void unbindAll() {
+ unbind('text');
+ super.unbindAll();
+ }
}
// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
@@ -25740,100 +26130,286 @@ abstract class KeyName {
// BSD-style license that can be found in the LICENSE file.
-class _ModelTreeObserver {
- static bool _initialized = false;
+// This code is inspired by ChangeSummary:
+// https://github.com/rafaelw/ChangeSummary/blob/master/change_summary.js
+// ...which underlies MDV. Since we don't need the functionality of
+// ChangeSummary, we just implement what we need for data bindings.
+// This allows our implementation to be much simpler.
- /**
- * Start an observer watching the document for tree changes to automatically
- * propagate model changes.
- *
- * Currently this does not support propagation through Shadow DOMs.
- */
- static void initialize() {
- if (!_initialized) {
- _initialized = true;
+// TODO(jmesserly): should we make these types stronger, and require
+// Observable objects? Currently, it is fine to say something like:
+// var path = new PathObserver(123, '');
+// print(path.value); // "123"
+//
+// Furthermore this degenerate case is allowed:
+// var path = new PathObserver(123, 'foo.bar.baz.qux');
+// print(path.value); // "null"
+//
+// Here we see that any invalid (i.e. not Observable) value will break the
+// path chain without producing an error or exception.
+//
+// Now the real question: should we do this? For the former case, the behavior
+// is correct but we could chose to handle it in the dart:html bindings layer.
+// For the latter case, it might be better to throw an error so users can find
+// the problem.
- if (MutationObserver.supported) {
- var observer = new MutationObserver(_processTreeChange);
- observer.observe(document, childList: true, subtree: true);
- } else {
- document.on['DOMNodeInserted'].listen(_handleNodeInserted);
- document.on['DOMNodeRemoved'].listen(_handleNodeRemoved);
- }
- }
- }
- static void _processTreeChange(List<MutationRecord> mutations,
- MutationObserver observer) {
- for (var record in mutations) {
- for (var node in record.addedNodes) {
- // When nodes enter the document we need to make sure that all of the
- // models are properly propagated through the entire sub-tree.
- propagateModel(node, _calculatedModel(node), true);
- }
- for (var node in record.removedNodes) {
- propagateModel(node, _calculatedModel(node), false);
+/**
+ * A data-bound path starting from a view-model or model object, for example
+ * `foo.bar.baz`.
+ *
+ * When the [values] stream is being listened to, this will observe changes to
+ * the object and any intermediate object along the path, and send [values]
+ * accordingly. When all listeners are unregistered it will stop observing
+ * the objects.
+ *
+ * This class is used to implement [Node.bind] and similar functionality.
+ */
+// TODO(jmesserly): find a better home for this type.
+@Experimental
+class PathObserver {
+ /** The object being observed. */
+ final object;
+
+ /** The path string. */
+ final String path;
+
+ /** True if the path is valid, otherwise false. */
+ final bool _isValid;
+
+ // TODO(jmesserly): same issue here as ObservableMixin: is there an easier
+ // way to get a broadcast stream?
+ StreamController _values;
+ Stream _valueStream;
+
+ _PropertyObserver _observer, _lastObserver;
+
+ Object _lastValue;
+ bool _scheduled = false;
+
+ /**
+ * Observes [path] on [object] for changes. This returns an object that can be
+ * used to get the changes and get/set the value at this path.
+ * See [PathObserver.values] and [PathObserver.value].
+ */
+ PathObserver(this.object, String path)
+ : path = path, _isValid = _isPathValid(path) {
+
+ // TODO(jmesserly): if the path is empty, or the object is! Observable, we
+ // can optimize the PathObserver to be more lightweight.
+
+ _values = new StreamController(onListen: _observe, onCancel: _unobserve);
+
+ if (_isValid) {
+ var segments = [];
+ for (var segment in path.trim().split('.')) {
+ if (segment == '') continue;
+ var index = int.parse(segment, onError: (_) {});
+ segments.add(index != null ? index : new Symbol(segment));
}
- }
- }
- static void _handleNodeInserted(MutationEvent e) {
- var node = e.target;
- window.setImmediate(() {
- propagateModel(node, _calculatedModel(node), true);
- });
+ // Create the property observer linked list.
+ // Note that the structure of a path can't change after it is initially
+ // constructed, even though the objects along the path can change.
+ for (int i = segments.length - 1; i >= 0; i--) {
+ _observer = new _PropertyObserver(this, segments[i], _observer);
+ if (_lastObserver == null) _lastObserver = _observer;
+ }
+ }
}
- static void _handleNodeRemoved(MutationEvent e) {
- var node = e.target;
- window.setImmediate(() {
- propagateModel(node, _calculatedModel(node), false);
- });
+ // TODO(jmesserly): we could try adding the first value to the stream, but
+ // that delivers the first record async.
+ /**
+ * Listens to the stream, and invokes the [callback] immediately with the
+ * current [value]. This is useful for bindings, which want to be up-to-date
+ * immediately.
+ */
+ StreamSubscription bindSync(void callback(value)) {
+ var result = values.listen(callback);
+ callback(value);
+ return result;
}
+ // TODO(jmesserly): should this be a change record with the old value?
+ // TODO(jmesserly): should this be a broadcast stream? We only need
+ // single-subscription in the bindings system, so single sub saves overhead.
/**
- * Figures out what the model should be for a node, avoiding any cached
- * model values.
+ * Gets the stream of values that were observed at this path.
+ * This returns a single-subscription stream.
*/
- static _calculatedModel(node) {
- if (node._hasLocalModel == true) {
- return node._model;
- } else if (node.parentNode != null) {
- return node.parentNode._model;
+ Stream get values => _values.stream;
+
+ /** Force synchronous delivery of [values]. */
+ void _deliverValues() {
+ _scheduled = false;
+
+ var newValue = value;
+ if (!identical(_lastValue, newValue)) {
+ _values.add(newValue);
+ _lastValue = newValue;
}
- return null;
}
- /**
- * Pushes model changes down through the tree.
- *
- * Set fullTree to true if the state of the tree is unknown and model changes
- * should be propagated through the entire tree.
- */
- static void propagateModel(Node node, model, bool fullTree) {
- // Calling into user code with the != call could generate exceptions.
- // Catch and report them a global exceptions.
- try {
- if (node._hasLocalModel != true && node._model != model &&
- node._modelChangedStreams != null &&
- !node._modelChangedStreams.isEmpty) {
- node._model = model;
- node._modelChangedStreams.toList()
- .forEach((controller) => controller.add(node));
- }
- } catch (e, s) {
- new Future.error(e, s);
+ void _observe() {
+ if (_observer != null) {
+ _lastValue = value;
+ _observer.observe();
}
- for (var child = node.$dom_firstChild; child != null;
- child = child.nextNode) {
- if (child._hasLocalModel != true) {
- propagateModel(child, model, fullTree);
- } else if (fullTree) {
- propagateModel(child, child._model, true);
+ }
+
+ void _unobserve() {
+ if (_observer != null) _observer.unobserve();
+ }
+
+ void _notifyChange() {
+ if (_scheduled) return;
+ _scheduled = true;
+
+ // TODO(jmesserly): should we have a guarenteed order with respect to other
+ // paths? If so, we could implement this fairly easily by sorting instances
+ // of this class by birth order before delivery.
+ queueChangeRecords(_deliverValues);
+ }
+
+ /** Gets the last reported value at this path. */
+ get value {
+ if (!_isValid) return null;
+ if (_observer == null) return object;
+ _observer.ensureValue(object);
+ return _lastObserver.value;
+ }
+
+ /** Sets the value at this path. */
+ void set value(Object value) {
+ // TODO(jmesserly): throw if property cannot be set?
+ // MDV seems tolerant of these error.
+ if (_observer == null || !_isValid) return;
+ _observer.ensureValue(object);
+ var last = _lastObserver;
+ if (_setObjectProperty(last._object, last._property, value)) {
+ // Technically, this would get updated asynchronously via a change record.
+ // However, it is nice if calling the getter will yield the same value
+ // that was just set. So we use this opportunity to update our cache.
+ last.value = value;
+ }
+ }
+}
+
+// TODO(jmesserly): these should go away in favor of mirrors!
+_getObjectProperty(object, property) {
+ if (object is List && property is int) {
+ if (property >= 0 && property < object.length) {
+ return object[property];
+ } else {
+ return null;
+ }
+ }
+
+ // TODO(jmesserly): what about length?
+ if (object is Map) return object[property];
+
+ if (object is Observable) return object.getValueWorkaround(property);
+
+ return null;
+}
+
+bool _setObjectProperty(object, property, value) {
+ if (object is List && property is int) {
+ object[property] = value;
+ } else if (object is Map) {
+ object[property] = value;
+ } else if (object is Observable) {
+ (object as Observable).setValueWorkaround(property, value);
+ } else {
+ return false;
+ }
+ return true;
+}
+
+
+class _PropertyObserver {
+ final PathObserver _path;
+ final _property;
+ final _PropertyObserver _next;
+
+ // TODO(jmesserly): would be nice not to store both of these.
+ Object _object;
+ Object _value;
+ StreamSubscription _sub;
+
+ _PropertyObserver(this._path, this._property, this._next);
+
+ get value => _value;
+
+ void set value(Object newValue) {
+ _value = newValue;
+ if (_next != null) {
+ if (_sub != null) _next.unobserve();
+ _next.ensureValue(_value);
+ if (_sub != null) _next.observe();
+ }
+ }
+
+ void ensureValue(object) {
+ // If we're observing, values should be up to date already.
+ if (_sub != null) return;
+
+ _object = object;
+ value = _getObjectProperty(object, _property);
+ }
+
+ void observe() {
+ if (_object is Observable) {
+ assert(_sub == null);
+ _sub = (_object as Observable).changes.listen(_onChange);
+ }
+ if (_next != null) _next.observe();
+ }
+
+ void unobserve() {
+ if (_sub == null) return;
+
+ _sub.cancel();
+ _sub = null;
+ if (_next != null) _next.unobserve();
+ }
+
+ void _onChange(List<ChangeRecord> changes) {
+ for (var change in changes) {
+ // TODO(jmesserly): what to do about "new Symbol" here?
+ // Ideally this would only preserve names if the user has opted in to
+ // them being preserved.
+ // TODO(jmesserly): should we drop observable maps with String keys?
+ // If so then we only need one check here.
+ if (change.changes(_property)) {
+ value = _getObjectProperty(_object, _property);
+ _path._notifyChange();
+ return;
}
}
}
}
+
+// From: https://github.com/rafaelw/ChangeSummary/blob/master/change_summary.js
+
+const _pathIndentPart = r'[$a-z0-9_]+[$a-z0-9_\d]*';
+final _pathRegExp = new RegExp('^'
+ '(?:#?' + _pathIndentPart + ')?'
+ '(?:'
+ '(?:\\.' + _pathIndentPart + ')'
+ ')*'
+ r'$', caseSensitive: false);
+
+final _spacesRegExp = new RegExp(r'\s');
+
+bool _isPathValid(String s) {
+ s = s.replaceAll(_spacesRegExp, '');
+
+ if (s == '') return true;
+ if (s[0] == '.') return false;
+ return _pathRegExp.hasMatch(s);
+}
// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
@@ -26061,6 +26637,781 @@ class Rect {
// BSD-style license that can be found in the LICENSE file.
+// This code is a port of Model-Driven-Views:
+// https://github.com/toolkitchen/mdv
+// The code mostly comes from src/template_element.js
+
+typedef void _ChangeHandler(value);
+
+/**
+ * Model-Driven Views (MDV)'s native features enables a wide-range of use cases,
+ * but (by design) don't attempt to implement a wide array of specialized
+ * behaviors.
+ *
+ * Enabling these features in MDV is a matter of implementing and registering an
+ * MDV Custom Syntax. A Custom Syntax is an object which contains one or more
+ * delegation functions which implement specialized behavior. This object is
+ * registered with MDV via [TemplateElement.syntax]:
+ *
+ *
+ * HTML:
+ * <template bind syntax="MySyntax">
+ * {{ What!Ever('crazy')->thing^^^I+Want(data) }}
+ * </template>
+ *
+ * Dart:
+ * class MySyntax extends CustomBindingSyntax {
+ * getBinding(model, path, name, node) {
+ * // The magic happens here!
+ * }
+ * }
+ *
+ * ...
+ *
+ * TemplateElement.syntax['MySyntax'] = new MySyntax();
+ *
+ * See <https://github.com/toolkitchen/mdv/blob/master/docs/syntax.md> for more
+ * information about Custom Syntax.
+ */
+// TODO(jmesserly): if this is just one method, a function type would make it
+// more Dart-friendly.
+@Experimental
+abstract class CustomBindingSyntax {
+ // TODO(jmesserly): I had to remove type annotations from "name" and "node"
+ // Normally they are String and Node respectively. But sometimes it will pass
+ // (int name, CompoundBinding node). That seems very confusing; we may want
+ // to change this API.
+ getBinding(model, String path, name, node);
+}
+
+/** The callback used in the [CompoundBinding.combinator] field. */
+@Experimental
+typedef Object CompoundBindingCombinator(Map objects);
+
+/** Information about the instantiated template. */
+@Experimental
+class TemplateInstance {
+ // TODO(rafaelw): firstNode & lastNode should be read-synchronous
+ // in cases where script has modified the template instance boundary.
+
+ /** The first node of this template instantiation. */
+ final Node firstNode;
+
+ /**
+ * The last node of this template instantiation.
+ * This could be identical to [firstNode] if the template only expanded to a
+ * single node.
+ */
+ final Node lastNode;
+
+ /** The model used to instantiate the template. */
+ final model;
+
+ TemplateInstance(this.firstNode, this.lastNode, this.model);
+}
+
+/**
+ * Model-Driven Views contains a helper object which is useful for the
+ * implementation of a Custom Syntax.
+ *
+ * var binding = new CompoundBinding((values) {
+ * var combinedValue;
+ * // compute combinedValue based on the current values which are provided
+ * return combinedValue;
+ * });
+ * binding.bind('name1', obj1, path1);
+ * binding.bind('name2', obj2, path2);
+ * //...
+ * binding.bind('nameN', objN, pathN);
+ *
+ * CompoundBinding is an object which knows how to listen to multiple path
+ * values (registered via [bind]) and invoke its [combinator] when one or more
+ * of the values have changed and set its [value] property to the return value
+ * of the function. When any value has changed, all current values are provided
+ * to the [combinator] in the single `values` argument.
+ *
+ * See [CustomBindingSyntax] for more information.
+ */
+// TODO(jmesserly): what is the public API surface here? I just guessed;
+// most of it seemed non-public.
+@Experimental
+class CompoundBinding extends ObservableBase {
+ CompoundBindingCombinator _combinator;
+
+ // TODO(jmesserly): ideally these would be String keys, but sometimes we
+ // use integers.
+ Map<dynamic, StreamSubscription> _bindings = new Map();
+ Map _values = new Map();
+ bool _scheduled = false;
+ bool _disposed = false;
+ Object _value;
+
+ CompoundBinding([CompoundBindingCombinator combinator]) {
+ // TODO(jmesserly): this is a tweak to the original code, it seemed to me
+ // that passing the combinator to the constructor should be equivalent to
+ // setting it via the property.
+ // I also added a null check to the combinator setter.
+ this.combinator = combinator;
+ }
+
+ CompoundBindingCombinator get combinator => _combinator;
+
+ set combinator(CompoundBindingCombinator combinator) {
+ _combinator = combinator;
+ if (combinator != null) _scheduleResolve();
+ }
+
+ static const _VALUE = const Symbol('value');
+
+ get value => _value;
+
+ void set value(newValue) {
+ _value = notifyPropertyChange(_VALUE, _value, newValue);
+ }
+
+ // TODO(jmesserly): remove these workarounds when dart2js supports mirrors!
+ getValueWorkaround(key) {
+ if (key == _VALUE) return value;
+ return null;
+ }
+ setValueWorkaround(key, val) {
+ if (key == _VALUE) value = val;
+ }
+
+ void bind(name, model, String path) {
+ unbind(name);
+
+ _bindings[name] = new PathObserver(model, path).bindSync((value) {
+ _values[name] = value;
+ _scheduleResolve();
+ });
+ }
+
+ void unbind(name, {bool suppressResolve: false}) {
+ var binding = _bindings.remove(name);
+ if (binding == null) return;
+
+ binding.cancel();
+ _values.remove(name);
+ if (!suppressResolve) _scheduleResolve();
+ }
+
+ // TODO(rafaelw): Is this the right processing model?
+ // TODO(rafaelw): Consider having a seperate ChangeSummary for
+ // CompoundBindings so to excess dirtyChecks.
+ void _scheduleResolve() {
+ if (_scheduled) return;
+ _scheduled = true;
+ queueChangeRecords(resolve);
+ }
+
+ void resolve() {
+ if (_disposed) return;
+ _scheduled = false;
+
+ if (_combinator == null) {
+ throw new StateError(
+ 'CompoundBinding attempted to resolve without a combinator');
+ }
+
+ value = _combinator(_values);
+ }
+
+ void dispose() {
+ for (var binding in _bindings.values) {
+ binding.cancel();
+ }
+ _bindings.clear();
+ _values.clear();
+
+ _disposed = true;
+ value = null;
+ }
+}
+
+Stream<Event> _getStreamForInputType(InputElement element) {
+ switch (element.type) {
+ case 'checkbox':
+ return element.onClick;
+ case 'radio':
+ case 'select-multiple':
+ case 'select-one':
+ return element.onChange;
+ default:
+ return element.onInput;
+ }
+}
+
+abstract class _InputBinding {
+ final InputElement element;
+ PathObserver binding;
+ StreamSubscription _pathSub;
+ StreamSubscription _eventSub;
+
+ _InputBinding(this.element, model, String path) {
+ binding = new PathObserver(model, path);
+ _pathSub = binding.bindSync(valueChanged);
+ _eventSub = _getStreamForInputType(element).listen(updateBinding);
+ }
+
+ void valueChanged(newValue);
+
+ void updateBinding(e);
+
+ void unbind() {
+ binding = null;
+ _pathSub.cancel();
+ _eventSub.cancel();
+ }
+}
+
+class _ValueBinding extends _InputBinding {
+ _ValueBinding(element, model, path) : super(element, model, path);
+
+ void valueChanged(value) {
+ element.value = value == null ? '' : '$value';
+ }
+
+ void updateBinding(e) {
+ binding.value = element.value;
+ }
+}
+
+// TODO(jmesserly): not sure what kind of boolean conversion rules to
+// apply for template data-binding. HTML attributes are true if they're present.
+// However Dart only treats "true" as true. Since this is HTML we'll use
+// something closer to the HTML rules: null (missing) and false are false,
+// everything else is true. See: https://github.com/toolkitchen/mdv/issues/59
+bool _templateBooleanConversion(value) => null != value && false != value;
+
+class _CheckedBinding extends _InputBinding {
+ _CheckedBinding(element, model, path) : super(element, model, path);
+
+ void valueChanged(value) {
+ element.checked = _templateBooleanConversion(value);
+ }
+
+ void updateBinding(e) {
+ binding.value = element.checked;
+
+ // Only the radio button that is getting checked gets an event. We
+ // therefore find all the associated radio buttons and update their
+ // CheckedBinding manually.
+ if (element is InputElement && element.type == 'radio') {
+ for (var r in _getAssociatedRadioButtons(element)) {
+ var checkedBinding = r._checkedBinding;
+ if (checkedBinding != null) {
+ // Set the value directly to avoid an infinite call stack.
+ checkedBinding.binding.value = false;
+ }
+ }
+ }
+ }
+}
+
+// TODO(jmesserly): polyfill document.contains API instead of doing it here
+bool _isNodeInDocument(Node node) {
+ // On non-IE this works:
+ // return node.document.contains(node);
+ var document = node.document;
+ if (node == document || node.parentNode == document) return true;
+ return document.documentElement.contains(node);
+}
+
+// |element| is assumed to be an HTMLInputElement with |type| == 'radio'.
+// Returns an array containing all radio buttons other than |element| that
+// have the same |name|, either in the form that |element| belongs to or,
+// if no form, in the document tree to which |element| belongs.
+//
+// This implementation is based upon the HTML spec definition of a
+// "radio button group":
+// http://www.whatwg.org/specs/web-apps/current-work/multipage/number-state.html#radio-button-group
+//
+Iterable _getAssociatedRadioButtons(element) {
+ if (!_isNodeInDocument(element)) return [];
+ if (element.form != null) {
+ return element.form.nodes.where((el) {
+ return el != element &&
+ el is InputElement &&
+ el.type == 'radio' &&
+ el.name == element.name;
+ });
+ } else {
+ var radios = element.document.queryAll(
+ 'input[type="radio"][name="${element.name}"]');
+ return radios.where((el) => el != element && el.form == null);
+ }
+}
+
+Node _createDeepCloneAndDecorateTemplates(Node node, String syntax) {
+ var clone = node.clone(false); // Shallow clone.
+ if (clone is Element && clone.isTemplate) {
+ TemplateElement.decorate(clone, node);
+ if (syntax != null) {
+ clone.attributes.putIfAbsent('syntax', () => syntax);
+ }
+ }
+
+ for (var c = node.$dom_firstChild; c != null; c = c.nextNode) {
+ clone.append(_createDeepCloneAndDecorateTemplates(c, syntax));
+ }
+ return clone;
+}
+
+// http://dvcs.w3.org/hg/webcomponents/raw-file/tip/spec/templates/index.html#dfn-template-contents-owner
+Document _getTemplateContentsOwner(Document doc) {
+ if (doc.window == null) {
+ return doc;
+ }
+ var d = doc._templateContentsOwner;
+ if (d == null) {
+ // TODO(arv): This should either be a Document or HTMLDocument depending
+ // on doc.
+ d = doc.implementation.createHtmlDocument('');
+ while (d.$dom_lastChild != null) {
+ d.$dom_lastChild.remove();
+ }
+ doc._templateContentsOwner = d;
+ }
+ return d;
+}
+
+Element _cloneAndSeperateAttributeTemplate(Element templateElement) {
+ var clone = templateElement.clone(false);
+ var attributes = templateElement.attributes;
+ for (var name in attributes.keys.toList()) {
+ switch (name) {
+ case 'template':
+ case 'repeat':
+ case 'bind':
+ case 'ref':
+ clone.attributes.remove(name);
+ break;
+ default:
+ attributes.remove(name);
+ break;
+ }
+ }
+
+ return clone;
+}
+
+void _liftNonNativeTemplateChildrenIntoContent(Element templateElement) {
+ var content = templateElement.content;
+
+ if (!templateElement._isAttributeTemplate) {
+ var child;
+ while ((child = templateElement.$dom_firstChild) != null) {
+ content.append(child);
+ }
+ return;
+ }
+
+ // For attribute templates we copy the whole thing into the content and
+ // we move the non template attributes into the content.
+ //
+ // <tr foo template>
+ //
+ // becomes
+ //
+ // <tr template>
+ // + #document-fragment
+ // + <tr foo>
+ //
+ var newRoot = _cloneAndSeperateAttributeTemplate(templateElement);
+ var child;
+ while ((child = templateElement.$dom_firstChild) != null) {
+ newRoot.append(child);
+ }
+ content.append(newRoot);
+}
+
+void _bootstrapTemplatesRecursivelyFrom(Node node) {
+ void bootstrap(template) {
+ if (!TemplateElement.decorate(template)) {
+ _bootstrapTemplatesRecursivelyFrom(template.content);
+ }
+ }
+
+ // Need to do this first as the contents may get lifted if |node| is
+ // template.
+ // TODO(jmesserly): node is DocumentFragment or Element
+ var templateDescendents = (node as dynamic).queryAll(_allTemplatesSelectors);
+ if (node is Element && node.isTemplate) bootstrap(node);
+
+ templateDescendents.forEach(bootstrap);
+}
+
+final String _allTemplatesSelectors = 'template, option[template], ' +
+ Element._TABLE_TAGS.keys.map((k) => "$k[template]").join(", ");
+
+void _addBindings(Node node, model, [CustomBindingSyntax syntax]) {
+ if (node is Element) {
+ _addAttributeBindings(node, model, syntax);
+ } else if (node is Text) {
+ _parseAndBind(node, node.text, 'text', model, syntax);
+ }
+
+ for (var c = node.$dom_firstChild; c != null; c = c.nextNode) {
+ _addBindings(c, model, syntax);
+ }
+}
+
+
+void _addAttributeBindings(Element element, model, syntax) {
+ element.attributes.forEach((name, value) {
+ if (value == '' && (name == 'bind' || name == 'repeat')) {
+ value = '{{}}';
+ }
+ _parseAndBind(element, value, name, model, syntax);
+ });
+}
+
+void _parseAndBind(Node node, String text, String name, model,
+ CustomBindingSyntax syntax) {
+
+ var tokens = _parseMustacheTokens(text);
+ if (tokens.length == 0 || (tokens.length == 1 && tokens[0].isText)) {
+ return;
+ }
+
+ if (tokens.length == 1 && tokens[0].isBinding) {
+ _bindOrDelegate(node, name, model, tokens[0].value, syntax);
+ return;
+ }
+
+ var replacementBinding = new CompoundBinding();
+ for (var i = 0; i < tokens.length; i++) {
+ var token = tokens[i];
+ if (token.isBinding) {
+ _bindOrDelegate(replacementBinding, i, model, token.value, syntax);
+ }
+ }
+
+ replacementBinding.combinator = (values) {
+ var newValue = new StringBuffer();
+
+ for (var i = 0; i < tokens.length; i++) {
+ var token = tokens[i];
+ if (token.isText) {
+ newValue.write(token.value);
+ } else {
+ var value = values[i];
+ if (value != null) {
+ newValue.write(value);
+ }
+ }
+ }
+
+ return newValue.toString();
+ };
+
+ node.bind(name, replacementBinding, 'value');
+}
+
+void _bindOrDelegate(node, name, model, String path,
+ CustomBindingSyntax syntax) {
+
+ if (syntax != null) {
+ var delegateBinding = syntax.getBinding(model, path, name, node);
+ if (delegateBinding != null) {
+ model = delegateBinding;
+ path = 'value';
+ }
+ }
+
+ node.bind(name, model, path);
+}
+
+class _BindingToken {
+ final String value;
+ final bool isBinding;
+
+ _BindingToken(this.value, {this.isBinding: false});
+
+ bool get isText => !isBinding;
+}
+
+List<_BindingToken> _parseMustacheTokens(String s) {
+ var result = [];
+ var length = s.length;
+ var index = 0, lastIndex = 0;
+ while (lastIndex < length) {
+ index = s.indexOf('{{', lastIndex);
+ if (index < 0) {
+ result.add(new _BindingToken(s.substring(lastIndex)));
+ break;
+ } else {
+ // There is a non-empty text run before the next path token.
+ if (index > 0 && lastIndex < index) {
+ result.add(new _BindingToken(s.substring(lastIndex, index)));
+ }
+ lastIndex = index + 2;
+ index = s.indexOf('}}', lastIndex);
+ if (index < 0) {
+ var text = s.substring(lastIndex - 2);
+ if (result.length > 0 && result.last.isText) {
+ result.last.value += text;
+ } else {
+ result.add(new _BindingToken(text));
+ }
+ break;
+ }
+
+ var value = s.substring(lastIndex, index).trim();
+ result.add(new _BindingToken(value, isBinding: true));
+ lastIndex = index + 2;
+ }
+ }
+ return result;
+}
+
+void _addTemplateInstanceRecord(fragment, model) {
+ if (fragment.$dom_firstChild == null) {
+ return;
+ }
+
+ var instanceRecord = new TemplateInstance(
+ fragment.$dom_firstChild, fragment.$dom_lastChild, model);
+
+ var node = instanceRecord.firstNode;
+ while (node != null) {
+ node._templateInstance = instanceRecord;
+ node = node.nextNode;
+ }
+}
+
+void _removeAllBindingsRecursively(Node node) {
+ node.unbindAll();
+ for (var c = node.$dom_firstChild; c != null; c = c.nextNode) {
+ _removeAllBindingsRecursively(c);
+ }
+}
+
+void _removeTemplateChild(Node parent, Node child) {
+ child._templateInstance = null;
+ if (child is Element && child.isTemplate) {
+ // Make sure we stop observing when we remove an element.
+ var templateIterator = child._templateIterator;
+ if (templateIterator != null) {
+ templateIterator.abandon();
+ child._templateIterator = null;
+ }
+ }
+ child.remove();
+ _removeAllBindingsRecursively(child);
+}
+
+class _InstanceCursor {
+ final Element _template;
+ Node _terminator;
+ Node _previousTerminator;
+ int _previousIndex = -1;
+ int _index = 0;
+
+ _InstanceCursor(this._template, [index]) {
+ _terminator = _template;
+ if (index != null) {
+ while (index-- > 0) {
+ next();
+ }
+ }
+ }
+
+ void next() {
+ _previousTerminator = _terminator;
+ _previousIndex = _index;
+ _index++;
+
+ while (_index > _terminator._instanceTerminatorCount) {
+ _index -= _terminator._instanceTerminatorCount;
+ _terminator = _terminator.nextNode;
+ if (_terminator is Element && _terminator.tagName == 'TEMPLATE') {
+ _index += _instanceCount(_terminator);
+ }
+ }
+ }
+
+ void abandon() {
+ assert(_instanceCount(_template) > 0);
+ assert(_terminator._instanceTerminatorCount > 0);
+ assert(_index > 0);
+
+ _terminator._instanceTerminatorCount--;
+ _index--;
+ }
+
+ void insert(fragment) {
+ assert(_template.parentNode != null);
+
+ _previousTerminator = _terminator;
+ _previousIndex = _index;
+ _index++;
+
+ _terminator = fragment.$dom_lastChild;
+ if (_terminator == null) _terminator = _previousTerminator;
+ _template.parentNode.insertBefore(fragment, _previousTerminator.nextNode);
+
+ _terminator._instanceTerminatorCount++;
+ if (_terminator != _previousTerminator) {
+ while (_previousTerminator._instanceTerminatorCount >
+ _previousIndex) {
+ _previousTerminator._instanceTerminatorCount--;
+ _terminator._instanceTerminatorCount++;
+ }
+ }
+ }
+
+ void remove() {
+ assert(_previousIndex != -1);
+ assert(_previousTerminator != null &&
+ (_previousIndex > 0 || _previousTerminator == _template));
+ assert(_terminator != null && _index > 0);
+ assert(_template.parentNode != null);
+ assert(_instanceCount(_template) > 0);
+
+ if (_previousTerminator == _terminator) {
+ assert(_index == _previousIndex + 1);
+ _terminator._instanceTerminatorCount--;
+ _terminator = _template;
+ _previousTerminator = null;
+ _previousIndex = -1;
+ return;
+ }
+
+ _terminator._instanceTerminatorCount--;
+
+ var parent = _template.parentNode;
+ while (_previousTerminator.nextNode != _terminator) {
+ _removeTemplateChild(parent, _previousTerminator.nextNode);
+ }
+ _removeTemplateChild(parent, _terminator);
+
+ _terminator = _previousTerminator;
+ _index = _previousIndex;
+ _previousTerminator = null;
+ _previousIndex = -1; // 0?
+ }
+}
+
+
+class _TemplateIterator {
+ final Element _templateElement;
+ int instanceCount = 0;
+ List iteratedValue;
+ bool observing = false;
+ final CompoundBinding inputs;
+
+ StreamSubscription _sub;
+ StreamSubscription _valueBinding;
+
+ _TemplateIterator(this._templateElement)
+ : inputs = new CompoundBinding(resolveInputs) {
+
+ _valueBinding = new PathObserver(inputs, 'value').bindSync(valueChanged);
+ }
+
+ static Object resolveInputs(Map values) {
+ if (values.containsKey('if') && !_templateBooleanConversion(values['if'])) {
+ return null;
+ }
+
+ if (values.containsKey('repeat')) {
+ return values['repeat'];
+ }
+
+ if (values.containsKey('bind')) {
+ return [values['bind']];
+ }
+
+ return null;
+ }
+
+ void valueChanged(value) {
+ clear();
+ if (value is! List) return;
+
+ iteratedValue = value;
+
+ if (value is Observable) {
+ _sub = value.changes.listen(_handleChanges);
+ }
+
+ int len = iteratedValue.length;
+ if (len > 0) {
+ _handleChanges([new ListChangeRecord(0, addedCount: len)]);
+ }
+ }
+
+ // TODO(jmesserly): port MDV v3.
+ getInstanceModel(model, syntax) => model;
+ getInstanceFragment(syntax) => _templateElement.createInstance();
+
+ void _handleChanges(List<ListChangeRecord> splices) {
+ var syntax = TemplateElement.syntax[_templateElement.attributes['syntax']];
+
+ for (var splice in splices) {
+ if (splice is! ListChangeRecord) continue;
+
+ for (int i = 0; i < splice.removedCount; i++) {
+ var cursor = new _InstanceCursor(_templateElement, splice.index + 1);
+ cursor.remove();
+ instanceCount--;
+ }
+
+ for (var addIndex = splice.index;
+ addIndex < splice.index + splice.addedCount;
+ addIndex++) {
+
+ var model = getInstanceModel(iteratedValue[addIndex], syntax);
+ var fragment = getInstanceFragment(syntax);
+
+ _addBindings(fragment, model, syntax);
+ _addTemplateInstanceRecord(fragment, model);
+
+ var cursor = new _InstanceCursor(_templateElement, addIndex);
+ cursor.insert(fragment);
+ instanceCount++;
+ }
+ }
+ }
+
+ void unobserve() {
+ if (_sub == null) return;
+ _sub.cancel();
+ _sub = null;
+ }
+
+ void clear() {
+ unobserve();
+
+ iteratedValue = null;
+ if (instanceCount == 0) return;
+
+ for (var i = 0; i < instanceCount; i++) {
+ var cursor = new _InstanceCursor(_templateElement, 1);
+ cursor.remove();
+ }
+
+ instanceCount = 0;
+ }
+
+ void abandon() {
+ unobserve();
+ _valueBinding.cancel();
+ inputs.dispose();
+ }
+}
+
+int _instanceCount(Element element) {
+ var templateIterator = element._templateIterator;
+ return templateIterator != null ? templateIterator.instanceCount : 0;
+}
+// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+
/**
* Helper class to implement custom events which wrap DOM events.
*/

Powered by Google App Engine
This is Rietveld 408576698