| OLD | NEW |
| 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file |
| 2 // for details. All rights reserved. Use of this source code is governed by a | 2 // for details. All rights reserved. Use of this source code is governed by a |
| 3 // BSD-style license that can be found in the LICENSE file. | 3 // BSD-style license that can be found in the LICENSE file. |
| 4 | 4 |
| 5 part of template_binding; | 5 part of template_binding; |
| 6 | 6 |
| 7 abstract class _InputBinding extends NodeBinding { | 7 |
| 8 // Note: the JavaScript version monkeypatches(!!) the close method of the passed |
| 9 // in Bindable. We use a wrapper instead. |
| 10 class _InputBinding extends Bindable { |
| 11 // Note: node can be an InputElement or TextAreaElement. Both have "value". |
| 12 var _node; |
| 8 StreamSubscription _eventSub; | 13 StreamSubscription _eventSub; |
| 14 Bindable _bindable; |
| 15 String _propertyName; |
| 9 | 16 |
| 10 _InputBinding(node, name, model, path): super(node, name, model, path) { | 17 _InputBinding(this._node, this._bindable, this._propertyName) { |
| 11 _eventSub = _getStreamForInputType(node).listen(nodeValueChanged); | 18 _eventSub = _getStreamForInputType(_node).listen(_nodeChanged); |
| 19 _updateNode(open(_updateNode)); |
| 12 } | 20 } |
| 13 | 21 |
| 14 void valueChanged(newValue); | 22 void _updateNode(newValue) => _updateProperty(_node, newValue, _propertyName); |
| 15 | 23 |
| 16 void nodeValueChanged(e); | 24 static void _updateProperty(node, newValue, String propertyName) { |
| 25 switch (propertyName) { |
| 26 case 'checked': |
| 27 node.checked = _toBoolean(newValue); |
| 28 return; |
| 29 case 'selectedIndex': |
| 30 node.selectedIndex = _toInt(newValue); |
| 31 return; |
| 32 case 'value': |
| 33 node.value = _sanitizeValue(newValue); |
| 34 return; |
| 35 } |
| 36 } |
| 37 |
| 38 void _nodeChanged(e) { |
| 39 switch (_propertyName) { |
| 40 case 'value': |
| 41 value = _node.value; |
| 42 break; |
| 43 case 'checked': |
| 44 value = _node.checked; |
| 45 |
| 46 // Only the radio button that is getting checked gets an event. We |
| 47 // therefore find all the associated radio buttons and update their |
| 48 // checked binding manually. |
| 49 if (_node is InputElement && _node.type == 'radio') { |
| 50 for (var r in _getAssociatedRadioButtons(_node)) { |
| 51 var checkedBinding = nodeBind(r).bindings['checked']; |
| 52 if (checkedBinding != null) { |
| 53 // Set the value directly to avoid an infinite call stack. |
| 54 checkedBinding.value = false; |
| 55 } |
| 56 } |
| 57 } |
| 58 break; |
| 59 case 'selectedIndex': |
| 60 value = _node.selectedIndex; |
| 61 break; |
| 62 } |
| 63 |
| 64 Observable.dirtyCheck(); |
| 65 } |
| 66 |
| 67 open(callback(value)) => _bindable.open(callback); |
| 68 get value => _bindable.value; |
| 69 set value(newValue) => _bindable.value = newValue; |
| 17 | 70 |
| 18 void close() { | 71 void close() { |
| 19 if (closed) return; | 72 if (_eventSub != null) { |
| 20 _eventSub.cancel(); | 73 _eventSub.cancel(); |
| 21 super.close(); | 74 _eventSub = null; |
| 75 } |
| 76 if (_bindable != null) { |
| 77 _bindable.close(); |
| 78 _bindable = null; |
| 79 } |
| 22 } | 80 } |
| 23 | 81 |
| 24 static EventStreamProvider<Event> _checkboxEventType = () { | 82 static EventStreamProvider<Event> _checkboxEventType = () { |
| 25 // Attempt to feature-detect which event (change or click) is fired first | 83 // Attempt to feature-detect which event (change or click) is fired first |
| 26 // for checkboxes. | 84 // for checkboxes. |
| 27 var div = new DivElement(); | 85 var div = new DivElement(); |
| 28 var checkbox = div.append(new InputElement()); | 86 var checkbox = div.append(new InputElement()); |
| 29 checkbox.type = 'checkbox'; | 87 checkbox.type = 'checkbox'; |
| 30 var fired = []; | 88 var fired = []; |
| 31 checkbox.onClick.listen((e) { | 89 checkbox.onClick.listen((e) { |
| (...skipping 14 matching lines...) Expand all Loading... |
| 46 case 'checkbox': | 104 case 'checkbox': |
| 47 return _checkboxEventType.forTarget(element); | 105 return _checkboxEventType.forTarget(element); |
| 48 case 'radio': | 106 case 'radio': |
| 49 case 'select-multiple': | 107 case 'select-multiple': |
| 50 case 'select-one': | 108 case 'select-one': |
| 51 return element.onChange; | 109 return element.onChange; |
| 52 default: | 110 default: |
| 53 return element.onInput; | 111 return element.onInput; |
| 54 } | 112 } |
| 55 } | 113 } |
| 56 } | |
| 57 | |
| 58 class _ValueBinding extends _InputBinding { | |
| 59 _ValueBinding(node, model, path) : super(node, 'value', model, path); | |
| 60 | |
| 61 get node => super.node; | |
| 62 | |
| 63 void valueChanged(newValue) { | |
| 64 // Note: node can be an InputElement or TextAreaElement. Both have "value". | |
| 65 node.value = sanitizeBoundValue(newValue); | |
| 66 } | |
| 67 | |
| 68 void nodeValueChanged(e) { | |
| 69 value = node.value; | |
| 70 Observable.dirtyCheck(); | |
| 71 } | |
| 72 } | |
| 73 | |
| 74 class _CheckedBinding extends _InputBinding { | |
| 75 _CheckedBinding(node, model, path) : super(node, 'checked', model, path); | |
| 76 | |
| 77 InputElement get node => super.node; | |
| 78 | |
| 79 void valueChanged(newValue) { | |
| 80 node.checked = _toBoolean(newValue); | |
| 81 } | |
| 82 | |
| 83 void nodeValueChanged(e) { | |
| 84 value = node.checked; | |
| 85 | |
| 86 // Only the radio button that is getting checked gets an event. We | |
| 87 // therefore find all the associated radio buttons and update their | |
| 88 // CheckedBinding manually. | |
| 89 if (node is InputElement && node.type == 'radio') { | |
| 90 for (var r in _getAssociatedRadioButtons(node)) { | |
| 91 var checkedBinding = nodeBind(r).bindings['checked']; | |
| 92 if (checkedBinding != null) { | |
| 93 // Set the value directly to avoid an infinite call stack. | |
| 94 checkedBinding.value = false; | |
| 95 } | |
| 96 } | |
| 97 } | |
| 98 | |
| 99 Observable.dirtyCheck(); | |
| 100 } | |
| 101 | 114 |
| 102 // |element| is assumed to be an HTMLInputElement with |type| == 'radio'. | 115 // |element| is assumed to be an HTMLInputElement with |type| == 'radio'. |
| 103 // Returns an array containing all radio buttons other than |element| that | 116 // Returns an array containing all radio buttons other than |element| that |
| 104 // have the same |name|, either in the form that |element| belongs to or, | 117 // have the same |name|, either in the form that |element| belongs to or, |
| 105 // if no form, in the document tree to which |element| belongs. | 118 // if no form, in the document tree to which |element| belongs. |
| 106 // | 119 // |
| 107 // This implementation is based upon the HTML spec definition of a | 120 // This implementation is based upon the HTML spec definition of a |
| 108 // "radio button group": | 121 // "radio button group": |
| 109 // http://www.whatwg.org/specs/web-apps/current-work/multipage/number-state.
html#radio-button-group | 122 // http://www.whatwg.org/specs/web-apps/current-work/multipage/number-state.
html#radio-button-group |
| 110 // | 123 // |
| 111 static Iterable _getAssociatedRadioButtons(element) { | 124 static Iterable _getAssociatedRadioButtons(element) { |
| 112 if (element.form != null) { | 125 if (element.form != null) { |
| 113 return element.form.nodes.where((el) { | 126 return element.form.nodes.where((el) { |
| 114 return el != element && | 127 return el != element && |
| 115 el is InputElement && | 128 el is InputElement && |
| 116 el.type == 'radio' && | 129 el.type == 'radio' && |
| 117 el.name == element.name; | 130 el.name == element.name; |
| 118 }); | 131 }); |
| 119 } else { | 132 } else { |
| 120 var treeScope = _getTreeScope(element); | 133 var treeScope = _getTreeScope(element); |
| 121 if (treeScope == null) return const []; | 134 if (treeScope == null) return const []; |
| 122 | 135 |
| 123 var radios = treeScope.querySelectorAll( | 136 var radios = treeScope.querySelectorAll( |
| 124 'input[type="radio"][name="${element.name}"]'); | 137 'input[type="radio"][name="${element.name}"]'); |
| 125 return radios.where((el) => el != element && el.form == null); | 138 return radios.where((el) => el != element && el.form == null); |
| 126 } | 139 } |
| 127 } | 140 } |
| 128 } | |
| 129 | |
| 130 class _SelectBinding extends _InputBinding { | |
| 131 MutationObserver _onMutation; | |
| 132 | |
| 133 _SelectBinding(node, property, model, path) | |
| 134 : super(node, property, model, path); | |
| 135 | |
| 136 SelectElement get node => super.node; | |
| 137 | |
| 138 void valueChanged(newValue) { | |
| 139 _cancelMutationObserver(); | |
| 140 | |
| 141 if (_tryUpdateValue(newValue)) return; | |
| 142 | |
| 143 // It could be that a template will expand an <option> child (or grandchild, | |
| 144 // if we have an <optgroup> in between). Since selected index cannot be set | |
| 145 // if the children aren't created yet, we need to wait for them to be | |
| 146 // created do this with a MutationObserver. | |
| 147 // Dart note: unlike JS we use mutation observers to avoid: | |
| 148 // https://github.com/Polymer/NodeBind/issues/5 | |
| 149 | |
| 150 // Note: it doesn't matter when the children get added; even if they get | |
| 151 // added much later, presumably we want the selected index data binding to | |
| 152 // still take effect. | |
| 153 _onMutation = new MutationObserver((x, y) { | |
| 154 if (_tryUpdateValue(value)) _cancelMutationObserver(); | |
| 155 })..observe(node, childList: true, subtree: true); | |
| 156 } | |
| 157 | |
| 158 bool _tryUpdateValue(newValue) { | |
| 159 if (property == 'selectedIndex') { | |
| 160 var intValue = _toInt(newValue); | |
| 161 node.selectedIndex = intValue; | |
| 162 return node.selectedIndex == intValue; | |
| 163 } else if (property == 'value') { | |
| 164 node.value = sanitizeBoundValue(newValue); | |
| 165 return node.value == newValue; | |
| 166 } | |
| 167 } | |
| 168 | |
| 169 void _cancelMutationObserver() { | |
| 170 if (_onMutation != null) { | |
| 171 _onMutation.disconnect(); | |
| 172 _onMutation = null; | |
| 173 } | |
| 174 } | |
| 175 | |
| 176 void nodeValueChanged(e) { | |
| 177 _cancelMutationObserver(); | |
| 178 | |
| 179 if (property == 'selectedIndex') { | |
| 180 value = node.selectedIndex; | |
| 181 } else if (property == 'value') { | |
| 182 value = node.value; | |
| 183 } | |
| 184 } | |
| 185 | 141 |
| 186 // TODO(jmesserly,sigmund): I wonder how many bindings typically convert from | 142 // TODO(jmesserly,sigmund): I wonder how many bindings typically convert from |
| 187 // one type to another (e.g. value-as-number) and whether it is useful to | 143 // one type to another (e.g. value-as-number) and whether it is useful to |
| 188 // have something like a int/num binding converter (either as a base class or | 144 // have something like a int/num binding converter (either as a base class or |
| 189 // a wrapper). | 145 // a wrapper). |
| 190 static int _toInt(value) { | 146 static int _toInt(value) { |
| 191 if (value is String) return int.parse(value, onError: (_) => 0); | 147 if (value is String) return int.parse(value, onError: (_) => 0); |
| 192 return value is int ? value : 0; | 148 return value is int ? value : 0; |
| 193 } | 149 } |
| 194 } | 150 } |
| OLD | NEW |