// 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. * */ 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

, 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
// 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
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
s around *each* indented line, // which adds big default
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 ). 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, '
'); } } // 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*