| // Copyright 2007 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 Rich text spell checker implementation. |
| * |
| * @author eae@google.com (Emil A Eklund) |
| * @see ../demos/richtextspellchecker.html |
| */ |
| |
| goog.provide('goog.ui.RichTextSpellChecker'); |
| |
| goog.require('goog.Timer'); |
| goog.require('goog.asserts'); |
| goog.require('goog.dom'); |
| goog.require('goog.dom.NodeType'); |
| goog.require('goog.dom.Range'); |
| goog.require('goog.events.EventHandler'); |
| goog.require('goog.events.EventType'); |
| goog.require('goog.events.KeyCodes'); |
| goog.require('goog.events.KeyHandler'); |
| goog.require('goog.math.Coordinate'); |
| goog.require('goog.spell.SpellCheck'); |
| goog.require('goog.string.StringBuffer'); |
| goog.require('goog.style'); |
| goog.require('goog.ui.AbstractSpellChecker'); |
| goog.require('goog.ui.Component'); |
| goog.require('goog.ui.PopupMenu'); |
| |
| |
| |
| /** |
| * Rich text spell checker implementation. |
| * |
| * @param {goog.spell.SpellCheck} handler Instance of the SpellCheckHandler |
| * support object to use. A single instance can be shared by multiple editor |
| * components. |
| * @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper. |
| * @constructor |
| * @extends {goog.ui.AbstractSpellChecker} |
| */ |
| goog.ui.RichTextSpellChecker = function(handler, opt_domHelper) { |
| goog.ui.AbstractSpellChecker.call(this, handler, opt_domHelper); |
| |
| /** |
| * String buffer for use in reassembly of the original text. |
| * @type {goog.string.StringBuffer} |
| * @private |
| */ |
| this.workBuffer_ = new goog.string.StringBuffer(); |
| |
| /** |
| * Bound async function (to avoid rebinding it on every call). |
| * @type {Function} |
| * @private |
| */ |
| this.boundContinueAsyncFn_ = goog.bind(this.continueAsync_, this); |
| |
| /** |
| * Event handler for listening to events without leaking. |
| * @private {!goog.events.EventHandler} |
| */ |
| this.eventHandler_ = new goog.events.EventHandler(this); |
| this.registerDisposable(this.eventHandler_); |
| |
| /** |
| * The object handling keyboard events. |
| * @private {!goog.events.KeyHandler} |
| */ |
| this.keyHandler_ = new goog.events.KeyHandler(); |
| this.registerDisposable(this.keyHandler_); |
| }; |
| goog.inherits(goog.ui.RichTextSpellChecker, goog.ui.AbstractSpellChecker); |
| goog.tagUnsealableClass(goog.ui.RichTextSpellChecker); |
| |
| |
| /** |
| * Root node for rich editor. |
| * @type {Node} |
| * @private |
| */ |
| goog.ui.RichTextSpellChecker.prototype.rootNode_; |
| |
| |
| /** |
| * Indicates whether the root node for the rich editor is an iframe. |
| * @private {boolean} |
| */ |
| goog.ui.RichTextSpellChecker.prototype.rootNodeIframe_ = false; |
| |
| |
| /** |
| * Current node where spell checker has interrupted to go to the next stack |
| * frame. |
| * @type {Node} |
| * @private |
| */ |
| goog.ui.RichTextSpellChecker.prototype.currentNode_; |
| |
| |
| /** |
| * Counter of inserted elements. Used in processing loop to attempt to preserve |
| * existing nodes if they contain no misspellings. |
| * @type {number} |
| * @private |
| */ |
| goog.ui.RichTextSpellChecker.prototype.elementsInserted_ = 0; |
| |
| |
| /** |
| * Number of words to scan to precharge the dictionary. |
| * @type {number} |
| * @private |
| */ |
| goog.ui.RichTextSpellChecker.prototype.dictionaryPreScanSize_ = 1000; |
| |
| |
| /** |
| * Class name for word spans. |
| * @type {string} |
| */ |
| goog.ui.RichTextSpellChecker.prototype.wordClassName = |
| goog.getCssName('goog-spellcheck-word'); |
| |
| |
| /** |
| * DomHelper to be used for interacting with the editable document/element. |
| * |
| * @type {goog.dom.DomHelper|undefined} |
| * @private |
| */ |
| goog.ui.RichTextSpellChecker.prototype.editorDom_; |
| |
| |
| /** |
| * Tag name portion of the marker for the text that does not need to be checked |
| * for spelling. |
| * |
| * @type {Array<string|undefined>} |
| */ |
| goog.ui.RichTextSpellChecker.prototype.excludeTags; |
| |
| |
| /** |
| * CSS Style text for invalid words. As it's set inside the rich edit iframe |
| * classes defined in the parent document are not availble, thus the style is |
| * set inline. |
| * @type {string} |
| */ |
| goog.ui.RichTextSpellChecker.prototype.invalidWordCssText = |
| 'background: yellow;'; |
| |
| |
| /** |
| * Creates the initial DOM representation for the component. |
| * |
| * @throws {Error} Not supported. Use decorate. |
| * @see #decorate |
| * @override |
| */ |
| goog.ui.RichTextSpellChecker.prototype.createDom = function() { |
| throw Error('Render not supported for goog.ui.RichTextSpellChecker.'); |
| }; |
| |
| |
| /** |
| * Decorates the element for the UI component. |
| * |
| * @param {Element} element Element to decorate. |
| * @override |
| */ |
| goog.ui.RichTextSpellChecker.prototype.decorateInternal = function(element) { |
| this.setElementInternal(element); |
| this.rootNodeIframe_ = element.contentDocument || element.contentWindow; |
| if (this.rootNodeIframe_) { |
| var doc = element.contentDocument || element.contentWindow.document; |
| this.rootNode_ = doc.body; |
| this.editorDom_ = goog.dom.getDomHelper(doc); |
| } else { |
| this.rootNode_ = element; |
| this.editorDom_ = goog.dom.getDomHelper(element); |
| } |
| }; |
| |
| |
| /** @override */ |
| goog.ui.RichTextSpellChecker.prototype.enterDocument = function() { |
| goog.ui.RichTextSpellChecker.superClass_.enterDocument.call(this); |
| |
| var rootElement = goog.asserts.assertElement(this.rootNode_, |
| 'The rootNode_ of a richtextspellchecker must be an Element.'); |
| this.keyHandler_.attach(rootElement); |
| |
| this.initSuggestionsMenu(); |
| }; |
| |
| |
| /** @override */ |
| goog.ui.RichTextSpellChecker.prototype.initSuggestionsMenu = function() { |
| goog.ui.RichTextSpellChecker.base(this, 'initSuggestionsMenu'); |
| |
| var menu = goog.asserts.assertInstanceof(this.getMenu(), goog.ui.PopupMenu, |
| 'The menu of a richtextspellchecker must be a PopupMenu.'); |
| this.eventHandler_.listen(menu, |
| goog.ui.Component.EventType.HIDE, this.onCorrectionHide_); |
| }; |
| |
| |
| /** |
| * Checks spelling for all text and displays correction UI. |
| * @override |
| */ |
| goog.ui.RichTextSpellChecker.prototype.check = function() { |
| this.blockReadyEvents(); |
| this.preChargeDictionary_(this.rootNode_, this.dictionaryPreScanSize_); |
| this.unblockReadyEvents(); |
| |
| this.eventHandler_.listen(this.spellCheck, |
| goog.spell.SpellCheck.EventType.READY, this.onDictionaryCharged_, true); |
| this.spellCheck.processPending(); |
| }; |
| |
| |
| /** |
| * Processes nodes recursively. |
| * |
| * @param {Node} node Node to start with. |
| * @param {number} words Max number of words to process. |
| * @private |
| */ |
| goog.ui.RichTextSpellChecker.prototype.preChargeDictionary_ = function(node, |
| words) { |
| while (node) { |
| var next = this.nextNode_(node); |
| if (this.isExcluded_(node)) { |
| node = next; |
| continue; |
| } |
| if (node.nodeType == goog.dom.NodeType.TEXT) { |
| if (node.nodeValue) { |
| words -= this.populateDictionary(node.nodeValue, words); |
| if (words <= 0) { |
| return; |
| } |
| } |
| } else if (node.nodeType == goog.dom.NodeType.ELEMENT) { |
| if (node.firstChild) { |
| next = node.firstChild; |
| } |
| } |
| node = next; |
| } |
| }; |
| |
| |
| /** |
| * Starts actual processing after the dictionary is charged. |
| * @param {goog.events.Event} e goog.spell.SpellCheck.EventType.READY event. |
| * @private |
| */ |
| goog.ui.RichTextSpellChecker.prototype.onDictionaryCharged_ = function(e) { |
| e.stopPropagation(); |
| this.eventHandler_.unlisten(this.spellCheck, |
| goog.spell.SpellCheck.EventType.READY, this.onDictionaryCharged_, true); |
| |
| // Now actually do the spell checking. |
| this.clearWordElements(); |
| this.initializeAsyncMode(); |
| this.elementsInserted_ = 0; |
| var result = this.processNode_(this.rootNode_); |
| if (result == goog.ui.AbstractSpellChecker.AsyncResult.PENDING) { |
| goog.Timer.callOnce(this.boundContinueAsyncFn_); |
| return; |
| } |
| this.finishAsyncProcessing(); |
| this.finishCheck_(); |
| }; |
| |
| |
| /** |
| * Continues asynchrnonous spell checking. |
| * @private |
| */ |
| goog.ui.RichTextSpellChecker.prototype.continueAsync_ = function() { |
| var result = this.continueAsyncProcessing(); |
| if (result == goog.ui.AbstractSpellChecker.AsyncResult.PENDING) { |
| goog.Timer.callOnce(this.boundContinueAsyncFn_); |
| return; |
| } |
| result = this.processNode_(this.currentNode_); |
| if (result == goog.ui.AbstractSpellChecker.AsyncResult.PENDING) { |
| goog.Timer.callOnce(this.boundContinueAsyncFn_); |
| return; |
| } |
| this.finishAsyncProcessing(); |
| this.finishCheck_(); |
| }; |
| |
| |
| /** |
| * Finalizes spelling check. |
| * @private |
| */ |
| goog.ui.RichTextSpellChecker.prototype.finishCheck_ = function() { |
| delete this.currentNode_; |
| this.spellCheck.processPending(); |
| |
| if (!this.isVisible()) { |
| this.eventHandler_. |
| listen(this.rootNode_, goog.events.EventType.CLICK, this.onWordClick_). |
| listen(this.keyHandler_, goog.events.KeyHandler.EventType.KEY, |
| this.handleRootNodeKeyEvent); |
| } |
| goog.ui.RichTextSpellChecker.superClass_.check.call(this); |
| }; |
| |
| |
| /** |
| * Finds next node in our enumeration of the tree. |
| * |
| * @param {Node} node The node to which we're computing the next node for. |
| * @return {Node} The next node or null if none was found. |
| * @private |
| */ |
| goog.ui.RichTextSpellChecker.prototype.nextNode_ = function(node) { |
| while (node != this.rootNode_) { |
| if (node.nextSibling) { |
| return node.nextSibling; |
| } |
| node = node.parentNode; |
| } |
| return null; |
| }; |
| |
| |
| /** |
| * Determines if the node is text node without any children. |
| * |
| * @param {Node} node The node to check. |
| * @return {boolean} Whether the node is a text leaf node. |
| * @private |
| */ |
| goog.ui.RichTextSpellChecker.prototype.isTextLeaf_ = function(node) { |
| return node != null && |
| node.nodeType == goog.dom.NodeType.TEXT && |
| !node.firstChild; |
| }; |
| |
| |
| /** @override */ |
| goog.ui.RichTextSpellChecker.prototype.setExcludeMarker = function(marker) { |
| if (marker) { |
| if (typeof marker == 'string') { |
| marker = [marker]; |
| } |
| |
| this.excludeTags = []; |
| this.excludeMarker = []; |
| for (var i = 0; i < marker.length; i++) { |
| var parts = marker[i].split('.'); |
| if (parts.length == 2) { |
| this.excludeTags.push(parts[0]); |
| this.excludeMarker.push(parts[1]); |
| } else { |
| this.excludeMarker.push(parts[0]); |
| this.excludeTags.push(undefined); |
| } |
| } |
| } |
| }; |
| |
| |
| /** |
| * Determines if the node is excluded from checking. |
| * |
| * @param {Node} node The node to check. |
| * @return {boolean} Whether the node is excluded. |
| * @private |
| */ |
| goog.ui.RichTextSpellChecker.prototype.isExcluded_ = function(node) { |
| if (this.excludeMarker && node.className) { |
| for (var i = 0; i < this.excludeMarker.length; i++) { |
| var excludeTag = this.excludeTags[i]; |
| var excludeClass = this.excludeMarker[i]; |
| var isExcluded = !!(excludeClass && |
| node.className.indexOf(excludeClass) != -1 && |
| (!excludeTag || node.tagName == excludeTag)); |
| if (isExcluded) { |
| return true; |
| } |
| } |
| } |
| return false; |
| }; |
| |
| |
| /** |
| * Processes nodes recursively. |
| * |
| * @param {Node} node Node where to start. |
| * @return {goog.ui.AbstractSpellChecker.AsyncResult|undefined} Result code. |
| * @private |
| */ |
| goog.ui.RichTextSpellChecker.prototype.processNode_ = function(node) { |
| delete this.currentNode_; |
| while (node) { |
| var next = this.nextNode_(node); |
| if (this.isExcluded_(node)) { |
| node = next; |
| continue; |
| } |
| if (node.nodeType == goog.dom.NodeType.TEXT) { |
| var deleteNode = true; |
| if (node.nodeValue) { |
| var currentElements = this.elementsInserted_; |
| var result = this.processTextAsync(node, node.nodeValue); |
| if (result == goog.ui.AbstractSpellChecker.AsyncResult.PENDING) { |
| // This markes node for deletion (empty nodes get deleted couple |
| // of lines down this function). This is so our algorithm terminates. |
| // In this case the node may be needlessly recreated, but it |
| // happens rather infrequently and saves a lot of code. |
| node.nodeValue = ''; |
| this.currentNode_ = node; |
| return result; |
| } |
| // If we did not add nodes in processing, the current element is still |
| // valid. Let's preserve it! |
| if (currentElements == this.elementsInserted_) { |
| deleteNode = false; |
| } |
| } |
| if (deleteNode) { |
| goog.dom.removeNode(node); |
| } |
| } else if (node.nodeType == goog.dom.NodeType.ELEMENT) { |
| // If this is a spell checker element... |
| if (node.className == this.wordClassName) { |
| // First, reconsolidate the text nodes inside the element - editing |
| // in IE splits them up. |
| var runner = node.firstChild; |
| while (runner) { |
| if (this.isTextLeaf_(runner)) { |
| while (this.isTextLeaf_(runner.nextSibling)) { |
| // Yes, this is not super efficient in IE, but it will almost |
| // never happen. |
| runner.nodeValue += runner.nextSibling.nodeValue; |
| goog.dom.removeNode(runner.nextSibling); |
| } |
| } |
| runner = runner.nextSibling; |
| } |
| // Move its contents out and reprocess it on the next iteration. |
| if (node.firstChild) { |
| next = node.firstChild; |
| while (node.firstChild) { |
| node.parentNode.insertBefore(node.firstChild, node); |
| } |
| } |
| // get rid of the empty shell. |
| goog.dom.removeNode(node); |
| } else { |
| if (node.firstChild) { |
| next = node.firstChild; |
| } |
| } |
| } |
| node = next; |
| } |
| }; |
| |
| |
| /** |
| * Processes word. |
| * |
| * @param {Node} node Node containing word. |
| * @param {string} word Word to process. |
| * @param {goog.spell.SpellCheck.WordStatus} status Status of the word. |
| * @protected |
| * @override |
| */ |
| goog.ui.RichTextSpellChecker.prototype.processWord = function(node, word, |
| status) { |
| node.parentNode.insertBefore(this.createWordElement(word, status), node); |
| this.elementsInserted_++; |
| }; |
| |
| |
| /** |
| * Processes recognized text and separators. |
| * |
| * @param {Node} node Node containing separator. |
| * @param {string} text Text to process. |
| * @protected |
| * @override |
| */ |
| goog.ui.RichTextSpellChecker.prototype.processRange = function(node, text) { |
| // The text does not change, it only gets split, so if the lengths are the |
| // same, the text is the same, so keep the existing node. |
| if (node.nodeType == goog.dom.NodeType.TEXT && node.nodeValue.length == |
| text.length) { |
| return; |
| } |
| |
| node.parentNode.insertBefore(this.editorDom_.createTextNode(text), node); |
| this.elementsInserted_++; |
| }; |
| |
| |
| /** @override */ |
| goog.ui.RichTextSpellChecker.prototype.getElementByIndex = function(id) { |
| return this.editorDom_.getElement(this.makeElementId(id)); |
| }; |
| |
| |
| /** |
| * Updates or replaces element based on word status. |
| * @see goog.ui.AbstractSpellChecker.prototype.updateElement_ |
| * |
| * Overridden from AbstractSpellChecker because we need to be mindful of |
| * deleting the currentNode_ - this can break our pending processing. |
| * |
| * @param {Element} el Word element. |
| * @param {string} word Word to update status for. |
| * @param {goog.spell.SpellCheck.WordStatus} status Status of word. |
| * @protected |
| * @override |
| */ |
| goog.ui.RichTextSpellChecker.prototype.updateElement = function(el, word, |
| status) { |
| if (status == goog.spell.SpellCheck.WordStatus.VALID && el != |
| this.currentNode_ && el.nextSibling != this.currentNode_) { |
| this.removeMarkup(el); |
| } else { |
| goog.dom.setProperties(el, this.getElementProperties(status)); |
| } |
| }; |
| |
| |
| /** |
| * Hides correction UI. |
| * @override |
| */ |
| goog.ui.RichTextSpellChecker.prototype.resume = function() { |
| goog.ui.RichTextSpellChecker.superClass_.resume.call(this); |
| |
| this.restoreNode_(this.rootNode_); |
| |
| this.eventHandler_. |
| unlisten(this.rootNode_, goog.events.EventType.CLICK, this.onWordClick_). |
| unlisten(this.keyHandler_, goog.events.KeyHandler.EventType.KEY, |
| this.handleRootNodeKeyEvent); |
| }; |
| |
| |
| /** |
| * Processes nodes recursively, removes all spell checker markup, and |
| * consolidates text nodes. |
| * |
| * @param {Node} node node on which to recurse. |
| * @private |
| */ |
| goog.ui.RichTextSpellChecker.prototype.restoreNode_ = function(node) { |
| while (node) { |
| if (this.isExcluded_(node)) { |
| node = node.nextSibling; |
| continue; |
| } |
| // Contents of the child of the element is usually 1 text element, but the |
| // user can actually add multiple nodes in it during editing. So we move |
| // all the children out, prepend, and reprocess (pointer is set back to |
| // the first node that's been moved out, and the loop repeats). |
| if (node.nodeType == goog.dom.NodeType.ELEMENT && node.className == |
| this.wordClassName) { |
| var firstElement = node.firstChild; |
| var next; |
| for (var child = firstElement; child; child = next) { |
| next = child.nextSibling; |
| node.parentNode.insertBefore(child, node); |
| } |
| next = firstElement || node.nextSibling; |
| goog.dom.removeNode(node); |
| node = next; |
| continue; |
| } |
| // If this is a chain of text elements, we're trying to consolidate it. |
| var textLeaf = this.isTextLeaf_(node); |
| if (textLeaf) { |
| var textNodes = 1; |
| var next = node.nextSibling; |
| while (this.isTextLeaf_(node.previousSibling)) { |
| node = node.previousSibling; |
| ++textNodes; |
| } |
| while (this.isTextLeaf_(next)) { |
| next = next.nextSibling; |
| ++textNodes; |
| } |
| if (textNodes > 1) { |
| this.workBuffer_.append(node.nodeValue); |
| while (this.isTextLeaf_(node.nextSibling)) { |
| this.workBuffer_.append(node.nextSibling.nodeValue); |
| goog.dom.removeNode(node.nextSibling); |
| } |
| node.nodeValue = this.workBuffer_.toString(); |
| this.workBuffer_.clear(); |
| } |
| } |
| // Process child nodes, if any. |
| if (node.firstChild) { |
| this.restoreNode_(node.firstChild); |
| } |
| node = node.nextSibling; |
| } |
| }; |
| |
| |
| /** |
| * Returns desired element properties for the specified status. |
| * |
| * @param {goog.spell.SpellCheck.WordStatus} status Status of the word. |
| * @return {!Object} Properties to apply to word element. |
| * @protected |
| * @override |
| */ |
| goog.ui.RichTextSpellChecker.prototype.getElementProperties = |
| function(status) { |
| return { |
| 'class': this.wordClassName, |
| 'style': (status == goog.spell.SpellCheck.WordStatus.INVALID) ? |
| this.invalidWordCssText : '' |
| }; |
| }; |
| |
| |
| /** |
| * Handler for click events. |
| * |
| * @param {goog.events.BrowserEvent} event Event object. |
| * @private |
| */ |
| goog.ui.RichTextSpellChecker.prototype.onWordClick_ = function(event) { |
| var target = /** @type {Element} */ (event.target); |
| if (event.target.className == this.wordClassName && |
| this.spellCheck.checkWord(goog.dom.getTextContent(target)) == |
| goog.spell.SpellCheck.WordStatus.INVALID) { |
| |
| this.showSuggestionsMenu(target, event); |
| |
| // Prevent document click handler from closing the menu. |
| event.stopPropagation(); |
| } |
| }; |
| |
| |
| /** @override */ |
| goog.ui.RichTextSpellChecker.prototype.disposeInternal = function() { |
| goog.ui.RichTextSpellChecker.superClass_.disposeInternal.call(this); |
| this.rootNode_ = null; |
| this.editorDom_ = null; |
| }; |
| |
| |
| /** |
| * Returns whether the editor node is an iframe. |
| * |
| * @return {boolean} true the editor node is an iframe, otherwise false. |
| * @protected |
| */ |
| goog.ui.RichTextSpellChecker.prototype.isEditorIframe = function() { |
| return this.rootNodeIframe_; |
| }; |
| |
| |
| /** |
| * Handles keyboard events inside the editor to allow keyboard navigation |
| * between misspelled words and activation of the suggestion menu. |
| * |
| * @param {goog.events.BrowserEvent} e the key event. |
| * @return {boolean} The handled value. |
| * @protected |
| */ |
| goog.ui.RichTextSpellChecker.prototype.handleRootNodeKeyEvent = function(e) { |
| var handled = false; |
| switch (e.keyCode) { |
| case goog.events.KeyCodes.RIGHT: |
| if (e.ctrlKey) { |
| handled = this.navigate(goog.ui.AbstractSpellChecker.Direction.NEXT); |
| } |
| break; |
| |
| case goog.events.KeyCodes.LEFT: |
| if (e.ctrlKey) { |
| handled = this.navigate( |
| goog.ui.AbstractSpellChecker.Direction.PREVIOUS); |
| } |
| break; |
| |
| case goog.events.KeyCodes.DOWN: |
| if (this.getFocusedElementIndex()) { |
| var el = this.editorDom_.getElement(this.makeElementId( |
| this.getFocusedElementIndex())); |
| if (el) { |
| var position = goog.style.getClientPosition(el); |
| |
| if (this.isEditorIframe()) { |
| var iframePosition = goog.style.getClientPosition( |
| this.getElementStrict()); |
| position = goog.math.Coordinate.sum(iframePosition, position); |
| } |
| |
| var size = goog.style.getSize(el); |
| position.x += size.width / 2; |
| position.y += size.height / 2; |
| this.showSuggestionsMenu(el, position); |
| handled = true; |
| } |
| } |
| break; |
| } |
| |
| if (handled) { |
| e.preventDefault(); |
| } |
| |
| return handled; |
| }; |
| |
| |
| /** @override */ |
| goog.ui.RichTextSpellChecker.prototype.onCorrectionAction = function(event) { |
| goog.ui.RichTextSpellChecker.base(this, 'onCorrectionAction', event); |
| |
| // In case of editWord base class has already set the focus (on the input), |
| // otherwise set the focus back on the word. |
| if (event.target != this.getMenuEdit()) { |
| this.reFocus_(); |
| } |
| }; |
| |
| |
| /** |
| * Restores focus when the suggestion menu is hidden. |
| * |
| * @param {goog.events.BrowserEvent} event Blur event. |
| * @private |
| */ |
| goog.ui.RichTextSpellChecker.prototype.onCorrectionHide_ = function(event) { |
| this.reFocus_(); |
| }; |
| |
| |
| /** |
| * Sets the focus back on the previously focused word element. |
| * @private |
| */ |
| goog.ui.RichTextSpellChecker.prototype.reFocus_ = function() { |
| this.getElementStrict().focus(); |
| |
| var el = this.getElementByIndex(this.getFocusedElementIndex()); |
| if (el) { |
| this.focusOnElement(el); |
| } |
| }; |
| |
| |
| /** @override */ |
| goog.ui.RichTextSpellChecker.prototype.focusOnElement = function(element) { |
| goog.dom.Range.createCaret(element, 0).select(); |
| }; |