OLD | NEW |
| (Empty) |
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 | |
3 // BSD-style license that can be found in the LICENSE file. | |
4 | |
5 part of template_binding; | |
6 | |
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; | |
13 StreamSubscription _eventSub; | |
14 Bindable _bindable; | |
15 String _propertyName; | |
16 | |
17 _InputBinding(this._node, this._bindable, this._propertyName) { | |
18 _eventSub = _getStreamForInputType(_node).listen(_nodeChanged); | |
19 _updateNode(open(_updateNode)); | |
20 } | |
21 | |
22 void _updateNode(newValue) => _updateProperty(_node, newValue, _propertyName); | |
23 | |
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; | |
70 | |
71 void close() { | |
72 if (_eventSub != null) { | |
73 _eventSub.cancel(); | |
74 _eventSub = null; | |
75 } | |
76 if (_bindable != null) { | |
77 _bindable.close(); | |
78 _bindable = null; | |
79 } | |
80 } | |
81 | |
82 static EventStreamProvider<Event> _checkboxEventType = () { | |
83 // Attempt to feature-detect which event (change or click) is fired first | |
84 // for checkboxes. | |
85 var div = new DivElement(); | |
86 var checkbox = div.append(new InputElement()); | |
87 checkbox.type = 'checkbox'; | |
88 var fired = []; | |
89 checkbox.onClick.listen((e) { | |
90 fired.add(Element.clickEvent); | |
91 }); | |
92 checkbox.onChange.listen((e) { | |
93 fired.add(Element.changeEvent); | |
94 }); | |
95 checkbox.dispatchEvent(new MouseEvent('click', view: window)); | |
96 // WebKit/Blink don't fire the change event if the element is outside the | |
97 // document, so assume 'change' for that case. | |
98 return fired.length == 1 ? Element.changeEvent : fired.first; | |
99 }(); | |
100 | |
101 static Stream<Event> _getStreamForInputType(element) { | |
102 if (element is OptionElement) return element.onInput; | |
103 switch (element.type) { | |
104 case 'checkbox': | |
105 return _checkboxEventType.forTarget(element); | |
106 case 'radio': | |
107 case 'select-multiple': | |
108 case 'select-one': | |
109 return element.onChange; | |
110 case 'range': | |
111 if (window.navigator.userAgent.contains(new RegExp('Trident|MSIE'))) { | |
112 return element.onChange; | |
113 } | |
114 } | |
115 return element.onInput; | |
116 } | |
117 | |
118 // |element| is assumed to be an HTMLInputElement with |type| == 'radio'. | |
119 // Returns an array containing all radio buttons other than |element| that | |
120 // have the same |name|, either in the form that |element| belongs to or, | |
121 // if no form, in the document tree to which |element| belongs. | |
122 // | |
123 // This implementation is based upon the HTML spec definition of a | |
124 // "radio button group": | |
125 // http://www.whatwg.org/specs/web-apps/current-work/multipage/number-state.
html#radio-button-group | |
126 // | |
127 static Iterable _getAssociatedRadioButtons(element) { | |
128 if (element.form != null) { | |
129 return element.form.nodes.where((el) { | |
130 return el != element && | |
131 el is InputElement && | |
132 el.type == 'radio' && | |
133 el.name == element.name; | |
134 }); | |
135 } else { | |
136 var treeScope = _getTreeScope(element); | |
137 if (treeScope == null) return const []; | |
138 | |
139 var radios = treeScope.querySelectorAll( | |
140 'input[type="radio"][name="${element.name}"]'); | |
141 return radios.where((el) => el != element && el.form == null); | |
142 } | |
143 } | |
144 | |
145 // TODO(jmesserly,sigmund): I wonder how many bindings typically convert from | |
146 // one type to another (e.g. value-as-number) and whether it is useful to | |
147 // have something like a int/num binding converter (either as a base class or | |
148 // a wrapper). | |
149 static int _toInt(value) { | |
150 if (value is String) return int.parse(value, onError: (_) => 0); | |
151 return value is int ? value : 0; | |
152 } | |
153 } | |
154 | |
155 _getTreeScope(Node node) { | |
156 Node parent; | |
157 while ((parent = node.parentNode) != null ) { | |
158 node = parent; | |
159 } | |
160 | |
161 return _hasGetElementById(node) ? node : null; | |
162 } | |
163 | |
164 // Note: JS code tests that getElementById is present. We can't do that | |
165 // easily, so instead check for the types known to implement it. | |
166 bool _hasGetElementById(Node node) => | |
167 node is Document || node is ShadowRoot || node is SvgSvgElement; | |
OLD | NEW |