| 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 abstract class _InputBinding extends NodeBinding { |
| 8 StreamSubscription _eventSub; | 8 StreamSubscription _eventSub; |
| 9 | 9 |
| 10 _InputBinding(node, name, model, path): super(node, name, model, path) { | 10 _InputBinding(node, name, model, path): super(node, name, model, path) { |
| 11 _eventSub = _getStreamForInputType(node).listen(nodeValueChanged); | 11 _eventSub = _getStreamForInputType(node).listen(nodeValueChanged); |
| 12 } | 12 } |
| 13 | 13 |
| 14 void boundValueChanged(newValue); | 14 void valueChanged(newValue); |
| 15 | 15 |
| 16 void nodeValueChanged(e); | 16 void nodeValueChanged(e); |
| 17 | 17 |
| 18 void close() { | 18 void close() { |
| 19 if (closed) return; | 19 if (closed) return; |
| 20 _eventSub.cancel(); | 20 _eventSub.cancel(); |
| 21 super.close(); | 21 super.close(); |
| 22 } | 22 } |
| 23 | 23 |
| 24 static EventStreamProvider<Event> _checkboxEventType = () { | 24 static EventStreamProvider<Event> _checkboxEventType = () { |
| 25 // Attempt to feature-detect which event (change or click) is fired first | 25 // Attempt to feature-detect which event (change or click) is fired first |
| 26 // for checkboxes. | 26 // for checkboxes. |
| 27 var div = new DivElement(); | 27 var div = new DivElement(); |
| 28 var checkbox = div.append(new InputElement()); | 28 var checkbox = div.append(new InputElement()); |
| 29 checkbox.type = 'checkbox'; | 29 checkbox.type = 'checkbox'; |
| 30 var fired = []; | 30 var fired = []; |
| 31 checkbox.onClick.listen((e) { | 31 checkbox.onClick.listen((e) { |
| 32 fired.add(Element.clickEvent); | 32 fired.add(Element.clickEvent); |
| 33 }); | 33 }); |
| 34 checkbox.onChange.listen((e) { | 34 checkbox.onChange.listen((e) { |
| 35 fired.add(Element.changeEvent); | 35 fired.add(Element.changeEvent); |
| 36 }); | 36 }); |
| 37 checkbox.dispatchEvent(new MouseEvent('click', view: window)); | 37 checkbox.dispatchEvent(new MouseEvent('click', view: window)); |
| 38 // WebKit/Blink don't fire the change event if the element is outside the | 38 // WebKit/Blink don't fire the change event if the element is outside the |
| 39 // document, so assume 'change' for that case. | 39 // document, so assume 'change' for that case. |
| 40 return fired.length == 1 ? Element.changeEvent : fired.first; | 40 return fired.length == 1 ? Element.changeEvent : fired.first; |
| 41 }(); | 41 }(); |
| 42 | 42 |
| 43 static Stream<Event> _getStreamForInputType(element) { | 43 static Stream<Event> _getStreamForInputType(element) { |
| 44 if (element is OptionElement) return element.onInput; |
| 44 switch (element.type) { | 45 switch (element.type) { |
| 45 case 'checkbox': | 46 case 'checkbox': |
| 46 return _checkboxEventType.forTarget(element); | 47 return _checkboxEventType.forTarget(element); |
| 47 case 'radio': | 48 case 'radio': |
| 48 case 'select-multiple': | 49 case 'select-multiple': |
| 49 case 'select-one': | 50 case 'select-one': |
| 50 return element.onChange; | 51 return element.onChange; |
| 51 default: | 52 default: |
| 52 return element.onInput; | 53 return element.onInput; |
| 53 } | 54 } |
| 54 } | 55 } |
| 55 } | 56 } |
| 56 | 57 |
| 57 class _ValueBinding extends _InputBinding { | 58 class _ValueBinding extends _InputBinding { |
| 58 _ValueBinding(node, model, path) : super(node, 'value', model, path); | 59 _ValueBinding(node, model, path) : super(node, 'value', model, path); |
| 59 | 60 |
| 60 get node => super.node; | 61 get node => super.node; |
| 61 | 62 |
| 62 void boundValueChanged(newValue) { | 63 void valueChanged(newValue) { |
| 63 // Note: node can be an InputElement or TextAreaElement. Both have "value". | 64 // Note: node can be an InputElement or TextAreaElement. Both have "value". |
| 64 (node as dynamic).value = sanitizeBoundValue(newValue); | 65 node.value = sanitizeBoundValue(newValue); |
| 65 } | 66 } |
| 66 | 67 |
| 67 void nodeValueChanged(e) { | 68 void nodeValueChanged(e) { |
| 68 value = (node as dynamic).value; | 69 value = node.value; |
| 70 Observable.dirtyCheck(); |
| 69 } | 71 } |
| 70 } | 72 } |
| 71 | 73 |
| 72 class _CheckedBinding extends _InputBinding { | 74 class _CheckedBinding extends _InputBinding { |
| 73 _CheckedBinding(node, model, path) : super(node, 'checked', model, path); | 75 _CheckedBinding(node, model, path) : super(node, 'checked', model, path); |
| 74 | 76 |
| 75 InputElement get node => super.node; | 77 InputElement get node => super.node; |
| 76 | 78 |
| 77 void boundValueChanged(newValue) { | 79 void valueChanged(newValue) { |
| 78 node.checked = _toBoolean(newValue); | 80 node.checked = _toBoolean(newValue); |
| 79 } | 81 } |
| 80 | 82 |
| 81 void nodeValueChanged(e) { | 83 void nodeValueChanged(e) { |
| 82 value = node.checked; | 84 value = node.checked; |
| 83 | 85 |
| 84 // Only the radio button that is getting checked gets an event. We | 86 // Only the radio button that is getting checked gets an event. We |
| 85 // therefore find all the associated radio buttons and update their | 87 // therefore find all the associated radio buttons and update their |
| 86 // CheckedBinding manually. | 88 // CheckedBinding manually. |
| 87 if (node is InputElement && node.type == 'radio') { | 89 if (node is InputElement && node.type == 'radio') { |
| 88 for (var r in _getAssociatedRadioButtons(node)) { | 90 for (var r in _getAssociatedRadioButtons(node)) { |
| 89 var checkedBinding = nodeBind(r).bindings['checked']; | 91 var checkedBinding = nodeBind(r).bindings['checked']; |
| 90 if (checkedBinding != null) { | 92 if (checkedBinding != null) { |
| 91 // Set the value directly to avoid an infinite call stack. | 93 // Set the value directly to avoid an infinite call stack. |
| 92 checkedBinding.value = false; | 94 checkedBinding.value = false; |
| 93 } | 95 } |
| 94 } | 96 } |
| 95 } | 97 } |
| 98 |
| 99 Observable.dirtyCheck(); |
| 96 } | 100 } |
| 97 | 101 |
| 98 // |element| is assumed to be an HTMLInputElement with |type| == 'radio'. | 102 // |element| is assumed to be an HTMLInputElement with |type| == 'radio'. |
| 99 // Returns an array containing all radio buttons other than |element| that | 103 // Returns an array containing all radio buttons other than |element| that |
| 100 // have the same |name|, either in the form that |element| belongs to or, | 104 // have the same |name|, either in the form that |element| belongs to or, |
| 101 // if no form, in the document tree to which |element| belongs. | 105 // if no form, in the document tree to which |element| belongs. |
| 102 // | 106 // |
| 103 // This implementation is based upon the HTML spec definition of a | 107 // This implementation is based upon the HTML spec definition of a |
| 104 // "radio button group": | 108 // "radio button group": |
| 105 // http://www.whatwg.org/specs/web-apps/current-work/multipage/number-state.
html#radio-button-group | 109 // http://www.whatwg.org/specs/web-apps/current-work/multipage/number-state.
html#radio-button-group |
| 106 // | 110 // |
| 107 static Iterable _getAssociatedRadioButtons(element) { | 111 static Iterable _getAssociatedRadioButtons(element) { |
| 108 if (!_isNodeInDocument(element)) return []; | |
| 109 if (element.form != null) { | 112 if (element.form != null) { |
| 110 return element.form.nodes.where((el) { | 113 return element.form.nodes.where((el) { |
| 111 return el != element && | 114 return el != element && |
| 112 el is InputElement && | 115 el is InputElement && |
| 113 el.type == 'radio' && | 116 el.type == 'radio' && |
| 114 el.name == element.name; | 117 el.name == element.name; |
| 115 }); | 118 }); |
| 116 } else { | 119 } else { |
| 117 var radios = element.ownerDocument.queryAll( | 120 var treeScope = _getTreeScope(element); |
| 121 if (treeScope == null) return const []; |
| 122 |
| 123 var radios = treeScope.querySelectorAll( |
| 118 'input[type="radio"][name="${element.name}"]'); | 124 'input[type="radio"][name="${element.name}"]'); |
| 119 return radios.where((el) => el != element && el.form == null); | 125 return radios.where((el) => el != element && el.form == null); |
| 120 } | 126 } |
| 121 } | 127 } |
| 122 | |
| 123 // TODO(jmesserly): polyfill document.contains API instead of doing it here | |
| 124 static bool _isNodeInDocument(Node node) { | |
| 125 // On non-IE this works: | |
| 126 // return node.ownerDocument.contains(node); | |
| 127 var document = node.ownerDocument; | |
| 128 if (node == document || node.parentNode == document) return true; | |
| 129 return document.documentElement.contains(node); | |
| 130 } | |
| 131 } | 128 } |
| 132 | 129 |
| 133 class _SelectedIndexBinding extends _InputBinding { | 130 class _SelectBinding extends _InputBinding { |
| 134 _SelectedIndexBinding(node, model, path) | 131 _SelectBinding(node, property, model, path) |
| 135 : super(node, 'selectedIndex', model, path); | 132 : super(node, property, model, path); |
| 136 | 133 |
| 137 SelectElement get node => super.node; | 134 SelectElement get node => super.node; |
| 138 | 135 |
| 139 void boundValueChanged(value) { | 136 void valueChanged(newValue) { |
| 140 var newValue = _toInt(value); | 137 if (_tryUpdateValue(newValue)) return; |
| 141 if (newValue <= node.length) { | |
| 142 node.selectedIndex = newValue; | |
| 143 return; | |
| 144 } | |
| 145 | 138 |
| 146 // The binding may wish to bind to an <option> which has not yet been | 139 // The binding may wish to bind to an <option> which has not yet been |
| 147 // produced by a child <template>. Furthermore, we may need to wait for | 140 // produced by a child <template>. Furthermore, we may need to wait for |
| 148 // <optgroup> iterating and then for <option>. | 141 // <optgroup> iterating and then for <option>. |
| 149 // | 142 // |
| 150 // Unlike the JavaScript MDV, we don't have a special "Object.observe" event | 143 // Unlike the JavaScript implemenation, we don't have a special |
| 151 // loop to schedule on. (See the the "ensureScheduled" function: | 144 // "Object.observe" event loop to schedule on. |
| 145 // (See the the "ensureScheduled" function: |
| 152 // https://github.com/Polymer/mdv/commit/9a51ad7ed74a292bf71662cea28acbd151f
f65c8) | 146 // https://github.com/Polymer/mdv/commit/9a51ad7ed74a292bf71662cea28acbd151f
f65c8) |
| 153 // | 147 // |
| 154 // Instead we use scheduleMicrotask. Each <template repeat> needs a delay of | 148 // Instead we use scheduleMicrotask. Each <template repeat> needs a delay of |
| 155 // 2: | 149 // 2: |
| 156 // * once to happen after the child _TemplateIterator is created | 150 // * once to happen after the child _TemplateIterator is created |
| 157 // * once to be after _TemplateIterator.inputs CompoundBinding resolve | 151 // * once to be after _TemplateIterator.inputs CompoundBinding resolve |
| 158 // And then we need to do this delay sequence twice: | 152 // And then we need to do this delay sequence twice: |
| 159 // * once for OPTGROUP | 153 // * once for OPTGROUP |
| 160 // * once for OPTION. | 154 // * once for OPTION. |
| 161 // The resulting 2 * 2 is our maxRetries. | 155 // The resulting 2 * 2 is our maxRetries. |
| 156 // TODO(jmesserly): a much better approach would be to find the nested |
| 157 // <template> and wait on some future that completes when it has expanded. |
| 162 var maxRetries = 4; | 158 var maxRetries = 4; |
| 163 delaySetSelectedIndex() { | 159 delaySetSelectedIndex() { |
| 164 if (newValue > node.length && --maxRetries >= 0) { | 160 if (!_tryUpdateValue(newValue) && maxRetries-- > 0) { |
| 165 scheduleMicrotask(delaySetSelectedIndex); | 161 scheduleMicrotask(delaySetSelectedIndex); |
| 166 } else { | |
| 167 node.selectedIndex = newValue; | |
| 168 } | 162 } |
| 169 } | 163 } |
| 170 | 164 |
| 171 scheduleMicrotask(delaySetSelectedIndex); | 165 scheduleMicrotask(delaySetSelectedIndex); |
| 172 } | 166 } |
| 173 | 167 |
| 168 bool _tryUpdateValue(newValue) { |
| 169 if (property == 'selectedIndex') { |
| 170 var intValue = _toInt(newValue); |
| 171 node.selectedIndex = intValue; |
| 172 return node.selectedIndex == intValue; |
| 173 } else if (property == 'value') { |
| 174 node.value = sanitizeBoundValue(newValue); |
| 175 return node.value == newValue; |
| 176 } |
| 177 } |
| 178 |
| 174 void nodeValueChanged(e) { | 179 void nodeValueChanged(e) { |
| 175 value = node.selectedIndex; | 180 if (property == 'selectedIndex') { |
| 181 value = node.selectedIndex; |
| 182 } else if (property == 'value') { |
| 183 value = node.value; |
| 184 } |
| 176 } | 185 } |
| 177 | 186 |
| 178 // TODO(jmesserly,sigmund): I wonder how many bindings typically convert from | 187 // TODO(jmesserly,sigmund): I wonder how many bindings typically convert from |
| 179 // one type to another (e.g. value-as-number) and whether it is useful to | 188 // one type to another (e.g. value-as-number) and whether it is useful to |
| 180 // have something like a int/num binding converter (either as a base class or | 189 // have something like a int/num binding converter (either as a base class or |
| 181 // a wrapper). | 190 // a wrapper). |
| 182 static int _toInt(value) { | 191 static int _toInt(value) { |
| 183 if (value is String) return int.parse(value, onError: (_) => null); | 192 if (value is String) return int.parse(value, onError: (_) => 0); |
| 184 return value is int ? value : null; | 193 return value is int ? value : 0; |
| 185 } | 194 } |
| 186 } | 195 } |
| OLD | NEW |