blob: 661f91f7cb4bfc2321c9504eeedf34f5a98268a9 [file] [log] [blame]
// 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<!goog.events.ImeHandler>}
* @private
*/
this.keyUpHandler_ = new goog.events.EventHandler(this);
/**
* Tracks all the browser events.
* @type {goog.events.EventHandler<!goog.events.ImeHandler>}
* @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');
};