Completed
Push — develop ( 48b424...bab30d )
by Daniel
15:53 queued 09:41
created

tinymce.PluginManager.add(ꞌlinkꞌ)   F

Complexity

Conditions 16
Paths > 20000

Size

Total Lines 367

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 367
rs 2
c 0
b 0
f 0
cc 16
nc 106168320
nop 1

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like tinymce.PluginManager.add(ꞌlinkꞌ) often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
/**
2
 * plugin.js
3
 *
4
 * Released under LGPL License.
5
 * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
6
 *
7
 * License: http://www.tinymce.com/license
8
 * Contributing: http://www.tinymce.com/contributing
9
 */
10
11
/*global tinymce:true */
12
13
tinymce.PluginManager.add('link', function(editor) {
0 ignored issues
show
Bug introduced by
The variable tinymce seems to be never declared. If this is a global, consider adding a /** global: tinymce */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
14
	var attachState = {};
15
16
	function isLink(elm) {
17
		return elm && elm.nodeName === 'A' && elm.href;
18
	}
19
20
	function hasLinks(elements) {
21
		return tinymce.util.Tools.grep(elements, isLink).length > 0;
0 ignored issues
show
Bug introduced by
The variable tinymce seems to be never declared. If this is a global, consider adding a /** global: tinymce */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
22
	}
23
24
	function getLink(elm) {
25
		return editor.dom.getParent(elm, 'a[href]');
26
	}
27
28
	function getSelectedLink() {
29
		return getLink(editor.selection.getStart());
30
	}
31
32
	function getHref(elm) {
33
		// Returns the real href value not the resolved a.href value
34
		var href = elm.getAttribute('data-mce-href');
35
		return href ? href : elm.getAttribute('href');
36
	}
37
38
	function isContextMenuVisible() {
39
		var contextmenu = editor.plugins.contextmenu;
40
		return contextmenu ? contextmenu.isContextMenuVisible() : false;
41
	}
42
43
	var hasOnlyAltModifier = function (e) {
44
		return e.altKey === true && e.shiftKey === false && e.ctrlKey === false && e.metaKey === false;
45
	};
46
47
	function leftClickedOnAHref(elm) {
48
		var sel, rng, node;
49
		if (editor.settings.link_context_toolbar && !isContextMenuVisible() && isLink(elm)) {
50
			sel = editor.selection;
51
			rng = sel.getRng();
52
			node = rng.startContainer;
53
			// ignore cursor positions at the beginning/end (to make context toolbar less noisy)
54
			if (node.nodeType == 3 && sel.isCollapsed() && rng.startOffset > 0 && rng.startOffset < node.data.length) {
55
				return true;
56
			}
57
		}
58
		return false;
59
	}
60
61
	function appendClickRemove(link, evt) {
62
		document.body.appendChild(link);
63
		link.dispatchEvent(evt);
64
		document.body.removeChild(link);
65
	}
66
67
	function openDetachedWindow(url) {
68
		// Chrome and Webkit has implemented noopener and works correctly with/without popup blocker
69
		// Firefox has it implemented noopener but when the popup blocker is activated it doesn't work
70
		// Edge has only implemented noreferrer and it seems to remove opener as well
71
		// Older IE versions pre IE 11 falls back to a window.open approach
72
		if (!tinymce.Env.ie || tinymce.Env.ie > 10) {
0 ignored issues
show
Bug introduced by
The variable tinymce seems to be never declared. If this is a global, consider adding a /** global: tinymce */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
73
			var link = document.createElement('a');
74
			link.target = '_blank';
75
			link.href = url;
76
			link.rel = 'noreferrer noopener';
77
78
			var evt = document.createEvent('MouseEvents');
79
			evt.initMouseEvent('click', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
80
81
			appendClickRemove(link, evt);
82
		} else {
83
			var win = window.open('', '_blank');
84
			if (win) {
85
				win.opener = null;
86
				var doc = win.document;
87
				doc.open();
88
				doc.write('<meta http-equiv="refresh" content="0; url=' + tinymce.DOM.encode(url) + '">');
89
				doc.close();
90
			}
91
		}
92
	}
93
94
	function gotoLink(a) {
95
		if (a) {
96
			var href = getHref(a);
97
			if (/^#/.test(href)) {
98
				var targetEl = editor.$(href);
99
				if (targetEl.length) {
100
					editor.selection.scrollIntoView(targetEl[0], true);
101
				}
102
			} else {
103
				openDetachedWindow(a.href);
104
			}
105
		}
106
	}
107
108
	function gotoSelectedLink() {
109
		gotoLink(getSelectedLink());
110
	}
111
112
	function toggleViewLinkState() {
113
        var self = this;
114
115
		var toggleVisibility = function (e) {
116
			if (hasLinks(e.parents)) {
117
				self.show();
118
			} else {
119
				self.hide();
120
			}
121
		};
122
123
		if (!hasLinks(editor.dom.getParents(editor.selection.getStart()))) {
124
			self.hide();
125
		}
126
127
        editor.on('nodechange', toggleVisibility);
128
129
		self.on('remove', function () {
130
			editor.off('nodechange', toggleVisibility);
131
		});
132
	}
133
134
	function createLinkList(callback) {
135
		return function() {
136
			var linkList = editor.settings.link_list;
137
138
			if (typeof linkList == "string") {
139
				tinymce.util.XHR.send({
0 ignored issues
show
Bug introduced by
The variable tinymce seems to be never declared. If this is a global, consider adding a /** global: tinymce */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
140
					url: linkList,
141
					success: function(text) {
142
						callback(tinymce.util.JSON.parse(text));
0 ignored issues
show
Bug introduced by
The variable tinymce seems to be never declared. If this is a global, consider adding a /** global: tinymce */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
143
					}
144
				});
145
			} else if (typeof linkList == "function") {
146
				linkList(callback);
147
			} else {
148
				callback(linkList);
149
			}
150
		};
151
	}
152
153
	function buildListItems(inputList, itemCallback, startItems) {
154
		function appendItems(values, output) {
155
			output = output || [];
156
157
			tinymce.each(values, function(item) {
0 ignored issues
show
Bug introduced by
The variable tinymce seems to be never declared. If this is a global, consider adding a /** global: tinymce */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
158
				var menuItem = {text: item.text || item.title};
159
160
				if (item.menu) {
161
					menuItem.menu = appendItems(item.menu);
162
				} else {
163
					menuItem.value = item.value;
164
165
					if (itemCallback) {
166
						itemCallback(menuItem);
167
					}
168
				}
169
170
				output.push(menuItem);
171
			});
172
173
			return output;
174
		}
175
176
		return appendItems(inputList, startItems || []);
177
	}
178
179
	function showDialog(linkList) {
180
		var data = {}, selection = editor.selection, dom = editor.dom, selectedElm, anchorElm, initialText;
181
		var win, onlyText, textListCtrl, linkListCtrl, relListCtrl, targetListCtrl, classListCtrl, linkTitleCtrl, value;
182
183
		function linkListChangeHandler(e) {
184
			var textCtrl = win.find('#text');
185
186
			if (!textCtrl.value() || (e.lastControl && textCtrl.value() == e.lastControl.text())) {
187
				textCtrl.value(e.control.text());
188
			}
189
190
			win.find('#href').value(e.control.value());
191
		}
192
193
		function buildAnchorListControl(url) {
194
			var anchorList = [];
195
196
			tinymce.each(editor.dom.select('a:not([href])'), function(anchor) {
0 ignored issues
show
Bug introduced by
The variable tinymce seems to be never declared. If this is a global, consider adding a /** global: tinymce */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
197
				var id = anchor.name || anchor.id;
198
199
				if (id) {
200
					anchorList.push({
201
						text: id,
202
						value: '#' + id,
203
						selected: url.indexOf('#' + id) != -1
204
					});
205
				}
206
			});
207
208
			if (anchorList.length) {
0 ignored issues
show
Complexity Best Practice introduced by
There is no return statement if anchorList.length is false. Are you sure this is correct? If so, consider adding return; explicitly.

This check looks for functions where a return statement is found in some execution paths, but not in all.

Consider this little piece of code

function isBig(a) {
    if (a > 5000) {
        return "yes";
    }
}

console.log(isBig(5001)); //returns yes
console.log(isBig(42)); //returns undefined

The function isBig will only return a specific value when its parameter is bigger than 5000. In any other case, it will implicitly return undefined.

This behaviour may not be what you had intended. In any case, you can add a return undefined to the other execution path to make the return value explicit.

Loading history...
209
				anchorList.unshift({text: 'None', value: ''});
210
211
				return {
212
					name: 'anchor',
213
					type: 'listbox',
214
					label: 'Anchors',
215
					values: anchorList,
216
					onselect: linkListChangeHandler
217
				};
218
			}
219
		}
220
221
		function updateText() {
222
			if (!initialText && data.text.length === 0 && onlyText) {
223
				this.parent().parent().find('#text')[0].value(this.value());
224
			}
225
		}
226
227
		function urlChange(e) {
228
			var meta = e.meta || {};
229
230
			if (linkListCtrl) {
231
				linkListCtrl.value(editor.convertURL(this.value(), 'href'));
232
			}
233
234
			tinymce.each(e.meta, function(value, key) {
0 ignored issues
show
Bug introduced by
The variable tinymce seems to be never declared. If this is a global, consider adding a /** global: tinymce */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
235
				var inp = win.find('#' + key);
236
237
				if (key === 'text') {
238
					if (initialText.length === 0) {
239
						inp.value(value);
240
						data.text = value;
241
					}
242
				} else {
243
					inp.value(value);
244
				}
245
			});
246
247
			if (meta.attach) {
248
				attachState = {
249
					href: this.value(),
250
					attach: meta.attach
251
				};
252
			}
253
254
			if (!meta.text) {
255
				updateText.call(this);
256
			}
257
		}
258
259
		function isOnlyTextSelected(anchorElm) {
260
			var html = selection.getContent();
261
262
			// Partial html and not a fully selected anchor element
263
			if (/</.test(html) && (!/^<a [^>]+>[^<]+<\/a>$/.test(html) || html.indexOf('href=') == -1)) {
264
				return false;
265
			}
266
267
			if (anchorElm) {
268
				var nodes = anchorElm.childNodes, i;
269
270
				if (nodes.length === 0) {
271
					return false;
272
				}
273
274
				for (i = nodes.length - 1; i >= 0; i--) {
275
					if (nodes[i].nodeType != 3) {
276
						return false;
277
					}
278
				}
279
			}
280
281
			return true;
282
		}
283
284
		function onBeforeCall(e) {
285
			e.meta = win.toJSON();
286
		}
287
288
		selectedElm = selection.getNode();
289
		anchorElm = dom.getParent(selectedElm, 'a[href]');
290
		onlyText = isOnlyTextSelected();
291
292
		data.text = initialText = anchorElm ? (anchorElm.innerText || anchorElm.textContent) : selection.getContent({format: 'text'});
293
		data.href = anchorElm ? dom.getAttrib(anchorElm, 'href') : '';
294
295
		if (anchorElm) {
296
			data.target = dom.getAttrib(anchorElm, 'target');
297
		} else if (editor.settings.default_link_target) {
298
			data.target = editor.settings.default_link_target;
299
		}
300
301
		if ((value = dom.getAttrib(anchorElm, 'rel'))) {
302
			data.rel = value;
303
		}
304
305
		if ((value = dom.getAttrib(anchorElm, 'class'))) {
306
			data['class'] = value;
307
		}
308
309
		if ((value = dom.getAttrib(anchorElm, 'title'))) {
310
			data.title = value;
311
		}
312
313
		if (onlyText) {
314
			textListCtrl = {
315
				name: 'text',
316
				type: 'textbox',
317
				size: 40,
318
				label: 'Text to display',
319
				onchange: function() {
320
					data.text = this.value();
321
				}
322
			};
323
		}
324
325
		if (linkList) {
326
			linkListCtrl = {
327
				type: 'listbox',
328
				label: 'Link list',
329
				values: buildListItems(
330
					linkList,
331
					function(item) {
332
						item.value = editor.convertURL(item.value || item.url, 'href');
333
					},
334
					[{text: 'None', value: ''}]
335
				),
336
				onselect: linkListChangeHandler,
337
				value: editor.convertURL(data.href, 'href'),
338
				onPostRender: function() {
339
					/*eslint consistent-this:0*/
340
					linkListCtrl = this;
341
				}
342
			};
343
		}
344
345
		if (editor.settings.target_list !== false) {
346
			if (!editor.settings.target_list) {
347
				editor.settings.target_list = [
348
					{text: 'None', value: ''},
349
					{text: 'New window', value: '_blank'}
350
				];
351
			}
352
353
			targetListCtrl = {
354
				name: 'target',
355
				type: 'listbox',
356
				label: 'Target',
357
				values: buildListItems(editor.settings.target_list)
358
			};
359
		}
360
361
		if (editor.settings.rel_list) {
362
			relListCtrl = {
363
				name: 'rel',
364
				type: 'listbox',
365
				label: 'Rel',
366
				values: buildListItems(editor.settings.rel_list)
367
			};
368
		}
369
370
		if (editor.settings.link_class_list) {
371
			classListCtrl = {
372
				name: 'class',
373
				type: 'listbox',
374
				label: 'Class',
375
				values: buildListItems(
376
					editor.settings.link_class_list,
377
					function(item) {
378
						if (item.value) {
379
							item.textStyle = function() {
380
								return editor.formatter.getCssText({inline: 'a', classes: [item.value]});
381
							};
382
						}
383
					}
384
				)
385
			};
386
		}
387
388
		if (editor.settings.link_title !== false) {
389
			linkTitleCtrl = {
390
				name: 'title',
391
				type: 'textbox',
392
				label: 'Title',
393
				value: data.title
394
			};
395
		}
396
397
		win = editor.windowManager.open({
398
			title: 'Insert link',
399
			data: data,
400
			body: [
401
				{
402
					name: 'href',
403
					type: 'filepicker',
404
					filetype: 'file',
405
					size: 40,
406
					autofocus: true,
407
					label: 'Url',
408
					onchange: urlChange,
409
					onkeyup: updateText,
410
					onbeforecall: onBeforeCall
411
				},
412
				textListCtrl,
0 ignored issues
show
Bug introduced by
The variable textListCtrl does not seem to be initialized in case onlyText on line 313 is false. Are you sure this can never be the case?
Loading history...
413
				linkTitleCtrl,
0 ignored issues
show
Bug introduced by
The variable linkTitleCtrl does not seem to be initialized in case editor.settings.link_title !== false on line 388 is false. Are you sure this can never be the case?
Loading history...
414
				buildAnchorListControl(data.href),
415
				linkListCtrl,
0 ignored issues
show
Bug introduced by
The variable linkListCtrl does not seem to be initialized in case linkList on line 325 is false. Are you sure this can never be the case?
Loading history...
416
				relListCtrl,
0 ignored issues
show
Bug introduced by
The variable relListCtrl does not seem to be initialized in case editor.settings.rel_list on line 361 is false. Are you sure this can never be the case?
Loading history...
417
				targetListCtrl,
0 ignored issues
show
Bug introduced by
The variable targetListCtrl does not seem to be initialized in case editor.settings.target_list !== false on line 345 is false. Are you sure this can never be the case?
Loading history...
418
				classListCtrl
0 ignored issues
show
Bug introduced by
The variable classListCtrl does not seem to be initialized in case editor.settings.link_class_list on line 370 is false. Are you sure this can never be the case?
Loading history...
419
			],
420
			onSubmit: function(e) {
421
				/*eslint dot-notation: 0*/
422
				var href;
423
424
				data = tinymce.extend(data, e.data);
0 ignored issues
show
Bug introduced by
The variable tinymce seems to be never declared. If this is a global, consider adding a /** global: tinymce */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
425
				href = data.href;
426
427
				// Delay confirm since onSubmit will move focus
428
				function delayedConfirm(message, callback) {
429
					var rng = editor.selection.getRng();
430
431
					tinymce.util.Delay.setEditorTimeout(editor, function() {
0 ignored issues
show
Bug introduced by
The variable tinymce seems to be never declared. If this is a global, consider adding a /** global: tinymce */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
432
						editor.windowManager.confirm(message, function(state) {
433
							editor.selection.setRng(rng);
434
							callback(state);
435
						});
436
					});
437
				}
438
439
				function toggleTargetRules(rel, isUnsafe) {
440
					var rules = 'noopener noreferrer';
441
442
					function addTargetRules(rel) {
443
						rel = removeTargetRules(rel);
444
						return rel ? [rel, rules].join(' ') : rules;
445
					}
446
447
					function removeTargetRules(rel) {
448
						var regExp = new RegExp('(' + rules.replace(' ', '|') + ')', 'g');
449
						if (rel) {
450
							rel = tinymce.trim(rel.replace(regExp, ''));
0 ignored issues
show
Bug introduced by
The variable tinymce seems to be never declared. If this is a global, consider adding a /** global: tinymce */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
451
						}
452
						return rel ? rel : null;
453
					}
454
455
					return isUnsafe ? addTargetRules(rel) : removeTargetRules(rel);
456
				}
457
458
				function createLink() {
459
					var linkAttrs = {
460
						href: href,
461
						target: data.target ? data.target : null,
462
						rel: data.rel ? data.rel : null,
463
						"class": data["class"] ? data["class"] : null,
464
						title: data.title ? data.title : null
465
					};
466
467
					if (!editor.settings.allow_unsafe_link_target) {
468
						linkAttrs.rel = toggleTargetRules(linkAttrs.rel, linkAttrs.target == '_blank');
469
					}
470
471
					if (href === attachState.href) {
472
						attachState.attach();
473
						attachState = {};
474
					}
475
476
					if (anchorElm) {
477
						editor.focus();
478
479
						if (onlyText && data.text != initialText) {
480
							if ("innerText" in anchorElm) {
481
								anchorElm.innerText = data.text;
482
							} else {
483
								anchorElm.textContent = data.text;
484
							}
485
						}
486
487
						dom.setAttribs(anchorElm, linkAttrs);
488
489
						selection.select(anchorElm);
490
						editor.undoManager.add();
491
					} else {
492
						if (onlyText) {
493
							editor.insertContent(dom.createHTML('a', linkAttrs, dom.encode(data.text)));
494
						} else {
495
							editor.execCommand('mceInsertLink', false, linkAttrs);
496
						}
497
					}
498
				}
499
500
				function insertLink() {
501
					editor.undoManager.transact(createLink);
502
				}
503
504
				if (!href) {
505
					editor.execCommand('unlink');
506
					return;
507
				}
508
509
				// Is email and not //[email protected]
510
				if (href.indexOf('@') > 0 && href.indexOf('//') == -1 && href.indexOf('mailto:') == -1) {
511
					delayedConfirm(
512
						'The URL you entered seems to be an email address. Do you want to add the required mailto: prefix?',
513
						function(state) {
514
							if (state) {
515
								href = 'mailto:' + href;
516
							}
517
518
							insertLink();
519
						}
520
					);
521
522
					return;
523
				}
524
525
				// Is not protocol prefixed
526
				if ((editor.settings.link_assume_external_targets && !/^\w+:/i.test(href)) ||
527
					(!editor.settings.link_assume_external_targets && /^\s*www[\.|\d\.]/i.test(href))) {
528
					delayedConfirm(
529
						'The URL you entered seems to be an external link. Do you want to add the required http:// prefix?',
530
						function(state) {
531
							if (state) {
532
								href = 'http://' + href;
533
							}
534
535
							insertLink();
536
						}
537
					);
538
539
					return;
540
				}
541
542
				insertLink();
543
			}
544
		});
545
	}
546
547
	editor.addButton('link', {
548
		icon: 'link',
549
		tooltip: 'Insert/edit link',
550
		shortcut: 'Meta+K',
551
		onclick: createLinkList(showDialog),
552
		stateSelector: 'a[href]'
553
	});
554
555
	editor.addButton('unlink', {
556
		icon: 'unlink',
557
		tooltip: 'Remove link',
558
		cmd: 'unlink',
559
		stateSelector: 'a[href]'
560
	});
561
562
563
	if (editor.addContextToolbar) {
564
		editor.addButton('openlink', {
565
			icon: 'newtab',
566
			tooltip: 'Open link',
567
			onclick: gotoSelectedLink
568
		});
569
570
		editor.addContextToolbar(
571
			leftClickedOnAHref,
572
			'openlink | link unlink'
573
		);
574
	}
575
576
577
	editor.addShortcut('Meta+K', '', createLinkList(showDialog));
578
	editor.addCommand('mceLink', createLinkList(showDialog));
579
580
	editor.on('click', function (e) {
581
		var link = getLink(e.target);
582
		if (link && tinymce.util.VK.metaKeyPressed(e)) {
0 ignored issues
show
Bug introduced by
The variable tinymce seems to be never declared. If this is a global, consider adding a /** global: tinymce */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
583
			e.preventDefault();
584
			gotoLink(link);
585
		}
586
	});
587
588
	editor.on('keydown', function (e) {
589
		var link = getSelectedLink();
590
		if (link && e.keyCode === 13 && hasOnlyAltModifier(e)) {
591
			e.preventDefault();
592
			gotoLink(link);
593
		}
594
	});
595
596
	this.showDialog = showDialog;
597
598
	editor.addMenuItem('openlink', {
599
		text: 'Open link',
600
		icon: 'newtab',
601
		onclick: gotoSelectedLink,
602
		onPostRender: toggleViewLinkState,
603
		prependToContext: true
604
	});
605
606
	editor.addMenuItem('link', {
607
		icon: 'link',
608
		text: 'Link',
609
		shortcut: 'Meta+K',
610
		onclick: createLinkList(showDialog),
611
		stateSelector: 'a[href]',
612
		context: 'insert',
613
		prependToContext: true
614
	});
615
});
616