Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
seleniumhq
GitHub Repository: seleniumhq/selenium
Path: blob/trunk/third_party/closure/goog/editor/plugins/linkbubble.js
2868 views
1
// Copyright 2008 The Closure Library Authors. All Rights Reserved.
2
//
3
// Licensed under the Apache License, Version 2.0 (the "License");
4
// you may not use this file except in compliance with the License.
5
// You may obtain a copy of the License at
6
//
7
// http://www.apache.org/licenses/LICENSE-2.0
8
//
9
// Unless required by applicable law or agreed to in writing, software
10
// distributed under the License is distributed on an "AS-IS" BASIS,
11
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
// See the License for the specific language governing permissions and
13
// limitations under the License.
14
15
/**
16
* @fileoverview Base class for bubble plugins.
17
*
18
*/
19
20
goog.provide('goog.editor.plugins.LinkBubble');
21
goog.provide('goog.editor.plugins.LinkBubble.Action');
22
23
goog.require('goog.array');
24
goog.require('goog.dom');
25
goog.require('goog.dom.Range');
26
goog.require('goog.dom.TagName');
27
goog.require('goog.editor.Command');
28
goog.require('goog.editor.Link');
29
goog.require('goog.editor.plugins.AbstractBubblePlugin');
30
goog.require('goog.functions');
31
goog.require('goog.string');
32
goog.require('goog.style');
33
goog.require('goog.ui.editor.messages');
34
goog.require('goog.uri.utils');
35
goog.require('goog.window');
36
37
38
39
/**
40
* Property bubble plugin for links.
41
* @param {...!goog.editor.plugins.LinkBubble.Action} var_args List of
42
* extra actions supported by the bubble.
43
* @constructor
44
* @extends {goog.editor.plugins.AbstractBubblePlugin}
45
*/
46
goog.editor.plugins.LinkBubble = function(var_args) {
47
goog.editor.plugins.LinkBubble.base(this, 'constructor');
48
49
/**
50
* List of extra actions supported by the bubble.
51
* @type {Array<!goog.editor.plugins.LinkBubble.Action>}
52
* @private
53
*/
54
this.extraActions_ = goog.array.toArray(arguments);
55
56
/**
57
* List of spans corresponding to the extra actions.
58
* @type {Array<!Element>}
59
* @private
60
*/
61
this.actionSpans_ = [];
62
63
/**
64
* A list of whitelisted URL schemes which are safe to open.
65
* @type {Array<string>}
66
* @private
67
*/
68
this.safeToOpenSchemes_ = ['http', 'https', 'ftp'];
69
};
70
goog.inherits(
71
goog.editor.plugins.LinkBubble, goog.editor.plugins.AbstractBubblePlugin);
72
73
74
/**
75
* Element id for the link text.
76
* type {string}
77
* @private
78
*/
79
goog.editor.plugins.LinkBubble.LINK_TEXT_ID_ = 'tr_link-text';
80
81
82
/**
83
* Element id for the test link span.
84
* type {string}
85
* @private
86
*/
87
goog.editor.plugins.LinkBubble.TEST_LINK_SPAN_ID_ = 'tr_test-link-span';
88
89
90
/**
91
* Element id for the test link.
92
* type {string}
93
* @private
94
*/
95
goog.editor.plugins.LinkBubble.TEST_LINK_ID_ = 'tr_test-link';
96
97
98
/**
99
* Element id for the change link span.
100
* type {string}
101
* @private
102
*/
103
goog.editor.plugins.LinkBubble.CHANGE_LINK_SPAN_ID_ = 'tr_change-link-span';
104
105
106
/**
107
* Element id for the link.
108
* type {string}
109
* @private
110
*/
111
goog.editor.plugins.LinkBubble.CHANGE_LINK_ID_ = 'tr_change-link';
112
113
114
/**
115
* Element id for the delete link span.
116
* type {string}
117
* @private
118
*/
119
goog.editor.plugins.LinkBubble.DELETE_LINK_SPAN_ID_ = 'tr_delete-link-span';
120
121
122
/**
123
* Element id for the delete link.
124
* type {string}
125
* @private
126
*/
127
goog.editor.plugins.LinkBubble.DELETE_LINK_ID_ = 'tr_delete-link';
128
129
130
/**
131
* Element id for the link bubble wrapper div.
132
* type {string}
133
* @private
134
*/
135
goog.editor.plugins.LinkBubble.LINK_DIV_ID_ = 'tr_link-div';
136
137
138
/**
139
* @desc Text label for link that lets the user click it to see where the link
140
* this bubble is for point to.
141
*/
142
goog.editor.plugins.LinkBubble.MSG_LINK_BUBBLE_TEST_LINK =
143
goog.getMsg('Go to link: ');
144
145
146
/**
147
* @desc Label that pops up a dialog to change the link.
148
*/
149
goog.editor.plugins.LinkBubble.MSG_LINK_BUBBLE_CHANGE = goog.getMsg('Change');
150
151
152
/**
153
* @desc Label that allow the user to remove this link.
154
*/
155
goog.editor.plugins.LinkBubble.MSG_LINK_BUBBLE_REMOVE = goog.getMsg('Remove');
156
157
158
/**
159
* @desc Message shown in a link bubble when the link is not a valid url.
160
*/
161
goog.editor.plugins.LinkBubble.MSG_INVALID_URL_LINK_BUBBLE =
162
goog.getMsg('invalid url');
163
164
165
/**
166
* Whether to stop leaking the page's url via the referrer header when the
167
* link text link is clicked.
168
* @type {boolean}
169
* @private
170
*/
171
goog.editor.plugins.LinkBubble.prototype.stopReferrerLeaks_ = false;
172
173
174
/**
175
* Whether to block opening links with a non-whitelisted URL scheme.
176
* @type {boolean}
177
* @private
178
*/
179
goog.editor.plugins.LinkBubble.prototype.blockOpeningUnsafeSchemes_ = true;
180
181
182
/**
183
* Tells the plugin to stop leaking the page's url via the referrer header when
184
* the link text link is clicked. When the user clicks on a link, the
185
* browser makes a request for the link url, passing the url of the current page
186
* in the request headers. If the user wants the current url to be kept secret
187
* (e.g. an unpublished document), the owner of the url that was clicked will
188
* see the secret url in the request headers, and it will no longer be a secret.
189
* Calling this method will not send a referrer header in the request, just as
190
* if the user had opened a blank window and typed the url in themselves.
191
*/
192
goog.editor.plugins.LinkBubble.prototype.stopReferrerLeaks = function() {
193
// TODO(user): Right now only 2 plugins have this API to stop
194
// referrer leaks. If more plugins need to do this, come up with a way to
195
// enable the functionality in all plugins at once. Same thing for
196
// setBlockOpeningUnsafeSchemes and associated functionality.
197
this.stopReferrerLeaks_ = true;
198
};
199
200
201
/**
202
* Tells the plugin whether to block URLs with schemes not in the whitelist.
203
* If blocking is enabled, this plugin will not linkify the link in the bubble
204
* popup.
205
* @param {boolean} blockOpeningUnsafeSchemes Whether to block non-whitelisted
206
* schemes.
207
*/
208
goog.editor.plugins.LinkBubble.prototype.setBlockOpeningUnsafeSchemes =
209
function(blockOpeningUnsafeSchemes) {
210
this.blockOpeningUnsafeSchemes_ = blockOpeningUnsafeSchemes;
211
};
212
213
214
/**
215
* Sets a whitelist of allowed URL schemes that are safe to open.
216
* Schemes should all be in lowercase. If the plugin is set to block opening
217
* unsafe schemes, user-entered URLs will be converted to lowercase and checked
218
* against this list. The whitelist has no effect if blocking is not enabled.
219
* @param {Array<string>} schemes String array of URL schemes to allow (http,
220
* https, etc.).
221
*/
222
goog.editor.plugins.LinkBubble.prototype.setSafeToOpenSchemes = function(
223
schemes) {
224
this.safeToOpenSchemes_ = schemes;
225
};
226
227
228
/** @override */
229
goog.editor.plugins.LinkBubble.prototype.getTrogClassId = function() {
230
return 'LinkBubble';
231
};
232
233
234
/** @override */
235
goog.editor.plugins.LinkBubble.prototype.isSupportedCommand = function(
236
command) {
237
return command == goog.editor.Command.UPDATE_LINK_BUBBLE;
238
};
239
240
241
/** @override */
242
goog.editor.plugins.LinkBubble.prototype.execCommandInternal = function(
243
command, var_args) {
244
if (command == goog.editor.Command.UPDATE_LINK_BUBBLE) {
245
this.updateLink_();
246
}
247
};
248
249
250
/**
251
* Updates the href in the link bubble with a new link.
252
* @private
253
*/
254
goog.editor.plugins.LinkBubble.prototype.updateLink_ = function() {
255
var targetEl = this.getTargetElement();
256
if (targetEl) {
257
this.closeBubble();
258
this.createBubble(targetEl);
259
}
260
};
261
262
263
/** @override */
264
goog.editor.plugins.LinkBubble.prototype.getBubbleTargetFromSelection =
265
function(selectedElement) {
266
var bubbleTarget = goog.dom.getAncestorByTagNameAndClass(
267
selectedElement, goog.dom.TagName.A);
268
269
if (!bubbleTarget) {
270
// See if the selection is touching the right side of a link, and if so,
271
// show a bubble for that link. The check for "touching" is very brittle,
272
// and currently only guarantees that it will pop up a bubble at the
273
// position the cursor is placed at after the link dialog is closed.
274
// NOTE(robbyw): This assumes this method is always called with
275
// selected element = range.getContainerElement(). Right now this is true,
276
// but attempts to re-use this method for other purposes could cause issues.
277
// TODO(robbyw): Refactor this method to also take a range, and use that.
278
var range = this.getFieldObject().getRange();
279
if (range && range.isCollapsed() && range.getStartOffset() == 0) {
280
var startNode = range.getStartNode();
281
var previous = startNode.previousSibling;
282
if (previous && previous.tagName == goog.dom.TagName.A) {
283
bubbleTarget = previous;
284
}
285
}
286
}
287
288
return /** @type {Element} */ (bubbleTarget);
289
};
290
291
292
/**
293
* Set the optional function for getting the "test" link of a url.
294
* @param {function(string) : string} func The function to use.
295
*/
296
goog.editor.plugins.LinkBubble.prototype.setTestLinkUrlFn = function(func) {
297
this.testLinkUrlFn_ = func;
298
};
299
300
301
/**
302
* Returns the target element url for the bubble.
303
* @return {string} The url href.
304
* @protected
305
*/
306
goog.editor.plugins.LinkBubble.prototype.getTargetUrl = function() {
307
// Get the href-attribute through getAttribute() rather than the href property
308
// because Google-Toolbar on Firefox with "Send with Gmail" turned on
309
// modifies the href-property of 'mailto:' links but leaves the attribute
310
// untouched.
311
return this.getTargetElement().getAttribute('href') || '';
312
};
313
314
315
/** @override */
316
goog.editor.plugins.LinkBubble.prototype.getBubbleType = function() {
317
return String(goog.dom.TagName.A);
318
};
319
320
321
/** @override */
322
goog.editor.plugins.LinkBubble.prototype.getBubbleTitle = function() {
323
return goog.ui.editor.messages.MSG_LINK_CAPTION;
324
};
325
326
327
/**
328
* Returns the message to display for testing a link.
329
* @return {string} The message for testing a link.
330
* @protected
331
*/
332
goog.editor.plugins.LinkBubble.prototype.getTestLinkMessage = function() {
333
return goog.editor.plugins.LinkBubble.MSG_LINK_BUBBLE_TEST_LINK;
334
};
335
336
337
/** @override */
338
goog.editor.plugins.LinkBubble.prototype.createBubbleContents = function(
339
bubbleContainer) {
340
var linkObj = this.getLinkToTextObj_();
341
342
// Create linkTextSpan, show plain text for e-mail address or truncate the
343
// text to <= 48 characters so that property bubbles don't grow too wide and
344
// create a link if URL. Only linkify valid links.
345
// TODO(robbyw): Repalce this color with a CSS class.
346
var color = linkObj.valid ? 'black' : 'red';
347
var shouldOpenUrl = this.shouldOpenUrl(linkObj.linkText);
348
var linkTextSpan;
349
if (goog.editor.Link.isLikelyEmailAddress(linkObj.linkText) ||
350
!linkObj.valid || !shouldOpenUrl) {
351
linkTextSpan = this.dom_.createDom(
352
goog.dom.TagName.SPAN, {
353
id: goog.editor.plugins.LinkBubble.LINK_TEXT_ID_,
354
style: 'color:' + color
355
},
356
this.dom_.createTextNode(linkObj.linkText));
357
} else {
358
var testMsgSpan = this.dom_.createDom(
359
goog.dom.TagName.SPAN,
360
{id: goog.editor.plugins.LinkBubble.TEST_LINK_SPAN_ID_},
361
this.getTestLinkMessage());
362
linkTextSpan = this.dom_.createDom(
363
goog.dom.TagName.SPAN, {
364
id: goog.editor.plugins.LinkBubble.LINK_TEXT_ID_,
365
style: 'color:' + color
366
},
367
'');
368
var linkText = goog.string.truncateMiddle(linkObj.linkText, 48);
369
// Actually creates a pseudo-link that can't be right-clicked to open in a
370
// new tab, because that would avoid the logic to stop referrer leaks.
371
this.createLink(
372
goog.editor.plugins.LinkBubble.TEST_LINK_ID_,
373
this.dom_.createTextNode(linkText).data, this.testLink, linkTextSpan);
374
}
375
376
var changeLinkSpan = this.createLinkOption(
377
goog.editor.plugins.LinkBubble.CHANGE_LINK_SPAN_ID_);
378
this.createLink(
379
goog.editor.plugins.LinkBubble.CHANGE_LINK_ID_,
380
goog.editor.plugins.LinkBubble.MSG_LINK_BUBBLE_CHANGE,
381
this.showLinkDialog_, changeLinkSpan);
382
383
// This function is called multiple times - we have to reset the array.
384
this.actionSpans_ = [];
385
for (var i = 0; i < this.extraActions_.length; i++) {
386
var action = this.extraActions_[i];
387
var actionSpan = this.createLinkOption(action.spanId_);
388
this.actionSpans_.push(actionSpan);
389
this.createLink(action.linkId_, action.message_, function() {
390
action.actionFn_(this.getTargetUrl());
391
}, actionSpan);
392
}
393
394
var removeLinkSpan = this.createLinkOption(
395
goog.editor.plugins.LinkBubble.DELETE_LINK_SPAN_ID_);
396
this.createLink(
397
goog.editor.plugins.LinkBubble.DELETE_LINK_ID_,
398
goog.editor.plugins.LinkBubble.MSG_LINK_BUBBLE_REMOVE, this.deleteLink_,
399
removeLinkSpan);
400
401
this.onShow();
402
403
var bubbleContents = this.dom_.createDom(
404
goog.dom.TagName.DIV, {id: goog.editor.plugins.LinkBubble.LINK_DIV_ID_},
405
testMsgSpan || '', linkTextSpan, changeLinkSpan);
406
407
for (i = 0; i < this.actionSpans_.length; i++) {
408
bubbleContents.appendChild(this.actionSpans_[i]);
409
}
410
bubbleContents.appendChild(removeLinkSpan);
411
412
goog.dom.appendChild(bubbleContainer, bubbleContents);
413
};
414
415
416
/**
417
* Tests the link by opening it in a new tab/window. Should be used as the
418
* click event handler for the test pseudo-link.
419
* @param {!Event=} opt_event If passed in, the event will be stopped.
420
* @protected
421
*/
422
goog.editor.plugins.LinkBubble.prototype.testLink = function(opt_event) {
423
goog.window.open(
424
this.getTestLinkAction_(),
425
{'target': '_blank', 'noreferrer': this.stopReferrerLeaks_},
426
this.getFieldObject().getAppWindow());
427
if (opt_event) {
428
opt_event.stopPropagation();
429
opt_event.preventDefault();
430
}
431
};
432
433
434
/**
435
* Returns whether the URL should be considered invalid. This always returns
436
* false in the base class, and should be overridden by subclasses that wish
437
* to impose validity rules on URLs.
438
* @param {string} url The url to check.
439
* @return {boolean} Whether the URL should be considered invalid.
440
*/
441
goog.editor.plugins.LinkBubble.prototype.isInvalidUrl = goog.functions.FALSE;
442
443
444
/**
445
* Gets the text to display for a link, based on the type of link
446
* @return {!Object} Returns an object of the form:
447
* {linkText: displayTextForLinkTarget, valid: ifTheLinkIsValid}.
448
* @private
449
*/
450
goog.editor.plugins.LinkBubble.prototype.getLinkToTextObj_ = function() {
451
var isError;
452
var targetUrl = this.getTargetUrl();
453
454
if (this.isInvalidUrl(targetUrl)) {
455
targetUrl = goog.editor.plugins.LinkBubble.MSG_INVALID_URL_LINK_BUBBLE;
456
isError = true;
457
} else if (goog.editor.Link.isMailto(targetUrl)) {
458
targetUrl = targetUrl.substring(7); // 7 == "mailto:".length
459
}
460
461
return {linkText: targetUrl, valid: !isError};
462
};
463
464
465
/**
466
* Shows the link dialog.
467
* @param {goog.events.BrowserEvent} e The event.
468
* @private
469
*/
470
goog.editor.plugins.LinkBubble.prototype.showLinkDialog_ = function(e) {
471
// Needed when this occurs due to an ENTER key event, else the newly created
472
// dialog manages to have its OK button pressed, causing it to disappear.
473
e.preventDefault();
474
475
this.getFieldObject().execCommand(
476
goog.editor.Command.MODAL_LINK_EDITOR,
477
new goog.editor.Link(
478
/** @type {HTMLAnchorElement} */ (this.getTargetElement()), false));
479
this.closeBubble();
480
};
481
482
483
/**
484
* Deletes the link associated with the bubble
485
* @param {goog.events.BrowserEvent} e The event.
486
* @private
487
*/
488
goog.editor.plugins.LinkBubble.prototype.deleteLink_ = function(e) {
489
// Needed when this occurs due to an ENTER key event, else the editor receives
490
// the key press and inserts a newline.
491
e.preventDefault();
492
493
this.getFieldObject().dispatchBeforeChange();
494
495
var link = this.getTargetElement();
496
var child = link.lastChild;
497
goog.dom.flattenElement(link);
498
499
var restoreScrollPosition = this.saveScrollPosition();
500
var range = goog.dom.Range.createFromNodeContents(child);
501
range.collapse(false);
502
range.select();
503
504
this.closeBubble();
505
506
this.getFieldObject().dispatchChange();
507
this.getFieldObject().focus();
508
restoreScrollPosition();
509
};
510
511
512
/**
513
* Sets the proper state for the action links.
514
* @protected
515
* @override
516
*/
517
goog.editor.plugins.LinkBubble.prototype.onShow = function() {
518
var linkDiv =
519
this.dom_.getElement(goog.editor.plugins.LinkBubble.LINK_DIV_ID_);
520
if (linkDiv) {
521
var testLinkSpan =
522
this.dom_.getElement(goog.editor.plugins.LinkBubble.TEST_LINK_SPAN_ID_);
523
if (testLinkSpan) {
524
var url = this.getTargetUrl();
525
goog.style.setElementShown(testLinkSpan, !goog.editor.Link.isMailto(url));
526
}
527
528
for (var i = 0; i < this.extraActions_.length; i++) {
529
var action = this.extraActions_[i];
530
var actionSpan = this.dom_.getElement(action.spanId_);
531
if (actionSpan) {
532
goog.style.setElementShown(
533
actionSpan, action.toShowFn_(this.getTargetUrl()));
534
}
535
}
536
}
537
};
538
539
540
/**
541
* Gets the url for the bubble test link. The test link is the link in the
542
* bubble the user can click on to make sure the link they entered is correct.
543
* @return {string} The url for the bubble link href.
544
* @private
545
*/
546
goog.editor.plugins.LinkBubble.prototype.getTestLinkAction_ = function() {
547
var targetUrl = this.getTargetUrl();
548
return this.testLinkUrlFn_ ? this.testLinkUrlFn_(targetUrl) : targetUrl;
549
};
550
551
552
/**
553
* Checks whether the plugin should open the given url in a new window.
554
* @param {string} url The url to check.
555
* @return {boolean} If the plugin should open the given url in a new window.
556
* @protected
557
*/
558
goog.editor.plugins.LinkBubble.prototype.shouldOpenUrl = function(url) {
559
return !this.blockOpeningUnsafeSchemes_ || this.isSafeSchemeToOpen_(url);
560
};
561
562
563
/**
564
* Determines whether or not a url has a scheme which is safe to open.
565
* Schemes like javascript are unsafe due to the possibility of XSS.
566
* @param {string} url A url.
567
* @return {boolean} Whether the url has a safe scheme.
568
* @private
569
*/
570
goog.editor.plugins.LinkBubble.prototype.isSafeSchemeToOpen_ = function(url) {
571
var scheme = goog.uri.utils.getScheme(url) || 'http';
572
return goog.array.contains(this.safeToOpenSchemes_, scheme.toLowerCase());
573
};
574
575
576
577
/**
578
* Constructor for extra actions that can be added to the link bubble.
579
* @param {string} spanId The ID for the span showing the action.
580
* @param {string} linkId The ID for the link showing the action.
581
* @param {string} message The text for the link showing the action.
582
* @param {function(string):boolean} toShowFn Test function to determine whether
583
* to show the action for the given URL.
584
* @param {function(string):void} actionFn Action function to run when the
585
* action is clicked. Takes the current target URL as a parameter.
586
* @constructor
587
* @final
588
*/
589
goog.editor.plugins.LinkBubble.Action = function(
590
spanId, linkId, message, toShowFn, actionFn) {
591
this.spanId_ = spanId;
592
this.linkId_ = linkId;
593
this.message_ = message;
594
this.toShowFn_ = toShowFn;
595
this.actionFn_ = actionFn;
596
};
597
598