flex-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From ftho...@apache.org
Subject [24/51] [abbrv] [partial] git commit: [flex-falcon] [refs/heads/JsToAs] - Added GCL extern.
Date Thu, 17 Sep 2015 15:28:39 GMT
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 = '&nbsp;' + 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
+            // &nbsp; 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;
+};


Mime
View raw message