563 lines
17 KiB
JavaScript
563 lines
17 KiB
JavaScript
// 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.editor.BrowserFeature');
|
|
goog.require('goog.editor.Command');
|
|
goog.require('goog.editor.Link');
|
|
goog.require('goog.editor.plugins.AbstractBubblePlugin');
|
|
goog.require('goog.editor.range');
|
|
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.base(this);
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
var MSG_LINK_BUBBLE_TEST_LINK = goog.getMsg('Go to link: ');
|
|
|
|
|
|
/**
|
|
* @desc Label that pops up a dialog to change the link.
|
|
*/
|
|
var MSG_LINK_BUBBLE_CHANGE = goog.getMsg('Change');
|
|
|
|
|
|
/**
|
|
* @desc Label that allow the user to remove this link.
|
|
*/
|
|
var MSG_LINK_BUBBLE_REMOVE = goog.getMsg('Remove');
|
|
|
|
|
|
/**
|
|
* 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();
|
|
this.closeBubble();
|
|
this.createBubble(targetEl);
|
|
};
|
|
|
|
|
|
/** @override */
|
|
goog.editor.plugins.LinkBubble.prototype.getBubbleTargetFromSelection =
|
|
function(selectedElement) {
|
|
var bubbleTarget = goog.dom.getAncestorByTagNameAndClass(selectedElement,
|
|
goog.dom.TagName.A);
|
|
|
|
if (!bubbleTarget) {
|
|
// See if the selection is touching the right side of a link, and if so,
|
|
// show a bubble for that link. The check for "touching" is very brittle,
|
|
// and currently only guarantees that it will pop up a bubble at the
|
|
// position the cursor is placed at after the link dialog is closed.
|
|
// NOTE(robbyw): This assumes this method is always called with
|
|
// selected element = range.getContainerElement(). Right now this is true,
|
|
// but attempts to re-use this method for other purposes could cause issues.
|
|
// TODO(robbyw): Refactor this method to also take a range, and use that.
|
|
var range = this.getFieldObject().getRange();
|
|
if (range && range.isCollapsed() && range.getStartOffset() == 0) {
|
|
var startNode = range.getStartNode();
|
|
var previous = startNode.previousSibling;
|
|
if (previous && previous.tagName == goog.dom.TagName.A) {
|
|
bubbleTarget = previous;
|
|
}
|
|
}
|
|
}
|
|
|
|
return /** @type {Element} */ (bubbleTarget);
|
|
};
|
|
|
|
|
|
/**
|
|
* Set the optional function for getting the "test" link of a url.
|
|
* @param {function(string) : string} func The function to use.
|
|
*/
|
|
goog.editor.plugins.LinkBubble.prototype.setTestLinkUrlFn = function(func) {
|
|
this.testLinkUrlFn_ = func;
|
|
};
|
|
|
|
|
|
/**
|
|
* Returns the target element url for the bubble.
|
|
* @return {string} The url href.
|
|
* @protected
|
|
*/
|
|
goog.editor.plugins.LinkBubble.prototype.getTargetUrl = function() {
|
|
// Get the href-attribute through getAttribute() rather than the href property
|
|
// because Google-Toolbar on Firefox with "Send with Gmail" turned on
|
|
// modifies the href-property of 'mailto:' links but leaves the attribute
|
|
// untouched.
|
|
return this.getTargetElement().getAttribute('href') || '';
|
|
};
|
|
|
|
|
|
/** @override */
|
|
goog.editor.plugins.LinkBubble.prototype.getBubbleType = function() {
|
|
return goog.dom.TagName.A;
|
|
};
|
|
|
|
|
|
/** @override */
|
|
goog.editor.plugins.LinkBubble.prototype.getBubbleTitle = function() {
|
|
return goog.ui.editor.messages.MSG_LINK_CAPTION;
|
|
};
|
|
|
|
|
|
/** @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_},
|
|
MSG_LINK_BUBBLE_TEST_LINK);
|
|
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_,
|
|
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_,
|
|
MSG_LINK_BUBBLE_REMOVE, this.deleteLink_, removeLinkSpan);
|
|
|
|
this.onShow();
|
|
|
|
var bubbleContents = this.dom_.createDom(goog.dom.TagName.DIV,
|
|
{id: goog.editor.plugins.LinkBubble.LINK_DIV_ID_},
|
|
testMsgSpan || '', linkTextSpan, changeLinkSpan);
|
|
|
|
for (i = 0; i < this.actionSpans_.length; i++) {
|
|
bubbleContents.appendChild(this.actionSpans_[i]);
|
|
}
|
|
bubbleContents.appendChild(removeLinkSpan);
|
|
|
|
goog.dom.appendChild(bubbleContainer, bubbleContents);
|
|
};
|
|
|
|
|
|
/**
|
|
* Tests the link by opening it in a new tab/window. Should be used as the
|
|
* click event handler for the test pseudo-link.
|
|
* @protected
|
|
*/
|
|
goog.editor.plugins.LinkBubble.prototype.testLink = function() {
|
|
goog.window.open(this.getTestLinkAction_(),
|
|
{
|
|
'target': '_blank',
|
|
'noreferrer': this.stopReferrerLeaks_
|
|
}, this.getFieldObject().getAppWindow());
|
|
};
|
|
|
|
|
|
/**
|
|
* Returns whether the URL should be considered invalid. This always returns
|
|
* false in the base class, and should be overridden by subclasses that wish
|
|
* to impose validity rules on URLs.
|
|
* @param {string} url The url to check.
|
|
* @return {boolean} Whether the URL should be considered invalid.
|
|
*/
|
|
goog.editor.plugins.LinkBubble.prototype.isInvalidUrl = goog.functions.FALSE;
|
|
|
|
|
|
/**
|
|
* Gets the text to display for a link, based on the type of link
|
|
* @return {Object} Returns an object of the form:
|
|
* {linkText: displayTextForLinkTarget, valid: ifTheLinkIsValid}.
|
|
* @private
|
|
*/
|
|
goog.editor.plugins.LinkBubble.prototype.getLinkToTextObj_ = function() {
|
|
var isError;
|
|
var targetUrl = this.getTargetUrl();
|
|
|
|
if (this.isInvalidUrl(targetUrl)) {
|
|
/**
|
|
* @desc Message shown in a link bubble when the link is not a valid url.
|
|
*/
|
|
var MSG_INVALID_URL_LINK_BUBBLE = goog.getMsg('invalid url');
|
|
targetUrl = MSG_INVALID_URL_LINK_BUBBLE;
|
|
isError = true;
|
|
} else if (goog.editor.Link.isMailto(targetUrl)) {
|
|
targetUrl = targetUrl.substring(7); // 7 == "mailto:".length
|
|
}
|
|
|
|
return {linkText: targetUrl, valid: !isError};
|
|
};
|
|
|
|
|
|
/**
|
|
* Shows the link dialog.
|
|
* @param {goog.events.BrowserEvent} e The event.
|
|
* @private
|
|
*/
|
|
goog.editor.plugins.LinkBubble.prototype.showLinkDialog_ = function(e) {
|
|
// Needed when this occurs due to an ENTER key event, else the newly created
|
|
// dialog manages to have its OK button pressed, causing it to disappear.
|
|
e.preventDefault();
|
|
|
|
this.getFieldObject().execCommand(goog.editor.Command.MODAL_LINK_EDITOR,
|
|
new goog.editor.Link(
|
|
/** @type {HTMLAnchorElement} */ (this.getTargetElement()),
|
|
false));
|
|
this.closeBubble();
|
|
};
|
|
|
|
|
|
/**
|
|
* Deletes the link associated with the bubble
|
|
* @private
|
|
*/
|
|
goog.editor.plugins.LinkBubble.prototype.deleteLink_ = function() {
|
|
this.getFieldObject().dispatchBeforeChange();
|
|
|
|
var link = this.getTargetElement();
|
|
var child = link.lastChild;
|
|
goog.dom.flattenElement(link);
|
|
goog.editor.range.placeCursorNextTo(child, false);
|
|
|
|
this.closeBubble();
|
|
|
|
this.getFieldObject().dispatchChange();
|
|
this.getFieldObject().focus();
|
|
};
|
|
|
|
|
|
/**
|
|
* Sets the proper state for the action links.
|
|
* @protected
|
|
* @override
|
|
*/
|
|
goog.editor.plugins.LinkBubble.prototype.onShow = function() {
|
|
var linkDiv = this.dom_.getElement(
|
|
goog.editor.plugins.LinkBubble.LINK_DIV_ID_);
|
|
if (linkDiv) {
|
|
var testLinkSpan = this.dom_.getElement(
|
|
goog.editor.plugins.LinkBubble.TEST_LINK_SPAN_ID_);
|
|
if (testLinkSpan) {
|
|
var url = this.getTargetUrl();
|
|
goog.style.setElementShown(testLinkSpan, !goog.editor.Link.isMailto(url));
|
|
}
|
|
|
|
for (var i = 0; i < this.extraActions_.length; i++) {
|
|
var action = this.extraActions_[i];
|
|
var actionSpan = this.dom_.getElement(action.spanId_);
|
|
if (actionSpan) {
|
|
goog.style.setElementShown(actionSpan, action.toShowFn_(
|
|
this.getTargetUrl()));
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Gets the url for the bubble test link. The test link is the link in the
|
|
* bubble the user can click on to make sure the link they entered is correct.
|
|
* @return {string} The url for the bubble link href.
|
|
* @private
|
|
*/
|
|
goog.editor.plugins.LinkBubble.prototype.getTestLinkAction_ = function() {
|
|
var targetUrl = this.getTargetUrl();
|
|
return this.testLinkUrlFn_ ? this.testLinkUrlFn_(targetUrl) : targetUrl;
|
|
};
|
|
|
|
|
|
/**
|
|
* Checks whether the plugin should open the given url in a new window.
|
|
* @param {string} url The url to check.
|
|
* @return {boolean} If the plugin should open the given url in a new window.
|
|
* @protected
|
|
*/
|
|
goog.editor.plugins.LinkBubble.prototype.shouldOpenUrl = function(url) {
|
|
return !this.blockOpeningUnsafeSchemes_ || this.isSafeSchemeToOpen_(url);
|
|
};
|
|
|
|
|
|
/**
|
|
* Determines whether or not a url has a scheme which is safe to open.
|
|
* Schemes like javascript are unsafe due to the possibility of XSS.
|
|
* @param {string} url A url.
|
|
* @return {boolean} Whether the url has a safe scheme.
|
|
* @private
|
|
*/
|
|
goog.editor.plugins.LinkBubble.prototype.isSafeSchemeToOpen_ =
|
|
function(url) {
|
|
var scheme = goog.uri.utils.getScheme(url) || 'http';
|
|
return goog.array.contains(this.safeToOpenSchemes_, scheme.toLowerCase());
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
* Constructor for extra actions that can be added to the link bubble.
|
|
* @param {string} spanId The ID for the span showing the action.
|
|
* @param {string} linkId The ID for the link showing the action.
|
|
* @param {string} message The text for the link showing the action.
|
|
* @param {function(string):boolean} toShowFn Test function to determine whether
|
|
* to show the action for the given URL.
|
|
* @param {function(string):void} actionFn Action function to run when the
|
|
* action is clicked. Takes the current target URL as a parameter.
|
|
* @constructor
|
|
*/
|
|
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;
|
|
};
|