API Docs for: 3.8.0
Show:

File: event-valuechange/js/event-valuechange.js

  1. /**
  2. Adds a synthetic `valuechange` event that fires when the `value` property of an
  3. `<input>` or `<textarea>` node changes as a result of a keystroke, mouse
  4. operation, or input method editor (IME) input event.

  5. Usage:

  6.     YUI().use('event-valuechange', function (Y) {
  7.         Y.one('#my-input').on('valuechange', function (e) {
  8.             Y.log('previous value: ' + e.prevVal);
  9.             Y.log('new value: ' + e.newVal);
  10.         });
  11.     });

  12. @module event-valuechange
  13. **/

  14. /**
  15. Provides the implementation for the synthetic `valuechange` event. This class
  16. isn't meant to be used directly, but is public to make monkeypatching possible.

  17. 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.     });

  24. @class ValueChange
  25. @static
  26. */

  27. var DATA_KEY = '_valuechange',
  28.     VALUE    = 'value',

  29.     config, // defined at the end of this file

  30. // Just a simple namespace to make methods overridable.
  31. VC = {
  32.     // -- Static Constants -----------------------------------------------------

  33.     /**
  34.     Interval (in milliseconds) at which to poll for changes to the value of an
  35.     element with one or more `valuechange` subscribers when the user is likely
  36.     to be interacting with it.

  37.     @property POLL_INTERVAL
  38.     @type Number
  39.     @default 50
  40.     @static
  41.     **/
  42.     POLL_INTERVAL: 50,

  43.     /**
  44.     Timeout (in milliseconds) after which to stop polling when there hasn't been
  45.     any new activity (keypresses, mouse clicks, etc.) on an element.

  46.     @property TIMEOUT
  47.     @type Number
  48.     @default 10000
  49.     @static
  50.     **/
  51.     TIMEOUT: 10000,

  52.     // -- Protected Static Methods ---------------------------------------------

  53.     /**
  54.     Called at an interval to poll for changes to the value of the specified
  55.     node.

  56.     @method _poll
  57.     @param {Node} node Node to poll.

  58.     @param {Object} options Options object.
  59.         @param {EventFacade} [options.e] Event facade of the event that
  60.             initiated the polling.

  61.     @protected
  62.     @static
  63.     **/
  64.     _poll: function (node, options) {
  65.         var domNode = node._node, // performance cheat; getValue() is a big hit when polling
  66.             event   = options.e,
  67.             newVal  = domNode && domNode.value,
  68.             vcData  = node._data && node._data[DATA_KEY], // another perf cheat
  69.             facade, prevVal;

  70.         if (!domNode || !vcData) {
  71.             Y.log('_poll: node #' + node.get('id') + ' disappeared; stopping polling and removing all notifiers.', 'warn', 'event-valuechange');
  72.             VC._stopPolling(node);
  73.             return;
  74.         }

  75.         prevVal = vcData.prevVal;

  76.         if (newVal !== prevVal) {
  77.             vcData.prevVal = newVal;

  78.             facade = {
  79.                 _event       : event,
  80.                 currentTarget: (event && event.currentTarget) || node,
  81.                 newVal       : newVal,
  82.                 prevVal      : prevVal,
  83.                 target       : (event && event.target) || node
  84.             };

  85.             Y.Object.each(vcData.notifiers, function (notifier) {
  86.                 notifier.fire(facade);
  87.             });

  88.             VC._refreshTimeout(node);
  89.         }
  90.     },

  91.     /**
  92.     Restarts the inactivity timeout for the specified node.

  93.     @method _refreshTimeout
  94.     @param {Node} node Node to refresh.
  95.     @param {SyntheticEvent.Notifier} notifier
  96.     @protected
  97.     @static
  98.     **/
  99.     _refreshTimeout: function (node, notifier) {
  100.         // The node may have been destroyed, so check that it still exists
  101.         // before trying to get its data. Otherwise an error will occur.
  102.         if (!node._node) {
  103.             Y.log('_stopPolling: node disappeared', 'warn', 'event-valuechange');
  104.             return;
  105.         }

  106.         var vcData = node.getData(DATA_KEY);

  107.         VC._stopTimeout(node); // avoid dupes

  108.         // If we don't see any changes within the timeout period (10 seconds by
  109.         // default), stop polling.
  110.         vcData.timeout = setTimeout(function () {
  111.             Y.log('timeout: #' + node.get('id'), 'info', 'event-valuechange');
  112.             VC._stopPolling(node, notifier);
  113.         }, VC.TIMEOUT);

  114.         Y.log('_refreshTimeout: #' + node.get('id'), 'info', 'event-valuechange');
  115.     },

  116.     /**
  117.     Begins polling for changes to the `value` property of the specified node. If
  118.     polling is already underway for the specified node, it will not be restarted
  119.     unless the `force` option is `true`

  120.     @method _startPolling
  121.     @param {Node} node Node to watch.
  122.     @param {SyntheticEvent.Notifier} notifier

  123.     @param {Object} options Options object.
  124.         @param {EventFacade} [options.e] Event facade of the event that
  125.             initiated the polling.
  126.         @param {Boolean} [options.force=false] If `true`, polling will be
  127.             restarted even if we're already polling this node.

  128.     @protected
  129.     @static
  130.     **/
  131.     _startPolling: function (node, notifier, options) {
  132.         if (!node.test('input,textarea')) {
  133.             Y.log('_startPolling: aborting poll on #' + node.get('id') + ' -- not an input or textarea', 'warn', 'event-valuechange');
  134.             return;
  135.         }

  136.         var vcData = node.getData(DATA_KEY);

  137.         if (!vcData) {
  138.             vcData = {prevVal: node.get(VALUE)};
  139.             node.setData(DATA_KEY, vcData);
  140.         }

  141.         vcData.notifiers || (vcData.notifiers = {});

  142.         // Don't bother continuing if we're already polling this node, unless
  143.         // `options.force` is true.
  144.         if (vcData.interval) {
  145.             if (options.force) {
  146.                 VC._stopPolling(node, notifier); // restart polling, but avoid dupe polls
  147.             } else {
  148.                 vcData.notifiers[Y.stamp(notifier)] = notifier;
  149.                 return;
  150.             }
  151.         }

  152.         // Poll for changes to the node's value. We can't rely on keyboard
  153.         // events for this, since the value may change due to a mouse-initiated
  154.         // paste event, an IME input event, or for some other reason that
  155.         // doesn't trigger a key event.
  156.         vcData.notifiers[Y.stamp(notifier)] = notifier;

  157.         vcData.interval = setInterval(function () {
  158.             VC._poll(node, vcData, options);
  159.         }, VC.POLL_INTERVAL);

  160.         Y.log('_startPolling: #' + node.get('id'), 'info', 'event-valuechange');

  161.         VC._refreshTimeout(node, notifier);
  162.     },

  163.     /**
  164.     Stops polling for changes to the specified node's `value` attribute.

  165.     @method _stopPolling
  166.     @param {Node} node Node to stop polling on.
  167.     @param {SyntheticEvent.Notifier} [notifier] Notifier to remove from the
  168.         node. If not specified, all notifiers will be removed.
  169.     @protected
  170.     @static
  171.     **/
  172.     _stopPolling: function (node, notifier) {
  173.         // The node may have been destroyed, so check that it still exists
  174.         // before trying to get its data. Otherwise an error will occur.
  175.         if (!node._node) {
  176.             Y.log('_stopPolling: node disappeared', 'info', 'event-valuechange');
  177.             return;
  178.         }

  179.         var vcData = node.getData(DATA_KEY) || {};

  180.         clearInterval(vcData.interval);
  181.         delete vcData.interval;

  182.         VC._stopTimeout(node);

  183.         if (notifier) {
  184.             vcData.notifiers && delete vcData.notifiers[Y.stamp(notifier)];
  185.         } else {
  186.             vcData.notifiers = {};
  187.         }

  188.         Y.log('_stopPolling: #' + node.get('id'), 'info', 'event-valuechange');
  189.     },

  190.     /**
  191.     Clears the inactivity timeout for the specified node, if any.

  192.     @method _stopTimeout
  193.     @param {Node} node
  194.     @protected
  195.     @static
  196.     **/
  197.     _stopTimeout: function (node) {
  198.         var vcData = node.getData(DATA_KEY) || {};

  199.         clearTimeout(vcData.timeout);
  200.         delete vcData.timeout;
  201.     },

  202.     // -- Protected Static Event Handlers --------------------------------------

  203.     /**
  204.     Stops polling when a node's blur event fires.

  205.     @method _onBlur
  206.     @param {EventFacade} e
  207.     @param {SyntheticEvent.Notifier} notifier
  208.     @protected
  209.     @static
  210.     **/
  211.     _onBlur: function (e, notifier) {
  212.         VC._stopPolling(e.currentTarget, notifier);
  213.     },

  214.     /**
  215.     Resets a node's history and starts polling when a focus event occurs.

  216.     @method _onFocus
  217.     @param {EventFacade} e
  218.     @param {SyntheticEvent.Notifier} notifier
  219.     @protected
  220.     @static
  221.     **/
  222.     _onFocus: function (e, notifier) {
  223.         var node   = e.currentTarget,
  224.             vcData = node.getData(DATA_KEY);

  225.         if (!vcData) {
  226.             vcData = {};
  227.             node.setData(DATA_KEY, vcData);
  228.         }

  229.         vcData.prevVal = node.get(VALUE);

  230.         VC._startPolling(node, notifier, {e: e});
  231.     },

  232.     /**
  233.     Starts polling when a node receives a keyDown event.

  234.     @method _onKeyDown
  235.     @param {EventFacade} e
  236.     @param {SyntheticEvent.Notifier} notifier
  237.     @protected
  238.     @static
  239.     **/
  240.     _onKeyDown: function (e, notifier) {
  241.         VC._startPolling(e.currentTarget, notifier, {e: e});
  242.     },

  243.     /**
  244.     Starts polling when an IME-related keyUp event occurs on a node.

  245.     @method _onKeyUp
  246.     @param {EventFacade} e
  247.     @param {SyntheticEvent.Notifier} notifier
  248.     @protected
  249.     @static
  250.     **/
  251.     _onKeyUp: function (e, notifier) {
  252.         // These charCodes indicate that an IME has started. We'll restart
  253.         // polling and give the IME up to 10 seconds (by default) to finish.
  254.         if (e.charCode === 229 || e.charCode === 197) {
  255.             VC._startPolling(e.currentTarget, notifier, {
  256.                 e    : e,
  257.                 force: true
  258.             });
  259.         }
  260.     },

  261.     /**
  262.     Starts polling when a node receives a mouseDown event.

  263.     @method _onMouseDown
  264.     @param {EventFacade} e
  265.     @param {SyntheticEvent.Notifier} notifier
  266.     @protected
  267.     @static
  268.     **/
  269.     _onMouseDown: function (e, notifier) {
  270.         VC._startPolling(e.currentTarget, notifier, {e: e});
  271.     },

  272.     /**
  273.     Called when the `valuechange` event receives a new subscriber.

  274.     @method _onSubscribe
  275.     @param {Node} node
  276.     @param {Subscription} sub
  277.     @param {SyntheticEvent.Notifier} notifier
  278.     @param {Function|String} [filter] Filter function or selector string. Only
  279.         provided for delegate subscriptions.
  280.     @protected
  281.     @static
  282.     **/
  283.     _onSubscribe: function (node, sub, notifier, filter) {
  284.         var _valuechange, callbacks, nodes;

  285.         callbacks = {
  286.             blur     : VC._onBlur,
  287.             focus    : VC._onFocus,
  288.             keydown  : VC._onKeyDown,
  289.             keyup    : VC._onKeyUp,
  290.             mousedown: VC._onMouseDown
  291.         };

  292.         // Store a utility object on the notifier to hold stuff that needs to be
  293.         // passed around to trigger event handlers, polling handlers, etc.
  294.         _valuechange = notifier._valuechange = {};

  295.         if (filter) {
  296.             // If a filter is provided, then this is a delegated subscription.
  297.             _valuechange.delegated = true;

  298.             // Add a function to the notifier that we can use to find all
  299.             // nodes that pass the delegate filter.
  300.             _valuechange.getNodes = function () {
  301.                 return node.all('input,textarea').filter(filter);
  302.             };

  303.             // Store the initial values for each descendant of the container
  304.             // node that passes the delegate filter.
  305.             _valuechange.getNodes().each(function (child) {
  306.                 if (!child.getData(DATA_KEY)) {
  307.                     child.setData(DATA_KEY, {prevVal: child.get(VALUE)});
  308.                 }
  309.             });

  310.             notifier._handles = Y.delegate(callbacks, node, filter, null,
  311.                 notifier);
  312.         } else {
  313.             // This is a normal (non-delegated) event subscription.

  314.             if (!node.test('input,textarea')) {
  315.                 return;
  316.             }

  317.             if (!node.getData(DATA_KEY)) {
  318.                 node.setData(DATA_KEY, {prevVal: node.get(VALUE)});
  319.             }

  320.             notifier._handles = node.on(callbacks, null, null, notifier);
  321.         }
  322.     },

  323.     /**
  324.     Called when the `valuechange` event loses a subscriber.

  325.     @method _onUnsubscribe
  326.     @param {Node} node
  327.     @param {Subscription} subscription
  328.     @param {SyntheticEvent.Notifier} notifier
  329.     @protected
  330.     @static
  331.     **/
  332.     _onUnsubscribe: function (node, subscription, notifier) {
  333.         var _valuechange = notifier._valuechange;

  334.         notifier._handles && notifier._handles.detach();

  335.         if (_valuechange.delegated) {
  336.             _valuechange.getNodes().each(function (child) {
  337.                 VC._stopPolling(child, notifier);
  338.             });
  339.         } else {
  340.             VC._stopPolling(node, notifier);
  341.         }
  342.     }
  343. };

  344. /**
  345. Synthetic event that fires when the `value` property of an `<input>` or
  346. `<textarea>` node changes as a result of a user-initiated keystroke, mouse
  347. operation, or input method editor (IME) input event.

  348. Unlike the `onchange` event, this event fires when the value actually changes
  349. and not when the element loses focus. This event also reports IME and
  350. multi-stroke input more reliably than `oninput` or the various key events across
  351. browsers.

  352. For performance reasons, only focused nodes are monitored for changes, so
  353. programmatic value changes on nodes that don't have focus won't be detected.

  354. @example

  355.     YUI().use('event-valuechange', function (Y) {
  356.         Y.one('#my-input').on('valuechange', function (e) {
  357.             Y.log('previous value: ' + e.prevVal);
  358.             Y.log('new value: ' + e.newVal);
  359.         });
  360.     });

  361. @event valuechange
  362. @param {String} prevVal Previous value prior to the latest change.
  363. @param {String} newVal New value after the latest change.
  364. @for YUI
  365. **/

  366. config = {
  367.     detach: VC._onUnsubscribe,
  368.     on    : VC._onSubscribe,

  369.     delegate      : VC._onSubscribe,
  370.     detachDelegate: VC._onUnsubscribe,

  371.     publishConfig: {
  372.         emitFacade: true
  373.     }
  374. };

  375. Y.Event.define('valuechange', config);
  376. Y.Event.define('valueChange', config); // deprecated, but supported for backcompat

  377. Y.ValueChange = VC;

  378.