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 |