|
1 YUI.add('event-valuechange', function (Y, NAME) { |
|
2 |
|
3 /** |
|
4 Adds a synthetic `valuechange` event that fires when the `value` property of an |
|
5 `<input>`, `<textarea>`, `<select>`, or `[contenteditable="true"]` node changes |
|
6 as a result of a keystroke, mouse operation, or input method editor (IME) |
|
7 input event. |
|
8 |
|
9 Usage: |
|
10 |
|
11 YUI().use('event-valuechange', function (Y) { |
|
12 Y.one('#my-input').on('valuechange', function (e) { |
|
13 Y.log('previous value: ' + e.prevVal); |
|
14 Y.log('new value: ' + e.newVal); |
|
15 }); |
|
16 }); |
|
17 |
|
18 @module event-valuechange |
|
19 **/ |
|
20 |
|
21 /** |
|
22 Provides the implementation for the synthetic `valuechange` event. This class |
|
23 isn't meant to be used directly, but is public to make monkeypatching possible. |
|
24 |
|
25 Usage: |
|
26 |
|
27 YUI().use('event-valuechange', function (Y) { |
|
28 Y.one('#my-input').on('valuechange', function (e) { |
|
29 Y.log('previous value: ' + e.prevVal); |
|
30 Y.log('new value: ' + e.newVal); |
|
31 }); |
|
32 }); |
|
33 |
|
34 @class ValueChange |
|
35 @static |
|
36 */ |
|
37 |
|
38 var DATA_KEY = '_valuechange', |
|
39 VALUE = 'value', |
|
40 NODE_NAME = 'nodeName', |
|
41 |
|
42 config, // defined at the end of this file |
|
43 |
|
44 // Just a simple namespace to make methods overridable. |
|
45 VC = { |
|
46 // -- Static Constants ----------------------------------------------------- |
|
47 |
|
48 /** |
|
49 Interval (in milliseconds) at which to poll for changes to the value of an |
|
50 element with one or more `valuechange` subscribers when the user is likely |
|
51 to be interacting with it. |
|
52 |
|
53 @property POLL_INTERVAL |
|
54 @type Number |
|
55 @default 50 |
|
56 @static |
|
57 **/ |
|
58 POLL_INTERVAL: 50, |
|
59 |
|
60 /** |
|
61 Timeout (in milliseconds) after which to stop polling when there hasn't been |
|
62 any new activity (keypresses, mouse clicks, etc.) on an element. |
|
63 |
|
64 @property TIMEOUT |
|
65 @type Number |
|
66 @default 10000 |
|
67 @static |
|
68 **/ |
|
69 TIMEOUT: 10000, |
|
70 |
|
71 // -- Protected Static Methods --------------------------------------------- |
|
72 |
|
73 /** |
|
74 Called at an interval to poll for changes to the value of the specified |
|
75 node. |
|
76 |
|
77 @method _poll |
|
78 @param {Node} node Node to poll. |
|
79 |
|
80 @param {Object} options Options object. |
|
81 @param {EventFacade} [options.e] Event facade of the event that |
|
82 initiated the polling. |
|
83 |
|
84 @protected |
|
85 @static |
|
86 **/ |
|
87 _poll: function (node, options) { |
|
88 var domNode = node._node, // performance cheat; getValue() is a big hit when polling |
|
89 event = options.e, |
|
90 vcData = node._data && node._data[DATA_KEY], // another perf cheat |
|
91 stopped = 0, |
|
92 facade, prevVal, newVal, nodeName, selectedOption, stopElement; |
|
93 |
|
94 if (!(domNode && vcData)) { |
|
95 Y.log('_poll: node #' + node.get('id') + ' disappeared; stopping polling and removing all notifiers.', 'warn', 'event-valuechange'); |
|
96 VC._stopPolling(node); |
|
97 return; |
|
98 } |
|
99 |
|
100 prevVal = vcData.prevVal; |
|
101 nodeName = vcData.nodeName; |
|
102 |
|
103 if (vcData.isEditable) { |
|
104 // Use innerHTML for performance |
|
105 newVal = domNode.innerHTML; |
|
106 } else if (nodeName === 'input' || nodeName === 'textarea') { |
|
107 // Use value property for performance |
|
108 newVal = domNode.value; |
|
109 } else if (nodeName === 'select') { |
|
110 // Back-compatibility with IE6 <select> element values. |
|
111 // Huge performance cheat to get past node.get('value'). |
|
112 selectedOption = domNode.options[domNode.selectedIndex]; |
|
113 newVal = selectedOption.value || selectedOption.text; |
|
114 } |
|
115 |
|
116 if (newVal !== prevVal) { |
|
117 vcData.prevVal = newVal; |
|
118 |
|
119 facade = { |
|
120 _event : event, |
|
121 currentTarget: (event && event.currentTarget) || node, |
|
122 newVal : newVal, |
|
123 prevVal : prevVal, |
|
124 target : (event && event.target) || node |
|
125 }; |
|
126 |
|
127 Y.Object.some(vcData.notifiers, function (notifier) { |
|
128 var evt = notifier.handle.evt, |
|
129 newStopped; |
|
130 |
|
131 // support e.stopPropagation() |
|
132 if (stopped !== 1) { |
|
133 notifier.fire(facade); |
|
134 } else if (evt.el === stopElement) { |
|
135 notifier.fire(facade); |
|
136 } |
|
137 |
|
138 newStopped = evt && evt._facade ? evt._facade.stopped : 0; |
|
139 |
|
140 // need to consider the condition in which there are two |
|
141 // listeners on the same element: |
|
142 // listener 1 calls e.stopPropagation() |
|
143 // listener 2 calls e.stopImmediatePropagation() |
|
144 if (newStopped > stopped) { |
|
145 stopped = newStopped; |
|
146 |
|
147 if (stopped === 1) { |
|
148 stopElement = evt.el; |
|
149 } |
|
150 } |
|
151 |
|
152 // support e.stopImmediatePropagation() |
|
153 if (stopped === 2) { |
|
154 return true; |
|
155 } |
|
156 }); |
|
157 |
|
158 VC._refreshTimeout(node); |
|
159 } |
|
160 }, |
|
161 |
|
162 /** |
|
163 Restarts the inactivity timeout for the specified node. |
|
164 |
|
165 @method _refreshTimeout |
|
166 @param {Node} node Node to refresh. |
|
167 @param {SyntheticEvent.Notifier} notifier |
|
168 @protected |
|
169 @static |
|
170 **/ |
|
171 _refreshTimeout: function (node, notifier) { |
|
172 // The node may have been destroyed, so check that it still exists |
|
173 // before trying to get its data. Otherwise an error will occur. |
|
174 if (!node._node) { |
|
175 Y.log('_stopPolling: node disappeared', 'warn', 'event-valuechange'); |
|
176 return; |
|
177 } |
|
178 |
|
179 var vcData = node.getData(DATA_KEY); |
|
180 |
|
181 VC._stopTimeout(node); // avoid dupes |
|
182 |
|
183 // If we don't see any changes within the timeout period (10 seconds by |
|
184 // default), stop polling. |
|
185 vcData.timeout = setTimeout(function () { |
|
186 Y.log('timeout: #' + node.get('id'), 'info', 'event-valuechange'); |
|
187 VC._stopPolling(node, notifier); |
|
188 }, VC.TIMEOUT); |
|
189 |
|
190 Y.log('_refreshTimeout: #' + node.get('id'), 'info', 'event-valuechange'); |
|
191 }, |
|
192 |
|
193 /** |
|
194 Begins polling for changes to the `value` property of the specified node. If |
|
195 polling is already underway for the specified node, it will not be restarted |
|
196 unless the `force` option is `true` |
|
197 |
|
198 @method _startPolling |
|
199 @param {Node} node Node to watch. |
|
200 @param {SyntheticEvent.Notifier} notifier |
|
201 |
|
202 @param {Object} options Options object. |
|
203 @param {EventFacade} [options.e] Event facade of the event that |
|
204 initiated the polling. |
|
205 @param {Boolean} [options.force=false] If `true`, polling will be |
|
206 restarted even if we're already polling this node. |
|
207 |
|
208 @protected |
|
209 @static |
|
210 **/ |
|
211 _startPolling: function (node, notifier, options) { |
|
212 var vcData, isEditable; |
|
213 |
|
214 if (!node.test('input,textarea,select') && !(isEditable = VC._isEditable(node))) { |
|
215 Y.log('_startPolling: aborting poll on #' + node.get('id') + ' -- not a detectable node', 'warn', 'event-valuechange'); |
|
216 return; |
|
217 } |
|
218 |
|
219 vcData = node.getData(DATA_KEY); |
|
220 |
|
221 if (!vcData) { |
|
222 vcData = { |
|
223 nodeName : node.get(NODE_NAME).toLowerCase(), |
|
224 isEditable : isEditable, |
|
225 prevVal : isEditable ? node.getDOMNode().innerHTML : node.get(VALUE) |
|
226 }; |
|
227 |
|
228 node.setData(DATA_KEY, vcData); |
|
229 } |
|
230 |
|
231 vcData.notifiers || (vcData.notifiers = {}); |
|
232 |
|
233 // Don't bother continuing if we're already polling this node, unless |
|
234 // `options.force` is true. |
|
235 if (vcData.interval) { |
|
236 if (options.force) { |
|
237 VC._stopPolling(node, notifier); // restart polling, but avoid dupe polls |
|
238 } else { |
|
239 vcData.notifiers[Y.stamp(notifier)] = notifier; |
|
240 return; |
|
241 } |
|
242 } |
|
243 |
|
244 // Poll for changes to the node's value. We can't rely on keyboard |
|
245 // events for this, since the value may change due to a mouse-initiated |
|
246 // paste event, an IME input event, or for some other reason that |
|
247 // doesn't trigger a key event. |
|
248 vcData.notifiers[Y.stamp(notifier)] = notifier; |
|
249 |
|
250 vcData.interval = setInterval(function () { |
|
251 VC._poll(node, options); |
|
252 }, VC.POLL_INTERVAL); |
|
253 |
|
254 Y.log('_startPolling: #' + node.get('id'), 'info', 'event-valuechange'); |
|
255 |
|
256 VC._refreshTimeout(node, notifier); |
|
257 }, |
|
258 |
|
259 /** |
|
260 Stops polling for changes to the specified node's `value` attribute. |
|
261 |
|
262 @method _stopPolling |
|
263 @param {Node} node Node to stop polling on. |
|
264 @param {SyntheticEvent.Notifier} [notifier] Notifier to remove from the |
|
265 node. If not specified, all notifiers will be removed. |
|
266 @protected |
|
267 @static |
|
268 **/ |
|
269 _stopPolling: function (node, notifier) { |
|
270 // The node may have been destroyed, so check that it still exists |
|
271 // before trying to get its data. Otherwise an error will occur. |
|
272 if (!node._node) { |
|
273 Y.log('_stopPolling: node disappeared', 'info', 'event-valuechange'); |
|
274 return; |
|
275 } |
|
276 |
|
277 var vcData = node.getData(DATA_KEY) || {}; |
|
278 |
|
279 clearInterval(vcData.interval); |
|
280 delete vcData.interval; |
|
281 |
|
282 VC._stopTimeout(node); |
|
283 |
|
284 if (notifier) { |
|
285 vcData.notifiers && delete vcData.notifiers[Y.stamp(notifier)]; |
|
286 } else { |
|
287 vcData.notifiers = {}; |
|
288 } |
|
289 |
|
290 Y.log('_stopPolling: #' + node.get('id'), 'info', 'event-valuechange'); |
|
291 }, |
|
292 |
|
293 /** |
|
294 Clears the inactivity timeout for the specified node, if any. |
|
295 |
|
296 @method _stopTimeout |
|
297 @param {Node} node |
|
298 @protected |
|
299 @static |
|
300 **/ |
|
301 _stopTimeout: function (node) { |
|
302 var vcData = node.getData(DATA_KEY) || {}; |
|
303 |
|
304 clearTimeout(vcData.timeout); |
|
305 delete vcData.timeout; |
|
306 }, |
|
307 |
|
308 /** |
|
309 Check to see if a node has editable content or not. |
|
310 |
|
311 TODO: Add additional checks to get it to work for child nodes |
|
312 that inherit "contenteditable" from parent nodes. This may be |
|
313 too computationally intensive to be placed inside of the `_poll` |
|
314 loop, however. |
|
315 |
|
316 @method _isEditable |
|
317 @param {Node} node |
|
318 @protected |
|
319 @static |
|
320 **/ |
|
321 _isEditable: function (node) { |
|
322 // Performance cheat because this is used inside `_poll` |
|
323 var domNode = node._node; |
|
324 return domNode.contentEditable === 'true' || |
|
325 domNode.contentEditable === ''; |
|
326 }, |
|
327 |
|
328 |
|
329 |
|
330 // -- Protected Static Event Handlers -------------------------------------- |
|
331 |
|
332 /** |
|
333 Stops polling when a node's blur event fires. |
|
334 |
|
335 @method _onBlur |
|
336 @param {EventFacade} e |
|
337 @param {SyntheticEvent.Notifier} notifier |
|
338 @protected |
|
339 @static |
|
340 **/ |
|
341 _onBlur: function (e, notifier) { |
|
342 VC._stopPolling(e.currentTarget, notifier); |
|
343 }, |
|
344 |
|
345 /** |
|
346 Resets a node's history and starts polling when a focus event occurs. |
|
347 |
|
348 @method _onFocus |
|
349 @param {EventFacade} e |
|
350 @param {SyntheticEvent.Notifier} notifier |
|
351 @protected |
|
352 @static |
|
353 **/ |
|
354 _onFocus: function (e, notifier) { |
|
355 var node = e.currentTarget, |
|
356 vcData = node.getData(DATA_KEY); |
|
357 |
|
358 if (!vcData) { |
|
359 vcData = { |
|
360 isEditable : VC._isEditable(node), |
|
361 nodeName : node.get(NODE_NAME).toLowerCase() |
|
362 }; |
|
363 node.setData(DATA_KEY, vcData); |
|
364 } |
|
365 |
|
366 vcData.prevVal = vcData.isEditable ? node.getDOMNode().innerHTML : node.get(VALUE); |
|
367 |
|
368 VC._startPolling(node, notifier, {e: e}); |
|
369 }, |
|
370 |
|
371 /** |
|
372 Starts polling when a node receives a keyDown event. |
|
373 |
|
374 @method _onKeyDown |
|
375 @param {EventFacade} e |
|
376 @param {SyntheticEvent.Notifier} notifier |
|
377 @protected |
|
378 @static |
|
379 **/ |
|
380 _onKeyDown: function (e, notifier) { |
|
381 VC._startPolling(e.currentTarget, notifier, {e: e}); |
|
382 }, |
|
383 |
|
384 /** |
|
385 Starts polling when an IME-related keyUp event occurs on a node. |
|
386 |
|
387 @method _onKeyUp |
|
388 @param {EventFacade} e |
|
389 @param {SyntheticEvent.Notifier} notifier |
|
390 @protected |
|
391 @static |
|
392 **/ |
|
393 _onKeyUp: function (e, notifier) { |
|
394 // These charCodes indicate that an IME has started. We'll restart |
|
395 // polling and give the IME up to 10 seconds (by default) to finish. |
|
396 if (e.charCode === 229 || e.charCode === 197) { |
|
397 VC._startPolling(e.currentTarget, notifier, { |
|
398 e : e, |
|
399 force: true |
|
400 }); |
|
401 } |
|
402 }, |
|
403 |
|
404 /** |
|
405 Starts polling when a node receives a mouseDown event. |
|
406 |
|
407 @method _onMouseDown |
|
408 @param {EventFacade} e |
|
409 @param {SyntheticEvent.Notifier} notifier |
|
410 @protected |
|
411 @static |
|
412 **/ |
|
413 _onMouseDown: function (e, notifier) { |
|
414 VC._startPolling(e.currentTarget, notifier, {e: e}); |
|
415 }, |
|
416 |
|
417 /** |
|
418 Called when the `valuechange` event receives a new subscriber. |
|
419 |
|
420 Child nodes that aren't initially available when this subscription is |
|
421 called will still fire the `valuechange` event after their data is |
|
422 collected when the delegated `focus` event is captured. This includes |
|
423 elements that haven't been inserted into the DOM yet, as well as |
|
424 elements that aren't initially `contenteditable`. |
|
425 |
|
426 @method _onSubscribe |
|
427 @param {Node} node |
|
428 @param {Subscription} sub |
|
429 @param {SyntheticEvent.Notifier} notifier |
|
430 @param {Function|String} [filter] Filter function or selector string. Only |
|
431 provided for delegate subscriptions. |
|
432 @protected |
|
433 @static |
|
434 **/ |
|
435 _onSubscribe: function (node, sub, notifier, filter) { |
|
436 var _valuechange, callbacks, isEditable, inputNodes, editableNodes; |
|
437 |
|
438 callbacks = { |
|
439 blur : VC._onBlur, |
|
440 focus : VC._onFocus, |
|
441 keydown : VC._onKeyDown, |
|
442 keyup : VC._onKeyUp, |
|
443 mousedown: VC._onMouseDown |
|
444 }; |
|
445 |
|
446 // Store a utility object on the notifier to hold stuff that needs to be |
|
447 // passed around to trigger event handlers, polling handlers, etc. |
|
448 _valuechange = notifier._valuechange = {}; |
|
449 |
|
450 if (filter) { |
|
451 // If a filter is provided, then this is a delegated subscription. |
|
452 _valuechange.delegated = true; |
|
453 |
|
454 // Add a function to the notifier that we can use to find all |
|
455 // nodes that pass the delegate filter. |
|
456 _valuechange.getNodes = function () { |
|
457 inputNodes = node.all('input,textarea,select').filter(filter); |
|
458 editableNodes = node.all('[contenteditable="true"],[contenteditable=""]').filter(filter); |
|
459 |
|
460 return inputNodes.concat(editableNodes); |
|
461 }; |
|
462 |
|
463 // Store the initial values for each descendant of the container |
|
464 // node that passes the delegate filter. |
|
465 _valuechange.getNodes().each(function (child) { |
|
466 if (!child.getData(DATA_KEY)) { |
|
467 child.setData(DATA_KEY, { |
|
468 nodeName : child.get(NODE_NAME).toLowerCase(), |
|
469 isEditable : VC._isEditable(child), |
|
470 prevVal : isEditable ? child.getDOMNode().innerHTML : child.get(VALUE) |
|
471 }); |
|
472 } |
|
473 }); |
|
474 |
|
475 notifier._handles = Y.delegate(callbacks, node, filter, null, |
|
476 notifier); |
|
477 } else { |
|
478 isEditable = VC._isEditable(node); |
|
479 // This is a normal (non-delegated) event subscription. |
|
480 if (!node.test('input,textarea,select') && !isEditable) { |
|
481 return; |
|
482 } |
|
483 |
|
484 if (!node.getData(DATA_KEY)) { |
|
485 node.setData(DATA_KEY, { |
|
486 nodeName : node.get(NODE_NAME).toLowerCase(), |
|
487 isEditable : isEditable, |
|
488 prevVal : isEditable ? node.getDOMNode().innerHTML : node.get(VALUE) |
|
489 }); |
|
490 } |
|
491 |
|
492 notifier._handles = node.on(callbacks, null, null, notifier); |
|
493 } |
|
494 }, |
|
495 |
|
496 /** |
|
497 Called when the `valuechange` event loses a subscriber. |
|
498 |
|
499 @method _onUnsubscribe |
|
500 @param {Node} node |
|
501 @param {Subscription} subscription |
|
502 @param {SyntheticEvent.Notifier} notifier |
|
503 @protected |
|
504 @static |
|
505 **/ |
|
506 _onUnsubscribe: function (node, subscription, notifier) { |
|
507 var _valuechange = notifier._valuechange; |
|
508 |
|
509 notifier._handles && notifier._handles.detach(); |
|
510 |
|
511 if (_valuechange.delegated) { |
|
512 _valuechange.getNodes().each(function (child) { |
|
513 VC._stopPolling(child, notifier); |
|
514 }); |
|
515 } else { |
|
516 VC._stopPolling(node, notifier); |
|
517 } |
|
518 } |
|
519 }; |
|
520 |
|
521 /** |
|
522 Synthetic event that fires when the `value` property of an `<input>`, |
|
523 `<textarea>`, `<select>`, or `[contenteditable="true"]` node changes as a |
|
524 result of a user-initiated keystroke, mouse operation, or input method |
|
525 editor (IME) input event. |
|
526 |
|
527 Unlike the `onchange` event, this event fires when the value actually changes |
|
528 and not when the element loses focus. This event also reports IME and |
|
529 multi-stroke input more reliably than `oninput` or the various key events across |
|
530 browsers. |
|
531 |
|
532 For performance reasons, only focused nodes are monitored for changes, so |
|
533 programmatic value changes on nodes that don't have focus won't be detected. |
|
534 |
|
535 @example |
|
536 |
|
537 YUI().use('event-valuechange', function (Y) { |
|
538 Y.one('#my-input').on('valuechange', function (e) { |
|
539 Y.log('previous value: ' + e.prevVal); |
|
540 Y.log('new value: ' + e.newVal); |
|
541 }); |
|
542 }); |
|
543 |
|
544 @event valuechange |
|
545 @param {String} prevVal Previous value prior to the latest change. |
|
546 @param {String} newVal New value after the latest change. |
|
547 @for YUI |
|
548 **/ |
|
549 |
|
550 config = { |
|
551 detach: VC._onUnsubscribe, |
|
552 on : VC._onSubscribe, |
|
553 |
|
554 delegate : VC._onSubscribe, |
|
555 detachDelegate: VC._onUnsubscribe, |
|
556 |
|
557 publishConfig: { |
|
558 emitFacade: true |
|
559 } |
|
560 }; |
|
561 |
|
562 Y.Event.define('valuechange', config); |
|
563 Y.Event.define('valueChange', config); // deprecated, but supported for backcompat |
|
564 |
|
565 Y.ValueChange = VC; |
|
566 |
|
567 |
|
568 }, '@VERSION@', {"requires": ["event-focus", "event-synthetic"]}); |