You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@flex.apache.org by ft...@apache.org on 2015/09/17 17:28:38 UTC
[23/51] [abbrv] [partial] git commit: [flex-falcon]
[refs/heads/JsToAs] - Added GCL extern.
http://git-wip-us.apache.org/repos/asf/flex-falcon/blob/e2cad6e6/externs/GCL/externs/goog/editor/plugins/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 = ' '; // 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 = ' ';
+
+ // 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> </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 = ' ';
+ 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. <div>) or a whole list (e.g. <ul>).
+ *
+ * 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;
+};