http://git-wip-us.apache.org/repos/asf/flex-falcon/blob/e2cad6e6/externs/GCL/externs/goog/editor/focus.js ---------------------------------------------------------------------- diff --git a/externs/GCL/externs/goog/editor/focus.js b/externs/GCL/externs/goog/editor/focus.js new file mode 100644 index 0000000..4253b3c --- /dev/null +++ b/externs/GCL/externs/goog/editor/focus.js @@ -0,0 +1,32 @@ +// 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. + +/** + * @fileoverview Utilties to handle focusing related to rich text editing. + * + */ + +goog.provide('goog.editor.focus'); + +goog.require('goog.dom.selection'); + + +/** + * Change focus to the given input field and set cursor to end of current text. + * @param {Element} inputElem Input DOM element. + */ +goog.editor.focus.focusInputField = function(inputElem) { + inputElem.focus(); + goog.dom.selection.setCursorPosition(inputElem, inputElem.value.length); +};
http://git-wip-us.apache.org/repos/asf/flex-falcon/blob/e2cad6e6/externs/GCL/externs/goog/editor/icontent.js ---------------------------------------------------------------------- diff --git a/externs/GCL/externs/goog/editor/icontent.js b/externs/GCL/externs/goog/editor/icontent.js new file mode 100644 index 0000000..fac433a --- /dev/null +++ b/externs/GCL/externs/goog/editor/icontent.js @@ -0,0 +1,300 @@ +// Copyright 2007 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 Static functions for writing the contents of an iframe-based + * editable field. These vary significantly from browser to browser. Uses + * strings and document.write instead of DOM manipulation, because + * iframe-loading is a performance bottleneck. + * + * @author [email protected] (Nick Santos) + */ + +goog.provide('goog.editor.icontent'); +goog.provide('goog.editor.icontent.FieldFormatInfo'); +goog.provide('goog.editor.icontent.FieldStyleInfo'); + +goog.require('goog.dom'); +goog.require('goog.editor.BrowserFeature'); +goog.require('goog.style'); +goog.require('goog.userAgent'); + + + +/** + * A data structure for storing simple rendering info about a field. + * + * @param {string} fieldId The id of the field. + * @param {boolean} standards Whether the field should be rendered in + * standards mode. + * @param {boolean} blended Whether the field is in blended mode. + * @param {boolean} fixedHeight Whether the field is in fixedHeight mode. + * @param {Object=} opt_extraStyles Other style attributes for the field, + * represented as a map of strings. + * @constructor + * @final + */ +goog.editor.icontent.FieldFormatInfo = function(fieldId, standards, blended, + fixedHeight, opt_extraStyles) { + this.fieldId_ = fieldId; + this.standards_ = standards; + this.blended_ = blended; + this.fixedHeight_ = fixedHeight; + this.extraStyles_ = opt_extraStyles || {}; +}; + + + +/** + * A data structure for storing simple info about the styles of a field. + * Only needed in Firefox/Blended mode. + * @param {Element} wrapper The wrapper div around a field. + * @param {string} css The css for a field. + * @constructor + * @final + */ +goog.editor.icontent.FieldStyleInfo = function(wrapper, css) { + this.wrapper_ = wrapper; + this.css_ = css; +}; + + +/** + * Whether to always use standards-mode iframes. + * @type {boolean} + * @private + */ +goog.editor.icontent.useStandardsModeIframes_ = false; + + +/** + * Sets up goog.editor.icontent to always use standards-mode iframes. + */ +goog.editor.icontent.forceStandardsModeIframes = function() { + goog.editor.icontent.useStandardsModeIframes_ = true; +}; + + +/** + * Generate the initial iframe content. + * @param {goog.editor.icontent.FieldFormatInfo} info Formatting info about + * the field. + * @param {string} bodyHtml The HTML to insert as the iframe body. + * @param {goog.editor.icontent.FieldStyleInfo?} style Style info about + * the field, if needed. + * @return {string} The initial IFRAME content HTML. + * @private + */ +goog.editor.icontent.getInitialIframeContent_ = + function(info, bodyHtml, style) { + var html = []; + + if (info.blended_ && info.standards_ || + goog.editor.icontent.useStandardsModeIframes_) { + html.push('<!DOCTYPE HTML>'); + } + + // <HTML> + // NOTE(user): Override min-widths that may be set for all + // HTML/BODY nodes. A similar workaround is below for the <body> tag. This + // can happen if the host page includes a rule like this in its CSS: + // + // html, body {min-width: 500px} + // + // In this case, the iframe's <html> and/or <body> may be affected. This was + // part of the problem observed in http://b/5674613. (The other part of that + // problem had to do with the presence of a spurious horizontal scrollbar, + // which caused the editor height to be computed incorrectly.) + html.push('<html style="background:none transparent;min-width:0;'); + + // Make sure that the HTML element's height has the + // correct value as the body element's percentage height is made relative + // to the HTML element's height. + // For fixed-height it should be 100% since we want the body to fill the + // whole height. For growing fields it should be auto since we want the + // body to size to its content. + if (info.blended_) { + html.push('height:', info.fixedHeight_ ? '100%' : 'auto'); + } + html.push('">'); + + // <HEAD><STYLE> + + // IE/Safari whitebox need styles set only iff the client specifically + // requested them. + html.push('<head><style>'); + if (style && style.css_) { + html.push(style.css_); + } + + // Firefox blended needs to inherit all the css from the original page. + // Firefox standards mode needs to set extra style for images. + if (goog.userAgent.GECKO && info.standards_) { + // Standards mode will collapse broken images. This means that they + // can never be removed from the field. This style forces the images + // to render as a broken image icon, sized based on the width and height + // of the image. + // TODO(user): Make sure we move this into a contentEditable code + // path if there ever is one for FF. + html.push(' img {-moz-force-broken-image-icon: 1;}'); + } + + html.push('</style></head>'); + + // <BODY> + // Hidefocus is needed to ensure that IE7 doesn't show the dotted, focus + // border when you tab into the field. + html.push('<body g_editable="true" hidefocus="true" '); + if (goog.editor.BrowserFeature.HAS_CONTENT_EDITABLE) { + html.push('contentEditable '); + } + + html.push('class="editable '); + + // TODO: put the field's original ID on the body and stop using ID as a + // way of getting the pointer to the field in the iframe now that it's + // always the body. + html.push('" id="', info.fieldId_, '" style="min-width:0;'); + + if (goog.userAgent.GECKO && info.blended_) { + // IMPORTANT: Apply the css from the body then all of the clearing + // CSS to make sure the clearing CSS overrides (e.g. if the body + // has a 3px margin, we want to make sure to override it with 0px. + html.push( + + // margin should not be applied to blended mode because the margin is + // outside the iframe + // In whitebox mode, we want to leave the margin to the default so + // there is a nice margin around the text. + ';width:100%;border:0;margin:0;background:none transparent;', + + // In standards-mode, height 100% makes the body size to its + // parent html element, but in quirks mode, we want auto because + // 100% makes it size to the containing window even if the html + // element is smaller. + // TODO: Fixed height, standards mode, CSS_WRITING, with margins on the + // paragraphs has a scrollbar when it doesn't need it. Putting the + // height to auto seems to fix it. Figure out if we should always + // just use auto? + ';height:', info.standards_ ? '100%' : 'auto'); + + // Only do this for mozilla. IE6 standards mode has a rendering bug when + // there are scrollbars and the body's overflow property is auto + if (info.fixedHeight_) { + html.push(';overflow:auto'); + } else { + html.push(';overflow-y:hidden;overflow-x:auto'); + } + } + + // Hide the native focus rect in Opera. + if (goog.userAgent.OPERA) { + html.push(';outline:hidden'); + } + + for (var key in info.extraStyles_) { + html.push(';' + key + ':' + info.extraStyles_[key]); + } + + html.push('">', bodyHtml, '</body></html>'); + + return html.join(''); +}; + + +/** + * Write the initial iframe content in normal mode. + * @param {goog.editor.icontent.FieldFormatInfo} info Formatting info about + * the field. + * @param {string} bodyHtml The HTML to insert as the iframe body. + * @param {goog.editor.icontent.FieldStyleInfo?} style Style info about + * the field, if needed. + * @param {HTMLIFrameElement} iframe The iframe. + */ +goog.editor.icontent.writeNormalInitialBlendedIframe = + function(info, bodyHtml, style, iframe) { + // Firefox blended needs to inherit all the css from the original page. + // Firefox standards mode needs to set extra style for images. + if (info.blended_) { + var field = style.wrapper_; + // If there is padding on the original field, then the iFrame will be + // positioned inside the padding by default. We don't want this, as it + // causes the contents to appear to shift, and also causes the + // scrollbars to appear inside the padding. + // + // To compensate, we set the iframe margins to offset the padding. + var paddingBox = goog.style.getPaddingBox(field); + if (paddingBox.top || paddingBox.left || + paddingBox.right || paddingBox.bottom) { + goog.style.setStyle(iframe, 'margin', + (-paddingBox.top) + 'px ' + + (-paddingBox.right) + 'px ' + + (-paddingBox.bottom) + 'px ' + + (-paddingBox.left) + 'px'); + } + } + + goog.editor.icontent.writeNormalInitialIframe( + info, bodyHtml, style, iframe); +}; + + +/** + * Write the initial iframe content in normal mode. + * @param {goog.editor.icontent.FieldFormatInfo} info Formatting info about + * the field. + * @param {string} bodyHtml The HTML to insert as the iframe body. + * @param {goog.editor.icontent.FieldStyleInfo?} style Style info about + * the field, if needed. + * @param {HTMLIFrameElement} iframe The iframe. + */ +goog.editor.icontent.writeNormalInitialIframe = + function(info, bodyHtml, style, iframe) { + + var html = goog.editor.icontent.getInitialIframeContent_( + info, bodyHtml, style); + + var doc = goog.dom.getFrameContentDocument(iframe); + doc.open(); + doc.write(html); + doc.close(); +}; + + +/** + * Write the initial iframe content in IE/HTTPS mode. + * @param {goog.editor.icontent.FieldFormatInfo} info Formatting info about + * the field. + * @param {Document} doc The iframe document. + * @param {string} bodyHtml The HTML to insert as the iframe body. + */ +goog.editor.icontent.writeHttpsInitialIframe = function(info, doc, bodyHtml) { + var body = doc.body; + + // For HTTPS we already have a document with a doc type and a body element + // and don't want to create a new history entry which can cause data loss if + // the user clicks the back button. + if (goog.editor.BrowserFeature.HAS_CONTENT_EDITABLE) { + body.contentEditable = true; + } + body.className = 'editable'; + body.setAttribute('g_editable', true); + body.hideFocus = true; + body.id = info.fieldId_; + + goog.style.setStyle(body, info.extraStyles_); + body.innerHTML = bodyHtml; +}; + http://git-wip-us.apache.org/repos/asf/flex-falcon/blob/e2cad6e6/externs/GCL/externs/goog/editor/link.js ---------------------------------------------------------------------- diff --git a/externs/GCL/externs/goog/editor/link.js b/externs/GCL/externs/goog/editor/link.js new file mode 100644 index 0000000..72f6c52 --- /dev/null +++ b/externs/GCL/externs/goog/editor/link.js @@ -0,0 +1,390 @@ +// Copyright 2007 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 utility class for managing editable links. + * + * @author [email protected] (Nick Santos) + */ + +goog.provide('goog.editor.Link'); + +goog.require('goog.array'); +goog.require('goog.dom'); +goog.require('goog.dom.NodeType'); +goog.require('goog.dom.Range'); +goog.require('goog.dom.TagName'); +goog.require('goog.editor.BrowserFeature'); +goog.require('goog.editor.Command'); +goog.require('goog.editor.node'); +goog.require('goog.editor.range'); +goog.require('goog.string'); +goog.require('goog.string.Unicode'); +goog.require('goog.uri.utils'); +goog.require('goog.uri.utils.ComponentIndex'); + + + +/** + * Wrap an editable link. + * @param {HTMLAnchorElement} anchor The anchor element. + * @param {boolean} isNew Whether this is a new link. + * @constructor + * @final + */ +goog.editor.Link = function(anchor, isNew) { + /** + * The link DOM element. + * @type {HTMLAnchorElement} + * @private + */ + this.anchor_ = anchor; + + /** + * Whether this link represents a link just added to the document. + * @type {boolean} + * @private + */ + this.isNew_ = isNew; + + + /** + * Any extra anchors created by the browser from a selection in the same + * operation that created the primary link + * @type {!Array<HTMLAnchorElement>} + * @private + */ + this.extraAnchors_ = []; +}; + + +/** + * @return {HTMLAnchorElement} The anchor element. + */ +goog.editor.Link.prototype.getAnchor = function() { + return this.anchor_; +}; + + +/** + * @return {!Array<HTMLAnchorElement>} The extra anchor elements, if any, + * created by the browser from a selection. + */ +goog.editor.Link.prototype.getExtraAnchors = function() { + return this.extraAnchors_; +}; + + +/** + * @return {string} The inner text for the anchor. + */ +goog.editor.Link.prototype.getCurrentText = function() { + if (!this.currentText_) { + var anchor = this.getAnchor(); + + var leaf = goog.editor.node.getLeftMostLeaf(anchor); + if (leaf.tagName && leaf.tagName == goog.dom.TagName.IMG) { + this.currentText_ = leaf.getAttribute('alt'); + } else { + this.currentText_ = goog.dom.getRawTextContent(this.getAnchor()); + } + } + return this.currentText_; +}; + + +/** + * @return {boolean} Whether the link is new. + */ +goog.editor.Link.prototype.isNew = function() { + return this.isNew_; +}; + + +/** + * Set the url without affecting the isNew() status of the link. + * @param {string} url A URL. + */ +goog.editor.Link.prototype.initializeUrl = function(url) { + this.getAnchor().href = url; +}; + + +/** + * Removes the link, leaving its contents in the document. Note that this + * object will no longer be usable/useful after this call. + */ +goog.editor.Link.prototype.removeLink = function() { + goog.dom.flattenElement(this.anchor_); + this.anchor_ = null; + while (this.extraAnchors_.length) { + goog.dom.flattenElement(/** @type {Element} */(this.extraAnchors_.pop())); + } +}; + + +/** + * Change the link. + * @param {string} newText New text for the link. If the link contains all its + * text in one descendent, newText will only replace the text in that + * one node. Otherwise, we'll change the innerHTML of the whole + * link to newText. + * @param {string} newUrl A new URL. + */ +goog.editor.Link.prototype.setTextAndUrl = function(newText, newUrl) { + var anchor = this.getAnchor(); + anchor.href = newUrl; + + // If the text did not change, don't update link text. + var currentText = this.getCurrentText(); + if (newText != currentText) { + var leaf = goog.editor.node.getLeftMostLeaf(anchor); + + if (leaf.tagName && leaf.tagName == goog.dom.TagName.IMG) { + leaf.setAttribute('alt', newText ? newText : ''); + } else { + if (leaf.nodeType == goog.dom.NodeType.TEXT) { + leaf = leaf.parentNode; + } + + if (goog.dom.getRawTextContent(leaf) != currentText) { + leaf = anchor; + } + + goog.dom.removeChildren(leaf); + var domHelper = goog.dom.getDomHelper(leaf); + goog.dom.appendChild(leaf, domHelper.createTextNode(newText)); + } + + // The text changed, so force getCurrentText to recompute. + this.currentText_ = null; + } + + this.isNew_ = false; +}; + + +/** + * Places the cursor to the right of the anchor. + * Note that this is different from goog.editor.range's placeCursorNextTo + * in that it specifically handles the placement of a cursor in browsers + * that trap you in links, by adding a space when necessary and placing the + * cursor after that space. + */ +goog.editor.Link.prototype.placeCursorRightOf = function() { + var anchor = this.getAnchor(); + // If the browser gets stuck in a link if we place the cursor next to it, + // we'll place the cursor after a space instead. + if (goog.editor.BrowserFeature.GETS_STUCK_IN_LINKS) { + var spaceNode; + var nextSibling = anchor.nextSibling; + + // Check if there is already a space after the link. Only handle the + // simple case - the next node is a text node that starts with a space. + if (nextSibling && + nextSibling.nodeType == goog.dom.NodeType.TEXT && + (goog.string.startsWith(nextSibling.data, goog.string.Unicode.NBSP) || + goog.string.startsWith(nextSibling.data, ' '))) { + spaceNode = nextSibling; + } else { + // If there isn't an obvious space to use, create one after the link. + var dh = goog.dom.getDomHelper(anchor); + spaceNode = dh.createTextNode(goog.string.Unicode.NBSP); + goog.dom.insertSiblingAfter(spaceNode, anchor); + } + + // Move the selection after the space. + var range = goog.dom.Range.createCaret(spaceNode, 1); + range.select(); + } else { + goog.editor.range.placeCursorNextTo(anchor, false); + } +}; + + +/** + * Updates the cursor position and link bubble for this link. + * @param {goog.editor.Field} field The field in which the link is created. + * @param {string} url The link url. + * @private + */ +goog.editor.Link.prototype.updateLinkDisplay_ = function(field, url) { + this.initializeUrl(url); + this.placeCursorRightOf(); + field.execCommand(goog.editor.Command.UPDATE_LINK_BUBBLE); +}; + + +/** + * @return {string?} The modified string for the link if the link + * text appears to be a valid link. Returns null if this is not + * a valid link address. + */ +goog.editor.Link.prototype.getValidLinkFromText = function() { + var text = goog.string.trim(this.getCurrentText()); + if (goog.editor.Link.isLikelyUrl(text)) { + if (text.search(/:/) < 0) { + return 'http://' + goog.string.trimLeft(text); + } + return text; + } else if (goog.editor.Link.isLikelyEmailAddress(text)) { + return 'mailto:' + text; + } + return null; +}; + + +/** + * After link creation, finish creating the link depending on the type + * of link being created. + * @param {goog.editor.Field} field The field where this link is being created. + */ +goog.editor.Link.prototype.finishLinkCreation = function(field) { + var linkFromText = this.getValidLinkFromText(); + if (linkFromText) { + this.updateLinkDisplay_(field, linkFromText); + } else { + field.execCommand(goog.editor.Command.MODAL_LINK_EDITOR, this); + } +}; + + +/** + * Initialize a new link. + * @param {HTMLAnchorElement} anchor The anchor element. + * @param {string} url The initial URL. + * @param {string=} opt_target The target. + * @param {Array<HTMLAnchorElement>=} opt_extraAnchors Extra anchors created + * by the browser when parsing a selection. + * @return {!goog.editor.Link} The link. + */ +goog.editor.Link.createNewLink = function(anchor, url, opt_target, + opt_extraAnchors) { + var link = new goog.editor.Link(anchor, true); + link.initializeUrl(url); + + if (opt_target) { + anchor.target = opt_target; + } + if (opt_extraAnchors) { + link.extraAnchors_ = opt_extraAnchors; + } + + return link; +}; + + +/** + * Initialize a new link using text in anchor, or empty string if there is no + * likely url in the anchor. + * @param {HTMLAnchorElement} anchor The anchor element with likely url content. + * @param {string=} opt_target The target. + * @return {!goog.editor.Link} The link. + */ +goog.editor.Link.createNewLinkFromText = function(anchor, opt_target) { + var link = new goog.editor.Link(anchor, true); + var text = link.getValidLinkFromText(); + link.initializeUrl(text ? text : ''); + if (opt_target) { + anchor.target = opt_target; + } + return link; +}; + + +/** + * Returns true if str could be a URL, false otherwise + * + * Ex: TR_Util.isLikelyUrl_("http://www.google.com") == true + * TR_Util.isLikelyUrl_("www.google.com") == true + * + * @param {string} str String to check if it looks like a URL. + * @return {boolean} Whether str could be a URL. + */ +goog.editor.Link.isLikelyUrl = function(str) { + // Whitespace means this isn't a domain. + if (/\s/.test(str)) { + return false; + } + + if (goog.editor.Link.isLikelyEmailAddress(str)) { + return false; + } + + // Add a scheme if the url doesn't have one - this helps the parser. + var addedScheme = false; + if (!/^[^:\/?#.]+:/.test(str)) { + str = 'http://' + str; + addedScheme = true; + } + + // Parse the domain. + var parts = goog.uri.utils.split(str); + + // Relax the rules for special schemes. + var scheme = parts[goog.uri.utils.ComponentIndex.SCHEME]; + if (goog.array.indexOf(['mailto', 'aim'], scheme) != -1) { + return true; + } + + // Require domains to contain a '.', unless the domain is fully qualified and + // forbids domains from containing invalid characters. + var domain = parts[goog.uri.utils.ComponentIndex.DOMAIN]; + if (!domain || (addedScheme && domain.indexOf('.') == -1) || + (/[^\w\d\-\u0100-\uffff.%]/.test(domain))) { + return false; + } + + // Require http and ftp paths to start with '/'. + var path = parts[goog.uri.utils.ComponentIndex.PATH]; + return !path || path.indexOf('/') == 0; +}; + + +/** + * Regular expression that matches strings that could be an email address. + * @type {RegExp} + * @private + */ +goog.editor.Link.LIKELY_EMAIL_ADDRESS_ = new RegExp( + '^' + // Test from start of string + '[\\w-]+(\\.[\\w-]+)*' + // Dot-delimited alphanumerics and dashes (name) + '\\@' + // @ + '([\\w-]+\\.)+' + // Alphanumerics, dashes and dots (domain) + '(\\d+|\\w\\w+)$', // Domain ends in at least one number or 2 letters + 'i'); + + +/** + * Returns true if str could be an email address, false otherwise + * + * Ex: goog.editor.Link.isLikelyEmailAddress_("some word") == false + * goog.editor.Link.isLikelyEmailAddress_("[email protected]") == true + * + * @param {string} str String to test for being email address. + * @return {boolean} Whether "str" looks like an email address. + */ +goog.editor.Link.isLikelyEmailAddress = function(str) { + return goog.editor.Link.LIKELY_EMAIL_ADDRESS_.test(str); +}; + + +/** + * Determines whether or not a url is an email link. + * @param {string} url A url. + * @return {boolean} Whether the url is a mailto link. + */ +goog.editor.Link.isMailto = function(url) { + return !!url && goog.string.startsWith(url, 'mailto:'); +}; http://git-wip-us.apache.org/repos/asf/flex-falcon/blob/e2cad6e6/externs/GCL/externs/goog/editor/node.js ---------------------------------------------------------------------- diff --git a/externs/GCL/externs/goog/editor/node.js b/externs/GCL/externs/goog/editor/node.js new file mode 100644 index 0000000..006936d --- /dev/null +++ b/externs/GCL/externs/goog/editor/node.js @@ -0,0 +1,484 @@ +// Copyright 2005 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 Utilties for working with DOM nodes related to rich text + * editing. Many of these are not general enough to go into goog.dom. + * + * @author [email protected] (Nick Santos) + */ + +goog.provide('goog.editor.node'); + +goog.require('goog.dom'); +goog.require('goog.dom.NodeType'); +goog.require('goog.dom.TagName'); +goog.require('goog.dom.iter.ChildIterator'); +goog.require('goog.dom.iter.SiblingIterator'); +goog.require('goog.iter'); +goog.require('goog.object'); +goog.require('goog.string'); +goog.require('goog.string.Unicode'); +goog.require('goog.userAgent'); + + +/** + * Names of all block-level tags + * @type {Object} + * @private + */ +goog.editor.node.BLOCK_TAG_NAMES_ = goog.object.createSet( + goog.dom.TagName.ADDRESS, + goog.dom.TagName.ARTICLE, + goog.dom.TagName.ASIDE, + goog.dom.TagName.BLOCKQUOTE, + goog.dom.TagName.BODY, + goog.dom.TagName.CAPTION, + goog.dom.TagName.CENTER, + goog.dom.TagName.COL, + goog.dom.TagName.COLGROUP, + goog.dom.TagName.DETAILS, + goog.dom.TagName.DIR, + goog.dom.TagName.DIV, + goog.dom.TagName.DL, + goog.dom.TagName.DD, + goog.dom.TagName.DT, + goog.dom.TagName.FIELDSET, + goog.dom.TagName.FIGCAPTION, + goog.dom.TagName.FIGURE, + goog.dom.TagName.FOOTER, + goog.dom.TagName.FORM, + goog.dom.TagName.H1, + goog.dom.TagName.H2, + goog.dom.TagName.H3, + goog.dom.TagName.H4, + goog.dom.TagName.H5, + goog.dom.TagName.H6, + goog.dom.TagName.HEADER, + goog.dom.TagName.HGROUP, + goog.dom.TagName.HR, + goog.dom.TagName.ISINDEX, + goog.dom.TagName.OL, + goog.dom.TagName.LI, + goog.dom.TagName.MAP, + goog.dom.TagName.MENU, + goog.dom.TagName.NAV, + goog.dom.TagName.OPTGROUP, + goog.dom.TagName.OPTION, + goog.dom.TagName.P, + goog.dom.TagName.PRE, + goog.dom.TagName.SECTION, + goog.dom.TagName.SUMMARY, + goog.dom.TagName.TABLE, + goog.dom.TagName.TBODY, + goog.dom.TagName.TD, + goog.dom.TagName.TFOOT, + goog.dom.TagName.TH, + goog.dom.TagName.THEAD, + goog.dom.TagName.TR, + goog.dom.TagName.UL); + + +/** + * Names of tags that have intrinsic content. + * TODO(robbyw): What about object, br, input, textarea, button, isindex, + * hr, keygen, select, table, tr, td? + * @type {Object} + * @private + */ +goog.editor.node.NON_EMPTY_TAGS_ = goog.object.createSet( + goog.dom.TagName.IMG, goog.dom.TagName.IFRAME, goog.dom.TagName.EMBED); + + +/** + * Check if the node is in a standards mode document. + * @param {Node} node The node to test. + * @return {boolean} Whether the node is in a standards mode document. + */ +goog.editor.node.isStandardsMode = function(node) { + return goog.dom.getDomHelper(node).isCss1CompatMode(); +}; + + +/** + * Get the right-most non-ignorable leaf node of the given node. + * @param {Node} parent The parent ndoe. + * @return {Node} The right-most non-ignorable leaf node. + */ +goog.editor.node.getRightMostLeaf = function(parent) { + var temp; + while (temp = goog.editor.node.getLastChild(parent)) { + parent = temp; + } + return parent; +}; + + +/** + * Get the left-most non-ignorable leaf node of the given node. + * @param {Node} parent The parent ndoe. + * @return {Node} The left-most non-ignorable leaf node. + */ +goog.editor.node.getLeftMostLeaf = function(parent) { + var temp; + while (temp = goog.editor.node.getFirstChild(parent)) { + parent = temp; + } + return parent; +}; + + +/** + * Version of firstChild that skips nodes that are entirely + * whitespace and comments. + * @param {Node} parent The reference node. + * @return {Node} The first child of sibling that is important according to + * goog.editor.node.isImportant, or null if no such node exists. + */ +goog.editor.node.getFirstChild = function(parent) { + return goog.editor.node.getChildHelper_(parent, false); +}; + + +/** + * Version of lastChild that skips nodes that are entirely whitespace or + * comments. (Normally lastChild is a property of all DOM nodes that gives the + * last of the nodes contained directly in the reference node.) + * @param {Node} parent The reference node. + * @return {Node} The last child of sibling that is important according to + * goog.editor.node.isImportant, or null if no such node exists. + */ +goog.editor.node.getLastChild = function(parent) { + return goog.editor.node.getChildHelper_(parent, true); +}; + + +/** + * Version of previoussibling that skips nodes that are entirely + * whitespace or comments. (Normally previousSibling is a property + * of all DOM nodes that gives the sibling node, the node that is + * a child of the same parent, that occurs immediately before the + * reference node.) + * @param {Node} sibling The reference node. + * @return {Node} The closest previous sibling to sibling that is + * important according to goog.editor.node.isImportant, or null if no such + * node exists. + */ +goog.editor.node.getPreviousSibling = function(sibling) { + return /** @type {Node} */ (goog.editor.node.getFirstValue_( + goog.iter.filter(new goog.dom.iter.SiblingIterator(sibling, false, true), + goog.editor.node.isImportant))); +}; + + +/** + * Version of nextSibling that skips nodes that are entirely whitespace or + * comments. + * @param {Node} sibling The reference node. + * @return {Node} The closest next sibling to sibling that is important + * according to goog.editor.node.isImportant, or null if no + * such node exists. + */ +goog.editor.node.getNextSibling = function(sibling) { + return /** @type {Node} */ (goog.editor.node.getFirstValue_( + goog.iter.filter(new goog.dom.iter.SiblingIterator(sibling), + goog.editor.node.isImportant))); +}; + + +/** + * Internal helper for lastChild/firstChild that skips nodes that are entirely + * whitespace or comments. + * @param {Node} parent The reference node. + * @param {boolean} isReversed Whether children should be traversed forward + * or backward. + * @return {Node} The first/last child of sibling that is important according + * to goog.editor.node.isImportant, or null if no such node exists. + * @private + */ +goog.editor.node.getChildHelper_ = function(parent, isReversed) { + return (!parent || parent.nodeType != goog.dom.NodeType.ELEMENT) ? null : + /** @type {Node} */ (goog.editor.node.getFirstValue_(goog.iter.filter( + new goog.dom.iter.ChildIterator( + /** @type {!Element} */ (parent), isReversed), + goog.editor.node.isImportant))); +}; + + +/** + * Utility function that returns the first value from an iterator or null if + * the iterator is empty. + * @param {goog.iter.Iterator} iterator The iterator to get a value from. + * @return {*} The first value from the iterator. + * @private + */ +goog.editor.node.getFirstValue_ = function(iterator) { + /** @preserveTry */ + try { + return iterator.next(); + } catch (e) { + return null; + } +}; + + +/** + * Determine if a node should be returned by the iterator functions. + * @param {Node} node An object implementing the DOM1 Node interface. + * @return {boolean} Whether the node is an element, or a text node that + * is not all whitespace. + */ +goog.editor.node.isImportant = function(node) { + // Return true if the node is not either a TextNode or an ElementNode. + return node.nodeType == goog.dom.NodeType.ELEMENT || + node.nodeType == goog.dom.NodeType.TEXT && + !goog.editor.node.isAllNonNbspWhiteSpace(node); +}; + + +/** + * Determine whether a node's text content is entirely whitespace. + * @param {Node} textNode A node implementing the CharacterData interface (i.e., + * a Text, Comment, or CDATASection node. + * @return {boolean} Whether the text content of node is whitespace, + * otherwise false. + */ +goog.editor.node.isAllNonNbspWhiteSpace = function(textNode) { + return goog.string.isBreakingWhitespace(textNode.nodeValue); +}; + + +/** + * Returns true if the node contains only whitespace and is not and does not + * contain any images, iframes or embed tags. + * @param {Node} node The node to check. + * @param {boolean=} opt_prohibitSingleNbsp By default, this function treats a + * single nbsp as empty. Set this to true to treat this case as non-empty. + * @return {boolean} Whether the node contains only whitespace. + */ +goog.editor.node.isEmpty = function(node, opt_prohibitSingleNbsp) { + var nodeData = goog.dom.getRawTextContent(node); + + if (node.getElementsByTagName) { + for (var tag in goog.editor.node.NON_EMPTY_TAGS_) { + if (node.tagName == tag || node.getElementsByTagName(tag).length > 0) { + return false; + } + } + } + return (!opt_prohibitSingleNbsp && nodeData == goog.string.Unicode.NBSP) || + goog.string.isBreakingWhitespace(nodeData); +}; + + +/** + * Returns the length of the text in node if it is a text node, or the number + * of children of the node, if it is an element. Useful for range-manipulation + * code where you need to know the offset for the right side of the node. + * @param {Node} node The node to get the length of. + * @return {number} The length of the node. + */ +goog.editor.node.getLength = function(node) { + return node.length || node.childNodes.length; +}; + + +/** + * Search child nodes using a predicate function and return the first node that + * satisfies the condition. + * @param {Node} parent The parent node to search. + * @param {function(Node):boolean} hasProperty A function that takes a child + * node as a parameter and returns true if it meets the criteria. + * @return {?number} The index of the node found, or null if no node is found. + */ +goog.editor.node.findInChildren = function(parent, hasProperty) { + for (var i = 0, len = parent.childNodes.length; i < len; i++) { + if (hasProperty(parent.childNodes[i])) { + return i; + } + } + return null; +}; + + +/** + * Search ancestor nodes using a predicate function and returns the topmost + * ancestor in the chain of consecutive ancestors that satisfies the condition. + * + * @param {Node} node The node whose ancestors have to be searched. + * @param {function(Node): boolean} hasProperty A function that takes a parent + * node as a parameter and returns true if it meets the criteria. + * @return {Node} The topmost ancestor or null if no ancestor satisfies the + * predicate function. + */ +goog.editor.node.findHighestMatchingAncestor = function(node, hasProperty) { + var parent = node.parentNode; + var ancestor = null; + while (parent && hasProperty(parent)) { + ancestor = parent; + parent = parent.parentNode; + } + return ancestor; +}; + + +/** +* Checks if node is a block-level html element. The <tt>display</tt> css + * property is ignored. + * @param {Node} node The node to test. + * @return {boolean} Whether the node is a block-level node. + */ +goog.editor.node.isBlockTag = function(node) { + return !!goog.editor.node.BLOCK_TAG_NAMES_[node.tagName]; +}; + + +/** + * Skips siblings of a node that are empty text nodes. + * @param {Node} node A node. May be null. + * @return {Node} The node or the first sibling of the node that is not an + * empty text node. May be null. + */ +goog.editor.node.skipEmptyTextNodes = function(node) { + while (node && node.nodeType == goog.dom.NodeType.TEXT && + !node.nodeValue) { + node = node.nextSibling; + } + return node; +}; + + +/** + * Checks if an element is a top-level editable container (meaning that + * it itself is not editable, but all its child nodes are editable). + * @param {Node} element The element to test. + * @return {boolean} Whether the element is a top-level editable container. + */ +goog.editor.node.isEditableContainer = function(element) { + return element.getAttribute && + element.getAttribute('g_editable') == 'true'; +}; + + +/** + * Checks if a node is inside an editable container. + * @param {Node} node The node to test. + * @return {boolean} Whether the node is in an editable container. + */ +goog.editor.node.isEditable = function(node) { + return !!goog.dom.getAncestor(node, goog.editor.node.isEditableContainer); +}; + + +/** + * Finds the top-most DOM node inside an editable field that is an ancestor + * (or self) of a given DOM node and meets the specified criteria. + * @param {Node} node The DOM node where the search starts. + * @param {function(Node) : boolean} criteria A function that takes a DOM node + * as a parameter and returns a boolean to indicate whether the node meets + * the criteria or not. + * @return {Node} The DOM node if found, or null. + */ +goog.editor.node.findTopMostEditableAncestor = function(node, criteria) { + var targetNode = null; + while (node && !goog.editor.node.isEditableContainer(node)) { + if (criteria(node)) { + targetNode = node; + } + node = node.parentNode; + } + return targetNode; +}; + + +/** + * Splits off a subtree. + * @param {!Node} currentNode The starting splitting point. + * @param {Node=} opt_secondHalf The initial leftmost leaf the new subtree. + * If null, siblings after currentNode will be placed in the subtree, but + * no additional node will be. + * @param {Node=} opt_root The top of the tree where splitting stops at. + * @return {!Node} The new subtree. + */ +goog.editor.node.splitDomTreeAt = function(currentNode, + opt_secondHalf, opt_root) { + var parent; + while (currentNode != opt_root && (parent = currentNode.parentNode)) { + opt_secondHalf = goog.editor.node.getSecondHalfOfNode_(parent, currentNode, + opt_secondHalf); + currentNode = parent; + } + return /** @type {!Node} */(opt_secondHalf); +}; + + +/** + * Creates a clone of node, moving all children after startNode to it. + * When firstChild is not null or undefined, it is also appended to the clone + * as the first child. + * @param {!Node} node The node to clone. + * @param {!Node} startNode All siblings after this node will be moved to the + * clone. + * @param {Node|undefined} firstChild The first child of the new cloned element. + * @return {!Node} The cloned node that now contains the children after + * startNode. + * @private + */ +goog.editor.node.getSecondHalfOfNode_ = function(node, startNode, firstChild) { + var secondHalf = /** @type {!Node} */(node.cloneNode(false)); + while (startNode.nextSibling) { + goog.dom.appendChild(secondHalf, startNode.nextSibling); + } + if (firstChild) { + secondHalf.insertBefore(firstChild, secondHalf.firstChild); + } + return secondHalf; +}; + + +/** + * Appends all of oldNode's children to newNode. This removes all children from + * oldNode and appends them to newNode. oldNode is left with no children. + * @param {!Node} newNode Node to transfer children to. + * @param {Node} oldNode Node to transfer children from. + * @deprecated Use goog.dom.append directly instead. + */ +goog.editor.node.transferChildren = function(newNode, oldNode) { + goog.dom.append(newNode, oldNode.childNodes); +}; + + +/** + * Replaces the innerHTML of a node. + * + * IE has serious problems if you try to set innerHTML of an editable node with + * any selection. Early versions of IE tear up the old internal tree storage, to + * help avoid ref-counting loops. But this sometimes leaves the selection object + * in a bad state and leads to segfaults. + * + * Removing the nodes first prevents IE from tearing them up. This is not + * strictly necessary in nodes that do not have the selection. You should always + * use this function when setting innerHTML inside of a field. + * + * @param {Node} node A node. + * @param {string} html The innerHTML to set on the node. + */ +goog.editor.node.replaceInnerHtml = function(node, html) { + // Only do this IE. On gecko, we use element change events, and don't + // want to trigger spurious events. + if (goog.userAgent.IE) { + goog.dom.removeChildren(node); + } + node.innerHTML = html; +}; http://git-wip-us.apache.org/repos/asf/flex-falcon/blob/e2cad6e6/externs/GCL/externs/goog/editor/plugin.js ---------------------------------------------------------------------- diff --git a/externs/GCL/externs/goog/editor/plugin.js b/externs/GCL/externs/goog/editor/plugin.js new file mode 100644 index 0000000..d823ccb --- /dev/null +++ b/externs/GCL/externs/goog/editor/plugin.js @@ -0,0 +1,463 @@ +// 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. +// All Rights Reserved. + +/** + * @fileoverview Abstract API for TrogEdit plugins. + * + * @see ../demos/editor/editor.html + */ + +goog.provide('goog.editor.Plugin'); + +// TODO(user): Remove the dependency on goog.editor.Command asap. Currently only +// needed for execCommand issues with links. +goog.require('goog.events.EventTarget'); +goog.require('goog.functions'); +goog.require('goog.log'); +goog.require('goog.object'); +goog.require('goog.reflect'); +goog.require('goog.userAgent'); + + + +/** + * Abstract API for trogedit plugins. + * @constructor + * @extends {goog.events.EventTarget} + */ +goog.editor.Plugin = function() { + goog.events.EventTarget.call(this); + + /** + * Whether this plugin is enabled for the registered field object. + * @type {boolean} + * @private + */ + this.enabled_ = this.activeOnUneditableFields(); +}; +goog.inherits(goog.editor.Plugin, goog.events.EventTarget); + + +/** + * The field object this plugin is attached to. + * @type {goog.editor.Field} + * @protected + * @deprecated Use goog.editor.Plugin.getFieldObject and + * goog.editor.Plugin.setFieldObject. + */ +goog.editor.Plugin.prototype.fieldObject = null; + + +/** + * @return {goog.dom.DomHelper?} The dom helper object associated with the + * currently active field. + */ +goog.editor.Plugin.prototype.getFieldDomHelper = function() { + return this.getFieldObject() && this.getFieldObject().getEditableDomHelper(); +}; + + +/** + * Indicates if this plugin should be automatically disposed when the + * registered field is disposed. This should be changed to false for + * plugins used as multi-field plugins. + * @type {boolean} + * @private + */ +goog.editor.Plugin.prototype.autoDispose_ = true; + + +/** + * The logger for this plugin. + * @type {goog.log.Logger} + * @protected + */ +goog.editor.Plugin.prototype.logger = + goog.log.getLogger('goog.editor.Plugin'); + + +/** + * Sets the field object for use with this plugin. + * @return {goog.editor.Field} The editable field object. + * @protected + * @suppress {deprecated} Until fieldObject can be made private. + */ +goog.editor.Plugin.prototype.getFieldObject = function() { + return this.fieldObject; +}; + + +/** + * Sets the field object for use with this plugin. + * @param {goog.editor.Field} fieldObject The editable field object. + * @protected + * @suppress {deprecated} Until fieldObject can be made private. + */ +goog.editor.Plugin.prototype.setFieldObject = function(fieldObject) { + this.fieldObject = fieldObject; +}; + + +/** + * Registers the field object for use with this plugin. + * @param {goog.editor.Field} fieldObject The editable field object. + */ +goog.editor.Plugin.prototype.registerFieldObject = function(fieldObject) { + this.setFieldObject(fieldObject); +}; + + +/** + * Unregisters and disables this plugin for the current field object. + * @param {goog.editor.Field} fieldObj The field object. For single-field + * plugins, this parameter is ignored. + */ +goog.editor.Plugin.prototype.unregisterFieldObject = function(fieldObj) { + if (this.getFieldObject()) { + this.disable(this.getFieldObject()); + this.setFieldObject(null); + } +}; + + +/** + * Enables this plugin for the specified, registered field object. A field + * object should only be enabled when it is loaded. + * @param {goog.editor.Field} fieldObject The field object. + */ +goog.editor.Plugin.prototype.enable = function(fieldObject) { + if (this.getFieldObject() == fieldObject) { + this.enabled_ = true; + } else { + goog.log.error(this.logger, 'Trying to enable an unregistered field with ' + + 'this plugin.'); + } +}; + + +/** + * Disables this plugin for the specified, registered field object. + * @param {goog.editor.Field} fieldObject The field object. + */ +goog.editor.Plugin.prototype.disable = function(fieldObject) { + if (this.getFieldObject() == fieldObject) { + this.enabled_ = false; + } else { + goog.log.error(this.logger, 'Trying to disable an unregistered field ' + + 'with this plugin.'); + } +}; + + +/** + * Returns whether this plugin is enabled for the field object. + * + * @param {goog.editor.Field} fieldObject The field object. + * @return {boolean} Whether this plugin is enabled for the field object. + */ +goog.editor.Plugin.prototype.isEnabled = function(fieldObject) { + return this.getFieldObject() == fieldObject ? this.enabled_ : false; +}; + + +/** + * Set if this plugin should automatically be disposed when the registered + * field is disposed. + * @param {boolean} autoDispose Whether to autoDispose. + */ +goog.editor.Plugin.prototype.setAutoDispose = function(autoDispose) { + this.autoDispose_ = autoDispose; +}; + + +/** + * @return {boolean} Whether or not this plugin should automatically be disposed + * when it's registered field is disposed. + */ +goog.editor.Plugin.prototype.isAutoDispose = function() { + return this.autoDispose_; +}; + + +/** + * @return {boolean} If true, field will not disable the command + * when the field becomes uneditable. + */ +goog.editor.Plugin.prototype.activeOnUneditableFields = goog.functions.FALSE; + + +/** + * @param {string} command The command to check. + * @return {boolean} If true, field will not dispatch change events + * for commands of this type. This is useful for "seamless" plugins like + * dialogs and lorem ipsum. + */ +goog.editor.Plugin.prototype.isSilentCommand = goog.functions.FALSE; + + +/** @override */ +goog.editor.Plugin.prototype.disposeInternal = function() { + if (this.getFieldObject()) { + this.unregisterFieldObject(this.getFieldObject()); + } + + goog.editor.Plugin.superClass_.disposeInternal.call(this); +}; + + +/** + * @return {string} The ID unique to this plugin class. Note that different + * instances off the plugin share the same classId. + */ +goog.editor.Plugin.prototype.getTrogClassId; + + +/** + * An enum of operations that plugins may support. + * @enum {number} + */ +goog.editor.Plugin.Op = { + KEYDOWN: 1, + KEYPRESS: 2, + KEYUP: 3, + SELECTION: 4, + SHORTCUT: 5, + EXEC_COMMAND: 6, + QUERY_COMMAND: 7, + PREPARE_CONTENTS_HTML: 8, + CLEAN_CONTENTS_HTML: 10, + CLEAN_CONTENTS_DOM: 11 +}; + + +/** + * A map from plugin operations to the names of the methods that + * invoke those operations. + */ +goog.editor.Plugin.OPCODE = goog.object.transpose( + goog.reflect.object(goog.editor.Plugin, { + handleKeyDown: goog.editor.Plugin.Op.KEYDOWN, + handleKeyPress: goog.editor.Plugin.Op.KEYPRESS, + handleKeyUp: goog.editor.Plugin.Op.KEYUP, + handleSelectionChange: goog.editor.Plugin.Op.SELECTION, + handleKeyboardShortcut: goog.editor.Plugin.Op.SHORTCUT, + execCommand: goog.editor.Plugin.Op.EXEC_COMMAND, + queryCommandValue: goog.editor.Plugin.Op.QUERY_COMMAND, + prepareContentsHtml: goog.editor.Plugin.Op.PREPARE_CONTENTS_HTML, + cleanContentsHtml: goog.editor.Plugin.Op.CLEAN_CONTENTS_HTML, + cleanContentsDom: goog.editor.Plugin.Op.CLEAN_CONTENTS_DOM + })); + + +/** + * A set of op codes that run even on disabled plugins. + */ +goog.editor.Plugin.IRREPRESSIBLE_OPS = goog.object.createSet( + goog.editor.Plugin.Op.PREPARE_CONTENTS_HTML, + goog.editor.Plugin.Op.CLEAN_CONTENTS_HTML, + goog.editor.Plugin.Op.CLEAN_CONTENTS_DOM); + + +/** + * Handles keydown. It is run before handleKeyboardShortcut and if it returns + * true handleKeyboardShortcut will not be called. + * @param {!goog.events.BrowserEvent} e The browser event. + * @return {boolean} Whether the event was handled and thus should *not* be + * propagated to other plugins or handleKeyboardShortcut. + */ +goog.editor.Plugin.prototype.handleKeyDown; + + +/** + * Handles keypress. It is run before handleKeyboardShortcut and if it returns + * true handleKeyboardShortcut will not be called. + * @param {!goog.events.BrowserEvent} e The browser event. + * @return {boolean} Whether the event was handled and thus should *not* be + * propagated to other plugins or handleKeyboardShortcut. + */ +goog.editor.Plugin.prototype.handleKeyPress; + + +/** + * Handles keyup. + * @param {!goog.events.BrowserEvent} e The browser event. + * @return {boolean} Whether the event was handled and thus should *not* be + * propagated to other plugins. + */ +goog.editor.Plugin.prototype.handleKeyUp; + + +/** + * Handles selection change. + * @param {!goog.events.BrowserEvent=} opt_e The browser event. + * @param {!Node=} opt_target The node the selection changed to. + * @return {boolean} Whether the event was handled and thus should *not* be + * propagated to other plugins. + */ +goog.editor.Plugin.prototype.handleSelectionChange; + + +/** + * Handles keyboard shortcuts. Preferred to using handleKey* as it will use + * the proper event based on browser and will be more performant. If + * handleKeyPress/handleKeyDown returns true, this will not be called. If the + * plugin handles the shortcut, it is responsible for dispatching appropriate + * events (change, selection change at the time of this comment). If the plugin + * calls execCommand on the editable field, then execCommand already takes care + * of dispatching events. + * NOTE: For performance reasons this is only called when any key is pressed + * in conjunction with ctrl/meta keys OR when a small subset of keys (defined + * in goog.editor.Field.POTENTIAL_SHORTCUT_KEYCODES_) are pressed without + * ctrl/meta keys. We specifically don't invoke it when altKey is pressed since + * alt key is used in many i8n UIs to enter certain characters. + * @param {!goog.events.BrowserEvent} e The browser event. + * @param {string} key The key pressed. + * @param {boolean} isModifierPressed Whether the ctrl/meta key was pressed or + * not. + * @return {boolean} Whether the event was handled and thus should *not* be + * propagated to other plugins. We also call preventDefault on the event if + * the return value is true. + */ +goog.editor.Plugin.prototype.handleKeyboardShortcut; + + +/** + * Handles execCommand. This default implementation handles dispatching + * BEFORECHANGE, CHANGE, and SELECTIONCHANGE events, and calls + * execCommandInternal to perform the actual command. Plugins that want to + * do their own event dispatching should override execCommand, otherwise + * it is preferred to only override execCommandInternal. + * + * This version of execCommand will only work for single field plugins. + * Multi-field plugins must override execCommand. + * + * @param {string} command The command to execute. + * @param {...*} var_args Any additional parameters needed to + * execute the command. + * @return {*} The result of the execCommand, if any. + */ +goog.editor.Plugin.prototype.execCommand = function(command, var_args) { + // TODO(user): Replace all uses of isSilentCommand with plugins that just + // override this base execCommand method. + var silent = this.isSilentCommand(command); + if (!silent) { + // Stop listening to mutation events in Firefox while text formatting + // is happening. This prevents us from trying to size the field in the + // middle of an execCommand, catching the field in a strange intermediary + // state where both replacement nodes and original nodes are appended to + // the dom. Note that change events get turned back on by + // fieldObj.dispatchChange. + if (goog.userAgent.GECKO) { + this.getFieldObject().stopChangeEvents(true, true); + } + + this.getFieldObject().dispatchBeforeChange(); + } + + try { + var result = this.execCommandInternal.apply(this, arguments); + } finally { + // If the above execCommandInternal call throws an exception, we still need + // to turn change events back on (see http://b/issue?id=1471355). + // NOTE: If if you add to or change the methods called in this finally + // block, please add them as expected calls to the unit test function + // testExecCommandException(). + if (!silent) { + // dispatchChange includes a call to startChangeEvents, which unwinds the + // call to stopChangeEvents made before the try block. + this.getFieldObject().dispatchChange(); + this.getFieldObject().dispatchSelectionChangeEvent(); + } + } + + return result; +}; + + +/** + * Handles execCommand. This default implementation does nothing, and is + * called by execCommand, which handles event dispatching. This method should + * be overriden by plugins that don't need to do their own event dispatching. + * If custom event dispatching is needed, execCommand shoul be overriden + * instead. + * + * @param {string} command The command to execute. + * @param {...*} var_args Any additional parameters needed to + * execute the command. + * @return {*} The result of the execCommand, if any. + * @protected + */ +goog.editor.Plugin.prototype.execCommandInternal; + + +/** + * Gets the state of this command if this plugin serves that command. + * @param {string} command The command to check. + * @return {*} The value of the command. + */ +goog.editor.Plugin.prototype.queryCommandValue; + + +/** + * Prepares the given HTML for editing. Strips out content that should not + * appear in an editor, and normalizes content as appropriate. The inverse + * of cleanContentsHtml. + * + * This op is invoked even on disabled plugins. + * + * @param {string} originalHtml The original HTML. + * @param {Object} styles A map of strings. If the plugin wants to add + * any styles to the field element, it should add them as key-value + * pairs to this object. + * @return {string} New HTML that's ok for editing. + */ +goog.editor.Plugin.prototype.prepareContentsHtml; + + +/** + * Cleans the contents of the node passed to it. The node contents are modified + * directly, and the modifications will subsequently be used, for operations + * such as saving the innerHTML of the editor etc. Since the plugins act on + * the DOM directly, this method can be very expensive. + * + * This op is invoked even on disabled plugins. + * + * @param {!Element} fieldCopy The copy of the editable field which + * needs to be cleaned up. + */ +goog.editor.Plugin.prototype.cleanContentsDom; + + +/** + * Cleans the html contents of Trogedit. Both cleanContentsDom and + * and cleanContentsHtml will be called on contents extracted from Trogedit. + * The inverse of prepareContentsHtml. + * + * This op is invoked even on disabled plugins. + * + * @param {string} originalHtml The trogedit HTML. + * @return {string} Cleaned-up HTML. + */ +goog.editor.Plugin.prototype.cleanContentsHtml; + + +/** + * Whether the string corresponds to a command this plugin handles. + * @param {string} command Command string to check. + * @return {boolean} Whether the plugin handles this type of command. + */ +goog.editor.Plugin.prototype.isSupportedCommand = function(command) { + return false; +}; http://git-wip-us.apache.org/repos/asf/flex-falcon/blob/e2cad6e6/externs/GCL/externs/goog/editor/plugins/abstractbubbleplugin.js ---------------------------------------------------------------------- diff --git a/externs/GCL/externs/goog/editor/plugins/abstractbubbleplugin.js b/externs/GCL/externs/goog/editor/plugins/abstractbubbleplugin.js new file mode 100644 index 0000000..4951ed6 --- /dev/null +++ b/externs/GCL/externs/goog/editor/plugins/abstractbubbleplugin.js @@ -0,0 +1,712 @@ +// Copyright 2005 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. + * @author [email protected] (Robby Walker) + */ + +goog.provide('goog.editor.plugins.AbstractBubblePlugin'); + +goog.require('goog.array'); +goog.require('goog.dom'); +goog.require('goog.dom.NodeType'); +goog.require('goog.dom.Range'); +goog.require('goog.dom.TagName'); +goog.require('goog.dom.classlist'); +goog.require('goog.editor.Plugin'); +goog.require('goog.editor.style'); +goog.require('goog.events'); +goog.require('goog.events.EventHandler'); +goog.require('goog.events.EventType'); +goog.require('goog.events.KeyCodes'); +goog.require('goog.events.actionEventWrapper'); +goog.require('goog.functions'); +goog.require('goog.string.Unicode'); +goog.require('goog.ui.Component'); +goog.require('goog.ui.editor.Bubble'); +goog.require('goog.userAgent'); + + + +/** + * Base class for bubble plugins. This is used for to connect user behavior + * in the editor to a goog.ui.editor.Bubble UI element that allows + * the user to modify the properties of an element on their page (e.g. the alt + * text of an image tag). + * + * Subclasses should override the abstract method getBubbleTargetFromSelection() + * with code to determine if the current selection should activate the bubble + * type. The other abstract method createBubbleContents() should be overriden + * with code to create the inside markup of the bubble. The base class creates + * the rest of the bubble. + * + * @constructor + * @extends {goog.editor.Plugin} + */ +goog.editor.plugins.AbstractBubblePlugin = function() { + goog.editor.plugins.AbstractBubblePlugin.base(this, 'constructor'); + + /** + * Place to register events the plugin listens to. + * @type {goog.events.EventHandler< + * !goog.editor.plugins.AbstractBubblePlugin>} + * @protected + */ + this.eventRegister = new goog.events.EventHandler(this); + + /** + * Instance factory function that creates a bubble UI component. If set to a + * non-null value, this function will be used to create a bubble instead of + * the global factory function. It takes as parameters the bubble parent + * element and the z index to draw the bubble at. + * @type {?function(!Element, number): !goog.ui.editor.Bubble} + * @private + */ + this.bubbleFactory_ = null; +}; +goog.inherits(goog.editor.plugins.AbstractBubblePlugin, goog.editor.Plugin); + + +/** + * The css class name of option link elements. + * @type {string} + * @private + */ +goog.editor.plugins.AbstractBubblePlugin.OPTION_LINK_CLASSNAME_ = + goog.getCssName('tr_option-link'); + + +/** + * The css class name of link elements. + * @type {string} + * @private + */ +goog.editor.plugins.AbstractBubblePlugin.LINK_CLASSNAME_ = + goog.getCssName('tr_bubble_link'); + + +/** + * A class name to mark elements that should be reachable by keyboard tabbing. + * @type {string} + * @private + */ +goog.editor.plugins.AbstractBubblePlugin.TABBABLE_CLASSNAME_ = + goog.getCssName('tr_bubble_tabbable'); + + +/** + * The constant string used to separate option links. + * @type {string} + * @protected + */ +goog.editor.plugins.AbstractBubblePlugin.DASH_NBSP_STRING = + goog.string.Unicode.NBSP + '-' + goog.string.Unicode.NBSP; + + +/** + * Default factory function for creating a bubble UI component. + * @param {!Element} parent The parent element for the bubble. + * @param {number} zIndex The z index to draw the bubble at. + * @return {!goog.ui.editor.Bubble} The new bubble component. + * @private + */ +goog.editor.plugins.AbstractBubblePlugin.defaultBubbleFactory_ = function( + parent, zIndex) { + return new goog.ui.editor.Bubble(parent, zIndex); +}; + + +/** + * Global factory function that creates a bubble UI component. It takes as + * parameters the bubble parent element and the z index to draw the bubble at. + * @type {function(!Element, number): !goog.ui.editor.Bubble} + * @private + */ +goog.editor.plugins.AbstractBubblePlugin.globalBubbleFactory_ = + goog.editor.plugins.AbstractBubblePlugin.defaultBubbleFactory_; + + +/** + * Sets the global bubble factory function. + * @param {function(!Element, number): !goog.ui.editor.Bubble} + * bubbleFactory Function that creates a bubble for the given bubble parent + * element and z index. + */ +goog.editor.plugins.AbstractBubblePlugin.setBubbleFactory = function( + bubbleFactory) { + goog.editor.plugins.AbstractBubblePlugin.globalBubbleFactory_ = bubbleFactory; +}; + + +/** + * Map from field id to shared bubble object. + * @type {!Object<goog.ui.editor.Bubble>} + * @private + */ +goog.editor.plugins.AbstractBubblePlugin.bubbleMap_ = {}; + + +/** + * The optional parent of the bubble. If null or not set, we will use the + * application document. This is useful when you have an editor embedded in + * a scrolling DIV. + * @type {Element|undefined} + * @private + */ +goog.editor.plugins.AbstractBubblePlugin.prototype.bubbleParent_; + + +/** + * The id of the panel this plugin added to the shared bubble. Null when + * this plugin doesn't currently have a panel in a bubble. + * @type {string?} + * @private + */ +goog.editor.plugins.AbstractBubblePlugin.prototype.panelId_ = null; + + +/** + * Whether this bubble should support tabbing through elements. False + * by default. + * @type {boolean} + * @private + */ +goog.editor.plugins.AbstractBubblePlugin.prototype.keyboardNavigationEnabled_ = + false; + + +/** + * Sets the instance bubble factory function. If set to a non-null value, this + * function will be used to create a bubble instead of the global factory + * function. + * @param {?function(!Element, number): !goog.ui.editor.Bubble} bubbleFactory + * Function that creates a bubble for the given bubble parent element and z + * index. Null to reset the factory function. + */ +goog.editor.plugins.AbstractBubblePlugin.prototype.setBubbleFactory = function( + bubbleFactory) { + this.bubbleFactory_ = bubbleFactory; +}; + + +/** + * Sets whether the bubble should support tabbing through elements. + * @param {boolean} keyboardNavigationEnabled + */ +goog.editor.plugins.AbstractBubblePlugin.prototype.enableKeyboardNavigation = + function(keyboardNavigationEnabled) { + this.keyboardNavigationEnabled_ = keyboardNavigationEnabled; +}; + + +/** + * Sets the bubble parent. + * @param {Element} bubbleParent An element where the bubble will be + * anchored. If null, we will use the application document. This + * is useful when you have an editor embedded in a scrolling div. + */ +goog.editor.plugins.AbstractBubblePlugin.prototype.setBubbleParent = function( + bubbleParent) { + this.bubbleParent_ = bubbleParent; +}; + + +/** + * Returns the bubble map. Subclasses may override to use a separate map. + * @return {!Object<goog.ui.editor.Bubble>} + * @protected + */ +goog.editor.plugins.AbstractBubblePlugin.prototype.getBubbleMap = function() { + return goog.editor.plugins.AbstractBubblePlugin.bubbleMap_; +}; + + +/** + * @return {goog.dom.DomHelper} The dom helper for the bubble window. + */ +goog.editor.plugins.AbstractBubblePlugin.prototype.getBubbleDom = function() { + return this.dom_; +}; + + +/** @override */ +goog.editor.plugins.AbstractBubblePlugin.prototype.getTrogClassId = + goog.functions.constant('AbstractBubblePlugin'); + + +/** + * Returns the element whose properties the bubble manipulates. + * @return {Element} The target element. + */ +goog.editor.plugins.AbstractBubblePlugin.prototype.getTargetElement = + function() { + return this.targetElement_; +}; + + +/** @override */ +goog.editor.plugins.AbstractBubblePlugin.prototype.handleKeyUp = function(e) { + // For example, when an image is selected, pressing any key overwrites + // the image and the panel should be hidden. + // Therefore we need to track key presses when the bubble is showing. + if (this.isVisible()) { + this.handleSelectionChange(); + } + return false; +}; + + +/** + * Pops up a property bubble for the given selection if appropriate and closes + * open property bubbles if no longer needed. This should not be overridden. + * @override + */ +goog.editor.plugins.AbstractBubblePlugin.prototype.handleSelectionChange = + function(opt_e, opt_target) { + var selectedElement; + if (opt_e) { + selectedElement = /** @type {Element} */ (opt_e.target); + } else if (opt_target) { + selectedElement = /** @type {Element} */ (opt_target); + } else { + var range = this.getFieldObject().getRange(); + if (range) { + var startNode = range.getStartNode(); + var endNode = range.getEndNode(); + var startOffset = range.getStartOffset(); + var endOffset = range.getEndOffset(); + // Sometimes in IE, the range will be collapsed, but think the end node + // and start node are different (although in the same visible position). + // In this case, favor the position IE thinks is the start node. + if (goog.userAgent.IE && range.isCollapsed() && startNode != endNode) { + range = goog.dom.Range.createCaret(startNode, startOffset); + } + if (startNode.nodeType == goog.dom.NodeType.ELEMENT && + startNode == endNode && startOffset == endOffset - 1) { + var element = startNode.childNodes[startOffset]; + if (element.nodeType == goog.dom.NodeType.ELEMENT) { + selectedElement = element; + } + } + } + selectedElement = selectedElement || range && range.getContainerElement(); + } + return this.handleSelectionChangeInternal(selectedElement); +}; + + +/** + * Pops up a property bubble for the given selection if appropriate and closes + * open property bubbles if no longer needed. + * @param {Element?} selectedElement The selected element. + * @return {boolean} Always false, allowing every bubble plugin to handle the + * event. + * @protected + */ +goog.editor.plugins.AbstractBubblePlugin.prototype. + handleSelectionChangeInternal = function(selectedElement) { + if (selectedElement) { + var bubbleTarget = this.getBubbleTargetFromSelection(selectedElement); + if (bubbleTarget) { + if (bubbleTarget != this.targetElement_ || !this.panelId_) { + // Make sure any existing panel of the same type is closed before + // creating a new one. + if (this.panelId_) { + this.closeBubble(); + } + this.createBubble(bubbleTarget); + } + return false; + } + } + + if (this.panelId_) { + this.closeBubble(); + } + + return false; +}; + + +/** + * Should be overriden by subclasses to return the bubble target element or + * null if an element of their required type isn't found. + * @param {Element} selectedElement The target of the selection change event or + * the parent container of the current entire selection. + * @return {Element?} The HTML bubble target element or null if no element of + * the required type is not found. + */ +goog.editor.plugins.AbstractBubblePlugin.prototype. + getBubbleTargetFromSelection = goog.abstractMethod; + + +/** @override */ +goog.editor.plugins.AbstractBubblePlugin.prototype.disable = function(field) { + // When the field is made uneditable, dispose of the bubble. We do this + // because the next time the field is made editable again it may be in + // a different document / iframe. + if (field.isUneditable()) { + var bubbleMap = this.getBubbleMap(); + var bubble = bubbleMap[field.id]; + if (bubble) { + if (field == this.getFieldObject()) { + this.closeBubble(); + } + bubble.dispose(); + delete bubbleMap[field.id]; + } + } +}; + + +/** + * @return {!goog.ui.editor.Bubble} The shared bubble object for the field this + * plugin is registered on. Creates it if necessary. + * @private + */ +goog.editor.plugins.AbstractBubblePlugin.prototype.getSharedBubble_ = + function() { + var bubbleParent = /** @type {!Element} */ (this.bubbleParent_ || + this.getFieldObject().getAppWindow().document.body); + this.dom_ = goog.dom.getDomHelper(bubbleParent); + + var bubbleMap = this.getBubbleMap(); + var bubble = bubbleMap[this.getFieldObject().id]; + if (!bubble) { + var factory = this.bubbleFactory_ || + goog.editor.plugins.AbstractBubblePlugin.globalBubbleFactory_; + bubble = factory.call(null, bubbleParent, + this.getFieldObject().getBaseZindex()); + bubbleMap[this.getFieldObject().id] = bubble; + } + return bubble; +}; + + +/** + * Creates and shows the property bubble. + * @param {Element} targetElement The target element of the bubble. + */ +goog.editor.plugins.AbstractBubblePlugin.prototype.createBubble = function( + targetElement) { + var bubble = this.getSharedBubble_(); + if (!bubble.hasPanelOfType(this.getBubbleType())) { + this.targetElement_ = targetElement; + + this.panelId_ = bubble.addPanel(this.getBubbleType(), this.getBubbleTitle(), + targetElement, + goog.bind(this.createBubbleContents, this), + this.shouldPreferBubbleAboveElement()); + this.eventRegister.listen(bubble, goog.ui.Component.EventType.HIDE, + this.handlePanelClosed_); + + this.onShow(); + + if (this.keyboardNavigationEnabled_) { + this.eventRegister.listen(bubble.getContentElement(), + goog.events.EventType.KEYDOWN, this.onBubbleKey_); + } + } +}; + + +/** + * @return {string} The type of bubble shown by this plugin. Usually the tag + * name of the element this bubble targets. + * @protected + */ +goog.editor.plugins.AbstractBubblePlugin.prototype.getBubbleType = function() { + return ''; +}; + + +/** + * @return {string} The title for bubble shown by this plugin. Defaults to no + * title. Should be overridden by subclasses. + * @protected + */ +goog.editor.plugins.AbstractBubblePlugin.prototype.getBubbleTitle = function() { + return ''; +}; + + +/** + * @return {boolean} Whether the bubble should prefer placement above the + * target element. + * @protected + */ +goog.editor.plugins.AbstractBubblePlugin.prototype. + shouldPreferBubbleAboveElement = goog.functions.FALSE; + + +/** + * Should be overriden by subclasses to add the type specific contents to the + * bubble. + * @param {Element} bubbleContainer The container element of the bubble to + * which the contents should be added. + * @protected + */ +goog.editor.plugins.AbstractBubblePlugin.prototype.createBubbleContents = + goog.abstractMethod; + + +/** + * Register the handler for the target's CLICK event. + * @param {Element} target The event source element. + * @param {Function} handler The event handler. + * @protected + * @deprecated Use goog.editor.plugins.AbstractBubblePlugin. + * registerActionHandler to register click and enter events. + */ +goog.editor.plugins.AbstractBubblePlugin.prototype.registerClickHandler = + function(target, handler) { + this.registerActionHandler(target, handler); +}; + + +/** + * Register the handler for the target's CLICK and ENTER key events. + * @param {Element} target The event source element. + * @param {Function} handler The event handler. + * @protected + */ +goog.editor.plugins.AbstractBubblePlugin.prototype.registerActionHandler = + function(target, handler) { + this.eventRegister.listenWithWrapper(target, goog.events.actionEventWrapper, + handler); +}; + + +/** + * Closes the bubble. + */ +goog.editor.plugins.AbstractBubblePlugin.prototype.closeBubble = function() { + if (this.panelId_) { + this.getSharedBubble_().removePanel(this.panelId_); + this.handlePanelClosed_(); + } +}; + + +/** + * Called after the bubble is shown. The default implementation does nothing. + * Override it to provide your own one. + * @protected + */ +goog.editor.plugins.AbstractBubblePlugin.prototype.onShow = goog.nullFunction; + + +/** + * Called when the bubble is closed or hidden. The default implementation does + * nothing. + * @protected + */ +goog.editor.plugins.AbstractBubblePlugin.prototype.cleanOnBubbleClose = + goog.nullFunction; + + +/** + * Handles when the bubble panel is closed. Invoked when the entire bubble is + * hidden and also directly when the panel is closed manually. + * @private + */ +goog.editor.plugins.AbstractBubblePlugin.prototype.handlePanelClosed_ = + function() { + this.targetElement_ = null; + this.panelId_ = null; + this.eventRegister.removeAll(); + this.cleanOnBubbleClose(); +}; + + +/** + * In case the keyboard navigation is enabled, this will set focus on the first + * tabbable element in the bubble when TAB is clicked. + * @override + */ +goog.editor.plugins.AbstractBubblePlugin.prototype.handleKeyDown = function(e) { + if (this.keyboardNavigationEnabled_ && + this.isVisible() && + e.keyCode == goog.events.KeyCodes.TAB && !e.shiftKey) { + var bubbleEl = this.getSharedBubble_().getContentElement(); + var tabbable = goog.dom.getElementByClass( + goog.editor.plugins.AbstractBubblePlugin.TABBABLE_CLASSNAME_, bubbleEl); + if (tabbable) { + tabbable.focus(); + e.preventDefault(); + return true; + } + } + return false; +}; + + +/** + * Handles a key event on the bubble. This ensures that the focus loops through + * the tabbable elements found in the bubble and then the focus is got by the + * field element. + * @param {goog.events.BrowserEvent} e The event. + * @private + */ +goog.editor.plugins.AbstractBubblePlugin.prototype.onBubbleKey_ = function(e) { + if (this.isVisible() && + e.keyCode == goog.events.KeyCodes.TAB) { + var bubbleEl = this.getSharedBubble_().getContentElement(); + var tabbables = goog.dom.getElementsByClass( + goog.editor.plugins.AbstractBubblePlugin.TABBABLE_CLASSNAME_, bubbleEl); + var tabbable = e.shiftKey ? tabbables[0] : goog.array.peek(tabbables); + var tabbingOutOfBubble = tabbable == e.target; + if (tabbingOutOfBubble) { + this.getFieldObject().focus(); + e.preventDefault(); + } + } +}; + + +/** + * @return {boolean} Whether the bubble is visible. + */ +goog.editor.plugins.AbstractBubblePlugin.prototype.isVisible = function() { + return !!this.panelId_; +}; + + +/** + * Reposition the property bubble. + */ +goog.editor.plugins.AbstractBubblePlugin.prototype.reposition = function() { + var bubble = this.getSharedBubble_(); + if (bubble) { + bubble.reposition(); + } +}; + + +/** + * Helper method that creates option links (such as edit, test, remove) + * @param {string} id String id for the span id. + * @return {Element} The option link element. + * @protected + */ +goog.editor.plugins.AbstractBubblePlugin.prototype.createLinkOption = function( + id) { + // Dash plus link are together in a span so we can hide/show them easily + return this.dom_.createDom(goog.dom.TagName.SPAN, + { + id: id, + className: + goog.editor.plugins.AbstractBubblePlugin.OPTION_LINK_CLASSNAME_ + }, + this.dom_.createTextNode( + goog.editor.plugins.AbstractBubblePlugin.DASH_NBSP_STRING)); +}; + + +/** + * Helper method that creates a link with text set to linkText and optionally + * wires up a listener for the CLICK event or the link. The link is navigable by + * tabs if {@code enableKeyboardNavigation(true)} was called. + * @param {string} linkId The id of the link. + * @param {string} linkText Text of the link. + * @param {Function=} opt_onClick Optional function to call when the link is + * clicked. + * @param {Element=} opt_container If specified, location to insert link. If no + * container is specified, the old link is removed and replaced. + * @return {Element} The link element. + * @protected + */ +goog.editor.plugins.AbstractBubblePlugin.prototype.createLink = function( + linkId, linkText, opt_onClick, opt_container) { + var link = this.createLinkHelper(linkId, linkText, false, opt_container); + if (opt_onClick) { + this.registerActionHandler(link, opt_onClick); + } + return link; +}; + + +/** + * Helper method to create a link to insert into the bubble. The link is + * navigable by tabs if {@code enableKeyboardNavigation(true)} was called. + * @param {string} linkId The id of the link. + * @param {string} linkText Text of the link. + * @param {boolean} isAnchor Set to true to create an actual anchor tag + * instead of a span. Actual links are right clickable (e.g. to open in + * a new window) and also update window status on hover. + * @param {Element=} opt_container If specified, location to insert link. If no + * container is specified, the old link is removed and replaced. + * @return {Element} The link element. + * @protected + */ +goog.editor.plugins.AbstractBubblePlugin.prototype.createLinkHelper = function( + linkId, linkText, isAnchor, opt_container) { + var link = this.dom_.createDom( + isAnchor ? goog.dom.TagName.A : goog.dom.TagName.SPAN, + {className: goog.editor.plugins.AbstractBubblePlugin.LINK_CLASSNAME_}, + linkText); + if (this.keyboardNavigationEnabled_) { + this.setTabbable(link); + } + link.setAttribute('role', 'link'); + this.setupLink(link, linkId, opt_container); + goog.editor.style.makeUnselectable(link, this.eventRegister); + return link; +}; + + +/** + * Makes the given element tabbable. + * + * <p>Elements created by createLink[Helper] are tabbable even without + * calling this method. Call it for other elements if needed. + * + * <p>If tabindex is not already set in the element, this function sets it to 0. + * You'll usually want to also call {@code enableKeyboardNavigation(true)}. + * + * @param {!Element} element + * @protected + */ +goog.editor.plugins.AbstractBubblePlugin.prototype.setTabbable = + function(element) { + if (!element.hasAttribute('tabindex')) { + element.setAttribute('tabindex', 0); + } + goog.dom.classlist.add(element, + goog.editor.plugins.AbstractBubblePlugin.TABBABLE_CLASSNAME_); +}; + + +/** + * Inserts a link in the given container if it is specified or removes + * the old link with this id and replaces it with the new link + * @param {Element} link Html element to insert. + * @param {string} linkId Id of the link. + * @param {Element=} opt_container If specified, location to insert link. + * @protected + */ +goog.editor.plugins.AbstractBubblePlugin.prototype.setupLink = function( + link, linkId, opt_container) { + if (opt_container) { + opt_container.appendChild(link); + } else { + var oldLink = this.dom_.getElement(linkId); + if (oldLink) { + goog.dom.replaceNode(link, oldLink); + } + } + + link.id = linkId; +};
