Path: blob/trunk/third_party/closure/goog/editor/plugins/linkbubble.js
2868 views
// Copyright 2008 The Closure Library Authors. All Rights Reserved.1//2// Licensed under the Apache License, Version 2.0 (the "License");3// you may not use this file except in compliance with the License.4// You may obtain a copy of the License at5//6// http://www.apache.org/licenses/LICENSE-2.07//8// Unless required by applicable law or agreed to in writing, software9// distributed under the License is distributed on an "AS-IS" BASIS,10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.11// See the License for the specific language governing permissions and12// limitations under the License.1314/**15* @fileoverview Base class for bubble plugins.16*17*/1819goog.provide('goog.editor.plugins.LinkBubble');20goog.provide('goog.editor.plugins.LinkBubble.Action');2122goog.require('goog.array');23goog.require('goog.dom');24goog.require('goog.dom.Range');25goog.require('goog.dom.TagName');26goog.require('goog.editor.Command');27goog.require('goog.editor.Link');28goog.require('goog.editor.plugins.AbstractBubblePlugin');29goog.require('goog.functions');30goog.require('goog.string');31goog.require('goog.style');32goog.require('goog.ui.editor.messages');33goog.require('goog.uri.utils');34goog.require('goog.window');35363738/**39* Property bubble plugin for links.40* @param {...!goog.editor.plugins.LinkBubble.Action} var_args List of41* extra actions supported by the bubble.42* @constructor43* @extends {goog.editor.plugins.AbstractBubblePlugin}44*/45goog.editor.plugins.LinkBubble = function(var_args) {46goog.editor.plugins.LinkBubble.base(this, 'constructor');4748/**49* List of extra actions supported by the bubble.50* @type {Array<!goog.editor.plugins.LinkBubble.Action>}51* @private52*/53this.extraActions_ = goog.array.toArray(arguments);5455/**56* List of spans corresponding to the extra actions.57* @type {Array<!Element>}58* @private59*/60this.actionSpans_ = [];6162/**63* A list of whitelisted URL schemes which are safe to open.64* @type {Array<string>}65* @private66*/67this.safeToOpenSchemes_ = ['http', 'https', 'ftp'];68};69goog.inherits(70goog.editor.plugins.LinkBubble, goog.editor.plugins.AbstractBubblePlugin);717273/**74* Element id for the link text.75* type {string}76* @private77*/78goog.editor.plugins.LinkBubble.LINK_TEXT_ID_ = 'tr_link-text';798081/**82* Element id for the test link span.83* type {string}84* @private85*/86goog.editor.plugins.LinkBubble.TEST_LINK_SPAN_ID_ = 'tr_test-link-span';878889/**90* Element id for the test link.91* type {string}92* @private93*/94goog.editor.plugins.LinkBubble.TEST_LINK_ID_ = 'tr_test-link';959697/**98* Element id for the change link span.99* type {string}100* @private101*/102goog.editor.plugins.LinkBubble.CHANGE_LINK_SPAN_ID_ = 'tr_change-link-span';103104105/**106* Element id for the link.107* type {string}108* @private109*/110goog.editor.plugins.LinkBubble.CHANGE_LINK_ID_ = 'tr_change-link';111112113/**114* Element id for the delete link span.115* type {string}116* @private117*/118goog.editor.plugins.LinkBubble.DELETE_LINK_SPAN_ID_ = 'tr_delete-link-span';119120121/**122* Element id for the delete link.123* type {string}124* @private125*/126goog.editor.plugins.LinkBubble.DELETE_LINK_ID_ = 'tr_delete-link';127128129/**130* Element id for the link bubble wrapper div.131* type {string}132* @private133*/134goog.editor.plugins.LinkBubble.LINK_DIV_ID_ = 'tr_link-div';135136137/**138* @desc Text label for link that lets the user click it to see where the link139* this bubble is for point to.140*/141goog.editor.plugins.LinkBubble.MSG_LINK_BUBBLE_TEST_LINK =142goog.getMsg('Go to link: ');143144145/**146* @desc Label that pops up a dialog to change the link.147*/148goog.editor.plugins.LinkBubble.MSG_LINK_BUBBLE_CHANGE = goog.getMsg('Change');149150151/**152* @desc Label that allow the user to remove this link.153*/154goog.editor.plugins.LinkBubble.MSG_LINK_BUBBLE_REMOVE = goog.getMsg('Remove');155156157/**158* @desc Message shown in a link bubble when the link is not a valid url.159*/160goog.editor.plugins.LinkBubble.MSG_INVALID_URL_LINK_BUBBLE =161goog.getMsg('invalid url');162163164/**165* Whether to stop leaking the page's url via the referrer header when the166* link text link is clicked.167* @type {boolean}168* @private169*/170goog.editor.plugins.LinkBubble.prototype.stopReferrerLeaks_ = false;171172173/**174* Whether to block opening links with a non-whitelisted URL scheme.175* @type {boolean}176* @private177*/178goog.editor.plugins.LinkBubble.prototype.blockOpeningUnsafeSchemes_ = true;179180181/**182* Tells the plugin to stop leaking the page's url via the referrer header when183* the link text link is clicked. When the user clicks on a link, the184* browser makes a request for the link url, passing the url of the current page185* in the request headers. If the user wants the current url to be kept secret186* (e.g. an unpublished document), the owner of the url that was clicked will187* see the secret url in the request headers, and it will no longer be a secret.188* Calling this method will not send a referrer header in the request, just as189* if the user had opened a blank window and typed the url in themselves.190*/191goog.editor.plugins.LinkBubble.prototype.stopReferrerLeaks = function() {192// TODO(user): Right now only 2 plugins have this API to stop193// referrer leaks. If more plugins need to do this, come up with a way to194// enable the functionality in all plugins at once. Same thing for195// setBlockOpeningUnsafeSchemes and associated functionality.196this.stopReferrerLeaks_ = true;197};198199200/**201* Tells the plugin whether to block URLs with schemes not in the whitelist.202* If blocking is enabled, this plugin will not linkify the link in the bubble203* popup.204* @param {boolean} blockOpeningUnsafeSchemes Whether to block non-whitelisted205* schemes.206*/207goog.editor.plugins.LinkBubble.prototype.setBlockOpeningUnsafeSchemes =208function(blockOpeningUnsafeSchemes) {209this.blockOpeningUnsafeSchemes_ = blockOpeningUnsafeSchemes;210};211212213/**214* Sets a whitelist of allowed URL schemes that are safe to open.215* Schemes should all be in lowercase. If the plugin is set to block opening216* unsafe schemes, user-entered URLs will be converted to lowercase and checked217* against this list. The whitelist has no effect if blocking is not enabled.218* @param {Array<string>} schemes String array of URL schemes to allow (http,219* https, etc.).220*/221goog.editor.plugins.LinkBubble.prototype.setSafeToOpenSchemes = function(222schemes) {223this.safeToOpenSchemes_ = schemes;224};225226227/** @override */228goog.editor.plugins.LinkBubble.prototype.getTrogClassId = function() {229return 'LinkBubble';230};231232233/** @override */234goog.editor.plugins.LinkBubble.prototype.isSupportedCommand = function(235command) {236return command == goog.editor.Command.UPDATE_LINK_BUBBLE;237};238239240/** @override */241goog.editor.plugins.LinkBubble.prototype.execCommandInternal = function(242command, var_args) {243if (command == goog.editor.Command.UPDATE_LINK_BUBBLE) {244this.updateLink_();245}246};247248249/**250* Updates the href in the link bubble with a new link.251* @private252*/253goog.editor.plugins.LinkBubble.prototype.updateLink_ = function() {254var targetEl = this.getTargetElement();255if (targetEl) {256this.closeBubble();257this.createBubble(targetEl);258}259};260261262/** @override */263goog.editor.plugins.LinkBubble.prototype.getBubbleTargetFromSelection =264function(selectedElement) {265var bubbleTarget = goog.dom.getAncestorByTagNameAndClass(266selectedElement, goog.dom.TagName.A);267268if (!bubbleTarget) {269// See if the selection is touching the right side of a link, and if so,270// show a bubble for that link. The check for "touching" is very brittle,271// and currently only guarantees that it will pop up a bubble at the272// position the cursor is placed at after the link dialog is closed.273// NOTE(robbyw): This assumes this method is always called with274// selected element = range.getContainerElement(). Right now this is true,275// but attempts to re-use this method for other purposes could cause issues.276// TODO(robbyw): Refactor this method to also take a range, and use that.277var range = this.getFieldObject().getRange();278if (range && range.isCollapsed() && range.getStartOffset() == 0) {279var startNode = range.getStartNode();280var previous = startNode.previousSibling;281if (previous && previous.tagName == goog.dom.TagName.A) {282bubbleTarget = previous;283}284}285}286287return /** @type {Element} */ (bubbleTarget);288};289290291/**292* Set the optional function for getting the "test" link of a url.293* @param {function(string) : string} func The function to use.294*/295goog.editor.plugins.LinkBubble.prototype.setTestLinkUrlFn = function(func) {296this.testLinkUrlFn_ = func;297};298299300/**301* Returns the target element url for the bubble.302* @return {string} The url href.303* @protected304*/305goog.editor.plugins.LinkBubble.prototype.getTargetUrl = function() {306// Get the href-attribute through getAttribute() rather than the href property307// because Google-Toolbar on Firefox with "Send with Gmail" turned on308// modifies the href-property of 'mailto:' links but leaves the attribute309// untouched.310return this.getTargetElement().getAttribute('href') || '';311};312313314/** @override */315goog.editor.plugins.LinkBubble.prototype.getBubbleType = function() {316return String(goog.dom.TagName.A);317};318319320/** @override */321goog.editor.plugins.LinkBubble.prototype.getBubbleTitle = function() {322return goog.ui.editor.messages.MSG_LINK_CAPTION;323};324325326/**327* Returns the message to display for testing a link.328* @return {string} The message for testing a link.329* @protected330*/331goog.editor.plugins.LinkBubble.prototype.getTestLinkMessage = function() {332return goog.editor.plugins.LinkBubble.MSG_LINK_BUBBLE_TEST_LINK;333};334335336/** @override */337goog.editor.plugins.LinkBubble.prototype.createBubbleContents = function(338bubbleContainer) {339var linkObj = this.getLinkToTextObj_();340341// Create linkTextSpan, show plain text for e-mail address or truncate the342// text to <= 48 characters so that property bubbles don't grow too wide and343// create a link if URL. Only linkify valid links.344// TODO(robbyw): Repalce this color with a CSS class.345var color = linkObj.valid ? 'black' : 'red';346var shouldOpenUrl = this.shouldOpenUrl(linkObj.linkText);347var linkTextSpan;348if (goog.editor.Link.isLikelyEmailAddress(linkObj.linkText) ||349!linkObj.valid || !shouldOpenUrl) {350linkTextSpan = this.dom_.createDom(351goog.dom.TagName.SPAN, {352id: goog.editor.plugins.LinkBubble.LINK_TEXT_ID_,353style: 'color:' + color354},355this.dom_.createTextNode(linkObj.linkText));356} else {357var testMsgSpan = this.dom_.createDom(358goog.dom.TagName.SPAN,359{id: goog.editor.plugins.LinkBubble.TEST_LINK_SPAN_ID_},360this.getTestLinkMessage());361linkTextSpan = this.dom_.createDom(362goog.dom.TagName.SPAN, {363id: goog.editor.plugins.LinkBubble.LINK_TEXT_ID_,364style: 'color:' + color365},366'');367var linkText = goog.string.truncateMiddle(linkObj.linkText, 48);368// Actually creates a pseudo-link that can't be right-clicked to open in a369// new tab, because that would avoid the logic to stop referrer leaks.370this.createLink(371goog.editor.plugins.LinkBubble.TEST_LINK_ID_,372this.dom_.createTextNode(linkText).data, this.testLink, linkTextSpan);373}374375var changeLinkSpan = this.createLinkOption(376goog.editor.plugins.LinkBubble.CHANGE_LINK_SPAN_ID_);377this.createLink(378goog.editor.plugins.LinkBubble.CHANGE_LINK_ID_,379goog.editor.plugins.LinkBubble.MSG_LINK_BUBBLE_CHANGE,380this.showLinkDialog_, changeLinkSpan);381382// This function is called multiple times - we have to reset the array.383this.actionSpans_ = [];384for (var i = 0; i < this.extraActions_.length; i++) {385var action = this.extraActions_[i];386var actionSpan = this.createLinkOption(action.spanId_);387this.actionSpans_.push(actionSpan);388this.createLink(action.linkId_, action.message_, function() {389action.actionFn_(this.getTargetUrl());390}, actionSpan);391}392393var removeLinkSpan = this.createLinkOption(394goog.editor.plugins.LinkBubble.DELETE_LINK_SPAN_ID_);395this.createLink(396goog.editor.plugins.LinkBubble.DELETE_LINK_ID_,397goog.editor.plugins.LinkBubble.MSG_LINK_BUBBLE_REMOVE, this.deleteLink_,398removeLinkSpan);399400this.onShow();401402var bubbleContents = this.dom_.createDom(403goog.dom.TagName.DIV, {id: goog.editor.plugins.LinkBubble.LINK_DIV_ID_},404testMsgSpan || '', linkTextSpan, changeLinkSpan);405406for (i = 0; i < this.actionSpans_.length; i++) {407bubbleContents.appendChild(this.actionSpans_[i]);408}409bubbleContents.appendChild(removeLinkSpan);410411goog.dom.appendChild(bubbleContainer, bubbleContents);412};413414415/**416* Tests the link by opening it in a new tab/window. Should be used as the417* click event handler for the test pseudo-link.418* @param {!Event=} opt_event If passed in, the event will be stopped.419* @protected420*/421goog.editor.plugins.LinkBubble.prototype.testLink = function(opt_event) {422goog.window.open(423this.getTestLinkAction_(),424{'target': '_blank', 'noreferrer': this.stopReferrerLeaks_},425this.getFieldObject().getAppWindow());426if (opt_event) {427opt_event.stopPropagation();428opt_event.preventDefault();429}430};431432433/**434* Returns whether the URL should be considered invalid. This always returns435* false in the base class, and should be overridden by subclasses that wish436* to impose validity rules on URLs.437* @param {string} url The url to check.438* @return {boolean} Whether the URL should be considered invalid.439*/440goog.editor.plugins.LinkBubble.prototype.isInvalidUrl = goog.functions.FALSE;441442443/**444* Gets the text to display for a link, based on the type of link445* @return {!Object} Returns an object of the form:446* {linkText: displayTextForLinkTarget, valid: ifTheLinkIsValid}.447* @private448*/449goog.editor.plugins.LinkBubble.prototype.getLinkToTextObj_ = function() {450var isError;451var targetUrl = this.getTargetUrl();452453if (this.isInvalidUrl(targetUrl)) {454targetUrl = goog.editor.plugins.LinkBubble.MSG_INVALID_URL_LINK_BUBBLE;455isError = true;456} else if (goog.editor.Link.isMailto(targetUrl)) {457targetUrl = targetUrl.substring(7); // 7 == "mailto:".length458}459460return {linkText: targetUrl, valid: !isError};461};462463464/**465* Shows the link dialog.466* @param {goog.events.BrowserEvent} e The event.467* @private468*/469goog.editor.plugins.LinkBubble.prototype.showLinkDialog_ = function(e) {470// Needed when this occurs due to an ENTER key event, else the newly created471// dialog manages to have its OK button pressed, causing it to disappear.472e.preventDefault();473474this.getFieldObject().execCommand(475goog.editor.Command.MODAL_LINK_EDITOR,476new goog.editor.Link(477/** @type {HTMLAnchorElement} */ (this.getTargetElement()), false));478this.closeBubble();479};480481482/**483* Deletes the link associated with the bubble484* @param {goog.events.BrowserEvent} e The event.485* @private486*/487goog.editor.plugins.LinkBubble.prototype.deleteLink_ = function(e) {488// Needed when this occurs due to an ENTER key event, else the editor receives489// the key press and inserts a newline.490e.preventDefault();491492this.getFieldObject().dispatchBeforeChange();493494var link = this.getTargetElement();495var child = link.lastChild;496goog.dom.flattenElement(link);497498var restoreScrollPosition = this.saveScrollPosition();499var range = goog.dom.Range.createFromNodeContents(child);500range.collapse(false);501range.select();502503this.closeBubble();504505this.getFieldObject().dispatchChange();506this.getFieldObject().focus();507restoreScrollPosition();508};509510511/**512* Sets the proper state for the action links.513* @protected514* @override515*/516goog.editor.plugins.LinkBubble.prototype.onShow = function() {517var linkDiv =518this.dom_.getElement(goog.editor.plugins.LinkBubble.LINK_DIV_ID_);519if (linkDiv) {520var testLinkSpan =521this.dom_.getElement(goog.editor.plugins.LinkBubble.TEST_LINK_SPAN_ID_);522if (testLinkSpan) {523var url = this.getTargetUrl();524goog.style.setElementShown(testLinkSpan, !goog.editor.Link.isMailto(url));525}526527for (var i = 0; i < this.extraActions_.length; i++) {528var action = this.extraActions_[i];529var actionSpan = this.dom_.getElement(action.spanId_);530if (actionSpan) {531goog.style.setElementShown(532actionSpan, action.toShowFn_(this.getTargetUrl()));533}534}535}536};537538539/**540* Gets the url for the bubble test link. The test link is the link in the541* bubble the user can click on to make sure the link they entered is correct.542* @return {string} The url for the bubble link href.543* @private544*/545goog.editor.plugins.LinkBubble.prototype.getTestLinkAction_ = function() {546var targetUrl = this.getTargetUrl();547return this.testLinkUrlFn_ ? this.testLinkUrlFn_(targetUrl) : targetUrl;548};549550551/**552* Checks whether the plugin should open the given url in a new window.553* @param {string} url The url to check.554* @return {boolean} If the plugin should open the given url in a new window.555* @protected556*/557goog.editor.plugins.LinkBubble.prototype.shouldOpenUrl = function(url) {558return !this.blockOpeningUnsafeSchemes_ || this.isSafeSchemeToOpen_(url);559};560561562/**563* Determines whether or not a url has a scheme which is safe to open.564* Schemes like javascript are unsafe due to the possibility of XSS.565* @param {string} url A url.566* @return {boolean} Whether the url has a safe scheme.567* @private568*/569goog.editor.plugins.LinkBubble.prototype.isSafeSchemeToOpen_ = function(url) {570var scheme = goog.uri.utils.getScheme(url) || 'http';571return goog.array.contains(this.safeToOpenSchemes_, scheme.toLowerCase());572};573574575576/**577* Constructor for extra actions that can be added to the link bubble.578* @param {string} spanId The ID for the span showing the action.579* @param {string} linkId The ID for the link showing the action.580* @param {string} message The text for the link showing the action.581* @param {function(string):boolean} toShowFn Test function to determine whether582* to show the action for the given URL.583* @param {function(string):void} actionFn Action function to run when the584* action is clicked. Takes the current target URL as a parameter.585* @constructor586* @final587*/588goog.editor.plugins.LinkBubble.Action = function(589spanId, linkId, message, toShowFn, actionFn) {590this.spanId_ = spanId;591this.linkId_ = linkId;592this.message_ = message;593this.toShowFn_ = toShowFn;594this.actionFn_ = actionFn;595};596597598