1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795 |
- // 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 nicksantos@google.com (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',
- CREATE_LINK: '+createLink',
- 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_;
- };
- /**
- * Array of execCommand strings which should be silent.
- * @type {!Array<goog.editor.plugins.BasicTextFormatter.COMMAND>}
- * @private
- */
- goog.editor.plugins.BasicTextFormatter.SILENT_COMMANDS_ =
- [goog.editor.plugins.BasicTextFormatter.COMMAND.CREATE_LINK];
- /**
- * Whether the string corresponds to a command that should be silent.
- * @override
- */
- goog.editor.plugins.BasicTextFormatter.prototype.isSilentCommand = function(
- command) {
- return goog.array.contains(
- goog.editor.plugins.BasicTextFormatter.SILENT_COMMANDS_, command);
- };
- /**
- * @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.CREATE_LINK:
- result = this.createLink_(arguments[1], arguments[2], arguments[3]);
- 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 = goog.dom.getElementsByTagName(goog.dom.TagName.IMG, fieldCopy);
- 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 {number} */ 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 =
- goog.dom.getElementsByTagName(goog.dom.TagName.STYLE, heads[i]);
- 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_();
- var dom = this.getFieldDomHelper();
- 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(
- goog.dom.getElementsByTagName(goog.dom.TagName.P, parent));
- 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) :
- dom.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 comparison since the href can contain the
- // absolute url.
- if (goog.string.endsWith(element.href, uniqueId)) {
- anchors.push(element);
- }
- };
- goog.array.forEach(
- goog.dom.getElementsByTagName(
- goog.dom.TagName.A,
- /** @type {!Element} */ (this.getFieldObject().getElement())),
- 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 &&
- /** @type {!Element} */ (container).tagName != goog.dom.TagName.UL &&
- /** @type {!Element} */ (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(goog.dom.getElementsByTagName(
- goog.dom.TagName.UL, /** @type {!Element} */ (container)));
- goog.array.extend(
- lists, goog.array.toArray(goog.dom.getElementsByTagName(
- goog.dom.TagName.OL, /** @type {!Element} */ (container))));
- // 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 &&
- /** @type {!Element} */ (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 {!goog.dom.TagName} 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;
- };
|