Completed
Pull Request — master (#3325)
by Emanuele
11:19
created

mentioning.plugin.js ➔ suggest   F

Complexity

Conditions 25

Size

Total Lines 31
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 25
eloc 19
dl 0
loc 31
rs 0
c 0
b 0
f 0

How to fix   Complexity   

Complexity

Complex classes like mentioning.plugin.js ➔ suggest 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
 * @name      ElkArte Forum
3
 * @copyright ElkArte Forum contributors
4
 * @license   BSD http://opensource.org/licenses/BSD-3-Clause
5
 *
6
 * @version 1.1.6
7
 */
8
9
/**
10
 * This file contains javascript associated with the atwho function as it
11
 * relates to an sceditor invocation
12
 */
13
var disableDrafts = false;
14
15
(function($, window, document) {
0 ignored issues
show
Unused Code introduced by
The parameter document is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
16
	'use strict';
17
18
	// Editor instance
19
	var editor,
20
		rangeHelper;
21
22
	function elk_Mentions(options) {
23
		// All the passed options and defaults are loaded to the opts object
24
		this.opts = $.extend({}, this.defaults, options);
25
	}
26
27
	elk_Mentions.prototype.attachAtWho = function(oMentions, $element, oIframeWindow) {
28
		var mentioned = $('#mentioned');
29
30
		// Create / use a container to hold the results
31
		if (mentioned.length === 0)
32
			$('#' + oMentions.opts.editor_id).after(oMentions.opts._mentioned);
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...
33
		else
34
			oMentions.opts._mentioned = mentioned;
35
36
		oMentions.opts.cache.mentions = this.opts._mentioned;
37
38
		$element.atwho({
39
			at: "@",
40
			limit: 8,
41
			maxLen: 25,
42
			displayTpl: "<li data-value='${atwho-at}${name}' data-id='${id}'>${name}</li>",
43
			acceptSpaceBar: true,
44
			callbacks: {
45
				filter: function (query, items, search_key) {
0 ignored issues
show
Unused Code introduced by
The parameter search_key is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
Unused Code introduced by
The parameter items is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
46
					// Already cached this query, then use it
47
					if (typeof oMentions.opts.cache.names[query] !== 'undefined') {
48
						return oMentions.opts.cache.names[query];
49
					}
50
51
					return [];
52
				},
53
				// Well then lets make a find member suggest call
54
				remoteFilter: function(query, callback) {
55
					// Let be easy-ish on the server, don't go looking until we have at least two characters
56
					if (query.length < 2)
57
						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...
58
59
					// No slamming the server either
60
					var current_call = parseInt(new Date().getTime() / 1000);
61
					if (oMentions.opts._last_call !== 0 && oMentions.opts._last_call + 0.5 > current_call) {
62
						callback(oMentions.opts._names);
63
						return;
64
					}
65
66
					// What we want
67
					var obj = {
68
						"suggest_type": "member",
69
						"search": query.php_urlencode(),
70
						"time": current_call
71
					};
72
73
					// Make the request
74
					suggest(obj, function() {
75
						// Update the time gate
76
						oMentions.opts._last_call = current_call;
77
78
						// Update the cache with the values for reuse in local filter
79
						oMentions.opts.cache.names[query] = oMentions.opts._names;
80
81
						// Update the query cache for use in revalidateMentions
82
						oMentions.opts.cache.queries[oMentions.opts.cache.queries.length] = query;
83
84
						callback(oMentions.opts._names);
85
					});
86
				},
87
				beforeInsert: function(value, $li) {
88
					oMentions.addUID($li.data('id'), $li.data('value'));
89
90
					return value;
91
				},
92
				matcher: function(flag, subtext, should_startWithSpace, acceptSpaceBar) {
93
					var _a, _y, match, regexp, space;
94
95
					flag = flag.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
96
97
					if (should_startWithSpace) {
98
						flag = '(?:^|\\s)' + flag;
99
					}
100
101
					// Allow À - ÿ
102
					_a = decodeURI("%C3%80");
103
					_y = decodeURI("%C3%BF");
104
105
					// Allow first last name entry?
106
					space = acceptSpaceBar ? "\ " : "";
107
108
					// regexp = new RegExp(flag + '([^ <>&"\'=\\\\\n]*)$|' + flag + '([^\\x00-\\xff]*)$', 'gi');
109
					regexp = new RegExp(flag + "([A-Za-z" + _a + "-" + _y + "0-9_" + space + "\\[\\]\'\.\+\-]*)$|" + flag + "([^\\x00-\\xff]*)$", 'gi');
110
					match = regexp.exec(subtext);
111
112
					if (match) {
113
						return match[2] || match[1];
114
					}
115
					else {
0 ignored issues
show
Comprehensibility introduced by
else is not necessary here since all if branches return, consider removing it to reduce nesting and make code more readable.
Loading history...
116
						return null;
117
					}
118
				},
119 View Code Duplication
				highlighter: function(li, query) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
120
					var regexp;
121
122
					if (!query)
123
						return li;
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...
124
125
					// Preg Quote regexp from http://phpjs.org/functions/preg_quote/
126
					query = query.replace(new RegExp('[.\\\\+*?\\[\\^\\]$(){}=!<>|:\\-]', 'g'), '\\$&');
127
128
					regexp = new RegExp(">\\s*(\\w*)(" + query.replace("+", "\\+") + ")(\\w*)\\s*<", 'ig');
129
					return li.replace(regexp, function(str, $1, $2, $3) {
130
						return '> ' + $1 + '<strong>' + $2 + '</strong>' + $3 + ' <';
131
					});
132
				},
133
				beforeReposition: function (offset) {
134
					// We only need to adjust when in wysiwyg
135
					if (editor.inSourceMode())
136
						return offset;
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...
137
138
					// Lets get the caret position so we can add the mentions box there
139
					var corrected_offset = findAtPosition();
140
141
					offset.top = corrected_offset.top;
142
					offset.left = corrected_offset.left;
143
144
					return offset;
145
				}
146
			}
147
		});
148
149
		// Use atwho selection box show/hide events to prevent autosave from firing
150
		$(oIframeWindow).on("shown.atwho", function(event, offset) {
0 ignored issues
show
Unused Code introduced by
The parameter event is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
Unused Code introduced by
The parameter offset is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
151
			disableDrafts = true;
152
		});
153
154
		$(oIframeWindow).on("hidden.atwho", function(event, offset) {
0 ignored issues
show
Unused Code introduced by
The parameter offset is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
Unused Code introduced by
The parameter event is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
155
			disableDrafts = false;
156
		});
157
158
		/**
159
		 * Makes the ajax call for data, returns to callback function when done.
160
		 *
161
		 * @param obj values to pass to action suggest
162
		 * @param callback function to call when we have completed our call
163
		 */
164
		function suggest(obj, callback)
165
		{
166
			var postString = "jsonString=" + JSON.stringify(obj) + "&" + elk_session_var + "=" + elk_session_id;
0 ignored issues
show
Bug introduced by
The variable elk_session_var seems to be never declared. If this is a global, consider adding a /** global: elk_session_var */ 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 elk_session_id seems to be never declared. If this is a global, consider adding a /** global: elk_session_id */ 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...
167
168
			oMentions.opts._names = [];
169
170
			$.ajax({
171
				url: elk_scripturl + "?action=suggest;xml",
0 ignored issues
show
Bug introduced by
The variable elk_scripturl seems to be never declared. If this is a global, consider adding a /** global: elk_scripturl */ 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...
172
				type: "post",
173
				data: postString,
174
				dataType: "xml"
175
			})
176
			.done(function(data) {
177
				$(data).find('item').each(function (idx, item) {
178
					oMentions.opts._names[idx] = {
179
						"id": $(item).attr('id'),
180
						"name": $(item).text()
181
					};
182
				});
183
184
				callback();
185
			})
186
			.fail(function(jqXHR, textStatus, errorThrown) {
187
				if ('console' in window) {
188
					window.console.info('Error:', textStatus, errorThrown.name);
189
					window.console.info(jqXHR.responseText);
190
				}
191
192
				callback();
193
			});
194
		}
195
196
		/**
197
		 * Determine the caret position inside of sceditor's iframe
198
		 *
199
		 * What it does:
200
		 * - Caret.js does not seem to return the correct position for (FF & IE) when
201
		 * the iframe has vertically scrolled.
202
		 * - This is an sceditor specific function to return a screen caret position
203
		 * - Called just before At.js adds the mentions dropdown box
204
		 * - Finds the @mentions tag and adds an invisible zero width space before it
205
		 * - Gets the location offset() in the iframe "window" of the added space
206
		 * - Adjusts for the iframe scroll
207
		 * - Adds in the iframe container location offset() to main window
208
		 * - Removes the space, restores the editor range.
209
		 *
210
		 * @returns {{}}
211
		 */
212
		function findAtPosition() {
213
			// Get sceditor's RangeHelper for use
214
			rangeHelper = editor.getRangeHelper();
215
216
			// Save the current state
217
			rangeHelper.saveRange();
218
219
			var start = rangeHelper.getMarker('sceditor-start-marker'),
220
				parent = start.parentNode,
221
				prev = start.previousSibling,
222
				offset = {},
223
				atPos,
224
				placefinder;
225
226
			// Create a placefinder span containing a 'ZERO WIDTH SPACE' Character
227
			placefinder = start.ownerDocument.createElement('span');
228
			$(placefinder).text("200B").addClass('placefinder');
229
230
			// Look back and find the mentions @ tag, so we can insert our span ahead of it
231
			while (prev) {
232
				atPos = (prev.nodeValue || '').lastIndexOf('@');
233
234
				// Found the start of @mention
235
				if (atPos > -1) {
236
					parent.insertBefore(placefinder, prev.splitText(atPos + 1));
237
					break;
238
				}
239
240
				prev = prev.previousSibling;
241
			}
242
243
			// If we were successful in adding the placefinder
244
			if (placefinder.parentNode) {
245
				var $_placefinder = $(placefinder);
246
247
				// offset() returns the top offset inside the total iframe, so we need the vertical scroll
248
				// value to adjust back to main window position
249
				//	wizzy_height = $('#' + oMentions.opts.editor_id).parent().find('iframe').height(),
250
				//	wizzy_window = $('#' + oMentions.opts.editor_id).parent().find('iframe').contents().height(),
251
				var	wizzy_scroll = $('#' + oMentions.opts.editor_id).parent().find('iframe').contents().scrollTop();
252
253
				// Determine its Location in the iframe
254
				offset = $_placefinder.offset();
255
256
				// If we have scrolled, then we also need to account for those offsets
257
				offset.top -= wizzy_scroll;
258
				offset.top += $_placefinder.height();
259
260
				// Remove our placefinder
261
				$_placefinder.remove();
262
			}
263
264
			// Put things back just like we found them
265
			rangeHelper.restoreRange();
266
267
			// Add in the iframe's offset to get the final location.
268
			if (offset) {
269
				var iframeOffset = editor.getContentAreaContainer().offset();
270
271
				// Some fudge for the kids
272
				offset.top += iframeOffset.top + 5;
273
				offset.left += iframeOffset.left + 5;
274
			}
275
276
			return offset;
277
		}
278
	};
279
280
	elk_Mentions.prototype.addUID = function(user_id, name) {
281
		this.opts._mentioned.append($('<input type="hidden" name="uid[]" />').val(user_id).attr('data-name', name));
282
	};
283
284
	/**
285
	 * Private mention vars
286
	 */
287
	elk_Mentions.prototype.defaults = {
288
		_names: [],
289
		_last_call: 0,
290
		_mentioned: $('<div id="mentioned" style="display: none;" />')
291
	};
292
293
	/**
294
	 * Holds all current mention (defaults + passed options)
295
	 */
296
	elk_Mentions.prototype.opts = {};
297
298
	/**
299
	 * Mentioning plugin interface to SCEditor
300
	 *  - Called from the editor as a plugin
301
	 *  - Monitors events so we control the elk_mention
302
	 */
303
	$.sceditor.plugins.mention = function() {
304
		var base = this,
305
			oMentions;
306
307
		base.init = function() {
308
			// Grab this instance for use use in oMentions
309
			editor = this;
310
		};
311
312
		/**
313
		 * Initialize, called when sceditor starts and initializes plugins
314
		 */
315
		base.signalReady = function() {
316
			// Init the mention instance, load in the options
317
			oMentions = new elk_Mentions(this.opts.mentionOptions);
0 ignored issues
show
Coding Style Best Practice introduced by
By convention, constructors like elk_Mentions should be capitalized.
Loading history...
318
319
			var $option_eid = $('#' + oMentions.opts.editor_id);
320
321
			// Adds the selector to the list of known "mentioner"
322
			add_elk_mention(oMentions.opts.editor_id, {isPlugin: true});
323
			oMentions.attachAtWho(oMentions, $option_eid.parent().find('textarea'));
324
325
			// Using wysiwyg, then lets attach atwho to it
326
			var instance = $option_eid.sceditor('instance');
327
			if (!instance.opts.runWithoutWysiwygSupport)
328
			{
329
				// We need to monitor the iframe window and body to text input
330
				var oIframe = $option_eid.parent().find('iframe')[0],
331
					oIframeWindow = oIframe.contentWindow,
332
					oIframeBody = $(oIframe.contentDocument.body);
333
334
					oMentions.attachAtWho(oMentions, oIframeBody, oIframeWindow);
335
			}
336
		};
337
	};
338
})(jQuery, window, document);