richtextspellchecker.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784
  1. // Copyright 2007 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 Rich text spell checker implementation.
  16. *
  17. * @author eae@google.com (Emil A Eklund)
  18. * @see ../demos/richtextspellchecker.html
  19. */
  20. goog.provide('goog.ui.RichTextSpellChecker');
  21. goog.require('goog.Timer');
  22. goog.require('goog.asserts');
  23. goog.require('goog.dom');
  24. goog.require('goog.dom.NodeType');
  25. goog.require('goog.dom.Range');
  26. goog.require('goog.events.EventHandler');
  27. goog.require('goog.events.EventType');
  28. goog.require('goog.events.KeyCodes');
  29. goog.require('goog.events.KeyHandler');
  30. goog.require('goog.math.Coordinate');
  31. goog.require('goog.spell.SpellCheck');
  32. goog.require('goog.string.StringBuffer');
  33. goog.require('goog.style');
  34. goog.require('goog.ui.AbstractSpellChecker');
  35. goog.require('goog.ui.Component');
  36. goog.require('goog.ui.PopupMenu');
  37. /**
  38. * Rich text spell checker implementation.
  39. *
  40. * @param {goog.spell.SpellCheck} handler Instance of the SpellCheckHandler
  41. * support object to use. A single instance can be shared by multiple editor
  42. * components.
  43. * @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper.
  44. * @constructor
  45. * @extends {goog.ui.AbstractSpellChecker}
  46. */
  47. goog.ui.RichTextSpellChecker = function(handler, opt_domHelper) {
  48. goog.ui.AbstractSpellChecker.call(this, handler, opt_domHelper);
  49. /**
  50. * String buffer for use in reassembly of the original text.
  51. * @type {goog.string.StringBuffer}
  52. * @private
  53. */
  54. this.workBuffer_ = new goog.string.StringBuffer();
  55. /**
  56. * Bound async function (to avoid rebinding it on every call).
  57. * @type {Function}
  58. * @private
  59. */
  60. this.boundContinueAsyncFn_ = goog.bind(this.continueAsync_, this);
  61. /**
  62. * Event handler for listening to events without leaking.
  63. * @private {!goog.events.EventHandler}
  64. */
  65. this.eventHandler_ = new goog.events.EventHandler(this);
  66. this.registerDisposable(this.eventHandler_);
  67. /**
  68. * The object handling keyboard events.
  69. * @private {!goog.events.KeyHandler}
  70. */
  71. this.keyHandler_ = new goog.events.KeyHandler();
  72. this.registerDisposable(this.keyHandler_);
  73. };
  74. goog.inherits(goog.ui.RichTextSpellChecker, goog.ui.AbstractSpellChecker);
  75. goog.tagUnsealableClass(goog.ui.RichTextSpellChecker);
  76. /**
  77. * Root node for rich editor.
  78. * @type {Node}
  79. * @private
  80. */
  81. goog.ui.RichTextSpellChecker.prototype.rootNode_;
  82. /**
  83. * Indicates whether the root node for the rich editor is an iframe.
  84. * @private {boolean}
  85. */
  86. goog.ui.RichTextSpellChecker.prototype.rootNodeIframe_ = false;
  87. /**
  88. * Current node where spell checker has interrupted to go to the next stack
  89. * frame.
  90. * @type {Node}
  91. * @private
  92. */
  93. goog.ui.RichTextSpellChecker.prototype.currentNode_;
  94. /**
  95. * Counter of inserted elements. Used in processing loop to attempt to preserve
  96. * existing nodes if they contain no misspellings.
  97. * @type {number}
  98. * @private
  99. */
  100. goog.ui.RichTextSpellChecker.prototype.elementsInserted_ = 0;
  101. /**
  102. * Number of words to scan to precharge the dictionary.
  103. * @type {number}
  104. * @private
  105. */
  106. goog.ui.RichTextSpellChecker.prototype.dictionaryPreScanSize_ = 1000;
  107. /**
  108. * Class name for word spans.
  109. * @type {string}
  110. */
  111. goog.ui.RichTextSpellChecker.prototype.wordClassName =
  112. goog.getCssName('goog-spellcheck-word');
  113. /**
  114. * DomHelper to be used for interacting with the editable document/element.
  115. *
  116. * @type {goog.dom.DomHelper|undefined}
  117. * @private
  118. */
  119. goog.ui.RichTextSpellChecker.prototype.editorDom_;
  120. /**
  121. * Tag name portion of the marker for the text that does not need to be checked
  122. * for spelling.
  123. *
  124. * @type {Array<string|undefined>}
  125. */
  126. goog.ui.RichTextSpellChecker.prototype.excludeTags;
  127. /**
  128. * CSS Style text for invalid words. As it's set inside the rich edit iframe
  129. * classes defined in the parent document are not available, thus the style is
  130. * set inline.
  131. * @type {string}
  132. */
  133. goog.ui.RichTextSpellChecker.prototype.invalidWordCssText =
  134. 'background: yellow;';
  135. /**
  136. * Creates the initial DOM representation for the component.
  137. *
  138. * @throws {Error} Not supported. Use decorate.
  139. * @see #decorate
  140. * @override
  141. */
  142. goog.ui.RichTextSpellChecker.prototype.createDom = function() {
  143. throw Error('Render not supported for goog.ui.RichTextSpellChecker.');
  144. };
  145. /**
  146. * Decorates the element for the UI component.
  147. *
  148. * @param {Element} element Element to decorate.
  149. * @override
  150. */
  151. goog.ui.RichTextSpellChecker.prototype.decorateInternal = function(element) {
  152. this.setElementInternal(element);
  153. this.rootNodeIframe_ = element.contentDocument || element.contentWindow;
  154. if (this.rootNodeIframe_) {
  155. var doc = element.contentDocument || element.contentWindow.document;
  156. this.rootNode_ = doc.body;
  157. this.editorDom_ = goog.dom.getDomHelper(doc);
  158. } else {
  159. this.rootNode_ = element;
  160. this.editorDom_ = goog.dom.getDomHelper(element);
  161. }
  162. };
  163. /** @override */
  164. goog.ui.RichTextSpellChecker.prototype.enterDocument = function() {
  165. goog.ui.RichTextSpellChecker.superClass_.enterDocument.call(this);
  166. var rootElement = goog.asserts.assertElement(
  167. this.rootNode_,
  168. 'The rootNode_ of a richtextspellchecker must be an Element.');
  169. this.keyHandler_.attach(rootElement);
  170. this.initSuggestionsMenu();
  171. };
  172. /** @override */
  173. goog.ui.RichTextSpellChecker.prototype.initSuggestionsMenu = function() {
  174. goog.ui.RichTextSpellChecker.base(this, 'initSuggestionsMenu');
  175. var menu = goog.asserts.assertInstanceof(
  176. this.getMenu(), goog.ui.PopupMenu,
  177. 'The menu of a richtextspellchecker must be a PopupMenu.');
  178. this.eventHandler_.listen(
  179. menu, goog.ui.Component.EventType.HIDE, this.onCorrectionHide_);
  180. };
  181. /**
  182. * Checks spelling for all text and displays correction UI.
  183. * @override
  184. */
  185. goog.ui.RichTextSpellChecker.prototype.check = function() {
  186. this.blockReadyEvents();
  187. this.preChargeDictionary_(this.rootNode_, this.dictionaryPreScanSize_);
  188. this.unblockReadyEvents();
  189. this.eventHandler_.listen(
  190. this.spellCheck, goog.spell.SpellCheck.EventType.READY,
  191. this.onDictionaryCharged_, true);
  192. this.spellCheck.processPending();
  193. };
  194. /**
  195. * Processes nodes recursively.
  196. *
  197. * @param {Node} node Node to start with.
  198. * @param {number} words Max number of words to process.
  199. * @private
  200. */
  201. goog.ui.RichTextSpellChecker.prototype.preChargeDictionary_ = function(
  202. node, words) {
  203. while (node) {
  204. var next = this.nextNode_(node);
  205. if (this.isExcluded_(node)) {
  206. node = next;
  207. continue;
  208. }
  209. if (node.nodeType == goog.dom.NodeType.TEXT) {
  210. if (node.nodeValue) {
  211. words -= this.populateDictionary(node.nodeValue, words);
  212. if (words <= 0) {
  213. return;
  214. }
  215. }
  216. } else if (node.nodeType == goog.dom.NodeType.ELEMENT) {
  217. if (node.firstChild) {
  218. next = node.firstChild;
  219. }
  220. }
  221. node = next;
  222. }
  223. };
  224. /**
  225. * Starts actual processing after the dictionary is charged.
  226. * @param {goog.events.Event} e goog.spell.SpellCheck.EventType.READY event.
  227. * @private
  228. */
  229. goog.ui.RichTextSpellChecker.prototype.onDictionaryCharged_ = function(e) {
  230. e.stopPropagation();
  231. this.eventHandler_.unlisten(
  232. this.spellCheck, goog.spell.SpellCheck.EventType.READY,
  233. this.onDictionaryCharged_, true);
  234. // Now actually do the spell checking.
  235. this.clearWordElements();
  236. this.initializeAsyncMode();
  237. this.elementsInserted_ = 0;
  238. var result = this.processNode_(this.rootNode_);
  239. if (result == goog.ui.AbstractSpellChecker.AsyncResult.PENDING) {
  240. goog.Timer.callOnce(this.boundContinueAsyncFn_);
  241. return;
  242. }
  243. this.finishAsyncProcessing();
  244. this.finishCheck_();
  245. };
  246. /**
  247. * Continues asynchrnonous spell checking.
  248. * @private
  249. */
  250. goog.ui.RichTextSpellChecker.prototype.continueAsync_ = function() {
  251. var result = this.continueAsyncProcessing();
  252. if (result == goog.ui.AbstractSpellChecker.AsyncResult.PENDING) {
  253. goog.Timer.callOnce(this.boundContinueAsyncFn_);
  254. return;
  255. }
  256. result = this.processNode_(this.currentNode_);
  257. if (result == goog.ui.AbstractSpellChecker.AsyncResult.PENDING) {
  258. goog.Timer.callOnce(this.boundContinueAsyncFn_);
  259. return;
  260. }
  261. this.finishAsyncProcessing();
  262. this.finishCheck_();
  263. };
  264. /**
  265. * Finalizes spelling check.
  266. * @private
  267. */
  268. goog.ui.RichTextSpellChecker.prototype.finishCheck_ = function() {
  269. delete this.currentNode_;
  270. this.spellCheck.processPending();
  271. if (!this.isVisible()) {
  272. this.eventHandler_
  273. .listen(this.rootNode_, goog.events.EventType.CLICK, this.onWordClick_)
  274. .listen(
  275. this.keyHandler_, goog.events.KeyHandler.EventType.KEY,
  276. this.handleRootNodeKeyEvent);
  277. }
  278. goog.ui.RichTextSpellChecker.superClass_.check.call(this);
  279. };
  280. /**
  281. * Finds next node in our enumeration of the tree.
  282. *
  283. * @param {Node} node The node to which we're computing the next node for.
  284. * @return {Node} The next node or null if none was found.
  285. * @private
  286. */
  287. goog.ui.RichTextSpellChecker.prototype.nextNode_ = function(node) {
  288. while (node != this.rootNode_) {
  289. if (node.nextSibling) {
  290. return node.nextSibling;
  291. }
  292. node = node.parentNode;
  293. }
  294. return null;
  295. };
  296. /**
  297. * Determines if the node is text node without any children.
  298. *
  299. * @param {Node} node The node to check.
  300. * @return {boolean} Whether the node is a text leaf node.
  301. * @private
  302. */
  303. goog.ui.RichTextSpellChecker.prototype.isTextLeaf_ = function(node) {
  304. return node != null && node.nodeType == goog.dom.NodeType.TEXT &&
  305. !node.firstChild;
  306. };
  307. /** @override */
  308. goog.ui.RichTextSpellChecker.prototype.setExcludeMarker = function(marker) {
  309. if (marker) {
  310. if (typeof marker == 'string') {
  311. marker = [marker];
  312. }
  313. this.excludeTags = [];
  314. this.excludeMarker = [];
  315. for (var i = 0; i < marker.length; i++) {
  316. var parts = marker[i].split('.');
  317. if (parts.length == 2) {
  318. this.excludeTags.push(parts[0]);
  319. this.excludeMarker.push(parts[1]);
  320. } else {
  321. this.excludeMarker.push(parts[0]);
  322. this.excludeTags.push(undefined);
  323. }
  324. }
  325. }
  326. };
  327. /**
  328. * Determines if the node is excluded from checking.
  329. *
  330. * @param {Node} node The node to check.
  331. * @return {boolean} Whether the node is excluded.
  332. * @private
  333. */
  334. goog.ui.RichTextSpellChecker.prototype.isExcluded_ = function(node) {
  335. if (this.excludeMarker && node.className) {
  336. for (var i = 0; i < this.excludeMarker.length; i++) {
  337. var excludeTag = this.excludeTags[i];
  338. var excludeClass = this.excludeMarker[i];
  339. var isExcluded =
  340. !!(excludeClass && node.className.indexOf(excludeClass) != -1 &&
  341. (!excludeTag || node.tagName == excludeTag));
  342. if (isExcluded) {
  343. return true;
  344. }
  345. }
  346. }
  347. return false;
  348. };
  349. /**
  350. * Processes nodes recursively.
  351. *
  352. * @param {Node} node Node where to start.
  353. * @return {goog.ui.AbstractSpellChecker.AsyncResult|undefined} Result code.
  354. * @private
  355. */
  356. goog.ui.RichTextSpellChecker.prototype.processNode_ = function(node) {
  357. delete this.currentNode_;
  358. while (node) {
  359. var next = this.nextNode_(node);
  360. if (this.isExcluded_(node)) {
  361. node = next;
  362. continue;
  363. }
  364. if (node.nodeType == goog.dom.NodeType.TEXT) {
  365. var deleteNode = true;
  366. if (node.nodeValue) {
  367. var currentElements = this.elementsInserted_;
  368. var result = this.processTextAsync(node, node.nodeValue);
  369. if (result == goog.ui.AbstractSpellChecker.AsyncResult.PENDING) {
  370. // This marks node for deletion (empty nodes get deleted couple
  371. // of lines down this function). This is so our algorithm terminates.
  372. // In this case the node may be needlessly recreated, but it
  373. // happens rather infrequently and saves a lot of code.
  374. node.nodeValue = '';
  375. this.currentNode_ = node;
  376. return result;
  377. }
  378. // If we did not add nodes in processing, the current element is still
  379. // valid. Let's preserve it!
  380. if (currentElements == this.elementsInserted_) {
  381. deleteNode = false;
  382. }
  383. }
  384. if (deleteNode) {
  385. goog.dom.removeNode(node);
  386. }
  387. } else if (node.nodeType == goog.dom.NodeType.ELEMENT) {
  388. // If this is a spell checker element...
  389. if (node.className == this.wordClassName) {
  390. // First, reconsolidate the text nodes inside the element - editing
  391. // in IE splits them up.
  392. var runner = node.firstChild;
  393. while (runner) {
  394. if (this.isTextLeaf_(runner)) {
  395. while (this.isTextLeaf_(runner.nextSibling)) {
  396. // Yes, this is not super efficient in IE, but it will almost
  397. // never happen.
  398. runner.nodeValue += runner.nextSibling.nodeValue;
  399. goog.dom.removeNode(runner.nextSibling);
  400. }
  401. }
  402. runner = runner.nextSibling;
  403. }
  404. // Move its contents out and reprocess it on the next iteration.
  405. if (node.firstChild) {
  406. next = node.firstChild;
  407. while (node.firstChild) {
  408. node.parentNode.insertBefore(node.firstChild, node);
  409. }
  410. }
  411. // get rid of the empty shell.
  412. goog.dom.removeNode(node);
  413. } else {
  414. if (node.firstChild) {
  415. next = node.firstChild;
  416. }
  417. }
  418. }
  419. node = next;
  420. }
  421. };
  422. /**
  423. * Processes word.
  424. *
  425. * @param {Node} node Node containing word.
  426. * @param {string} word Word to process.
  427. * @param {goog.spell.SpellCheck.WordStatus} status Status of the word.
  428. * @protected
  429. * @override
  430. */
  431. goog.ui.RichTextSpellChecker.prototype.processWord = function(
  432. node, word, status) {
  433. node.parentNode.insertBefore(this.createWordElement(word, status), node);
  434. this.elementsInserted_++;
  435. };
  436. /**
  437. * Processes recognized text and separators.
  438. *
  439. * @param {Node} node Node containing separator.
  440. * @param {string} text Text to process.
  441. * @protected
  442. * @override
  443. */
  444. goog.ui.RichTextSpellChecker.prototype.processRange = function(node, text) {
  445. // The text does not change, it only gets split, so if the lengths are the
  446. // same, the text is the same, so keep the existing node.
  447. if (node.nodeType == goog.dom.NodeType.TEXT &&
  448. node.nodeValue.length == text.length) {
  449. return;
  450. }
  451. node.parentNode.insertBefore(this.editorDom_.createTextNode(text), node);
  452. this.elementsInserted_++;
  453. };
  454. /** @override */
  455. goog.ui.RichTextSpellChecker.prototype.getElementByIndex = function(id) {
  456. return this.editorDom_.getElement(this.makeElementId(id));
  457. };
  458. /**
  459. * Updates or replaces element based on word status.
  460. * @see goog.ui.AbstractSpellChecker.prototype.updateElement_
  461. *
  462. * Overridden from AbstractSpellChecker because we need to be mindful of
  463. * deleting the currentNode_ - this can break our pending processing.
  464. *
  465. * @param {Element} el Word element.
  466. * @param {string} word Word to update status for.
  467. * @param {goog.spell.SpellCheck.WordStatus} status Status of word.
  468. * @protected
  469. * @override
  470. */
  471. goog.ui.RichTextSpellChecker.prototype.updateElement = function(
  472. el, word, status) {
  473. if (status == goog.spell.SpellCheck.WordStatus.VALID &&
  474. el != this.currentNode_ && el.nextSibling != this.currentNode_) {
  475. this.removeMarkup(el);
  476. } else {
  477. goog.dom.setProperties(el, this.getElementProperties(status));
  478. }
  479. };
  480. /**
  481. * Hides correction UI.
  482. * @override
  483. */
  484. goog.ui.RichTextSpellChecker.prototype.resume = function() {
  485. goog.ui.RichTextSpellChecker.superClass_.resume.call(this);
  486. this.restoreNode_(this.rootNode_);
  487. this.eventHandler_
  488. .unlisten(this.rootNode_, goog.events.EventType.CLICK, this.onWordClick_)
  489. .unlisten(
  490. this.keyHandler_, goog.events.KeyHandler.EventType.KEY,
  491. this.handleRootNodeKeyEvent);
  492. };
  493. /**
  494. * Processes nodes recursively, removes all spell checker markup, and
  495. * consolidates text nodes.
  496. *
  497. * @param {Node} node node on which to recurse.
  498. * @private
  499. */
  500. goog.ui.RichTextSpellChecker.prototype.restoreNode_ = function(node) {
  501. while (node) {
  502. if (this.isExcluded_(node)) {
  503. node = node.nextSibling;
  504. continue;
  505. }
  506. // Contents of the child of the element is usually 1 text element, but the
  507. // user can actually add multiple nodes in it during editing. So we move
  508. // all the children out, prepend, and reprocess (pointer is set back to
  509. // the first node that's been moved out, and the loop repeats).
  510. if (node.nodeType == goog.dom.NodeType.ELEMENT &&
  511. node.className == this.wordClassName) {
  512. var firstElement = node.firstChild;
  513. var next;
  514. for (var child = firstElement; child; child = next) {
  515. next = child.nextSibling;
  516. node.parentNode.insertBefore(child, node);
  517. }
  518. next = firstElement || node.nextSibling;
  519. goog.dom.removeNode(node);
  520. node = next;
  521. continue;
  522. }
  523. // If this is a chain of text elements, we're trying to consolidate it.
  524. var textLeaf = this.isTextLeaf_(node);
  525. if (textLeaf) {
  526. var textNodes = 1;
  527. var next = node.nextSibling;
  528. while (this.isTextLeaf_(node.previousSibling)) {
  529. node = node.previousSibling;
  530. ++textNodes;
  531. }
  532. while (this.isTextLeaf_(next)) {
  533. next = next.nextSibling;
  534. ++textNodes;
  535. }
  536. if (textNodes > 1) {
  537. this.workBuffer_.append(node.nodeValue);
  538. while (this.isTextLeaf_(node.nextSibling)) {
  539. this.workBuffer_.append(node.nextSibling.nodeValue);
  540. goog.dom.removeNode(node.nextSibling);
  541. }
  542. node.nodeValue = this.workBuffer_.toString();
  543. this.workBuffer_.clear();
  544. }
  545. }
  546. // Process child nodes, if any.
  547. if (node.firstChild) {
  548. this.restoreNode_(node.firstChild);
  549. }
  550. node = node.nextSibling;
  551. }
  552. };
  553. /**
  554. * Returns desired element properties for the specified status.
  555. *
  556. * @param {goog.spell.SpellCheck.WordStatus} status Status of the word.
  557. * @return {!Object} Properties to apply to word element.
  558. * @protected
  559. * @override
  560. */
  561. goog.ui.RichTextSpellChecker.prototype.getElementProperties = function(status) {
  562. return {
  563. 'class': this.wordClassName,
  564. 'style': (status == goog.spell.SpellCheck.WordStatus.INVALID) ?
  565. this.invalidWordCssText :
  566. ''
  567. };
  568. };
  569. /**
  570. * Handler for click events.
  571. *
  572. * @param {goog.events.BrowserEvent} event Event object.
  573. * @private
  574. */
  575. goog.ui.RichTextSpellChecker.prototype.onWordClick_ = function(event) {
  576. var target = /** @type {Element} */ (event.target);
  577. if (event.target.className == this.wordClassName &&
  578. this.spellCheck.checkWord(goog.dom.getTextContent(target)) ==
  579. goog.spell.SpellCheck.WordStatus.INVALID) {
  580. this.showSuggestionsMenu(target, event);
  581. // Prevent document click handler from closing the menu.
  582. event.stopPropagation();
  583. }
  584. };
  585. /** @override */
  586. goog.ui.RichTextSpellChecker.prototype.disposeInternal = function() {
  587. goog.ui.RichTextSpellChecker.superClass_.disposeInternal.call(this);
  588. this.rootNode_ = null;
  589. this.editorDom_ = null;
  590. };
  591. /**
  592. * Returns whether the editor node is an iframe.
  593. *
  594. * @return {boolean} true the editor node is an iframe, otherwise false.
  595. * @protected
  596. */
  597. goog.ui.RichTextSpellChecker.prototype.isEditorIframe = function() {
  598. return this.rootNodeIframe_;
  599. };
  600. /**
  601. * Handles keyboard events inside the editor to allow keyboard navigation
  602. * between misspelled words and activation of the suggestion menu.
  603. *
  604. * @param {goog.events.BrowserEvent} e the key event.
  605. * @return {boolean} The handled value.
  606. * @protected
  607. */
  608. goog.ui.RichTextSpellChecker.prototype.handleRootNodeKeyEvent = function(e) {
  609. var handled = false;
  610. switch (e.keyCode) {
  611. case goog.events.KeyCodes.RIGHT:
  612. if (e.ctrlKey) {
  613. handled = this.navigate(goog.ui.AbstractSpellChecker.Direction.NEXT);
  614. }
  615. break;
  616. case goog.events.KeyCodes.LEFT:
  617. if (e.ctrlKey) {
  618. handled =
  619. this.navigate(goog.ui.AbstractSpellChecker.Direction.PREVIOUS);
  620. }
  621. break;
  622. case goog.events.KeyCodes.DOWN:
  623. if (this.getFocusedElementIndex()) {
  624. var el = this.editorDom_.getElement(
  625. this.makeElementId(this.getFocusedElementIndex()));
  626. if (el) {
  627. var position = goog.style.getClientPosition(el);
  628. if (this.isEditorIframe()) {
  629. var iframePosition =
  630. goog.style.getClientPosition(this.getElementStrict());
  631. position = goog.math.Coordinate.sum(iframePosition, position);
  632. }
  633. var size = goog.style.getSize(el);
  634. position.x += size.width / 2;
  635. position.y += size.height / 2;
  636. this.showSuggestionsMenu(el, position);
  637. handled = true;
  638. }
  639. }
  640. break;
  641. }
  642. if (handled) {
  643. e.preventDefault();
  644. }
  645. return handled;
  646. };
  647. /** @override */
  648. goog.ui.RichTextSpellChecker.prototype.onCorrectionAction = function(event) {
  649. goog.ui.RichTextSpellChecker.base(this, 'onCorrectionAction', event);
  650. // In case of editWord base class has already set the focus (on the input),
  651. // otherwise set the focus back on the word.
  652. if (event.target != this.getMenuEdit()) {
  653. this.reFocus_();
  654. }
  655. };
  656. /**
  657. * Restores focus when the suggestion menu is hidden.
  658. *
  659. * @param {goog.events.BrowserEvent} event Blur event.
  660. * @private
  661. */
  662. goog.ui.RichTextSpellChecker.prototype.onCorrectionHide_ = function(event) {
  663. this.reFocus_();
  664. };
  665. /**
  666. * Sets the focus back on the previously focused word element.
  667. * @private
  668. */
  669. goog.ui.RichTextSpellChecker.prototype.reFocus_ = function() {
  670. this.getElementStrict().focus();
  671. var el = this.getElementByIndex(this.getFocusedElementIndex());
  672. if (el) {
  673. this.focusOnElement(el);
  674. }
  675. };
  676. /** @override */
  677. goog.ui.RichTextSpellChecker.prototype.focusOnElement = function(element) {
  678. goog.dom.Range.createCaret(element, 0).select();
  679. };