OLD | NEW |
| (Empty) |
1 // Copyright (c) 2014 The Polymer Project Authors. All rights reserved. | |
2 // This code may only be used under the BSD style license found at http://polyme
r.github.io/LICENSE.txt | |
3 // The complete set of authors may be found at http://polymer.github.io/AUTHORS.
txt | |
4 // The complete set of contributors may be found at http://polymer.github.io/CON
TRIBUTORS.txt | |
5 // Code distributed by Google as part of the polymer project is also | |
6 // subject to an additional IP rights grant found at http://polymer.github.io/PA
TENTS.txt | |
7 | |
8 (function(global) { | |
9 'use strict'; | |
10 | |
11 var filter = Array.prototype.filter.call.bind(Array.prototype.filter); | |
12 | |
13 function getTreeScope(node) { | |
14 while (node.parentNode) { | |
15 node = node.parentNode; | |
16 } | |
17 | |
18 return typeof node.getElementById === 'function' ? node : null; | |
19 } | |
20 | |
21 Node.prototype.bind = function(name, observable) { | |
22 console.error('Unhandled binding to Node: ', this, name, observable); | |
23 }; | |
24 | |
25 Node.prototype.bindFinished = function() {}; | |
26 | |
27 function updateBindings(node, name, binding) { | |
28 var bindings = node.bindings_; | |
29 if (!bindings) | |
30 bindings = node.bindings_ = {}; | |
31 | |
32 if (bindings[name]) | |
33 binding[name].close(); | |
34 | |
35 return bindings[name] = binding; | |
36 } | |
37 | |
38 function returnBinding(node, name, binding) { | |
39 return binding; | |
40 } | |
41 | |
42 function sanitizeValue(value) { | |
43 return value == null ? '' : value; | |
44 } | |
45 | |
46 function updateText(node, value) { | |
47 node.data = sanitizeValue(value); | |
48 } | |
49 | |
50 function textBinding(node) { | |
51 return function(value) { | |
52 return updateText(node, value); | |
53 }; | |
54 } | |
55 | |
56 var maybeUpdateBindings = returnBinding; | |
57 | |
58 Object.defineProperty(Platform, 'enableBindingsReflection', { | |
59 get: function() { | |
60 return maybeUpdateBindings === updateBindings; | |
61 }, | |
62 set: function(enable) { | |
63 maybeUpdateBindings = enable ? updateBindings : returnBinding; | |
64 return enable; | |
65 }, | |
66 configurable: true | |
67 }); | |
68 | |
69 Text.prototype.bind = function(name, value, oneTime) { | |
70 if (name !== 'textContent') | |
71 return Node.prototype.bind.call(this, name, value, oneTime); | |
72 | |
73 if (oneTime) | |
74 return updateText(this, value); | |
75 | |
76 var observable = value; | |
77 updateText(this, observable.open(textBinding(this))); | |
78 return maybeUpdateBindings(this, name, observable); | |
79 } | |
80 | |
81 function updateAttribute(el, name, conditional, value) { | |
82 if (conditional) { | |
83 if (value) | |
84 el.setAttribute(name, ''); | |
85 else | |
86 el.removeAttribute(name); | |
87 return; | |
88 } | |
89 | |
90 el.setAttribute(name, sanitizeValue(value)); | |
91 } | |
92 | |
93 function attributeBinding(el, name, conditional) { | |
94 return function(value) { | |
95 updateAttribute(el, name, conditional, value); | |
96 }; | |
97 } | |
98 | |
99 Element.prototype.bind = function(name, value, oneTime) { | |
100 var conditional = name[name.length - 1] == '?'; | |
101 if (conditional) { | |
102 this.removeAttribute(name); | |
103 name = name.slice(0, -1); | |
104 } | |
105 | |
106 if (oneTime) | |
107 return updateAttribute(this, name, conditional, value); | |
108 | |
109 | |
110 var observable = value; | |
111 updateAttribute(this, name, conditional, | |
112 observable.open(attributeBinding(this, name, conditional))); | |
113 | |
114 return maybeUpdateBindings(this, name, observable); | |
115 }; | |
116 | |
117 var checkboxEventType; | |
118 (function() { | |
119 // Attempt to feature-detect which event (change or click) is fired first | |
120 // for checkboxes. | |
121 var div = document.createElement('div'); | |
122 var checkbox = div.appendChild(document.createElement('input')); | |
123 checkbox.setAttribute('type', 'checkbox'); | |
124 var first; | |
125 var count = 0; | |
126 checkbox.addEventListener('click', function(e) { | |
127 count++; | |
128 first = first || 'click'; | |
129 }); | |
130 checkbox.addEventListener('change', function() { | |
131 count++; | |
132 first = first || 'change'; | |
133 }); | |
134 | |
135 var event = document.createEvent('MouseEvent'); | |
136 event.initMouseEvent("click", true, true, window, 0, 0, 0, 0, 0, false, | |
137 false, false, false, 0, null); | |
138 checkbox.dispatchEvent(event); | |
139 // WebKit/Blink don't fire the change event if the element is outside the | |
140 // document, so assume 'change' for that case. | |
141 checkboxEventType = count == 1 ? 'change' : first; | |
142 })(); | |
143 | |
144 function getEventForInputType(element) { | |
145 switch (element.type) { | |
146 case 'checkbox': | |
147 return checkboxEventType; | |
148 case 'radio': | |
149 case 'select-multiple': | |
150 case 'select-one': | |
151 return 'change'; | |
152 case 'range': | |
153 if (/Trident|MSIE/.test(navigator.userAgent)) | |
154 return 'change'; | |
155 default: | |
156 return 'input'; | |
157 } | |
158 } | |
159 | |
160 function updateInput(input, property, value, santizeFn) { | |
161 input[property] = (santizeFn || sanitizeValue)(value); | |
162 } | |
163 | |
164 function inputBinding(input, property, santizeFn) { | |
165 return function(value) { | |
166 return updateInput(input, property, value, santizeFn); | |
167 } | |
168 } | |
169 | |
170 function noop() {} | |
171 | |
172 function bindInputEvent(input, property, observable, postEventFn) { | |
173 var eventType = getEventForInputType(input); | |
174 | |
175 function eventHandler() { | |
176 observable.setValue(input[property]); | |
177 observable.discardChanges(); | |
178 (postEventFn || noop)(input); | |
179 Platform.performMicrotaskCheckpoint(); | |
180 } | |
181 input.addEventListener(eventType, eventHandler); | |
182 | |
183 return { | |
184 close: function() { | |
185 input.removeEventListener(eventType, eventHandler); | |
186 observable.close(); | |
187 }, | |
188 | |
189 observable_: observable | |
190 } | |
191 } | |
192 | |
193 function booleanSanitize(value) { | |
194 return Boolean(value); | |
195 } | |
196 | |
197 // |element| is assumed to be an HTMLInputElement with |type| == 'radio'. | |
198 // Returns an array containing all radio buttons other than |element| that | |
199 // have the same |name|, either in the form that |element| belongs to or, | |
200 // if no form, in the document tree to which |element| belongs. | |
201 // | |
202 // This implementation is based upon the HTML spec definition of a | |
203 // "radio button group": | |
204 // http://www.whatwg.org/specs/web-apps/current-work/multipage/number-state.
html#radio-button-group | |
205 // | |
206 function getAssociatedRadioButtons(element) { | |
207 if (element.form) { | |
208 return filter(element.form.elements, function(el) { | |
209 return el != element && | |
210 el.tagName == 'INPUT' && | |
211 el.type == 'radio' && | |
212 el.name == element.name; | |
213 }); | |
214 } else { | |
215 var treeScope = getTreeScope(element); | |
216 if (!treeScope) | |
217 return []; | |
218 var radios = treeScope.querySelectorAll( | |
219 'input[type="radio"][name="' + element.name + '"]'); | |
220 return filter(radios, function(el) { | |
221 return el != element && !el.form; | |
222 }); | |
223 } | |
224 } | |
225 | |
226 function checkedPostEvent(input) { | |
227 // Only the radio button that is getting checked gets an event. We | |
228 // therefore find all the associated radio buttons and update their | |
229 // check binding manually. | |
230 if (input.tagName === 'INPUT' && | |
231 input.type === 'radio') { | |
232 getAssociatedRadioButtons(input).forEach(function(radio) { | |
233 var checkedBinding = radio.bindings_.checked; | |
234 if (checkedBinding) { | |
235 // Set the value directly to avoid an infinite call stack. | |
236 checkedBinding.observable_.setValue(false); | |
237 } | |
238 }); | |
239 } | |
240 } | |
241 | |
242 HTMLInputElement.prototype.bind = function(name, value, oneTime) { | |
243 if (name !== 'value' && name !== 'checked') | |
244 return HTMLElement.prototype.bind.call(this, name, value, oneTime); | |
245 | |
246 this.removeAttribute(name); | |
247 var sanitizeFn = name == 'checked' ? booleanSanitize : sanitizeValue; | |
248 var postEventFn = name == 'checked' ? checkedPostEvent : noop; | |
249 | |
250 if (oneTime) | |
251 return updateInput(this, name, value, sanitizeFn); | |
252 | |
253 | |
254 var observable = value; | |
255 var binding = bindInputEvent(this, name, observable, postEventFn); | |
256 updateInput(this, name, | |
257 observable.open(inputBinding(this, name, sanitizeFn)), | |
258 sanitizeFn); | |
259 | |
260 // Checkboxes may need to update bindings of other checkboxes. | |
261 return updateBindings(this, name, binding); | |
262 } | |
263 | |
264 HTMLTextAreaElement.prototype.bind = function(name, value, oneTime) { | |
265 if (name !== 'value') | |
266 return HTMLElement.prototype.bind.call(this, name, value, oneTime); | |
267 | |
268 this.removeAttribute('value'); | |
269 | |
270 if (oneTime) | |
271 return updateInput(this, 'value', value); | |
272 | |
273 var observable = value; | |
274 var binding = bindInputEvent(this, 'value', observable); | |
275 updateInput(this, 'value', | |
276 observable.open(inputBinding(this, 'value', sanitizeValue))); | |
277 return maybeUpdateBindings(this, name, binding); | |
278 } | |
279 | |
280 function updateOption(option, value) { | |
281 var parentNode = option.parentNode;; | |
282 var select; | |
283 var selectBinding; | |
284 var oldValue; | |
285 if (parentNode instanceof HTMLSelectElement && | |
286 parentNode.bindings_ && | |
287 parentNode.bindings_.value) { | |
288 select = parentNode; | |
289 selectBinding = select.bindings_.value; | |
290 oldValue = select.value; | |
291 } | |
292 | |
293 option.value = sanitizeValue(value); | |
294 | |
295 if (select && select.value != oldValue) { | |
296 selectBinding.observable_.setValue(select.value); | |
297 selectBinding.observable_.discardChanges(); | |
298 Platform.performMicrotaskCheckpoint(); | |
299 } | |
300 } | |
301 | |
302 function optionBinding(option) { | |
303 return function(value) { | |
304 updateOption(option, value); | |
305 } | |
306 } | |
307 | |
308 HTMLOptionElement.prototype.bind = function(name, value, oneTime) { | |
309 if (name !== 'value') | |
310 return HTMLElement.prototype.bind.call(this, name, value, oneTime); | |
311 | |
312 this.removeAttribute('value'); | |
313 | |
314 if (oneTime) | |
315 return updateOption(this, value); | |
316 | |
317 var observable = value; | |
318 var binding = bindInputEvent(this, 'value', observable); | |
319 updateOption(this, observable.open(optionBinding(this))); | |
320 return maybeUpdateBindings(this, name, binding); | |
321 } | |
322 | |
323 HTMLSelectElement.prototype.bind = function(name, value, oneTime) { | |
324 if (name === 'selectedindex') | |
325 name = 'selectedIndex'; | |
326 | |
327 if (name !== 'selectedIndex' && name !== 'value') | |
328 return HTMLElement.prototype.bind.call(this, name, value, oneTime); | |
329 | |
330 this.removeAttribute(name); | |
331 | |
332 if (oneTime) | |
333 return updateInput(this, name, value); | |
334 | |
335 var observable = value; | |
336 var binding = bindInputEvent(this, name, observable); | |
337 updateInput(this, name, | |
338 observable.open(inputBinding(this, name))); | |
339 | |
340 // Option update events may need to access select bindings. | |
341 return updateBindings(this, name, binding); | |
342 } | |
343 })(this); | |
OLD | NEW |