// Copyright 2010 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 Input Method Editors (IMEs) are OS-level widgets that make * it easier to type non-ascii characters on ascii keyboards (in particular, * characters that require more than one keystroke). * * When the user wants to type such a character, a modal menu pops up and * suggests possible "next" characters in the IME character sequence. After * typing N characters, the user hits "enter" to commit the IME to the field. * N differs from language to language. * * This class offers high-level events for how the user is interacting with the * IME in editable regions. * * Known Issues: * * Firefox always fires an extra pair of compositionstart/compositionend events. * We do not normalize for this. * * Opera does not fire any IME events. * * Spurious UPDATE events are common on all browsers. * * We currently do a bad job detecting when the IME closes on IE, and * make a "best effort" guess on when we know it's closed. * * @author nicksantos@google.com (Nick Santos) (Ported to Closure) */ goog.provide('goog.events.ImeHandler'); goog.provide('goog.events.ImeHandler.Event'); goog.provide('goog.events.ImeHandler.EventType'); goog.require('goog.events.Event'); goog.require('goog.events.EventHandler'); goog.require('goog.events.EventTarget'); goog.require('goog.events.EventType'); goog.require('goog.events.KeyCodes'); goog.require('goog.userAgent'); /** * Dispatches high-level events for IMEs. * @param {Element} el The element to listen on. * @extends {goog.events.EventTarget} * @constructor * @final */ goog.events.ImeHandler = function(el) { goog.events.ImeHandler.base(this, 'constructor'); /** * The element to listen on. * @type {Element} * @private */ this.el_ = el; /** * Tracks the keyup event only, because it has a different life-cycle from * other events. * @type {goog.events.EventHandler} * @private */ this.keyUpHandler_ = new goog.events.EventHandler(this); /** * Tracks all the browser events. * @type {goog.events.EventHandler} * @private */ this.handler_ = new goog.events.EventHandler(this); if (goog.events.ImeHandler.USES_COMPOSITION_EVENTS) { this.handler_ .listen( el, goog.events.EventType.COMPOSITIONSTART, this.handleCompositionStart_) .listen( el, goog.events.EventType.COMPOSITIONEND, this.handleCompositionEnd_) .listen( el, goog.events.EventType.COMPOSITIONUPDATE, this.handleTextModifyingInput_); } this.handler_ .listen(el, goog.events.EventType.TEXTINPUT, this.handleTextInput_) .listen(el, goog.events.EventType.TEXT, this.handleTextModifyingInput_) .listen(el, goog.events.EventType.KEYDOWN, this.handleKeyDown_); }; goog.inherits(goog.events.ImeHandler, goog.events.EventTarget); /** * Event types fired by ImeHandler. These events do not make any guarantees * about whether they were fired before or after the event in question. * @enum {string} */ goog.events.ImeHandler.EventType = { // After the IME opens. START: 'startIme', // An update to the state of the IME. An 'update' does not necessarily mean // that the text contents of the field were modified in any way. UPDATE: 'updateIme', // After the IME closes. END: 'endIme' }; /** * An event fired by ImeHandler. * @param {goog.events.ImeHandler.EventType} type The type. * @param {goog.events.BrowserEvent} reason The trigger for this event. * @constructor * @extends {goog.events.Event} * @final */ goog.events.ImeHandler.Event = function(type, reason) { goog.events.ImeHandler.Event.base(this, 'constructor', type); /** * The event that triggered this. * @type {goog.events.BrowserEvent} */ this.reason = reason; }; goog.inherits(goog.events.ImeHandler.Event, goog.events.Event); /** * Whether to use the composition events. * @type {boolean} */ goog.events.ImeHandler.USES_COMPOSITION_EVENTS = goog.userAgent.GECKO || (goog.userAgent.WEBKIT && goog.userAgent.isVersionOrHigher(532)); /** * Stores whether IME mode is active. * @type {boolean} * @private */ goog.events.ImeHandler.prototype.imeMode_ = false; /** * The keyCode value of the last keyDown event. This value is used for * identiying whether or not a textInput event is sent by an IME. * @type {number} * @private */ goog.events.ImeHandler.prototype.lastKeyCode_ = 0; /** * @return {boolean} Whether an IME is active. */ goog.events.ImeHandler.prototype.isImeMode = function() { return this.imeMode_; }; /** * Handles the compositionstart event. * @param {goog.events.BrowserEvent} e The event. * @private */ goog.events.ImeHandler.prototype.handleCompositionStart_ = function(e) { this.handleImeActivate_(e); }; /** * Handles the compositionend event. * @param {goog.events.BrowserEvent} e The event. * @private */ goog.events.ImeHandler.prototype.handleCompositionEnd_ = function(e) { this.handleImeDeactivate_(e); }; /** * Handles the compositionupdate and text events. * @param {goog.events.BrowserEvent} e The event. * @private */ goog.events.ImeHandler.prototype.handleTextModifyingInput_ = function(e) { if (this.isImeMode()) { this.processImeComposition_(e); } }; /** * Handles IME activation. * @param {goog.events.BrowserEvent} e The event. * @private */ goog.events.ImeHandler.prototype.handleImeActivate_ = function(e) { if (this.imeMode_) { return; } // Listens for keyup events to handle unexpected IME keydown events on older // versions of webkit. // // In those versions, we currently use textInput events deactivate IME // (see handleTextInput_() for the reason). However, // Safari fires a keydown event (as a result of pressing keys to commit IME // text) with keyCode == WIN_IME after textInput event. This activates IME // mode again unnecessarily. To prevent this problem, listens keyup events // which can use to determine whether IME text has been committed. if (goog.userAgent.WEBKIT && !goog.events.ImeHandler.USES_COMPOSITION_EVENTS) { this.keyUpHandler_.listen( this.el_, goog.events.EventType.KEYUP, this.handleKeyUpSafari4_); } this.imeMode_ = true; this.dispatchEvent( new goog.events.ImeHandler.Event( goog.events.ImeHandler.EventType.START, e)); }; /** * Handles the IME compose changes. * @param {goog.events.BrowserEvent} e The event. * @private */ goog.events.ImeHandler.prototype.processImeComposition_ = function(e) { this.dispatchEvent( new goog.events.ImeHandler.Event( goog.events.ImeHandler.EventType.UPDATE, e)); }; /** * Handles IME deactivation. * @param {goog.events.BrowserEvent} e The event. * @private */ goog.events.ImeHandler.prototype.handleImeDeactivate_ = function(e) { this.imeMode_ = false; this.keyUpHandler_.removeAll(); this.dispatchEvent( new goog.events.ImeHandler.Event( goog.events.ImeHandler.EventType.END, e)); }; /** * Handles a key down event. * @param {!goog.events.BrowserEvent} e The event. * @private */ goog.events.ImeHandler.prototype.handleKeyDown_ = function(e) { // Firefox and Chrome have a separate event for IME composition ('text' // and 'compositionupdate', respectively), other browsers do not. if (!goog.events.ImeHandler.USES_COMPOSITION_EVENTS) { var imeMode = this.isImeMode(); // If we're in IE and we detect an IME input on keyDown then activate // the IME, otherwise if the imeMode was previously active, deactivate. if (!imeMode && e.keyCode == goog.events.KeyCodes.WIN_IME) { this.handleImeActivate_(e); } else if (imeMode && e.keyCode != goog.events.KeyCodes.WIN_IME) { if (goog.events.ImeHandler.isImeDeactivateKeyEvent_(e)) { this.handleImeDeactivate_(e); } } else if (imeMode) { this.processImeComposition_(e); } } // Safari on Mac doesn't send IME events in the right order so that we must // ignore some modifier key events to insert IME text correctly. if (goog.events.ImeHandler.isImeDeactivateKeyEvent_(e)) { this.lastKeyCode_ = e.keyCode; } }; /** * Handles a textInput event. * @param {!goog.events.BrowserEvent} e The event. * @private */ goog.events.ImeHandler.prototype.handleTextInput_ = function(e) { // Some WebKit-based browsers including Safari 4 don't send composition // events. So, we turn down IME mode when it's still there. if (!goog.events.ImeHandler.USES_COMPOSITION_EVENTS && goog.userAgent.WEBKIT && this.lastKeyCode_ == goog.events.KeyCodes.WIN_IME && this.isImeMode()) { this.handleImeDeactivate_(e); } }; /** * Handles the key up event for any IME activity. This handler is just used to * prevent activating IME unnecessary in Safari at this time. * @param {!goog.events.BrowserEvent} e The event. * @private */ goog.events.ImeHandler.prototype.handleKeyUpSafari4_ = function(e) { if (this.isImeMode()) { switch (e.keyCode) { // These keyup events indicates that IME text has been committed or // cancelled. We should turn off IME mode when these keyup events // received. case goog.events.KeyCodes.ENTER: case goog.events.KeyCodes.TAB: case goog.events.KeyCodes.ESC: this.handleImeDeactivate_(e); break; } } }; /** * Returns whether the given event should be treated as an IME * deactivation trigger. * @param {!goog.events.Event} e The event. * @return {boolean} Whether the given event is an IME deactivate trigger. * @private */ goog.events.ImeHandler.isImeDeactivateKeyEvent_ = function(e) { // Which key events involve IME deactivation depends on the user's // environment (i.e. browsers, platforms, and IMEs). Usually Shift key // and Ctrl key does not involve IME deactivation, so we currently assume // that these keys are not IME deactivation trigger. switch (e.keyCode) { case goog.events.KeyCodes.SHIFT: case goog.events.KeyCodes.CTRL: return false; default: return true; } }; /** @override */ goog.events.ImeHandler.prototype.disposeInternal = function() { this.handler_.dispose(); this.keyUpHandler_.dispose(); this.el_ = null; goog.events.ImeHandler.base(this, 'disposeInternal'); };