inputhandler.js 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. // Copyright 2006 The Closure Library Authors. All Rights Reserved.
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS-IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. /**
  15. * @fileoverview An object that encapsulates text changed events for textareas
  16. * and input element of type text and password. The event occurs after the value
  17. * has been changed. The event does not occur if value was changed
  18. * programmatically.<br>
  19. * <br>
  20. * Note: this does not guarantee the correctness of {@code keyCode} or
  21. * {@code charCode}, or attempt to unify them across browsers. See
  22. * {@code goog.events.KeyHandler} for that functionality<br>
  23. * <br>
  24. * Known issues:
  25. * <ul>
  26. * <li>IE doesn't have native support for input event. WebKit before version 531
  27. * doesn't have support for textareas. For those browsers an emulation mode
  28. * based on key, clipboard and drop events is used. Thus this event won't
  29. * trigger in emulation mode if text was modified by context menu commands
  30. * such as 'Undo' and 'Delete'.
  31. * </ul>
  32. * @author arv@google.com (Erik Arvidsson)
  33. * @see ../demos/inputhandler.html
  34. */
  35. goog.provide('goog.events.InputHandler');
  36. goog.provide('goog.events.InputHandler.EventType');
  37. goog.require('goog.Timer');
  38. goog.require('goog.dom.TagName');
  39. goog.require('goog.events.BrowserEvent');
  40. goog.require('goog.events.EventHandler');
  41. goog.require('goog.events.EventTarget');
  42. goog.require('goog.events.KeyCodes');
  43. goog.require('goog.userAgent');
  44. /**
  45. * This event handler will dispatch events when the user types into a text
  46. * input, password input or a textarea
  47. * @param {Element} element The element that you want to listen for input
  48. * events on.
  49. * @constructor
  50. * @extends {goog.events.EventTarget}
  51. */
  52. goog.events.InputHandler = function(element) {
  53. goog.events.InputHandler.base(this, 'constructor');
  54. /**
  55. * Id of a timer used to postpone firing input event in emulation mode.
  56. * @type {?number}
  57. * @private
  58. */
  59. this.timer_ = null;
  60. /**
  61. * The element that you want to listen for input events on.
  62. * @type {Element}
  63. * @private
  64. */
  65. this.element_ = element;
  66. // Determine whether input event should be emulated.
  67. // IE8 doesn't support input events. We could use property change events but
  68. // they are broken in many ways:
  69. // - Fire even if value was changed programmatically.
  70. // - Aren't always delivered. For example, if you change value or even width
  71. // of input programmatically, next value change made by user won't fire an
  72. // event.
  73. // IE9 supports input events when characters are inserted, but not deleted.
  74. // WebKit before version 531 did not support input events for textareas.
  75. var emulateInputEvents = goog.userAgent.IE || goog.userAgent.EDGE ||
  76. (goog.userAgent.WEBKIT && !goog.userAgent.isVersionOrHigher('531') &&
  77. element.tagName == goog.dom.TagName.TEXTAREA);
  78. /**
  79. * @type {goog.events.EventHandler<!goog.events.InputHandler>}
  80. * @private
  81. */
  82. this.eventHandler_ = new goog.events.EventHandler(this);
  83. // Even if input event emulation is enabled, still listen for input events
  84. // since they may be partially supported by the browser (such as IE9).
  85. // If the input event does fire, we will be able to dispatch synchronously.
  86. // (InputHandler events being asynchronous for IE is a common issue for
  87. // cases like auto-grow textareas where they result in a quick flash of
  88. // scrollbars between the textarea content growing and it being resized to
  89. // fit.)
  90. this.eventHandler_.listen(
  91. this.element_,
  92. emulateInputEvents ? ['keydown', 'paste', 'cut', 'drop', 'input'] :
  93. 'input',
  94. this);
  95. };
  96. goog.inherits(goog.events.InputHandler, goog.events.EventTarget);
  97. /**
  98. * Enum type for the events fired by the input handler
  99. * @enum {string}
  100. */
  101. goog.events.InputHandler.EventType = {
  102. INPUT: 'input'
  103. };
  104. /**
  105. * This handles the underlying events and dispatches a new event as needed.
  106. * @param {goog.events.BrowserEvent} e The underlying browser event.
  107. */
  108. goog.events.InputHandler.prototype.handleEvent = function(e) {
  109. if (e.type == 'input') {
  110. // http://stackoverflow.com/questions/18389732/changing-placeholder-triggers-input-event-in-ie-10
  111. // IE 10+ fires an input event when there are inputs with placeholders.
  112. // It fires the event with keycode 0, so if we detect it we don't
  113. // propagate the input event.
  114. if (goog.userAgent.IE && goog.userAgent.isVersionOrHigher(10) &&
  115. e.keyCode == 0 && e.charCode == 0) {
  116. return;
  117. }
  118. // This event happens after all the other events we listen to, so cancel
  119. // an asynchronous event dispatch if we have it queued up. Otherwise, we
  120. // will end up firing an extra event.
  121. this.cancelTimerIfSet_();
  122. this.dispatchEvent(this.createInputEvent_(e));
  123. } else {
  124. // Filter out key events that don't modify text.
  125. if (e.type == 'keydown' &&
  126. !goog.events.KeyCodes.isTextModifyingKeyEvent(e)) {
  127. return;
  128. }
  129. // It is still possible that pressed key won't modify the value of an
  130. // element. Storing old value will help us to detect modification but is
  131. // also a little bit dangerous. If value is changed programmatically in
  132. // another key down handler, we will detect it as user-initiated change.
  133. var valueBeforeKey = e.type == 'keydown' ? this.element_.value : null;
  134. // In IE on XP, IME the element's value has already changed when we get
  135. // keydown events when the user is using an IME. In this case, we can't
  136. // check the current value normally, so we assume that it's a modifying key
  137. // event. This means that ENTER when used to commit will fire a spurious
  138. // input event, but it's better to have a false positive than let some input
  139. // slip through the cracks.
  140. if (goog.userAgent.IE && e.keyCode == goog.events.KeyCodes.WIN_IME) {
  141. valueBeforeKey = null;
  142. }
  143. // Create an input event now, because when we fire it on timer, the
  144. // underlying event will already be disposed.
  145. var inputEvent = this.createInputEvent_(e);
  146. // Since key down, paste, cut and drop events are fired before actual value
  147. // of the element has changed, we need to postpone dispatching input event
  148. // until value is updated.
  149. this.cancelTimerIfSet_();
  150. this.timer_ = goog.Timer.callOnce(function() {
  151. this.timer_ = null;
  152. if (this.element_.value != valueBeforeKey) {
  153. this.dispatchEvent(inputEvent);
  154. }
  155. }, 0, this);
  156. }
  157. };
  158. /**
  159. * Cancels timer if it is set, does nothing otherwise.
  160. * @private
  161. */
  162. goog.events.InputHandler.prototype.cancelTimerIfSet_ = function() {
  163. if (this.timer_ != null) {
  164. goog.Timer.clear(this.timer_);
  165. this.timer_ = null;
  166. }
  167. };
  168. /**
  169. * Creates an input event from the browser event.
  170. * @param {goog.events.BrowserEvent} be A browser event.
  171. * @return {!goog.events.BrowserEvent} An input event.
  172. * @private
  173. */
  174. goog.events.InputHandler.prototype.createInputEvent_ = function(be) {
  175. var e = new goog.events.BrowserEvent(be.getBrowserEvent());
  176. e.type = goog.events.InputHandler.EventType.INPUT;
  177. return e;
  178. };
  179. /** @override */
  180. goog.events.InputHandler.prototype.disposeInternal = function() {
  181. goog.events.InputHandler.base(this, 'disposeInternal');
  182. this.eventHandler_.dispose();
  183. this.cancelTimerIfSet_();
  184. delete this.element_;
  185. };