http://git-wip-us.apache.org/repos/asf/flex-falcon/blob/e2cad6e6/externs/GCL/externs/goog/editor/plugins/abstractdialogplugin.js ---------------------------------------------------------------------- diff --git a/externs/GCL/externs/goog/editor/plugins/abstractdialogplugin.js b/externs/GCL/externs/goog/editor/plugins/abstractdialogplugin.js new file mode 100644 index 0000000..278277e --- /dev/null +++ b/externs/GCL/externs/goog/editor/plugins/abstractdialogplugin.js @@ -0,0 +1,333 @@ +// Copyright 2008 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 An abstract superclass for TrogEdit dialog plugins. Each + * Trogedit dialog has its own plugin. + * + * @author [email protected] (Nick Santos) + */ + +goog.provide('goog.editor.plugins.AbstractDialogPlugin'); +goog.provide('goog.editor.plugins.AbstractDialogPlugin.EventType'); + +goog.require('goog.dom'); +goog.require('goog.dom.Range'); +goog.require('goog.editor.Field'); +goog.require('goog.editor.Plugin'); +goog.require('goog.editor.range'); +goog.require('goog.events'); +goog.require('goog.ui.editor.AbstractDialog'); + + +// *** Public interface ***************************************************** // + + + +/** + * An abstract superclass for a Trogedit plugin that creates exactly one + * dialog. By default dialogs are not reused -- each time execCommand is called, + * a new instance of the dialog object is created (and the old one disposed of). + * To enable reusing of the dialog object, subclasses should call + * setReuseDialog() after calling the superclass constructor. + * @param {string} command The command that this plugin handles. + * @constructor + * @extends {goog.editor.Plugin} + */ +goog.editor.plugins.AbstractDialogPlugin = function(command) { + goog.editor.Plugin.call(this); + this.command_ = command; +}; +goog.inherits(goog.editor.plugins.AbstractDialogPlugin, goog.editor.Plugin); + + +/** @override */ +goog.editor.plugins.AbstractDialogPlugin.prototype.isSupportedCommand = + function(command) { + return command == this.command_; +}; + + +/** + * Handles execCommand. Dialog plugins don't make any changes when they open a + * dialog, just when the dialog closes (because only modal dialogs are + * supported). Hence this method does not dispatch the change events that the + * superclass method does. + * @param {string} command The command to execute. + * @param {...*} var_args Any additional parameters needed to + * execute the command. + * @return {*} The result of the execCommand, if any. + * @override + */ +goog.editor.plugins.AbstractDialogPlugin.prototype.execCommand = function( + command, var_args) { + return this.execCommandInternal.apply(this, arguments); +}; + + +// *** Events *************************************************************** // + + +/** + * Event type constants for events the dialog plugins fire. + * @enum {string} + */ +goog.editor.plugins.AbstractDialogPlugin.EventType = { + // This event is fired when a dialog has been opened. + OPENED: 'dialogOpened', + // This event is fired when a dialog has been closed. + CLOSED: 'dialogClosed' +}; + + +// *** Protected interface ************************************************** // + + +/** + * Creates a new instance of this plugin's dialog. Must be overridden by + * subclasses. + * @param {!goog.dom.DomHelper} dialogDomHelper The dom helper to be used to + * create the dialog. + * @param {*=} opt_arg The dialog specific argument. Concrete subclasses should + * declare a specific type. + * @return {goog.ui.editor.AbstractDialog} The newly created dialog. + * @protected + */ +goog.editor.plugins.AbstractDialogPlugin.prototype.createDialog = + goog.abstractMethod; + + +/** + * Returns the current dialog that was created and opened by this plugin. + * @return {goog.ui.editor.AbstractDialog} The current dialog that was created + * and opened by this plugin. + * @protected + */ +goog.editor.plugins.AbstractDialogPlugin.prototype.getDialog = function() { + return this.dialog_; +}; + + +/** + * Sets whether this plugin should reuse the same instance of the dialog each + * time execCommand is called or create a new one. This is intended for use by + * subclasses only, hence protected. + * @param {boolean} reuse Whether to reuse the dialog. + * @protected + */ +goog.editor.plugins.AbstractDialogPlugin.prototype.setReuseDialog = + function(reuse) { + this.reuseDialog_ = reuse; +}; + + +/** + * Handles execCommand by opening the dialog. Dispatches + * {@link goog.editor.plugins.AbstractDialogPlugin.EventType.OPENED} after the + * dialog is shown. + * @param {string} command The command to execute. + * @param {*=} opt_arg The dialog specific argument. Should be the same as + * {@link createDialog}. + * @return {*} Always returns true, indicating the dialog was shown. + * @protected + * @override + */ +goog.editor.plugins.AbstractDialogPlugin.prototype.execCommandInternal = + function(command, opt_arg) { + // If this plugin should not reuse dialog instances, first dispose of the + // previous dialog. + if (!this.reuseDialog_) { + this.disposeDialog_(); + } + // If there is no dialog yet (or we aren't reusing the previous one), create + // one. + if (!this.dialog_) { + this.dialog_ = this.createDialog( + // TODO(user): Add Field.getAppDomHelper. (Note dom helper will + // need to be updated if setAppWindow is called by clients.) + goog.dom.getDomHelper(this.getFieldObject().getAppWindow()), + opt_arg); + } + + // Since we're opening a dialog, we need to clear the selection because the + // focus will be going to the dialog, and if we leave an selection in the + // editor while another selection is active in the dialog as the user is + // typing, some browsers will screw up the original selection. But first we + // save it so we can restore it when the dialog closes. + // getRange may return null if there is no selection in the field. + var tempRange = this.getFieldObject().getRange(); + // saveUsingDom() did not work as well as saveUsingNormalizedCarets(), + // not sure why. + this.savedRange_ = tempRange && goog.editor.range.saveUsingNormalizedCarets( + tempRange); + goog.dom.Range.clearSelection( + this.getFieldObject().getEditableDomHelper().getWindow()); + + // Listen for the dialog closing so we can clean up. + goog.events.listenOnce(this.dialog_, + goog.ui.editor.AbstractDialog.EventType.AFTER_HIDE, + this.handleAfterHide, + false, + this); + + this.getFieldObject().setModalMode(true); + this.dialog_.show(); + this.dispatchEvent(goog.editor.plugins.AbstractDialogPlugin.EventType.OPENED); + + // Since the selection has left the document, dispatch a selection + // change event. + this.getFieldObject().dispatchSelectionChangeEvent(); + + return true; +}; + + +/** + * Cleans up after the dialog has closed, including restoring the selection to + * what it was before the dialog was opened. If a subclass modifies the editable + * field's content such that the original selection is no longer valid (usually + * the case when the user clicks OK, and sometimes also on Cancel), it is that + * subclass' responsibility to place the selection in the desired place during + * the OK or Cancel (or other) handler. In that case, this method will leave the + * selection in place. + * @param {goog.events.Event} e The AFTER_HIDE event object. + * @protected + */ +goog.editor.plugins.AbstractDialogPlugin.prototype.handleAfterHide = function( + e) { + this.getFieldObject().setModalMode(false); + this.restoreOriginalSelection(); + + if (!this.reuseDialog_) { + this.disposeDialog_(); + } + + this.dispatchEvent(goog.editor.plugins.AbstractDialogPlugin.EventType.CLOSED); + + // Since the selection has returned to the document, dispatch a selection + // change event. + this.getFieldObject().dispatchSelectionChangeEvent(); + + // When the dialog closes due to pressing enter or escape, that happens on the + // keydown event. But the browser will still fire a keyup event after that, + // which is caught by the editable field and causes it to try to fire a + // selection change event. To avoid that, we "debounce" the selection change + // event, meaning the editable field will not fire that event if the keyup + // that caused it immediately after this dialog was hidden ("immediately" + // means a small number of milliseconds defined by the editable field). + this.getFieldObject().debounceEvent( + goog.editor.Field.EventType.SELECTIONCHANGE); +}; + + +/** + * Restores the selection in the editable field to what it was before the dialog + * was opened. This is not guaranteed to work if the contents of the field + * have changed. + * @protected + */ +goog.editor.plugins.AbstractDialogPlugin.prototype.restoreOriginalSelection = + function() { + this.getFieldObject().restoreSavedRange(this.savedRange_); + this.savedRange_ = null; +}; + + +/** + * Cleans up the structure used to save the original selection before the dialog + * was opened. Should be used by subclasses that don't restore the original + * selection via restoreOriginalSelection. + * @protected + */ +goog.editor.plugins.AbstractDialogPlugin.prototype.disposeOriginalSelection = + function() { + if (this.savedRange_) { + this.savedRange_.dispose(); + this.savedRange_ = null; + } +}; + + +/** @override */ +goog.editor.plugins.AbstractDialogPlugin.prototype.disposeInternal = + function() { + this.disposeDialog_(); + goog.editor.plugins.AbstractDialogPlugin.base(this, 'disposeInternal'); +}; + + +// *** Private implementation *********************************************** // + + +/** + * The command that this plugin handles. + * @type {string} + * @private + */ +goog.editor.plugins.AbstractDialogPlugin.prototype.command_; + + +/** + * The current dialog that was created and opened by this plugin. + * @type {goog.ui.editor.AbstractDialog} + * @private + */ +goog.editor.plugins.AbstractDialogPlugin.prototype.dialog_; + + +/** + * Whether this plugin should reuse the same instance of the dialog each time + * execCommand is called or create a new one. + * @type {boolean} + * @private + */ +goog.editor.plugins.AbstractDialogPlugin.prototype.reuseDialog_ = false; + + +/** + * Mutex to prevent recursive calls to disposeDialog_. + * @type {boolean} + * @private + */ +goog.editor.plugins.AbstractDialogPlugin.prototype.isDisposingDialog_ = false; + + +/** + * SavedRange representing the selection before the dialog was opened. + * @type {goog.dom.SavedRange} + * @private + */ +goog.editor.plugins.AbstractDialogPlugin.prototype.savedRange_; + + +/** + * Disposes of the dialog if needed. It is this abstract class' responsibility + * to dispose of the dialog. The "if needed" refers to the fact this method + * might be called twice (nested calls, not sequential) in the dispose flow, so + * if the dialog was already disposed once it should not be disposed again. + * @private + */ +goog.editor.plugins.AbstractDialogPlugin.prototype.disposeDialog_ = function() { + // Wrap disposing the dialog in a mutex. Otherwise disposing it would cause it + // to get hidden (if it is still open) and fire AFTER_HIDE, which in + // turn would cause the dialog to be disposed again (closure only flags an + // object as disposed after the dispose call chain completes, so it doesn't + // prevent recursive dispose calls). + if (this.dialog_ && !this.isDisposingDialog_) { + this.isDisposingDialog_ = true; + this.dialog_.dispose(); + this.dialog_ = null; + this.isDisposingDialog_ = false; + } +};
http://git-wip-us.apache.org/repos/asf/flex-falcon/blob/e2cad6e6/externs/GCL/externs/goog/editor/plugins/abstracttabhandler.js ---------------------------------------------------------------------- diff --git a/externs/GCL/externs/goog/editor/plugins/abstracttabhandler.js b/externs/GCL/externs/goog/editor/plugins/abstracttabhandler.js new file mode 100644 index 0000000..de1a13a --- /dev/null +++ b/externs/GCL/externs/goog/editor/plugins/abstracttabhandler.js @@ -0,0 +1,78 @@ +// Copyright 2008 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 Abstract Editor plugin class to handle tab keys. Has one + * abstract method which should be overriden to handle a tab key press. + * + * @author [email protected] (Robby Walker) + */ + +goog.provide('goog.editor.plugins.AbstractTabHandler'); + +goog.require('goog.editor.Plugin'); +goog.require('goog.events.KeyCodes'); +goog.require('goog.userAgent'); + + + +/** + * Plugin to handle tab keys. Specific tab behavior defined by subclasses. + * + * @constructor + * @extends {goog.editor.Plugin} + */ +goog.editor.plugins.AbstractTabHandler = function() { + goog.editor.Plugin.call(this); +}; +goog.inherits(goog.editor.plugins.AbstractTabHandler, goog.editor.Plugin); + + +/** @override */ +goog.editor.plugins.AbstractTabHandler.prototype.getTrogClassId = + goog.abstractMethod; + + +/** @override */ +goog.editor.plugins.AbstractTabHandler.prototype.handleKeyboardShortcut = + function(e, key, isModifierPressed) { + // If a dialog doesn't have selectable field, Moz grabs the event and + // performs actions in editor window. This solves that problem and allows + // the event to be passed on to proper handlers. + if (goog.userAgent.GECKO && this.getFieldObject().inModalMode()) { + return false; + } + + // Don't handle Ctrl+Tab since the user is most likely trying to switch + // browser tabs. See bug 1305086. + // FF3 on Mac sends Ctrl-Tab to trogedit and we end up inserting a tab, but + // then it also switches the tabs. See bug 1511681. Note that we don't use + // isModifierPressed here since isModifierPressed is true only if metaKey + // is true on Mac. + if (e.keyCode == goog.events.KeyCodes.TAB && !e.metaKey && !e.ctrlKey) { + return this.handleTabKey(e); + } + + return false; +}; + + +/** + * Handle a tab key press. + * @param {goog.events.Event} e The key event. + * @return {boolean} Whether this event was handled by this plugin. + * @protected + */ +goog.editor.plugins.AbstractTabHandler.prototype.handleTabKey = + goog.abstractMethod; http://git-wip-us.apache.org/repos/asf/flex-falcon/blob/e2cad6e6/externs/GCL/externs/goog/editor/plugins/basictextformatter.js ---------------------------------------------------------------------- diff --git a/externs/GCL/externs/goog/editor/plugins/basictextformatter.js b/externs/GCL/externs/goog/editor/plugins/basictextformatter.js new file mode 100644 index 0000000..1cac1cd --- /dev/null +++ b/externs/GCL/externs/goog/editor/plugins/basictextformatter.js @@ -0,0 +1,1769 @@ +// Copyright 2006 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 Functions to style text. + * + * @author [email protected] (Nick Santos) + */ + +goog.provide('goog.editor.plugins.BasicTextFormatter'); +goog.provide('goog.editor.plugins.BasicTextFormatter.COMMAND'); + +goog.require('goog.array'); +goog.require('goog.dom'); +goog.require('goog.dom.NodeType'); +goog.require('goog.dom.Range'); +goog.require('goog.dom.TagName'); +goog.require('goog.editor.BrowserFeature'); +goog.require('goog.editor.Command'); +goog.require('goog.editor.Link'); +goog.require('goog.editor.Plugin'); +goog.require('goog.editor.node'); +goog.require('goog.editor.range'); +goog.require('goog.editor.style'); +goog.require('goog.iter'); +goog.require('goog.iter.StopIteration'); +goog.require('goog.log'); +goog.require('goog.object'); +goog.require('goog.string'); +goog.require('goog.string.Unicode'); +goog.require('goog.style'); +goog.require('goog.ui.editor.messages'); +goog.require('goog.userAgent'); + + + +/** + * Functions to style text (e.g. underline, make bold, etc.) + * @constructor + * @extends {goog.editor.Plugin} + */ +goog.editor.plugins.BasicTextFormatter = function() { + goog.editor.Plugin.call(this); +}; +goog.inherits(goog.editor.plugins.BasicTextFormatter, goog.editor.Plugin); + + +/** @override */ +goog.editor.plugins.BasicTextFormatter.prototype.getTrogClassId = function() { + return 'BTF'; +}; + + +/** + * Logging object. + * @type {goog.log.Logger} + * @protected + * @override + */ +goog.editor.plugins.BasicTextFormatter.prototype.logger = + goog.log.getLogger('goog.editor.plugins.BasicTextFormatter'); + + +/** + * Commands implemented by this plugin. + * @enum {string} + */ +goog.editor.plugins.BasicTextFormatter.COMMAND = { + LINK: '+link', + FORMAT_BLOCK: '+formatBlock', + INDENT: '+indent', + OUTDENT: '+outdent', + STRIKE_THROUGH: '+strikeThrough', + HORIZONTAL_RULE: '+insertHorizontalRule', + SUBSCRIPT: '+subscript', + SUPERSCRIPT: '+superscript', + UNDERLINE: '+underline', + BOLD: '+bold', + ITALIC: '+italic', + FONT_SIZE: '+fontSize', + FONT_FACE: '+fontName', + FONT_COLOR: '+foreColor', + BACKGROUND_COLOR: '+backColor', + ORDERED_LIST: '+insertOrderedList', + UNORDERED_LIST: '+insertUnorderedList', + JUSTIFY_CENTER: '+justifyCenter', + JUSTIFY_FULL: '+justifyFull', + JUSTIFY_RIGHT: '+justifyRight', + JUSTIFY_LEFT: '+justifyLeft' +}; + + +/** + * Inverse map of execCommand strings to + * {@link goog.editor.plugins.BasicTextFormatter.COMMAND} constants. Used to + * determine whether a string corresponds to a command this plugin + * handles in O(1) time. + * @type {Object} + * @private + */ +goog.editor.plugins.BasicTextFormatter.SUPPORTED_COMMANDS_ = + goog.object.transpose(goog.editor.plugins.BasicTextFormatter.COMMAND); + + +/** + * Whether the string corresponds to a command this plugin handles. + * @param {string} command Command string to check. + * @return {boolean} Whether the string corresponds to a command + * this plugin handles. + * @override + */ +goog.editor.plugins.BasicTextFormatter.prototype.isSupportedCommand = function( + command) { + // TODO(user): restore this to simple check once table editing + // is moved out into its own plugin + return command in goog.editor.plugins.BasicTextFormatter.SUPPORTED_COMMANDS_; +}; + + +/** + * @return {goog.dom.AbstractRange} The closure range object that wraps the + * current user selection. + * @private + */ +goog.editor.plugins.BasicTextFormatter.prototype.getRange_ = function() { + return this.getFieldObject().getRange(); +}; + + +/** + * @return {!Document} The document object associated with the currently active + * field. + * @private + */ +goog.editor.plugins.BasicTextFormatter.prototype.getDocument_ = function() { + return this.getFieldDomHelper().getDocument(); +}; + + +/** + * Execute a user-initiated command. + * @param {string} command Command to execute. + * @param {...*} var_args For color commands, this + * should be the hex color (with the #). For FORMAT_BLOCK, this should be + * the goog.editor.plugins.BasicTextFormatter.BLOCK_COMMAND. + * It will be unused for other commands. + * @return {Object|undefined} The result of the command. + * @override + */ +goog.editor.plugins.BasicTextFormatter.prototype.execCommandInternal = function( + command, var_args) { + var preserveDir, styleWithCss, needsFormatBlockDiv, hasDummySelection; + var result; + var opt_arg = arguments[1]; + + switch (command) { + case goog.editor.plugins.BasicTextFormatter.COMMAND.BACKGROUND_COLOR: + // Don't bother for no color selected, color picker is resetting itself. + if (!goog.isNull(opt_arg)) { + if (goog.editor.BrowserFeature.EATS_EMPTY_BACKGROUND_COLOR) { + this.applyBgColorManually_(opt_arg); + } else if (goog.userAgent.OPERA) { + // backColor will color the block level element instead of + // the selected span of text in Opera. + this.execCommandHelper_('hiliteColor', opt_arg); + } else { + this.execCommandHelper_(command, opt_arg); + } + } + break; + + case goog.editor.plugins.BasicTextFormatter.COMMAND.LINK: + result = this.toggleLink_(opt_arg); + break; + + case goog.editor.plugins.BasicTextFormatter.COMMAND.JUSTIFY_CENTER: + case goog.editor.plugins.BasicTextFormatter.COMMAND.JUSTIFY_FULL: + case goog.editor.plugins.BasicTextFormatter.COMMAND.JUSTIFY_RIGHT: + case goog.editor.plugins.BasicTextFormatter.COMMAND.JUSTIFY_LEFT: + this.justify_(command); + break; + + default: + if (goog.userAgent.IE && + command == + goog.editor.plugins.BasicTextFormatter.COMMAND.FORMAT_BLOCK && + opt_arg) { + // IE requires that the argument be in the form of an opening + // tag, like <h1>, including angle brackets. WebKit will accept + // the arguemnt with or without brackets, and Firefox pre-3 supports + // only a fixed subset of tags with brackets, and prefers without. + // So we only add them IE only. + opt_arg = '<' + opt_arg + '>'; + } + + if (command == + goog.editor.plugins.BasicTextFormatter.COMMAND.FONT_COLOR && + goog.isNull(opt_arg)) { + // If we don't have a color, then FONT_COLOR is a no-op. + break; + } + + switch (command) { + case goog.editor.plugins.BasicTextFormatter.COMMAND.INDENT: + case goog.editor.plugins.BasicTextFormatter.COMMAND.OUTDENT: + if (goog.editor.BrowserFeature.HAS_STYLE_WITH_CSS) { + if (goog.userAgent.GECKO) { + styleWithCss = true; + } + if (goog.userAgent.OPERA) { + if (command == + goog.editor.plugins.BasicTextFormatter.COMMAND.OUTDENT) { + // styleWithCSS actually sets negative margins on <blockquote> + // to outdent them. If the command is enabled without + // styleWithCSS flipped on, then the caret is in a blockquote so + // styleWithCSS must not be used. But if the command is not + // enabled, styleWithCSS should be used so that elements such as + // a <div> with a margin-left style can still be outdented. + // (Opera bug: CORE-21118) + styleWithCss = + !this.getDocument_().queryCommandEnabled('outdent'); + } else { + // Always use styleWithCSS for indenting. Otherwise, Opera will + // make separate <blockquote>s around *each* indented line, + // which adds big default <blockquote> margins between each + // indented line. + styleWithCss = true; + } + } + } + // Fall through. + + case goog.editor.plugins.BasicTextFormatter.COMMAND.ORDERED_LIST: + case goog.editor.plugins.BasicTextFormatter.COMMAND.UNORDERED_LIST: + if (goog.editor.BrowserFeature.LEAVES_P_WHEN_REMOVING_LISTS && + this.queryCommandStateInternal_(this.getDocument_(), + command)) { + // IE leaves behind P tags when unapplying lists. + // If we're not in P-mode, then we want divs + // So, unlistify, then convert the Ps into divs. + needsFormatBlockDiv = this.getFieldObject().queryCommandValue( + goog.editor.Command.DEFAULT_TAG) != goog.dom.TagName.P; + } else if (!goog.editor.BrowserFeature.CAN_LISTIFY_BR) { + // IE doesn't convert BRed line breaks into separate list items. + // So convert the BRs to divs, then do the listify. + this.convertBreaksToDivs_(); + } + + // This fix only works in Gecko. + if (goog.userAgent.GECKO && + goog.editor.BrowserFeature.FORGETS_FORMATTING_WHEN_LISTIFYING && + !this.queryCommandValue(command)) { + hasDummySelection |= this.beforeInsertListGecko_(); + } + // Fall through to preserveDir block + + case goog.editor.plugins.BasicTextFormatter.COMMAND.FORMAT_BLOCK: + // Both FF & IE may lose directionality info. Save/restore it. + // TODO(user): Does Safari also need this? + // TODO (gmark, jparent): This isn't ideal because it uses a string + // literal, so if the plugin name changes, it would break. We need a + // better solution. See also other places in code that use + // this.getPluginByClassId('Bidi'). + preserveDir = !!this.getFieldObject().getPluginByClassId('Bidi'); + break; + + case goog.editor.plugins.BasicTextFormatter.COMMAND.SUBSCRIPT: + case goog.editor.plugins.BasicTextFormatter.COMMAND.SUPERSCRIPT: + if (goog.editor.BrowserFeature.NESTS_SUBSCRIPT_SUPERSCRIPT) { + // This browser nests subscript and superscript when both are + // applied, instead of canceling out the first when applying the + // second. + this.applySubscriptSuperscriptWorkarounds_(command); + } + break; + + case goog.editor.plugins.BasicTextFormatter.COMMAND.UNDERLINE: + case goog.editor.plugins.BasicTextFormatter.COMMAND.BOLD: + case goog.editor.plugins.BasicTextFormatter.COMMAND.ITALIC: + // If we are applying the formatting, then we want to have + // styleWithCSS false so that we generate html tags (like <b>). If we + // are unformatting something, we want to have styleWithCSS true so + // that we can unformat both html tags and inline styling. + // TODO(user): What about WebKit and Opera? + styleWithCss = goog.userAgent.GECKO && + goog.editor.BrowserFeature.HAS_STYLE_WITH_CSS && + this.queryCommandValue(command); + break; + + case goog.editor.plugins.BasicTextFormatter.COMMAND.FONT_COLOR: + case goog.editor.plugins.BasicTextFormatter.COMMAND.FONT_FACE: + // It is very expensive in FF (order of magnitude difference) to use + // font tags instead of styled spans. Whenever possible, + // force FF to use spans. + // Font size is very expensive too, but FF always uses font tags, + // regardless of which styleWithCSS value you use. + styleWithCss = goog.editor.BrowserFeature.HAS_STYLE_WITH_CSS && + goog.userAgent.GECKO; + } + + /** + * Cases where we just use the default execCommand (in addition + * to the above fall-throughs) + * goog.editor.plugins.BasicTextFormatter.COMMAND.STRIKE_THROUGH: + * goog.editor.plugins.BasicTextFormatter.COMMAND.HORIZONTAL_RULE: + * goog.editor.plugins.BasicTextFormatter.COMMAND.SUBSCRIPT: + * goog.editor.plugins.BasicTextFormatter.COMMAND.SUPERSCRIPT: + * goog.editor.plugins.BasicTextFormatter.COMMAND.UNDERLINE: + * goog.editor.plugins.BasicTextFormatter.COMMAND.BOLD: + * goog.editor.plugins.BasicTextFormatter.COMMAND.ITALIC: + * goog.editor.plugins.BasicTextFormatter.COMMAND.FONT_SIZE: + * goog.editor.plugins.BasicTextFormatter.COMMAND.FONT_FACE: + */ + this.execCommandHelper_(command, opt_arg, preserveDir, !!styleWithCss); + + if (hasDummySelection) { + this.getDocument_().execCommand('Delete', false, true); + } + + if (needsFormatBlockDiv) { + this.getDocument_().execCommand('FormatBlock', false, '<div>'); + } + } + // FF loses focus, so we have to set the focus back to the document or the + // user can't type after selecting from menu. In IE, focus is set correctly + // and resetting it here messes it up. + if (goog.userAgent.GECKO && !this.getFieldObject().inModalMode()) { + this.focusField_(); + } + return result; +}; + + +/** + * Focuses on the field. + * @private + */ +goog.editor.plugins.BasicTextFormatter.prototype.focusField_ = function() { + this.getFieldDomHelper().getWindow().focus(); +}; + + +/** + * Gets the command value. + * @param {string} command The command value to get. + * @return {string|boolean|null} The current value of the command in the given + * selection. NOTE: This return type list is not documented in MSDN or MDC + * and has been constructed from experience. Please update it + * if necessary. + * @override + */ +goog.editor.plugins.BasicTextFormatter.prototype.queryCommandValue = function( + command) { + var styleWithCss; + switch (command) { + case goog.editor.plugins.BasicTextFormatter.COMMAND.LINK: + return this.isNodeInState_(goog.dom.TagName.A); + + case goog.editor.plugins.BasicTextFormatter.COMMAND.JUSTIFY_CENTER: + case goog.editor.plugins.BasicTextFormatter.COMMAND.JUSTIFY_FULL: + case goog.editor.plugins.BasicTextFormatter.COMMAND.JUSTIFY_RIGHT: + case goog.editor.plugins.BasicTextFormatter.COMMAND.JUSTIFY_LEFT: + return this.isJustification_(command); + + case goog.editor.plugins.BasicTextFormatter.COMMAND.FORMAT_BLOCK: + // TODO(nicksantos): See if we can use queryCommandValue here. + return goog.editor.plugins.BasicTextFormatter.getSelectionBlockState_( + this.getFieldObject().getRange()); + + case goog.editor.plugins.BasicTextFormatter.COMMAND.INDENT: + case goog.editor.plugins.BasicTextFormatter.COMMAND.OUTDENT: + case goog.editor.plugins.BasicTextFormatter.COMMAND.HORIZONTAL_RULE: + // TODO: See if there are reasonable results to return for + // these commands. + return false; + + case goog.editor.plugins.BasicTextFormatter.COMMAND.FONT_SIZE: + case goog.editor.plugins.BasicTextFormatter.COMMAND.FONT_FACE: + case goog.editor.plugins.BasicTextFormatter.COMMAND.FONT_COLOR: + case goog.editor.plugins.BasicTextFormatter.COMMAND.BACKGROUND_COLOR: + // We use queryCommandValue here since we don't just want to know if a + // color/fontface/fontsize is applied, we want to know WHICH one it is. + return this.queryCommandValueInternal_(this.getDocument_(), command, + goog.editor.BrowserFeature.HAS_STYLE_WITH_CSS && + goog.userAgent.GECKO); + + case goog.editor.plugins.BasicTextFormatter.COMMAND.UNDERLINE: + case goog.editor.plugins.BasicTextFormatter.COMMAND.BOLD: + case goog.editor.plugins.BasicTextFormatter.COMMAND.ITALIC: + styleWithCss = goog.editor.BrowserFeature.HAS_STYLE_WITH_CSS && + goog.userAgent.GECKO; + + default: + /** + * goog.editor.plugins.BasicTextFormatter.COMMAND.STRIKE_THROUGH + * goog.editor.plugins.BasicTextFormatter.COMMAND.SUBSCRIPT + * goog.editor.plugins.BasicTextFormatter.COMMAND.SUPERSCRIPT + * goog.editor.plugins.BasicTextFormatter.COMMAND.UNDERLINE + * goog.editor.plugins.BasicTextFormatter.COMMAND.BOLD + * goog.editor.plugins.BasicTextFormatter.COMMAND.ITALIC + * goog.editor.plugins.BasicTextFormatter.COMMAND.ORDERED_LIST + * goog.editor.plugins.BasicTextFormatter.COMMAND.UNORDERED_LIST + */ + // This only works for commands that use the default execCommand + return this.queryCommandStateInternal_(this.getDocument_(), command, + styleWithCss); + } +}; + + +/** + * @override + */ +goog.editor.plugins.BasicTextFormatter.prototype.prepareContentsHtml = + function(html) { + // If the browser collapses empty nodes and the field has only a script + // tag in it, then it will collapse this node. Which will mean the user + // can't click into it to edit it. + if (goog.editor.BrowserFeature.COLLAPSES_EMPTY_NODES && + html.match(/^\s*<script/i)) { + html = ' ' + html; + } + + if (goog.editor.BrowserFeature.CONVERT_TO_B_AND_I_TAGS) { + // Some browsers (FF) can't undo strong/em in some cases, but can undo b/i! + html = html.replace(/<(\/?)strong([^\w])/gi, '<$1b$2'); + html = html.replace(/<(\/?)em([^\w])/gi, '<$1i$2'); + } + + return html; +}; + + +/** + * @override + */ +goog.editor.plugins.BasicTextFormatter.prototype.cleanContentsDom = + function(fieldCopy) { + var images = fieldCopy.getElementsByTagName(goog.dom.TagName.IMG); + for (var i = 0, image; image = images[i]; i++) { + if (goog.editor.BrowserFeature.SHOWS_CUSTOM_ATTRS_IN_INNER_HTML) { + // Only need to remove these attributes in IE because + // Firefox and Safari don't show custom attributes in the innerHTML. + image.removeAttribute('tabIndex'); + image.removeAttribute('tabIndexSet'); + goog.removeUid(image); + + // Declare oldTypeIndex for the compiler. The associated plugin may not be + // included in the compiled bundle. + /** @type {string} */ image.oldTabIndex; + + // oldTabIndex will only be set if + // goog.editor.BrowserFeature.TABS_THROUGH_IMAGES is true and we're in + // P-on-enter mode. + if (image.oldTabIndex) { + image.tabIndex = image.oldTabIndex; + } + } + } +}; + + +/** + * @override + */ +goog.editor.plugins.BasicTextFormatter.prototype.cleanContentsHtml = + function(html) { + if (goog.editor.BrowserFeature.MOVES_STYLE_TO_HEAD) { + // Safari creates a new <head> element for <style> tags, so prepend their + // contents to the output. + var heads = this.getFieldObject().getEditableDomHelper(). + getElementsByTagNameAndClass(goog.dom.TagName.HEAD); + var stylesHtmlArr = []; + + // i starts at 1 so we don't copy in the original, legitimate <head>. + var numHeads = heads.length; + for (var i = 1; i < numHeads; ++i) { + var styles = heads[i].getElementsByTagName(goog.dom.TagName.STYLE); + var numStyles = styles.length; + for (var j = 0; j < numStyles; ++j) { + stylesHtmlArr.push(styles[j].outerHTML); + } + } + return stylesHtmlArr.join('') + html; + } + + return html; +}; + + +/** + * @override + */ +goog.editor.plugins.BasicTextFormatter.prototype.handleKeyboardShortcut = + function(e, key, isModifierPressed) { + if (!isModifierPressed) { + return false; + } + var command; + switch (key) { + case 'b': // Ctrl+B + command = goog.editor.plugins.BasicTextFormatter.COMMAND.BOLD; + break; + case 'i': // Ctrl+I + command = goog.editor.plugins.BasicTextFormatter.COMMAND.ITALIC; + break; + case 'u': // Ctrl+U + command = goog.editor.plugins.BasicTextFormatter.COMMAND.UNDERLINE; + break; + case 's': // Ctrl+S + // TODO(user): This doesn't belong in here. Clients should handle + // this themselves. + // Catching control + s prevents the annoying browser save dialog + // from appearing. + return true; + } + + if (command) { + this.getFieldObject().execCommand(command); + return true; + } + + return false; +}; + + +// Helpers for execCommand + + +/** + * Regular expression to match BRs in HTML. Saves the BRs' attributes in $1 for + * use with replace(). In non-IE browsers, does not match BRs adjacent to an + * opening or closing DIV or P tag, since nonrendered BR elements can occur at + * the end of block level containers in those browsers' editors. + * @type {RegExp} + * @private + */ +goog.editor.plugins.BasicTextFormatter.BR_REGEXP_ = + goog.userAgent.IE ? /<br([^\/>]*)\/?>/gi : + /<br([^\/>]*)\/?>(?!<\/(div|p)>)/gi; + + +/** + * Convert BRs in the selection to divs. + * This is only intended to be used in IE and Opera. + * @return {boolean} Whether any BR's were converted. + * @private + */ +goog.editor.plugins.BasicTextFormatter.prototype.convertBreaksToDivs_ = + function() { + if (!goog.userAgent.IE && !goog.userAgent.OPERA) { + // This function is only supported on IE and Opera. + return false; + } + var range = this.getRange_(); + var parent = range.getContainerElement(); + var doc = this.getDocument_(); + + goog.editor.plugins.BasicTextFormatter.BR_REGEXP_.lastIndex = 0; + // Only mess with the HTML/selection if it contains a BR. + if (goog.editor.plugins.BasicTextFormatter.BR_REGEXP_.test( + parent.innerHTML)) { + // Insert temporary markers to remember the selection. + var savedRange = range.saveUsingCarets(); + + if (parent.tagName == goog.dom.TagName.P) { + // Can't append paragraphs to paragraph tags. Throws an exception in IE. + goog.editor.plugins.BasicTextFormatter.convertParagraphToDiv_( + parent, true); + } else { + // Used to do: + // IE: <div>foo<br>bar</div> --> <div>foo<p id="temp_br">bar</div> + // Opera: <div>foo<br>bar</div> --> <div>foo<p class="temp_br">bar</div> + // To fix bug 1939883, now does for both: + // <div>foo<br>bar</div> --> <div>foo<p trtempbr="temp_br">bar</div> + // TODO(user): Confirm if there's any way to skip this + // intermediate step of converting br's to p's before converting those to + // div's. The reason may be hidden in CLs 5332866 and 8530601. + var attribute = 'trtempbr'; + var value = 'temp_br'; + var newHtml = parent.innerHTML.replace( + goog.editor.plugins.BasicTextFormatter.BR_REGEXP_, + '<p$1 ' + attribute + '="' + value + '">'); + goog.editor.node.replaceInnerHtml(parent, newHtml); + + var paragraphs = + goog.array.toArray(parent.getElementsByTagName(goog.dom.TagName.P)); + goog.iter.forEach(paragraphs, function(paragraph) { + if (paragraph.getAttribute(attribute) == value) { + paragraph.removeAttribute(attribute); + if (goog.string.isBreakingWhitespace( + goog.dom.getTextContent(paragraph))) { + // Prevent the empty blocks from collapsing. + // A <BR> is preferable because it doesn't result in any text being + // added to the "blank" line. In IE, however, it is possible to + // place the caret after the <br>, which effectively creates a + // visible line break. Because of this, we have to resort to using a + // in IE. + var child = goog.userAgent.IE ? + doc.createTextNode(goog.string.Unicode.NBSP) : + doc.createElement(goog.dom.TagName.BR); + paragraph.appendChild(child); + } + goog.editor.plugins.BasicTextFormatter.convertParagraphToDiv_( + paragraph); + } + }); + } + + // Select the previously selected text so we only listify + // the selected portion and maintain the user's selection. + savedRange.restore(); + return true; + } + + return false; +}; + + +/** + * Convert the given paragraph to being a div. This clobbers the + * passed-in node! + * This is only intended to be used in IE and Opera. + * @param {Node} paragraph Paragragh to convert to a div. + * @param {boolean=} opt_convertBrs If true, also convert BRs to divs. + * @private + */ +goog.editor.plugins.BasicTextFormatter.convertParagraphToDiv_ = + function(paragraph, opt_convertBrs) { + if (!goog.userAgent.IE && !goog.userAgent.OPERA) { + // This function is only supported on IE and Opera. + return; + } + var outerHTML = paragraph.outerHTML.replace(/<(\/?)p/gi, '<$1div'); + if (opt_convertBrs) { + // IE fills in the closing div tag if it's missing! + outerHTML = outerHTML.replace( + goog.editor.plugins.BasicTextFormatter.BR_REGEXP_, + '</div><div$1>'); + } + if (goog.userAgent.OPERA && !/<\/div>$/i.test(outerHTML)) { + // Opera doesn't automatically add the closing tag, so add it if needed. + outerHTML += '</div>'; + } + paragraph.outerHTML = outerHTML; +}; + + +/** + * If this is a goog.editor.plugins.BasicTextFormatter.COMMAND, + * convert it to something that we can pass into execCommand, + * queryCommandState, etc. + * + * TODO(user): Consider doing away with the + and converter completely. + * + * @param {goog.editor.plugins.BasicTextFormatter.COMMAND|string} + * command A command key. + * @return {string} The equivalent execCommand command. + * @private + */ +goog.editor.plugins.BasicTextFormatter.convertToRealExecCommand_ = function( + command) { + return command.indexOf('+') == 0 ? command.substring(1) : command; +}; + + +/** + * Justify the text in the selection. + * @param {string} command The type of justification to perform. + * @private + */ +goog.editor.plugins.BasicTextFormatter.prototype.justify_ = function(command) { + this.execCommandHelper_(command, null, false, true); + // Firefox cannot justify divs. In fact, justifying divs results in removing + // the divs and replacing them with brs. So "<div>foo</div><div>bar</div>" + // becomes "foo<br>bar" after alignment is applied. However, if you justify + // again, then you get "<div style='text-align: right'>foo<br>bar</div>", + // which at least looks visually correct. Since justification is (normally) + // idempotent, it isn't a problem when the selection does not contain divs to + // apply justifcation again. + if (goog.userAgent.GECKO) { + this.execCommandHelper_(command, null, false, true); + } + + // Convert all block elements in the selection to use CSS text-align + // instead of the align property. This works better because the align + // property is overridden by the CSS text-align property. + // + // Only for browsers that can't handle this by the styleWithCSS execCommand, + // which allows us to specify if we should insert align or text-align. + // TODO(user): What about WebKit or Opera? + if (!(goog.editor.BrowserFeature.HAS_STYLE_WITH_CSS && + goog.userAgent.GECKO)) { + goog.iter.forEach(this.getFieldObject().getRange(), + goog.editor.plugins.BasicTextFormatter.convertContainerToTextAlign_); + } +}; + + +/** + * Converts the block element containing the given node to use CSS text-align + * instead of the align property. + * @param {Node} node The node to convert the container of. + * @private + */ +goog.editor.plugins.BasicTextFormatter.convertContainerToTextAlign_ = + function(node) { + var container = goog.editor.style.getContainer(node); + + // TODO(user): Fix this so that it doesn't screw up tables. + if (container.align) { + container.style.textAlign = container.align; + container.removeAttribute('align'); + } +}; + + +/** + * Perform an execCommand on the active document. + * @param {string} command The command to execute. + * @param {string|number|boolean|null=} opt_value Optional value. + * @param {boolean=} opt_preserveDir Set true to make sure that command does not + * change directionality of the selected text (works only if all selected + * text has the same directionality, otherwise ignored). Should not be true + * if bidi plugin is not loaded. + * @param {boolean=} opt_styleWithCss Set to true to ask the browser to use CSS + * to perform the execCommand. + * @private + */ +goog.editor.plugins.BasicTextFormatter.prototype.execCommandHelper_ = function( + command, opt_value, opt_preserveDir, opt_styleWithCss) { + // There is a bug in FF: some commands do not preserve attributes of the + // block-level elements they replace. + // This (among the rest) leads to loss of directionality information. + // For now we use a hack (when opt_preserveDir==true) to avoid this + // directionality problem in the simplest cases. + // Known affected commands: formatBlock, insertOrderedList, + // insertUnorderedList, indent, outdent. + // A similar problem occurs in IE when insertOrderedList or + // insertUnorderedList remove existing list. + var dir = null; + if (opt_preserveDir) { + dir = + this.getFieldObject().queryCommandValue( + goog.editor.Command.DIR_RTL) ? 'rtl' : + this.getFieldObject().queryCommandValue( + goog.editor.Command.DIR_LTR) ? 'ltr' : + null; + } + + command = goog.editor.plugins.BasicTextFormatter.convertToRealExecCommand_( + command); + + var endDiv, nbsp; + if (goog.userAgent.IE) { + var ret = this.applyExecCommandIEFixes_(command); + endDiv = ret[0]; + nbsp = ret[1]; + } + + if (goog.userAgent.WEBKIT) { + endDiv = this.applyExecCommandSafariFixes_(command); + } + + if (goog.userAgent.GECKO) { + this.applyExecCommandGeckoFixes_(command); + } + + if (goog.editor.BrowserFeature.DOESNT_OVERRIDE_FONT_SIZE_IN_STYLE_ATTR && + command.toLowerCase() == 'fontsize') { + this.removeFontSizeFromStyleAttrs_(); + } + + var doc = this.getDocument_(); + if (opt_styleWithCss && + goog.editor.BrowserFeature.HAS_STYLE_WITH_CSS) { + doc.execCommand('styleWithCSS', false, true); + if (goog.userAgent.OPERA) { + this.invalidateInlineCss_(); + } + } + + doc.execCommand(command, false, opt_value); + if (opt_styleWithCss && + goog.editor.BrowserFeature.HAS_STYLE_WITH_CSS) { + // If we enabled styleWithCSS, turn it back off. + doc.execCommand('styleWithCSS', false, false); + } + + if (goog.userAgent.WEBKIT && + !goog.userAgent.isVersionOrHigher('526') && + command.toLowerCase() == 'formatblock' && + opt_value && /^[<]?h\d[>]?$/i.test(opt_value)) { + this.cleanUpSafariHeadings_(); + } + + if (/insert(un)?orderedlist/i.test(command)) { + // NOTE(user): This doesn't check queryCommandState because it seems to + // lie. Also, this runs for insertunorderedlist so that the the list + // isn't made up of an <ul> for each <li> - even though it looks the same, + // the markup is disgusting. + if (goog.userAgent.WEBKIT && + !goog.userAgent.isVersionOrHigher(534)) { + this.fixSafariLists_(); + } + if (goog.userAgent.IE) { + this.fixIELists_(); + + if (nbsp) { + // Remove the text node, if applicable. Do not try to instead clobber + // the contents of the text node if it was added, or the same invalid + // node thing as above will happen. The error won't happen here, it + // will happen after you hit enter and then do anything that loops + // through the dom and tries to read that node. + goog.dom.removeNode(nbsp); + } + } + } + + if (endDiv) { + // Remove the dummy div. + goog.dom.removeNode(endDiv); + } + + // Restore directionality if required and only when unambigous (dir!=null). + if (dir) { + this.getFieldObject().execCommand(dir); + } +}; + + +/** + * Applies a background color to a selection when the browser can't do the job. + * + * NOTE(nicksantos): If you think this is hacky, you should try applying + * background color in Opera. It made me cry. + * + * @param {string} bgColor backgroundColor from .formatText to .execCommand. + * @private + */ +goog.editor.plugins.BasicTextFormatter.prototype.applyBgColorManually_ = + function(bgColor) { + var needsSpaceInTextNode = goog.userAgent.GECKO; + var range = this.getFieldObject().getRange(); + var textNode; + var parentTag; + if (range && range.isCollapsed()) { + // Hack to handle Firefox bug: + // https://bugzilla.mozilla.org/show_bug.cgi?id=279330 + // execCommand hiliteColor in Firefox on collapsed selection creates + // a font tag onkeypress + textNode = this.getFieldDomHelper(). + createTextNode(needsSpaceInTextNode ? ' ' : ''); + + var containerNode = range.getStartNode(); + // Check if we're inside a tag that contains the cursor and nothing else; + // if we are, don't create a dummySpan. Just use this containing tag to + // hide the 1-space selection. + // If the user sets a background color on a collapsed selection, then sets + // another one immediately, we get a span tag with a single empty TextNode. + // If the user sets a background color, types, then backspaces, we get a + // span tag with nothing inside it (container is the span). + parentTag = containerNode.nodeType == goog.dom.NodeType.ELEMENT ? + containerNode : containerNode.parentNode; + + if (parentTag.innerHTML == '') { + // There's an Element to work with + // make the space character invisible using a CSS indent hack + parentTag.style.textIndent = '-10000px'; + parentTag.appendChild(textNode); + } else { + // No Element to work with; make one + // create a span with a space character inside + // make the space character invisible using a CSS indent hack + parentTag = this.getFieldDomHelper().createDom(goog.dom.TagName.SPAN, + {'style': 'text-indent:-10000px'}, textNode); + range.replaceContentsWithNode(parentTag); + } + goog.dom.Range.createFromNodeContents(textNode).select(); + } + + this.execCommandHelper_('hiliteColor', bgColor, false, true); + + if (textNode) { + // eliminate the space if necessary. + if (needsSpaceInTextNode) { + textNode.data = ''; + } + + // eliminate the hack. + parentTag.style.textIndent = ''; + // execCommand modified our span so we leave it in place. + } +}; + + +/** + * Toggle link for the current selection: + * If selection contains a link, unlink it, return null. + * Otherwise, make selection into a link, return the link. + * @param {string=} opt_target Target for the link. + * @return {goog.editor.Link?} The resulting link, or null if a link was + * removed. + * @private + */ +goog.editor.plugins.BasicTextFormatter.prototype.toggleLink_ = function( + opt_target) { + if (!this.getFieldObject().isSelectionEditable()) { + this.focusField_(); + } + + var range = this.getRange_(); + // Since we wrap images in links, its possible that the user selected an + // image and clicked link, in which case we want to actually use the + // image as the selection. + var parent = range && range.getContainerElement(); + var link = /** @type {Element} */ ( + goog.dom.getAncestorByTagNameAndClass(parent, goog.dom.TagName.A)); + if (link && goog.editor.node.isEditable(link)) { + goog.dom.flattenElement(link); + } else { + var editableLink = this.createLink_(range, '/', opt_target); + if (editableLink) { + if (!this.getFieldObject().execCommand( + goog.editor.Command.MODAL_LINK_EDITOR, editableLink)) { + var url = this.getFieldObject().getAppWindow().prompt( + goog.ui.editor.messages.MSG_LINK_TO, 'http://'); + if (url) { + editableLink.setTextAndUrl(editableLink.getCurrentText() || url, url); + editableLink.placeCursorRightOf(); + } else { + var savedRange = goog.editor.range.saveUsingNormalizedCarets( + goog.dom.Range.createFromNodeContents(editableLink.getAnchor())); + editableLink.removeLink(); + savedRange.restore().select(); + return null; + } + } + return editableLink; + } + } + return null; +}; + + +/** + * Create a link out of the current selection. If nothing is selected, insert + * a new link. Otherwise, enclose the selection in a link. + * @param {goog.dom.AbstractRange} range The closure range object for the + * current selection. + * @param {string} url The url to link to. + * @param {string=} opt_target Target for the link. + * @return {goog.editor.Link?} The newly created link, or null if the link + * couldn't be created. + * @private + */ +goog.editor.plugins.BasicTextFormatter.prototype.createLink_ = function(range, + url, opt_target) { + var anchor = null; + var anchors = []; + var parent = range && range.getContainerElement(); + // We do not yet support creating links around images. Instead of throwing + // lots of js errors, just fail silently. + // TODO(user): Add support for linking images. + if (parent && parent.tagName == goog.dom.TagName.IMG) { + return null; + } + // If range is not present, the editable field doesn't have focus, abort + // creating a link. + if (!range) { + return null; + } + + if (range.isCollapsed()) { + var textRange = range.getTextRange(0).getBrowserRangeObject(); + if (goog.editor.BrowserFeature.HAS_W3C_RANGES) { + anchor = this.getFieldDomHelper().createElement(goog.dom.TagName.A); + textRange.insertNode(anchor); + } else if (goog.editor.BrowserFeature.HAS_IE_RANGES) { + // TODO: Use goog.dom.AbstractRange's surroundContents + textRange.pasteHTML("<a id='newLink'></a>"); + anchor = this.getFieldDomHelper().getElement('newLink'); + anchor.removeAttribute('id'); + } + } else { + // Create a unique identifier for the link so we can retrieve it later. + // execCommand doesn't return the link to us, and we need a way to find + // the newly created link in the dom, and the url is the only property + // we have control over, so we set that to be unique and then find it. + var uniqueId = goog.string.createUniqueString(); + this.execCommandHelper_('CreateLink', uniqueId); + var setHrefAndLink = function(element, index, arr) { + // We can't do straight comparision since the href can contain the + // absolute url. + if (goog.string.endsWith(element.href, uniqueId)) { + anchors.push(element); + } + }; + + goog.array.forEach(this.getFieldObject().getElement().getElementsByTagName( + goog.dom.TagName.A), setHrefAndLink); + if (anchors.length) { + anchor = anchors.pop(); + } + var isLikelyUrl = function(a, i, anchors) { + return goog.editor.Link.isLikelyUrl(goog.dom.getRawTextContent(a)); + }; + if (anchors.length && goog.array.every(anchors, isLikelyUrl)) { + for (var i = 0, a; a = anchors[i]; i++) { + goog.editor.Link.createNewLinkFromText(a, opt_target); + } + anchors = null; + } + } + + return goog.editor.Link.createNewLink( + /** @type {HTMLAnchorElement} */ (anchor), url, opt_target, anchors); +}; + + +//--------------------------------------------------------------------- +// browser fixes + + +/** + * The following execCommands are "broken" in some way - in IE they allow + * the nodes outside the contentEditable region to get modified (see + * execCommand below for more details). + * @const + * @private + */ +goog.editor.plugins.BasicTextFormatter.brokenExecCommandsIE_ = { + 'indent' : 1, + 'outdent' : 1, + 'insertOrderedList' : 1, + 'insertUnorderedList' : 1, + 'justifyCenter' : 1, + 'justifyFull' : 1, + 'justifyRight': 1, + 'justifyLeft': 1, + 'ltr' : 1, + 'rtl' : 1 +}; + + +/** + * When the following commands are executed while the selection is + * inside a blockquote, they hose the blockquote tag in weird and + * unintuitive ways. + * @const + * @private + */ +goog.editor.plugins.BasicTextFormatter.blockquoteHatingCommandsIE_ = { + 'insertOrderedList' : 1, + 'insertUnorderedList' : 1 +}; + + +/** + * Makes sure that superscript is removed before applying subscript, and vice + * versa. Fixes {@link http://buganizer/issue?id=1173491} . + * @param {goog.editor.plugins.BasicTextFormatter.COMMAND} command The command + * being applied, either SUBSCRIPT or SUPERSCRIPT. + * @private + */ +goog.editor.plugins.BasicTextFormatter. + prototype.applySubscriptSuperscriptWorkarounds_ = function(command) { + if (!this.queryCommandValue(command)) { + // The current selection doesn't currently have the requested + // command, so we are applying it as opposed to removing it. + // (Note that queryCommandValue() will only return true if the + // command is applied to the whole selection, not just part of it. + // In this case it is fine because only if the whole selection has + // the command applied will we be removing it and thus skipping the + // removal of the opposite command.) + var oppositeCommand = + (command == goog.editor.plugins.BasicTextFormatter.COMMAND.SUBSCRIPT ? + goog.editor.plugins.BasicTextFormatter.COMMAND.SUPERSCRIPT : + goog.editor.plugins.BasicTextFormatter.COMMAND.SUBSCRIPT); + var oppositeExecCommand = goog.editor.plugins.BasicTextFormatter. + convertToRealExecCommand_(oppositeCommand); + // Executing the opposite command on a selection that already has it + // applied will cancel it out. But if the selection only has the + // opposite command applied to a part of it, the browser will + // normalize the selection to have the opposite command applied on + // the whole of it. + if (!this.queryCommandValue(oppositeCommand)) { + // The selection doesn't have the opposite command applied to the + // whole of it, so let's exec the opposite command to normalize + // the selection. + // Note: since we know both subscript and superscript commands + // will boil down to a simple call to the browser's execCommand(), + // for performance reasons we can do that directly instead of + // calling execCommandHelper_(). However this is a potential for + // bugs if the implementation of execCommandHelper_() is changed + // to do something more int eh case of subscript and superscript. + this.getDocument_().execCommand(oppositeExecCommand, false, null); + } + // Now that we know the whole selection has the opposite command + // applied, we exec it a second time to properly remove it. + this.getDocument_().execCommand(oppositeExecCommand, false, null); + } +}; + + +/** + * Removes inline font-size styles from elements fully contained in the + * selection, so the font tags produced by execCommand work properly. + * See {@bug 1286408}. + * @private + */ +goog.editor.plugins.BasicTextFormatter.prototype.removeFontSizeFromStyleAttrs_ = + function() { + // Expand the range so that we consider surrounding tags. E.g. if only the + // text node inside a span is selected, the browser could wrap a font tag + // around the span and leave the selection such that only the text node is + // found when looking inside the range, not the span. + var range = goog.editor.range.expand(this.getFieldObject().getRange(), + this.getFieldObject().getElement()); + goog.iter.forEach(goog.iter.filter(range, function(tag, dummy, iter) { + return iter.isStartTag() && range.containsNode(tag); + }), function(node) { + goog.style.setStyle(node, 'font-size', ''); + // Gecko doesn't remove empty style tags. + if (goog.userAgent.GECKO && + node.style.length == 0 && node.getAttribute('style') != null) { + node.removeAttribute('style'); + } + }); +}; + + +/** + * Apply pre-execCommand fixes for IE. + * @param {string} command The command to execute. + * @return {!Array<Node>} Array of nodes to be removed after the execCommand. + * Will never be longer than 2 elements. + * @private + */ +goog.editor.plugins.BasicTextFormatter.prototype.applyExecCommandIEFixes_ = + function(command) { + // IE has a crazy bug where executing list commands + // around blockquotes cause the blockquotes to get transformed + // into "<OL><OL>" or "<UL><UL>" tags. + var toRemove = []; + var endDiv = null; + var range = this.getRange_(); + var dh = this.getFieldDomHelper(); + if (command in + goog.editor.plugins.BasicTextFormatter.blockquoteHatingCommandsIE_) { + var parent = range && range.getContainerElement(); + if (parent) { + var blockquotes = goog.dom.getElementsByTagNameAndClass( + goog.dom.TagName.BLOCKQUOTE, null, parent); + + // If a blockquote contains the selection, the fix is easy: + // add a dummy div to the blockquote that isn't in the current selection. + // + // if the selection contains a blockquote, + // there appears to be no easy way to protect it from getting mangled. + // For now, we're just going to punt on this and try to + // adjust the selection so that IE does something reasonable. + // + // TODO(nicksantos): Find a better fix for this. + var bq; + for (var i = 0; i < blockquotes.length; i++) { + if (range.containsNode(blockquotes[i])) { + bq = blockquotes[i]; + break; + } + } + + var bqThatNeedsDummyDiv = bq || goog.dom.getAncestorByTagNameAndClass( + parent, goog.dom.TagName.BLOCKQUOTE); + if (bqThatNeedsDummyDiv) { + endDiv = dh.createDom(goog.dom.TagName.DIV, {style: 'height:0'}); + goog.dom.appendChild(bqThatNeedsDummyDiv, endDiv); + toRemove.push(endDiv); + + if (bq) { + range = goog.dom.Range.createFromNodes(bq, 0, endDiv, 0); + } else if (range.containsNode(endDiv)) { + // the selection might be the entire blockquote, and + // it's important that endDiv not be in the selection. + range = goog.dom.Range.createFromNodes( + range.getStartNode(), range.getStartOffset(), + endDiv, 0); + } + range.select(); + } + } + } + + // IE has a crazy bug where certain block execCommands cause it to mess with + // the DOM nodes above the contentEditable element if the selection contains + // or partially contains the last block element in the contentEditable + // element. + // Known commands: Indent, outdent, insertorderedlist, insertunorderedlist, + // Justify (all of them) + + // Both of the above are "solved" by appending a dummy div to the field + // before the execCommand and removing it after, but we don't need to do this + // if we've alread added a dummy div somewhere else. + var fieldObject = this.getFieldObject(); + if (!fieldObject.usesIframe() && !endDiv) { + if (command in + goog.editor.plugins.BasicTextFormatter.brokenExecCommandsIE_) { + var field = fieldObject.getElement(); + + // If the field is totally empty, or if the field contains only text nodes + // and the cursor is at the end of the field, then IE stills walks outside + // the contentEditable region and destroys things AND justify will not + // work. This is "solved" by adding a text node into the end of the + // field and moving the cursor before it. + if (range && range.isCollapsed() && + !goog.dom.getFirstElementChild(field)) { + // The problem only occurs if the selection is at the end of the field. + var selection = range.getTextRange(0).getBrowserRangeObject(); + var testRange = selection.duplicate(); + testRange.moveToElementText(field); + testRange.collapse(false); + + if (testRange.isEqual(selection)) { + // For reasons I really don't understand, if you use a breaking space + // here, either " " or String.fromCharCode(32), this textNode becomes + // corrupted, only after you hit ENTER to split it. It exists in the + // dom in that its parent has it as childNode and the parent's + // innerText is correct, but the node itself throws invalid argument + // errors when you try to access its data, parentNode, nextSibling, + // previousSibling or most other properties. WTF. + var nbsp = dh.createTextNode(goog.string.Unicode.NBSP); + field.appendChild(nbsp); + selection.move('character', 1); + selection.move('character', -1); + selection.select(); + toRemove.push(nbsp); + } + } + + endDiv = dh.createDom(goog.dom.TagName.DIV, {style: 'height:0'}); + goog.dom.appendChild(field, endDiv); + toRemove.push(endDiv); + } + } + + return toRemove; +}; + + +/** + * Fix a ridiculous Safari bug: the first letters of new headings + * somehow retain their original font size and weight if multiple lines are + * selected during the execCommand that turns them into headings. + * The solution is to strip these styles which are normally stripped when + * making things headings anyway. + * @private + */ +goog.editor.plugins.BasicTextFormatter.prototype.cleanUpSafariHeadings_ = + function() { + goog.iter.forEach(this.getRange_(), function(node) { + if (node.className == 'Apple-style-span') { + // These shouldn't persist after creating headings via + // a FormatBlock execCommand. + node.style.fontSize = ''; + node.style.fontWeight = ''; + } + }); +}; + + +/** + * Prevent Safari from making each list item be "1" when converting from + * unordered to ordered lists. + * (see https://bugs.webkit.org/show_bug.cgi?id=19539, fixed by 2010-04-21) + * @private + */ +goog.editor.plugins.BasicTextFormatter.prototype.fixSafariLists_ = function() { + var previousList = false; + goog.iter.forEach(this.getRange_(), function(node) { + var tagName = node.tagName; + if (tagName == goog.dom.TagName.UL || tagName == goog.dom.TagName.OL) { + // Don't disturb lists outside of the selection. If this is the first <ul> + // or <ol> in the range, we don't really want to merge the previous list + // into it, since that list isn't in the range. + if (!previousList) { + previousList = true; + return; + } + // The lists must be siblings to be merged; otherwise, indented sublists + // could be broken. + var previousElementSibling = goog.dom.getPreviousElementSibling(node); + if (!previousElementSibling) { + return; + } + // Make sure there isn't text between the two lists before they are merged + var range = node.ownerDocument.createRange(); + range.setStartAfter(previousElementSibling); + range.setEndBefore(node); + if (!goog.string.isEmptyOrWhitespace(range.toString())) { + return; + } + // Make sure both are lists of the same type (ordered or unordered) + if (previousElementSibling.nodeName == node.nodeName) { + // We must merge the previous list into this one. Moving around + // the current node will break the iterator, so we can't merge + // this list into the previous one. + while (previousElementSibling.lastChild) { + node.insertBefore(previousElementSibling.lastChild, node.firstChild); + } + previousElementSibling.parentNode.removeChild(previousElementSibling); + } + } + }); +}; + + +/** + * Sane "type" attribute values for OL elements + * @private + */ +goog.editor.plugins.BasicTextFormatter.orderedListTypes_ = { + '1' : 1, + 'a' : 1, + 'A' : 1, + 'i' : 1, + 'I' : 1 +}; + + +/** + * Sane "type" attribute values for UL elements + * @private + */ +goog.editor.plugins.BasicTextFormatter.unorderedListTypes_ = { + 'disc' : 1, + 'circle' : 1, + 'square' : 1 +}; + + +/** + * Changing an OL to a UL (or the other way around) will fail if the list + * has a type attribute (such as "UL type=disc" becoming "OL type=disc", which + * is visually identical). Most browsers will remove the type attribute + * automatically, but IE doesn't. This does it manually. + * @private + */ +goog.editor.plugins.BasicTextFormatter.prototype.fixIELists_ = function() { + // Find the lowest-level <ul> or <ol> that contains the entire range. + var range = this.getRange_(); + var container = range && range.getContainer(); + while (container && + container.tagName != goog.dom.TagName.UL && + container.tagName != goog.dom.TagName.OL) { + container = container.parentNode; + } + if (container) { + // We want the parent node of the list so that we can grab it using + // getElementsByTagName + container = container.parentNode; + } + if (!container) return; + var lists = goog.array.toArray( + container.getElementsByTagName(goog.dom.TagName.UL)); + goog.array.extend(lists, goog.array.toArray( + container.getElementsByTagName(goog.dom.TagName.OL))); + // Fix the lists + goog.array.forEach(lists, function(node) { + var type = node.type; + if (type) { + var saneTypes = + (node.tagName == goog.dom.TagName.UL ? + goog.editor.plugins.BasicTextFormatter.unorderedListTypes_ : + goog.editor.plugins.BasicTextFormatter.orderedListTypes_); + if (!saneTypes[type]) { + node.type = ''; + } + } + }); +}; + + +/** + * In WebKit, the following commands will modify the node with + * contentEditable=true if there are no block-level elements. + * @private + */ +goog.editor.plugins.BasicTextFormatter.brokenExecCommandsSafari_ = { + 'justifyCenter' : 1, + 'justifyFull' : 1, + 'justifyRight': 1, + 'justifyLeft': 1, + 'formatBlock' : 1 +}; + + +/** + * In WebKit, the following commands can hang the browser if the selection + * touches the beginning of the field. + * https://bugs.webkit.org/show_bug.cgi?id=19735 + * @private + */ +goog.editor.plugins.BasicTextFormatter.hangingExecCommandWebkit_ = { + 'insertOrderedList': 1, + 'insertUnorderedList': 1 +}; + + +/** + * Apply pre-execCommand fixes for Safari. + * @param {string} command The command to execute. + * @return {!Element|undefined} The div added to the field. + * @private + */ +goog.editor.plugins.BasicTextFormatter.prototype.applyExecCommandSafariFixes_ = + function(command) { + // See the comment on brokenExecCommandsSafari_ + var div; + if (goog.editor.plugins.BasicTextFormatter. + brokenExecCommandsSafari_[command]) { + // Add a new div at the end of the field. + // Safari knows that it would be wrong to apply text-align to the + // contentEditable element if there are non-empty block nodes in the field, + // because then it would align them too. So in this case, it will + // enclose the current selection in a block node. + div = this.getFieldDomHelper().createDom( + goog.dom.TagName.DIV, {'style': 'height: 0'}, 'x'); + goog.dom.appendChild(this.getFieldObject().getElement(), div); + } + + if (!goog.userAgent.isVersionOrHigher(534) && + goog.editor.plugins.BasicTextFormatter. + hangingExecCommandWebkit_[command]) { + // Add a new div at the beginning of the field. + var field = this.getFieldObject().getElement(); + div = this.getFieldDomHelper().createDom( + goog.dom.TagName.DIV, {'style': 'height: 0'}, 'x'); + field.insertBefore(div, field.firstChild); + } + + return div; +}; + + +/** + * Apply pre-execCommand fixes for Gecko. + * @param {string} command The command to execute. + * @private + */ +goog.editor.plugins.BasicTextFormatter.prototype.applyExecCommandGeckoFixes_ = + function(command) { + if (goog.userAgent.isVersionOrHigher('1.9') && + command.toLowerCase() == 'formatblock') { + // Firefox 3 and above throw a JS error for formatblock if the range is + // a child of the body node. Changing the selection to the BR fixes the + // problem. + // See https://bugzilla.mozilla.org/show_bug.cgi?id=481696 + var range = this.getRange_(); + var startNode = range.getStartNode(); + if (range.isCollapsed() && startNode && + startNode.tagName == goog.dom.TagName.BODY) { + var startOffset = range.getStartOffset(); + var childNode = startNode.childNodes[startOffset]; + if (childNode && childNode.tagName == goog.dom.TagName.BR) { + // Change the range using getBrowserRange() because goog.dom.TextRange + // will avoid setting <br>s directly. + // @see goog.dom.TextRange#createFromNodes + var browserRange = range.getBrowserRangeObject(); + browserRange.setStart(childNode, 0); + browserRange.setEnd(childNode, 0); + } + } + } +}; + + +/** + * Workaround for Opera bug CORE-23903. Opera sometimes fails to invalidate + * serialized CSS or innerHTML for the DOM after certain execCommands when + * styleWithCSS is on. Toggling an inline style on the elements fixes it. + * @private + */ +goog.editor.plugins.BasicTextFormatter.prototype.invalidateInlineCss_ = + function() { + var ancestors = []; + var ancestor = this.getFieldObject().getRange().getContainerElement(); + do { + ancestors.push(ancestor); + } while (ancestor = ancestor.parentNode); + var nodesInSelection = goog.iter.chain( + goog.iter.toIterator(this.getFieldObject().getRange()), + goog.iter.toIterator(ancestors)); + var containersInSelection = + goog.iter.filter(nodesInSelection, goog.editor.style.isContainer); + goog.iter.forEach(containersInSelection, function(element) { + var oldOutline = element.style.outline; + element.style.outline = '0px solid red'; + element.style.outline = oldOutline; + }); +}; + + +/** + * Work around a Gecko bug that causes inserted lists to forget the current + * font. This affects WebKit in the same way and Opera in a slightly different + * way, but this workaround only works in Gecko. + * WebKit bug: https://bugs.webkit.org/show_bug.cgi?id=19653 + * Mozilla bug: https://bugzilla.mozilla.org/show_bug.cgi?id=439966 + * Opera bug: https://bugs.opera.com/show_bug.cgi?id=340392 + * TODO: work around this issue in WebKit and Opera as well. + * @return {boolean} Whether the workaround was applied. + * @private + */ +goog.editor.plugins.BasicTextFormatter.prototype.beforeInsertListGecko_ = + function() { + var tag = this.getFieldObject().queryCommandValue( + goog.editor.Command.DEFAULT_TAG); + if (tag == goog.dom.TagName.P || tag == goog.dom.TagName.DIV) { + return false; + } + + // Prevent Firefox from forgetting current formatting + // when creating a list. + // The bug happens with a collapsed selection, but it won't + // happen when text with the desired formatting is selected. + // So, we insert some dummy text, insert the list, + // then remove the dummy text (while preserving its formatting). + // (This formatting bug also affects WebKit, but this fix + // only seems to work in Firefox) + var range = this.getRange_(); + if (range.isCollapsed() && + (range.getContainer().nodeType != goog.dom.NodeType.TEXT)) { + var tempTextNode = this.getFieldDomHelper(). + createTextNode(goog.string.Unicode.NBSP); + range.insertNode(tempTextNode, false); + goog.dom.Range.createFromNodeContents(tempTextNode).select(); + return true; + } + return false; +}; + + +// Helpers for queryCommandState + + +/** + * Get the toolbar state for the block-level elements in the given range. + * @param {goog.dom.AbstractRange} range The range to get toolbar state for. + * @return {string?} The selection block state. + * @private + */ +goog.editor.plugins.BasicTextFormatter.getSelectionBlockState_ = + function(range) { + var tagName = null; + goog.iter.forEach(range, function(node, ignore, it) { + if (!it.isEndTag()) { + // Iterate over all containers in the range, checking if they all have the + // same tagName. + var container = goog.editor.style.getContainer(node); + var thisTagName = container.tagName; + tagName = tagName || thisTagName; + + if (tagName != thisTagName) { + // If we find a container tag that doesn't match, exit right away. + tagName = null; + throw goog.iter.StopIteration; + } + + // Skip the tag. + it.skipTag(); + } + }); + + return tagName; +}; + + +/** + * Hash of suppoted justifications. + * @type {Object} + * @private + */ +goog.editor.plugins.BasicTextFormatter.SUPPORTED_JUSTIFICATIONS_ = { + 'center': 1, + 'justify': 1, + 'right': 1, + 'left': 1 +}; + + +/** + * Returns true if the current justification matches the justification + * command for the entire selection. + * @param {string} command The justification command to check for. + * @return {boolean} Whether the current justification matches the justification + * command for the entire selection. + * @private + */ +goog.editor.plugins.BasicTextFormatter.prototype.isJustification_ = + function(command) { + var alignment = command.replace('+justify', '').toLowerCase(); + if (alignment == 'full') { + alignment = 'justify'; + } + var bidiPlugin = this.getFieldObject().getPluginByClassId('Bidi'); + if (bidiPlugin) { + // BiDi aware version + + // TODO: Since getComputedStyle is not used here, this version may be even + // faster. If profiling confirms that it would be good to use this approach + // in both cases. Otherwise the bidi part should be moved into an + // execCommand so this bidi plugin dependence isn't needed here. + /** @type {Function} */ + bidiPlugin.getSelectionAlignment; + return alignment == bidiPlugin.getSelectionAlignment(); + } else { + // BiDi unaware version + var range = this.getRange_(); + if (!range) { + // When nothing is in the selection then no justification + // command matches. + return false; + } + + var parent = range.getContainerElement(); + var nodes = + goog.array.filter( + parent.childNodes, + function(node) { + return goog.editor.node.isImportant(node) && + range.containsNode(node, true); + }); + nodes = nodes.length ? nodes : [parent]; + + for (var i = 0; i < nodes.length; i++) { + var current = nodes[i]; + + // If any node in the selection is not aligned the way we are checking, + // then the justification command does not match. + var container = goog.editor.style.getContainer( + /** @type {Node} */ (current)); + if (alignment != + goog.editor.plugins.BasicTextFormatter.getNodeJustification_( + container)) { + return false; + } + } + + // If all nodes in the selection are aligned the way we are checking, + // the justification command does match. + return true; + } +}; + + +/** + * Determines the justification for a given block-level element. + * @param {Element} element The node to get justification for. + * @return {string} The justification for a given block-level node. + * @private + */ +goog.editor.plugins.BasicTextFormatter.getNodeJustification_ = + function(element) { + var value = goog.style.getComputedTextAlign(element); + // Strip preceding -moz- or -webkit- (@bug 2472589). + value = value.replace(/^-(moz|webkit)-/, ''); + + // If there is no alignment, try the inline property, + // otherwise assume left aligned. + // TODO: for rtl languages we probably need to assume right. + if (!goog.editor.plugins.BasicTextFormatter. + SUPPORTED_JUSTIFICATIONS_[value]) { + value = element.align || 'left'; + } + return /** @type {string} */ (value); +}; + + +/** + * Returns true if a selection contained in the node should set the appropriate + * toolbar state for the given nodeName, e.g. if the node is contained in a + * strong element and nodeName is "strong", then it will return true. + * @param {string} nodeName The type of node to check for. + * @return {boolean} Whether the user's selection is in the given state. + * @private + */ +goog.editor.plugins.BasicTextFormatter.prototype.isNodeInState_ = + function(nodeName) { + var range = this.getRange_(); + var node = range && range.getContainerElement(); + var ancestor = goog.dom.getAncestorByTagNameAndClass(node, nodeName); + return !!ancestor && goog.editor.node.isEditable(ancestor); +}; + + +/** + * Wrapper for browser's queryCommandState. + * @param {Document|TextRange|Range} queryObject The object to query. + * @param {string} command The command to check. + * @param {boolean=} opt_styleWithCss Set to true to enable styleWithCSS before + * performing the queryCommandState. + * @return {boolean} The command state. + * @private + */ +goog.editor.plugins.BasicTextFormatter.prototype.queryCommandStateInternal_ = + function(queryObject, command, opt_styleWithCss) { + return /** @type {boolean} */ (this.queryCommandHelper_(true, queryObject, + command, opt_styleWithCss)); +}; + + +/** + * Wrapper for browser's queryCommandValue. + * @param {Document|TextRange|Range} queryObject The object to query. + * @param {string} command The command to check. + * @param {boolean=} opt_styleWithCss Set to true to enable styleWithCSS before + * performing the queryCommandValue. + * @return {string|boolean|null} The command value. + * @private + */ +goog.editor.plugins.BasicTextFormatter.prototype.queryCommandValueInternal_ = + function(queryObject, command, opt_styleWithCss) { + return this.queryCommandHelper_(false, queryObject, + command, opt_styleWithCss); +}; + + +/** + * Helper function to perform queryCommand(Value|State). + * @param {boolean} isGetQueryCommandState True to use queryCommandState, false + * to use queryCommandValue. + * @param {Document|TextRange|Range} queryObject The object to query. + * @param {string} command The command to check. + * @param {boolean=} opt_styleWithCss Set to true to enable styleWithCSS before + * performing the queryCommand(Value|State). + * @return {string|boolean|null} The command value. + * @private + */ +goog.editor.plugins.BasicTextFormatter.prototype.queryCommandHelper_ = function( + isGetQueryCommandState, queryObject, command, opt_styleWithCss) { + command = + goog.editor.plugins.BasicTextFormatter.convertToRealExecCommand_( + command); + if (opt_styleWithCss) { + var doc = this.getDocument_(); + // Don't use this.execCommandHelper_ here, as it is more heavyweight + // and inserts a dummy div to protect against comamnds that could step + // outside the editable region, which would cause change event on + // every toolbar update. + doc.execCommand('styleWithCSS', false, true); + } + var ret = isGetQueryCommandState ? queryObject.queryCommandState(command) : + queryObject.queryCommandValue(command); + if (opt_styleWithCss) { + doc.execCommand('styleWithCSS', false, false); + } + return ret; +};
