2 YUI 3.13.0 (build 508226d)
3 Copyright 2013 Yahoo! Inc. All rights reserved.
4 Licensed under the BSD License.
5 http://yuilibrary.com/license/
8 YUI.add('event-valuechange', function (Y, NAME) {
11 Adds a synthetic `valuechange` event that fires when the `value` property of an
12 `<input>`, `<textarea>`, `<select>`, or `[contenteditable="true"]` node changes
13 as a result of a keystroke, mouse operation, or input method editor (IME)
18 YUI().use('event-valuechange', function (Y) {
19 Y.one('#my-input').on('valuechange', function (e) {
20 Y.log('previous value: ' + e.prevVal);
21 Y.log('new value: ' + e.newVal);
25 @module event-valuechange
29 Provides the implementation for the synthetic `valuechange` event. This class
30 isn't meant to be used directly, but is public to make monkeypatching possible.
34 YUI().use('event-valuechange', function (Y) {
35 Y.one('#my-input').on('valuechange', function (e) {
36 Y.log('previous value: ' + e.prevVal);
37 Y.log('new value: ' + e.newVal);
45 var DATA_KEY = '_valuechange',
47 NODE_NAME = 'nodeName',
49 config, // defined at the end of this file
51 // Just a simple namespace to make methods overridable.
53 // -- Static Constants -----------------------------------------------------
56 Interval (in milliseconds) at which to poll for changes to the value of an
57 element with one or more `valuechange` subscribers when the user is likely
58 to be interacting with it.
60 @property POLL_INTERVAL
68 Timeout (in milliseconds) after which to stop polling when there hasn't been
69 any new activity (keypresses, mouse clicks, etc.) on an element.
78 // -- Protected Static Methods ---------------------------------------------
81 Called at an interval to poll for changes to the value of the specified
85 @param {Node} node Node to poll.
87 @param {Object} options Options object.
88 @param {EventFacade} [options.e] Event facade of the event that
89 initiated the polling.
94 _poll: function (node, options) {
95 var domNode = node._node, // performance cheat; getValue() is a big hit when polling
97 vcData = node._data && node._data[DATA_KEY], // another perf cheat
99 facade, prevVal, newVal, nodeName, selectedOption, stopElement;
101 if (!(domNode && vcData)) {
102 Y.log('_poll: node #' + node.get('id') + ' disappeared; stopping polling and removing all notifiers.', 'warn', 'event-valuechange');
103 VC._stopPolling(node);
107 prevVal = vcData.prevVal;
108 nodeName = vcData.nodeName;
110 if (vcData.isEditable) {
111 // Use innerHTML for performance
112 newVal = domNode.innerHTML;
113 } else if (nodeName === 'input' || nodeName === 'textarea') {
114 // Use value property for performance
115 newVal = domNode.value;
116 } else if (nodeName === 'select') {
117 // Back-compatibility with IE6 <select> element values.
118 // Huge performance cheat to get past node.get('value').
119 selectedOption = domNode.options[domNode.selectedIndex];
120 newVal = selectedOption.value || selectedOption.text;
123 if (newVal !== prevVal) {
124 vcData.prevVal = newVal;
128 currentTarget: (event && event.currentTarget) || node,
131 target : (event && event.target) || node
134 Y.Object.some(vcData.notifiers, function (notifier) {
135 var evt = notifier.handle.evt,
138 // support e.stopPropagation()
140 notifier.fire(facade);
141 } else if (evt.el === stopElement) {
142 notifier.fire(facade);
145 newStopped = evt && evt._facade ? evt._facade.stopped : 0;
147 // need to consider the condition in which there are two
148 // listeners on the same element:
149 // listener 1 calls e.stopPropagation()
150 // listener 2 calls e.stopImmediatePropagation()
151 if (newStopped > stopped) {
152 stopped = newStopped;
155 stopElement = evt.el;
159 // support e.stopImmediatePropagation()
165 VC._refreshTimeout(node);
170 Restarts the inactivity timeout for the specified node.
172 @method _refreshTimeout
173 @param {Node} node Node to refresh.
174 @param {SyntheticEvent.Notifier} notifier
178 _refreshTimeout: function (node, notifier) {
179 // The node may have been destroyed, so check that it still exists
180 // before trying to get its data. Otherwise an error will occur.
182 Y.log('_stopPolling: node disappeared', 'warn', 'event-valuechange');
186 var vcData = node.getData(DATA_KEY);
188 VC._stopTimeout(node); // avoid dupes
190 // If we don't see any changes within the timeout period (10 seconds by
191 // default), stop polling.
192 vcData.timeout = setTimeout(function () {
193 Y.log('timeout: #' + node.get('id'), 'info', 'event-valuechange');
194 VC._stopPolling(node, notifier);
197 Y.log('_refreshTimeout: #' + node.get('id'), 'info', 'event-valuechange');
201 Begins polling for changes to the `value` property of the specified node. If
202 polling is already underway for the specified node, it will not be restarted
203 unless the `force` option is `true`
205 @method _startPolling
206 @param {Node} node Node to watch.
207 @param {SyntheticEvent.Notifier} notifier
209 @param {Object} options Options object.
210 @param {EventFacade} [options.e] Event facade of the event that
211 initiated the polling.
212 @param {Boolean} [options.force=false] If `true`, polling will be
213 restarted even if we're already polling this node.
218 _startPolling: function (node, notifier, options) {
219 var vcData, isEditable;
221 if (!node.test('input,textarea,select') && !(isEditable = VC._isEditable(node))) {
222 Y.log('_startPolling: aborting poll on #' + node.get('id') + ' -- not a detectable node', 'warn', 'event-valuechange');
226 vcData = node.getData(DATA_KEY);
230 nodeName : node.get(NODE_NAME).toLowerCase(),
231 isEditable : isEditable,
232 prevVal : isEditable ? node.getDOMNode().innerHTML : node.get(VALUE)
235 node.setData(DATA_KEY, vcData);
238 vcData.notifiers || (vcData.notifiers = {});
240 // Don't bother continuing if we're already polling this node, unless
241 // `options.force` is true.
242 if (vcData.interval) {
244 VC._stopPolling(node, notifier); // restart polling, but avoid dupe polls
246 vcData.notifiers[Y.stamp(notifier)] = notifier;
251 // Poll for changes to the node's value. We can't rely on keyboard
252 // events for this, since the value may change due to a mouse-initiated
253 // paste event, an IME input event, or for some other reason that
254 // doesn't trigger a key event.
255 vcData.notifiers[Y.stamp(notifier)] = notifier;
257 vcData.interval = setInterval(function () {
258 VC._poll(node, options);
259 }, VC.POLL_INTERVAL);
261 Y.log('_startPolling: #' + node.get('id'), 'info', 'event-valuechange');
263 VC._refreshTimeout(node, notifier);
267 Stops polling for changes to the specified node's `value` attribute.
270 @param {Node} node Node to stop polling on.
271 @param {SyntheticEvent.Notifier} [notifier] Notifier to remove from the
272 node. If not specified, all notifiers will be removed.
276 _stopPolling: function (node, notifier) {
277 // The node may have been destroyed, so check that it still exists
278 // before trying to get its data. Otherwise an error will occur.
280 Y.log('_stopPolling: node disappeared', 'info', 'event-valuechange');
284 var vcData = node.getData(DATA_KEY) || {};
286 clearInterval(vcData.interval);
287 delete vcData.interval;
289 VC._stopTimeout(node);
292 vcData.notifiers && delete vcData.notifiers[Y.stamp(notifier)];
294 vcData.notifiers = {};
297 Y.log('_stopPolling: #' + node.get('id'), 'info', 'event-valuechange');
301 Clears the inactivity timeout for the specified node, if any.
308 _stopTimeout: function (node) {
309 var vcData = node.getData(DATA_KEY) || {};
311 clearTimeout(vcData.timeout);
312 delete vcData.timeout;
316 Check to see if a node has editable content or not.
318 TODO: Add additional checks to get it to work for child nodes
319 that inherit "contenteditable" from parent nodes. This may be
320 too computationally intensive to be placed inside of the `_poll`
328 _isEditable: function (node) {
329 // Performance cheat because this is used inside `_poll`
330 var domNode = node._node;
331 return domNode.contentEditable === 'true' ||
332 domNode.contentEditable === '';
337 // -- Protected Static Event Handlers --------------------------------------
340 Stops polling when a node's blur event fires.
343 @param {EventFacade} e
344 @param {SyntheticEvent.Notifier} notifier
348 _onBlur: function (e, notifier) {
349 VC._stopPolling(e.currentTarget, notifier);
353 Resets a node's history and starts polling when a focus event occurs.
356 @param {EventFacade} e
357 @param {SyntheticEvent.Notifier} notifier
361 _onFocus: function (e, notifier) {
362 var node = e.currentTarget,
363 vcData = node.getData(DATA_KEY);
367 isEditable : VC._isEditable(node),
368 nodeName : node.get(NODE_NAME).toLowerCase()
370 node.setData(DATA_KEY, vcData);
373 vcData.prevVal = vcData.isEditable ? node.getDOMNode().innerHTML : node.get(VALUE);
375 VC._startPolling(node, notifier, {e: e});
379 Starts polling when a node receives a keyDown event.
382 @param {EventFacade} e
383 @param {SyntheticEvent.Notifier} notifier
387 _onKeyDown: function (e, notifier) {
388 VC._startPolling(e.currentTarget, notifier, {e: e});
392 Starts polling when an IME-related keyUp event occurs on a node.
395 @param {EventFacade} e
396 @param {SyntheticEvent.Notifier} notifier
400 _onKeyUp: function (e, notifier) {
401 // These charCodes indicate that an IME has started. We'll restart
402 // polling and give the IME up to 10 seconds (by default) to finish.
403 if (e.charCode === 229 || e.charCode === 197) {
404 VC._startPolling(e.currentTarget, notifier, {
412 Starts polling when a node receives a mouseDown event.
415 @param {EventFacade} e
416 @param {SyntheticEvent.Notifier} notifier
420 _onMouseDown: function (e, notifier) {
421 VC._startPolling(e.currentTarget, notifier, {e: e});
425 Called when the `valuechange` event receives a new subscriber.
427 Child nodes that aren't initially available when this subscription is
428 called will still fire the `valuechange` event after their data is
429 collected when the delegated `focus` event is captured. This includes
430 elements that haven't been inserted into the DOM yet, as well as
431 elements that aren't initially `contenteditable`.
435 @param {Subscription} sub
436 @param {SyntheticEvent.Notifier} notifier
437 @param {Function|String} [filter] Filter function or selector string. Only
438 provided for delegate subscriptions.
442 _onSubscribe: function (node, sub, notifier, filter) {
443 var _valuechange, callbacks, isEditable, inputNodes, editableNodes;
448 keydown : VC._onKeyDown,
450 mousedown: VC._onMouseDown
453 // Store a utility object on the notifier to hold stuff that needs to be
454 // passed around to trigger event handlers, polling handlers, etc.
455 _valuechange = notifier._valuechange = {};
458 // If a filter is provided, then this is a delegated subscription.
459 _valuechange.delegated = true;
461 // Add a function to the notifier that we can use to find all
462 // nodes that pass the delegate filter.
463 _valuechange.getNodes = function () {
464 inputNodes = node.all('input,textarea,select').filter(filter);
465 editableNodes = node.all('[contenteditable="true"],[contenteditable=""]').filter(filter);
467 return inputNodes.concat(editableNodes);
470 // Store the initial values for each descendant of the container
471 // node that passes the delegate filter.
472 _valuechange.getNodes().each(function (child) {
473 if (!child.getData(DATA_KEY)) {
474 child.setData(DATA_KEY, {
475 nodeName : child.get(NODE_NAME).toLowerCase(),
476 isEditable : VC._isEditable(child),
477 prevVal : isEditable ? child.getDOMNode().innerHTML : child.get(VALUE)
482 notifier._handles = Y.delegate(callbacks, node, filter, null,
485 isEditable = VC._isEditable(node);
486 // This is a normal (non-delegated) event subscription.
487 if (!node.test('input,textarea,select') && !isEditable) {
491 if (!node.getData(DATA_KEY)) {
492 node.setData(DATA_KEY, {
493 nodeName : node.get(NODE_NAME).toLowerCase(),
494 isEditable : isEditable,
495 prevVal : isEditable ? node.getDOMNode().innerHTML : node.get(VALUE)
499 notifier._handles = node.on(callbacks, null, null, notifier);
504 Called when the `valuechange` event loses a subscriber.
506 @method _onUnsubscribe
508 @param {Subscription} subscription
509 @param {SyntheticEvent.Notifier} notifier
513 _onUnsubscribe: function (node, subscription, notifier) {
514 var _valuechange = notifier._valuechange;
516 notifier._handles && notifier._handles.detach();
518 if (_valuechange.delegated) {
519 _valuechange.getNodes().each(function (child) {
520 VC._stopPolling(child, notifier);
523 VC._stopPolling(node, notifier);
529 Synthetic event that fires when the `value` property of an `<input>`,
530 `<textarea>`, `<select>`, or `[contenteditable="true"]` node changes as a
531 result of a user-initiated keystroke, mouse operation, or input method
532 editor (IME) input event.
534 Unlike the `onchange` event, this event fires when the value actually changes
535 and not when the element loses focus. This event also reports IME and
536 multi-stroke input more reliably than `oninput` or the various key events across
539 For performance reasons, only focused nodes are monitored for changes, so
540 programmatic value changes on nodes that don't have focus won't be detected.
544 YUI().use('event-valuechange', function (Y) {
545 Y.one('#my-input').on('valuechange', function (e) {
546 Y.log('previous value: ' + e.prevVal);
547 Y.log('new value: ' + e.newVal);
552 @param {String} prevVal Previous value prior to the latest change.
553 @param {String} newVal New value after the latest change.
558 detach: VC._onUnsubscribe,
559 on : VC._onSubscribe,
561 delegate : VC._onSubscribe,
562 detachDelegate: VC._onUnsubscribe,
569 Y.Event.define('valuechange', config);
570 Y.Event.define('valueChange', config); // deprecated, but supported for backcompat
575 }, '3.13.0', {"requires": ["event-focus", "event-synthetic"]});