Passed
Push — master ( d9e5dd...36764d )
by Spuds
01:07 queued 26s
created

themes/default/scripts/quickQuote.js   F

Complexity

Total Complexity 90
Complexity/F 3.91

Size

Lines of Code 752
Function Count 23

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
wmc 90
eloc 393
mnd 67
bc 67
fnc 23
dl 0
loc 752
rs 2
bpm 2.9129
cpm 3.913
noi 19
c 0
b 0
f 0

How to fix   Complexity   

Complexity

Complex classes like themes/default/scripts/quickQuote.js 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
 * @package   Quick Quote
3
 * @copyright Frenzie : Frans de Jonge
4
 * @license   BSD http://opensource.org/licenses/BSD-3-Clause (see accompanying LICENSE.txt file)
5
 *
6
 * @version 2.0 dev
7
 */
8
9
/**
10
 * This particular function was based on My Opera Enhancements and is
11
 * licensed under the BSD license.
12
 *
13
 * Some specific refactoring done for ElkArte core inclusion
14
 */
15
function Elk_QuickQuote(oOptions)
16
{
17
	'use strict';
18
19
	this.opts = Object.assign({}, this.defaults, oOptions);
20
21
	this.pointerDirection = 'right';
22
	this.startPointerX = 0;
23
	this.endPointerX = 0;
24
25
	this.init();
26
}
27
28
/**
29
 * Get things rolling
30
 */
31
Elk_QuickQuote.prototype.init = function ()
32
{
33
	this.treeToBBCode.defaults = {
34
		strong: {before: '[b]', after: '[/b]'},
35
		b: {before: '[b]', after: '[/b]'},
36
		i: {before: '[i]', after: '[/i]'},
37
		em: {before: '[i]', after: '[/i]'},
38
		s: {before: '[s]', after: '[/s]'},
39
		sup: {before: '[sup]', after: '[/sup]'},
40
		sub: {before: '[sub]', after: '[/sub]'},
41
		pre: {before: '[code]', after: '[/code]'},
42
		br: {before: '\n', after: ''}
43
	};
44
45
	// Check if passive is supported, should be for most browsers since 2016
46
	let supportsPassive = false;
47
	try {
48
		let opts = Object.defineProperty({}, 'passive', {get: function() {supportsPassive = true;}});
49
		window.addEventListener('test', null, opts);
50
	} catch (e) {
0 ignored issues
show
Coding Style Comprehensibility Best Practice introduced by
Empty catch clauses should be used with caution; consider adding a comment why this is needed.
Loading history...
51
		// Just fall through, it does not support passive mouse events
52
	}
53
54
	// Pointer event capabilities
55
	let hasPointerEvents = (('PointerEvent' in window) || (window.navigator && 'msPointerEnabled' in window.navigator));
56
	this.mouseDown = hasPointerEvents ? 'pointerdown' : is_touch ? 'touchstart' : 'mousedown';
0 ignored issues
show
Best Practice introduced by
If you intend to check if the variable is_touch is declared in the current environment, consider using typeof is_touch === "undefined" instead. This is safe if the variable is not actually declared.
Loading history...
57
	this.mouseUp = hasPointerEvents ? 'pointerup' : is_touch ? 'touchend' : 'mouseup';
58
59
	// Initialize Quick Quote, set event listener to all messageContent areas
60
	document.querySelectorAll('.messageContent').forEach((message) =>
61
	{
62
		message.addEventListener(this.mouseDown, this.getEventStartPosition.bind(this), supportsPassive ? {passive: true} : false);
63
		message.addEventListener(this.mouseUp, this.getEventEndPosition.bind(this), supportsPassive ? {passive: true} : false);
64
		message.addEventListener(this.mouseUp, this.prepareQuickQuoteButton.bind(this), supportsPassive ? {passive: true} : false);
65
66
		// Needed for android touch chrome as the mouseUp event is held by the context menu ribbon
67
		if (is_touch)
0 ignored issues
show
Best Practice introduced by
If you intend to check if the variable is_touch is declared in the current environment, consider using typeof is_touch === "undefined" instead. This is safe if the variable is not actually declared.
Loading history...
68
		{
69
			message.addEventListener('contextmenu', this.prepareQuickQuoteButton.bind(this), false);
70
		}
71
	});
72
};
73
74
/**
75
 * Get the X coordinate of the pointer down event
76
 *
77
 * @param {TouchEvent|MouseEvent} event
78
 */
79
Elk_QuickQuote.prototype.getEventStartPosition = function (event)
80
{
81
	if (typeof event.changedTouches !== 'undefined')
82
	{
83
		this.startPointerX = event.changedTouches[0].pageX;
84
	}
85
	else
86
	{
87
		this.startPointerX = event.clientX;
88
	}
89
};
90
91
/**
92
 * Get the X coordinate of the pointer up event, Set right or left for movement
93
 *
94
 * @param {TouchEvent|MouseEvent} event
95
 */
96
Elk_QuickQuote.prototype.getEventEndPosition = function (event)
97
{
98
	if (typeof event.changedTouches !== 'undefined')
99
	{
100
		this.endPointerX = event.changedTouches[0].pageX;
101
	}
102
	else
103
	{
104
		this.endPointerX = event.clientX;
105
	}
106
107
	this.pointerDirection = this.endPointerX > this.startPointerX ? 'right' : 'left';
108
};
109
110
/**
111
 * Determine the window position of the selected text.  If the selection
112
 * can not be determined (multi click or other) then the event location would be used.
113
 *
114
 * @param {PointerEvent} event
115
 * @return {Object} Returns the x and y position
116
 */
117
Elk_QuickQuote.prototype.getEventPosition = function (event)
118
{
119
	// Set an approximate position as a backup
120
	let posRight = window.innerWidth - event.pageX - 10,
121
		posLeft = event.pageX,
122
		posBottom = event.pageY + 15,
123
		posTop = event.pageY - 5;
124
125
	let selectionRange = window.getSelection().getRangeAt(0).cloneRange(),
126
		relativePos = document.body.parentNode.getBoundingClientRect();
127
128
	// Collapse on start or end based on pointer movement direction
129
	selectionRange.collapse(this.pointerDirection === 'left');
130
	let selectionBox = selectionRange.getClientRects();
131
132
	if (selectionBox.length > 0)
133
	{
134
		posRight = -Math.round(selectionBox[0].right - relativePos.right);
135
		posLeft = Math.round(selectionBox[0].left - relativePos.left);
136
137
		posBottom = Math.round(selectionBox[0].bottom - relativePos.top + 5);
138
		posTop = Math.round(selectionBox[0].top - relativePos.top - 5);
139
	}
140
141
	return {
142
		right: posRight,
143
		left: posLeft,
144
		bottom: posBottom,
145
		top: posTop
146
	};
147
};
148
149
/**
150
 * Positions the button close to the mouse/touch up location for best
151
 * user interaction
152
 *
153
 * @param {PointerEvent} event The event
154
 * @param {HTMLElement} button The element to position
155
 */
156
Elk_QuickQuote.prototype.setButtonPosition = function (event, button)
157
{
158
	let clickCoords = this.getEventPosition(event),
159
		buttonBottom = clickCoords.bottom + button.offsetHeight,
160
		windowBottom = window.scrollY + window.innerHeight;
161
162
	// Don't go off the bottom of the viewport
163
	if (buttonBottom > windowBottom)
164
	{
165
		button.style.top = clickCoords.top - button.offsetHeight + 'px';
166
	}
167
	else
168
	{
169
		button.style.top = clickCoords.bottom + 'px';
170
	}
171
172
	// For touch devices we need to account for selection bounding handles.  There is not a consistent
173
	// way to disable the default selection menu, so positioning below the text + handles is the
174
	// only available option
175
	if (is_touch)
0 ignored issues
show
Best Practice introduced by
If you intend to check if the variable is_touch is declared in the current environment, consider using typeof is_touch === "undefined" instead. This is safe if the variable is not actually declared.
Loading history...
176
	{
177
		button.style.top = clickCoords.bottom + 25 + 'px';
178
	}
179
180
	// Don't go outside our message area
181
	// @todo simplify
182
	let postPos = event.currentTarget.getBoundingClientRect();
183
	if (this.pointerDirection === 'right')
184
	{
185
		if (clickCoords.left - button.offsetWidth < postPos.left)
186
		{
187
			let shift = postPos.left - (clickCoords.left - button.offsetWidth);
188
			button.style.right = Math.round(clickCoords.right - shift - 10) + 'px';
189
		}
190
		else
191
		{
192
			button.style.right = clickCoords.right + "px";
193
		}
194
	}
195
	else
196
	{
197
		if (clickCoords.left + button.offsetWidth > postPos.right)
198
		{
199
			let shift = (clickCoords.left + button.offsetWidth) - postPos.right;
200
			button.style.right = Math.round(clickCoords.right - shift - 10) + "px";
201
		}
202
		else
203
		{
204
			button.style.right = clickCoords.right - button.offsetWidth + "px";
205
		}
206
	}
207
};
208
209
/**
210
 * Transverses tree under node and set a flag telling whether element is hidden or not
211
 *
212
 * @param {object} node
213
 */
214
Elk_QuickQuote.prototype.setHiddenFlag = function (node)
215
{
216
	if (!node)
217
	{
218
		return;
219
	}
220
221
	if (typeof node.item === 'function')
222
	{
223
		node.forEach((asNode) =>
224
		{
225
			this.setHiddenFlag(asNode);
226
		});
227
	}
228
	else if (this.isHidden(node) !== '')
229
	{
230
		node.setAttribute('userjsishidden', 'true');
231
	}
232
	else
233
	{
234
		if (node.removeAttribute)
235
		{
236
			node.removeAttribute('userjsishidden');
237
		}
238
239
		if (node.childNodes)
240
		{
241
			this.setHiddenFlag(node.childNodes);
242
		}
243
	}
244
};
245
246
/**
247
 * Tells if element should be considered as not visible
248
 *
249
 * @param {Node} node
250
 * @returns {string}
251
 */
252
Elk_QuickQuote.prototype.isHidden = function (node)
253
{
254
	if (node && node.nodeType === Node.ELEMENT_NODE)
0 ignored issues
show
Bug introduced by
The variable Node seems to be never declared. If this is a global, consider adding a /** global: Node */ 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...
255
	{
256
		let compStyles = getComputedStyle(node, '');
257
258
		if (node.nodeName.toLowerCase() === 'br') return '';
0 ignored issues
show
Coding Style Best Practice introduced by
Curly braces around statements make for more readable code and help prevent bugs when you add further statements.

Consider adding curly braces around all statements when they are executed conditionally. This is optional if there is only one statement, but leaving them out can lead to unexpected behaviour if another statement is added later.

Consider:

if (a > 0)
    b = 42;

If you or someone else later decides to put another statement in, only the first statement will be executed.

if (a > 0)
    console.log("a > 0");
    b = 42;

In this case the statement b = 42 will always be executed, while the logging statement will be executed conditionally.

if (a > 0) {
    console.log("a > 0");
    b = 42;
}

ensures that the proper code will be executed conditionally no matter how many statements are added or removed.

Loading history...
259
		if (compStyles.display === 'none') return 'display:none';
0 ignored issues
show
Coding Style Best Practice introduced by
Curly braces around statements make for more readable code and help prevent bugs when you add further statements.

Consider adding curly braces around all statements when they are executed conditionally. This is optional if there is only one statement, but leaving them out can lead to unexpected behaviour if another statement is added later.

Consider:

if (a > 0)
    b = 42;

If you or someone else later decides to put another statement in, only the first statement will be executed.

if (a > 0)
    console.log("a > 0");
    b = 42;

In this case the statement b = 42 will always be executed, while the logging statement will be executed conditionally.

if (a > 0) {
    console.log("a > 0");
    b = 42;
}

ensures that the proper code will be executed conditionally no matter how many statements are added or removed.

Loading history...
260
		if (compStyles.visibility === 'hidden') return 'visibility:hidden';
0 ignored issues
show
Coding Style Best Practice introduced by
Curly braces around statements make for more readable code and help prevent bugs when you add further statements.

Consider adding curly braces around all statements when they are executed conditionally. This is optional if there is only one statement, but leaving them out can lead to unexpected behaviour if another statement is added later.

Consider:

if (a > 0)
    b = 42;

If you or someone else later decides to put another statement in, only the first statement will be executed.

if (a > 0)
    console.log("a > 0");
    b = 42;

In this case the statement b = 42 will always be executed, while the logging statement will be executed conditionally.

if (a > 0) {
    console.log("a > 0");
    b = 42;
}

ensures that the proper code will be executed conditionally no matter how many statements are added or removed.

Loading history...
261
		if (parseFloat(compStyles.opacity) < 0.1) return 'opacity';
0 ignored issues
show
Coding Style Best Practice introduced by
Curly braces around statements make for more readable code and help prevent bugs when you add further statements.

Consider adding curly braces around all statements when they are executed conditionally. This is optional if there is only one statement, but leaving them out can lead to unexpected behaviour if another statement is added later.

Consider:

if (a > 0)
    b = 42;

If you or someone else later decides to put another statement in, only the first statement will be executed.

if (a > 0)
    console.log("a > 0");
    b = 42;

In this case the statement b = 42 will always be executed, while the logging statement will be executed conditionally.

if (a > 0) {
    console.log("a > 0");
    b = 42;
}

ensures that the proper code will be executed conditionally no matter how many statements are added or removed.

Loading history...
262
		if (node.offsetHeight < 4) return 'offsetHeight';
0 ignored issues
show
Coding Style Best Practice introduced by
Curly braces around statements make for more readable code and help prevent bugs when you add further statements.

Consider adding curly braces around all statements when they are executed conditionally. This is optional if there is only one statement, but leaving them out can lead to unexpected behaviour if another statement is added later.

Consider:

if (a > 0)
    b = 42;

If you or someone else later decides to put another statement in, only the first statement will be executed.

if (a > 0)
    console.log("a > 0");
    b = 42;

In this case the statement b = 42 will always be executed, while the logging statement will be executed conditionally.

if (a > 0) {
    console.log("a > 0");
    b = 42;
}

ensures that the proper code will be executed conditionally no matter how many statements are added or removed.

Loading history...
263
		if (node.offsetWidth < 4) return 'offsetWidth';
0 ignored issues
show
Coding Style Best Practice introduced by
Curly braces around statements make for more readable code and help prevent bugs when you add further statements.

Consider adding curly braces around all statements when they are executed conditionally. This is optional if there is only one statement, but leaving them out can lead to unexpected behaviour if another statement is added later.

Consider:

if (a > 0)
    b = 42;

If you or someone else later decides to put another statement in, only the first statement will be executed.

if (a > 0)
    console.log("a > 0");
    b = 42;

In this case the statement b = 42 will always be executed, while the logging statement will be executed conditionally.

if (a > 0) {
    console.log("a > 0");
    b = 42;
}

ensures that the proper code will be executed conditionally no matter how many statements are added or removed.

Loading history...
264
265
		return '';
266
	}
267
268
	return '';
269
};
270
271
/**
272
 * Compares CSS properties against a predefined array
273
 *
274
 * @param {object} node
275
 * @param {array} props
276
 * @returns {{start: string, end: string}}
277
 */
278
Elk_QuickQuote.prototype.checkCSSProps = function (node, props)
279
{
280
	let start = '',
281
		end = '',
282
		value;
283
284
	props.forEach((prop) =>
285
	{
286
		// Check for class name
287
		if (typeof prop.isClass !== 'undefined')
288
		{
289
			value = node.classList.contains(prop.name) ? prop.name : '';
290
		}
291
		// Or style attribute
292
		else
293
		{
294
			value = this.trim(node.style[prop.name] || '', ' "');
295
		}
296
297
		if ((prop.forceValue && value === prop.forceValue) || (!prop.forceValue && value))
298
		{
299
			start += prop.before.replace('@value', (prop.values ? prop.values[value] : null) || value);
300
			end += prop.after;
301
		}
302
	});
303
304
	return {start: start, end: end};
305
};
306
307
/**
308
 * Parses the tree into bbcode
309
 *
310
 * @param {object} node
311
 * @returns {string}
312
 */
313
Elk_QuickQuote.prototype.treeToBBCode = function (node)
314
{
315
	let checked,
316
		start,
317
		end,
318
		bb = [],
319
		props = [];
0 ignored issues
show
Unused Code introduced by
The assignment to variable props seems to be never used. Consider removing it.
Loading history...
320
321
	if (typeof node.item === 'function')
322
	{
323
		node.forEach((asNode) =>
324
		{
325
			bb.push(this.treeToBBCode(asNode));
326
		});
327
328
		return bb.join('');
329
	}
330
331
	if (node.getAttribute && node.getAttribute('userjsishidden') === 'true')
332
	{
333
		return '';
334
	}
335
336
	switch (node.nodeType)
337
	{
338
		// nodeType 1, like div, p, ul
339
		case Node.ELEMENT_NODE:
0 ignored issues
show
Bug introduced by
The variable Node seems to be never declared. If this is a global, consider adding a /** global: Node */ 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...
340
			let nodeName = node.nodeName.toLowerCase(),
341
				def = this.treeToBBCode.defaults[nodeName];
342
343
			// Generic wrap behavior for basic BBC tags like [b], [i], [u]
344
			if (def)
345
			{
346
				bb.push(def.before || '');
347
				bb.push(this.treeToBBCode(node.childNodes));
348
				bb.push(def.after || '');
349
			}
350
			// Special Processing cases
351
			else
352
			{
353
				switch (nodeName)
354
				{
355
					case 'a':
356
						if (node.href.indexOf('mailto:') === 0)
357
						{
358
							bb.push('[email=' + node.href.substring(7) + ']');
359
							bb.push(this.treeToBBCode(node.childNodes));
360
							bb.push('[/email]');
361
						}
362
						else if (node.className.indexOf("attach") >= 0)
363
						{
364
							bb.push('[attach=' + node.href + ']');
365
							bb.push(this.treeToBBCode(node.childNodes));
366
							bb.push('[/attach]');
367
						}
368
						else
369
						{
370
							bb.push('[url=' + node.href + ']');
371
							bb.push(this.treeToBBCode(node.childNodes));
372
							bb.push('[/url]');
373
						}
374
						break;
375
					case 'div':
376
						props = [
377
							{name: 'textAlign', forceValue: 'left', before: '[left]', after: '[/left]'},
378
							{name: 'textAlign', forceValue: 'right', before: '[right]', after: '[/right]'},
379
							{name: 'centertext', before: '[center]', after: '[/center]', isClass: true},
380
						];
381
						checked = this.checkCSSProps(node, props);
382
383
						bb.push(checked.start);
384
						bb.push(this.treeToBBCode(node.childNodes));
385
						bb.push(checked.end);
386
						break;
387
					case 'img':
388
						let smileyCode = this.getSmileyCode(node);
389
390
						bb.push(smileyCode ? ' ' + smileyCode + ' ' : '[img]' + node.src + '[/img]');
391
						break;
392
					case 'ul':
393
						props = [
394
							{
395
								name: 'listStyleType',
396
								forceValue: 'decimal',
397
								before: '[list type=decimal]',
398
								after: '[/list]'
399
							},
400
						];
401
						checked = this.checkCSSProps(node, props);
402
403
						bb.push((checked.start !== '') ? checked.start : '[list]');
404
405
						let lis = node.querySelectorAll('li');
406
407
						lis.forEach((li) =>
408
						{
409
							bb.push('\n  [*] ' + this.trim(this.treeToBBCode(li)));
410
						});
411
412
						bb.push('[/list]');
413
						break;
414
					case 'span':
415
						// Check for css properties
416
						props = [
417
							{name: 'textDecoration', forceValue: 'underline', before: '[u]', after: '[/u]'},
418
							{name: 'color', before: '[color=@value]', after: '[/color]'},
419
							{name: 'fontFamily', before: '[font=@value]', after: '[/font]'},
420
							{name: 'bbc_tt', before: '[tt]', after: '[/tt]', isClass: true},
421
							{
422
								name: 'fontSize', before: '[size=@value]', after: '[/size]', values: {
423
									'xx-small': 1,
424
									'x-small': 2,
425
									'small': 3,
426
									'medium': 4,
427
									'large': 5,
428
									'x-large': 6,
429
									'xx-large': 7
430
								}
431
							}
432
						];
433
434
						checked = this.checkCSSProps(node, props);
435
						start = checked.start;
436
						end = checked.end;
437
438
						bb.push(start);
439
						bb.push(this.treeToBBCode(node.childNodes));
440
						bb.push(end);
441
						break;
442
					case 'p':
443
						bb.push(this.treeToBBCode(node.childNodes));
444
						break;
445
					case 'blockquote':
446
						if (node.classList.contains("bbc_quote"))
447
						{
448
							let author = node.getAttribute('data-quoted'),
449
								datetime = node.getAttribute('data-datetime'),
450
								link = node.getAttribute('data-link');
451
452
							bb.push('[quote' +
453
								(author ? ' author=' + author : '') +
454
								((link && datetime) ? ' link=' + link + ' date=' + datetime : '') +
455
								']\n');
456
							bb.push(this.treeToBBCode(node.childNodes));
457
							bb.push('\n[/quote]\n');
458
						}
459
						else
460
						{
461
							bb.push(this.treeToBBCode(node.childNodes));
462
						}
463
						break;
464
					default:
465
						bb.push(this.treeToBBCode(node.childNodes));
466
						break;
467
				}
468
			}
469
			break;
470
		case Node.DOCUMENT_NODE:// 9
471
		case Node.DOCUMENT_FRAGMENT_NODE:// 11
472
			bb.push(this.treeToBBCode(node.childNodes));
473
			break;
474
		case Node.TEXT_NODE:// 3
475
		case Node.CDATA_SECTION_NODE:// 4
476
			let text = node.nodeValue,
477
				codecheck = document.evaluate('ancestor::pre', node, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
0 ignored issues
show
Bug introduced by
The variable XPathResult seems to be never declared. If this is a global, consider adding a /** global: XPathResult */ 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...
478
479
			if (!codecheck)
480
			{
481
				text = text.replace(/\n[ \t]+/g, '\n');
482
			}
483
			bb.push(text);
484
			break;
485
	}
486
487
	return bb.join('').replace(/quote\]\n\n\[\//g, 'quote]\n[\/');
488
};
489
490
/**
491
 * Trim string by whitespace or specific characters
492
 *
493
 * @param {string} str
494
 * @param {string|null} charToReplace
495
 * @returns {string}
496
 */
497
Elk_QuickQuote.prototype.trim = function (str, charToReplace)
498
{
499
	if (charToReplace)
500
	{
501
		return String(str).replace(new RegExp('^[' + charToReplace + ']+|[' + charToReplace + ']+$', 'g'), '');
502
	}
503
504
	return str.trim();
505
};
506
507
/**
508
 * Returns smiley code
509
 *
510
 * @param {Object} img
511
 * @returns {string}
512
 */
513
Elk_QuickQuote.prototype.getSmileyCode = function (img)
514
{
515
	if (img.alt && img.className && img.classList.contains('smiley'))
516
	{
517
		// Alternative text corresponds to smiley and emoji code
518
		return img.alt;
519
	}
520
521
	return '';
522
};
523
524
/**
525
 * Called when the quick quote button is pressed, passed a PointerEvent
526
 *
527
 * @param {PointerEvent} event
528
 */
529
Elk_QuickQuote.prototype.executeQuickQuote = function (event)
530
{
531
	event.preventDefault();
532
	event.stopImmediatePropagation();
533
534
	let startTag = event.target.startTag,
535
		endTag = event.target.endTag;
536
537
	// isCollapsed is true for an empty selection
538
	let selection = window.getSelection().isCollapsed ? null : window.getSelection().getRangeAt(0);
539
540
	// Always clear out the button
541
	this.removeQuickQuote(event, true);
542
543
	if (selection)
544
	{
545
		let selectionAncestor = selection.commonAncestorContainer,
546
			selectionContents,
547
			postAncestor = document.evaluate('ancestor-or-self::section[contains(@class,"messageContent")]', selectionAncestor, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
0 ignored issues
show
Bug introduced by
The variable XPathResult seems to be never declared. If this is a global, consider adding a /** global: XPathResult */ 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...
548
549
		this.setHiddenFlag(selectionAncestor);
550
551
		if (selectionAncestor.nodeType !== 3 && selectionAncestor.nodeType !== 4)
552
		{
553
			// Most likely an element node
554
			selectionContents = selectionAncestor.cloneNode(false);
555
			selectionContents.appendChild(selection.cloneContents());
556
		}
557
		else
558
		{
559
			// Plain text
560
			selectionContents = selection.cloneContents();
561
		}
562
563
		if (postAncestor)
564
		{
565
			// Clone tree upwards. Some BBCode requires more context
566
			// than just the current node, like lists.
567
			let newSelectionContents;
568
			while (selectionAncestor !== postAncestor)
569
			{
570
				selectionAncestor = selectionAncestor.parentNode;
571
572
				// If in a blockquote, grab the cite details
573
				if (selectionAncestor.nodeName.toLowerCase() === 'blockquote' &&
574
					selectionAncestor.classList.contains('bbc_quote'))
575
				{
576
					this.handleQuote(selectionAncestor);
577
				}
578
579
				newSelectionContents = selectionAncestor.cloneNode(false);
580
581
				newSelectionContents.appendChild(selectionContents);
582
583
				selectionContents = newSelectionContents;
584
			}
585
		}
586
587
		let selectedText = this.trim(this.treeToBBCode(selectionContents));
588
589
		if (typeof oQuickReply === 'undefined' || oQuickReply.bIsFull)
0 ignored issues
show
Bug introduced by
The variable oQuickReply seems to be never declared. If this is a global, consider adding a /** global: oQuickReply */ 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...
590
		{
591
			// Full Editor
592
			let $editor = $editor_data[post_box_name],
0 ignored issues
show
Bug introduced by
The variable $editor_data seems to be never declared. If this is a global, consider adding a /** global: $editor_data */ 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...
Bug introduced by
The variable post_box_name seems to be never declared. If this is a global, consider adding a /** global: post_box_name */ 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...
593
				text = startTag + selectedText + endTag;
594
595
			// Add the text to the editor
596
			$editor.insert(this.trim(text));
597
598
			// In wizzy mode, we need to move the cursor out of the quote block
599
			let
600
				rangeHelper = $editor.getRangeHelper(),
601
				parent = rangeHelper.parentNode();
602
603
			if (parent && parent.nodeName === 'BLOCKQUOTE')
604
			{
605
				let range = rangeHelper.selectedRange();
606
607
				range.setStartAfter(parent);
608
				rangeHelper.selectRange(range);
609
			}
610
			else
611
			{
612
				$editor.insert('\n');
613
			}
614
		}
615
		else
616
		{
617
			// Just the textarea
618
			let textarea = document.querySelector('#postmodify').message,
619
				newText = (textarea.value ? textarea.value + '\n' : '') + startTag + selectedText + endTag + '\n';
620
621
			textarea.value = newText;
622
623
			// Reading again, to get normalized white-space
624
			newText = textarea.value;
625
			textarea.setSelectionRange(newText.length, newText.length);
626
627
			// Needed for Webkit/Blink
628
			textarea.blur();
629
			textarea.focus();
630
		}
631
632
		// Move to the editor
633
		if (typeof oQuickReply !== 'undefined')
634
		{
635
			document.getElementById(oQuickReply.opt.sJumpAnchor).scrollIntoView();
636
		}
637
		else
638
		{
639
			document.getElementById("editor_toolbar_container").scrollIntoView();
640
		}
641
	}
642
};
643
644
/**
645
 * Extracts the cite data and places them in the blockquote data- attributes
646
 *
647
 * @param {Element} selectionAncestor
648
 */
649
Elk_QuickQuote.prototype.handleQuote = function(selectionAncestor)
650
{
651
	let data_quoted = '',
652
		data_link = '',
653
		data_datetime = '';
654
655
	// The quoteheader
656
	let cite = selectionAncestor.previousSibling;
657
658
	// Extract the cite details
659
	if (cite.textContent.includes(':'))
660
	{
661
		data_quoted = cite.textContent.split(':')[1].trim();
662
	}
663
664
	// Check for &ndash; used to seperate name from date
665
	let separator = ' ' + String.fromCharCode(8211) + ' ';
666
	if (data_quoted.includes(separator))
667
	{
668
		data_quoted = data_quoted.split(separator)[0].trim();
669
	}
670
671
	if (cite.firstElementChild && cite.firstElementChild.hasAttribute('href'))
672
	{
673
		data_link = new URL(cite.firstElementChild.getAttribute('href')).search.substring(1);
0 ignored issues
show
Bug introduced by
The variable URL seems to be never declared. If this is a global, consider adding a /** global: URL */ 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...
674
	}
675
676
	if (cite.querySelector('time') !== null)
677
	{
678
		data_datetime = cite.querySelector('time').getAttribute('data-timestamp');
679
	}
680
681
	// Set what we found as data attributes of the blockquote
682
	selectionAncestor.setAttribute('data-quoted', data_quoted);
683
	selectionAncestor.setAttribute('data-link', data_link);
684
	selectionAncestor.setAttribute('data-datetime', data_datetime);
685
};
686
687
/**
688
 * Called when the user selects some text.  It prepares the Quick Quote Button
689
 * action
690
 *
691
 * @param {PointerEvent} event
692
 */
693
Elk_QuickQuote.prototype.prepareQuickQuoteButton = function (event)
694
{
695
	// The message that this event is attached to
696
	let postArea = event.currentTarget;
697
698
	// The link button to show, poster and time of post being quoted
699
	let msgid = parseInt(postArea.getAttribute('data-msgid')),
700
		link = document.getElementById('button_float_qq_' + msgid),
701
		username,
702
		time_unix;
703
704
	// If there is some text selected
705
	if (!window.getSelection().isCollapsed && msgid && link.classList.contains('hide'))
706
	{
707
		// Show and then position the button
708
		link.classList.remove('hide');
709
710
		// These are here to support old themes
711
		link.style.display = 'inline-block';
712
		link.style.position = 'absolute';
713
		link.style.zIndex = '10000';
714
715
		this.setButtonPosition(event, link);
716
717
		// Grab the name from the aside area
718
		username = (postArea.parentNode.previousElementSibling.querySelector('.name').textContent).trim();
719
		time_unix = postArea.parentNode.querySelector('time').getAttribute('data-forumtime');
720
721
		// Build the quick quote wrapper and set the button click event
722
		link.startTag = '[quote' +
723
			(username ? ' author=' + username : '') +
724
			((msgid && time_unix) ? ' link=msg=' + msgid + ' date=' + time_unix : '') + ']\n';
725
		link.endTag = '\n[/quote]';
726
727
		// Save the function pointers (due to bind) so we can remove the EventListeners
728
		this.execute = this.executeQuickQuote.bind(this);
729
		this.remove = this.removeQuickQuote.bind(this);
730
731
		// Button click
732
		link.addEventListener(this.mouseDown, this.execute, true);
733
734
		// Provide a way to escape should they click anywhere in the window
735
		document.addEventListener('click', this.remove, false);
736
	}
737
};
738
739
/**
740
 * Removes all QQ button click listeners and hides them all.
741
 *
742
 * @param {PointerEvent} event
743
 * @param {boolean} always
744
 */
745
Elk_QuickQuote.prototype.removeQuickQuote = function (event, always = false)
746
{
747
	event.stopImmediatePropagation();
748
749
	// Nothing selected, reset the UI and listeners
750
	if (window.getSelection().isCollapsed || always)
751
	{
752
		document.removeEventListener('click', this.remove, false);
753
754
		let topicContents = document.querySelectorAll('.messageContent'),
755
			link;
756
757
		// Reset the UI on de-selection
758
		topicContents.forEach((message) =>
759
		{
760
			link = message.parentElement.querySelector('.quick_quote_button');
761
			link.classList.add('hide');
762
			link.style.display = 'none';
763
			link.removeEventListener(this.mouseDown, this.execute, true);
764
		});
765
	}
766
};
767