123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784 |
- // 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 available, 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 marks 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();
- };
|