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