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 [email protected] (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 [email protected] (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 [email protected] (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; +};
