| // Copyright 2006 The Closure Library Authors. All Rights Reserved. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS-IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| /** |
| * @fileoverview An object that encapsulates text changed events for textareas |
| * and input element of type text and password. The event occurs after the value |
| * has been changed. The event does not occur if value was changed |
| * programmatically.<br> |
| * <br> |
| * Note: this does not guarantee the correctness of {@code keyCode} or |
| * {@code charCode}, or attempt to unify them across browsers. See |
| * {@code goog.events.KeyHandler} for that functionality<br> |
| * <br> |
| * Known issues: |
| * <ul> |
| * <li>Does not trigger for drop events on Opera due to browser bug. |
| * <li>IE doesn't have native support for input event. WebKit before version 531 |
| * doesn't have support for textareas. For those browsers an emulation mode |
| * based on key, clipboard and drop events is used. Thus this event won't |
| * trigger in emulation mode if text was modified by context menu commands |
| * such as 'Undo' and 'Delete'. |
| * </ul> |
| * @author arv@google.com (Erik Arvidsson) |
| * @see ../demos/inputhandler.html |
| */ |
| |
| goog.provide('goog.events.InputHandler'); |
| goog.provide('goog.events.InputHandler.EventType'); |
| |
| goog.require('goog.Timer'); |
| goog.require('goog.dom'); |
| goog.require('goog.events.BrowserEvent'); |
| goog.require('goog.events.EventHandler'); |
| goog.require('goog.events.EventTarget'); |
| goog.require('goog.events.KeyCodes'); |
| goog.require('goog.userAgent'); |
| |
| |
| |
| /** |
| * This event handler will dispatch events when the user types into a text |
| * input, password input or a textarea |
| * @param {Element} element The element that you want to listen for input |
| * events on. |
| * @constructor |
| * @extends {goog.events.EventTarget} |
| */ |
| goog.events.InputHandler = function(element) { |
| goog.events.InputHandler.base(this, 'constructor'); |
| |
| /** |
| * Id of a timer used to postpone firing input event in emulation mode. |
| * @type {?number} |
| * @private |
| */ |
| this.timer_ = null; |
| |
| /** |
| * The element that you want to listen for input events on. |
| * @type {Element} |
| * @private |
| */ |
| this.element_ = element; |
| |
| // Determine whether input event should be emulated. |
| // IE8 doesn't support input events. We could use property change events but |
| // they are broken in many ways: |
| // - Fire even if value was changed programmatically. |
| // - Aren't always delivered. For example, if you change value or even width |
| // of input programmatically, next value change made by user won't fire an |
| // event. |
| // IE9 supports input events when characters are inserted, but not deleted. |
| // WebKit before version 531 did not support input events for textareas. |
| var emulateInputEvents = goog.userAgent.IE || |
| (goog.userAgent.WEBKIT && !goog.userAgent.isVersionOrHigher('531') && |
| element.tagName == 'TEXTAREA'); |
| |
| /** |
| * @type {goog.events.EventHandler<!goog.events.InputHandler>} |
| * @private |
| */ |
| this.eventHandler_ = new goog.events.EventHandler(this); |
| |
| // Even if input event emulation is enabled, still listen for input events |
| // since they may be partially supported by the browser (such as IE9). |
| // If the input event does fire, we will be able to dispatch synchronously. |
| // (InputHandler events being asynchronous for IE is a common issue for |
| // cases like auto-grow textareas where they result in a quick flash of |
| // scrollbars between the textarea content growing and it being resized to |
| // fit.) |
| this.eventHandler_.listen( |
| this.element_, |
| emulateInputEvents ? |
| ['keydown', 'paste', 'cut', 'drop', 'input'] : |
| 'input', |
| this); |
| }; |
| goog.inherits(goog.events.InputHandler, goog.events.EventTarget); |
| |
| |
| /** |
| * Enum type for the events fired by the input handler |
| * @enum {string} |
| */ |
| goog.events.InputHandler.EventType = { |
| INPUT: 'input' |
| }; |
| |
| |
| /** |
| * This handles the underlying events and dispatches a new event as needed. |
| * @param {goog.events.BrowserEvent} e The underlying browser event. |
| */ |
| goog.events.InputHandler.prototype.handleEvent = function(e) { |
| if (e.type == 'input') { |
| // http://stackoverflow.com/questions/18389732/changing-placeholder-triggers-input-event-in-ie-10 |
| // IE 10+ fires an input event when there are inputs with placeholders. |
| // It fires the event with keycode 0, so if we detect it we don't |
| // propagate the input event. |
| if (goog.userAgent.IE && goog.userAgent.isVersionOrHigher(10) && |
| e.keyCode == 0 && e.charCode == 0) { |
| return; |
| } |
| // This event happens after all the other events we listen to, so cancel |
| // an asynchronous event dispatch if we have it queued up. Otherwise, we |
| // will end up firing an extra event. |
| this.cancelTimerIfSet_(); |
| |
| // Unlike other browsers, Opera fires an extra input event when an element |
| // is blurred after the user has input into it. Since Opera doesn't fire |
| // input event on drop, it's enough to check whether element still has focus |
| // to suppress bogus notification. |
| if (!goog.userAgent.OPERA || this.element_ == |
| goog.dom.getOwnerDocument(this.element_).activeElement) { |
| this.dispatchEvent(this.createInputEvent_(e)); |
| } |
| } else { |
| // Filter out key events that don't modify text. |
| if (e.type == 'keydown' && |
| !goog.events.KeyCodes.isTextModifyingKeyEvent(e)) { |
| return; |
| } |
| |
| // It is still possible that pressed key won't modify the value of an |
| // element. Storing old value will help us to detect modification but is |
| // also a little bit dangerous. If value is changed programmatically in |
| // another key down handler, we will detect it as user-initiated change. |
| var valueBeforeKey = e.type == 'keydown' ? this.element_.value : null; |
| |
| // In IE on XP, IME the element's value has already changed when we get |
| // keydown events when the user is using an IME. In this case, we can't |
| // check the current value normally, so we assume that it's a modifying key |
| // event. This means that ENTER when used to commit will fire a spurious |
| // input event, but it's better to have a false positive than let some input |
| // slip through the cracks. |
| if (goog.userAgent.IE && e.keyCode == goog.events.KeyCodes.WIN_IME) { |
| valueBeforeKey = null; |
| } |
| |
| // Create an input event now, because when we fire it on timer, the |
| // underlying event will already be disposed. |
| var inputEvent = this.createInputEvent_(e); |
| |
| // Since key down, paste, cut and drop events are fired before actual value |
| // of the element has changed, we need to postpone dispatching input event |
| // until value is updated. |
| this.cancelTimerIfSet_(); |
| this.timer_ = goog.Timer.callOnce(function() { |
| this.timer_ = null; |
| if (this.element_.value != valueBeforeKey) { |
| this.dispatchEvent(inputEvent); |
| } |
| }, 0, this); |
| } |
| }; |
| |
| |
| /** |
| * Cancels timer if it is set, does nothing otherwise. |
| * @private |
| */ |
| goog.events.InputHandler.prototype.cancelTimerIfSet_ = function() { |
| if (this.timer_ != null) { |
| goog.Timer.clear(this.timer_); |
| this.timer_ = null; |
| } |
| }; |
| |
| |
| /** |
| * Creates an input event from the browser event. |
| * @param {goog.events.BrowserEvent} be A browser event. |
| * @return {!goog.events.BrowserEvent} An input event. |
| * @private |
| */ |
| goog.events.InputHandler.prototype.createInputEvent_ = function(be) { |
| var e = new goog.events.BrowserEvent(be.getBrowserEvent()); |
| e.type = goog.events.InputHandler.EventType.INPUT; |
| return e; |
| }; |
| |
| |
| /** @override */ |
| goog.events.InputHandler.prototype.disposeInternal = function() { |
| goog.events.InputHandler.base(this, 'disposeInternal'); |
| this.eventHandler_.dispose(); |
| this.cancelTimerIfSet_(); |
| delete this.element_; |
| }; |