NOBUG: Fixed file access permissions
[moodle.git] / lib / yuilib / 3.13.0 / event-valuechange / event-valuechange-debug.js
blobbf304641586ee7ad7c1611bd986aedc102f11cb7
1 /*
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/
6 */
8 YUI.add('event-valuechange', function (Y, NAME) {
10 /**
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)
14 input event.
16 Usage:
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);
22         });
23     });
25 @module event-valuechange
26 **/
28 /**
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.
32 Usage:
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);
38         });
39     });
41 @class ValueChange
42 @static
45 var DATA_KEY = '_valuechange',
46     VALUE    = 'value',
47     NODE_NAME = 'nodeName',
49     config, // defined at the end of this file
51 // Just a simple namespace to make methods overridable.
52 VC = {
53     // -- Static Constants -----------------------------------------------------
55     /**
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
61     @type Number
62     @default 50
63     @static
64     **/
65     POLL_INTERVAL: 50,
67     /**
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.
71     @property TIMEOUT
72     @type Number
73     @default 10000
74     @static
75     **/
76     TIMEOUT: 10000,
78     // -- Protected Static Methods ---------------------------------------------
80     /**
81     Called at an interval to poll for changes to the value of the specified
82     node.
84     @method _poll
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.
91     @protected
92     @static
93     **/
94     _poll: function (node, options) {
95         var domNode  = node._node, // performance cheat; getValue() is a big hit when polling
96             event    = options.e,
97             vcData   = node._data && node._data[DATA_KEY], // another perf cheat
98             stopped  = 0,
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);
104             return;
105         }
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;
121         }
123         if (newVal !== prevVal) {
124             vcData.prevVal = newVal;
126             facade = {
127                 _event       : event,
128                 currentTarget: (event && event.currentTarget) || node,
129                 newVal       : newVal,
130                 prevVal      : prevVal,
131                 target       : (event && event.target) || node
132             };
134             Y.Object.some(vcData.notifiers, function (notifier) {
135                 var evt = notifier.handle.evt,
136                     newStopped;
138                 // support e.stopPropagation()
139                 if (stopped !== 1) {
140                     notifier.fire(facade);
141                 } else if (evt.el === stopElement) {
142                     notifier.fire(facade);
143                 }
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;
154                     if (stopped === 1) {
155                         stopElement = evt.el;
156                     }
157                 }
159                 // support e.stopImmediatePropagation()
160                 if (stopped === 2) {
161                     return true;
162                 }
163             });
165             VC._refreshTimeout(node);
166         }
167     },
169     /**
170     Restarts the inactivity timeout for the specified node.
172     @method _refreshTimeout
173     @param {Node} node Node to refresh.
174     @param {SyntheticEvent.Notifier} notifier
175     @protected
176     @static
177     **/
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.
181         if (!node._node) {
182             Y.log('_stopPolling: node disappeared', 'warn', 'event-valuechange');
183             return;
184         }
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);
195         }, VC.TIMEOUT);
197         Y.log('_refreshTimeout: #' + node.get('id'), 'info', 'event-valuechange');
198     },
200     /**
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.
215     @protected
216     @static
217     **/
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');
223             return;
224         }
226         vcData = node.getData(DATA_KEY);
228         if (!vcData) {
229             vcData = {
230                 nodeName   : node.get(NODE_NAME).toLowerCase(),
231                 isEditable : isEditable,
232                 prevVal    : isEditable ? node.getDOMNode().innerHTML : node.get(VALUE)
233             };
235             node.setData(DATA_KEY, vcData);
236         }
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) {
243             if (options.force) {
244                 VC._stopPolling(node, notifier); // restart polling, but avoid dupe polls
245             } else {
246                 vcData.notifiers[Y.stamp(notifier)] = notifier;
247                 return;
248             }
249         }
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);
264     },
266     /**
267     Stops polling for changes to the specified node's `value` attribute.
269     @method _stopPolling
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.
273     @protected
274     @static
275     **/
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.
279         if (!node._node) {
280             Y.log('_stopPolling: node disappeared', 'info', 'event-valuechange');
281             return;
282         }
284         var vcData = node.getData(DATA_KEY) || {};
286         clearInterval(vcData.interval);
287         delete vcData.interval;
289         VC._stopTimeout(node);
291         if (notifier) {
292             vcData.notifiers && delete vcData.notifiers[Y.stamp(notifier)];
293         } else {
294             vcData.notifiers = {};
295         }
297         Y.log('_stopPolling: #' + node.get('id'), 'info', 'event-valuechange');
298     },
300     /**
301     Clears the inactivity timeout for the specified node, if any.
303     @method _stopTimeout
304     @param {Node} node
305     @protected
306     @static
307     **/
308     _stopTimeout: function (node) {
309         var vcData = node.getData(DATA_KEY) || {};
311         clearTimeout(vcData.timeout);
312         delete vcData.timeout;
313     },
315     /**
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`
321     loop, however.
323     @method _isEditable
324     @param {Node} node
325     @protected
326     @static
327     **/
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 === '';
333     },
337     // -- Protected Static Event Handlers --------------------------------------
339     /**
340     Stops polling when a node's blur event fires.
342     @method _onBlur
343     @param {EventFacade} e
344     @param {SyntheticEvent.Notifier} notifier
345     @protected
346     @static
347     **/
348     _onBlur: function (e, notifier) {
349         VC._stopPolling(e.currentTarget, notifier);
350     },
352     /**
353     Resets a node's history and starts polling when a focus event occurs.
355     @method _onFocus
356     @param {EventFacade} e
357     @param {SyntheticEvent.Notifier} notifier
358     @protected
359     @static
360     **/
361     _onFocus: function (e, notifier) {
362         var node       = e.currentTarget,
363             vcData     = node.getData(DATA_KEY);
365         if (!vcData) {
366             vcData = {
367                 isEditable : VC._isEditable(node),
368                 nodeName   : node.get(NODE_NAME).toLowerCase()
369             };
370             node.setData(DATA_KEY, vcData);
371         }
373         vcData.prevVal = vcData.isEditable ? node.getDOMNode().innerHTML : node.get(VALUE);
375         VC._startPolling(node, notifier, {e: e});
376     },
378     /**
379     Starts polling when a node receives a keyDown event.
381     @method _onKeyDown
382     @param {EventFacade} e
383     @param {SyntheticEvent.Notifier} notifier
384     @protected
385     @static
386     **/
387     _onKeyDown: function (e, notifier) {
388         VC._startPolling(e.currentTarget, notifier, {e: e});
389     },
391     /**
392     Starts polling when an IME-related keyUp event occurs on a node.
394     @method _onKeyUp
395     @param {EventFacade} e
396     @param {SyntheticEvent.Notifier} notifier
397     @protected
398     @static
399     **/
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, {
405                 e    : e,
406                 force: true
407             });
408         }
409     },
411     /**
412     Starts polling when a node receives a mouseDown event.
414     @method _onMouseDown
415     @param {EventFacade} e
416     @param {SyntheticEvent.Notifier} notifier
417     @protected
418     @static
419     **/
420     _onMouseDown: function (e, notifier) {
421         VC._startPolling(e.currentTarget, notifier, {e: e});
422     },
424     /**
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`.
433     @method _onSubscribe
434     @param {Node} node
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.
439     @protected
440     @static
441     **/
442     _onSubscribe: function (node, sub, notifier, filter) {
443         var _valuechange, callbacks, isEditable, inputNodes, editableNodes;
445         callbacks = {
446             blur     : VC._onBlur,
447             focus    : VC._onFocus,
448             keydown  : VC._onKeyDown,
449             keyup    : VC._onKeyUp,
450             mousedown: VC._onMouseDown
451         };
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 = {};
457         if (filter) {
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);
468             };
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)
478                     });
479                 }
480             });
482             notifier._handles = Y.delegate(callbacks, node, filter, null,
483                 notifier);
484         } else {
485             isEditable = VC._isEditable(node);
486             // This is a normal (non-delegated) event subscription.
487             if (!node.test('input,textarea,select') && !isEditable) {
488                 return;
489             }
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)
496                 });
497             }
499             notifier._handles = node.on(callbacks, null, null, notifier);
500         }
501     },
503     /**
504     Called when the `valuechange` event loses a subscriber.
506     @method _onUnsubscribe
507     @param {Node} node
508     @param {Subscription} subscription
509     @param {SyntheticEvent.Notifier} notifier
510     @protected
511     @static
512     **/
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);
521             });
522         } else {
523             VC._stopPolling(node, notifier);
524         }
525     }
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
537 browsers.
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.
542 @example
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);
548         });
549     });
551 @event valuechange
552 @param {String} prevVal Previous value prior to the latest change.
553 @param {String} newVal New value after the latest change.
554 @for YUI
557 config = {
558     detach: VC._onUnsubscribe,
559     on    : VC._onSubscribe,
561     delegate      : VC._onSubscribe,
562     detachDelegate: VC._onUnsubscribe,
564     publishConfig: {
565         emitFacade: true
566     }
569 Y.Event.define('valuechange', config);
570 Y.Event.define('valueChange', config); // deprecated, but supported for backcompat
572 Y.ValueChange = VC;
575 }, '3.13.0', {"requires": ["event-focus", "event-synthetic"]});