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