flex-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From aha...@apache.org
Subject [23/83] [abbrv] [partial] git commit: [flex-falcon] [refs/heads/develop] - Added GCL extern.
Date Fri, 25 Sep 2015 04:48:52 GMT
http://git-wip-us.apache.org/repos/asf/flex-falcon/blob/e2cad6e6/externs/GCL/externs/goog/editor/plugins/blockquote.js
----------------------------------------------------------------------
diff --git a/externs/GCL/externs/goog/editor/plugins/blockquote.js b/externs/GCL/externs/goog/editor/plugins/blockquote.js
new file mode 100644
index 0000000..5f0387d
--- /dev/null
+++ b/externs/GCL/externs/goog/editor/plugins/blockquote.js
@@ -0,0 +1,451 @@
+// 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 goog.editor plugin to handle splitting block quotes.
+ *
+ * @author robbyw@google.com (Robby Walker)
+ */
+
+goog.provide('goog.editor.plugins.Blockquote');
+
+goog.require('goog.dom');
+goog.require('goog.dom.NodeType');
+goog.require('goog.dom.TagName');
+goog.require('goog.dom.classlist');
+goog.require('goog.editor.BrowserFeature');
+goog.require('goog.editor.Command');
+goog.require('goog.editor.Plugin');
+goog.require('goog.editor.node');
+goog.require('goog.functions');
+goog.require('goog.log');
+
+
+
+/**
+ * Plugin to handle splitting block quotes.  This plugin does nothing on its
+ * own and should be used in conjunction with EnterHandler or one of its
+ * subclasses.
+ * @param {boolean} requiresClassNameToSplit Whether to split only blockquotes
+ *     that have the given classname.
+ * @param {string=} opt_className The classname to apply to generated
+ *     blockquotes.  Defaults to 'tr_bq'.
+ * @constructor
+ * @extends {goog.editor.Plugin}
+ * @final
+ */
+goog.editor.plugins.Blockquote = function(requiresClassNameToSplit,
+    opt_className) {
+  goog.editor.Plugin.call(this);
+
+  /**
+   * Whether we only split blockquotes that have {@link classname}, or whether
+   * all blockquote tags should be split on enter.
+   * @type {boolean}
+   * @private
+   */
+  this.requiresClassNameToSplit_ = requiresClassNameToSplit;
+
+  /**
+   * Classname to put on blockquotes that are generated via the toolbar for
+   * blockquote, so that we can internally distinguish these from blockquotes
+   * that are used for indentation.  This classname can be over-ridden by
+   * clients for styling or other purposes.
+   * @type {string}
+   * @private
+   */
+  this.className_ = opt_className || goog.getCssName('tr_bq');
+};
+goog.inherits(goog.editor.plugins.Blockquote, goog.editor.Plugin);
+
+
+/**
+ * Command implemented by this plugin.
+ * @type {string}
+ */
+goog.editor.plugins.Blockquote.SPLIT_COMMAND = '+splitBlockquote';
+
+
+/**
+ * Class ID used to identify this plugin.
+ * @type {string}
+ */
+goog.editor.plugins.Blockquote.CLASS_ID = 'Blockquote';
+
+
+/**
+ * Logging object.
+ * @type {goog.log.Logger}
+ * @protected
+ * @override
+ */
+goog.editor.plugins.Blockquote.prototype.logger =
+    goog.log.getLogger('goog.editor.plugins.Blockquote');
+
+
+/** @override */
+goog.editor.plugins.Blockquote.prototype.getTrogClassId = function() {
+  return goog.editor.plugins.Blockquote.CLASS_ID;
+};
+
+
+/**
+ * Since our exec command is always called from elsewhere, we make it silent.
+ * @override
+ */
+goog.editor.plugins.Blockquote.prototype.isSilentCommand = goog.functions.TRUE;
+
+
+/**
+ * Checks if a node is a blockquote which can be split. A splittable blockquote
+ * meets the following criteria:
+ * <ol>
+ *   <li>Node is a blockquote element</li>
+ *   <li>Node has the blockquote classname if the classname is required to
+ *       split</li>
+ * </ol>
+ *
+ * @param {Node} node DOM node in question.
+ * @return {boolean} Whether the node is a splittable blockquote.
+ */
+goog.editor.plugins.Blockquote.prototype.isSplittableBlockquote =
+    function(node) {
+  if (node.tagName != goog.dom.TagName.BLOCKQUOTE) {
+    return false;
+  }
+
+  if (!this.requiresClassNameToSplit_) {
+    return true;
+  }
+
+  return goog.dom.classlist.contains(/** @type {!Element} */ (node),
+      this.className_);
+};
+
+
+/**
+ * Checks if a node is a blockquote element which has been setup.
+ * @param {Node} node DOM node to check.
+ * @return {boolean} Whether the node is a blockquote with the required class
+ *     name applied.
+ */
+goog.editor.plugins.Blockquote.prototype.isSetupBlockquote =
+    function(node) {
+  return node.tagName == goog.dom.TagName.BLOCKQUOTE &&
+      goog.dom.classlist.contains(/** @type {!Element} */ (node),
+          this.className_);
+};
+
+
+/**
+ * Checks if a node is a blockquote element which has not been setup yet.
+ * @param {Node} node DOM node to check.
+ * @return {boolean} Whether the node is a blockquote without the required
+ *     class name applied.
+ */
+goog.editor.plugins.Blockquote.prototype.isUnsetupBlockquote =
+    function(node) {
+  return node.tagName == goog.dom.TagName.BLOCKQUOTE &&
+      !this.isSetupBlockquote(node);
+};
+
+
+/**
+ * Gets the class name required for setup blockquotes.
+ * @return {string} The blockquote class name.
+ */
+goog.editor.plugins.Blockquote.prototype.getBlockquoteClassName = function() {
+  return this.className_;
+};
+
+
+/**
+ * Helper routine which walks up the tree to find the topmost
+ * ancestor with only a single child. The ancestor node or the original
+ * node (if no ancestor was found) is then removed from the DOM.
+ *
+ * @param {Node} node The node whose ancestors have to be searched.
+ * @param {Node} root The root node to stop the search at.
+ * @private
+ */
+goog.editor.plugins.Blockquote.findAndRemoveSingleChildAncestor_ = function(
+    node, root) {
+  var predicateFunc = function(parentNode) {
+    return parentNode != root && parentNode.childNodes.length == 1;
+  };
+  var ancestor = goog.editor.node.findHighestMatchingAncestor(node,
+      predicateFunc);
+  if (!ancestor) {
+    ancestor = node;
+  }
+  goog.dom.removeNode(ancestor);
+};
+
+
+/**
+ * Remove every nodes from the DOM tree that are all white space nodes.
+ * @param {Array<Node>} nodes Nodes to be checked.
+ * @private
+ */
+goog.editor.plugins.Blockquote.removeAllWhiteSpaceNodes_ = function(nodes) {
+  for (var i = 0; i < nodes.length; ++i) {
+    if (goog.editor.node.isEmpty(nodes[i], true)) {
+      goog.dom.removeNode(nodes[i]);
+    }
+  }
+};
+
+
+/** @override */
+goog.editor.plugins.Blockquote.prototype.isSupportedCommand = function(
+    command) {
+  return command == goog.editor.plugins.Blockquote.SPLIT_COMMAND;
+};
+
+
+/**
+ * Splits a quoted region if any.  To be called on a key press event.  When this
+ * function returns true, the event that caused it to be called should be
+ * canceled.
+ * @param {string} command The command to execute.
+ * @param {...*} var_args Single additional argument representing the current
+ *     cursor position. If BrowserFeature.HAS_W3C_RANGES it is an object with a
+ *     {@code node} key and an {@code offset} key. In other cases (legacy IE)
+ *     it is a single node.
+ * @return {boolean|undefined} Boolean true when the quoted region has been
+ *     split, false or undefined otherwise.
+ * @override
+ */
+goog.editor.plugins.Blockquote.prototype.execCommandInternal = function(
+    command, var_args) {
+  var pos = arguments[1];
+  if (command == goog.editor.plugins.Blockquote.SPLIT_COMMAND && pos &&
+      (this.className_ || !this.requiresClassNameToSplit_)) {
+    return goog.editor.BrowserFeature.HAS_W3C_RANGES ?
+        this.splitQuotedBlockW3C_(pos) :
+        this.splitQuotedBlockIE_(/** @type {Node} */ (pos));
+  }
+};
+
+
+/**
+ * Version of splitQuotedBlock_ that uses W3C ranges.
+ * @param {Object} anchorPos The current cursor position.
+ * @return {boolean} Whether the blockquote was split.
+ * @private
+ */
+goog.editor.plugins.Blockquote.prototype.splitQuotedBlockW3C_ =
+    function(anchorPos) {
+  var cursorNode = anchorPos.node;
+  var quoteNode = goog.editor.node.findTopMostEditableAncestor(
+      cursorNode.parentNode, goog.bind(this.isSplittableBlockquote, this));
+
+  var secondHalf, textNodeToRemove;
+  var insertTextNode = false;
+  // There are two special conditions that we account for here.
+  //
+  // 1. Whenever the cursor is after (one<BR>|) or just before a BR element
+  //    (one|<BR>) and the user presses enter, the second quoted block starts
+  //    with a BR which appears to the user as an extra newline. This stems
+  //    from the fact that we create two text nodes as our split boundaries
+  //    and the BR becomes a part of the second half because of this.
+  //
+  // 2. When the cursor is at the end of a text node with no siblings and
+  //    the user presses enter, the second blockquote might contain a
+  //    empty subtree that ends in a 0 length text node. We account for that
+  //    as a post-splitting operation.
+  if (quoteNode) {
+
+    // selection is in a line that has text in it
+    if (cursorNode.nodeType == goog.dom.NodeType.TEXT) {
+      if (anchorPos.offset == cursorNode.length) {
+        var siblingNode = cursorNode.nextSibling;
+
+        // This accounts for the condition where the cursor appears at the
+        // end of a text node and right before the BR eg: one|<BR>. We ensure
+        // that we split on the BR in that case.
+        if (siblingNode && siblingNode.tagName == goog.dom.TagName.BR) {
+          cursorNode = siblingNode;
+          // This might be null but splitDomTreeAt accounts for the null case.
+          secondHalf = siblingNode.nextSibling;
+        } else {
+          textNodeToRemove = cursorNode.splitText(anchorPos.offset);
+          secondHalf = textNodeToRemove;
+        }
+      } else {
+        secondHalf = cursorNode.splitText(anchorPos.offset);
+      }
+    } else if (cursorNode.tagName == goog.dom.TagName.BR) {
+      // This might be null but splitDomTreeAt accounts for the null case.
+      secondHalf = cursorNode.nextSibling;
+    } else {
+      // The selection is in a line that is empty, with more than 1 level
+      // of quote.
+      insertTextNode = true;
+    }
+  } else {
+    // Check if current node is a quote node.
+    // This will happen if user clicks in an empty line in the quote,
+    // when there is 1 level of quote.
+    if (this.isSetupBlockquote(cursorNode)) {
+      quoteNode = cursorNode;
+      insertTextNode = true;
+    }
+  }
+
+  if (insertTextNode) {
+    // Create two empty text nodes to split between.
+    cursorNode = this.insertEmptyTextNodeBeforeRange_();
+    secondHalf = this.insertEmptyTextNodeBeforeRange_();
+  }
+
+  if (!quoteNode) {
+    return false;
+  }
+
+  secondHalf = goog.editor.node.splitDomTreeAt(cursorNode, secondHalf,
+      quoteNode);
+  goog.dom.insertSiblingAfter(secondHalf, quoteNode);
+
+  // Set the insertion point.
+  var dh = this.getFieldDomHelper();
+  var tagToInsert =
+      this.getFieldObject().queryCommandValue(
+          goog.editor.Command.DEFAULT_TAG) ||
+          goog.dom.TagName.DIV;
+  var container = dh.createElement(/** @type {string} */ (tagToInsert));
+  container.innerHTML = '&nbsp;';  // Prevent the div from collapsing.
+  quoteNode.parentNode.insertBefore(container, secondHalf);
+  dh.getWindow().getSelection().collapse(container, 0);
+
+  // We need to account for the condition where the second blockquote
+  // might contain an empty DOM tree. This arises from trying to split
+  // at the end of an empty text node. We resolve this by walking up the tree
+  // till we either reach the blockquote or till we hit a node with more
+  // than one child. The resulting node is then removed from the DOM.
+  if (textNodeToRemove) {
+    goog.editor.plugins.Blockquote.findAndRemoveSingleChildAncestor_(
+        textNodeToRemove, secondHalf);
+  }
+
+  goog.editor.plugins.Blockquote.removeAllWhiteSpaceNodes_(
+      [quoteNode, secondHalf]);
+  return true;
+};
+
+
+/**
+ * Inserts an empty text node before the field's range.
+ * @return {!Node} The empty text node.
+ * @private
+ */
+goog.editor.plugins.Blockquote.prototype.insertEmptyTextNodeBeforeRange_ =
+    function() {
+  var range = this.getFieldObject().getRange();
+  var node = this.getFieldDomHelper().createTextNode('');
+  range.insertNode(node, true);
+  return node;
+};
+
+
+/**
+ * IE version of splitQuotedBlock_.
+ * @param {Node} splitNode The current cursor position.
+ * @return {boolean} Whether the blockquote was split.
+ * @private
+ */
+goog.editor.plugins.Blockquote.prototype.splitQuotedBlockIE_ =
+    function(splitNode) {
+  var dh = this.getFieldDomHelper();
+  var quoteNode = goog.editor.node.findTopMostEditableAncestor(
+      splitNode.parentNode, goog.bind(this.isSplittableBlockquote, this));
+
+  if (!quoteNode) {
+    return false;
+  }
+
+  var clone = splitNode.cloneNode(false);
+
+  // Whenever the cursor is just before a BR element (one|<BR>) and the user
+  // presses enter, the second quoted block starts with a BR which appears
+  // to the user as an extra newline. This stems from the fact that the
+  // dummy span that we create (splitNode) occurs before the BR and we split
+  // on that.
+  if (splitNode.nextSibling &&
+      splitNode.nextSibling.tagName == goog.dom.TagName.BR) {
+    splitNode = splitNode.nextSibling;
+  }
+  var secondHalf = goog.editor.node.splitDomTreeAt(splitNode, clone, quoteNode);
+  goog.dom.insertSiblingAfter(secondHalf, quoteNode);
+
+  // Set insertion point.
+  var tagToInsert =
+      this.getFieldObject().queryCommandValue(
+          goog.editor.Command.DEFAULT_TAG) ||
+          goog.dom.TagName.DIV;
+  var div = dh.createElement(/** @type {string} */ (tagToInsert));
+  quoteNode.parentNode.insertBefore(div, secondHalf);
+
+  // The div needs non-whitespace contents in order for the insertion point
+  // to get correctly inserted.
+  div.innerHTML = '&nbsp;';
+
+  // Moving the range 1 char isn't enough when you have markup.
+  // This moves the range to the end of the nbsp.
+  var range = dh.getDocument().selection.createRange();
+  range.moveToElementText(splitNode);
+  range.move('character', 2);
+  range.select();
+
+  // Remove the no-longer-necessary nbsp.
+  div.innerHTML = '';
+
+  // Clear the original selection.
+  range.pasteHTML('');
+
+  // We need to remove clone from the DOM but just removing clone alone will
+  // not suffice. Let's assume we have the following DOM structure and the
+  // cursor is placed after the first numbered list item "one".
+  //
+  // <blockquote class="gmail-quote">
+  //   <div><div>a</div><ol><li>one|</li></ol></div>
+  //   <div>b</div>
+  // </blockquote>
+  //
+  // After pressing enter, we have the following structure.
+  //
+  // <blockquote class="gmail-quote">
+  //   <div><div>a</div><ol><li>one|</li></ol></div>
+  // </blockquote>
+  // <div>&nbsp;</div>
+  // <blockquote class="gmail-quote">
+  //   <div><ol><li><span id=""></span></li></ol></div>
+  //   <div>b</div>
+  // </blockquote>
+  //
+  // The clone is contained in a subtree which should be removed. This stems
+  // from the fact that we invoke splitDomTreeAt with the dummy span
+  // as the starting splitting point and this results in the empty subtree
+  // <div><ol><li><span id=""></span></li></ol></div>.
+  //
+  // We resolve this by walking up the tree till we either reach the
+  // blockquote or till we hit a node with more than one child. The resulting
+  // node is then removed from the DOM.
+  goog.editor.plugins.Blockquote.findAndRemoveSingleChildAncestor_(
+      clone, secondHalf);
+
+  goog.editor.plugins.Blockquote.removeAllWhiteSpaceNodes_(
+      [quoteNode, secondHalf]);
+  return true;
+};

http://git-wip-us.apache.org/repos/asf/flex-falcon/blob/e2cad6e6/externs/GCL/externs/goog/editor/plugins/emoticons.js
----------------------------------------------------------------------
diff --git a/externs/GCL/externs/goog/editor/plugins/emoticons.js b/externs/GCL/externs/goog/editor/plugins/emoticons.js
new file mode 100644
index 0000000..4d0c065
--- /dev/null
+++ b/externs/GCL/externs/goog/editor/plugins/emoticons.js
@@ -0,0 +1,89 @@
+// Copyright 2009 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.
+// All Rights Reserved
+
+/**
+ * @fileoverview Plugin for generating emoticons.
+ *
+ * @author nicksantos@google.com (Nick Santos)
+ */
+
+goog.provide('goog.editor.plugins.Emoticons');
+
+goog.require('goog.dom.TagName');
+goog.require('goog.editor.Plugin');
+goog.require('goog.editor.range');
+goog.require('goog.functions');
+goog.require('goog.ui.emoji.Emoji');
+goog.require('goog.userAgent');
+
+
+
+/**
+ * Plugin for generating emoticons.
+ *
+ * @constructor
+ * @extends {goog.editor.Plugin}
+ * @final
+ */
+goog.editor.plugins.Emoticons = function() {
+  goog.editor.plugins.Emoticons.base(this, 'constructor');
+};
+goog.inherits(goog.editor.plugins.Emoticons, goog.editor.Plugin);
+
+
+/** The emoticon command. */
+goog.editor.plugins.Emoticons.COMMAND = '+emoticon';
+
+
+/** @override */
+goog.editor.plugins.Emoticons.prototype.getTrogClassId =
+    goog.functions.constant(goog.editor.plugins.Emoticons.COMMAND);
+
+
+/** @override */
+goog.editor.plugins.Emoticons.prototype.isSupportedCommand = function(
+    command) {
+  return command == goog.editor.plugins.Emoticons.COMMAND;
+};
+
+
+/**
+ * Inserts an emoticon into the editor at the cursor location. Places the
+ * cursor to the right of the inserted emoticon.
+ * @param {string} command Command to execute.
+ * @param {*=} opt_arg Emoji to insert.
+ * @return {!Object|undefined} The result of the command.
+ * @override
+ */
+goog.editor.plugins.Emoticons.prototype.execCommandInternal = function(
+    command, opt_arg) {
+  var emoji = /** @type {goog.ui.emoji.Emoji} */ (opt_arg);
+  var dom = this.getFieldDomHelper();
+  var img = dom.createDom(goog.dom.TagName.IMG, {
+    'src': emoji.getUrl(),
+    'style': 'margin:0 0.2ex;vertical-align:middle'
+  });
+  img.setAttribute(goog.ui.emoji.Emoji.ATTRIBUTE, emoji.getId());
+
+  this.getFieldObject().getRange().replaceContentsWithNode(img);
+
+  // IE8 does the right thing with the cursor, and has a js error when we try
+  // to place the cursor manually.
+  // IE9 loses the cursor when the window is focused, so focus first.
+  if (!goog.userAgent.IE || goog.userAgent.isDocumentModeOrHigher(9)) {
+    this.getFieldObject().focus();
+    goog.editor.range.placeCursorNextTo(img, false);
+  }
+};

http://git-wip-us.apache.org/repos/asf/flex-falcon/blob/e2cad6e6/externs/GCL/externs/goog/editor/plugins/enterhandler.js
----------------------------------------------------------------------
diff --git a/externs/GCL/externs/goog/editor/plugins/enterhandler.js b/externs/GCL/externs/goog/editor/plugins/enterhandler.js
new file mode 100644
index 0000000..b6ebc1d
--- /dev/null
+++ b/externs/GCL/externs/goog/editor/plugins/enterhandler.js
@@ -0,0 +1,768 @@
+// 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 Plugin to handle enter keys.
+ *
+ * @author robbyw@google.com (Robby Walker)
+ */
+
+goog.provide('goog.editor.plugins.EnterHandler');
+
+goog.require('goog.dom');
+goog.require('goog.dom.NodeOffset');
+goog.require('goog.dom.NodeType');
+goog.require('goog.dom.Range');
+goog.require('goog.dom.TagName');
+goog.require('goog.editor.BrowserFeature');
+goog.require('goog.editor.Plugin');
+goog.require('goog.editor.node');
+goog.require('goog.editor.plugins.Blockquote');
+goog.require('goog.editor.range');
+goog.require('goog.editor.style');
+goog.require('goog.events.KeyCodes');
+goog.require('goog.functions');
+goog.require('goog.object');
+goog.require('goog.string');
+goog.require('goog.userAgent');
+
+
+
+/**
+ * Plugin to handle enter keys. This does all the crazy to normalize (as much as
+ * is reasonable) what happens when you hit enter. This also handles the
+ * special casing of hitting enter in a blockquote.
+ *
+ * In IE, Webkit, and Opera, the resulting HTML uses one DIV tag per line. In
+ * Firefox, the resulting HTML uses BR tags at the end of each line.
+ *
+ * @constructor
+ * @extends {goog.editor.Plugin}
+ */
+goog.editor.plugins.EnterHandler = function() {
+  goog.editor.Plugin.call(this);
+};
+goog.inherits(goog.editor.plugins.EnterHandler, goog.editor.Plugin);
+
+
+/**
+ * The type of block level tag to add on enter, for browsers that support
+ * specifying the default block-level tag. Can be overriden by subclasses; must
+ * be either DIV or P.
+ * @type {goog.dom.TagName}
+ * @protected
+ */
+goog.editor.plugins.EnterHandler.prototype.tag = goog.dom.TagName.DIV;
+
+
+/** @override */
+goog.editor.plugins.EnterHandler.prototype.getTrogClassId = function() {
+  return 'EnterHandler';
+};
+
+
+/** @override */
+goog.editor.plugins.EnterHandler.prototype.enable = function(fieldObject) {
+  goog.editor.plugins.EnterHandler.base(this, 'enable', fieldObject);
+
+  if (goog.editor.BrowserFeature.SUPPORTS_OPERA_DEFAULTBLOCK_COMMAND &&
+      (this.tag == goog.dom.TagName.P || this.tag == goog.dom.TagName.DIV)) {
+    var doc = this.getFieldDomHelper().getDocument();
+    doc.execCommand('opera-defaultBlock', false, this.tag);
+  }
+};
+
+
+/**
+ * If the contents are empty, return the 'default' html for the field.
+ * The 'default' contents depend on the enter handling mode, so it
+ * makes the most sense in this plugin.
+ * @param {string} html The html to prepare.
+ * @return {string} The original HTML, or default contents if that
+ *    html is empty.
+ * @override
+ */
+goog.editor.plugins.EnterHandler.prototype.prepareContentsHtml = function(
+    html) {
+  if (!html || goog.string.isBreakingWhitespace(html)) {
+    return goog.editor.BrowserFeature.COLLAPSES_EMPTY_NODES ?
+        this.getNonCollapsingBlankHtml() : '';
+  }
+  return html;
+};
+
+
+/**
+ * Gets HTML with no contents that won't collapse, for browsers that
+ * collapse the empty string.
+ * @return {string} Blank html.
+ * @protected
+ */
+goog.editor.plugins.EnterHandler.prototype.getNonCollapsingBlankHtml =
+    goog.functions.constant('<br>');
+
+
+/**
+ * Internal backspace handler.
+ * @param {goog.events.Event} e The keypress event.
+ * @param {goog.dom.AbstractRange} range The closure range object.
+ * @protected
+ */
+goog.editor.plugins.EnterHandler.prototype.handleBackspaceInternal = function(e,
+    range) {
+  var field = this.getFieldObject().getElement();
+  var container = range && range.getStartNode();
+
+  if (field.firstChild == container && goog.editor.node.isEmpty(container)) {
+    e.preventDefault();
+    // TODO(user): I think we probably don't need to stopPropagation here
+    e.stopPropagation();
+  }
+};
+
+
+/**
+ * Fix paragraphs to be the correct type of node.
+ * @param {goog.events.Event} e The <enter> key event.
+ * @param {boolean} split Whether we already split up a blockquote by
+ *     manually inserting elements.
+ * @protected
+ */
+goog.editor.plugins.EnterHandler.prototype.processParagraphTagsInternal =
+    function(e, split) {
+  // Force IE to turn the node we are leaving into a DIV.  If we do turn
+  // it into a DIV, the node IE creates in response to ENTER will also be
+  // a DIV.  If we don't, it will be a P.  We handle that case
+  // in handleKeyUpIE_
+  if (goog.userAgent.IE || goog.userAgent.OPERA) {
+    this.ensureBlockIeOpera(goog.dom.TagName.DIV);
+  } else if (!split && goog.userAgent.WEBKIT) {
+    // WebKit duplicates a blockquote when the user hits enter. Let's cancel
+    // this and insert a BR instead, to make it more consistent with the other
+    // browsers.
+    var range = this.getFieldObject().getRange();
+    if (!range || !goog.editor.plugins.EnterHandler.isDirectlyInBlockquote(
+        range.getContainerElement())) {
+      return;
+    }
+
+    var dh = this.getFieldDomHelper();
+    var br = dh.createElement(goog.dom.TagName.BR);
+    range.insertNode(br, true);
+
+    // If the BR is at the end of a block element, Safari still thinks there is
+    // only one line instead of two, so we need to add another BR in that case.
+    if (goog.editor.node.isBlockTag(br.parentNode) &&
+        !goog.editor.node.skipEmptyTextNodes(br.nextSibling)) {
+      goog.dom.insertSiblingBefore(
+          dh.createElement(goog.dom.TagName.BR), br);
+    }
+
+    goog.editor.range.placeCursorNextTo(br, false);
+    e.preventDefault();
+  }
+};
+
+
+/**
+ * Determines whether the lowest containing block node is a blockquote.
+ * @param {Node} n The node.
+ * @return {boolean} Whether the deepest block ancestor of n is a blockquote.
+ */
+goog.editor.plugins.EnterHandler.isDirectlyInBlockquote = function(n) {
+  for (var current = n; current; current = current.parentNode) {
+    if (goog.editor.node.isBlockTag(current)) {
+      return current.tagName == goog.dom.TagName.BLOCKQUOTE;
+    }
+  }
+
+  return false;
+};
+
+
+/**
+ * Internal delete key handler.
+ * @param {goog.events.Event} e The keypress event.
+ * @protected
+ */
+goog.editor.plugins.EnterHandler.prototype.handleDeleteGecko = function(e) {
+  this.deleteBrGecko(e);
+};
+
+
+/**
+ * Deletes the element at the cursor if it is a BR node, and if it does, calls
+ * e.preventDefault to stop the browser from deleting. Only necessary in Gecko
+ * as a workaround for mozilla bug 205350 where deleting a BR that is followed
+ * by a block element doesn't work (the BR gets immediately replaced). We also
+ * need to account for an ill-formed cursor which occurs from us trying to
+ * stop the browser from deleting.
+ *
+ * @param {goog.events.Event} e The DELETE keypress event.
+ * @protected
+ */
+goog.editor.plugins.EnterHandler.prototype.deleteBrGecko = function(e) {
+  var range = this.getFieldObject().getRange();
+  if (range.isCollapsed()) {
+    var container = range.getEndNode();
+    if (container.nodeType == goog.dom.NodeType.ELEMENT) {
+      var nextNode = container.childNodes[range.getEndOffset()];
+      if (nextNode && nextNode.tagName == goog.dom.TagName.BR) {
+        // We want to retrieve the first non-whitespace previous sibling
+        // as we could have added an empty text node below and want to
+        // properly handle deleting a sequence of BR's.
+        var previousSibling = goog.editor.node.getPreviousSibling(nextNode);
+        var nextSibling = nextNode.nextSibling;
+
+        container.removeChild(nextNode);
+        e.preventDefault();
+
+        // When we delete a BR followed by a block level element, the cursor
+        // has a line-height which spans the height of the block level element.
+        // e.g. If we delete a BR followed by a UL, the resulting HTML will
+        // appear to the end user like:-
+        //
+        // |  * one
+        // |  * two
+        // |  * three
+        //
+        // There are a couple of cases that we have to account for in order to
+        // properly conform to what the user expects when DELETE is pressed.
+        //
+        // 1. If the BR has a previous sibling and the previous sibling is
+        //    not a block level element or a BR, we place the cursor at the
+        //    end of that.
+        // 2. If the BR doesn't have a previous sibling or the previous sibling
+        //    is a block level element or a BR, we place the cursor at the
+        //    beginning of the leftmost leaf of its next sibling.
+        if (nextSibling && goog.editor.node.isBlockTag(nextSibling)) {
+          if (previousSibling &&
+              !(previousSibling.tagName == goog.dom.TagName.BR ||
+                goog.editor.node.isBlockTag(previousSibling))) {
+            goog.dom.Range.createCaret(
+                previousSibling,
+                goog.editor.node.getLength(previousSibling)).select();
+          } else {
+            var leftMostLeaf = goog.editor.node.getLeftMostLeaf(nextSibling);
+            goog.dom.Range.createCaret(leftMostLeaf, 0).select();
+          }
+        }
+      }
+    }
+  }
+};
+
+
+/** @override */
+goog.editor.plugins.EnterHandler.prototype.handleKeyPress = function(e) {
+  // If a dialog doesn't have selectable field, Gecko 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;
+  }
+
+  // Firefox will allow the first node in an iframe to be deleted
+  // on a backspace.  Disallow it if the node is empty.
+  if (e.keyCode == goog.events.KeyCodes.BACKSPACE) {
+    this.handleBackspaceInternal(e, this.getFieldObject().getRange());
+
+  } else if (e.keyCode == goog.events.KeyCodes.ENTER) {
+    if (goog.userAgent.GECKO) {
+      if (!e.shiftKey) {
+        // Behave similarly to IE's content editable return carriage:
+        // If the shift key is down or specified by the application, insert a
+        // BR, otherwise split paragraphs
+        this.handleEnterGecko_(e);
+      }
+    } else {
+      // In Gecko-based browsers, this is handled in the handleEnterGecko_
+      // method.
+      this.getFieldObject().dispatchBeforeChange();
+      var cursorPosition = this.deleteCursorSelection_();
+
+      var split = !!this.getFieldObject().execCommand(
+          goog.editor.plugins.Blockquote.SPLIT_COMMAND, cursorPosition);
+      if (split) {
+        // TODO(user): I think we probably don't need to stopPropagation here
+        e.preventDefault();
+        e.stopPropagation();
+      }
+
+      this.releasePositionObject_(cursorPosition);
+
+      if (goog.userAgent.WEBKIT) {
+        this.handleEnterWebkitInternal(e);
+      }
+
+      this.processParagraphTagsInternal(e, split);
+      this.getFieldObject().dispatchChange();
+    }
+
+  } else if (goog.userAgent.GECKO && e.keyCode == goog.events.KeyCodes.DELETE) {
+    this.handleDeleteGecko(e);
+  }
+
+  return false;
+};
+
+
+/** @override */
+goog.editor.plugins.EnterHandler.prototype.handleKeyUp = function(e) {
+  // If a dialog doesn't have selectable field, Gecko 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;
+  }
+  this.handleKeyUpInternal(e);
+  return false;
+};
+
+
+/**
+ * Internal handler for keyup events.
+ * @param {goog.events.Event} e The key event.
+ * @protected
+ */
+goog.editor.plugins.EnterHandler.prototype.handleKeyUpInternal = function(e) {
+  if ((goog.userAgent.IE || goog.userAgent.OPERA) &&
+      e.keyCode == goog.events.KeyCodes.ENTER) {
+    this.ensureBlockIeOpera(goog.dom.TagName.DIV, true);
+  }
+};
+
+
+/**
+ * Handles an enter keypress event on fields in Gecko.
+ * @param {goog.events.BrowserEvent} e The key event.
+ * @private
+ */
+goog.editor.plugins.EnterHandler.prototype.handleEnterGecko_ = function(e) {
+  // Retrieve whether the selection is collapsed before we delete it.
+  var range = this.getFieldObject().getRange();
+  var wasCollapsed = !range || range.isCollapsed();
+  var cursorPosition = this.deleteCursorSelection_();
+
+  var handled = this.getFieldObject().execCommand(
+      goog.editor.plugins.Blockquote.SPLIT_COMMAND, cursorPosition);
+  if (handled) {
+    // TODO(user): I think we probably don't need to stopPropagation here
+    e.preventDefault();
+    e.stopPropagation();
+  }
+
+  this.releasePositionObject_(cursorPosition);
+  if (!handled) {
+    this.handleEnterAtCursorGeckoInternal(e, wasCollapsed, range);
+  }
+};
+
+
+/**
+ * Handle an enter key press in WebKit.
+ * @param {goog.events.BrowserEvent} e The key press event.
+ * @protected
+ */
+goog.editor.plugins.EnterHandler.prototype.handleEnterWebkitInternal =
+    goog.nullFunction;
+
+
+/**
+ * Handle an enter key press on collapsed selection.  handleEnterGecko_ ensures
+ * the selection is collapsed by deleting its contents if it is not.  The
+ * default implementation does nothing.
+ * @param {goog.events.BrowserEvent} e The key press event.
+ * @param {boolean} wasCollapsed Whether the selection was collapsed before
+ *     the key press.  If it was not, code before this function has already
+ *     cleared the contents of the selection.
+ * @param {goog.dom.AbstractRange} range Object representing the selection.
+ * @protected
+ */
+goog.editor.plugins.EnterHandler.prototype.handleEnterAtCursorGeckoInternal =
+    goog.nullFunction;
+
+
+/**
+ * Names of all the nodes that we don't want to turn into block nodes in IE when
+ * the user hits enter.
+ * @type {Object}
+ * @private
+ */
+goog.editor.plugins.EnterHandler.DO_NOT_ENSURE_BLOCK_NODES_ =
+    goog.object.createSet(
+        goog.dom.TagName.LI, goog.dom.TagName.DIV, goog.dom.TagName.H1,
+        goog.dom.TagName.H2, goog.dom.TagName.H3, goog.dom.TagName.H4,
+        goog.dom.TagName.H5, goog.dom.TagName.H6);
+
+
+/**
+ * Whether this is a node that contains a single BR tag and non-nbsp
+ * whitespace.
+ * @param {Node} node Node to check.
+ * @return {boolean} Whether this is an element that only contains a BR.
+ * @protected
+ */
+goog.editor.plugins.EnterHandler.isBrElem = function(node) {
+  return goog.editor.node.isEmpty(node) &&
+      node.getElementsByTagName(goog.dom.TagName.BR).length == 1;
+};
+
+
+/**
+ * Ensures all text in IE and Opera to be in the given tag in order to control
+ * Enter spacing. Call this when Enter is pressed if desired.
+ *
+ * We want to make sure the user is always inside of a block (or other nodes
+ * listed in goog.editor.plugins.EnterHandler.IGNORE_ENSURE_BLOCK_NODES_).  We
+ * listen to keypress to force nodes that the user is leaving to turn into
+ * blocks, but we also need to listen to keyup to force nodes that the user is
+ * entering to turn into blocks.
+ * Example:  html is: "<h2>foo[cursor]</h2>", and the user hits enter.  We
+ * don't want to format the h2, but we do want to format the P that is
+ * created on enter.  The P node is not available until keyup.
+ * @param {goog.dom.TagName} tag The tag name to convert to.
+ * @param {boolean=} opt_keyUp Whether the function is being called on key up.
+ *     When called on key up, the cursor is in the newly created node, so the
+ *     semantics for when to change it to a block are different.  Specifically,
+ *     if the resulting node contains only a BR, it is converted to <tag>.
+ * @protected
+ */
+goog.editor.plugins.EnterHandler.prototype.ensureBlockIeOpera = function(tag,
+    opt_keyUp) {
+  var range = this.getFieldObject().getRange();
+  var container = range.getContainer();
+  var field = this.getFieldObject().getElement();
+
+  var paragraph;
+  while (container && container != field) {
+    // We don't need to ensure a block if we are already in the same block, or
+    // in another block level node that we don't want to change the format of
+    // (unless we're handling keyUp and that block node just contains a BR).
+    var nodeName = container.nodeName;
+    // Due to @bug 2455389, the call to isBrElem needs to be inlined in the if
+    // instead of done before and saved in a variable, so that it can be
+    // short-circuited and avoid a weird IE edge case.
+    if (nodeName == tag ||
+        (goog.editor.plugins.EnterHandler.
+            DO_NOT_ENSURE_BLOCK_NODES_[nodeName] && !(opt_keyUp &&
+                goog.editor.plugins.EnterHandler.isBrElem(container)))) {
+      // Opera can create a <p> inside of a <div> in some situations,
+      // such as when breaking out of a list that is contained in a <div>.
+      if (goog.userAgent.OPERA && paragraph) {
+        if (nodeName == tag &&
+            paragraph == container.lastChild &&
+            goog.editor.node.isEmpty(paragraph)) {
+          goog.dom.insertSiblingAfter(paragraph, container);
+          goog.dom.Range.createFromNodeContents(paragraph).select();
+        }
+        break;
+      }
+      return;
+    }
+    if (goog.userAgent.OPERA && opt_keyUp && nodeName == goog.dom.TagName.P &&
+        nodeName != tag) {
+      paragraph = container;
+    }
+
+    container = container.parentNode;
+  }
+
+
+  if (goog.userAgent.IE && !goog.userAgent.isVersionOrHigher(9)) {
+    // IE (before IE9) has a bug where if the cursor is directly before a block
+    // node (e.g., the content is "foo[cursor]<blockquote>bar</blockquote>"),
+    // the FormatBlock command actually formats the "bar" instead of the "foo".
+    // This is just wrong. To work-around this, we want to move the
+    // selection back one character, and then restore it to its prior position.
+    // NOTE: We use the following "range math" to detect this situation because
+    // using Closure ranges here triggers a bug in IE that causes a crash.
+    // parent2 != parent3 ensures moving the cursor forward one character
+    // crosses at least 1 element boundary, and therefore tests if the cursor is
+    // at such a boundary.  The second check, parent3 != range.parentElement()
+    // weeds out some cases where the elements are siblings instead of cousins.
+    var needsHelp = false;
+    range = range.getBrowserRangeObject();
+    var range2 = range.duplicate();
+    range2.moveEnd('character', 1);
+    // In whitebox mode, when the cursor is at the end of the field, trying to
+    // move the end of the range will do nothing, and hence the range's text
+    // will be empty.  In this case, the cursor clearly isn't sitting just
+    // before a block node, since it isn't before anything.
+    if (range2.text.length) {
+      var parent2 = range2.parentElement();
+
+      var range3 = range2.duplicate();
+      range3.collapse(false);
+      var parent3 = range3.parentElement();
+
+      if ((needsHelp = parent2 != parent3 &&
+                       parent3 != range.parentElement())) {
+        range.move('character', -1);
+        range.select();
+      }
+    }
+  }
+
+  this.getFieldObject().getEditableDomHelper().getDocument().execCommand(
+      'FormatBlock', false, '<' + tag + '>');
+
+  if (needsHelp) {
+    range.move('character', 1);
+    range.select();
+  }
+};
+
+
+/**
+ * Deletes the content at the current cursor position.
+ * @return {!Node|!Object} Something representing the current cursor position.
+ *    See deleteCursorSelectionIE_ and deleteCursorSelectionW3C_ for details.
+ *    Should be passed to releasePositionObject_ when no longer in use.
+ * @private
+ */
+goog.editor.plugins.EnterHandler.prototype.deleteCursorSelection_ = function() {
+  return goog.editor.BrowserFeature.HAS_W3C_RANGES ?
+      this.deleteCursorSelectionW3C_() : this.deleteCursorSelectionIE_();
+};
+
+
+/**
+ * Releases the object returned by deleteCursorSelection_.
+ * @param {Node|Object} position The object returned by deleteCursorSelection_.
+ * @private
+ */
+goog.editor.plugins.EnterHandler.prototype.releasePositionObject_ =
+    function(position) {
+  if (!goog.editor.BrowserFeature.HAS_W3C_RANGES) {
+    (/** @type {Node} */ (position)).removeNode(true);
+  }
+};
+
+
+/**
+ * Delete the selection at the current cursor position, then returns a temporary
+ * node at the current position.
+ * @return {!Node} A temporary node marking the current cursor position. This
+ *     node should eventually be removed from the DOM.
+ * @private
+ */
+goog.editor.plugins.EnterHandler.prototype.deleteCursorSelectionIE_ =
+    function() {
+  var doc = this.getFieldDomHelper().getDocument();
+  var range = doc.selection.createRange();
+
+  var id = goog.string.createUniqueString();
+  range.pasteHTML('<span id="' + id + '"></span>');
+  var splitNode = doc.getElementById(id);
+  splitNode.id = '';
+  return splitNode;
+};
+
+
+/**
+ * Delete the selection at the current cursor position, then returns the node
+ * at the current position.
+ * @return {!goog.editor.range.Point} The current cursor position. Note that
+ *    unlike simulateEnterIE_, this should not be removed from the DOM.
+ * @private
+ */
+goog.editor.plugins.EnterHandler.prototype.deleteCursorSelectionW3C_ =
+    function() {
+  var range = this.getFieldObject().getRange();
+
+  // Delete the current selection if it's is non-collapsed.
+  // Although this is redundant in FF, it's necessary for Safari
+  if (!range.isCollapsed()) {
+    var shouldDelete = true;
+    // Opera selects the <br> in an empty block if there is no text node
+    // preceding it. To preserve inline formatting when pressing [enter] inside
+    // an empty block, don't delete the selection if it only selects a <br> at
+    // the end of the block.
+    // TODO(user): Move this into goog.dom.Range. It should detect this state
+    // when creating a range from the window selection and fix it in the created
+    // range.
+    if (goog.userAgent.OPERA) {
+      var startNode = range.getStartNode();
+      var startOffset = range.getStartOffset();
+      if (startNode == range.getEndNode() &&
+          // This weeds out cases where startNode is a text node.
+          startNode.lastChild &&
+          startNode.lastChild.tagName == goog.dom.TagName.BR &&
+          // If this check is true, then endOffset is implied to be
+          // startOffset + 1, because the selection is not collapsed and
+          // it starts and ends within the same element.
+          startOffset == startNode.childNodes.length - 1) {
+        shouldDelete = false;
+      }
+    }
+    if (shouldDelete) {
+      goog.editor.plugins.EnterHandler.deleteW3cRange_(range);
+    }
+  }
+
+  return goog.editor.range.getDeepEndPoint(range, true);
+};
+
+
+/**
+ * Deletes the contents of the selection from the DOM.
+ * @param {goog.dom.AbstractRange} range The range to remove contents from.
+ * @return {goog.dom.AbstractRange} The resulting range. Used for testing.
+ * @private
+ */
+goog.editor.plugins.EnterHandler.deleteW3cRange_ = function(range) {
+  if (range && !range.isCollapsed()) {
+    var reselect = true;
+    var baseNode = range.getContainerElement();
+    var nodeOffset = new goog.dom.NodeOffset(range.getStartNode(), baseNode);
+    var rangeOffset = range.getStartOffset();
+
+    // Whether the selection crosses no container boundaries.
+    var isInOneContainer =
+        goog.editor.plugins.EnterHandler.isInOneContainerW3c_(range);
+
+    // Whether the selection ends in a container it doesn't fully select.
+    var isPartialEnd = !isInOneContainer &&
+        goog.editor.plugins.EnterHandler.isPartialEndW3c_(range);
+
+    // Remove The range contents, and ensure the correct content stays selected.
+    range.removeContents();
+    var node = nodeOffset.findTargetNode(baseNode);
+    if (node) {
+      range = goog.dom.Range.createCaret(node, rangeOffset);
+    } else {
+      // This occurs when the node that would have been referenced has now been
+      // deleted and there are no other nodes in the baseNode. Thus need to
+      // set the caret to the end of the base node.
+      range =
+          goog.dom.Range.createCaret(baseNode, baseNode.childNodes.length);
+      reselect = false;
+    }
+    range.select();
+
+    // If we just deleted everything from the container, add an nbsp
+    // to the container, and leave the cursor inside of it
+    if (isInOneContainer) {
+      var container = goog.editor.style.getContainer(range.getStartNode());
+      if (goog.editor.node.isEmpty(container, true)) {
+        var html = '&nbsp;';
+        if (goog.userAgent.OPERA &&
+            container.tagName == goog.dom.TagName.LI) {
+          // Don't break Opera's native break-out-of-lists behavior.
+          html = '<br>';
+        }
+        goog.editor.node.replaceInnerHtml(container, html);
+        goog.editor.range.selectNodeStart(container.firstChild);
+        reselect = false;
+      }
+    }
+
+    if (isPartialEnd) {
+      /*
+       This code handles the following, where | is the cursor:
+         <div>a|b</div><div>c|d</div>
+       After removeContents, the remaining HTML is
+         <div>a</div><div>d</div>
+       which means the line break between the two divs remains.  This block
+       moves children of the second div in to the first div to get the correct
+       result:
+         <div>ad</div>
+
+       TODO(robbyw): Should we wrap the second div's contents in a span if they
+                     have inline style?
+      */
+      var rangeStart = goog.editor.style.getContainer(range.getStartNode());
+      var redundantContainer = goog.editor.node.getNextSibling(rangeStart);
+      if (rangeStart && redundantContainer) {
+        goog.dom.append(rangeStart, redundantContainer.childNodes);
+        goog.dom.removeNode(redundantContainer);
+      }
+    }
+
+    if (reselect) {
+      // The contents of the original range are gone, so restore the cursor
+      // position at the start of where the range once was.
+      range = goog.dom.Range.createCaret(nodeOffset.findTargetNode(baseNode),
+          rangeOffset);
+      range.select();
+    }
+  }
+
+  return range;
+};
+
+
+/**
+ * Checks whether the whole range is in a single block-level element.
+ * @param {goog.dom.AbstractRange} range The range to check.
+ * @return {boolean} Whether the whole range is in a single block-level element.
+ * @private
+ */
+goog.editor.plugins.EnterHandler.isInOneContainerW3c_ = function(range) {
+  // Find the block element containing the start of the selection.
+  var startContainer = range.getStartNode();
+  if (goog.editor.style.isContainer(startContainer)) {
+    startContainer = startContainer.childNodes[range.getStartOffset()] ||
+                     startContainer;
+  }
+  startContainer = goog.editor.style.getContainer(startContainer);
+
+  // Find the block element containing the end of the selection.
+  var endContainer = range.getEndNode();
+  if (goog.editor.style.isContainer(endContainer)) {
+    endContainer = endContainer.childNodes[range.getEndOffset()] ||
+                   endContainer;
+  }
+  endContainer = goog.editor.style.getContainer(endContainer);
+
+  // Compare the two.
+  return startContainer == endContainer;
+};
+
+
+/**
+ * Checks whether the end of the range is not at the end of a block-level
+ * element.
+ * @param {goog.dom.AbstractRange} range The range to check.
+ * @return {boolean} Whether the end of the range is not at the end of a
+ *     block-level element.
+ * @private
+ */
+goog.editor.plugins.EnterHandler.isPartialEndW3c_ = function(range) {
+  var endContainer = range.getEndNode();
+  var endOffset = range.getEndOffset();
+  var node = endContainer;
+  if (goog.editor.style.isContainer(node)) {
+    var child = node.childNodes[endOffset];
+    // Child is null when end offset is >= length, which indicates the entire
+    // container is selected.  Otherwise, we also know the entire container
+    // is selected if the selection ends at a new container.
+    if (!child ||
+        child.nodeType == goog.dom.NodeType.ELEMENT &&
+        goog.editor.style.isContainer(child)) {
+      return false;
+    }
+  }
+
+  var container = goog.editor.style.getContainer(node);
+  while (container != node) {
+    if (goog.editor.node.getNextSibling(node)) {
+      return true;
+    }
+    node = node.parentNode;
+  }
+
+  return endOffset != goog.editor.node.getLength(endContainer);
+};

http://git-wip-us.apache.org/repos/asf/flex-falcon/blob/e2cad6e6/externs/GCL/externs/goog/editor/plugins/firststrong.js
----------------------------------------------------------------------
diff --git a/externs/GCL/externs/goog/editor/plugins/firststrong.js b/externs/GCL/externs/goog/editor/plugins/firststrong.js
new file mode 100644
index 0000000..7342bad
--- /dev/null
+++ b/externs/GCL/externs/goog/editor/plugins/firststrong.js
@@ -0,0 +1,334 @@
+// Copyright 2012 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 A plugin to enable the First Strong Bidi algorithm.  The First
+ * Strong algorithm as a heuristic used to automatically set paragraph direction
+ * depending on its content.
+ *
+ * In the documentation below, a 'paragraph' is the local element which we
+ * evaluate as a whole for purposes of determining directionality. It may be a
+ * block-level element (e.g. &lt;div&gt;) or a whole list (e.g. &lt;ul&gt;).
+ *
+ * This implementation is based on, but is not identical to, the original
+ * First Strong algorithm defined in Unicode
+ * @see http://www.unicode.org/reports/tr9/
+ * The central difference from the original First Strong algorithm is that this
+ * implementation decides the paragraph direction based on the first strong
+ * character that is <em>typed</em> into the paragraph, regardless of its
+ * location in the paragraph, as opposed to the original algorithm where it is
+ * the first character in the paragraph <em>by location</em>, regardless of
+ * whether other strong characters already appear in the paragraph, further its
+ * start.
+ *
+ * <em>Please note</em> that this plugin does not perform the direction change
+ * itself. Rather, it fires editor commands upon the key up event when a
+ * direction change needs to be performed; {@code goog.editor.Command.DIR_RTL}
+ * or {@code goog.editor.Command.DIR_RTL}.
+ *
+ */
+
+goog.provide('goog.editor.plugins.FirstStrong');
+
+goog.require('goog.dom.NodeType');
+goog.require('goog.dom.TagIterator');
+goog.require('goog.dom.TagName');
+goog.require('goog.editor.Command');
+goog.require('goog.editor.Field');
+goog.require('goog.editor.Plugin');
+goog.require('goog.editor.node');
+goog.require('goog.editor.range');
+goog.require('goog.i18n.bidi');
+goog.require('goog.i18n.uChar');
+goog.require('goog.iter');
+goog.require('goog.userAgent');
+
+
+
+/**
+ * First Strong plugin.
+ * @constructor
+ * @extends {goog.editor.Plugin}
+ * @final
+ */
+goog.editor.plugins.FirstStrong = function() {
+  goog.editor.plugins.FirstStrong.base(this, 'constructor');
+
+  /**
+   * Indicates whether or not the cursor is in a paragraph we have not yet
+   * finished evaluating for directionality. This is set to true whenever the
+   * cursor is moved, and set to false after seeing a strong character in the
+   * paragraph the cursor is currently in.
+   *
+   * @type {boolean}
+   * @private
+   */
+  this.isNewBlock_ = true;
+
+  /**
+   * Indicates whether or not the current paragraph the cursor is in should be
+   * set to Right-To-Left directionality.
+   *
+   * @type {boolean}
+   * @private
+   */
+  this.switchToRtl_ = false;
+
+  /**
+   * Indicates whether or not the current paragraph the cursor is in should be
+   * set to Left-To-Right directionality.
+   *
+   * @type {boolean}
+   * @private
+   */
+  this.switchToLtr_ = false;
+};
+goog.inherits(goog.editor.plugins.FirstStrong, goog.editor.Plugin);
+
+
+/** @override */
+goog.editor.plugins.FirstStrong.prototype.getTrogClassId = function() {
+  return 'FirstStrong';
+};
+
+
+/** @override */
+goog.editor.plugins.FirstStrong.prototype.queryCommandValue =
+    function(command) {
+  return false;
+};
+
+
+/** @override */
+goog.editor.plugins.FirstStrong.prototype.handleSelectionChange =
+    function(e, node) {
+  this.isNewBlock_ = true;
+  return false;
+};
+
+
+/**
+ * The name of the attribute which records the input text.
+ *
+ * @type {string}
+ * @const
+ */
+goog.editor.plugins.FirstStrong.INPUT_ATTRIBUTE = 'fs-input';
+
+
+/** @override */
+goog.editor.plugins.FirstStrong.prototype.handleKeyPress = function(e) {
+  if (goog.editor.Field.SELECTION_CHANGE_KEYCODES[e.keyCode]) {
+    // Key triggered selection change event (e.g. on ENTER) is throttled and a
+    // later LTR/RTL strong keypress may come before it. Need to capture it.
+    this.isNewBlock_ = true;
+    return false;  // A selection-changing key is not LTR/RTL strong.
+  }
+  if (!this.isNewBlock_) {
+    return false;  // We've already determined this paragraph's direction.
+  }
+  // Ignore non-character key press events.
+  if (e.ctrlKey || e.metaKey) {
+    return false;
+  }
+  var newInput = goog.i18n.uChar.fromCharCode(e.charCode);
+
+  // IME's may return 0 for the charCode, which is a legitimate, non-Strong
+  // charCode, or they may return an illegal charCode (for which newInput will
+  // be false).
+  if (!newInput || !e.charCode) {
+    var browserEvent = e.getBrowserEvent();
+    if (browserEvent) {
+      if (goog.userAgent.IE && browserEvent['getAttribute']) {
+        newInput = browserEvent['getAttribute'](
+            goog.editor.plugins.FirstStrong.INPUT_ATTRIBUTE);
+      } else {
+        newInput = browserEvent[
+            goog.editor.plugins.FirstStrong.INPUT_ATTRIBUTE];
+      }
+    }
+  }
+
+  if (!newInput) {
+    return false;  // Unrecognized key.
+  }
+
+  var isLtr = goog.i18n.bidi.isLtrChar(newInput);
+  var isRtl = !isLtr && goog.i18n.bidi.isRtlChar(newInput);
+  if (!isLtr && !isRtl) {
+    return false;  // This character cannot change anything (it is not Strong).
+  }
+  // This character is Strongly LTR or Strongly RTL. We might switch direction
+  // on it now, but in any case we do not need to check any more characters in
+  // this paragraph after it.
+  this.isNewBlock_ = false;
+
+  // Are there no Strong characters already in the paragraph?
+  if (this.isNeutralBlock_()) {
+    this.switchToRtl_ = isRtl;
+    this.switchToLtr_ = isLtr;
+  }
+  return false;
+};
+
+
+/**
+ * Calls the flip directionality commands.  This is done here so things go into
+ * the redo-undo stack at the expected order; fist enter the input, then flip
+ * directionality.
+ * @override
+ */
+goog.editor.plugins.FirstStrong.prototype.handleKeyUp = function(e) {
+  if (this.switchToRtl_) {
+    var field = this.getFieldObject();
+    field.dispatchChange(true);
+    field.execCommand(goog.editor.Command.DIR_RTL);
+    this.switchToRtl_ = false;
+  } else if (this.switchToLtr_) {
+    var field = this.getFieldObject();
+    field.dispatchChange(true);
+    field.execCommand(goog.editor.Command.DIR_LTR);
+    this.switchToLtr_ = false;
+  }
+  return false;
+};
+
+
+/**
+ * @return {Element} The lowest Block element ancestor of the node where the
+ *     next character will be placed.
+ * @private
+ */
+goog.editor.plugins.FirstStrong.prototype.getBlockAncestor_ = function() {
+  var start = this.getFieldObject().getRange().getStartNode();
+  // Go up in the DOM until we reach a Block element.
+  while (!goog.editor.plugins.FirstStrong.isBlock_(start)) {
+    start = start.parentNode;
+  }
+  return /** @type {Element} */ (start);
+};
+
+
+/**
+ * @return {boolean} Whether the paragraph where the next character will be
+ *     entered contains only non-Strong characters.
+ * @private
+ */
+goog.editor.plugins.FirstStrong.prototype.isNeutralBlock_ = function() {
+  var root = this.getBlockAncestor_();
+  // The exact node with the cursor location. Simply calling getStartNode() on
+  // the range only returns the containing block node.
+  var cursor = goog.editor.range.getDeepEndPoint(
+      this.getFieldObject().getRange(), false).node;
+
+  // In FireFox the BR tag also represents a change in paragraph if not inside a
+  // list. So we need special handling to only look at the sub-block between
+  // BR elements.
+  var blockFunction = (goog.userAgent.GECKO &&
+      !this.isList_(root)) ?
+          goog.editor.plugins.FirstStrong.isGeckoBlock_ :
+          goog.editor.plugins.FirstStrong.isBlock_;
+  var paragraph = this.getTextAround_(root, cursor,
+      blockFunction);
+  // Not using {@code goog.i18n.bidi.isNeutralText} as it contains additional,
+  // unwanted checks to the content.
+  return !goog.i18n.bidi.hasAnyLtr(paragraph) &&
+      !goog.i18n.bidi.hasAnyRtl(paragraph);
+};
+
+
+/**
+ * Checks if an element is a list element ('UL' or 'OL').
+ *
+ * @param {Element} element The element to test.
+ * @return {boolean} Whether the element is a list element ('UL' or 'OL').
+ * @private
+ */
+goog.editor.plugins.FirstStrong.prototype.isList_ = function(element) {
+  if (!element) {
+    return false;
+  }
+  var tagName = element.tagName;
+  return tagName == goog.dom.TagName.UL || tagName == goog.dom.TagName.OL;
+};
+
+
+/**
+ * Returns the text within the local paragraph around the cursor.
+ * Notice that for GECKO a BR represents a pargraph change despite not being a
+ * block element.
+ *
+ * @param {Element} root The first block element ancestor of the node the cursor
+ *     is in.
+ * @param {Node} cursorLocation Node where the cursor currently is, marking the
+ *     paragraph whose text we will return.
+ * @param {function(Node): boolean} isParagraphBoundary The function to
+ *     determine if a node represents the start or end of the paragraph.
+ * @return {string} the text in the paragraph around the cursor location.
+ * @private
+ */
+goog.editor.plugins.FirstStrong.prototype.getTextAround_ = function(root,
+    cursorLocation, isParagraphBoundary) {
+  // The buffer where we're collecting the text.
+  var buffer = [];
+  // Have we reached the cursor yet, or are we still before it?
+  var pastCursorLocation = false;
+
+  if (root && cursorLocation) {
+    goog.iter.some(new goog.dom.TagIterator(root), function(node) {
+      if (node == cursorLocation) {
+        pastCursorLocation = true;
+      } else if (isParagraphBoundary(node)) {
+        if (pastCursorLocation) {
+          // This is the end of the paragraph containing the cursor. We're done.
+          return true;
+        } else {
+          // All we collected so far does not count; it was in a previous
+          // paragraph that did not contain the cursor.
+          buffer = [];
+        }
+      }
+      if (node.nodeType == goog.dom.NodeType.TEXT) {
+        buffer.push(node.nodeValue);
+      }
+      return false;  // Keep going.
+    });
+  }
+  return buffer.join('');
+};
+
+
+/**
+ * @param {Node} node Node to check.
+ * @return {boolean} Does the given node represent a Block element? Notice we do
+ *     not consider list items as Block elements in the algorithm.
+ * @private
+ */
+goog.editor.plugins.FirstStrong.isBlock_ = function(node) {
+  return !!node && goog.editor.node.isBlockTag(node) &&
+      node.tagName != goog.dom.TagName.LI;
+};
+
+
+/**
+ * @param {Node} node Node to check.
+ * @return {boolean} Does the given node represent a Block element from the
+ *     point of view of FireFox? Notice we do not consider list items as Block
+ *     elements in the algorithm.
+ * @private
+ */
+goog.editor.plugins.FirstStrong.isGeckoBlock_ = function(node) {
+  return !!node && (node.tagName == goog.dom.TagName.BR ||
+      goog.editor.plugins.FirstStrong.isBlock_(node));
+};

http://git-wip-us.apache.org/repos/asf/flex-falcon/blob/e2cad6e6/externs/GCL/externs/goog/editor/plugins/headerformatter.js
----------------------------------------------------------------------
diff --git a/externs/GCL/externs/goog/editor/plugins/headerformatter.js b/externs/GCL/externs/goog/editor/plugins/headerformatter.js
new file mode 100644
index 0000000..fa4bb1f
--- /dev/null
+++ b/externs/GCL/externs/goog/editor/plugins/headerformatter.js
@@ -0,0 +1,96 @@
+// 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 Handles applying header styles to text.
+ *
+ */
+
+goog.provide('goog.editor.plugins.HeaderFormatter');
+
+goog.require('goog.editor.Command');
+goog.require('goog.editor.Plugin');
+goog.require('goog.userAgent');
+
+
+
+/**
+ * Applies header styles to text.
+ * @constructor
+ * @extends {goog.editor.Plugin}
+ * @final
+ */
+goog.editor.plugins.HeaderFormatter = function() {
+  goog.editor.Plugin.call(this);
+};
+goog.inherits(goog.editor.plugins.HeaderFormatter, goog.editor.Plugin);
+
+
+/** @override */
+goog.editor.plugins.HeaderFormatter.prototype.getTrogClassId = function() {
+  return 'HeaderFormatter';
+};
+
+// TODO(user):  Move execCommand functionality from basictextformatter into
+// here for headers.  I'm not doing this now because it depends on the
+// switch statements in basictextformatter and we'll need to abstract that out
+// in order to seperate out any of the functions from basictextformatter.
+
+
+/**
+ * Commands that can be passed as the optional argument to execCommand.
+ * @enum {string}
+ */
+goog.editor.plugins.HeaderFormatter.HEADER_COMMAND = {
+  H1: 'H1',
+  H2: 'H2',
+  H3: 'H3',
+  H4: 'H4'
+};
+
+
+/**
+ * @override
+ */
+goog.editor.plugins.HeaderFormatter.prototype.handleKeyboardShortcut = function(
+    e, key, isModifierPressed) {
+  if (!isModifierPressed) {
+    return false;
+  }
+  var command = null;
+  switch (key) {
+    case '1':
+      command = goog.editor.plugins.HeaderFormatter.HEADER_COMMAND.H1;
+      break;
+    case '2':
+      command = goog.editor.plugins.HeaderFormatter.HEADER_COMMAND.H2;
+      break;
+    case '3':
+      command = goog.editor.plugins.HeaderFormatter.HEADER_COMMAND.H3;
+      break;
+    case '4':
+      command = goog.editor.plugins.HeaderFormatter.HEADER_COMMAND.H4;
+      break;
+  }
+  if (command) {
+    this.getFieldObject().execCommand(
+        goog.editor.Command.FORMAT_BLOCK, command);
+    // Prevent default isn't enough to cancel tab navigation in FF.
+    if (goog.userAgent.GECKO) {
+      e.stopPropagation();
+    }
+    return true;
+  }
+  return false;
+};

http://git-wip-us.apache.org/repos/asf/flex-falcon/blob/e2cad6e6/externs/GCL/externs/goog/editor/plugins/linkbubble.js
----------------------------------------------------------------------
diff --git a/externs/GCL/externs/goog/editor/plugins/linkbubble.js b/externs/GCL/externs/goog/editor/plugins/linkbubble.js
new file mode 100644
index 0000000..01c84f3
--- /dev/null
+++ b/externs/GCL/externs/goog/editor/plugins/linkbubble.js
@@ -0,0 +1,585 @@
+// 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 Base class for bubble plugins.
+ *
+ */
+
+goog.provide('goog.editor.plugins.LinkBubble');
+goog.provide('goog.editor.plugins.LinkBubble.Action');
+
+goog.require('goog.array');
+goog.require('goog.dom');
+goog.require('goog.dom.TagName');
+goog.require('goog.editor.Command');
+goog.require('goog.editor.Link');
+goog.require('goog.editor.plugins.AbstractBubblePlugin');
+goog.require('goog.editor.range');
+goog.require('goog.functions');
+goog.require('goog.string');
+goog.require('goog.style');
+goog.require('goog.ui.editor.messages');
+goog.require('goog.uri.utils');
+goog.require('goog.window');
+
+
+
+/**
+ * Property bubble plugin for links.
+ * @param {...!goog.editor.plugins.LinkBubble.Action} var_args List of
+ *     extra actions supported by the bubble.
+ * @constructor
+ * @extends {goog.editor.plugins.AbstractBubblePlugin}
+ */
+goog.editor.plugins.LinkBubble = function(var_args) {
+  goog.editor.plugins.LinkBubble.base(this, 'constructor');
+
+  /**
+   * List of extra actions supported by the bubble.
+   * @type {Array<!goog.editor.plugins.LinkBubble.Action>}
+   * @private
+   */
+  this.extraActions_ = goog.array.toArray(arguments);
+
+  /**
+   * List of spans corresponding to the extra actions.
+   * @type {Array<!Element>}
+   * @private
+   */
+  this.actionSpans_ = [];
+
+  /**
+   * A list of whitelisted URL schemes which are safe to open.
+   * @type {Array<string>}
+   * @private
+   */
+  this.safeToOpenSchemes_ = ['http', 'https', 'ftp'];
+};
+goog.inherits(goog.editor.plugins.LinkBubble,
+    goog.editor.plugins.AbstractBubblePlugin);
+
+
+/**
+ * Element id for the link text.
+ * type {string}
+ * @private
+ */
+goog.editor.plugins.LinkBubble.LINK_TEXT_ID_ = 'tr_link-text';
+
+
+/**
+ * Element id for the test link span.
+ * type {string}
+ * @private
+ */
+goog.editor.plugins.LinkBubble.TEST_LINK_SPAN_ID_ = 'tr_test-link-span';
+
+
+/**
+ * Element id for the test link.
+ * type {string}
+ * @private
+ */
+goog.editor.plugins.LinkBubble.TEST_LINK_ID_ = 'tr_test-link';
+
+
+/**
+ * Element id for the change link span.
+ * type {string}
+ * @private
+ */
+goog.editor.plugins.LinkBubble.CHANGE_LINK_SPAN_ID_ = 'tr_change-link-span';
+
+
+/**
+ * Element id for the link.
+ * type {string}
+ * @private
+ */
+goog.editor.plugins.LinkBubble.CHANGE_LINK_ID_ = 'tr_change-link';
+
+
+/**
+ * Element id for the delete link span.
+ * type {string}
+ * @private
+ */
+goog.editor.plugins.LinkBubble.DELETE_LINK_SPAN_ID_ = 'tr_delete-link-span';
+
+
+/**
+ * Element id for the delete link.
+ * type {string}
+ * @private
+ */
+goog.editor.plugins.LinkBubble.DELETE_LINK_ID_ = 'tr_delete-link';
+
+
+/**
+ * Element id for the link bubble wrapper div.
+ * type {string}
+ * @private
+ */
+goog.editor.plugins.LinkBubble.LINK_DIV_ID_ = 'tr_link-div';
+
+
+/**
+ * @desc Text label for link that lets the user click it to see where the link
+ *     this bubble is for point to.
+ */
+goog.editor.plugins.LinkBubble.MSG_LINK_BUBBLE_TEST_LINK = goog.getMsg(
+    'Go to link: ');
+
+
+/**
+ * @desc Label that pops up a dialog to change the link.
+ */
+goog.editor.plugins.LinkBubble.MSG_LINK_BUBBLE_CHANGE = goog.getMsg(
+    'Change');
+
+
+/**
+ * @desc Label that allow the user to remove this link.
+ */
+goog.editor.plugins.LinkBubble.MSG_LINK_BUBBLE_REMOVE = goog.getMsg(
+    'Remove');
+
+
+/**
+ * @desc Message shown in a link bubble when the link is not a valid url.
+ */
+goog.editor.plugins.LinkBubble.MSG_INVALID_URL_LINK_BUBBLE = goog.getMsg(
+    'invalid url');
+
+
+/**
+ * Whether to stop leaking the page's url via the referrer header when the
+ * link text link is clicked.
+ * @type {boolean}
+ * @private
+ */
+goog.editor.plugins.LinkBubble.prototype.stopReferrerLeaks_ = false;
+
+
+/**
+ * Whether to block opening links with a non-whitelisted URL scheme.
+ * @type {boolean}
+ * @private
+ */
+goog.editor.plugins.LinkBubble.prototype.blockOpeningUnsafeSchemes_ =
+    true;
+
+
+/**
+ * Tells the plugin to stop leaking the page's url via the referrer header when
+ * the link text link is clicked. When the user clicks on a link, the
+ * browser makes a request for the link url, passing the url of the current page
+ * in the request headers. If the user wants the current url to be kept secret
+ * (e.g. an unpublished document), the owner of the url that was clicked will
+ * see the secret url in the request headers, and it will no longer be a secret.
+ * Calling this method will not send a referrer header in the request, just as
+ * if the user had opened a blank window and typed the url in themselves.
+ */
+goog.editor.plugins.LinkBubble.prototype.stopReferrerLeaks = function() {
+  // TODO(user): Right now only 2 plugins have this API to stop
+  // referrer leaks. If more plugins need to do this, come up with a way to
+  // enable the functionality in all plugins at once. Same thing for
+  // setBlockOpeningUnsafeSchemes and associated functionality.
+  this.stopReferrerLeaks_ = true;
+};
+
+
+/**
+ * Tells the plugin whether to block URLs with schemes not in the whitelist.
+ * If blocking is enabled, this plugin will not linkify the link in the bubble
+ * popup.
+ * @param {boolean} blockOpeningUnsafeSchemes Whether to block non-whitelisted
+ *     schemes.
+ */
+goog.editor.plugins.LinkBubble.prototype.setBlockOpeningUnsafeSchemes =
+    function(blockOpeningUnsafeSchemes) {
+  this.blockOpeningUnsafeSchemes_ = blockOpeningUnsafeSchemes;
+};
+
+
+/**
+ * Sets a whitelist of allowed URL schemes that are safe to open.
+ * Schemes should all be in lowercase. If the plugin is set to block opening
+ * unsafe schemes, user-entered URLs will be converted to lowercase and checked
+ * against this list. The whitelist has no effect if blocking is not enabled.
+ * @param {Array<string>} schemes String array of URL schemes to allow (http,
+ *     https, etc.).
+ */
+goog.editor.plugins.LinkBubble.prototype.setSafeToOpenSchemes =
+    function(schemes) {
+  this.safeToOpenSchemes_ = schemes;
+};
+
+
+/** @override */
+goog.editor.plugins.LinkBubble.prototype.getTrogClassId = function() {
+  return 'LinkBubble';
+};
+
+
+/** @override */
+goog.editor.plugins.LinkBubble.prototype.isSupportedCommand =
+    function(command) {
+  return command == goog.editor.Command.UPDATE_LINK_BUBBLE;
+};
+
+
+/** @override */
+goog.editor.plugins.LinkBubble.prototype.execCommandInternal =
+    function(command, var_args) {
+  if (command == goog.editor.Command.UPDATE_LINK_BUBBLE) {
+    this.updateLink_();
+  }
+};
+
+
+/**
+ * Updates the href in the link bubble with a new link.
+ * @private
+ */
+goog.editor.plugins.LinkBubble.prototype.updateLink_ = function() {
+  var targetEl = this.getTargetElement();
+  if (targetEl) {
+    this.closeBubble();
+    this.createBubble(targetEl);
+  }
+};
+
+
+/** @override */
+goog.editor.plugins.LinkBubble.prototype.getBubbleTargetFromSelection =
+    function(selectedElement) {
+  var bubbleTarget = goog.dom.getAncestorByTagNameAndClass(selectedElement,
+      goog.dom.TagName.A);
+
+  if (!bubbleTarget) {
+    // See if the selection is touching the right side of a link, and if so,
+    // show a bubble for that link.  The check for "touching" is very brittle,
+    // and currently only guarantees that it will pop up a bubble at the
+    // position the cursor is placed at after the link dialog is closed.
+    // NOTE(robbyw): This assumes this method is always called with
+    // selected element = range.getContainerElement().  Right now this is true,
+    // but attempts to re-use this method for other purposes could cause issues.
+    // TODO(robbyw): Refactor this method to also take a range, and use that.
+    var range = this.getFieldObject().getRange();
+    if (range && range.isCollapsed() && range.getStartOffset() == 0) {
+      var startNode = range.getStartNode();
+      var previous = startNode.previousSibling;
+      if (previous && previous.tagName == goog.dom.TagName.A) {
+        bubbleTarget = previous;
+      }
+    }
+  }
+
+  return /** @type {Element} */ (bubbleTarget);
+};
+
+
+/**
+ * Set the optional function for getting the "test" link of a url.
+ * @param {function(string) : string} func The function to use.
+ */
+goog.editor.plugins.LinkBubble.prototype.setTestLinkUrlFn = function(func) {
+  this.testLinkUrlFn_ = func;
+};
+
+
+/**
+ * Returns the target element url for the bubble.
+ * @return {string} The url href.
+ * @protected
+ */
+goog.editor.plugins.LinkBubble.prototype.getTargetUrl = function() {
+  // Get the href-attribute through getAttribute() rather than the href property
+  // because Google-Toolbar on Firefox with "Send with Gmail" turned on
+  // modifies the href-property of 'mailto:' links but leaves the attribute
+  // untouched.
+  return this.getTargetElement().getAttribute('href') || '';
+};
+
+
+/** @override */
+goog.editor.plugins.LinkBubble.prototype.getBubbleType = function() {
+  return goog.dom.TagName.A;
+};
+
+
+/** @override */
+goog.editor.plugins.LinkBubble.prototype.getBubbleTitle = function() {
+  return goog.ui.editor.messages.MSG_LINK_CAPTION;
+};
+
+
+/**
+ * Returns the message to display for testing a link.
+ * @return {string} The message for testing a link.
+ * @protected
+ */
+goog.editor.plugins.LinkBubble.prototype.getTestLinkMessage = function() {
+  return goog.editor.plugins.LinkBubble.MSG_LINK_BUBBLE_TEST_LINK;
+};
+
+
+/** @override */
+goog.editor.plugins.LinkBubble.prototype.createBubbleContents = function(
+    bubbleContainer) {
+  var linkObj = this.getLinkToTextObj_();
+
+  // Create linkTextSpan, show plain text for e-mail address or truncate the
+  // text to <= 48 characters so that property bubbles don't grow too wide and
+  // create a link if URL.  Only linkify valid links.
+  // TODO(robbyw): Repalce this color with a CSS class.
+  var color = linkObj.valid ? 'black' : 'red';
+  var shouldOpenUrl = this.shouldOpenUrl(linkObj.linkText);
+  var linkTextSpan;
+  if (goog.editor.Link.isLikelyEmailAddress(linkObj.linkText) ||
+      !linkObj.valid || !shouldOpenUrl) {
+    linkTextSpan = this.dom_.createDom(goog.dom.TagName.SPAN,
+        {
+          id: goog.editor.plugins.LinkBubble.LINK_TEXT_ID_,
+          style: 'color:' + color
+        }, this.dom_.createTextNode(linkObj.linkText));
+  } else {
+    var testMsgSpan = this.dom_.createDom(goog.dom.TagName.SPAN,
+        {id: goog.editor.plugins.LinkBubble.TEST_LINK_SPAN_ID_},
+        this.getTestLinkMessage());
+    linkTextSpan = this.dom_.createDom(goog.dom.TagName.SPAN,
+        {
+          id: goog.editor.plugins.LinkBubble.LINK_TEXT_ID_,
+          style: 'color:' + color
+        }, '');
+    var linkText = goog.string.truncateMiddle(linkObj.linkText, 48);
+    // Actually creates a pseudo-link that can't be right-clicked to open in a
+    // new tab, because that would avoid the logic to stop referrer leaks.
+    this.createLink(goog.editor.plugins.LinkBubble.TEST_LINK_ID_,
+                    this.dom_.createTextNode(linkText).data,
+                    this.testLink,
+                    linkTextSpan);
+  }
+
+  var changeLinkSpan = this.createLinkOption(
+      goog.editor.plugins.LinkBubble.CHANGE_LINK_SPAN_ID_);
+  this.createLink(goog.editor.plugins.LinkBubble.CHANGE_LINK_ID_,
+      goog.editor.plugins.LinkBubble.MSG_LINK_BUBBLE_CHANGE,
+      this.showLinkDialog_, changeLinkSpan);
+
+  // This function is called multiple times - we have to reset the array.
+  this.actionSpans_ = [];
+  for (var i = 0; i < this.extraActions_.length; i++) {
+    var action = this.extraActions_[i];
+    var actionSpan = this.createLinkOption(action.spanId_);
+    this.actionSpans_.push(actionSpan);
+    this.createLink(action.linkId_, action.message_,
+        function() {
+          action.actionFn_(this.getTargetUrl());
+        },
+        actionSpan);
+  }
+
+  var removeLinkSpan = this.createLinkOption(
+      goog.editor.plugins.LinkBubble.DELETE_LINK_SPAN_ID_);
+  this.createLink(goog.editor.plugins.LinkBubble.DELETE_LINK_ID_,
+      goog.editor.plugins.LinkBubble.MSG_LINK_BUBBLE_REMOVE,
+      this.deleteLink_, removeLinkSpan);
+
+  this.onShow();
+
+  var bubbleContents = this.dom_.createDom(goog.dom.TagName.DIV,
+      {id: goog.editor.plugins.LinkBubble.LINK_DIV_ID_},
+      testMsgSpan || '', linkTextSpan, changeLinkSpan);
+
+  for (i = 0; i < this.actionSpans_.length; i++) {
+    bubbleContents.appendChild(this.actionSpans_[i]);
+  }
+  bubbleContents.appendChild(removeLinkSpan);
+
+  goog.dom.appendChild(bubbleContainer, bubbleContents);
+};
+
+
+/**
+ * Tests the link by opening it in a new tab/window. Should be used as the
+ * click event handler for the test pseudo-link.
+ * @protected
+ */
+goog.editor.plugins.LinkBubble.prototype.testLink = function() {
+  goog.window.open(this.getTestLinkAction_(),
+      {
+        'target': '_blank',
+        'noreferrer': this.stopReferrerLeaks_
+      }, this.getFieldObject().getAppWindow());
+};
+
+
+/**
+ * Returns whether the URL should be considered invalid.  This always returns
+ * false in the base class, and should be overridden by subclasses that wish
+ * to impose validity rules on URLs.
+ * @param {string} url The url to check.
+ * @return {boolean} Whether the URL should be considered invalid.
+ */
+goog.editor.plugins.LinkBubble.prototype.isInvalidUrl = goog.functions.FALSE;
+
+
+/**
+ * Gets the text to display for a link, based on the type of link
+ * @return {!Object} Returns an object of the form:
+ *     {linkText: displayTextForLinkTarget, valid: ifTheLinkIsValid}.
+ * @private
+ */
+goog.editor.plugins.LinkBubble.prototype.getLinkToTextObj_ = function() {
+  var isError;
+  var targetUrl = this.getTargetUrl();
+
+  if (this.isInvalidUrl(targetUrl)) {
+
+    targetUrl = goog.editor.plugins.LinkBubble.MSG_INVALID_URL_LINK_BUBBLE;
+    isError = true;
+  } else if (goog.editor.Link.isMailto(targetUrl)) {
+    targetUrl = targetUrl.substring(7); // 7 == "mailto:".length
+  }
+
+  return {linkText: targetUrl, valid: !isError};
+};
+
+
+/**
+ * Shows the link dialog.
+ * @param {goog.events.BrowserEvent} e The event.
+ * @private
+ */
+goog.editor.plugins.LinkBubble.prototype.showLinkDialog_ = function(e) {
+  // Needed when this occurs due to an ENTER key event, else the newly created
+  // dialog manages to have its OK button pressed, causing it to disappear.
+  e.preventDefault();
+
+  this.getFieldObject().execCommand(goog.editor.Command.MODAL_LINK_EDITOR,
+      new goog.editor.Link(
+          /** @type {HTMLAnchorElement} */ (this.getTargetElement()),
+          false));
+  this.closeBubble();
+};
+
+
+/**
+ * Deletes the link associated with the bubble
+ * @private
+ */
+goog.editor.plugins.LinkBubble.prototype.deleteLink_ = function() {
+  this.getFieldObject().dispatchBeforeChange();
+
+  var link = this.getTargetElement();
+  var child = link.lastChild;
+  goog.dom.flattenElement(link);
+  goog.editor.range.placeCursorNextTo(child, false);
+
+  this.closeBubble();
+
+  this.getFieldObject().dispatchChange();
+  this.getFieldObject().focus();
+};
+
+
+/**
+ * Sets the proper state for the action links.
+ * @protected
+ * @override
+ */
+goog.editor.plugins.LinkBubble.prototype.onShow = function() {
+  var linkDiv = this.dom_.getElement(
+      goog.editor.plugins.LinkBubble.LINK_DIV_ID_);
+  if (linkDiv) {
+    var testLinkSpan = this.dom_.getElement(
+        goog.editor.plugins.LinkBubble.TEST_LINK_SPAN_ID_);
+    if (testLinkSpan) {
+      var url = this.getTargetUrl();
+      goog.style.setElementShown(testLinkSpan, !goog.editor.Link.isMailto(url));
+    }
+
+    for (var i = 0; i < this.extraActions_.length; i++) {
+      var action = this.extraActions_[i];
+      var actionSpan = this.dom_.getElement(action.spanId_);
+      if (actionSpan) {
+        goog.style.setElementShown(actionSpan, action.toShowFn_(
+            this.getTargetUrl()));
+      }
+    }
+  }
+};
+
+
+/**
+ * Gets the url for the bubble test link.  The test link is the link in the
+ * bubble the user can click on to make sure the link they entered is correct.
+ * @return {string} The url for the bubble link href.
+ * @private
+ */
+goog.editor.plugins.LinkBubble.prototype.getTestLinkAction_ = function() {
+  var targetUrl = this.getTargetUrl();
+  return this.testLinkUrlFn_ ? this.testLinkUrlFn_(targetUrl) : targetUrl;
+};
+
+
+/**
+ * Checks whether the plugin should open the given url in a new window.
+ * @param {string} url The url to check.
+ * @return {boolean} If the plugin should open the given url in a new window.
+ * @protected
+ */
+goog.editor.plugins.LinkBubble.prototype.shouldOpenUrl = function(url) {
+  return !this.blockOpeningUnsafeSchemes_ || this.isSafeSchemeToOpen_(url);
+};
+
+
+/**
+ * Determines whether or not a url has a scheme which is safe to open.
+ * Schemes like javascript are unsafe due to the possibility of XSS.
+ * @param {string} url A url.
+ * @return {boolean} Whether the url has a safe scheme.
+ * @private
+ */
+goog.editor.plugins.LinkBubble.prototype.isSafeSchemeToOpen_ =
+    function(url) {
+  var scheme = goog.uri.utils.getScheme(url) || 'http';
+  return goog.array.contains(this.safeToOpenSchemes_, scheme.toLowerCase());
+};
+
+
+
+/**
+ * Constructor for extra actions that can be added to the link bubble.
+ * @param {string} spanId The ID for the span showing the action.
+ * @param {string} linkId The ID for the link showing the action.
+ * @param {string} message The text for the link showing the action.
+ * @param {function(string):boolean} toShowFn Test function to determine whether
+ *     to show the action for the given URL.
+ * @param {function(string):void} actionFn Action function to run when the
+ *     action is clicked.  Takes the current target URL as a parameter.
+ * @constructor
+ * @final
+ */
+goog.editor.plugins.LinkBubble.Action = function(spanId, linkId, message,
+    toShowFn, actionFn) {
+  this.spanId_ = spanId;
+  this.linkId_ = linkId;
+  this.message_ = message;
+  this.toShowFn_ = toShowFn;
+  this.actionFn_ = actionFn;
+};


Mime
View raw message