123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596 |
- // 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.Range');
- goog.require('goog.dom.TagName');
- goog.require('goog.editor.Command');
- goog.require('goog.editor.Link');
- goog.require('goog.editor.plugins.AbstractBubblePlugin');
- 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 String(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.
- * @param {!Event=} opt_event If passed in, the event will be stopped.
- * @protected
- */
- goog.editor.plugins.LinkBubble.prototype.testLink = function(opt_event) {
- goog.window.open(
- this.getTestLinkAction_(),
- {'target': '_blank', 'noreferrer': this.stopReferrerLeaks_},
- this.getFieldObject().getAppWindow());
- if (opt_event) {
- opt_event.stopPropagation();
- opt_event.preventDefault();
- }
- };
- /**
- * 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
- * @param {goog.events.BrowserEvent} e The event.
- * @private
- */
- goog.editor.plugins.LinkBubble.prototype.deleteLink_ = function(e) {
- // Needed when this occurs due to an ENTER key event, else the editor receives
- // the key press and inserts a newline.
- e.preventDefault();
- this.getFieldObject().dispatchBeforeChange();
- var link = this.getTargetElement();
- var child = link.lastChild;
- goog.dom.flattenElement(link);
- var restoreScrollPosition = this.saveScrollPosition();
- var range = goog.dom.Range.createFromNodeContents(child);
- range.collapse(false);
- range.select();
- this.closeBubble();
- this.getFieldObject().dispatchChange();
- this.getFieldObject().focus();
- restoreScrollPosition();
- };
- /**
- * 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;
- };
|