imehandler.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368
  1. // Copyright 2010 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 Input Method Editors (IMEs) are OS-level widgets that make
  16. * it easier to type non-ascii characters on ascii keyboards (in particular,
  17. * characters that require more than one keystroke).
  18. *
  19. * When the user wants to type such a character, a modal menu pops up and
  20. * suggests possible "next" characters in the IME character sequence. After
  21. * typing N characters, the user hits "enter" to commit the IME to the field.
  22. * N differs from language to language.
  23. *
  24. * This class offers high-level events for how the user is interacting with the
  25. * IME in editable regions.
  26. *
  27. * Known Issues:
  28. *
  29. * Firefox always fires an extra pair of compositionstart/compositionend events.
  30. * We do not normalize for this.
  31. *
  32. * Opera does not fire any IME events.
  33. *
  34. * Spurious UPDATE events are common on all browsers.
  35. *
  36. * We currently do a bad job detecting when the IME closes on IE, and
  37. * make a "best effort" guess on when we know it's closed.
  38. *
  39. * @author nicksantos@google.com (Nick Santos) (Ported to Closure)
  40. */
  41. goog.provide('goog.events.ImeHandler');
  42. goog.provide('goog.events.ImeHandler.Event');
  43. goog.provide('goog.events.ImeHandler.EventType');
  44. goog.require('goog.events.Event');
  45. goog.require('goog.events.EventHandler');
  46. goog.require('goog.events.EventTarget');
  47. goog.require('goog.events.EventType');
  48. goog.require('goog.events.KeyCodes');
  49. goog.require('goog.userAgent');
  50. /**
  51. * Dispatches high-level events for IMEs.
  52. * @param {Element} el The element to listen on.
  53. * @extends {goog.events.EventTarget}
  54. * @constructor
  55. * @final
  56. */
  57. goog.events.ImeHandler = function(el) {
  58. goog.events.ImeHandler.base(this, 'constructor');
  59. /**
  60. * The element to listen on.
  61. * @type {Element}
  62. * @private
  63. */
  64. this.el_ = el;
  65. /**
  66. * Tracks the keyup event only, because it has a different life-cycle from
  67. * other events.
  68. * @type {goog.events.EventHandler<!goog.events.ImeHandler>}
  69. * @private
  70. */
  71. this.keyUpHandler_ = new goog.events.EventHandler(this);
  72. /**
  73. * Tracks all the browser events.
  74. * @type {goog.events.EventHandler<!goog.events.ImeHandler>}
  75. * @private
  76. */
  77. this.handler_ = new goog.events.EventHandler(this);
  78. if (goog.events.ImeHandler.USES_COMPOSITION_EVENTS) {
  79. this.handler_
  80. .listen(
  81. el, goog.events.EventType.COMPOSITIONSTART,
  82. this.handleCompositionStart_)
  83. .listen(
  84. el, goog.events.EventType.COMPOSITIONEND,
  85. this.handleCompositionEnd_)
  86. .listen(
  87. el, goog.events.EventType.COMPOSITIONUPDATE,
  88. this.handleTextModifyingInput_);
  89. }
  90. this.handler_
  91. .listen(el, goog.events.EventType.TEXTINPUT, this.handleTextInput_)
  92. .listen(el, goog.events.EventType.TEXT, this.handleTextModifyingInput_)
  93. .listen(el, goog.events.EventType.KEYDOWN, this.handleKeyDown_);
  94. };
  95. goog.inherits(goog.events.ImeHandler, goog.events.EventTarget);
  96. /**
  97. * Event types fired by ImeHandler. These events do not make any guarantees
  98. * about whether they were fired before or after the event in question.
  99. * @enum {string}
  100. */
  101. goog.events.ImeHandler.EventType = {
  102. // After the IME opens.
  103. START: 'startIme',
  104. // An update to the state of the IME. An 'update' does not necessarily mean
  105. // that the text contents of the field were modified in any way.
  106. UPDATE: 'updateIme',
  107. // After the IME closes.
  108. END: 'endIme'
  109. };
  110. /**
  111. * An event fired by ImeHandler.
  112. * @param {goog.events.ImeHandler.EventType} type The type.
  113. * @param {goog.events.BrowserEvent} reason The trigger for this event.
  114. * @constructor
  115. * @extends {goog.events.Event}
  116. * @final
  117. */
  118. goog.events.ImeHandler.Event = function(type, reason) {
  119. goog.events.ImeHandler.Event.base(this, 'constructor', type);
  120. /**
  121. * The event that triggered this.
  122. * @type {goog.events.BrowserEvent}
  123. */
  124. this.reason = reason;
  125. };
  126. goog.inherits(goog.events.ImeHandler.Event, goog.events.Event);
  127. /**
  128. * Whether to use the composition events.
  129. * @type {boolean}
  130. */
  131. goog.events.ImeHandler.USES_COMPOSITION_EVENTS = goog.userAgent.GECKO ||
  132. (goog.userAgent.WEBKIT && goog.userAgent.isVersionOrHigher(532));
  133. /**
  134. * Stores whether IME mode is active.
  135. * @type {boolean}
  136. * @private
  137. */
  138. goog.events.ImeHandler.prototype.imeMode_ = false;
  139. /**
  140. * The keyCode value of the last keyDown event. This value is used for
  141. * identiying whether or not a textInput event is sent by an IME.
  142. * @type {number}
  143. * @private
  144. */
  145. goog.events.ImeHandler.prototype.lastKeyCode_ = 0;
  146. /**
  147. * @return {boolean} Whether an IME is active.
  148. */
  149. goog.events.ImeHandler.prototype.isImeMode = function() {
  150. return this.imeMode_;
  151. };
  152. /**
  153. * Handles the compositionstart event.
  154. * @param {goog.events.BrowserEvent} e The event.
  155. * @private
  156. */
  157. goog.events.ImeHandler.prototype.handleCompositionStart_ = function(e) {
  158. this.handleImeActivate_(e);
  159. };
  160. /**
  161. * Handles the compositionend event.
  162. * @param {goog.events.BrowserEvent} e The event.
  163. * @private
  164. */
  165. goog.events.ImeHandler.prototype.handleCompositionEnd_ = function(e) {
  166. this.handleImeDeactivate_(e);
  167. };
  168. /**
  169. * Handles the compositionupdate and text events.
  170. * @param {goog.events.BrowserEvent} e The event.
  171. * @private
  172. */
  173. goog.events.ImeHandler.prototype.handleTextModifyingInput_ = function(e) {
  174. if (this.isImeMode()) {
  175. this.processImeComposition_(e);
  176. }
  177. };
  178. /**
  179. * Handles IME activation.
  180. * @param {goog.events.BrowserEvent} e The event.
  181. * @private
  182. */
  183. goog.events.ImeHandler.prototype.handleImeActivate_ = function(e) {
  184. if (this.imeMode_) {
  185. return;
  186. }
  187. // Listens for keyup events to handle unexpected IME keydown events on older
  188. // versions of webkit.
  189. //
  190. // In those versions, we currently use textInput events deactivate IME
  191. // (see handleTextInput_() for the reason). However,
  192. // Safari fires a keydown event (as a result of pressing keys to commit IME
  193. // text) with keyCode == WIN_IME after textInput event. This activates IME
  194. // mode again unnecessarily. To prevent this problem, listens keyup events
  195. // which can use to determine whether IME text has been committed.
  196. if (goog.userAgent.WEBKIT &&
  197. !goog.events.ImeHandler.USES_COMPOSITION_EVENTS) {
  198. this.keyUpHandler_.listen(
  199. this.el_, goog.events.EventType.KEYUP, this.handleKeyUpSafari4_);
  200. }
  201. this.imeMode_ = true;
  202. this.dispatchEvent(
  203. new goog.events.ImeHandler.Event(
  204. goog.events.ImeHandler.EventType.START, e));
  205. };
  206. /**
  207. * Handles the IME compose changes.
  208. * @param {goog.events.BrowserEvent} e The event.
  209. * @private
  210. */
  211. goog.events.ImeHandler.prototype.processImeComposition_ = function(e) {
  212. this.dispatchEvent(
  213. new goog.events.ImeHandler.Event(
  214. goog.events.ImeHandler.EventType.UPDATE, e));
  215. };
  216. /**
  217. * Handles IME deactivation.
  218. * @param {goog.events.BrowserEvent} e The event.
  219. * @private
  220. */
  221. goog.events.ImeHandler.prototype.handleImeDeactivate_ = function(e) {
  222. this.imeMode_ = false;
  223. this.keyUpHandler_.removeAll();
  224. this.dispatchEvent(
  225. new goog.events.ImeHandler.Event(
  226. goog.events.ImeHandler.EventType.END, e));
  227. };
  228. /**
  229. * Handles a key down event.
  230. * @param {!goog.events.BrowserEvent} e The event.
  231. * @private
  232. */
  233. goog.events.ImeHandler.prototype.handleKeyDown_ = function(e) {
  234. // Firefox and Chrome have a separate event for IME composition ('text'
  235. // and 'compositionupdate', respectively), other browsers do not.
  236. if (!goog.events.ImeHandler.USES_COMPOSITION_EVENTS) {
  237. var imeMode = this.isImeMode();
  238. // If we're in IE and we detect an IME input on keyDown then activate
  239. // the IME, otherwise if the imeMode was previously active, deactivate.
  240. if (!imeMode && e.keyCode == goog.events.KeyCodes.WIN_IME) {
  241. this.handleImeActivate_(e);
  242. } else if (imeMode && e.keyCode != goog.events.KeyCodes.WIN_IME) {
  243. if (goog.events.ImeHandler.isImeDeactivateKeyEvent_(e)) {
  244. this.handleImeDeactivate_(e);
  245. }
  246. } else if (imeMode) {
  247. this.processImeComposition_(e);
  248. }
  249. }
  250. // Safari on Mac doesn't send IME events in the right order so that we must
  251. // ignore some modifier key events to insert IME text correctly.
  252. if (goog.events.ImeHandler.isImeDeactivateKeyEvent_(e)) {
  253. this.lastKeyCode_ = e.keyCode;
  254. }
  255. };
  256. /**
  257. * Handles a textInput event.
  258. * @param {!goog.events.BrowserEvent} e The event.
  259. * @private
  260. */
  261. goog.events.ImeHandler.prototype.handleTextInput_ = function(e) {
  262. // Some WebKit-based browsers including Safari 4 don't send composition
  263. // events. So, we turn down IME mode when it's still there.
  264. if (!goog.events.ImeHandler.USES_COMPOSITION_EVENTS &&
  265. goog.userAgent.WEBKIT &&
  266. this.lastKeyCode_ == goog.events.KeyCodes.WIN_IME && this.isImeMode()) {
  267. this.handleImeDeactivate_(e);
  268. }
  269. };
  270. /**
  271. * Handles the key up event for any IME activity. This handler is just used to
  272. * prevent activating IME unnecessary in Safari at this time.
  273. * @param {!goog.events.BrowserEvent} e The event.
  274. * @private
  275. */
  276. goog.events.ImeHandler.prototype.handleKeyUpSafari4_ = function(e) {
  277. if (this.isImeMode()) {
  278. switch (e.keyCode) {
  279. // These keyup events indicates that IME text has been committed or
  280. // cancelled. We should turn off IME mode when these keyup events
  281. // received.
  282. case goog.events.KeyCodes.ENTER:
  283. case goog.events.KeyCodes.TAB:
  284. case goog.events.KeyCodes.ESC:
  285. this.handleImeDeactivate_(e);
  286. break;
  287. }
  288. }
  289. };
  290. /**
  291. * Returns whether the given event should be treated as an IME
  292. * deactivation trigger.
  293. * @param {!goog.events.Event} e The event.
  294. * @return {boolean} Whether the given event is an IME deactivate trigger.
  295. * @private
  296. */
  297. goog.events.ImeHandler.isImeDeactivateKeyEvent_ = function(e) {
  298. // Which key events involve IME deactivation depends on the user's
  299. // environment (i.e. browsers, platforms, and IMEs). Usually Shift key
  300. // and Ctrl key does not involve IME deactivation, so we currently assume
  301. // that these keys are not IME deactivation trigger.
  302. switch (e.keyCode) {
  303. case goog.events.KeyCodes.SHIFT:
  304. case goog.events.KeyCodes.CTRL:
  305. return false;
  306. default:
  307. return true;
  308. }
  309. };
  310. /** @override */
  311. goog.events.ImeHandler.prototype.disposeInternal = function() {
  312. this.handler_.dispose();
  313. this.keyUpHandler_.dispose();
  314. this.el_ = null;
  315. goog.events.ImeHandler.base(this, 'disposeInternal');
  316. };