| OLD | NEW |
| 1 /// The Dart HTML library. | 1 /// The Dart HTML library. |
| 2 library dart.dom.html; | 2 library dart.dom.html; |
| 3 | 3 |
| 4 import 'dart:async'; | 4 import 'dart:async'; |
| 5 import 'dart:collection'; | 5 import 'dart:collection'; |
| 6 import 'dart:_collection-dev'; | 6 import 'dart:_collection-dev' hide Symbol; |
| 7 import 'dart:html_common'; | 7 import 'dart:html_common'; |
| 8 import 'dart:indexed_db'; | 8 import 'dart:indexed_db'; |
| 9 import 'dart:isolate'; | 9 import 'dart:isolate'; |
| 10 import 'dart:json' as json; | 10 import 'dart:json' as json; |
| 11 import 'dart:math'; | 11 import 'dart:math'; |
| 12 import 'dart:nativewrappers'; | 12 import 'dart:nativewrappers'; |
| 13 import 'dart:mdv_observe_impl'; |
| 13 import 'dart:typed_data'; | 14 import 'dart:typed_data'; |
| 14 import 'dart:web_gl' as gl; | 15 import 'dart:web_gl' as gl; |
| 15 import 'dart:web_sql'; | 16 import 'dart:web_sql'; |
| 16 import 'dart:svg' as svg; | 17 import 'dart:svg' as svg; |
| 17 import 'dart:web_audio' as web_audio; | 18 import 'dart:web_audio' as web_audio; |
| 18 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file | 19 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file |
| 19 // for details. All rights reserved. Use of this source code is governed by a | 20 // for details. All rights reserved. Use of this source code is governed by a |
| 20 // BSD-style license that can be found in the LICENSE file. | 21 // BSD-style license that can be found in the LICENSE file. |
| 21 | 22 |
| 22 // DO NOT EDIT | 23 // DO NOT EDIT |
| (...skipping 7013 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 7036 | 7037 |
| 7037 // TODO(nweiz): Do we want to support some variant of innerHtml for XML and/or | 7038 // TODO(nweiz): Do we want to support some variant of innerHtml for XML and/or |
| 7038 // SVG strings? | 7039 // SVG strings? |
| 7039 void set innerHtml(String value) { | 7040 void set innerHtml(String value) { |
| 7040 this.nodes.clear(); | 7041 this.nodes.clear(); |
| 7041 | 7042 |
| 7042 final e = new Element.tag("div"); | 7043 final e = new Element.tag("div"); |
| 7043 e.innerHtml = value; | 7044 e.innerHtml = value; |
| 7044 | 7045 |
| 7045 // Copy list first since we don't want liveness during iteration. | 7046 // Copy list first since we don't want liveness during iteration. |
| 7046 List nodes = new List.from(e.nodes); | 7047 List nodes = new List.from(e.nodes, growable: false); |
| 7047 this.nodes.addAll(nodes); | 7048 this.nodes.addAll(nodes); |
| 7048 } | 7049 } |
| 7049 | 7050 |
| 7050 /** | 7051 /** |
| 7051 * Adds the specified text as a text node after the last child of this | 7052 * Adds the specified text as a text node after the last child of this |
| 7052 * document fragment. | 7053 * document fragment. |
| 7053 */ | 7054 */ |
| 7054 void appendText(String text) { | 7055 void appendText(String text) { |
| 7055 this.append(new Text(text)); | 7056 this.append(new Text(text)); |
| 7056 } | 7057 } |
| (...skipping 397 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 7454 if (result == null) throw new StateError("No elements"); | 7455 if (result == null) throw new StateError("No elements"); |
| 7455 return result; | 7456 return result; |
| 7456 } | 7457 } |
| 7457 | 7458 |
| 7458 Element get single { | 7459 Element get single { |
| 7459 if (length > 1) throw new StateError("More than one element"); | 7460 if (length > 1) throw new StateError("More than one element"); |
| 7460 return first; | 7461 return first; |
| 7461 } | 7462 } |
| 7462 } | 7463 } |
| 7463 | 7464 |
| 7464 /** | 7465 /** |
| 7465 * An immutable list containing HTML elements. This list contains some | 7466 * An immutable list containing HTML elements. This list contains some |
| 7466 * additional methods for ease of CSS manipulation on a group of elements. | 7467 * additional methods for ease of CSS manipulation on a group of elements. |
| 7467 */ | 7468 */ |
| 7468 abstract class ElementList<T extends Element> extends ListBase<T> { | 7469 abstract class ElementList<T extends Element> extends ListBase<T> { |
| 7469 /** | 7470 /** |
| 7470 * The union of all CSS classes applied to the elements in this list. | 7471 * The union of all CSS classes applied to the elements in this list. |
| 7471 * | 7472 * |
| 7472 * This set makes it easy to add, remove or toggle (add if not present, remove | 7473 * This set makes it easy to add, remove or toggle (add if not present, remove |
| 7473 * if present) the classes applied to a collection of elements. | 7474 * if present) the classes applied to a collection of elements. |
| 7474 * | 7475 * |
| (...skipping 314 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 7789 this.$dom_scrollIntoViewIfNeeded(true); | 7790 this.$dom_scrollIntoViewIfNeeded(true); |
| 7790 } else { | 7791 } else { |
| 7791 this.$dom_scrollIntoViewIfNeeded(); | 7792 this.$dom_scrollIntoViewIfNeeded(); |
| 7792 } | 7793 } |
| 7793 } else { | 7794 } else { |
| 7794 this.$dom_scrollIntoView(); | 7795 this.$dom_scrollIntoView(); |
| 7795 } | 7796 } |
| 7796 } | 7797 } |
| 7797 | 7798 |
| 7798 | 7799 |
| 7800 @Creates('Null') |
| 7801 Map<String, StreamSubscription> _attributeBindings; |
| 7802 |
| 7803 // TODO(jmesserly): I'm concerned about adding these to every element. |
| 7804 // Conceptually all of these belong on TemplateElement. They are here to |
| 7805 // support browsers that don't have <template> yet. |
| 7806 // However even in the polyfill they're restricted to certain tags |
| 7807 // (see [isTemplate]). So we can probably convert it to a (public) mixin, and |
| 7808 // only mix it in to the elements that need it. |
| 7809 var _model; |
| 7810 |
| 7811 _TemplateIterator _templateIterator; |
| 7812 |
| 7813 Element _templateInstanceRef; |
| 7814 |
| 7815 // Note: only used if `this is! TemplateElement` |
| 7816 DocumentFragment _templateContent; |
| 7817 |
| 7818 bool _templateIsDecorated; |
| 7819 |
| 7820 // TODO(jmesserly): should path be optional, and default to empty path? |
| 7821 // It is used that way in at least one path in JS TemplateElement tests |
| 7822 // (see "BindImperative" test in original JS code). |
| 7823 @Experimental |
| 7824 void bind(String name, model, String path) { |
| 7825 _bindElement(this, name, model, path); |
| 7826 } |
| 7827 |
| 7828 // TODO(jmesserly): this is static to work around http://dartbug.com/10166 |
| 7829 // Similar issue for unbind/unbindAll below. |
| 7830 static void _bindElement(Element self, String name, model, String path) { |
| 7831 if (self._bindTemplate(name, model, path)) return; |
| 7832 |
| 7833 if (self._attributeBindings == null) { |
| 7834 self._attributeBindings = new Map<String, StreamSubscription>(); |
| 7835 } |
| 7836 |
| 7837 self.attributes.remove(name); |
| 7838 |
| 7839 var changed; |
| 7840 if (name.endsWith('?')) { |
| 7841 name = name.substring(0, name.length - 1); |
| 7842 |
| 7843 changed = (value) { |
| 7844 if (_templateBooleanConversion(value)) { |
| 7845 self.attributes[name] = ''; |
| 7846 } else { |
| 7847 self.attributes.remove(name); |
| 7848 } |
| 7849 }; |
| 7850 } else { |
| 7851 changed = (value) { |
| 7852 // TODO(jmesserly): escape value if needed to protect against XSS. |
| 7853 // See https://github.com/toolkitchen/mdv/issues/58 |
| 7854 self.attributes[name] = value == null ? '' : '$value'; |
| 7855 }; |
| 7856 } |
| 7857 |
| 7858 self.unbind(name); |
| 7859 |
| 7860 self._attributeBindings[name] = |
| 7861 new PathObserver(model, path).bindSync(changed); |
| 7862 } |
| 7863 |
| 7864 @Experimental |
| 7865 void unbind(String name) { |
| 7866 _unbindElement(this, name); |
| 7867 } |
| 7868 |
| 7869 static _unbindElement(Element self, String name) { |
| 7870 if (self._unbindTemplate(name)) return; |
| 7871 if (self._attributeBindings != null) { |
| 7872 var binding = self._attributeBindings.remove(name); |
| 7873 if (binding != null) binding.cancel(); |
| 7874 } |
| 7875 } |
| 7876 |
| 7877 @Experimental |
| 7878 void unbindAll() { |
| 7879 _unbindAllElement(this); |
| 7880 } |
| 7881 |
| 7882 static void _unbindAllElement(Element self) { |
| 7883 self._unbindAllTemplate(); |
| 7884 |
| 7885 if (self._attributeBindings != null) { |
| 7886 for (var binding in self._attributeBindings.values) { |
| 7887 binding.cancel(); |
| 7888 } |
| 7889 self._attributeBindings = null; |
| 7890 } |
| 7891 } |
| 7892 |
| 7893 // TODO(jmesserly): unlike the JS polyfill, we can't mixin |
| 7894 // HTMLTemplateElement at runtime into things that are semantically template |
| 7895 // elements. So instead we implement it here with a runtime check. |
| 7896 // If the bind succeeds, we return true, otherwise we return false and let |
| 7897 // the normal Element.bind logic kick in. |
| 7898 bool _bindTemplate(String name, model, String path) { |
| 7899 if (isTemplate) { |
| 7900 switch (name) { |
| 7901 case 'bind': |
| 7902 case 'repeat': |
| 7903 case 'if': |
| 7904 _ensureTemplate(); |
| 7905 if (_templateIterator == null) { |
| 7906 _templateIterator = new _TemplateIterator(this); |
| 7907 } |
| 7908 _templateIterator.inputs.bind(name, model, path); |
| 7909 return true; |
| 7910 } |
| 7911 } |
| 7912 return false; |
| 7913 } |
| 7914 |
| 7915 bool _unbindTemplate(String name) { |
| 7916 if (isTemplate) { |
| 7917 switch (name) { |
| 7918 case 'bind': |
| 7919 case 'repeat': |
| 7920 case 'if': |
| 7921 _ensureTemplate(); |
| 7922 if (_templateIterator != null) { |
| 7923 _templateIterator.inputs.unbind(name); |
| 7924 } |
| 7925 return true; |
| 7926 } |
| 7927 } |
| 7928 return false; |
| 7929 } |
| 7930 |
| 7931 void _unbindAllTemplate() { |
| 7932 if (isTemplate) { |
| 7933 unbind('bind'); |
| 7934 unbind('repeat'); |
| 7935 unbind('if'); |
| 7936 } |
| 7937 } |
| 7938 |
| 7939 /** |
| 7940 * Gets the template this node refers to. |
| 7941 * This is only supported if [isTemplate] is true. |
| 7942 */ |
| 7943 @Experimental |
| 7944 Element get ref { |
| 7945 _ensureTemplate(); |
| 7946 |
| 7947 Element ref = null; |
| 7948 var refId = attributes['ref']; |
| 7949 if (refId != null) { |
| 7950 ref = document.getElementById(refId); |
| 7951 } |
| 7952 |
| 7953 return ref != null ? ref : _templateInstanceRef; |
| 7954 } |
| 7955 |
| 7956 /** |
| 7957 * Gets the content of this template. |
| 7958 * This is only supported if [isTemplate] is true. |
| 7959 */ |
| 7960 @Experimental |
| 7961 DocumentFragment get content { |
| 7962 _ensureTemplate(); |
| 7963 return _templateContent; |
| 7964 } |
| 7965 |
| 7966 /** |
| 7967 * Creates an instance of the template. |
| 7968 * This is only supported if [isTemplate] is true. |
| 7969 */ |
| 7970 @Experimental |
| 7971 DocumentFragment createInstance() { |
| 7972 _ensureTemplate(); |
| 7973 |
| 7974 var template = ref; |
| 7975 if (template == null) template = this; |
| 7976 |
| 7977 var instance = _createDeepCloneAndDecorateTemplates(template.content, |
| 7978 attributes['syntax']); |
| 7979 |
| 7980 if (TemplateElement._instanceCreated != null) { |
| 7981 TemplateElement._instanceCreated.add(instance); |
| 7982 } |
| 7983 return instance; |
| 7984 } |
| 7985 |
| 7986 /** |
| 7987 * The data model which is inherited through the tree. |
| 7988 * This is only supported if [isTemplate] is true. |
| 7989 * |
| 7990 * Setting this will destructive propagate the value to all descendant nodes, |
| 7991 * and reinstantiate all of the nodes expanded by this template. |
| 7992 * |
| 7993 * Currently this does not support propagation through Shadow DOMs. |
| 7994 */ |
| 7995 @Experimental |
| 7996 get model => _model; |
| 7997 |
| 7998 @Experimental |
| 7999 void set model(value) { |
| 8000 _ensureTemplate(); |
| 8001 |
| 8002 _model = value; |
| 8003 _addBindings(this, model); |
| 8004 } |
| 8005 |
| 8006 // TODO(jmesserly): const set would be better |
| 8007 static const _TABLE_TAGS = const { |
| 8008 'caption': null, |
| 8009 'col': null, |
| 8010 'colgroup': null, |
| 8011 'tbody': null, |
| 8012 'td': null, |
| 8013 'tfoot': null, |
| 8014 'th': null, |
| 8015 'thead': null, |
| 8016 'tr': null, |
| 8017 }; |
| 8018 |
| 8019 bool get _isAttributeTemplate => attributes.containsKey('template') && |
| 8020 (localName == 'option' || _TABLE_TAGS.containsKey(localName)); |
| 8021 |
| 8022 /** |
| 8023 * Returns true if this node is a template. |
| 8024 * |
| 8025 * A node is a template if [tagName] is TEMPLATE, or the node has the |
| 8026 * 'template' attribute and this tag supports attribute form for backwards |
| 8027 * compatibility with existing HTML parsers. The nodes that can use attribute |
| 8028 * form are table elments (THEAD, TBODY, TFOOT, TH, TR, TD, CAPTION, COLGROUP |
| 8029 * and COL) and OPTION. |
| 8030 */ |
| 8031 // TODO(jmesserly): this is not a public MDV API, but it seems like a useful |
| 8032 // place to document which tags our polyfill considers to be templates. |
| 8033 // Otherwise I'd be repeating it in several other places. |
| 8034 // See if we can replace this with a TemplateMixin. |
| 8035 @Experimental |
| 8036 bool get isTemplate => tagName == 'TEMPLATE' || _isAttributeTemplate; |
| 8037 |
| 8038 void _ensureTemplate() { |
| 8039 if (!isTemplate) { |
| 8040 throw new UnsupportedError('$this is not a template.'); |
| 8041 } |
| 8042 TemplateElement.decorate(this); |
| 8043 } |
| 8044 |
| 7799 Element.internal() : super.internal(); | 8045 Element.internal() : super.internal(); |
| 7800 | 8046 |
| 7801 @DomName('Element.abortEvent') | 8047 @DomName('Element.abortEvent') |
| 7802 @DocsEditable | 8048 @DocsEditable |
| 7803 static const EventStreamProvider<Event> abortEvent = const EventStreamProvider
<Event>('abort'); | 8049 static const EventStreamProvider<Event> abortEvent = const EventStreamProvider
<Event>('abort'); |
| 7804 | 8050 |
| 7805 @DomName('Element.beforecopyEvent') | 8051 @DomName('Element.beforecopyEvent') |
| 7806 @DocsEditable | 8052 @DocsEditable |
| 7807 static const EventStreamProvider<Event> beforeCopyEvent = const EventStreamPro
vider<Event>('beforecopy'); | 8053 static const EventStreamProvider<Event> beforeCopyEvent = const EventStreamPro
vider<Event>('beforecopy'); |
| 7808 | 8054 |
| (...skipping 714 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 8523 @DomName('Element.onwebkitfullscreenchange') | 8769 @DomName('Element.onwebkitfullscreenchange') |
| 8524 @DocsEditable | 8770 @DocsEditable |
| 8525 Stream<Event> get onFullscreenChange => fullscreenChangeEvent.forTarget(this); | 8771 Stream<Event> get onFullscreenChange => fullscreenChangeEvent.forTarget(this); |
| 8526 | 8772 |
| 8527 @DomName('Element.onwebkitfullscreenerror') | 8773 @DomName('Element.onwebkitfullscreenerror') |
| 8528 @DocsEditable | 8774 @DocsEditable |
| 8529 Stream<Event> get onFullscreenError => fullscreenErrorEvent.forTarget(this); | 8775 Stream<Event> get onFullscreenError => fullscreenErrorEvent.forTarget(this); |
| 8530 | 8776 |
| 8531 } | 8777 } |
| 8532 | 8778 |
| 8779 |
| 8533 final _START_TAG_REGEXP = new RegExp('<(\\w+)'); | 8780 final _START_TAG_REGEXP = new RegExp('<(\\w+)'); |
| 8534 class _ElementFactoryProvider { | 8781 class _ElementFactoryProvider { |
| 8535 static const _CUSTOM_PARENT_TAG_MAP = const { | 8782 static const _CUSTOM_PARENT_TAG_MAP = const { |
| 8536 'body' : 'html', | 8783 'body' : 'html', |
| 8537 'head' : 'html', | 8784 'head' : 'html', |
| 8538 'caption' : 'table', | 8785 'caption' : 'table', |
| 8539 'td': 'tr', | 8786 'td': 'tr', |
| 8540 'th': 'tr', | 8787 'th': 'tr', |
| 8541 'colgroup': 'table', | 8788 'colgroup': 'table', |
| 8542 'col' : 'colgroup', | 8789 'col' : 'colgroup', |
| 8543 'tr' : 'tbody', | 8790 'tr' : 'tbody', |
| 8544 'tbody' : 'table', | 8791 'tbody' : 'table', |
| 8545 'tfoot' : 'table', | 8792 'tfoot' : 'table', |
| 8546 'thead' : 'table', | 8793 'thead' : 'table', |
| 8547 'track' : 'audio', | 8794 'track' : 'audio', |
| 8548 }; | 8795 }; |
| 8549 | 8796 |
| 8550 // TODO(jmesserly): const set would be better | |
| 8551 static const _TABLE_TAGS = const { | |
| 8552 'caption': null, | |
| 8553 'col': null, | |
| 8554 'colgroup': null, | |
| 8555 'tbody': null, | |
| 8556 'td': null, | |
| 8557 'tfoot': null, | |
| 8558 'th': null, | |
| 8559 'thead': null, | |
| 8560 'tr': null, | |
| 8561 }; | |
| 8562 | |
| 8563 @DomName('Document.createElement') | 8797 @DomName('Document.createElement') |
| 8564 static Element createElement_html(String html) { | 8798 static Element createElement_html(String html) { |
| 8565 // TODO(jacobr): this method can be made more robust and performant. | 8799 // TODO(jacobr): this method can be made more robust and performant. |
| 8566 // 1) Cache the dummy parent elements required to use innerHTML rather than | 8800 // 1) Cache the dummy parent elements required to use innerHTML rather than |
| 8567 // creating them every call. | 8801 // creating them every call. |
| 8568 // 2) Verify that the html does not contain leading or trailing text nodes. | 8802 // 2) Verify that the html does not contain leading or trailing text nodes. |
| 8569 // 3) Verify that the html does not contain both <head> and <body> tags. | 8803 // 3) Verify that the html does not contain both <head> and <body> tags. |
| 8570 // 4) Detatch the created element from its dummy parent. | 8804 // 4) Detatch the created element from its dummy parent. |
| 8571 String parentTag = 'div'; | 8805 String parentTag = 'div'; |
| 8572 String tag; | 8806 String tag; |
| 8573 final match = _START_TAG_REGEXP.firstMatch(html); | 8807 final match = _START_TAG_REGEXP.firstMatch(html); |
| 8574 if (match != null) { | 8808 if (match != null) { |
| 8575 tag = match.group(1).toLowerCase(); | 8809 tag = match.group(1).toLowerCase(); |
| 8576 if (Device.isIE && _TABLE_TAGS.containsKey(tag)) { | 8810 if (Device.isIE && Element._TABLE_TAGS.containsKey(tag)) { |
| 8577 return _createTableForIE(html, tag); | 8811 return _createTableForIE(html, tag); |
| 8578 } | 8812 } |
| 8579 parentTag = _CUSTOM_PARENT_TAG_MAP[tag]; | 8813 parentTag = _CUSTOM_PARENT_TAG_MAP[tag]; |
| 8580 if (parentTag == null) parentTag = 'div'; | 8814 if (parentTag == null) parentTag = 'div'; |
| 8581 } | 8815 } |
| 8582 | 8816 |
| 8583 final temp = new Element.tag(parentTag); | 8817 final temp = new Element.tag(parentTag); |
| 8584 temp.innerHtml = html; | 8818 temp.innerHtml = html; |
| 8585 | 8819 |
| 8586 Element element; | 8820 Element element; |
| (...skipping 2015 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 10602 @SupportedBrowser(SupportedBrowser.SAFARI) | 10836 @SupportedBrowser(SupportedBrowser.SAFARI) |
| 10603 @Experimental | 10837 @Experimental |
| 10604 Element get pointerLockElement => | 10838 Element get pointerLockElement => |
| 10605 $dom_webkitPointerLockElement; | 10839 $dom_webkitPointerLockElement; |
| 10606 | 10840 |
| 10607 @DomName('Document.webkitVisibilityState') | 10841 @DomName('Document.webkitVisibilityState') |
| 10608 @SupportedBrowser(SupportedBrowser.CHROME) | 10842 @SupportedBrowser(SupportedBrowser.CHROME) |
| 10609 @SupportedBrowser(SupportedBrowser.SAFARI) | 10843 @SupportedBrowser(SupportedBrowser.SAFARI) |
| 10610 @Experimental | 10844 @Experimental |
| 10611 String get visibilityState => $dom_webkitVisibilityState; | 10845 String get visibilityState => $dom_webkitVisibilityState; |
| 10846 |
| 10847 |
| 10848 // Note: used to polyfill <template> |
| 10849 Document _templateContentsOwner; |
| 10612 } | 10850 } |
| 10613 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file | 10851 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file |
| 10614 // for details. All rights reserved. Use of this source code is governed by a | 10852 // for details. All rights reserved. Use of this source code is governed by a |
| 10615 // BSD-style license that can be found in the LICENSE file. | 10853 // BSD-style license that can be found in the LICENSE file. |
| 10616 | 10854 |
| 10617 // WARNING: Do not edit - generated code. | 10855 // WARNING: Do not edit - generated code. |
| 10618 | 10856 |
| 10619 | 10857 |
| 10620 @DocsEditable | 10858 @DocsEditable |
| 10621 @DomName('HTMLHtmlElement') | 10859 @DomName('HTMLHtmlElement') |
| (...skipping 920 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 11542 factory InputElement({String type}) { | 11780 factory InputElement({String type}) { |
| 11543 var e = document.$dom_createElement("input"); | 11781 var e = document.$dom_createElement("input"); |
| 11544 if (type != null) { | 11782 if (type != null) { |
| 11545 try { | 11783 try { |
| 11546 // IE throws an exception for unknown types. | 11784 // IE throws an exception for unknown types. |
| 11547 e.type = type; | 11785 e.type = type; |
| 11548 } catch(_) {} | 11786 } catch(_) {} |
| 11549 } | 11787 } |
| 11550 return e; | 11788 return e; |
| 11551 } | 11789 } |
| 11790 |
| 11791 _ValueBinding _valueBinding; |
| 11792 |
| 11793 _CheckedBinding _checkedBinding; |
| 11794 |
| 11795 @Experimental |
| 11796 void bind(String name, model, String path) { |
| 11797 switch (name) { |
| 11798 case 'value': |
| 11799 unbind('value'); |
| 11800 attributes.remove('value'); |
| 11801 _valueBinding = new _ValueBinding(this, model, path); |
| 11802 break; |
| 11803 case 'checked': |
| 11804 unbind('checked'); |
| 11805 attributes.remove('checked'); |
| 11806 _checkedBinding = new _CheckedBinding(this, model, path); |
| 11807 break; |
| 11808 default: |
| 11809 // TODO(jmesserly): this should be "super" (http://dartbug.com/10166). |
| 11810 // Similar issue for unbind/unbindAll below. |
| 11811 Element._bindElement(this, name, model, path); |
| 11812 break; |
| 11813 } |
| 11814 } |
| 11815 |
| 11816 @Experimental |
| 11817 void unbind(String name) { |
| 11818 switch (name) { |
| 11819 case 'value': |
| 11820 if (_valueBinding != null) { |
| 11821 _valueBinding.unbind(); |
| 11822 _valueBinding = null; |
| 11823 } |
| 11824 break; |
| 11825 case 'checked': |
| 11826 if (_checkedBinding != null) { |
| 11827 _checkedBinding.unbind(); |
| 11828 _checkedBinding = null; |
| 11829 } |
| 11830 break; |
| 11831 default: |
| 11832 Element._unbindElement(this, name); |
| 11833 break; |
| 11834 } |
| 11835 } |
| 11836 |
| 11837 @Experimental |
| 11838 void unbindAll() { |
| 11839 unbind('value'); |
| 11840 unbind('checked'); |
| 11841 Element._unbindAllElement(this); |
| 11842 } |
| 11843 |
| 11552 InputElement.internal() : super.internal(); | 11844 InputElement.internal() : super.internal(); |
| 11553 | 11845 |
| 11554 @DomName('HTMLInputElement.webkitSpeechChangeEvent') | 11846 @DomName('HTMLInputElement.webkitSpeechChangeEvent') |
| 11555 @DocsEditable | 11847 @DocsEditable |
| 11556 @SupportedBrowser(SupportedBrowser.CHROME) | 11848 @SupportedBrowser(SupportedBrowser.CHROME) |
| 11557 @SupportedBrowser(SupportedBrowser.SAFARI) | 11849 @SupportedBrowser(SupportedBrowser.SAFARI) |
| 11558 @Experimental | 11850 @Experimental |
| 11559 static const EventStreamProvider<Event> speechChangeEvent = const EventStreamP
rovider<Event>('webkitSpeechChange'); | 11851 static const EventStreamProvider<Event> speechChangeEvent = const EventStreamP
rovider<Event>('webkitSpeechChange'); |
| 11560 | 11852 |
| 11561 @DomName('HTMLInputElement.accept') | 11853 @DomName('HTMLInputElement.accept') |
| (...skipping 3710 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 15272 _this.$dom_removeChild(node); | 15564 _this.$dom_removeChild(node); |
| 15273 return true; | 15565 return true; |
| 15274 } | 15566 } |
| 15275 | 15567 |
| 15276 void _filter(bool test(Node node), bool removeMatching) { | 15568 void _filter(bool test(Node node), bool removeMatching) { |
| 15277 // This implementation of removeWhere/retainWhere is more efficient | 15569 // This implementation of removeWhere/retainWhere is more efficient |
| 15278 // than the default in ListBase. Child nodes can be removed in constant | 15570 // than the default in ListBase. Child nodes can be removed in constant |
| 15279 // time. | 15571 // time. |
| 15280 Node child = _this.$dom_firstChild; | 15572 Node child = _this.$dom_firstChild; |
| 15281 while (child != null) { | 15573 while (child != null) { |
| 15282 Node nextChild = child.nextSibling; | 15574 Node nextChild = child.nextNode; |
| 15283 if (test(child) == removeMatching) { | 15575 if (test(child) == removeMatching) { |
| 15284 _this.$dom_removeChild(child); | 15576 _this.$dom_removeChild(child); |
| 15285 } | 15577 } |
| 15286 child = nextChild; | 15578 child = nextChild; |
| 15287 } | 15579 } |
| 15288 } | 15580 } |
| 15289 | 15581 |
| 15290 void removeWhere(bool test(Node node)) { | 15582 void removeWhere(bool test(Node node)) { |
| 15291 _filter(test, true); | 15583 _filter(test, true); |
| 15292 } | 15584 } |
| (...skipping 104 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 15397 // Should use $dom_firstChild, Bug 8886. | 15689 // Should use $dom_firstChild, Bug 8886. |
| 15398 this.insertBefore(newNodes[0], refChild); | 15690 this.insertBefore(newNodes[0], refChild); |
| 15399 } | 15691 } |
| 15400 } else { | 15692 } else { |
| 15401 for (var node in newNodes) { | 15693 for (var node in newNodes) { |
| 15402 this.insertBefore(node, refChild); | 15694 this.insertBefore(node, refChild); |
| 15403 } | 15695 } |
| 15404 } | 15696 } |
| 15405 } | 15697 } |
| 15406 | 15698 |
| 15407 // Note that this may either be the locally set model or a cached value | |
| 15408 // of the inherited model. This is cached to minimize model change | |
| 15409 // notifications. | |
| 15410 var _model; | |
| 15411 bool _hasLocalModel; | |
| 15412 Set<StreamController<Node>> _modelChangedStreams; | |
| 15413 | |
| 15414 /** | |
| 15415 * The data model which is inherited through the tree. | |
| 15416 * | |
| 15417 * Setting this will propagate the value to all descendant nodes. If the | |
| 15418 * model is not set on this node then it will be inherited from ancestor | |
| 15419 * nodes. | |
| 15420 * | |
| 15421 * Currently this does not support propagation through Shadow DOMs. | |
| 15422 * | |
| 15423 * [clearModel] must be used to remove the model property from this node | |
| 15424 * and have the model inherit from ancestor nodes. | |
| 15425 */ | |
| 15426 @Experimental | |
| 15427 get model { | |
| 15428 // If we have a change handler then we've cached the model locally. | |
| 15429 if (_modelChangedStreams != null && !_modelChangedStreams.isEmpty) { | |
| 15430 return _model; | |
| 15431 } | |
| 15432 // Otherwise start looking up the tree. | |
| 15433 for (var node = this; node != null; node = node.parentNode) { | |
| 15434 if (node._hasLocalModel == true) { | |
| 15435 return node._model; | |
| 15436 } | |
| 15437 } | |
| 15438 return null; | |
| 15439 } | |
| 15440 | |
| 15441 @Experimental | |
| 15442 void set model(value) { | |
| 15443 var changed = model != value; | |
| 15444 _model = value; | |
| 15445 _hasLocalModel = true; | |
| 15446 _ModelTreeObserver.initialize(); | |
| 15447 | |
| 15448 if (changed) { | |
| 15449 if (_modelChangedStreams != null && !_modelChangedStreams.isEmpty) { | |
| 15450 _modelChangedStreams.toList().forEach((stream) => stream.add(this)); | |
| 15451 } | |
| 15452 // Propagate new model to all descendants. | |
| 15453 _ModelTreeObserver.propagateModel(this, value, false); | |
| 15454 } | |
| 15455 } | |
| 15456 | |
| 15457 /** | |
| 15458 * Clears the locally set model and makes this model be inherited from parent | |
| 15459 * nodes. | |
| 15460 */ | |
| 15461 @Experimental | |
| 15462 void clearModel() { | |
| 15463 if (_hasLocalModel == true) { | |
| 15464 _hasLocalModel = false; | |
| 15465 | |
| 15466 // Propagate new model to all descendants. | |
| 15467 if (parentNode != null) { | |
| 15468 _ModelTreeObserver.propagateModel(this, parentNode.model, false); | |
| 15469 } else { | |
| 15470 _ModelTreeObserver.propagateModel(this, null, false); | |
| 15471 } | |
| 15472 } | |
| 15473 } | |
| 15474 | |
| 15475 /** | |
| 15476 * Get a stream of models, whenever the model changes. | |
| 15477 */ | |
| 15478 Stream<Node> get onModelChanged { | |
| 15479 if (_modelChangedStreams == null) { | |
| 15480 _modelChangedStreams = new Set<StreamController<Node>>(); | |
| 15481 } | |
| 15482 var controller; | |
| 15483 controller = new StreamController( | |
| 15484 onListen: () { _modelChangedStreams.add(controller); }, | |
| 15485 onCancel: () { _modelChangedStreams.remove(controller); }); | |
| 15486 return controller.stream; | |
| 15487 } | |
| 15488 | |
| 15489 /** | 15699 /** |
| 15490 * Print out a String representation of this Node. | 15700 * Print out a String representation of this Node. |
| 15491 */ | 15701 */ |
| 15492 String toString() => localName == null ? | 15702 String toString() => localName == null ? |
| 15493 (nodeValue == null ? super.toString() : nodeValue) : localName; | 15703 (nodeValue == null ? super.toString() : nodeValue) : localName; |
| 15494 | 15704 |
| 15705 /** |
| 15706 * Binds the attribute [name] to the [path] of the [model]. |
| 15707 * Path is a String of accessors such as `foo.bar.baz`. |
| 15708 */ |
| 15709 @Experimental |
| 15710 void bind(String name, model, String path) { |
| 15711 // TODO(jmesserly): should we throw instead? |
| 15712 window.console.error('Unhandled binding to Node: ' |
| 15713 '$this $name $model $path'); |
| 15714 } |
| 15715 |
| 15716 /** Unbinds the attribute [name]. */ |
| 15717 @Experimental |
| 15718 void unbind(String name) {} |
| 15719 |
| 15720 /** Unbinds all bound attributes. */ |
| 15721 @Experimental |
| 15722 void unbindAll() {} |
| 15723 |
| 15724 TemplateInstance _templateInstance; |
| 15725 |
| 15726 // TODO(arv): Consider storing all "NodeRareData" on a single object? |
| 15727 int __instanceTerminatorCount; |
| 15728 int get _instanceTerminatorCount { |
| 15729 if (__instanceTerminatorCount == null) return 0; |
| 15730 return __instanceTerminatorCount; |
| 15731 } |
| 15732 set _instanceTerminatorCount(int value) { |
| 15733 if (value == 0) value = null; |
| 15734 __instanceTerminatorCount = value; |
| 15735 } |
| 15736 |
| 15737 /** Gets the template instance that instantiated this node, if any. */ |
| 15738 @Experimental |
| 15739 TemplateInstance get templateInstance => |
| 15740 _templateInstance != null ? _templateInstance : |
| 15741 (parent != null ? parent.templateInstance : null); |
| 15742 |
| 15495 Node.internal() : super.internal(); | 15743 Node.internal() : super.internal(); |
| 15496 | 15744 |
| 15497 static const int ATTRIBUTE_NODE = 2; | 15745 static const int ATTRIBUTE_NODE = 2; |
| 15498 | 15746 |
| 15499 static const int CDATA_SECTION_NODE = 4; | 15747 static const int CDATA_SECTION_NODE = 4; |
| 15500 | 15748 |
| 15501 static const int COMMENT_NODE = 8; | 15749 static const int COMMENT_NODE = 8; |
| 15502 | 15750 |
| 15503 static const int DOCUMENT_FRAGMENT_NODE = 11; | 15751 static const int DOCUMENT_FRAGMENT_NODE = 11; |
| 15504 | 15752 |
| (...skipping 4308 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 19813 @DocsEditable | 20061 @DocsEditable |
| 19814 Element $dom_insertRow(int index) native "HTMLTableSectionElement_insertRow_Ca
llback"; | 20062 Element $dom_insertRow(int index) native "HTMLTableSectionElement_insertRow_Ca
llback"; |
| 19815 } | 20063 } |
| 19816 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file | 20064 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file |
| 19817 // for details. All rights reserved. Use of this source code is governed by a | 20065 // for details. All rights reserved. Use of this source code is governed by a |
| 19818 // BSD-style license that can be found in the LICENSE file. | 20066 // BSD-style license that can be found in the LICENSE file. |
| 19819 | 20067 |
| 19820 // WARNING: Do not edit - generated code. | 20068 // WARNING: Do not edit - generated code. |
| 19821 | 20069 |
| 19822 | 20070 |
| 19823 @DocsEditable | 20071 @Experimental |
| 19824 @DomName('HTMLTemplateElement') | 20072 @DomName('HTMLTemplateElement') |
| 19825 @SupportedBrowser(SupportedBrowser.CHROME) | 20073 @SupportedBrowser(SupportedBrowser.CHROME) |
| 19826 @Experimental | 20074 @Experimental |
| 19827 class TemplateElement extends _Element_Merged { | 20075 class TemplateElement extends _Element_Merged { |
| 19828 TemplateElement.internal() : super.internal(); | 20076 TemplateElement.internal() : super.internal(); |
| 19829 | 20077 |
| 19830 @DomName('HTMLTemplateElement.HTMLTemplateElement') | 20078 @DomName('HTMLTemplateElement.HTMLTemplateElement') |
| 19831 @DocsEditable | 20079 @DocsEditable |
| 19832 factory TemplateElement() => document.$dom_createElement("template"); | 20080 factory TemplateElement() => document.$dom_createElement("template"); |
| 19833 | 20081 |
| 19834 /// Checks if this type is supported on the current platform. | 20082 /// Checks if this type is supported on the current platform. |
| 19835 static bool get supported => true; | 20083 static bool get supported => true; |
| 19836 | 20084 |
| 19837 @DomName('HTMLTemplateElement.content') | 20085 @DomName('HTMLTemplateElement.content') |
| 19838 @DocsEditable | 20086 @DocsEditable |
| 19839 DocumentFragment get content native "HTMLTemplateElement_content_Getter"; | 20087 DocumentFragment get $dom_content native "HTMLTemplateElement_content_Getter"; |
| 19840 | 20088 |
| 20089 |
| 20090 // For real TemplateElement use the actual DOM .content field instead of |
| 20091 // our polyfilled expando. |
| 20092 @Experimental |
| 20093 DocumentFragment get content => $dom_content; |
| 20094 |
| 20095 static StreamController<DocumentFragment> _instanceCreated; |
| 20096 |
| 20097 /** |
| 20098 * *Warning*: This is an implementation helper for Model-Driven Views and |
| 20099 * should not be used in your code. |
| 20100 * |
| 20101 * This event is fired whenever a template is instantiated via |
| 20102 * [createInstance]. |
| 20103 */ |
| 20104 // TODO(rafaelw): This is a hack, and is neccesary for the polyfill |
| 20105 // because custom elements are not upgraded during clone() |
| 20106 @Experimental |
| 20107 static Stream<DocumentFragment> get instanceCreated { |
| 20108 if (_instanceCreated == null) { |
| 20109 _instanceCreated = new StreamController<DocumentFragment>(); |
| 20110 } |
| 20111 return _instanceCreated.stream; |
| 20112 } |
| 20113 |
| 20114 /** |
| 20115 * Ensures proper API and content model for template elements. |
| 20116 * |
| 20117 * [instanceRef] can be used to set the [Element.ref] property of [template], |
| 20118 * and use the ref's content will be used as source when createInstance() is |
| 20119 * invoked. |
| 20120 * |
| 20121 * Returns true if this template was just decorated, or false if it was |
| 20122 * already decorated. |
| 20123 */ |
| 20124 @Experimental |
| 20125 static bool decorate(Element template, [Element instanceRef]) { |
| 20126 // == true check because it starts as a null field. |
| 20127 if (template._templateIsDecorated == true) return false; |
| 20128 |
| 20129 template._templateIsDecorated = true; |
| 20130 |
| 20131 _injectStylesheet(); |
| 20132 |
| 20133 // Create content |
| 20134 if (template is! TemplateElement) { |
| 20135 var doc = _getTemplateContentsOwner(template.document); |
| 20136 template._templateContent = doc.createDocumentFragment(); |
| 20137 } |
| 20138 |
| 20139 if (instanceRef != null) { |
| 20140 template._templateInstanceRef = instanceRef; |
| 20141 return true; // content is empty. |
| 20142 } |
| 20143 |
| 20144 if (template is TemplateElement) { |
| 20145 _bootstrapTemplatesRecursivelyFrom(template.content); |
| 20146 } else { |
| 20147 _liftNonNativeTemplateChildrenIntoContent(template); |
| 20148 } |
| 20149 |
| 20150 return true; |
| 20151 } |
| 20152 |
| 20153 /** |
| 20154 * This used to decorate recursively all templates from a given node. |
| 20155 * |
| 20156 * By default [decorate] will be called on templates lazily when certain |
| 20157 * properties such as [model] are accessed, but it can be run eagerly to |
| 20158 * decorate an entire tree recursively. |
| 20159 */ |
| 20160 // TODO(rafaelw): Review whether this is the right public API. |
| 20161 @Experimental |
| 20162 static void bootstrap(Node content) { |
| 20163 _bootstrapTemplatesRecursivelyFrom(content); |
| 20164 } |
| 20165 |
| 20166 static bool _initStyles; |
| 20167 |
| 20168 static void _injectStylesheet() { |
| 20169 if (_initStyles == true) return; |
| 20170 _initStyles = true; |
| 20171 |
| 20172 var style = new StyleElement(); |
| 20173 style.text = r''' |
| 20174 template, |
| 20175 thead[template], |
| 20176 tbody[template], |
| 20177 tfoot[template], |
| 20178 th[template], |
| 20179 tr[template], |
| 20180 td[template], |
| 20181 caption[template], |
| 20182 colgroup[template], |
| 20183 col[template], |
| 20184 option[template] { |
| 20185 display: none; |
| 20186 }'''; |
| 20187 document.head.append(style); |
| 20188 } |
| 20189 |
| 20190 /** |
| 20191 * A mapping of names to Custom Syntax objects. See [CustomBindingSyntax] for |
| 20192 * more information. |
| 20193 */ |
| 20194 @Experimental |
| 20195 static Map<String, CustomBindingSyntax> syntax = {}; |
| 19841 } | 20196 } |
| 19842 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file | 20197 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file |
| 19843 // for details. All rights reserved. Use of this source code is governed by a | 20198 // for details. All rights reserved. Use of this source code is governed by a |
| 19844 // BSD-style license that can be found in the LICENSE file. | 20199 // BSD-style license that can be found in the LICENSE file. |
| 19845 | 20200 |
| 19846 // WARNING: Do not edit - generated code. | 20201 // WARNING: Do not edit - generated code. |
| 19847 | 20202 |
| 19848 | 20203 |
| 19849 @DomName('Text') | 20204 @DomName('Text') |
| 19850 class Text extends CharacterData { | 20205 class Text extends CharacterData { |
| (...skipping 12 matching lines...) Expand all Loading... |
| 19863 String get wholeText native "Text_wholeText_Getter"; | 20218 String get wholeText native "Text_wholeText_Getter"; |
| 19864 | 20219 |
| 19865 @DomName('Text.replaceWholeText') | 20220 @DomName('Text.replaceWholeText') |
| 19866 @DocsEditable | 20221 @DocsEditable |
| 19867 Text replaceWholeText(String content) native "Text_replaceWholeText_Callback"; | 20222 Text replaceWholeText(String content) native "Text_replaceWholeText_Callback"; |
| 19868 | 20223 |
| 19869 @DomName('Text.splitText') | 20224 @DomName('Text.splitText') |
| 19870 @DocsEditable | 20225 @DocsEditable |
| 19871 Text splitText(int offset) native "Text_splitText_Callback"; | 20226 Text splitText(int offset) native "Text_splitText_Callback"; |
| 19872 | 20227 |
| 20228 |
| 20229 StreamSubscription _textBinding; |
| 20230 |
| 20231 @Experimental |
| 20232 void bind(String name, model, String path) { |
| 20233 if (name != 'text') { |
| 20234 super.bind(name, model, path); |
| 20235 return; |
| 20236 } |
| 20237 |
| 20238 unbind('text'); |
| 20239 |
| 20240 _textBinding = new PathObserver(model, path).bindSync((value) { |
| 20241 text = value == null ? '' : '$value'; |
| 20242 }); |
| 20243 } |
| 20244 |
| 20245 @Experimental |
| 20246 void unbind(String name) { |
| 20247 if (name != 'text') { |
| 20248 super.unbind(name); |
| 20249 return; |
| 20250 } |
| 20251 |
| 20252 if (_textBinding == null) return; |
| 20253 |
| 20254 _textBinding.cancel(); |
| 20255 _textBinding = null; |
| 20256 } |
| 20257 |
| 20258 @Experimental |
| 20259 void unbindAll() { |
| 20260 unbind('text'); |
| 20261 super.unbindAll(); |
| 20262 } |
| 19873 } | 20263 } |
| 19874 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file | 20264 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file |
| 19875 // for details. All rights reserved. Use of this source code is governed by a | 20265 // for details. All rights reserved. Use of this source code is governed by a |
| 19876 // BSD-style license that can be found in the LICENSE file. | 20266 // BSD-style license that can be found in the LICENSE file. |
| 19877 | 20267 |
| 19878 // WARNING: Do not edit - generated code. | 20268 // WARNING: Do not edit - generated code. |
| 19879 | 20269 |
| 19880 | 20270 |
| 19881 @DocsEditable | 20271 @DocsEditable |
| 19882 @DomName('HTMLTextAreaElement') | 20272 @DomName('HTMLTextAreaElement') |
| (...skipping 5850 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 25733 * Key value used when an implementation is unable to identify another key | 26123 * Key value used when an implementation is unable to identify another key |
| 25734 * value, due to either hardware, platform, or software constraints | 26124 * value, due to either hardware, platform, or software constraints |
| 25735 */ | 26125 */ |
| 25736 static const String UNIDENTIFIED = "Unidentified"; | 26126 static const String UNIDENTIFIED = "Unidentified"; |
| 25737 } | 26127 } |
| 25738 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file | 26128 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file |
| 25739 // for details. All rights reserved. Use of this source code is governed by a | 26129 // for details. All rights reserved. Use of this source code is governed by a |
| 25740 // BSD-style license that can be found in the LICENSE file. | 26130 // BSD-style license that can be found in the LICENSE file. |
| 25741 | 26131 |
| 25742 | 26132 |
| 25743 class _ModelTreeObserver { | 26133 // This code is inspired by ChangeSummary: |
| 25744 static bool _initialized = false; | 26134 // https://github.com/rafaelw/ChangeSummary/blob/master/change_summary.js |
| 26135 // ...which underlies MDV. Since we don't need the functionality of |
| 26136 // ChangeSummary, we just implement what we need for data bindings. |
| 26137 // This allows our implementation to be much simpler. |
| 26138 |
| 26139 // TODO(jmesserly): should we make these types stronger, and require |
| 26140 // Observable objects? Currently, it is fine to say something like: |
| 26141 // var path = new PathObserver(123, ''); |
| 26142 // print(path.value); // "123" |
| 26143 // |
| 26144 // Furthermore this degenerate case is allowed: |
| 26145 // var path = new PathObserver(123, 'foo.bar.baz.qux'); |
| 26146 // print(path.value); // "null" |
| 26147 // |
| 26148 // Here we see that any invalid (i.e. not Observable) value will break the |
| 26149 // path chain without producing an error or exception. |
| 26150 // |
| 26151 // Now the real question: should we do this? For the former case, the behavior |
| 26152 // is correct but we could chose to handle it in the dart:html bindings layer. |
| 26153 // For the latter case, it might be better to throw an error so users can find |
| 26154 // the problem. |
| 26155 |
| 26156 |
| 26157 /** |
| 26158 * A data-bound path starting from a view-model or model object, for example |
| 26159 * `foo.bar.baz`. |
| 26160 * |
| 26161 * When the [values] stream is being listened to, this will observe changes to |
| 26162 * the object and any intermediate object along the path, and send [values] |
| 26163 * accordingly. When all listeners are unregistered it will stop observing |
| 26164 * the objects. |
| 26165 * |
| 26166 * This class is used to implement [Node.bind] and similar functionality. |
| 26167 */ |
| 26168 // TODO(jmesserly): find a better home for this type. |
| 26169 @Experimental |
| 26170 class PathObserver { |
| 26171 /** The object being observed. */ |
| 26172 final object; |
| 26173 |
| 26174 /** The path string. */ |
| 26175 final String path; |
| 26176 |
| 26177 /** True if the path is valid, otherwise false. */ |
| 26178 final bool _isValid; |
| 26179 |
| 26180 // TODO(jmesserly): same issue here as ObservableMixin: is there an easier |
| 26181 // way to get a broadcast stream? |
| 26182 StreamController _values; |
| 26183 Stream _valueStream; |
| 26184 |
| 26185 _PropertyObserver _observer, _lastObserver; |
| 26186 |
| 26187 Object _lastValue; |
| 26188 bool _scheduled = false; |
| 25745 | 26189 |
| 25746 /** | 26190 /** |
| 25747 * Start an observer watching the document for tree changes to automatically | 26191 * Observes [path] on [object] for changes. This returns an object that can be |
| 25748 * propagate model changes. | 26192 * used to get the changes and get/set the value at this path. |
| 25749 * | 26193 * See [PathObserver.values] and [PathObserver.value]. |
| 25750 * Currently this does not support propagation through Shadow DOMs. | |
| 25751 */ | 26194 */ |
| 25752 static void initialize() { | 26195 PathObserver(this.object, String path) |
| 25753 if (!_initialized) { | 26196 : path = path, _isValid = _isPathValid(path) { |
| 25754 _initialized = true; | 26197 |
| 25755 | 26198 // TODO(jmesserly): if the path is empty, or the object is! Observable, we |
| 25756 if (MutationObserver.supported) { | 26199 // can optimize the PathObserver to be more lightweight. |
| 25757 var observer = new MutationObserver(_processTreeChange); | 26200 |
| 25758 observer.observe(document, childList: true, subtree: true); | 26201 _values = new StreamController(onListen: _observe, onCancel: _unobserve); |
| 25759 } else { | 26202 |
| 25760 document.on['DOMNodeInserted'].listen(_handleNodeInserted); | 26203 if (_isValid) { |
| 25761 document.on['DOMNodeRemoved'].listen(_handleNodeRemoved); | 26204 var segments = []; |
| 26205 for (var segment in path.trim().split('.')) { |
| 26206 if (segment == '') continue; |
| 26207 var index = int.parse(segment, onError: (_) {}); |
| 26208 segments.add(index != null ? index : new Symbol(segment)); |
| 25762 } | 26209 } |
| 25763 } | 26210 |
| 25764 } | 26211 // Create the property observer linked list. |
| 25765 | 26212 // Note that the structure of a path can't change after it is initially |
| 25766 static void _processTreeChange(List<MutationRecord> mutations, | 26213 // constructed, even though the objects along the path can change. |
| 25767 MutationObserver observer) { | 26214 for (int i = segments.length - 1; i >= 0; i--) { |
| 25768 for (var record in mutations) { | 26215 _observer = new _PropertyObserver(this, segments[i], _observer); |
| 25769 for (var node in record.addedNodes) { | 26216 if (_lastObserver == null) _lastObserver = _observer; |
| 25770 // When nodes enter the document we need to make sure that all of the | |
| 25771 // models are properly propagated through the entire sub-tree. | |
| 25772 propagateModel(node, _calculatedModel(node), true); | |
| 25773 } | 26217 } |
| 25774 for (var node in record.removedNodes) { | 26218 } |
| 25775 propagateModel(node, _calculatedModel(node), false); | 26219 } |
| 26220 |
| 26221 // TODO(jmesserly): we could try adding the first value to the stream, but |
| 26222 // that delivers the first record async. |
| 26223 /** |
| 26224 * Listens to the stream, and invokes the [callback] immediately with the |
| 26225 * current [value]. This is useful for bindings, which want to be up-to-date |
| 26226 * immediately. |
| 26227 */ |
| 26228 StreamSubscription bindSync(void callback(value)) { |
| 26229 var result = values.listen(callback); |
| 26230 callback(value); |
| 26231 return result; |
| 26232 } |
| 26233 |
| 26234 // TODO(jmesserly): should this be a change record with the old value? |
| 26235 // TODO(jmesserly): should this be a broadcast stream? We only need |
| 26236 // single-subscription in the bindings system, so single sub saves overhead. |
| 26237 /** |
| 26238 * Gets the stream of values that were observed at this path. |
| 26239 * This returns a single-subscription stream. |
| 26240 */ |
| 26241 Stream get values => _values.stream; |
| 26242 |
| 26243 /** Force synchronous delivery of [values]. */ |
| 26244 void _deliverValues() { |
| 26245 _scheduled = false; |
| 26246 |
| 26247 var newValue = value; |
| 26248 if (!identical(_lastValue, newValue)) { |
| 26249 _values.add(newValue); |
| 26250 _lastValue = newValue; |
| 26251 } |
| 26252 } |
| 26253 |
| 26254 void _observe() { |
| 26255 if (_observer != null) { |
| 26256 _lastValue = value; |
| 26257 _observer.observe(); |
| 26258 } |
| 26259 } |
| 26260 |
| 26261 void _unobserve() { |
| 26262 if (_observer != null) _observer.unobserve(); |
| 26263 } |
| 26264 |
| 26265 void _notifyChange() { |
| 26266 if (_scheduled) return; |
| 26267 _scheduled = true; |
| 26268 |
| 26269 // TODO(jmesserly): should we have a guarenteed order with respect to other |
| 26270 // paths? If so, we could implement this fairly easily by sorting instances |
| 26271 // of this class by birth order before delivery. |
| 26272 queueChangeRecords(_deliverValues); |
| 26273 } |
| 26274 |
| 26275 /** Gets the last reported value at this path. */ |
| 26276 get value { |
| 26277 if (!_isValid) return null; |
| 26278 if (_observer == null) return object; |
| 26279 _observer.ensureValue(object); |
| 26280 return _lastObserver.value; |
| 26281 } |
| 26282 |
| 26283 /** Sets the value at this path. */ |
| 26284 void set value(Object value) { |
| 26285 // TODO(jmesserly): throw if property cannot be set? |
| 26286 // MDV seems tolerant of these error. |
| 26287 if (_observer == null || !_isValid) return; |
| 26288 _observer.ensureValue(object); |
| 26289 var last = _lastObserver; |
| 26290 if (_setObjectProperty(last._object, last._property, value)) { |
| 26291 // Technically, this would get updated asynchronously via a change record. |
| 26292 // However, it is nice if calling the getter will yield the same value |
| 26293 // that was just set. So we use this opportunity to update our cache. |
| 26294 last.value = value; |
| 26295 } |
| 26296 } |
| 26297 } |
| 26298 |
| 26299 // TODO(jmesserly): these should go away in favor of mirrors! |
| 26300 _getObjectProperty(object, property) { |
| 26301 if (object is List && property is int) { |
| 26302 if (property >= 0 && property < object.length) { |
| 26303 return object[property]; |
| 26304 } else { |
| 26305 return null; |
| 26306 } |
| 26307 } |
| 26308 |
| 26309 // TODO(jmesserly): what about length? |
| 26310 if (object is Map) return object[property]; |
| 26311 |
| 26312 if (object is Observable) return object.getValueWorkaround(property); |
| 26313 |
| 26314 return null; |
| 26315 } |
| 26316 |
| 26317 bool _setObjectProperty(object, property, value) { |
| 26318 if (object is List && property is int) { |
| 26319 object[property] = value; |
| 26320 } else if (object is Map) { |
| 26321 object[property] = value; |
| 26322 } else if (object is Observable) { |
| 26323 (object as Observable).setValueWorkaround(property, value); |
| 26324 } else { |
| 26325 return false; |
| 26326 } |
| 26327 return true; |
| 26328 } |
| 26329 |
| 26330 |
| 26331 class _PropertyObserver { |
| 26332 final PathObserver _path; |
| 26333 final _property; |
| 26334 final _PropertyObserver _next; |
| 26335 |
| 26336 // TODO(jmesserly): would be nice not to store both of these. |
| 26337 Object _object; |
| 26338 Object _value; |
| 26339 StreamSubscription _sub; |
| 26340 |
| 26341 _PropertyObserver(this._path, this._property, this._next); |
| 26342 |
| 26343 get value => _value; |
| 26344 |
| 26345 void set value(Object newValue) { |
| 26346 _value = newValue; |
| 26347 if (_next != null) { |
| 26348 if (_sub != null) _next.unobserve(); |
| 26349 _next.ensureValue(_value); |
| 26350 if (_sub != null) _next.observe(); |
| 26351 } |
| 26352 } |
| 26353 |
| 26354 void ensureValue(object) { |
| 26355 // If we're observing, values should be up to date already. |
| 26356 if (_sub != null) return; |
| 26357 |
| 26358 _object = object; |
| 26359 value = _getObjectProperty(object, _property); |
| 26360 } |
| 26361 |
| 26362 void observe() { |
| 26363 if (_object is Observable) { |
| 26364 assert(_sub == null); |
| 26365 _sub = (_object as Observable).changes.listen(_onChange); |
| 26366 } |
| 26367 if (_next != null) _next.observe(); |
| 26368 } |
| 26369 |
| 26370 void unobserve() { |
| 26371 if (_sub == null) return; |
| 26372 |
| 26373 _sub.cancel(); |
| 26374 _sub = null; |
| 26375 if (_next != null) _next.unobserve(); |
| 26376 } |
| 26377 |
| 26378 void _onChange(List<ChangeRecord> changes) { |
| 26379 for (var change in changes) { |
| 26380 // TODO(jmesserly): what to do about "new Symbol" here? |
| 26381 // Ideally this would only preserve names if the user has opted in to |
| 26382 // them being preserved. |
| 26383 // TODO(jmesserly): should we drop observable maps with String keys? |
| 26384 // If so then we only need one check here. |
| 26385 if (change.changes(_property)) { |
| 26386 value = _getObjectProperty(_object, _property); |
| 26387 _path._notifyChange(); |
| 26388 return; |
| 25776 } | 26389 } |
| 25777 } | 26390 } |
| 25778 } | 26391 } |
| 25779 | 26392 } |
| 25780 static void _handleNodeInserted(MutationEvent e) { | 26393 |
| 25781 var node = e.target; | 26394 // From: https://github.com/rafaelw/ChangeSummary/blob/master/change_summary.js |
| 25782 window.setImmediate(() { | 26395 |
| 25783 propagateModel(node, _calculatedModel(node), true); | 26396 const _pathIndentPart = r'[$a-z0-9_]+[$a-z0-9_\d]*'; |
| 25784 }); | 26397 final _pathRegExp = new RegExp('^' |
| 25785 } | 26398 '(?:#?' + _pathIndentPart + ')?' |
| 25786 | 26399 '(?:' |
| 25787 static void _handleNodeRemoved(MutationEvent e) { | 26400 '(?:\\.' + _pathIndentPart + ')' |
| 25788 var node = e.target; | 26401 ')*' |
| 25789 window.setImmediate(() { | 26402 r'$', caseSensitive: false); |
| 25790 propagateModel(node, _calculatedModel(node), false); | 26403 |
| 25791 }); | 26404 final _spacesRegExp = new RegExp(r'\s'); |
| 25792 } | 26405 |
| 25793 | 26406 bool _isPathValid(String s) { |
| 25794 /** | 26407 s = s.replaceAll(_spacesRegExp, ''); |
| 25795 * Figures out what the model should be for a node, avoiding any cached | 26408 |
| 25796 * model values. | 26409 if (s == '') return true; |
| 25797 */ | 26410 if (s[0] == '.') return false; |
| 25798 static _calculatedModel(node) { | 26411 return _pathRegExp.hasMatch(s); |
| 25799 if (node._hasLocalModel == true) { | |
| 25800 return node._model; | |
| 25801 } else if (node.parentNode != null) { | |
| 25802 return node.parentNode._model; | |
| 25803 } | |
| 25804 return null; | |
| 25805 } | |
| 25806 | |
| 25807 /** | |
| 25808 * Pushes model changes down through the tree. | |
| 25809 * | |
| 25810 * Set fullTree to true if the state of the tree is unknown and model changes | |
| 25811 * should be propagated through the entire tree. | |
| 25812 */ | |
| 25813 static void propagateModel(Node node, model, bool fullTree) { | |
| 25814 // Calling into user code with the != call could generate exceptions. | |
| 25815 // Catch and report them a global exceptions. | |
| 25816 try { | |
| 25817 if (node._hasLocalModel != true && node._model != model && | |
| 25818 node._modelChangedStreams != null && | |
| 25819 !node._modelChangedStreams.isEmpty) { | |
| 25820 node._model = model; | |
| 25821 node._modelChangedStreams.toList() | |
| 25822 .forEach((controller) => controller.add(node)); | |
| 25823 } | |
| 25824 } catch (e, s) { | |
| 25825 new Future.error(e, s); | |
| 25826 } | |
| 25827 for (var child = node.$dom_firstChild; child != null; | |
| 25828 child = child.nextNode) { | |
| 25829 if (child._hasLocalModel != true) { | |
| 25830 propagateModel(child, model, fullTree); | |
| 25831 } else if (fullTree) { | |
| 25832 propagateModel(child, child._model, true); | |
| 25833 } | |
| 25834 } | |
| 25835 } | |
| 25836 } | 26412 } |
| 25837 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file | 26413 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file |
| 25838 // for details. All rights reserved. Use of this source code is governed by a | 26414 // for details. All rights reserved. Use of this source code is governed by a |
| 25839 // BSD-style license that can be found in the LICENSE file. | 26415 // BSD-style license that can be found in the LICENSE file. |
| 25840 | 26416 |
| 25841 | 26417 |
| 25842 /** | 26418 /** |
| 25843 * A utility class for representing two-dimensional positions. | 26419 * A utility class for representing two-dimensional positions. |
| 25844 */ | 26420 */ |
| 25845 class Point { | 26421 class Point { |
| (...skipping 208 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 26054 | 26630 |
| 26055 Point get topLeft => new Point(this.left, this.top); | 26631 Point get topLeft => new Point(this.left, this.top); |
| 26056 Point get bottomRight => new Point(this.left + this.width, | 26632 Point get bottomRight => new Point(this.left + this.width, |
| 26057 this.top + this.height); | 26633 this.top + this.height); |
| 26058 } | 26634 } |
| 26059 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file | 26635 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file |
| 26060 // for details. All rights reserved. Use of this source code is governed by a | 26636 // for details. All rights reserved. Use of this source code is governed by a |
| 26061 // BSD-style license that can be found in the LICENSE file. | 26637 // BSD-style license that can be found in the LICENSE file. |
| 26062 | 26638 |
| 26063 | 26639 |
| 26640 // This code is a port of Model-Driven-Views: |
| 26641 // https://github.com/toolkitchen/mdv |
| 26642 // The code mostly comes from src/template_element.js |
| 26643 |
| 26644 typedef void _ChangeHandler(value); |
| 26645 |
| 26646 /** |
| 26647 * Model-Driven Views (MDV)'s native features enables a wide-range of use cases, |
| 26648 * but (by design) don't attempt to implement a wide array of specialized |
| 26649 * behaviors. |
| 26650 * |
| 26651 * Enabling these features in MDV is a matter of implementing and registering an |
| 26652 * MDV Custom Syntax. A Custom Syntax is an object which contains one or more |
| 26653 * delegation functions which implement specialized behavior. This object is |
| 26654 * registered with MDV via [TemplateElement.syntax]: |
| 26655 * |
| 26656 * |
| 26657 * HTML: |
| 26658 * <template bind syntax="MySyntax"> |
| 26659 * {{ What!Ever('crazy')->thing^^^I+Want(data) }} |
| 26660 * </template> |
| 26661 * |
| 26662 * Dart: |
| 26663 * class MySyntax extends CustomBindingSyntax { |
| 26664 * getBinding(model, path, name, node) { |
| 26665 * // The magic happens here! |
| 26666 * } |
| 26667 * } |
| 26668 * |
| 26669 * ... |
| 26670 * |
| 26671 * TemplateElement.syntax['MySyntax'] = new MySyntax(); |
| 26672 * |
| 26673 * See <https://github.com/toolkitchen/mdv/blob/master/docs/syntax.md> for more |
| 26674 * information about Custom Syntax. |
| 26675 */ |
| 26676 // TODO(jmesserly): if this is just one method, a function type would make it |
| 26677 // more Dart-friendly. |
| 26678 @Experimental |
| 26679 abstract class CustomBindingSyntax { |
| 26680 // TODO(jmesserly): I had to remove type annotations from "name" and "node" |
| 26681 // Normally they are String and Node respectively. But sometimes it will pass |
| 26682 // (int name, CompoundBinding node). That seems very confusing; we may want |
| 26683 // to change this API. |
| 26684 getBinding(model, String path, name, node); |
| 26685 } |
| 26686 |
| 26687 /** The callback used in the [CompoundBinding.combinator] field. */ |
| 26688 @Experimental |
| 26689 typedef Object CompoundBindingCombinator(Map objects); |
| 26690 |
| 26691 /** Information about the instantiated template. */ |
| 26692 @Experimental |
| 26693 class TemplateInstance { |
| 26694 // TODO(rafaelw): firstNode & lastNode should be read-synchronous |
| 26695 // in cases where script has modified the template instance boundary. |
| 26696 |
| 26697 /** The first node of this template instantiation. */ |
| 26698 final Node firstNode; |
| 26699 |
| 26700 /** |
| 26701 * The last node of this template instantiation. |
| 26702 * This could be identical to [firstNode] if the template only expanded to a |
| 26703 * single node. |
| 26704 */ |
| 26705 final Node lastNode; |
| 26706 |
| 26707 /** The model used to instantiate the template. */ |
| 26708 final model; |
| 26709 |
| 26710 TemplateInstance(this.firstNode, this.lastNode, this.model); |
| 26711 } |
| 26712 |
| 26713 /** |
| 26714 * Model-Driven Views contains a helper object which is useful for the |
| 26715 * implementation of a Custom Syntax. |
| 26716 * |
| 26717 * var binding = new CompoundBinding((values) { |
| 26718 * var combinedValue; |
| 26719 * // compute combinedValue based on the current values which are provided |
| 26720 * return combinedValue; |
| 26721 * }); |
| 26722 * binding.bind('name1', obj1, path1); |
| 26723 * binding.bind('name2', obj2, path2); |
| 26724 * //... |
| 26725 * binding.bind('nameN', objN, pathN); |
| 26726 * |
| 26727 * CompoundBinding is an object which knows how to listen to multiple path |
| 26728 * values (registered via [bind]) and invoke its [combinator] when one or more |
| 26729 * of the values have changed and set its [value] property to the return value |
| 26730 * of the function. When any value has changed, all current values are provided |
| 26731 * to the [combinator] in the single `values` argument. |
| 26732 * |
| 26733 * See [CustomBindingSyntax] for more information. |
| 26734 */ |
| 26735 // TODO(jmesserly): what is the public API surface here? I just guessed; |
| 26736 // most of it seemed non-public. |
| 26737 @Experimental |
| 26738 class CompoundBinding extends ObservableBase { |
| 26739 CompoundBindingCombinator _combinator; |
| 26740 |
| 26741 // TODO(jmesserly): ideally these would be String keys, but sometimes we |
| 26742 // use integers. |
| 26743 Map<dynamic, StreamSubscription> _bindings = new Map(); |
| 26744 Map _values = new Map(); |
| 26745 bool _scheduled = false; |
| 26746 bool _disposed = false; |
| 26747 Object _value; |
| 26748 |
| 26749 CompoundBinding([CompoundBindingCombinator combinator]) { |
| 26750 // TODO(jmesserly): this is a tweak to the original code, it seemed to me |
| 26751 // that passing the combinator to the constructor should be equivalent to |
| 26752 // setting it via the property. |
| 26753 // I also added a null check to the combinator setter. |
| 26754 this.combinator = combinator; |
| 26755 } |
| 26756 |
| 26757 CompoundBindingCombinator get combinator => _combinator; |
| 26758 |
| 26759 set combinator(CompoundBindingCombinator combinator) { |
| 26760 _combinator = combinator; |
| 26761 if (combinator != null) _scheduleResolve(); |
| 26762 } |
| 26763 |
| 26764 static const _VALUE = const Symbol('value'); |
| 26765 |
| 26766 get value => _value; |
| 26767 |
| 26768 void set value(newValue) { |
| 26769 _value = notifyPropertyChange(_VALUE, _value, newValue); |
| 26770 } |
| 26771 |
| 26772 // TODO(jmesserly): remove these workarounds when dart2js supports mirrors! |
| 26773 getValueWorkaround(key) { |
| 26774 if (key == _VALUE) return value; |
| 26775 return null; |
| 26776 } |
| 26777 setValueWorkaround(key, val) { |
| 26778 if (key == _VALUE) value = val; |
| 26779 } |
| 26780 |
| 26781 void bind(name, model, String path) { |
| 26782 unbind(name); |
| 26783 |
| 26784 _bindings[name] = new PathObserver(model, path).bindSync((value) { |
| 26785 _values[name] = value; |
| 26786 _scheduleResolve(); |
| 26787 }); |
| 26788 } |
| 26789 |
| 26790 void unbind(name, {bool suppressResolve: false}) { |
| 26791 var binding = _bindings.remove(name); |
| 26792 if (binding == null) return; |
| 26793 |
| 26794 binding.cancel(); |
| 26795 _values.remove(name); |
| 26796 if (!suppressResolve) _scheduleResolve(); |
| 26797 } |
| 26798 |
| 26799 // TODO(rafaelw): Is this the right processing model? |
| 26800 // TODO(rafaelw): Consider having a seperate ChangeSummary for |
| 26801 // CompoundBindings so to excess dirtyChecks. |
| 26802 void _scheduleResolve() { |
| 26803 if (_scheduled) return; |
| 26804 _scheduled = true; |
| 26805 queueChangeRecords(resolve); |
| 26806 } |
| 26807 |
| 26808 void resolve() { |
| 26809 if (_disposed) return; |
| 26810 _scheduled = false; |
| 26811 |
| 26812 if (_combinator == null) { |
| 26813 throw new StateError( |
| 26814 'CompoundBinding attempted to resolve without a combinator'); |
| 26815 } |
| 26816 |
| 26817 value = _combinator(_values); |
| 26818 } |
| 26819 |
| 26820 void dispose() { |
| 26821 for (var binding in _bindings.values) { |
| 26822 binding.cancel(); |
| 26823 } |
| 26824 _bindings.clear(); |
| 26825 _values.clear(); |
| 26826 |
| 26827 _disposed = true; |
| 26828 value = null; |
| 26829 } |
| 26830 } |
| 26831 |
| 26832 Stream<Event> _getStreamForInputType(InputElement element) { |
| 26833 switch (element.type) { |
| 26834 case 'checkbox': |
| 26835 return element.onClick; |
| 26836 case 'radio': |
| 26837 case 'select-multiple': |
| 26838 case 'select-one': |
| 26839 return element.onChange; |
| 26840 default: |
| 26841 return element.onInput; |
| 26842 } |
| 26843 } |
| 26844 |
| 26845 abstract class _InputBinding { |
| 26846 final InputElement element; |
| 26847 PathObserver binding; |
| 26848 StreamSubscription _pathSub; |
| 26849 StreamSubscription _eventSub; |
| 26850 |
| 26851 _InputBinding(this.element, model, String path) { |
| 26852 binding = new PathObserver(model, path); |
| 26853 _pathSub = binding.bindSync(valueChanged); |
| 26854 _eventSub = _getStreamForInputType(element).listen(updateBinding); |
| 26855 } |
| 26856 |
| 26857 void valueChanged(newValue); |
| 26858 |
| 26859 void updateBinding(e); |
| 26860 |
| 26861 void unbind() { |
| 26862 binding = null; |
| 26863 _pathSub.cancel(); |
| 26864 _eventSub.cancel(); |
| 26865 } |
| 26866 } |
| 26867 |
| 26868 class _ValueBinding extends _InputBinding { |
| 26869 _ValueBinding(element, model, path) : super(element, model, path); |
| 26870 |
| 26871 void valueChanged(value) { |
| 26872 element.value = value == null ? '' : '$value'; |
| 26873 } |
| 26874 |
| 26875 void updateBinding(e) { |
| 26876 binding.value = element.value; |
| 26877 } |
| 26878 } |
| 26879 |
| 26880 // TODO(jmesserly): not sure what kind of boolean conversion rules to |
| 26881 // apply for template data-binding. HTML attributes are true if they're present. |
| 26882 // However Dart only treats "true" as true. Since this is HTML we'll use |
| 26883 // something closer to the HTML rules: null (missing) and false are false, |
| 26884 // everything else is true. See: https://github.com/toolkitchen/mdv/issues/59 |
| 26885 bool _templateBooleanConversion(value) => null != value && false != value; |
| 26886 |
| 26887 class _CheckedBinding extends _InputBinding { |
| 26888 _CheckedBinding(element, model, path) : super(element, model, path); |
| 26889 |
| 26890 void valueChanged(value) { |
| 26891 element.checked = _templateBooleanConversion(value); |
| 26892 } |
| 26893 |
| 26894 void updateBinding(e) { |
| 26895 binding.value = element.checked; |
| 26896 |
| 26897 // Only the radio button that is getting checked gets an event. We |
| 26898 // therefore find all the associated radio buttons and update their |
| 26899 // CheckedBinding manually. |
| 26900 if (element is InputElement && element.type == 'radio') { |
| 26901 for (var r in _getAssociatedRadioButtons(element)) { |
| 26902 var checkedBinding = r._checkedBinding; |
| 26903 if (checkedBinding != null) { |
| 26904 // Set the value directly to avoid an infinite call stack. |
| 26905 checkedBinding.binding.value = false; |
| 26906 } |
| 26907 } |
| 26908 } |
| 26909 } |
| 26910 } |
| 26911 |
| 26912 // TODO(jmesserly): polyfill document.contains API instead of doing it here |
| 26913 bool _isNodeInDocument(Node node) { |
| 26914 // On non-IE this works: |
| 26915 // return node.document.contains(node); |
| 26916 var document = node.document; |
| 26917 if (node == document || node.parentNode == document) return true; |
| 26918 return document.documentElement.contains(node); |
| 26919 } |
| 26920 |
| 26921 // |element| is assumed to be an HTMLInputElement with |type| == 'radio'. |
| 26922 // Returns an array containing all radio buttons other than |element| that |
| 26923 // have the same |name|, either in the form that |element| belongs to or, |
| 26924 // if no form, in the document tree to which |element| belongs. |
| 26925 // |
| 26926 // This implementation is based upon the HTML spec definition of a |
| 26927 // "radio button group": |
| 26928 // http://www.whatwg.org/specs/web-apps/current-work/multipage/number-state.ht
ml#radio-button-group |
| 26929 // |
| 26930 Iterable _getAssociatedRadioButtons(element) { |
| 26931 if (!_isNodeInDocument(element)) return []; |
| 26932 if (element.form != null) { |
| 26933 return element.form.nodes.where((el) { |
| 26934 return el != element && |
| 26935 el is InputElement && |
| 26936 el.type == 'radio' && |
| 26937 el.name == element.name; |
| 26938 }); |
| 26939 } else { |
| 26940 var radios = element.document.queryAll( |
| 26941 'input[type="radio"][name="${element.name}"]'); |
| 26942 return radios.where((el) => el != element && el.form == null); |
| 26943 } |
| 26944 } |
| 26945 |
| 26946 Node _createDeepCloneAndDecorateTemplates(Node node, String syntax) { |
| 26947 var clone = node.clone(false); // Shallow clone. |
| 26948 if (clone is Element && clone.isTemplate) { |
| 26949 TemplateElement.decorate(clone, node); |
| 26950 if (syntax != null) { |
| 26951 clone.attributes.putIfAbsent('syntax', () => syntax); |
| 26952 } |
| 26953 } |
| 26954 |
| 26955 for (var c = node.$dom_firstChild; c != null; c = c.nextNode) { |
| 26956 clone.append(_createDeepCloneAndDecorateTemplates(c, syntax)); |
| 26957 } |
| 26958 return clone; |
| 26959 } |
| 26960 |
| 26961 // http://dvcs.w3.org/hg/webcomponents/raw-file/tip/spec/templates/index.html#df
n-template-contents-owner |
| 26962 Document _getTemplateContentsOwner(Document doc) { |
| 26963 if (doc.window == null) { |
| 26964 return doc; |
| 26965 } |
| 26966 var d = doc._templateContentsOwner; |
| 26967 if (d == null) { |
| 26968 // TODO(arv): This should either be a Document or HTMLDocument depending |
| 26969 // on doc. |
| 26970 d = doc.implementation.createHtmlDocument(''); |
| 26971 while (d.$dom_lastChild != null) { |
| 26972 d.$dom_lastChild.remove(); |
| 26973 } |
| 26974 doc._templateContentsOwner = d; |
| 26975 } |
| 26976 return d; |
| 26977 } |
| 26978 |
| 26979 Element _cloneAndSeperateAttributeTemplate(Element templateElement) { |
| 26980 var clone = templateElement.clone(false); |
| 26981 var attributes = templateElement.attributes; |
| 26982 for (var name in attributes.keys.toList()) { |
| 26983 switch (name) { |
| 26984 case 'template': |
| 26985 case 'repeat': |
| 26986 case 'bind': |
| 26987 case 'ref': |
| 26988 clone.attributes.remove(name); |
| 26989 break; |
| 26990 default: |
| 26991 attributes.remove(name); |
| 26992 break; |
| 26993 } |
| 26994 } |
| 26995 |
| 26996 return clone; |
| 26997 } |
| 26998 |
| 26999 void _liftNonNativeTemplateChildrenIntoContent(Element templateElement) { |
| 27000 var content = templateElement.content; |
| 27001 |
| 27002 if (!templateElement._isAttributeTemplate) { |
| 27003 var child; |
| 27004 while ((child = templateElement.$dom_firstChild) != null) { |
| 27005 content.append(child); |
| 27006 } |
| 27007 return; |
| 27008 } |
| 27009 |
| 27010 // For attribute templates we copy the whole thing into the content and |
| 27011 // we move the non template attributes into the content. |
| 27012 // |
| 27013 // <tr foo template> |
| 27014 // |
| 27015 // becomes |
| 27016 // |
| 27017 // <tr template> |
| 27018 // + #document-fragment |
| 27019 // + <tr foo> |
| 27020 // |
| 27021 var newRoot = _cloneAndSeperateAttributeTemplate(templateElement); |
| 27022 var child; |
| 27023 while ((child = templateElement.$dom_firstChild) != null) { |
| 27024 newRoot.append(child); |
| 27025 } |
| 27026 content.append(newRoot); |
| 27027 } |
| 27028 |
| 27029 void _bootstrapTemplatesRecursivelyFrom(Node node) { |
| 27030 void bootstrap(template) { |
| 27031 if (!TemplateElement.decorate(template)) { |
| 27032 _bootstrapTemplatesRecursivelyFrom(template.content); |
| 27033 } |
| 27034 } |
| 27035 |
| 27036 // Need to do this first as the contents may get lifted if |node| is |
| 27037 // template. |
| 27038 // TODO(jmesserly): node is DocumentFragment or Element |
| 27039 var templateDescendents = (node as dynamic).queryAll(_allTemplatesSelectors); |
| 27040 if (node is Element && node.isTemplate) bootstrap(node); |
| 27041 |
| 27042 templateDescendents.forEach(bootstrap); |
| 27043 } |
| 27044 |
| 27045 final String _allTemplatesSelectors = 'template, option[template], ' + |
| 27046 Element._TABLE_TAGS.keys.map((k) => "$k[template]").join(", "); |
| 27047 |
| 27048 void _addBindings(Node node, model, [CustomBindingSyntax syntax]) { |
| 27049 if (node is Element) { |
| 27050 _addAttributeBindings(node, model, syntax); |
| 27051 } else if (node is Text) { |
| 27052 _parseAndBind(node, node.text, 'text', model, syntax); |
| 27053 } |
| 27054 |
| 27055 for (var c = node.$dom_firstChild; c != null; c = c.nextNode) { |
| 27056 _addBindings(c, model, syntax); |
| 27057 } |
| 27058 } |
| 27059 |
| 27060 |
| 27061 void _addAttributeBindings(Element element, model, syntax) { |
| 27062 element.attributes.forEach((name, value) { |
| 27063 if (value == '' && (name == 'bind' || name == 'repeat')) { |
| 27064 value = '{{}}'; |
| 27065 } |
| 27066 _parseAndBind(element, value, name, model, syntax); |
| 27067 }); |
| 27068 } |
| 27069 |
| 27070 void _parseAndBind(Node node, String text, String name, model, |
| 27071 CustomBindingSyntax syntax) { |
| 27072 |
| 27073 var tokens = _parseMustacheTokens(text); |
| 27074 if (tokens.length == 0 || (tokens.length == 1 && tokens[0].isText)) { |
| 27075 return; |
| 27076 } |
| 27077 |
| 27078 if (tokens.length == 1 && tokens[0].isBinding) { |
| 27079 _bindOrDelegate(node, name, model, tokens[0].value, syntax); |
| 27080 return; |
| 27081 } |
| 27082 |
| 27083 var replacementBinding = new CompoundBinding(); |
| 27084 for (var i = 0; i < tokens.length; i++) { |
| 27085 var token = tokens[i]; |
| 27086 if (token.isBinding) { |
| 27087 _bindOrDelegate(replacementBinding, i, model, token.value, syntax); |
| 27088 } |
| 27089 } |
| 27090 |
| 27091 replacementBinding.combinator = (values) { |
| 27092 var newValue = new StringBuffer(); |
| 27093 |
| 27094 for (var i = 0; i < tokens.length; i++) { |
| 27095 var token = tokens[i]; |
| 27096 if (token.isText) { |
| 27097 newValue.write(token.value); |
| 27098 } else { |
| 27099 var value = values[i]; |
| 27100 if (value != null) { |
| 27101 newValue.write(value); |
| 27102 } |
| 27103 } |
| 27104 } |
| 27105 |
| 27106 return newValue.toString(); |
| 27107 }; |
| 27108 |
| 27109 node.bind(name, replacementBinding, 'value'); |
| 27110 } |
| 27111 |
| 27112 void _bindOrDelegate(node, name, model, String path, |
| 27113 CustomBindingSyntax syntax) { |
| 27114 |
| 27115 if (syntax != null) { |
| 27116 var delegateBinding = syntax.getBinding(model, path, name, node); |
| 27117 if (delegateBinding != null) { |
| 27118 model = delegateBinding; |
| 27119 path = 'value'; |
| 27120 } |
| 27121 } |
| 27122 |
| 27123 node.bind(name, model, path); |
| 27124 } |
| 27125 |
| 27126 class _BindingToken { |
| 27127 final String value; |
| 27128 final bool isBinding; |
| 27129 |
| 27130 _BindingToken(this.value, {this.isBinding: false}); |
| 27131 |
| 27132 bool get isText => !isBinding; |
| 27133 } |
| 27134 |
| 27135 List<_BindingToken> _parseMustacheTokens(String s) { |
| 27136 var result = []; |
| 27137 var length = s.length; |
| 27138 var index = 0, lastIndex = 0; |
| 27139 while (lastIndex < length) { |
| 27140 index = s.indexOf('{{', lastIndex); |
| 27141 if (index < 0) { |
| 27142 result.add(new _BindingToken(s.substring(lastIndex))); |
| 27143 break; |
| 27144 } else { |
| 27145 // There is a non-empty text run before the next path token. |
| 27146 if (index > 0 && lastIndex < index) { |
| 27147 result.add(new _BindingToken(s.substring(lastIndex, index))); |
| 27148 } |
| 27149 lastIndex = index + 2; |
| 27150 index = s.indexOf('}}', lastIndex); |
| 27151 if (index < 0) { |
| 27152 var text = s.substring(lastIndex - 2); |
| 27153 if (result.length > 0 && result.last.isText) { |
| 27154 result.last.value += text; |
| 27155 } else { |
| 27156 result.add(new _BindingToken(text)); |
| 27157 } |
| 27158 break; |
| 27159 } |
| 27160 |
| 27161 var value = s.substring(lastIndex, index).trim(); |
| 27162 result.add(new _BindingToken(value, isBinding: true)); |
| 27163 lastIndex = index + 2; |
| 27164 } |
| 27165 } |
| 27166 return result; |
| 27167 } |
| 27168 |
| 27169 void _addTemplateInstanceRecord(fragment, model) { |
| 27170 if (fragment.$dom_firstChild == null) { |
| 27171 return; |
| 27172 } |
| 27173 |
| 27174 var instanceRecord = new TemplateInstance( |
| 27175 fragment.$dom_firstChild, fragment.$dom_lastChild, model); |
| 27176 |
| 27177 var node = instanceRecord.firstNode; |
| 27178 while (node != null) { |
| 27179 node._templateInstance = instanceRecord; |
| 27180 node = node.nextNode; |
| 27181 } |
| 27182 } |
| 27183 |
| 27184 void _removeAllBindingsRecursively(Node node) { |
| 27185 node.unbindAll(); |
| 27186 for (var c = node.$dom_firstChild; c != null; c = c.nextNode) { |
| 27187 _removeAllBindingsRecursively(c); |
| 27188 } |
| 27189 } |
| 27190 |
| 27191 void _removeTemplateChild(Node parent, Node child) { |
| 27192 child._templateInstance = null; |
| 27193 if (child is Element && child.isTemplate) { |
| 27194 // Make sure we stop observing when we remove an element. |
| 27195 var templateIterator = child._templateIterator; |
| 27196 if (templateIterator != null) { |
| 27197 templateIterator.abandon(); |
| 27198 child._templateIterator = null; |
| 27199 } |
| 27200 } |
| 27201 child.remove(); |
| 27202 _removeAllBindingsRecursively(child); |
| 27203 } |
| 27204 |
| 27205 class _InstanceCursor { |
| 27206 final Element _template; |
| 27207 Node _terminator; |
| 27208 Node _previousTerminator; |
| 27209 int _previousIndex = -1; |
| 27210 int _index = 0; |
| 27211 |
| 27212 _InstanceCursor(this._template, [index]) { |
| 27213 _terminator = _template; |
| 27214 if (index != null) { |
| 27215 while (index-- > 0) { |
| 27216 next(); |
| 27217 } |
| 27218 } |
| 27219 } |
| 27220 |
| 27221 void next() { |
| 27222 _previousTerminator = _terminator; |
| 27223 _previousIndex = _index; |
| 27224 _index++; |
| 27225 |
| 27226 while (_index > _terminator._instanceTerminatorCount) { |
| 27227 _index -= _terminator._instanceTerminatorCount; |
| 27228 _terminator = _terminator.nextNode; |
| 27229 if (_terminator is Element && _terminator.tagName == 'TEMPLATE') { |
| 27230 _index += _instanceCount(_terminator); |
| 27231 } |
| 27232 } |
| 27233 } |
| 27234 |
| 27235 void abandon() { |
| 27236 assert(_instanceCount(_template) > 0); |
| 27237 assert(_terminator._instanceTerminatorCount > 0); |
| 27238 assert(_index > 0); |
| 27239 |
| 27240 _terminator._instanceTerminatorCount--; |
| 27241 _index--; |
| 27242 } |
| 27243 |
| 27244 void insert(fragment) { |
| 27245 assert(_template.parentNode != null); |
| 27246 |
| 27247 _previousTerminator = _terminator; |
| 27248 _previousIndex = _index; |
| 27249 _index++; |
| 27250 |
| 27251 _terminator = fragment.$dom_lastChild; |
| 27252 if (_terminator == null) _terminator = _previousTerminator; |
| 27253 _template.parentNode.insertBefore(fragment, _previousTerminator.nextNode); |
| 27254 |
| 27255 _terminator._instanceTerminatorCount++; |
| 27256 if (_terminator != _previousTerminator) { |
| 27257 while (_previousTerminator._instanceTerminatorCount > |
| 27258 _previousIndex) { |
| 27259 _previousTerminator._instanceTerminatorCount--; |
| 27260 _terminator._instanceTerminatorCount++; |
| 27261 } |
| 27262 } |
| 27263 } |
| 27264 |
| 27265 void remove() { |
| 27266 assert(_previousIndex != -1); |
| 27267 assert(_previousTerminator != null && |
| 27268 (_previousIndex > 0 || _previousTerminator == _template)); |
| 27269 assert(_terminator != null && _index > 0); |
| 27270 assert(_template.parentNode != null); |
| 27271 assert(_instanceCount(_template) > 0); |
| 27272 |
| 27273 if (_previousTerminator == _terminator) { |
| 27274 assert(_index == _previousIndex + 1); |
| 27275 _terminator._instanceTerminatorCount--; |
| 27276 _terminator = _template; |
| 27277 _previousTerminator = null; |
| 27278 _previousIndex = -1; |
| 27279 return; |
| 27280 } |
| 27281 |
| 27282 _terminator._instanceTerminatorCount--; |
| 27283 |
| 27284 var parent = _template.parentNode; |
| 27285 while (_previousTerminator.nextNode != _terminator) { |
| 27286 _removeTemplateChild(parent, _previousTerminator.nextNode); |
| 27287 } |
| 27288 _removeTemplateChild(parent, _terminator); |
| 27289 |
| 27290 _terminator = _previousTerminator; |
| 27291 _index = _previousIndex; |
| 27292 _previousTerminator = null; |
| 27293 _previousIndex = -1; // 0? |
| 27294 } |
| 27295 } |
| 27296 |
| 27297 |
| 27298 class _TemplateIterator { |
| 27299 final Element _templateElement; |
| 27300 int instanceCount = 0; |
| 27301 List iteratedValue; |
| 27302 bool observing = false; |
| 27303 final CompoundBinding inputs; |
| 27304 |
| 27305 StreamSubscription _sub; |
| 27306 StreamSubscription _valueBinding; |
| 27307 |
| 27308 _TemplateIterator(this._templateElement) |
| 27309 : inputs = new CompoundBinding(resolveInputs) { |
| 27310 |
| 27311 _valueBinding = new PathObserver(inputs, 'value').bindSync(valueChanged); |
| 27312 } |
| 27313 |
| 27314 static Object resolveInputs(Map values) { |
| 27315 if (values.containsKey('if') && !_templateBooleanConversion(values['if'])) { |
| 27316 return null; |
| 27317 } |
| 27318 |
| 27319 if (values.containsKey('repeat')) { |
| 27320 return values['repeat']; |
| 27321 } |
| 27322 |
| 27323 if (values.containsKey('bind')) { |
| 27324 return [values['bind']]; |
| 27325 } |
| 27326 |
| 27327 return null; |
| 27328 } |
| 27329 |
| 27330 void valueChanged(value) { |
| 27331 clear(); |
| 27332 if (value is! List) return; |
| 27333 |
| 27334 iteratedValue = value; |
| 27335 |
| 27336 if (value is Observable) { |
| 27337 _sub = value.changes.listen(_handleChanges); |
| 27338 } |
| 27339 |
| 27340 int len = iteratedValue.length; |
| 27341 if (len > 0) { |
| 27342 _handleChanges([new ListChangeRecord(0, addedCount: len)]); |
| 27343 } |
| 27344 } |
| 27345 |
| 27346 // TODO(jmesserly): port MDV v3. |
| 27347 getInstanceModel(model, syntax) => model; |
| 27348 getInstanceFragment(syntax) => _templateElement.createInstance(); |
| 27349 |
| 27350 void _handleChanges(List<ListChangeRecord> splices) { |
| 27351 var syntax = TemplateElement.syntax[_templateElement.attributes['syntax']]; |
| 27352 |
| 27353 for (var splice in splices) { |
| 27354 if (splice is! ListChangeRecord) continue; |
| 27355 |
| 27356 for (int i = 0; i < splice.removedCount; i++) { |
| 27357 var cursor = new _InstanceCursor(_templateElement, splice.index + 1); |
| 27358 cursor.remove(); |
| 27359 instanceCount--; |
| 27360 } |
| 27361 |
| 27362 for (var addIndex = splice.index; |
| 27363 addIndex < splice.index + splice.addedCount; |
| 27364 addIndex++) { |
| 27365 |
| 27366 var model = getInstanceModel(iteratedValue[addIndex], syntax); |
| 27367 var fragment = getInstanceFragment(syntax); |
| 27368 |
| 27369 _addBindings(fragment, model, syntax); |
| 27370 _addTemplateInstanceRecord(fragment, model); |
| 27371 |
| 27372 var cursor = new _InstanceCursor(_templateElement, addIndex); |
| 27373 cursor.insert(fragment); |
| 27374 instanceCount++; |
| 27375 } |
| 27376 } |
| 27377 } |
| 27378 |
| 27379 void unobserve() { |
| 27380 if (_sub == null) return; |
| 27381 _sub.cancel(); |
| 27382 _sub = null; |
| 27383 } |
| 27384 |
| 27385 void clear() { |
| 27386 unobserve(); |
| 27387 |
| 27388 iteratedValue = null; |
| 27389 if (instanceCount == 0) return; |
| 27390 |
| 27391 for (var i = 0; i < instanceCount; i++) { |
| 27392 var cursor = new _InstanceCursor(_templateElement, 1); |
| 27393 cursor.remove(); |
| 27394 } |
| 27395 |
| 27396 instanceCount = 0; |
| 27397 } |
| 27398 |
| 27399 void abandon() { |
| 27400 unobserve(); |
| 27401 _valueBinding.cancel(); |
| 27402 inputs.dispose(); |
| 27403 } |
| 27404 } |
| 27405 |
| 27406 int _instanceCount(Element element) { |
| 27407 var templateIterator = element._templateIterator; |
| 27408 return templateIterator != null ? templateIterator.instanceCount : 0; |
| 27409 } |
| 27410 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file |
| 27411 // for details. All rights reserved. Use of this source code is governed by a |
| 27412 // BSD-style license that can be found in the LICENSE file. |
| 27413 |
| 27414 |
| 26064 /** | 27415 /** |
| 26065 * Helper class to implement custom events which wrap DOM events. | 27416 * Helper class to implement custom events which wrap DOM events. |
| 26066 */ | 27417 */ |
| 26067 class _WrappedEvent implements Event { | 27418 class _WrappedEvent implements Event { |
| 26068 final Event wrapped; | 27419 final Event wrapped; |
| 26069 _WrappedEvent(this.wrapped); | 27420 _WrappedEvent(this.wrapped); |
| 26070 | 27421 |
| 26071 bool get bubbles => wrapped.bubbles; | 27422 bool get bubbles => wrapped.bubbles; |
| 26072 | 27423 |
| 26073 bool get cancelBubble => wrapped.bubbles; | 27424 bool get cancelBubble => wrapped.bubbles; |
| (...skipping 1225 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 27299 } | 28650 } |
| 27300 | 28651 |
| 27301 _send(msg) { | 28652 _send(msg) { |
| 27302 _sendToHelperIsolate(msg, _sendPort); | 28653 _sendToHelperIsolate(msg, _sendPort); |
| 27303 } | 28654 } |
| 27304 } | 28655 } |
| 27305 | 28656 |
| 27306 get _pureIsolateTimerFactoryClosure => | 28657 get _pureIsolateTimerFactoryClosure => |
| 27307 ((int milliSeconds, void callback(Timer time), bool repeating) => | 28658 ((int milliSeconds, void callback(Timer time), bool repeating) => |
| 27308 new _PureIsolateTimer(milliSeconds, callback, repeating)); | 28659 new _PureIsolateTimer(milliSeconds, callback, repeating)); |
| OLD | NEW |