Path: blob/trunk/third_party/closure/goog/editor/plugins/basictextformatter.js
2868 views
// Copyright 2006 The Closure Library Authors. All Rights Reserved.1//2// Licensed under the Apache License, Version 2.0 (the "License");3// you may not use this file except in compliance with the License.4// You may obtain a copy of the License at5//6// http://www.apache.org/licenses/LICENSE-2.07//8// Unless required by applicable law or agreed to in writing, software9// distributed under the License is distributed on an "AS-IS" BASIS,10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.11// See the License for the specific language governing permissions and12// limitations under the License.1314/**15* @fileoverview Functions to style text.16*17* @author [email protected] (Nick Santos)18*/1920goog.provide('goog.editor.plugins.BasicTextFormatter');21goog.provide('goog.editor.plugins.BasicTextFormatter.COMMAND');2223goog.require('goog.array');24goog.require('goog.dom');25goog.require('goog.dom.NodeType');26goog.require('goog.dom.Range');27goog.require('goog.dom.TagName');28goog.require('goog.editor.BrowserFeature');29goog.require('goog.editor.Command');30goog.require('goog.editor.Link');31goog.require('goog.editor.Plugin');32goog.require('goog.editor.node');33goog.require('goog.editor.range');34goog.require('goog.editor.style');35goog.require('goog.iter');36goog.require('goog.iter.StopIteration');37goog.require('goog.log');38goog.require('goog.object');39goog.require('goog.string');40goog.require('goog.string.Unicode');41goog.require('goog.style');42goog.require('goog.ui.editor.messages');43goog.require('goog.userAgent');44454647/**48* Functions to style text (e.g. underline, make bold, etc.)49* @constructor50* @extends {goog.editor.Plugin}51*/52goog.editor.plugins.BasicTextFormatter = function() {53goog.editor.Plugin.call(this);54};55goog.inherits(goog.editor.plugins.BasicTextFormatter, goog.editor.Plugin);565758/** @override */59goog.editor.plugins.BasicTextFormatter.prototype.getTrogClassId = function() {60return 'BTF';61};626364/**65* Logging object.66* @type {goog.log.Logger}67* @protected68* @override69*/70goog.editor.plugins.BasicTextFormatter.prototype.logger =71goog.log.getLogger('goog.editor.plugins.BasicTextFormatter');727374/**75* Commands implemented by this plugin.76* @enum {string}77*/78goog.editor.plugins.BasicTextFormatter.COMMAND = {79LINK: '+link',80CREATE_LINK: '+createLink',81FORMAT_BLOCK: '+formatBlock',82INDENT: '+indent',83OUTDENT: '+outdent',84STRIKE_THROUGH: '+strikeThrough',85HORIZONTAL_RULE: '+insertHorizontalRule',86SUBSCRIPT: '+subscript',87SUPERSCRIPT: '+superscript',88UNDERLINE: '+underline',89BOLD: '+bold',90ITALIC: '+italic',91FONT_SIZE: '+fontSize',92FONT_FACE: '+fontName',93FONT_COLOR: '+foreColor',94BACKGROUND_COLOR: '+backColor',95ORDERED_LIST: '+insertOrderedList',96UNORDERED_LIST: '+insertUnorderedList',97JUSTIFY_CENTER: '+justifyCenter',98JUSTIFY_FULL: '+justifyFull',99JUSTIFY_RIGHT: '+justifyRight',100JUSTIFY_LEFT: '+justifyLeft'101};102103104/**105* Inverse map of execCommand strings to106* {@link goog.editor.plugins.BasicTextFormatter.COMMAND} constants. Used to107* determine whether a string corresponds to a command this plugin108* handles in O(1) time.109* @type {Object}110* @private111*/112goog.editor.plugins.BasicTextFormatter.SUPPORTED_COMMANDS_ =113goog.object.transpose(goog.editor.plugins.BasicTextFormatter.COMMAND);114115116/**117* Whether the string corresponds to a command this plugin handles.118* @param {string} command Command string to check.119* @return {boolean} Whether the string corresponds to a command120* this plugin handles.121* @override122*/123goog.editor.plugins.BasicTextFormatter.prototype.isSupportedCommand = function(124command) {125// TODO(user): restore this to simple check once table editing126// is moved out into its own plugin127return command in goog.editor.plugins.BasicTextFormatter.SUPPORTED_COMMANDS_;128};129130131/**132* Array of execCommand strings which should be silent.133* @type {!Array<goog.editor.plugins.BasicTextFormatter.COMMAND>}134* @private135*/136goog.editor.plugins.BasicTextFormatter.SILENT_COMMANDS_ =137[goog.editor.plugins.BasicTextFormatter.COMMAND.CREATE_LINK];138139140/**141* Whether the string corresponds to a command that should be silent.142* @override143*/144goog.editor.plugins.BasicTextFormatter.prototype.isSilentCommand = function(145command) {146return goog.array.contains(147goog.editor.plugins.BasicTextFormatter.SILENT_COMMANDS_, command);148};149150151/**152* @return {goog.dom.AbstractRange} The closure range object that wraps the153* current user selection.154* @private155*/156goog.editor.plugins.BasicTextFormatter.prototype.getRange_ = function() {157return this.getFieldObject().getRange();158};159160161/**162* @return {!Document} The document object associated with the currently active163* field.164* @private165*/166goog.editor.plugins.BasicTextFormatter.prototype.getDocument_ = function() {167return this.getFieldDomHelper().getDocument();168};169170171/**172* Execute a user-initiated command.173* @param {string} command Command to execute.174* @param {...*} var_args For color commands, this175* should be the hex color (with the #). For FORMAT_BLOCK, this should be176* the goog.editor.plugins.BasicTextFormatter.BLOCK_COMMAND.177* It will be unused for other commands.178* @return {Object|undefined} The result of the command.179* @override180*/181goog.editor.plugins.BasicTextFormatter.prototype.execCommandInternal = function(182command, var_args) {183var preserveDir, styleWithCss, needsFormatBlockDiv, hasDummySelection;184var result;185var opt_arg = arguments[1];186187switch (command) {188case goog.editor.plugins.BasicTextFormatter.COMMAND.BACKGROUND_COLOR:189// Don't bother for no color selected, color picker is resetting itself.190if (!goog.isNull(opt_arg)) {191if (goog.editor.BrowserFeature.EATS_EMPTY_BACKGROUND_COLOR) {192this.applyBgColorManually_(opt_arg);193} else if (goog.userAgent.OPERA) {194// backColor will color the block level element instead of195// the selected span of text in Opera.196this.execCommandHelper_('hiliteColor', opt_arg);197} else {198this.execCommandHelper_(command, opt_arg);199}200}201break;202203case goog.editor.plugins.BasicTextFormatter.COMMAND.CREATE_LINK:204result = this.createLink_(arguments[1], arguments[2], arguments[3]);205break;206207case goog.editor.plugins.BasicTextFormatter.COMMAND.LINK:208result = this.toggleLink_(opt_arg);209break;210211case goog.editor.plugins.BasicTextFormatter.COMMAND.JUSTIFY_CENTER:212case goog.editor.plugins.BasicTextFormatter.COMMAND.JUSTIFY_FULL:213case goog.editor.plugins.BasicTextFormatter.COMMAND.JUSTIFY_RIGHT:214case goog.editor.plugins.BasicTextFormatter.COMMAND.JUSTIFY_LEFT:215this.justify_(command);216break;217218default:219if (goog.userAgent.IE &&220command ==221goog.editor.plugins.BasicTextFormatter.COMMAND.FORMAT_BLOCK &&222opt_arg) {223// IE requires that the argument be in the form of an opening224// tag, like <h1>, including angle brackets. WebKit will accept225// the arguemnt with or without brackets, and Firefox pre-3 supports226// only a fixed subset of tags with brackets, and prefers without.227// So we only add them IE only.228opt_arg = '<' + opt_arg + '>';229}230231if (command ==232goog.editor.plugins.BasicTextFormatter.COMMAND.FONT_COLOR &&233goog.isNull(opt_arg)) {234// If we don't have a color, then FONT_COLOR is a no-op.235break;236}237238switch (command) {239case goog.editor.plugins.BasicTextFormatter.COMMAND.INDENT:240case goog.editor.plugins.BasicTextFormatter.COMMAND.OUTDENT:241if (goog.editor.BrowserFeature.HAS_STYLE_WITH_CSS) {242if (goog.userAgent.GECKO) {243styleWithCss = true;244}245if (goog.userAgent.OPERA) {246if (command ==247goog.editor.plugins.BasicTextFormatter.COMMAND.OUTDENT) {248// styleWithCSS actually sets negative margins on <blockquote>249// to outdent them. If the command is enabled without250// styleWithCSS flipped on, then the caret is in a blockquote so251// styleWithCSS must not be used. But if the command is not252// enabled, styleWithCSS should be used so that elements such as253// a <div> with a margin-left style can still be outdented.254// (Opera bug: CORE-21118)255styleWithCss =256!this.getDocument_().queryCommandEnabled('outdent');257} else {258// Always use styleWithCSS for indenting. Otherwise, Opera will259// make separate <blockquote>s around *each* indented line,260// which adds big default <blockquote> margins between each261// indented line.262styleWithCss = true;263}264}265}266// Fall through.267268case goog.editor.plugins.BasicTextFormatter.COMMAND.ORDERED_LIST:269case goog.editor.plugins.BasicTextFormatter.COMMAND.UNORDERED_LIST:270if (goog.editor.BrowserFeature.LEAVES_P_WHEN_REMOVING_LISTS &&271this.queryCommandStateInternal_(this.getDocument_(), command)) {272// IE leaves behind P tags when unapplying lists.273// If we're not in P-mode, then we want divs274// So, unlistify, then convert the Ps into divs.275needsFormatBlockDiv =276this.getFieldObject().queryCommandValue(277goog.editor.Command.DEFAULT_TAG) != goog.dom.TagName.P;278} else if (!goog.editor.BrowserFeature.CAN_LISTIFY_BR) {279// IE doesn't convert BRed line breaks into separate list items.280// So convert the BRs to divs, then do the listify.281this.convertBreaksToDivs_();282}283284// This fix only works in Gecko.285if (goog.userAgent.GECKO &&286goog.editor.BrowserFeature.FORGETS_FORMATTING_WHEN_LISTIFYING &&287!this.queryCommandValue(command)) {288hasDummySelection |= this.beforeInsertListGecko_();289}290// Fall through to preserveDir block291292case goog.editor.plugins.BasicTextFormatter.COMMAND.FORMAT_BLOCK:293// Both FF & IE may lose directionality info. Save/restore it.294// TODO(user): Does Safari also need this?295// TODO (gmark, jparent): This isn't ideal because it uses a string296// literal, so if the plugin name changes, it would break. We need a297// better solution. See also other places in code that use298// this.getPluginByClassId('Bidi').299preserveDir = !!this.getFieldObject().getPluginByClassId('Bidi');300break;301302case goog.editor.plugins.BasicTextFormatter.COMMAND.SUBSCRIPT:303case goog.editor.plugins.BasicTextFormatter.COMMAND.SUPERSCRIPT:304if (goog.editor.BrowserFeature.NESTS_SUBSCRIPT_SUPERSCRIPT) {305// This browser nests subscript and superscript when both are306// applied, instead of canceling out the first when applying the307// second.308this.applySubscriptSuperscriptWorkarounds_(command);309}310break;311312case goog.editor.plugins.BasicTextFormatter.COMMAND.UNDERLINE:313case goog.editor.plugins.BasicTextFormatter.COMMAND.BOLD:314case goog.editor.plugins.BasicTextFormatter.COMMAND.ITALIC:315// If we are applying the formatting, then we want to have316// styleWithCSS false so that we generate html tags (like <b>). If we317// are unformatting something, we want to have styleWithCSS true so318// that we can unformat both html tags and inline styling.319// TODO(user): What about WebKit and Opera?320styleWithCss = goog.userAgent.GECKO &&321goog.editor.BrowserFeature.HAS_STYLE_WITH_CSS &&322this.queryCommandValue(command);323break;324325case goog.editor.plugins.BasicTextFormatter.COMMAND.FONT_COLOR:326case goog.editor.plugins.BasicTextFormatter.COMMAND.FONT_FACE:327// It is very expensive in FF (order of magnitude difference) to use328// font tags instead of styled spans. Whenever possible,329// force FF to use spans.330// Font size is very expensive too, but FF always uses font tags,331// regardless of which styleWithCSS value you use.332styleWithCss = goog.editor.BrowserFeature.HAS_STYLE_WITH_CSS &&333goog.userAgent.GECKO;334}335336/**337* Cases where we just use the default execCommand (in addition338* to the above fall-throughs)339* goog.editor.plugins.BasicTextFormatter.COMMAND.STRIKE_THROUGH:340* goog.editor.plugins.BasicTextFormatter.COMMAND.HORIZONTAL_RULE:341* goog.editor.plugins.BasicTextFormatter.COMMAND.SUBSCRIPT:342* goog.editor.plugins.BasicTextFormatter.COMMAND.SUPERSCRIPT:343* goog.editor.plugins.BasicTextFormatter.COMMAND.UNDERLINE:344* goog.editor.plugins.BasicTextFormatter.COMMAND.BOLD:345* goog.editor.plugins.BasicTextFormatter.COMMAND.ITALIC:346* goog.editor.plugins.BasicTextFormatter.COMMAND.FONT_SIZE:347* goog.editor.plugins.BasicTextFormatter.COMMAND.FONT_FACE:348*/349this.execCommandHelper_(command, opt_arg, preserveDir, !!styleWithCss);350351if (hasDummySelection) {352this.getDocument_().execCommand('Delete', false, true);353}354355if (needsFormatBlockDiv) {356this.getDocument_().execCommand('FormatBlock', false, '<div>');357}358}359// FF loses focus, so we have to set the focus back to the document or the360// user can't type after selecting from menu. In IE, focus is set correctly361// and resetting it here messes it up.362if (goog.userAgent.GECKO && !this.getFieldObject().inModalMode()) {363this.focusField_();364}365return result;366};367368369/**370* Focuses on the field.371* @private372*/373goog.editor.plugins.BasicTextFormatter.prototype.focusField_ = function() {374this.getFieldDomHelper().getWindow().focus();375};376377378/**379* Gets the command value.380* @param {string} command The command value to get.381* @return {string|boolean|null} The current value of the command in the given382* selection. NOTE: This return type list is not documented in MSDN or MDC383* and has been constructed from experience. Please update it384* if necessary.385* @override386*/387goog.editor.plugins.BasicTextFormatter.prototype.queryCommandValue = function(388command) {389var styleWithCss;390switch (command) {391case goog.editor.plugins.BasicTextFormatter.COMMAND.LINK:392return this.isNodeInState_(goog.dom.TagName.A);393394case goog.editor.plugins.BasicTextFormatter.COMMAND.JUSTIFY_CENTER:395case goog.editor.plugins.BasicTextFormatter.COMMAND.JUSTIFY_FULL:396case goog.editor.plugins.BasicTextFormatter.COMMAND.JUSTIFY_RIGHT:397case goog.editor.plugins.BasicTextFormatter.COMMAND.JUSTIFY_LEFT:398return this.isJustification_(command);399400case goog.editor.plugins.BasicTextFormatter.COMMAND.FORMAT_BLOCK:401// TODO(nicksantos): See if we can use queryCommandValue here.402return goog.editor.plugins.BasicTextFormatter.getSelectionBlockState_(403this.getFieldObject().getRange());404405case goog.editor.plugins.BasicTextFormatter.COMMAND.INDENT:406case goog.editor.plugins.BasicTextFormatter.COMMAND.OUTDENT:407case goog.editor.plugins.BasicTextFormatter.COMMAND.HORIZONTAL_RULE:408// TODO: See if there are reasonable results to return for409// these commands.410return false;411412case goog.editor.plugins.BasicTextFormatter.COMMAND.FONT_SIZE:413case goog.editor.plugins.BasicTextFormatter.COMMAND.FONT_FACE:414case goog.editor.plugins.BasicTextFormatter.COMMAND.FONT_COLOR:415case goog.editor.plugins.BasicTextFormatter.COMMAND.BACKGROUND_COLOR:416// We use queryCommandValue here since we don't just want to know if a417// color/fontface/fontsize is applied, we want to know WHICH one it is.418return this.queryCommandValueInternal_(419this.getDocument_(), command,420goog.editor.BrowserFeature.HAS_STYLE_WITH_CSS &&421goog.userAgent.GECKO);422423case goog.editor.plugins.BasicTextFormatter.COMMAND.UNDERLINE:424case goog.editor.plugins.BasicTextFormatter.COMMAND.BOLD:425case goog.editor.plugins.BasicTextFormatter.COMMAND.ITALIC:426styleWithCss =427goog.editor.BrowserFeature.HAS_STYLE_WITH_CSS && goog.userAgent.GECKO;428429default:430/**431* goog.editor.plugins.BasicTextFormatter.COMMAND.STRIKE_THROUGH432* goog.editor.plugins.BasicTextFormatter.COMMAND.SUBSCRIPT433* goog.editor.plugins.BasicTextFormatter.COMMAND.SUPERSCRIPT434* goog.editor.plugins.BasicTextFormatter.COMMAND.UNDERLINE435* goog.editor.plugins.BasicTextFormatter.COMMAND.BOLD436* goog.editor.plugins.BasicTextFormatter.COMMAND.ITALIC437* goog.editor.plugins.BasicTextFormatter.COMMAND.ORDERED_LIST438* goog.editor.plugins.BasicTextFormatter.COMMAND.UNORDERED_LIST439*/440// This only works for commands that use the default execCommand441return this.queryCommandStateInternal_(442this.getDocument_(), command, styleWithCss);443}444};445446447/**448* @override449*/450goog.editor.plugins.BasicTextFormatter.prototype.prepareContentsHtml = function(451html) {452// If the browser collapses empty nodes and the field has only a script453// tag in it, then it will collapse this node. Which will mean the user454// can't click into it to edit it.455if (goog.editor.BrowserFeature.COLLAPSES_EMPTY_NODES &&456html.match(/^\s*<script/i)) {457html = ' ' + html;458}459460if (goog.editor.BrowserFeature.CONVERT_TO_B_AND_I_TAGS) {461// Some browsers (FF) can't undo strong/em in some cases, but can undo b/i!462html = html.replace(/<(\/?)strong([^\w])/gi, '<$1b$2');463html = html.replace(/<(\/?)em([^\w])/gi, '<$1i$2');464}465466return html;467};468469470/**471* @override472*/473goog.editor.plugins.BasicTextFormatter.prototype.cleanContentsDom = function(474fieldCopy) {475var images = goog.dom.getElementsByTagName(goog.dom.TagName.IMG, fieldCopy);476for (var i = 0, image; image = images[i]; i++) {477if (goog.editor.BrowserFeature.SHOWS_CUSTOM_ATTRS_IN_INNER_HTML) {478// Only need to remove these attributes in IE because479// Firefox and Safari don't show custom attributes in the innerHTML.480image.removeAttribute('tabIndex');481image.removeAttribute('tabIndexSet');482goog.removeUid(image);483484// Declare oldTypeIndex for the compiler. The associated plugin may not be485// included in the compiled bundle.486/** @type {number} */ image.oldTabIndex;487488// oldTabIndex will only be set if489// goog.editor.BrowserFeature.TABS_THROUGH_IMAGES is true and we're in490// P-on-enter mode.491if (image.oldTabIndex) {492image.tabIndex = image.oldTabIndex;493}494}495}496};497498499/**500* @override501*/502goog.editor.plugins.BasicTextFormatter.prototype.cleanContentsHtml = function(503html) {504if (goog.editor.BrowserFeature.MOVES_STYLE_TO_HEAD) {505// Safari creates a new <head> element for <style> tags, so prepend their506// contents to the output.507var heads = this.getFieldObject()508.getEditableDomHelper()509.getElementsByTagNameAndClass(goog.dom.TagName.HEAD);510var stylesHtmlArr = [];511512// i starts at 1 so we don't copy in the original, legitimate <head>.513var numHeads = heads.length;514for (var i = 1; i < numHeads; ++i) {515var styles =516goog.dom.getElementsByTagName(goog.dom.TagName.STYLE, heads[i]);517var numStyles = styles.length;518for (var j = 0; j < numStyles; ++j) {519stylesHtmlArr.push(styles[j].outerHTML);520}521}522return stylesHtmlArr.join('') + html;523}524525return html;526};527528529/**530* @override531*/532goog.editor.plugins.BasicTextFormatter.prototype.handleKeyboardShortcut =533function(e, key, isModifierPressed) {534if (!isModifierPressed) {535return false;536}537var command;538switch (key) {539case 'b': // Ctrl+B540command = goog.editor.plugins.BasicTextFormatter.COMMAND.BOLD;541break;542case 'i': // Ctrl+I543command = goog.editor.plugins.BasicTextFormatter.COMMAND.ITALIC;544break;545case 'u': // Ctrl+U546command = goog.editor.plugins.BasicTextFormatter.COMMAND.UNDERLINE;547break;548case 's': // Ctrl+S549// TODO(user): This doesn't belong in here. Clients should handle550// this themselves.551// Catching control + s prevents the annoying browser save dialog552// from appearing.553return true;554}555556if (command) {557this.getFieldObject().execCommand(command);558return true;559}560561return false;562};563564565// Helpers for execCommand566567568/**569* Regular expression to match BRs in HTML. Saves the BRs' attributes in $1 for570* use with replace(). In non-IE browsers, does not match BRs adjacent to an571* opening or closing DIV or P tag, since nonrendered BR elements can occur at572* the end of block level containers in those browsers' editors.573* @type {RegExp}574* @private575*/576goog.editor.plugins.BasicTextFormatter.BR_REGEXP_ = goog.userAgent.IE ?577/<br([^\/>]*)\/?>/gi :578/<br([^\/>]*)\/?>(?!<\/(div|p)>)/gi;579580581/**582* Convert BRs in the selection to divs.583* This is only intended to be used in IE and Opera.584* @return {boolean} Whether any BR's were converted.585* @private586*/587goog.editor.plugins.BasicTextFormatter.prototype.convertBreaksToDivs_ =588function() {589if (!goog.userAgent.IE && !goog.userAgent.OPERA) {590// This function is only supported on IE and Opera.591return false;592}593var range = this.getRange_();594var parent = range.getContainerElement();595var doc = this.getDocument_();596var dom = this.getFieldDomHelper();597598goog.editor.plugins.BasicTextFormatter.BR_REGEXP_.lastIndex = 0;599// Only mess with the HTML/selection if it contains a BR.600if (goog.editor.plugins.BasicTextFormatter.BR_REGEXP_.test(601parent.innerHTML)) {602// Insert temporary markers to remember the selection.603var savedRange = range.saveUsingCarets();604605if (parent.tagName == goog.dom.TagName.P) {606// Can't append paragraphs to paragraph tags. Throws an exception in IE.607goog.editor.plugins.BasicTextFormatter.convertParagraphToDiv_(608parent, true);609} else {610// Used to do:611// IE: <div>foo<br>bar</div> --> <div>foo<p id="temp_br">bar</div>612// Opera: <div>foo<br>bar</div> --> <div>foo<p class="temp_br">bar</div>613// To fix bug 1939883, now does for both:614// <div>foo<br>bar</div> --> <div>foo<p trtempbr="temp_br">bar</div>615// TODO(user): Confirm if there's any way to skip this616// intermediate step of converting br's to p's before converting those to617// div's. The reason may be hidden in CLs 5332866 and 8530601.618var attribute = 'trtempbr';619var value = 'temp_br';620var newHtml = parent.innerHTML.replace(621goog.editor.plugins.BasicTextFormatter.BR_REGEXP_,622'<p$1 ' + attribute + '="' + value + '">');623goog.editor.node.replaceInnerHtml(parent, newHtml);624625var paragraphs = goog.array.toArray(626goog.dom.getElementsByTagName(goog.dom.TagName.P, parent));627goog.iter.forEach(paragraphs, function(paragraph) {628if (paragraph.getAttribute(attribute) == value) {629paragraph.removeAttribute(attribute);630if (goog.string.isBreakingWhitespace(631goog.dom.getTextContent(paragraph))) {632// Prevent the empty blocks from collapsing.633// A <BR> is preferable because it doesn't result in any text being634// added to the "blank" line. In IE, however, it is possible to635// place the caret after the <br>, which effectively creates a636// visible line break. Because of this, we have to resort to using a637// in IE.638var child = goog.userAgent.IE ?639doc.createTextNode(goog.string.Unicode.NBSP) :640dom.createElement(goog.dom.TagName.BR);641paragraph.appendChild(child);642}643goog.editor.plugins.BasicTextFormatter.convertParagraphToDiv_(644paragraph);645}646});647}648649// Select the previously selected text so we only listify650// the selected portion and maintain the user's selection.651savedRange.restore();652return true;653}654655return false;656};657658659/**660* Convert the given paragraph to being a div. This clobbers the661* passed-in node!662* This is only intended to be used in IE and Opera.663* @param {Node} paragraph Paragragh to convert to a div.664* @param {boolean=} opt_convertBrs If true, also convert BRs to divs.665* @private666*/667goog.editor.plugins.BasicTextFormatter.convertParagraphToDiv_ = function(668paragraph, opt_convertBrs) {669if (!goog.userAgent.IE && !goog.userAgent.OPERA) {670// This function is only supported on IE and Opera.671return;672}673var outerHTML = paragraph.outerHTML.replace(/<(\/?)p/gi, '<$1div');674if (opt_convertBrs) {675// IE fills in the closing div tag if it's missing!676outerHTML = outerHTML.replace(677goog.editor.plugins.BasicTextFormatter.BR_REGEXP_, '</div><div$1>');678}679if (goog.userAgent.OPERA && !/<\/div>$/i.test(outerHTML)) {680// Opera doesn't automatically add the closing tag, so add it if needed.681outerHTML += '</div>';682}683paragraph.outerHTML = outerHTML;684};685686687/**688* If this is a goog.editor.plugins.BasicTextFormatter.COMMAND,689* convert it to something that we can pass into execCommand,690* queryCommandState, etc.691*692* TODO(user): Consider doing away with the + and converter completely.693*694* @param {goog.editor.plugins.BasicTextFormatter.COMMAND|string}695* command A command key.696* @return {string} The equivalent execCommand command.697* @private698*/699goog.editor.plugins.BasicTextFormatter.convertToRealExecCommand_ = function(700command) {701return command.indexOf('+') == 0 ? command.substring(1) : command;702};703704705/**706* Justify the text in the selection.707* @param {string} command The type of justification to perform.708* @private709*/710goog.editor.plugins.BasicTextFormatter.prototype.justify_ = function(command) {711this.execCommandHelper_(command, null, false, true);712// Firefox cannot justify divs. In fact, justifying divs results in removing713// the divs and replacing them with brs. So "<div>foo</div><div>bar</div>"714// becomes "foo<br>bar" after alignment is applied. However, if you justify715// again, then you get "<div style='text-align: right'>foo<br>bar</div>",716// which at least looks visually correct. Since justification is (normally)717// idempotent, it isn't a problem when the selection does not contain divs to718// apply justifcation again.719if (goog.userAgent.GECKO) {720this.execCommandHelper_(command, null, false, true);721}722723// Convert all block elements in the selection to use CSS text-align724// instead of the align property. This works better because the align725// property is overridden by the CSS text-align property.726//727// Only for browsers that can't handle this by the styleWithCSS execCommand,728// which allows us to specify if we should insert align or text-align.729// TODO(user): What about WebKit or Opera?730if (!(goog.editor.BrowserFeature.HAS_STYLE_WITH_CSS &&731goog.userAgent.GECKO)) {732goog.iter.forEach(733this.getFieldObject().getRange(),734goog.editor.plugins.BasicTextFormatter.convertContainerToTextAlign_);735}736};737738739/**740* Converts the block element containing the given node to use CSS text-align741* instead of the align property.742* @param {Node} node The node to convert the container of.743* @private744*/745goog.editor.plugins.BasicTextFormatter.convertContainerToTextAlign_ = function(746node) {747var container = goog.editor.style.getContainer(node);748749// TODO(user): Fix this so that it doesn't screw up tables.750if (container.align) {751container.style.textAlign = container.align;752container.removeAttribute('align');753}754};755756757/**758* Perform an execCommand on the active document.759* @param {string} command The command to execute.760* @param {string|number|boolean|null=} opt_value Optional value.761* @param {boolean=} opt_preserveDir Set true to make sure that command does not762* change directionality of the selected text (works only if all selected763* text has the same directionality, otherwise ignored). Should not be true764* if bidi plugin is not loaded.765* @param {boolean=} opt_styleWithCss Set to true to ask the browser to use CSS766* to perform the execCommand.767* @private768*/769goog.editor.plugins.BasicTextFormatter.prototype.execCommandHelper_ = function(770command, opt_value, opt_preserveDir, opt_styleWithCss) {771// There is a bug in FF: some commands do not preserve attributes of the772// block-level elements they replace.773// This (among the rest) leads to loss of directionality information.774// For now we use a hack (when opt_preserveDir==true) to avoid this775// directionality problem in the simplest cases.776// Known affected commands: formatBlock, insertOrderedList,777// insertUnorderedList, indent, outdent.778// A similar problem occurs in IE when insertOrderedList or779// insertUnorderedList remove existing list.780var dir = null;781if (opt_preserveDir) {782dir = this.getFieldObject().queryCommandValue(goog.editor.Command.DIR_RTL) ?783'rtl' :784this.getFieldObject().queryCommandValue(goog.editor.Command.DIR_LTR) ?785'ltr' :786null;787}788789command =790goog.editor.plugins.BasicTextFormatter.convertToRealExecCommand_(command);791792var endDiv, nbsp;793if (goog.userAgent.IE) {794var ret = this.applyExecCommandIEFixes_(command);795endDiv = ret[0];796nbsp = ret[1];797}798799if (goog.userAgent.WEBKIT) {800endDiv = this.applyExecCommandSafariFixes_(command);801}802803if (goog.userAgent.GECKO) {804this.applyExecCommandGeckoFixes_(command);805}806807if (goog.editor.BrowserFeature.DOESNT_OVERRIDE_FONT_SIZE_IN_STYLE_ATTR &&808command.toLowerCase() == 'fontsize') {809this.removeFontSizeFromStyleAttrs_();810}811812var doc = this.getDocument_();813if (opt_styleWithCss && goog.editor.BrowserFeature.HAS_STYLE_WITH_CSS) {814doc.execCommand('styleWithCSS', false, true);815if (goog.userAgent.OPERA) {816this.invalidateInlineCss_();817}818}819820doc.execCommand(command, false, opt_value);821if (opt_styleWithCss && goog.editor.BrowserFeature.HAS_STYLE_WITH_CSS) {822// If we enabled styleWithCSS, turn it back off.823doc.execCommand('styleWithCSS', false, false);824}825826if (goog.userAgent.WEBKIT && !goog.userAgent.isVersionOrHigher('526') &&827command.toLowerCase() == 'formatblock' && opt_value &&828/^[<]?h\d[>]?$/i.test(opt_value)) {829this.cleanUpSafariHeadings_();830}831832if (/insert(un)?orderedlist/i.test(command)) {833// NOTE(user): This doesn't check queryCommandState because it seems to834// lie. Also, this runs for insertunorderedlist so that the the list835// isn't made up of an <ul> for each <li> - even though it looks the same,836// the markup is disgusting.837if (goog.userAgent.WEBKIT && !goog.userAgent.isVersionOrHigher(534)) {838this.fixSafariLists_();839}840if (goog.userAgent.IE) {841this.fixIELists_();842843if (nbsp) {844// Remove the text node, if applicable. Do not try to instead clobber845// the contents of the text node if it was added, or the same invalid846// node thing as above will happen. The error won't happen here, it847// will happen after you hit enter and then do anything that loops848// through the dom and tries to read that node.849goog.dom.removeNode(nbsp);850}851}852}853854if (endDiv) {855// Remove the dummy div.856goog.dom.removeNode(endDiv);857}858859// Restore directionality if required and only when unambigous (dir!=null).860if (dir) {861this.getFieldObject().execCommand(dir);862}863};864865866/**867* Applies a background color to a selection when the browser can't do the job.868*869* NOTE(nicksantos): If you think this is hacky, you should try applying870* background color in Opera. It made me cry.871*872* @param {string} bgColor backgroundColor from .formatText to .execCommand.873* @private874*/875goog.editor.plugins.BasicTextFormatter.prototype.applyBgColorManually_ =876function(bgColor) {877var needsSpaceInTextNode = goog.userAgent.GECKO;878var range = this.getFieldObject().getRange();879var textNode;880var parentTag;881if (range && range.isCollapsed()) {882// Hack to handle Firefox bug:883// https://bugzilla.mozilla.org/show_bug.cgi?id=279330884// execCommand hiliteColor in Firefox on collapsed selection creates885// a font tag onkeypress886textNode = this.getFieldDomHelper().createTextNode(887needsSpaceInTextNode ? ' ' : '');888889var containerNode = range.getStartNode();890// Check if we're inside a tag that contains the cursor and nothing else;891// if we are, don't create a dummySpan. Just use this containing tag to892// hide the 1-space selection.893// If the user sets a background color on a collapsed selection, then sets894// another one immediately, we get a span tag with a single empty TextNode.895// If the user sets a background color, types, then backspaces, we get a896// span tag with nothing inside it (container is the span).897parentTag = containerNode.nodeType == goog.dom.NodeType.ELEMENT ?898containerNode :899containerNode.parentNode;900901if (parentTag.innerHTML == '') {902// There's an Element to work with903// make the space character invisible using a CSS indent hack904parentTag.style.textIndent = '-10000px';905parentTag.appendChild(textNode);906} else {907// No Element to work with; make one908// create a span with a space character inside909// make the space character invisible using a CSS indent hack910parentTag = this.getFieldDomHelper().createDom(911goog.dom.TagName.SPAN, {'style': 'text-indent:-10000px'}, textNode);912range.replaceContentsWithNode(parentTag);913}914goog.dom.Range.createFromNodeContents(textNode).select();915}916917this.execCommandHelper_('hiliteColor', bgColor, false, true);918919if (textNode) {920// eliminate the space if necessary.921if (needsSpaceInTextNode) {922textNode.data = '';923}924925// eliminate the hack.926parentTag.style.textIndent = '';927// execCommand modified our span so we leave it in place.928}929};930931932/**933* Toggle link for the current selection:934* If selection contains a link, unlink it, return null.935* Otherwise, make selection into a link, return the link.936* @param {string=} opt_target Target for the link.937* @return {goog.editor.Link?} The resulting link, or null if a link was938* removed.939* @private940*/941goog.editor.plugins.BasicTextFormatter.prototype.toggleLink_ = function(942opt_target) {943if (!this.getFieldObject().isSelectionEditable()) {944this.focusField_();945}946947var range = this.getRange_();948// Since we wrap images in links, its possible that the user selected an949// image and clicked link, in which case we want to actually use the950// image as the selection.951var parent = range && range.getContainerElement();952var link = /** @type {Element} */ (953goog.dom.getAncestorByTagNameAndClass(parent, goog.dom.TagName.A));954if (link && goog.editor.node.isEditable(link)) {955goog.dom.flattenElement(link);956} else {957var editableLink = this.createLink_(range, '/', opt_target);958if (editableLink) {959if (!this.getFieldObject().execCommand(960goog.editor.Command.MODAL_LINK_EDITOR, editableLink)) {961var url = this.getFieldObject().getAppWindow().prompt(962goog.ui.editor.messages.MSG_LINK_TO, 'http://');963if (url) {964editableLink.setTextAndUrl(editableLink.getCurrentText() || url, url);965editableLink.placeCursorRightOf();966} else {967var savedRange = goog.editor.range.saveUsingNormalizedCarets(968goog.dom.Range.createFromNodeContents(editableLink.getAnchor()));969editableLink.removeLink();970savedRange.restore().select();971return null;972}973}974return editableLink;975}976}977return null;978};979980981/**982* Create a link out of the current selection. If nothing is selected, insert983* a new link. Otherwise, enclose the selection in a link.984* @param {goog.dom.AbstractRange} range The closure range object for the985* current selection.986* @param {string} url The url to link to.987* @param {string=} opt_target Target for the link.988* @return {goog.editor.Link?} The newly created link, or null if the link989* couldn't be created.990* @private991*/992goog.editor.plugins.BasicTextFormatter.prototype.createLink_ = function(993range, url, opt_target) {994var anchor = null;995var anchors = [];996var parent = range && range.getContainerElement();997// We do not yet support creating links around images. Instead of throwing998// lots of js errors, just fail silently.999// TODO(user): Add support for linking images.1000if (parent && parent.tagName == goog.dom.TagName.IMG) {1001return null;1002}1003// If range is not present, the editable field doesn't have focus, abort1004// creating a link.1005if (!range) {1006return null;1007}10081009if (range.isCollapsed()) {1010var textRange = range.getTextRange(0).getBrowserRangeObject();1011if (goog.editor.BrowserFeature.HAS_W3C_RANGES) {1012anchor = this.getFieldDomHelper().createElement(goog.dom.TagName.A);1013textRange.insertNode(anchor);1014} else if (goog.editor.BrowserFeature.HAS_IE_RANGES) {1015// TODO: Use goog.dom.AbstractRange's surroundContents1016textRange.pasteHTML("<a id='newLink'></a>");1017anchor = this.getFieldDomHelper().getElement('newLink');1018anchor.removeAttribute('id');1019}1020} else {1021// Create a unique identifier for the link so we can retrieve it later.1022// execCommand doesn't return the link to us, and we need a way to find1023// the newly created link in the dom, and the url is the only property1024// we have control over, so we set that to be unique and then find it.1025var uniqueId = goog.string.createUniqueString();1026this.execCommandHelper_('CreateLink', uniqueId);1027var setHrefAndLink = function(element, index, arr) {1028// We can't do straight comparison since the href can contain the1029// absolute url.1030if (goog.string.endsWith(element.href, uniqueId)) {1031anchors.push(element);1032}1033};10341035goog.array.forEach(1036goog.dom.getElementsByTagName(1037goog.dom.TagName.A,1038/** @type {!Element} */ (this.getFieldObject().getElement())),1039setHrefAndLink);1040if (anchors.length) {1041anchor = anchors.pop();1042}1043var isLikelyUrl = function(a, i, anchors) {1044return goog.editor.Link.isLikelyUrl(goog.dom.getRawTextContent(a));1045};1046if (anchors.length && goog.array.every(anchors, isLikelyUrl)) {1047for (var i = 0, a; a = anchors[i]; i++) {1048goog.editor.Link.createNewLinkFromText(a, opt_target);1049}1050anchors = null;1051}1052}10531054return goog.editor.Link.createNewLink(1055/** @type {HTMLAnchorElement} */ (anchor), url, opt_target, anchors);1056};105710581059//---------------------------------------------------------------------1060// browser fixes106110621063/**1064* The following execCommands are "broken" in some way - in IE they allow1065* the nodes outside the contentEditable region to get modified (see1066* execCommand below for more details).1067* @const1068* @private1069*/1070goog.editor.plugins.BasicTextFormatter.brokenExecCommandsIE_ = {1071'indent': 1,1072'outdent': 1,1073'insertOrderedList': 1,1074'insertUnorderedList': 1,1075'justifyCenter': 1,1076'justifyFull': 1,1077'justifyRight': 1,1078'justifyLeft': 1,1079'ltr': 1,1080'rtl': 11081};108210831084/**1085* When the following commands are executed while the selection is1086* inside a blockquote, they hose the blockquote tag in weird and1087* unintuitive ways.1088* @const1089* @private1090*/1091goog.editor.plugins.BasicTextFormatter.blockquoteHatingCommandsIE_ = {1092'insertOrderedList': 1,1093'insertUnorderedList': 11094};109510961097/**1098* Makes sure that superscript is removed before applying subscript, and vice1099* versa. Fixes {@link http://buganizer/issue?id=1173491} .1100* @param {goog.editor.plugins.BasicTextFormatter.COMMAND} command The command1101* being applied, either SUBSCRIPT or SUPERSCRIPT.1102* @private1103*/1104goog.editor.plugins.BasicTextFormatter.prototype1105.applySubscriptSuperscriptWorkarounds_ = function(command) {1106if (!this.queryCommandValue(command)) {1107// The current selection doesn't currently have the requested1108// command, so we are applying it as opposed to removing it.1109// (Note that queryCommandValue() will only return true if the1110// command is applied to the whole selection, not just part of it.1111// In this case it is fine because only if the whole selection has1112// the command applied will we be removing it and thus skipping the1113// removal of the opposite command.)1114var oppositeCommand =1115(command == goog.editor.plugins.BasicTextFormatter.COMMAND.SUBSCRIPT ?1116goog.editor.plugins.BasicTextFormatter.COMMAND.SUPERSCRIPT :1117goog.editor.plugins.BasicTextFormatter.COMMAND.SUBSCRIPT);1118var oppositeExecCommand =1119goog.editor.plugins.BasicTextFormatter.convertToRealExecCommand_(1120oppositeCommand);1121// Executing the opposite command on a selection that already has it1122// applied will cancel it out. But if the selection only has the1123// opposite command applied to a part of it, the browser will1124// normalize the selection to have the opposite command applied on1125// the whole of it.1126if (!this.queryCommandValue(oppositeCommand)) {1127// The selection doesn't have the opposite command applied to the1128// whole of it, so let's exec the opposite command to normalize1129// the selection.1130// Note: since we know both subscript and superscript commands1131// will boil down to a simple call to the browser's execCommand(),1132// for performance reasons we can do that directly instead of1133// calling execCommandHelper_(). However this is a potential for1134// bugs if the implementation of execCommandHelper_() is changed1135// to do something more int eh case of subscript and superscript.1136this.getDocument_().execCommand(oppositeExecCommand, false, null);1137}1138// Now that we know the whole selection has the opposite command1139// applied, we exec it a second time to properly remove it.1140this.getDocument_().execCommand(oppositeExecCommand, false, null);1141}1142};114311441145/**1146* Removes inline font-size styles from elements fully contained in the1147* selection, so the font tags produced by execCommand work properly.1148* See {@bug 1286408}.1149* @private1150*/1151goog.editor.plugins.BasicTextFormatter.prototype.removeFontSizeFromStyleAttrs_ =1152function() {1153// Expand the range so that we consider surrounding tags. E.g. if only the1154// text node inside a span is selected, the browser could wrap a font tag1155// around the span and leave the selection such that only the text node is1156// found when looking inside the range, not the span.1157var range = goog.editor.range.expand(1158this.getFieldObject().getRange(), this.getFieldObject().getElement());1159goog.iter.forEach(goog.iter.filter(range, function(tag, dummy, iter) {1160return iter.isStartTag() && range.containsNode(tag);1161}), function(node) {1162goog.style.setStyle(node, 'font-size', '');1163// Gecko doesn't remove empty style tags.1164if (goog.userAgent.GECKO && node.style.length == 0 &&1165node.getAttribute('style') != null) {1166node.removeAttribute('style');1167}1168});1169};117011711172/**1173* Apply pre-execCommand fixes for IE.1174* @param {string} command The command to execute.1175* @return {!Array<Node>} Array of nodes to be removed after the execCommand.1176* Will never be longer than 2 elements.1177* @private1178*/1179goog.editor.plugins.BasicTextFormatter.prototype.applyExecCommandIEFixes_ =1180function(command) {1181// IE has a crazy bug where executing list commands1182// around blockquotes cause the blockquotes to get transformed1183// into "<OL><OL>" or "<UL><UL>" tags.1184var toRemove = [];1185var endDiv = null;1186var range = this.getRange_();1187var dh = this.getFieldDomHelper();1188if (command in1189goog.editor.plugins.BasicTextFormatter.blockquoteHatingCommandsIE_) {1190var parent = range && range.getContainerElement();1191if (parent) {1192var blockquotes = goog.dom.getElementsByTagNameAndClass(1193goog.dom.TagName.BLOCKQUOTE, null, parent);11941195// If a blockquote contains the selection, the fix is easy:1196// add a dummy div to the blockquote that isn't in the current selection.1197//1198// if the selection contains a blockquote,1199// there appears to be no easy way to protect it from getting mangled.1200// For now, we're just going to punt on this and try to1201// adjust the selection so that IE does something reasonable.1202//1203// TODO(nicksantos): Find a better fix for this.1204var bq;1205for (var i = 0; i < blockquotes.length; i++) {1206if (range.containsNode(blockquotes[i])) {1207bq = blockquotes[i];1208break;1209}1210}12111212var bqThatNeedsDummyDiv = bq ||1213goog.dom.getAncestorByTagNameAndClass(1214parent, goog.dom.TagName.BLOCKQUOTE);1215if (bqThatNeedsDummyDiv) {1216endDiv = dh.createDom(goog.dom.TagName.DIV, {style: 'height:0'});1217goog.dom.appendChild(bqThatNeedsDummyDiv, endDiv);1218toRemove.push(endDiv);12191220if (bq) {1221range = goog.dom.Range.createFromNodes(bq, 0, endDiv, 0);1222} else if (range.containsNode(endDiv)) {1223// the selection might be the entire blockquote, and1224// it's important that endDiv not be in the selection.1225range = goog.dom.Range.createFromNodes(1226range.getStartNode(), range.getStartOffset(), endDiv, 0);1227}1228range.select();1229}1230}1231}12321233// IE has a crazy bug where certain block execCommands cause it to mess with1234// the DOM nodes above the contentEditable element if the selection contains1235// or partially contains the last block element in the contentEditable1236// element.1237// Known commands: Indent, outdent, insertorderedlist, insertunorderedlist,1238// Justify (all of them)12391240// Both of the above are "solved" by appending a dummy div to the field1241// before the execCommand and removing it after, but we don't need to do this1242// if we've alread added a dummy div somewhere else.1243var fieldObject = this.getFieldObject();1244if (!fieldObject.usesIframe() && !endDiv) {1245if (command in1246goog.editor.plugins.BasicTextFormatter.brokenExecCommandsIE_) {1247var field = fieldObject.getElement();12481249// If the field is totally empty, or if the field contains only text nodes1250// and the cursor is at the end of the field, then IE stills walks outside1251// the contentEditable region and destroys things AND justify will not1252// work. This is "solved" by adding a text node into the end of the1253// field and moving the cursor before it.1254if (range && range.isCollapsed() &&1255!goog.dom.getFirstElementChild(field)) {1256// The problem only occurs if the selection is at the end of the field.1257var selection = range.getTextRange(0).getBrowserRangeObject();1258var testRange = selection.duplicate();1259testRange.moveToElementText(field);1260testRange.collapse(false);12611262if (testRange.isEqual(selection)) {1263// For reasons I really don't understand, if you use a breaking space1264// here, either " " or String.fromCharCode(32), this textNode becomes1265// corrupted, only after you hit ENTER to split it. It exists in the1266// dom in that its parent has it as childNode and the parent's1267// innerText is correct, but the node itself throws invalid argument1268// errors when you try to access its data, parentNode, nextSibling,1269// previousSibling or most other properties. WTF.1270var nbsp = dh.createTextNode(goog.string.Unicode.NBSP);1271field.appendChild(nbsp);1272selection.move('character', 1);1273selection.move('character', -1);1274selection.select();1275toRemove.push(nbsp);1276}1277}12781279endDiv = dh.createDom(goog.dom.TagName.DIV, {style: 'height:0'});1280goog.dom.appendChild(field, endDiv);1281toRemove.push(endDiv);1282}1283}12841285return toRemove;1286};128712881289/**1290* Fix a ridiculous Safari bug: the first letters of new headings1291* somehow retain their original font size and weight if multiple lines are1292* selected during the execCommand that turns them into headings.1293* The solution is to strip these styles which are normally stripped when1294* making things headings anyway.1295* @private1296*/1297goog.editor.plugins.BasicTextFormatter.prototype.cleanUpSafariHeadings_ =1298function() {1299goog.iter.forEach(this.getRange_(), function(node) {1300if (node.className == 'Apple-style-span') {1301// These shouldn't persist after creating headings via1302// a FormatBlock execCommand.1303node.style.fontSize = '';1304node.style.fontWeight = '';1305}1306});1307};130813091310/**1311* Prevent Safari from making each list item be "1" when converting from1312* unordered to ordered lists.1313* (see https://bugs.webkit.org/show_bug.cgi?id=19539, fixed by 2010-04-21)1314* @private1315*/1316goog.editor.plugins.BasicTextFormatter.prototype.fixSafariLists_ = function() {1317var previousList = false;1318goog.iter.forEach(this.getRange_(), function(node) {1319var tagName = node.tagName;1320if (tagName == goog.dom.TagName.UL || tagName == goog.dom.TagName.OL) {1321// Don't disturb lists outside of the selection. If this is the first <ul>1322// or <ol> in the range, we don't really want to merge the previous list1323// into it, since that list isn't in the range.1324if (!previousList) {1325previousList = true;1326return;1327}1328// The lists must be siblings to be merged; otherwise, indented sublists1329// could be broken.1330var previousElementSibling = goog.dom.getPreviousElementSibling(node);1331if (!previousElementSibling) {1332return;1333}1334// Make sure there isn't text between the two lists before they are merged1335var range = node.ownerDocument.createRange();1336range.setStartAfter(previousElementSibling);1337range.setEndBefore(node);1338if (!goog.string.isEmptyOrWhitespace(range.toString())) {1339return;1340}1341// Make sure both are lists of the same type (ordered or unordered)1342if (previousElementSibling.nodeName == node.nodeName) {1343// We must merge the previous list into this one. Moving around1344// the current node will break the iterator, so we can't merge1345// this list into the previous one.1346while (previousElementSibling.lastChild) {1347node.insertBefore(previousElementSibling.lastChild, node.firstChild);1348}1349previousElementSibling.parentNode.removeChild(previousElementSibling);1350}1351}1352});1353};135413551356/**1357* Sane "type" attribute values for OL elements1358* @private1359*/1360goog.editor.plugins.BasicTextFormatter.orderedListTypes_ = {1361'1': 1,1362'a': 1,1363'A': 1,1364'i': 1,1365'I': 11366};136713681369/**1370* Sane "type" attribute values for UL elements1371* @private1372*/1373goog.editor.plugins.BasicTextFormatter.unorderedListTypes_ = {1374'disc': 1,1375'circle': 1,1376'square': 11377};137813791380/**1381* Changing an OL to a UL (or the other way around) will fail if the list1382* has a type attribute (such as "UL type=disc" becoming "OL type=disc", which1383* is visually identical). Most browsers will remove the type attribute1384* automatically, but IE doesn't. This does it manually.1385* @private1386*/1387goog.editor.plugins.BasicTextFormatter.prototype.fixIELists_ = function() {1388// Find the lowest-level <ul> or <ol> that contains the entire range.1389var range = this.getRange_();1390var container = range && range.getContainer();1391while (container &&1392/** @type {!Element} */ (container).tagName != goog.dom.TagName.UL &&1393/** @type {!Element} */ (container).tagName != goog.dom.TagName.OL) {1394container = container.parentNode;1395}1396if (container) {1397// We want the parent node of the list so that we can grab it using1398// getElementsByTagName1399container = container.parentNode;1400}1401if (!container) return;1402var lists = goog.array.toArray(goog.dom.getElementsByTagName(1403goog.dom.TagName.UL, /** @type {!Element} */ (container)));1404goog.array.extend(1405lists, goog.array.toArray(goog.dom.getElementsByTagName(1406goog.dom.TagName.OL, /** @type {!Element} */ (container))));1407// Fix the lists1408goog.array.forEach(lists, function(node) {1409var type = node.type;1410if (type) {1411var saneTypes =1412(node.tagName == goog.dom.TagName.UL ?1413goog.editor.plugins.BasicTextFormatter.unorderedListTypes_ :1414goog.editor.plugins.BasicTextFormatter.orderedListTypes_);1415if (!saneTypes[type]) {1416node.type = '';1417}1418}1419});1420};142114221423/**1424* In WebKit, the following commands will modify the node with1425* contentEditable=true if there are no block-level elements.1426* @private1427*/1428goog.editor.plugins.BasicTextFormatter.brokenExecCommandsSafari_ = {1429'justifyCenter': 1,1430'justifyFull': 1,1431'justifyRight': 1,1432'justifyLeft': 1,1433'formatBlock': 11434};143514361437/**1438* In WebKit, the following commands can hang the browser if the selection1439* touches the beginning of the field.1440* https://bugs.webkit.org/show_bug.cgi?id=197351441* @private1442*/1443goog.editor.plugins.BasicTextFormatter.hangingExecCommandWebkit_ = {1444'insertOrderedList': 1,1445'insertUnorderedList': 11446};144714481449/**1450* Apply pre-execCommand fixes for Safari.1451* @param {string} command The command to execute.1452* @return {!Element|undefined} The div added to the field.1453* @private1454*/1455goog.editor.plugins.BasicTextFormatter.prototype.applyExecCommandSafariFixes_ =1456function(command) {1457// See the comment on brokenExecCommandsSafari_1458var div;1459if (goog.editor.plugins.BasicTextFormatter1460.brokenExecCommandsSafari_[command]) {1461// Add a new div at the end of the field.1462// Safari knows that it would be wrong to apply text-align to the1463// contentEditable element if there are non-empty block nodes in the field,1464// because then it would align them too. So in this case, it will1465// enclose the current selection in a block node.1466div = this.getFieldDomHelper().createDom(1467goog.dom.TagName.DIV, {'style': 'height: 0'}, 'x');1468goog.dom.appendChild(this.getFieldObject().getElement(), div);1469}14701471if (!goog.userAgent.isVersionOrHigher(534) &&1472goog.editor.plugins.BasicTextFormatter1473.hangingExecCommandWebkit_[command]) {1474// Add a new div at the beginning of the field.1475var field = this.getFieldObject().getElement();1476div = this.getFieldDomHelper().createDom(1477goog.dom.TagName.DIV, {'style': 'height: 0'}, 'x');1478field.insertBefore(div, field.firstChild);1479}14801481return div;1482};148314841485/**1486* Apply pre-execCommand fixes for Gecko.1487* @param {string} command The command to execute.1488* @private1489*/1490goog.editor.plugins.BasicTextFormatter.prototype.applyExecCommandGeckoFixes_ =1491function(command) {1492if (goog.userAgent.isVersionOrHigher('1.9') &&1493command.toLowerCase() == 'formatblock') {1494// Firefox 3 and above throw a JS error for formatblock if the range is1495// a child of the body node. Changing the selection to the BR fixes the1496// problem.1497// See https://bugzilla.mozilla.org/show_bug.cgi?id=4816961498var range = this.getRange_();1499var startNode = range.getStartNode();1500if (range.isCollapsed() && startNode &&1501/** @type {!Element} */ (startNode).tagName == goog.dom.TagName.BODY) {1502var startOffset = range.getStartOffset();1503var childNode = startNode.childNodes[startOffset];1504if (childNode && childNode.tagName == goog.dom.TagName.BR) {1505// Change the range using getBrowserRange() because goog.dom.TextRange1506// will avoid setting <br>s directly.1507// @see goog.dom.TextRange#createFromNodes1508var browserRange = range.getBrowserRangeObject();1509browserRange.setStart(childNode, 0);1510browserRange.setEnd(childNode, 0);1511}1512}1513}1514};151515161517/**1518* Workaround for Opera bug CORE-23903. Opera sometimes fails to invalidate1519* serialized CSS or innerHTML for the DOM after certain execCommands when1520* styleWithCSS is on. Toggling an inline style on the elements fixes it.1521* @private1522*/1523goog.editor.plugins.BasicTextFormatter.prototype.invalidateInlineCss_ =1524function() {1525var ancestors = [];1526var ancestor = this.getFieldObject().getRange().getContainerElement();1527do {1528ancestors.push(ancestor);1529} while (ancestor = ancestor.parentNode);1530var nodesInSelection = goog.iter.chain(1531goog.iter.toIterator(this.getFieldObject().getRange()),1532goog.iter.toIterator(ancestors));1533var containersInSelection =1534goog.iter.filter(nodesInSelection, goog.editor.style.isContainer);1535goog.iter.forEach(containersInSelection, function(element) {1536var oldOutline = element.style.outline;1537element.style.outline = '0px solid red';1538element.style.outline = oldOutline;1539});1540};154115421543/**1544* Work around a Gecko bug that causes inserted lists to forget the current1545* font. This affects WebKit in the same way and Opera in a slightly different1546* way, but this workaround only works in Gecko.1547* WebKit bug: https://bugs.webkit.org/show_bug.cgi?id=196531548* Mozilla bug: https://bugzilla.mozilla.org/show_bug.cgi?id=4399661549* Opera bug: https://bugs.opera.com/show_bug.cgi?id=3403921550* TODO: work around this issue in WebKit and Opera as well.1551* @return {boolean} Whether the workaround was applied.1552* @private1553*/1554goog.editor.plugins.BasicTextFormatter.prototype.beforeInsertListGecko_ =1555function() {1556var tag =1557this.getFieldObject().queryCommandValue(goog.editor.Command.DEFAULT_TAG);1558if (tag == goog.dom.TagName.P || tag == goog.dom.TagName.DIV) {1559return false;1560}15611562// Prevent Firefox from forgetting current formatting1563// when creating a list.1564// The bug happens with a collapsed selection, but it won't1565// happen when text with the desired formatting is selected.1566// So, we insert some dummy text, insert the list,1567// then remove the dummy text (while preserving its formatting).1568// (This formatting bug also affects WebKit, but this fix1569// only seems to work in Firefox)1570var range = this.getRange_();1571if (range.isCollapsed() &&1572(range.getContainer().nodeType != goog.dom.NodeType.TEXT)) {1573var tempTextNode =1574this.getFieldDomHelper().createTextNode(goog.string.Unicode.NBSP);1575range.insertNode(tempTextNode, false);1576goog.dom.Range.createFromNodeContents(tempTextNode).select();1577return true;1578}1579return false;1580};158115821583// Helpers for queryCommandState158415851586/**1587* Get the toolbar state for the block-level elements in the given range.1588* @param {goog.dom.AbstractRange} range The range to get toolbar state for.1589* @return {string?} The selection block state.1590* @private1591*/1592goog.editor.plugins.BasicTextFormatter.getSelectionBlockState_ = function(1593range) {1594var tagName = null;1595goog.iter.forEach(range, function(node, ignore, it) {1596if (!it.isEndTag()) {1597// Iterate over all containers in the range, checking if they all have the1598// same tagName.1599var container = goog.editor.style.getContainer(node);1600var thisTagName = container.tagName;1601tagName = tagName || thisTagName;16021603if (tagName != thisTagName) {1604// If we find a container tag that doesn't match, exit right away.1605tagName = null;1606throw goog.iter.StopIteration;1607}16081609// Skip the tag.1610it.skipTag();1611}1612});16131614return tagName;1615};161616171618/**1619* Hash of suppoted justifications.1620* @type {Object}1621* @private1622*/1623goog.editor.plugins.BasicTextFormatter.SUPPORTED_JUSTIFICATIONS_ = {1624'center': 1,1625'justify': 1,1626'right': 1,1627'left': 11628};162916301631/**1632* Returns true if the current justification matches the justification1633* command for the entire selection.1634* @param {string} command The justification command to check for.1635* @return {boolean} Whether the current justification matches the justification1636* command for the entire selection.1637* @private1638*/1639goog.editor.plugins.BasicTextFormatter.prototype.isJustification_ = function(1640command) {1641var alignment = command.replace('+justify', '').toLowerCase();1642if (alignment == 'full') {1643alignment = 'justify';1644}1645var bidiPlugin = this.getFieldObject().getPluginByClassId('Bidi');1646if (bidiPlugin) {1647// BiDi aware version16481649// TODO: Since getComputedStyle is not used here, this version may be even1650// faster. If profiling confirms that it would be good to use this approach1651// in both cases. Otherwise the bidi part should be moved into an1652// execCommand so this bidi plugin dependence isn't needed here.1653/** @type {Function} */1654bidiPlugin.getSelectionAlignment;1655return alignment == bidiPlugin.getSelectionAlignment();1656} else {1657// BiDi unaware version1658var range = this.getRange_();1659if (!range) {1660// When nothing is in the selection then no justification1661// command matches.1662return false;1663}16641665var parent = range.getContainerElement();1666var nodes = goog.array.filter(parent.childNodes, function(node) {1667return goog.editor.node.isImportant(node) &&1668range.containsNode(node, true);1669});1670nodes = nodes.length ? nodes : [parent];16711672for (var i = 0; i < nodes.length; i++) {1673var current = nodes[i];16741675// If any node in the selection is not aligned the way we are checking,1676// then the justification command does not match.1677var container = goog.editor.style.getContainer(1678/** @type {Node} */ (current));1679if (alignment !=1680goog.editor.plugins.BasicTextFormatter.getNodeJustification_(1681container)) {1682return false;1683}1684}16851686// If all nodes in the selection are aligned the way we are checking,1687// the justification command does match.1688return true;1689}1690};169116921693/**1694* Determines the justification for a given block-level element.1695* @param {Element} element The node to get justification for.1696* @return {string} The justification for a given block-level node.1697* @private1698*/1699goog.editor.plugins.BasicTextFormatter.getNodeJustification_ = function(1700element) {1701var value = goog.style.getComputedTextAlign(element);1702// Strip preceding -moz- or -webkit- (@bug 2472589).1703value = value.replace(/^-(moz|webkit)-/, '');17041705// If there is no alignment, try the inline property,1706// otherwise assume left aligned.1707// TODO: for rtl languages we probably need to assume right.1708if (!goog.editor.plugins.BasicTextFormatter1709.SUPPORTED_JUSTIFICATIONS_[value]) {1710value = element.align || 'left';1711}1712return /** @type {string} */ (value);1713};171417151716/**1717* Returns true if a selection contained in the node should set the appropriate1718* toolbar state for the given nodeName, e.g. if the node is contained in a1719* strong element and nodeName is "strong", then it will return true.1720* @param {!goog.dom.TagName} nodeName The type of node to check for.1721* @return {boolean} Whether the user's selection is in the given state.1722* @private1723*/1724goog.editor.plugins.BasicTextFormatter.prototype.isNodeInState_ = function(1725nodeName) {1726var range = this.getRange_();1727var node = range && range.getContainerElement();1728var ancestor = goog.dom.getAncestorByTagNameAndClass(node, nodeName);1729return !!ancestor && goog.editor.node.isEditable(ancestor);1730};173117321733/**1734* Wrapper for browser's queryCommandState.1735* @param {Document|TextRange|Range} queryObject The object to query.1736* @param {string} command The command to check.1737* @param {boolean=} opt_styleWithCss Set to true to enable styleWithCSS before1738* performing the queryCommandState.1739* @return {boolean} The command state.1740* @private1741*/1742goog.editor.plugins.BasicTextFormatter.prototype.queryCommandStateInternal_ =1743function(queryObject, command, opt_styleWithCss) {1744return /** @type {boolean} */ (1745this.queryCommandHelper_(true, queryObject, command, opt_styleWithCss));1746};174717481749/**1750* Wrapper for browser's queryCommandValue.1751* @param {Document|TextRange|Range} queryObject The object to query.1752* @param {string} command The command to check.1753* @param {boolean=} opt_styleWithCss Set to true to enable styleWithCSS before1754* performing the queryCommandValue.1755* @return {string|boolean|null} The command value.1756* @private1757*/1758goog.editor.plugins.BasicTextFormatter.prototype.queryCommandValueInternal_ =1759function(queryObject, command, opt_styleWithCss) {1760return this.queryCommandHelper_(1761false, queryObject, command, opt_styleWithCss);1762};176317641765/**1766* Helper function to perform queryCommand(Value|State).1767* @param {boolean} isGetQueryCommandState True to use queryCommandState, false1768* to use queryCommandValue.1769* @param {Document|TextRange|Range} queryObject The object to query.1770* @param {string} command The command to check.1771* @param {boolean=} opt_styleWithCss Set to true to enable styleWithCSS before1772* performing the queryCommand(Value|State).1773* @return {string|boolean|null} The command value.1774* @private1775*/1776goog.editor.plugins.BasicTextFormatter.prototype.queryCommandHelper_ = function(1777isGetQueryCommandState, queryObject, command, opt_styleWithCss) {1778command =1779goog.editor.plugins.BasicTextFormatter.convertToRealExecCommand_(command);1780if (opt_styleWithCss) {1781var doc = this.getDocument_();1782// Don't use this.execCommandHelper_ here, as it is more heavyweight1783// and inserts a dummy div to protect against comamnds that could step1784// outside the editable region, which would cause change event on1785// every toolbar update.1786doc.execCommand('styleWithCSS', false, true);1787}1788var ret = isGetQueryCommandState ? queryObject.queryCommandState(command) :1789queryObject.queryCommandValue(command);1790if (opt_styleWithCss) {1791doc.execCommand('styleWithCSS', false, false);1792}1793return ret;1794};179517961797