PageSourceSearch

https://webapppros.com/myhome/forms/assets/javascript/core.js?assets_version=2

js webapppros.com collected 2026-09-24 14:20:13 UTC 52,197 bytes, 1,863 lines download raw bytes

1/* global bbfontstyle */
2
3var phpbb = {};
4phpbb.alertTime = 100;
5
6(function($) {  // Avoid conflicts with other libraries
7
8'use strict';
9
10// define a couple constants for keydown functions.
11var keymap = {
12	TAB: 9,
13	ENTER: 13,
14	ESC: 27,
15	ARROW_UP: 38,
16	ARROW_DOWN: 40
17};
18
19var $dark = $('#darkenwrapper');
20var $loadingIndicator;
21var phpbbAlertTimer = null;
22
23phpbb.isTouch = (window && typeof window.ontouchstart !== 'undefined');
24
25// Add ajax pre-filter to prevent cross-domain script execution
26$.ajaxPrefilter(function(s) {
27	if (s.crossDomain) {
28		s.contents.script = false;
29	}
30});
31
32/**
33 * Display a loading screen
34 *
35 * @returns {object} Returns loadingIndicator.
36 */
37phpbb.loadingIndicator = function() {
38	if (!$loadingIndicator) {
39		$loadingIndicator = $('<div />', {
40			'id': 'loading_indicator',
41			'class': 'loading_indicator'
42		});
43		$loadingIndicator.appendTo('#page-footer');
44	}
45
46	if (!$loadingIndicator.is(':visible')) {
47		$loadingIndicator.fadeIn(phpbb.alertTime);
48		// Wait 60 seconds and display an error if nothing has been returned by then.
49		phpbb.clearLoadingTimeout();
50		phpbbAlertTimer = setTimeout(function() {
51			phpbb.showTimeoutMessage();
52		}, 60000);
53	}
54
55	return $loadingIndicator;
56};
57
58/**
59 * Show timeout message
60 */
61phpbb.showTimeoutMessage = function () {
62	var $alert = $('#phpbb_alert');
63
64	if ($loadingIndicator.is(':visible')) {
65		phpbb.alert($alert.attr('data-l-err'), $alert.attr('data-l-timeout-processing-req'));
66	}
67};
68
69/**
70 * Clear loading alert timeout
71*/
72phpbb.clearLoadingTimeout = function() {
73	if (phpbbAlertTimer !== null) {
74		clearTimeout(phpbbAlertTimer);
75		phpbbAlertTimer = null;
76	}
77};
78
79
80/**
81* Close popup alert after a specified delay
82*
83* @param {int} delay Delay in ms until darkenwrapper's click event is triggered
84*/
85phpbb.closeDarkenWrapper = function(delay) {
86	phpbbAlertTimer = setTimeout(function() {
87		$('#darkenwrapper').trigger('click');
88	}, delay);
89};
90
91/**
92 * Display a simple alert similar to JSs native alert().
93 *
94 * You can only call one alert or confirm box at any one time.
95 *
96 * @param {string} title Title of the message, eg "Information" (HTML).
97 * @param {string} msg Message to display (HTML).
98 *
99 * @returns {object} Returns the div created.
100 */
101phpbb.alert = function(title, msg) {
102	var $alert = $('#phpbb_alert');
103	$alert.find('.alert_title').html(title);
104	$alert.find('.alert_text').html(msg);
105
106	$(document).on('keydown.phpbb.alert', function(e) {
107		if (e.keyCode === keymap.ENTER || e.keyCode === keymap.ESC) {
108			phpbb.alert.close($alert, true);
109			e.preventDefault();
110			e.stopPropagation();
111		}
112	});
113	phpbb.alert.open($alert);
114
115	return $alert;
116};
117
118/**
119* Handler for opening an alert box.
120*
121* @param {jQuery} $alert jQuery object.
122*/
123phpbb.alert.open = function($alert) {
124	if (!$dark.is(':visible')) {
125		$dark.fadeIn(phpbb.alertTime);
126	}
127
128	if ($loadingIndicator && $loadingIndicator.is(':visible')) {
129		$loadingIndicator.fadeOut(phpbb.alertTime, function() {
130			$dark.append($alert);
131			$alert.fadeIn(phpbb.alertTime);
132		});
133	} else if ($dark.is(':visible')) {
134		$dark.append($alert);
135		$alert.fadeIn(phpbb.alertTime);
136	} else {
137		$dark.append($alert);
138		$alert.show();
139		$dark.fadeIn(phpbb.alertTime);
140	}
141
142	$alert.on('click', function(e) {
143		e.stopPropagation();
144	});
145
146	$dark.one('click', function(e) {
147		phpbb.alert.close($alert, true);
148		e.preventDefault();
149		e.stopPropagation();
150	});
151
152	$alert.find('.alert_close').one('click', function(e) {
153		phpbb.alert.close($alert, true);
154		e.preventDefault();
155	});
156};
157
158/**
159* Handler for closing an alert box.
160*
161* @param {jQuery} $alert jQuery object.
162* @param {bool} fadedark Whether to remove dark background.
163*/
164phpbb.alert.close = function($alert, fadedark) {
165	var $fade = (fadedark) ? $dark : $alert;
166
167	$fade.fadeOut(phpbb.alertTime, function() {
168		$alert.hide();
169	});
170
171	$alert.find('.alert_close').off('click');
172	$(document).off('keydown.phpbb.alert');
173};
174
175/**
176 * Display a simple yes / no box to the user.
177 *
178 * You can only call one alert or confirm box at any one time.
179 *
180 * @param {string} msg Message to display (HTML).
181 * @param {function} callback Callback. Bool param, whether the user pressed
182 *     yes or no (or whatever their language is).
183 * @param {bool} fadedark Remove the dark background when done? Defaults
184 *     to yes.
185 *
186 * @returns {object} Returns the div created.
187 */
188phpbb.confirm = function(msg, callback, fadedark) {
189	var $confirmDiv = $('#phpbb_confirm');
190	$confirmDiv.find('.alert_text').html(msg);
191	fadedark = typeof fadedark !== 'undefined' ? fadedark : true;
192
193	$(document).on('keydown.phpbb.alert', function(e) {
194		if (e.keyCode === keymap.ENTER || e.keyCode === keymap.ESC) {
195			var name = (e.keyCode === keymap.ENTER) ? 'confirm' : 'cancel';
196
197			$('input[name="' + name + '"]').trigger('click');
198			e.preventDefault();
199			e.stopPropagation();
200		}
201	});
202
203	$confirmDiv.find('input[type="button"]').one('click.phpbb.confirmbox', function(e) {
204		var confirmed = this.name === 'confirm';
205
206		callback(confirmed);
207		$confirmDiv.find('input[type="button"]').off('click.phpbb.confirmbox');
208		phpbb.alert.close($confirmDiv, fadedark || !confirmed);
209
210		e.preventDefault();
211		e.stopPropagation();
212	});
213
214	phpbb.alert.open($confirmDiv);
215
216	return $confirmDiv;
217};
218
219/**
220 * Turn a querystring into an array.
221 *
222 * @argument {string} string The querystring to parse.
223 * @returns {object} The object created.
224 */
225phpbb.parseQuerystring = function(string) {
226	var params = {}, i, split;
227
228	string = string.split('&');
229	for (i = 0; i < string.length; i++) {
230		split = string[i].split('=');
231		params[split[0]] = decodeURIComponent(split[1]);
232	}
233	return params;
234};
235
236
237/**
238 * Makes a link use AJAX instead of loading an entire page.
239 *
240 * This function will work for links (both standard links and links which
241 * invoke confirm_box) and forms. It will be called automatically for links
242 * and forms with the data-ajax attribute set, and will call the necessary
243 * callback.
244 *
245 * For more info, view the following page on the phpBB wiki:
246 * http://wiki.phpbb.com/JavaScript_Function.phpbb.ajaxify
247 *
248 * @param {object} options Options.
249 */
250phpbb.ajaxify = function(options) {
251	var $elements = $(options.selector),
252		refresh = options.refresh,
253		callback = options.callback,
254		overlay = (typeof options.overlay !== 'undefined') ? options.overlay : true,
255		isForm = $elements.is('form'),
256		isText = $elements.is('input[type="text"], textarea'),
257		eventName;
258
259	if (isForm) {
260		eventName = 'submit';
261	} else if (isText) {
262		eventName = 'keyup';
263	} else {
264		eventName = 'click';
265	}
266
267	$elements.on(eventName, function(event) {
268		var action, method, data, submit, that = this, $this = $(this);
269
270		if ($this.find('input[type="submit"][data-clicked]').attr('data-ajax') === 'false') {
271			return;
272		}
273
274		/**
275		 * Handler for AJAX errors
276		 */
277		function errorHandler(jqXHR, textStatus, errorThrown) {
278			if (typeof console !== 'undefined' && console.log) {
279				console.log('AJAX error. status: ' + textStatus + ', message: ' + errorThrown);
280			}
281			phpbb.clearLoadingTimeout();
282			var responseText, errorText = false;
283			try {
284				responseText = JSON.parse(jqXHR.responseText);
285				responseText = responseText.message;
286			} catch (e) {}
287			if (typeof responseText === 'string' && responseText.length > 0) {
288				errorText = responseText;
289			} else if (typeof errorThrown === 'string' && errorThrown.length > 0) {
290				errorText = errorThrown;
291			} else {
292				errorText = $dark.attr('data-ajax-error-text-' + textStatus);
293				if (typeof errorText !== 'string' || !errorText.length) {
294					errorText = $dark.attr('data-ajax-error-text');
295				}
296			}
297			phpbb.alert($dark.attr('data-ajax-error-title'), errorText);
298		}
299
300		/**
301		 * This is a private function used to handle the callbacks, refreshes
302		 * and alert. It calls the callback, refreshes the page if necessary, and
303		 * displays an alert to the user and removes it after an amount of time.
304		 *
305		 * It cannot be called from outside this function, and is purely here to
306		 * avoid repetition of code.
307		 *
308		 * @param {object} res The object sent back by the server.
309		 */
310		function returnHandler(res) {
311			var alert;
312
313			phpbb.clearLoadingTimeout();
314
315			// Is a confirmation required?
316			if (typeof res.S_CONFIRM_ACTION === 'undefined') {
317				// If a confirmation is not required, display an alert and call the
318				// callbacks.
319				if (typeof res.MESSAGE_TITLE !== 'undefined') {
320					alert = phpbb.alert(res.MESSAGE_TITLE, res.MESSAGE_TEXT);
321				} else {
322					$dark.fadeOut(phpbb.alertTime);
323
324					if ($loadingIndicator) {
325						$loadingIndicator.fadeOut(phpbb.alertTime);
326					}
327				}
328
329				if (typeof phpbb.ajaxCallbacks[callback] === 'function') {
330					phpbb.ajaxCallbacks[callback].call(that, res);
331				}
332
333				// If the server says to refresh the page, check whether the page should
334				// be refreshed and refresh page after specified time if required.
335				if (res.REFRESH_DATA) {
336					if (typeof refresh === 'function') {
337						refresh = refresh(res.REFRESH_DATA.url);
338					} else if (typeof refresh !== 'boolean') {
339						refresh = false;
340					}
341
342					phpbbAlertTimer = setTimeout(function() {
343						if (refresh) {
344							window.location = res.REFRESH_DATA.url;
345						}
346
347						// Hide the alert even if we refresh the page, in case the user
348						// presses the back button.
349						$dark.fadeOut(phpbb.alertTime, function() {
350							if (typeof alert !== 'undefined') {
351								alert.hide();
352							}
353						});
354					}, res.REFRESH_DATA.time * 1000); // Server specifies time in seconds
355				}
356			} else {
357				// If confirmation is required, display a dialog to the user.
358				phpbb.confirm(res.MESSAGE_BODY, function(del) {
359					if (!del) {
360						return;
361					}
362
363					phpbb.loadingIndicator();
364					data =  $('<form>' + res.S_HIDDEN_FIELDS + '</form>').serialize();
365					$.ajax({
366						url: res.S_CONFIRM_ACTION,
367						type: 'POST',
368						data: data + '&confirm=' + res.YES_VALUE + '&' + $('form', '#phpbb_confirm').serialize(),
369						success: returnHandler,
370						error: errorHandler
371					});
372				}, false);
373			}
374		}
375
376		// If the element is a form, POST must be used and some extra data must
377		// be taken from the form.
378		var runFilter = (typeof options.filter === 'function');
379		data = {};
380
381		if (isForm) {
382			action = $this.attr('action').replace('&amp;', '&');
383			data = $this.serializeArray();
384			method = $this.attr('method') || 'GET';
385
386			if ($this.find('input[type="submit"][data-clicked]')) {
387				submit = $this.find('input[type="submit"][data-clicked]');
388				data.push({
389					name: submit.attr('name'),
390					value: submit.val()
391				});
392			}
393		} else if (isText) {
394			var name = $this.attr('data-name') || this.name;
395			action = $this.attr('data-url').replace('&amp;', '&');
396			data[name] = this.value;
397			method = 'POST';
398		} else {
399			action = this.href;
400			data = null;
401			method = 'GET';
402		}
403
404		var sendRequest = function() {
405			var dataOverlay = $this.attr('data-overlay');
406			if (overlay && (typeof dataOverlay === 'undefined' || dataOverlay === 'true')) {
407				phpbb.loadingIndicator();
408			}
409
410			var request = $.ajax({
411				url: action,
412				type: method,
413				data: data,
414				success: returnHandler,
415				error: errorHandler,
416				cache: false
417			});
418
419			request.always(function() {
420				if ($loadingIndicator && $loadingIndicator.is(':visible')) {
421					$loadingIndicator.fadeOut(phpbb.alertTime);
422				}
423			});
424		};
425
426		// If filter function returns false, cancel the AJAX functionality,
427		// and return true (meaning that the HTTP request will be sent normally).
428		if (runFilter && !options.filter.call(this, data, event, sendRequest)) {
429			return;
430		}
431
432		sendRequest();
433		event.preventDefault();
434	});
435
436	if (isForm) {
437		$elements.find('input:submit').click(function () {
438			var $this = $(this);
439
440			// Remove data-clicked attribute from any submit button of form
441			$this.parents('form:first').find('input:submit[data-clicked]').removeAttr('data-clicked');
442
443			$this.attr('data-clicked', 'true');
444		});
445	}
446
447	return this;
448};
449
450phpbb.search = {
451	cache: {
452		data: []
453	},
454	tpl: [],
455	container: []
456};
457
458/**
459 * Get cached search data.
460 *
461 * @param {string} id Search ID.
462 * @returns {bool|object} Cached data object. Returns false if no data exists.
463 */
464phpbb.search.cache.get = function(id) {
465	if (this.data[id]) {
466		return this.data[id];
467	}
468	return false;
469};
470
471/**
472 * Set search cache data value.
473 *
474 * @param {string} id		Search ID.
475 * @param {string} key		Data key.
476 * @param {string} value	Data value.
477 */
478phpbb.search.cache.set = function(id, key, value) {
479	if (!this.data[id]) {
480		this.data[id] = { results: [] };
481	}
482	this.data[id][key] = value;
483};
484
485/**
486 * Cache search result.
487 *
488 * @param {string} id		Search ID.
489 * @param {string} keyword	Keyword.
490 * @param {Array} results	Search results.
491 */
492phpbb.search.cache.setResults = function(id, keyword, results) {
493	this.data[id].results[keyword] = results;
494};
495
496/**
497 * Trim spaces from keyword and lower its case.
498 *
499 * @param {string} keyword Search keyword to clean.
500 * @returns {string} Cleaned string.
501 */
502phpbb.search.cleanKeyword = function(keyword) {
503	return $.trim(keyword).toLowerCase();
504};
505
506/**
507 * Get clean version of search keyword. If textarea supports several keywords
508 * (one per line), it fetches the current keyword based on the caret position.
509 *
510 * @param {jQuery} $input	Search input|textarea.
511 * @param {string} keyword	Input|textarea value.
512 * @param {bool} multiline	Whether textarea supports multiple search keywords.
513 *
514 * @returns string Clean string.
515 */
516phpbb.search.getKeyword = function($input, keyword, multiline) {
517	if (multiline) {
518		var line = phpbb.search.getKeywordLine($input);
519		keyword = keyword.split('\n').splice(line, 1);
520	}
521	return phpbb.search.cleanKeyword(keyword);
522};
523
524/**
525 * Get the textarea line number on which the keyword resides - for textareas
526 * that support multiple keywords (one per line).
527 *
528 * @param {jQuery} $textarea Search textarea.
529 * @returns {int} The line number.
530 */
531phpbb.search.getKeywordLine = function ($textarea) {
532	var selectionStart = $textarea.get(0).selectionStart;
533	return $textarea.val().substr(0, selectionStart).split('\n').length - 1;
534};
535
536/**
537 * Set the value on the input|textarea. If textarea supports multiple
538 * keywords, only the active keyword is replaced.
539 *
540 * @param {jQuery} $input	Search input|textarea.
541 * @param {string} value	Value to set.
542 * @param {bool} multiline	Whether textarea supports multiple search keywords.
543 */
544phpbb.search.setValue = function($input, value, multiline) {
545	if (multiline) {
546		var line = phpbb.search.getKeywordLine($input),
547			lines = $input.val().split('\n');
548		lines[line] = value;
549		value = lines.join('\n');
550	}
551	$input.val(value);
552};
553
554/**
555 * Sets the onclick event to set the value on the input|textarea to the
556 * selected search result.
557 *
558 * @param {jQuery} $input		Search input|textarea.
559 * @param {object} value		Result object.
560 * @param {jQuery} $row			Result element.
561 * @param {jQuery} $container	jQuery object for the search container.
562 */
563phpbb.search.setValueOnClick = function($input, value, $row, $container) {
564	$row.click(function() {
565		phpbb.search.setValue($input, value.result, $input.attr('data-multiline'));
566		phpbb.search.closeResults($input, $container);
567	});
568};
569
570/**
571 * Runs before the AJAX search request is sent and determines whether
572 * there is a need to contact the server. If there are cached results
573 * already, those are displayed instead. Executes the AJAX request function
574 * itself due to the need to use a timeout to limit the number of requests.
575 *
576 * @param {Array} data				Data to be sent to the server.
577 * @param {object} event			Onkeyup event object.
578 * @param {function} sendRequest	Function to execute AJAX request.
579 *
580 * @returns {boolean} Returns false.
581 */
582phpbb.search.filter = function(data, event, sendRequest) {
583	var $this = $(this),
584		dataName = ($this.attr('data-name') !== undefined) ? $this.attr('data-name') : $this.attr('name'),
585		minLength = parseInt($this.attr('data-min-length'), 10),
586		searchID = $this.attr('data-results'),
587		keyword = phpbb.search.getKeyword($this, data[dataName], $this.attr('data-multiline')),
588		cache = phpbb.search.cache.get(searchID),
589		key = event.keyCode || event.which,
590		proceed = true;
591	data[dataName] = keyword;
592
593	// No need to search if enter was pressed
594	// for selecting a value from the results.
595	if (key === keymap.ENTER) {
596		return false;
597	}
598
599	if (cache.timeout) {
600		clearTimeout(cache.timeout);
601	}
602
603	var timeout = setTimeout(function() {
604		// Check min length and existence of cache.
605		if (minLength > keyword.length) {
606			proceed = false;
607		} else if (cache.lastSearch) {
608			// Has the keyword actually changed?
609			if (cache.lastSearch === keyword) {
610				proceed = false;
611			} else {
612				// Do we already have results for this?
613				if (cache.results[keyword]) {
614					var response = {
615						keyword: keyword,
616						results: cache.results[keyword]
617					};
618					phpbb.search.handleResponse(response, $this, true);
619					proceed = false;
620				}
621
622				// If the previous search didn't yield results and the string only had characters added to it,
623				// then we won't bother sending a request.
624				if (keyword.indexOf(cache.lastSearch) === 0 && cache.results[cache.lastSearch].length === 0) {
625					phpbb.search.cache.set(searchID, 'lastSearch', keyword);
626					phpbb.search.cache.setResults(searchID, keyword, []);
627					proceed = false;
628				}
629			}
630		}
631
632		if (proceed) {
633			sendRequest.call(this);
634		}
635	}, 350);
636	phpbb.search.cache.set(searchID, 'timeout', timeout);
637
638	return false;
639};
640
641/**
642 * Handle search result response.
643 *
644 * @param {object} res			Data received from server.
645 * @param {jQuery} $input		Search input|textarea.
646 * @param {bool} fromCache		Whether the results are from the cache.
647 * @param {function} callback	Optional callback to run when assigning each search result.
648 */
649phpbb.search.handleResponse = function(res, $input, fromCache, callback) {
650	if (typeof res !== 'object') {
651		return;
652	}
653
654	var searchID = $input.attr('data-results'),
655		$container = $(searchID);
656
657	if (this.cache.get(searchID).callback) {
658		callback = this.cache.get(searchID).callback;
659	} else if (typeof callback === 'function') {
660		this.cache.set(searchID, 'callback', callback);
661	}
662
663	if (!fromCache) {
664		this.cache.setResults(searchID, res.keyword, res.results);
665	}
666
667	this.cache.set(searchID, 'lastSearch', res.keyword);
668	this.showResults(res.results, $input, $container, callback);
669};
670
671/**
672 * Show search results.
673 *
674 * @param {Array} results		Search results.
675 * @param {jQuery} $input		Search input|textarea.
676 * @param {jQuery} $container	Search results container element.
677 * @param {function} callback	Optional callback to run when assigning each search result.
678 */
679phpbb.search.showResults = function(results, $input, $container, callback) {
680	var $resultContainer = $('.search-results', $container);
681	this.clearResults($resultContainer);
682
683	if (!results.length) {
684		$container.hide();
685		return;
686	}
687
688	var searchID = $container.attr('id'),
689		tpl,
690		row;
691
692	if (!this.tpl[searchID]) {
693		tpl = $('.search-result-tpl', $container);
694		this.tpl[searchID] = tpl.clone().removeClass('search-result-tpl');
695		tpl.remove();
696	}
697	tpl = this.tpl[searchID];
698
699	$.each(results, function(i, item) {
700		row = tpl.clone();
701		row.find('.search-result').html(item.display);
702
703		if (typeof callback === 'function') {
704			callback.call(this, $input, item, row, $container);
705		}
706		row.appendTo($resultContainer).show();
707	});
708	$container.show();
709
710	phpbb.search.navigateResults($input, $container, $resultContainer);
711};
712
713/**
714 * Clear search results.
715 *
716 * @param {jQuery} $container		Search results container.
717 */
718phpbb.search.clearResults = function($container) {
719	$container.children(':not(.search-result-tpl)').remove();
720};
721
722/**
723 * Close search results.
724 *
725 * @param {jQuery} $input			Search input|textarea.
726 * @param {jQuery} $container		Search results container.
727 */
728phpbb.search.closeResults = function($input, $container) {
729	$input.off('.phpbb.search');
730	$container.hide();
731};
732
733/**
734 * Navigate search results.
735 *
736 * @param {jQuery} $input			Search input|textarea.
737 * @param {jQuery} $container		Search results container.
738 * @param {jQuery} $resultContainer	Search results list container.
739 */
740phpbb.search.navigateResults = function($input, $container, $resultContainer) {
741	// Add a namespace to the event (.phpbb.search),
742	// so it can be unbound specifically later on.
743	// Rebind it, to ensure the event is 'dynamic'.
744	$input.off('.phpbb.search');
745	$input.on('keydown.phpbb.search', function(event) {
746		var key = event.keyCode || event.which,
747			$active = $resultContainer.children('.active');
748
749		switch (key) {
750			// Close the results
751			case keymap.ESC:
752				phpbb.search.closeResults($input, $container);
753			break;
754
755			// Set the value for the selected result
756			case keymap.ENTER:
757				if ($active.length) {
758					var value = $active.find('.search-result > span').text();
759
760					phpbb.search.setValue($input, value, $input.attr('data-multiline'));
761				}
762
763				phpbb.search.closeResults($input, $container);
764
765				// Do not submit the form
766				event.preventDefault();
767			break;
768
769			// Navigate the results
770			case keymap.ARROW_DOWN:
771			case keymap.ARROW_UP:
772				var up = key === keymap.ARROW_UP,
773					$children = $resultContainer.children();
774
775				if (!$active.length) {
776					if (up) {
777						$children.last().addClass('active');
778					} else {
779						$children.first().addClass('active');
780					}
781				} else if ($children.length > 1) {
782					if (up) {
783						if ($active.is(':first-child')) {
784							$children.last().addClass('active');
785						} else {
786							$active.prev().addClass('active');
787						}
788					} else {
789						if ($active.is(':last-child')) {
790							$children.first().addClass('active');
791						} else {
792							$active.next().addClass('active');
793						}
794					}
795
796					$active.removeClass('active');
797				}
798
799				// Do not change cursor position in the input element
800				event.preventDefault();
801			break;
802		}
803	});
804};
805
806$('#phpbb').click(function() {
807	var $this = $(this);
808
809	if (!$this.is('.live-search') && !$this.parents().is('.live-search')) {
810		phpbb.search.closeResults($('input, textarea'), $('.live-search'));
811	}
812});
813
814phpbb.history = {};
815
816/**
817* Check whether a method in the native history object is supported.
818*
819* @param {string} fn Method name.
820* @returns {bool} Returns true if the method is supported.
821*/
822phpbb.history.isSupported = function(fn) {
823	return !(typeof history === 'undefined' || typeof history[fn] === 'undefined');
824};
825
826/**
827* Wrapper for the pushState and replaceState methods of the
828* native history object.
829*
830* @param {string} mode		Mode. Either push or replace.
831* @param {string} url		New URL.
832* @param {string} [title]	Optional page title.
833* @param {object} [obj]		Optional state object.
834*/
835phpbb.history.alterUrl = function(mode, url, title, obj) {
836	var fn = mode + 'State';
837
838	if (!url || !phpbb.history.isSupported(fn)) {
839		return;
840	}
841	if (!title) {
842		title = document.title;
843	}
844	if (!obj) {
845		obj = null;
846	}
847
848	history[fn](obj, title, url);
849};
850
851/**
852* Wrapper for the native history.replaceState method.
853*
854* @param {string} url		New URL.
855* @param {string} [title]	Optional page title.
856* @param {object} [obj]		Optional state object.
857*/
858phpbb.history.replaceUrl = function(url, title, obj) {
859	phpbb.history.alterUrl('replace', url, title, obj);
860};
861
862/**
863* Wrapper for the native history.pushState method.
864*
865* @param {string} url		New URL.
866* @param {string} [title]	Optional page title.
867* @param {object} [obj]		Optional state object.
868*/
869phpbb.history.pushUrl = function(url, title, obj) {
870	phpbb.history.alterUrl('push', url, title, obj);
871};
872
873/**
874* Hide the optgroups that are not the selected timezone
875*
876* @param {bool} keepSelection Shall we keep the value selected, or shall the
877* 	user be forced to repick one.
878*/
879phpbb.timezoneSwitchDate = function(keepSelection) {
880	var $timezoneCopy = $('#timezone_copy');
881	var $timezone = $('#timezone');
882	var $tzDate = $('#tz_date');
883	var $tzSelectDateSuggest = $('#tz_select_date_suggest');
884
885	if ($timezoneCopy.length === 0) {
886		// We make a backup of the original dropdown, so we can remove optgroups
887		// instead of setting display to none, because IE and chrome will not
888		// hide options inside of optgroups and selects via css
889		$timezone.clone()
890			.attr('id', 'timezone_copy')
891			.css('display', 'none')
892			.attr('name', 'tz_copy')
893			.insertAfter('#timezone');
894	} else {
895		// Copy the content of our backup, so we can remove all unneeded options
896		$timezone.html($timezoneCopy.html());
897	}
898
899	if ($tzDate.val() !== '') {
900		$timezone.children('optgroup').remove(':not([data-tz-value="' + $tzDate.val() + '"])');
901	}
902
903	if ($tzDate.val() === $tzSelectDateSuggest.attr('data-suggested-tz')) {
904		$tzSelectDateSuggest.css('display', 'none');
905	} else {
906		$tzSelectDateSuggest.css('display', 'inline');
907	}
908
909	var $tzOptions = $timezone.children('optgroup[data-tz-value="' + $tzDate.val() + '"]').children('option');
910
911	if ($tzOptions.length === 1) {
912		// If there is only one timezone for the selected date, we just select that automatically.
913		$tzOptions.prop('selected', true);
914		keepSelection = true;
915	}
916
917	if (typeof keepSelection !== 'undefined' && !keepSelection) {
918		var $timezoneOptions = $timezone.find('optgroup option');
919		if ($timezoneOptions.filter(':selected').length <= 0) {
920			$timezoneOptions.filter(':first').prop('selected', true);
921		}
922	}
923};
924
925/**
926* Display the date/time select
927*/
928phpbb.timezoneEnableDateSelection = function() {
929	$('#tz_select_date').css('display', 'block');
930};
931
932/**
933* Preselect a date/time or suggest one, if it is not picked.
934*
935* @param {bool} forceSelector Shall we select the suggestion?
936*/
937phpbb.timezonePreselectSelect = function(forceSelector) {
938
939	// The offset returned here is in minutes and negated.
940	var offset = (new Date()).getTimezoneOffset();
941	var sign = '-';
942
943	if (offset < 0) {
944		sign = '+';
945		offset = -offset;
946	}
947
948	var minutes = offset % 60;
949	var hours = (offset - minutes) / 60;
950
951	if (hours === 0) {
952		hours = '00';
953		sign = '+';
954	} else if (hours < 10) {
955		hours = '0' + hours.toString();
956	} else {
957		hours = hours.toString();
958	}
959
960	if (minutes < 10) {
961		minutes = '0' + minutes.toString();
962	} else {
963		minutes = minutes.toString();
964	}
965
966	var prefix = 'UTC' + sign + hours + ':' + minutes;
967	var prefixLength = prefix.length;
968	var selectorOptions = $('option', '#tz_date');
969	var i;
970
971	var $tzSelectDateSuggest = $('#tz_select_date_suggest');
972
973	for (i = 0; i < selectorOptions.length; ++i) {
974		var option = selectorOptions[i];
975
976		if (option.value.substring(0, prefixLength) === prefix) {
977			if ($('#tz_date').val() !== option.value && !forceSelector) {
978				// We do not select the option for the user, but notify him,
979				// that we would suggest a different setting.
980				phpbb.timezoneSwitchDate(true);
981				$tzSelectDateSuggest.css('display', 'inline');
982			} else {
983				option.selected = true;
984				phpbb.timezoneSwitchDate(!forceSelector);
985				$tzSelectDateSuggest.css('display', 'none');
986			}
987
988			var suggestion = $tzSelectDateSuggest.attr('data-l-suggestion');
989
990			$tzSelectDateSuggest.attr('title', suggestion.replace('%s', option.innerHTML));
991			$tzSelectDateSuggest.attr('value', suggestion.replace('%s', option.innerHTML.substring(0, 9)));
992			$tzSelectDateSuggest.attr('data-suggested-tz', option.innerHTML);
993
994			// Found the suggestion, there cannot be more, so return from here.
995			return;
996		}
997	}
998};
999
1000phpbb.ajaxCallbacks = {};
1001
1002/**
1003 * Adds an AJAX callback to be used by phpbb.ajaxify.
1004 *
1005 * See the phpbb.ajaxify comments for information on stuff like parameters.
1006 *
1007 * @param {string} id The name of the callback.
1008 * @param {function} callback The callback to be called.
1009 */
1010phpbb.addAjaxCallback = function(id, callback) {
1011	if (typeof callback === 'function') {
1012		phpbb.ajaxCallbacks[id] = callback;
1013	}
1014	return this;
1015};
1016
1017/**
1018 * This callback handles live member searches.
1019 */
1020phpbb.addAjaxCallback('member_search', function(res) {
1021	phpbb.search.handleResponse(res, $(this), false, phpbb.getFunctionByName('phpbb.search.setValueOnClick'));
1022});
1023
1024/**
1025 * This callback alternates text - it replaces the current text with the text in
1026 * the alt-text data attribute, and replaces the text in the attribute with the
1027 * current text so that the process can be repeated.
1028 */
1029phpbb.addAjaxCallback('alt_text', function() {
1030	var $anchor,
1031		updateAll = $(this).data('update-all'),
1032		altText;
1033
1034	if (updateAll !== undefined && updateAll.length) {
1035		$anchor = $(updateAll);
1036	} else {
1037		$anchor = $(this);
1038	}
1039
1040	$anchor.each(function() {
1041		var $this = $(this);
1042		altText = $this.attr('data-alt-text');
1043		$this.attr('data-alt-text', $.trim($this.text()));
1044		$this.attr('title', altText);
1045		$this.children('span').text(altText);
1046	});
1047});
1048
1049/**
1050 * This callback is based on the alt_text callback.
1051 *
1052 * It replaces the current text with the text in the alt-text data attribute,
1053 * and replaces the text in the attribute with the current text so that the
1054 * process can be repeated.
1055 * Additionally it replaces the class of the link's parent
1056 * and changes the link itself.
1057 */
1058phpbb.addAjaxCallback('toggle_link', function() {
1059	var $anchor,
1060		updateAll = $(this).data('update-all') ,
1061		toggleText,
1062		toggleUrl,
1063		toggleClass;
1064
1065	if (updateAll !== undefined && updateAll.length) {
1066		$anchor = $(updateAll);
1067	} else {
1068		$anchor = $(this);
1069	}
1070
1071	$anchor.each(function() {
1072		var $this = $(this);
1073
1074		// Toggle link url
1075		toggleUrl = $this.attr('data-toggle-url');
1076		$this.attr('data-toggle-url', $this.attr('href'));
1077		$this.attr('href', toggleUrl);
1078
1079		// Toggle class of link parent
1080		toggleClass = $this.attr('data-toggle-class');
1081		$this.attr('data-toggle-class', $this.children().attr('class'));
1082		$this.children('.icon').attr('class', toggleClass);
1083
1084		// Toggle link text
1085		toggleText = $this.attr('data-toggle-text');
1086		$this.attr('data-toggle-text', $this.children('span').text());
1087		$this.attr('title', $.trim(toggleText));
1088		$this.children('span').text(toggleText);
1089	});
1090});
1091
1092/**
1093* Automatically resize textarea
1094*
1095* This function automatically resizes textarea elements when user
1096* types text.
1097*
1098* @param {jQuery} $items jQuery object(s) to resize
1099* @param {object} [options] Optional parameter that adjusts default
1100* 	configuration. See configuration variable
1101*
1102* Optional parameters:
1103*	minWindowHeight {number} Minimum browser window height when textareas are resized. Default = 500
1104*	minHeight {number} Minimum height of textarea. Default = 200
1105*	maxHeight {number} Maximum height of textarea. Default = 500
1106*	heightDiff {number} Minimum difference between window and textarea height. Default = 200
1107*	resizeCallback {function} Function to call after resizing textarea
1108*	resetCallback {function} Function to call when resize has been canceled
1109
1110*		Callback function format: function(item) {}
1111*			this points to DOM object
1112*			item is a jQuery object, same as this
1113*/
1114phpbb.resizeTextArea = function($items, options) {
1115	// Configuration
1116	var configuration = {
1117		minWindowHeight: 500,
1118		minHeight: 200,
1119		maxHeight: 500,
1120		heightDiff: 200,
1121		resizeCallback: function() {},
1122		resetCallback: function() {}
1123	};
1124
1125	if (phpbb.isTouch) {
1126		return;
1127	}
1128
1129	if (arguments.length > 1) {
1130		configuration = $.extend(configuration, options);
1131	}
1132
1133	function resetAutoResize(item) {
1134		var $item = $(item);
1135		if ($item.hasClass('auto-resized')) {
1136			$(item)
1137				.css({ height: '', resize: '' })
1138				.removeClass('auto-resized');
1139			configuration.resetCallback.call(item, $item);
1140		}
1141	}
1142
1143	function autoResize(item) {
1144		function setHeight(height) {
1145			height += parseInt($item.css('height'), 10) - $item.innerHeight();
1146			$item
1147				.css({ height: height + 'px', resize: 'none' })
1148				.addClass('auto-resized');
1149			configuration.resizeCallback.call(item, $item);
1150		}
1151
1152		var windowHeight = $(window).height();
1153
1154		if (windowHeight < configuration.minWindowHeight) {
1155			resetAutoResize(item);
1156			return;
1157		}
1158
1159		var maxHeight = Math.min(
1160				Math.max(windowHeight - configuration.heightDiff, configuration.minHeight),
1161				configuration.maxHeight
1162			),
1163			$item = $(item),
1164			height = parseInt($item.innerHeight(), 10),
1165			scrollHeight = (item.scrollHeight) ? item.scrollHeight : 0;
1166
1167		if (height < 0) {
1168			return;
1169		}
1170
1171		if (height > maxHeight) {
1172			setHeight(maxHeight);
1173		} else if (scrollHeight > (height + 5)) {
1174			setHeight(Math.min(maxHeight, scrollHeight));
1175		}
1176	}
1177
1178	$items.on('focus change keyup', function() {
1179		$(this).each(function() {
1180			autoResize(this);
1181		});
1182	}).change();
1183
1184	$(window).resize(function() {
1185		$items.each(function() {
1186			if ($(this).hasClass('auto-resized')) {
1187				autoResize(this);
1188			}
1189		});
1190	});
1191};
1192
1193/**
1194* Check if cursor in textarea is currently inside a bbcode tag
1195*
1196* @param {object} textarea Textarea DOM object
1197* @param {Array} startTags List of start tags to look for
1198*		For example, Array('[code]', '[code=')
1199* @param {Array} endTags List of end tags to look for
1200*		For example, Array('[/code]')
1201*
1202* @returns {boolean} True if cursor is in bbcode tag
1203*/
1204phpbb.inBBCodeTag = function(textarea, startTags, endTags) {
1205	var start = textarea.selectionStart,
1206		lastEnd = -1,
1207		lastStart = -1,
1208		i, index, value;
1209
1210	if (typeof start !== 'number') {
1211		return false;
1212	}
1213
1214	value = textarea.value.toLowerCase();
1215
1216	for (i = 0; i < startTags.length; i++) {
1217		var tagLength = startTags[i].length;
1218		if (start >= tagLength) {
1219			index = value.lastIndexOf(startTags[i], start - tagLength);
1220			lastStart = Math.max(lastStart, index);
1221		}
1222	}
1223	if (lastStart === -1) {
1224		return false;
1225	}
1226
1227	if (start > 0) {
1228		for (i = 0; i < endTags.length; i++) {
1229			index = value.lastIndexOf(endTags[i], start - 1);
1230			lastEnd = Math.max(lastEnd, index);
1231		}
1232	}
1233
1234	return (lastEnd < lastStart);
1235};
1236
1237
1238/**
1239* Adjust textarea to manage code bbcode
1240*
1241* This function allows to use tab characters when typing code
1242* and keeps indentation of previous line of code when adding new
1243* line while typing code.
1244*
1245* Editor's functionality is changed only when cursor is between
1246* [code] and [/code] bbcode tags.
1247*
1248* @param {object} textarea Textarea DOM object to apply editor to
1249*/
1250phpbb.applyCodeEditor = function(textarea) {
1251	// list of allowed start and end bbcode code tags, in lower case
1252	var startTags = ['[code]', '[code='],
1253		startTagsEnd = ']',
1254		endTags = ['[/code]'];
1255
1256	if (!textarea || typeof textarea.selectionStart !== 'number') {
1257		return;
1258	}
1259
1260	if ($(textarea).data('code-editor') === true) {
1261		return;
1262	}
1263
1264	function inTag() {
1265		return phpbb.inBBCodeTag(textarea, startTags, endTags);
1266	}
1267
1268	/**
1269	* Get line of text before cursor
1270	*
1271	* @param {boolean} stripCodeStart If true, only part of line
1272	*		after [code] tag will be returned.
1273	*
1274	* @returns {string} Line of text
1275	*/
1276	function getLastLine(stripCodeStart) {
1277		var start = textarea.selectionStart,
1278			value = textarea.value,
1279			index = value.lastIndexOf('\n', start - 1);
1280
1281		value = value.substring(index + 1, start);
1282
1283		if (stripCodeStart) {
1284			for (var i = 0; i < startTags.length; i++) {
1285				index = value.lastIndexOf(startTags[i]);
1286				if (index >= 0) {
1287					var tagLength = startTags[i].length;
1288
1289					value = value.substring(index + tagLength);
1290					if (startTags[i].lastIndexOf(startTagsEnd) !== tagLength) {
1291						index = value.indexOf(startTagsEnd);
1292
1293						if (index >= 0) {
1294							value = value.substr(index + 1);
1295						}
1296					}
1297				}
1298			}
1299		}
1300
1301		return value;
1302	}
1303
1304	/**
1305	* Append text at cursor position
1306	*
1307	* @param {string} text Text to append
1308	*/
1309	function appendText(text) {
1310		var start = textarea.selectionStart,
1311			end = textarea.selectionEnd,
1312			value = textarea.value;
1313
1314		textarea.value = value.substr(0, start) + text + value.substr(end);
1315		textarea.selectionStart = textarea.selectionEnd = start + text.length;
1316	}
1317
1318	$(textarea).data('code-editor', true).on('keydown', function(event) {
1319		var key = event.keyCode || event.which;
1320
1321		// intercept tabs
1322		if (key === keymap.TAB	&&
1323			!event.ctrlKey		&&
1324			!event.shiftKey		&&
1325			!event.altKey		&&
1326			!event.metaKey) {
1327			if (inTag()) {
1328				appendText('\t');
1329				event.preventDefault();
1330				return;
1331			}
1332		}
1333
1334		// intercept new line characters
1335		if (key === keymap.ENTER) {
1336			if (inTag()) {
1337				var lastLine = getLastLine(true),
1338					code = '' + /^\s*/g.exec(lastLine);
1339
1340				if (code.length > 0) {
1341					appendText('\n' + code);
1342					event.preventDefault();
1343				}
1344			}
1345		}
1346	});
1347};
1348
1349/**
1350 * Show drag and drop animation when textarea is present
1351 *
1352 * This function will enable the drag and drop animation for a specified
1353 * textarea.
1354 *
1355 * @param {HTMLElement} textarea Textarea DOM object to apply editor to
1356 */
1357phpbb.showDragNDrop = function(textarea) {
1358	if (!textarea) {
1359		return;
1360	}
1361
1362	$('body').on('dragenter dragover', function () {
1363		$(textarea).addClass('drag-n-drop');
1364	}).on('dragleave dragout dragend drop', function() {
1365		$(textarea).removeClass('drag-n-drop');
1366	});
1367	$(textarea).on('dragenter dragover', function () {
1368		$(textarea).addClass('drag-n-drop-highlight');
1369	}).on('dragleave dragout dragend drop', function() {
1370		$(textarea).removeClass('drag-n-drop-highlight');
1371	});
1372};
1373
1374/**
1375* List of classes that toggle dropdown menu,
1376* list of classes that contain visible dropdown menu
1377*
1378* Add your own classes to strings with comma (probably you
1379* will never need to do that)
1380*/
1381phpbb.dropdownHandles = '.dropdown-container.dropdown-visible .dropdown-toggle';
1382phpbb.dropdownVisibleContainers = '.dropdown-container.dropdown-visible';
1383
1384/**
1385* Dropdown toggle event handler
1386* This handler is used by phpBB.registerDropdown() and other functions
1387*/
1388phpbb.toggleDropdown = function() {
1389	var $this = $(this),
1390		options = $this.data('dropdown-options'),
1391		parent = options.parent,
1392		visible = parent.hasClass('dropdown-visible'),
1393		direction;
1394
1395	if (!visible) {
1396		// Hide other dropdown menus
1397		$(phpbb.dropdownHandles).each(phpbb.toggleDropdown);
1398
1399		// Figure out direction of dropdown
1400		direction = options.direction;
1401		var verticalDirection = options.verticalDirection,
1402			offset = $this.offset();
1403
1404		if (direction === 'auto') {
1405			if (($(window).width() - $this.outerWidth(true)) / 2 > offset.left) {
1406				direction = 'right';
1407			} else {
1408				direction = 'left';
1409			}
1410		}
1411		parent.toggleClass(options.leftClass, direction === 'left')
1412			.toggleClass(options.rightClass, direction === 'right');
1413
1414		if (verticalDirection === 'auto') {
1415			var height = $(window).height(),
1416				top = offset.top - $(window).scrollTop();
1417
1418			verticalDirection = (top < height * 0.7) ? 'down' : 'up';
1419		}
1420		parent.toggleClass(options.upClass, verticalDirection === 'up')
1421			.toggleClass(options.downClass, verticalDirection === 'down');
1422	}
1423
1424	options.dropdown.toggle();
1425	parent.toggleClass(options.visibleClass, !visible)
1426		.toggleClass('dropdown-visible', !visible);
1427
1428	// Check dimensions when showing dropdown
1429	// !visible because variable shows state of dropdown before it was toggled
1430	if (!visible) {
1431		var windowWidth = $(window).width();
1432
1433		options.dropdown.find('.dropdown-contents').each(function() {
1434			var $this = $(this);
1435
1436			$this.css({
1437				marginLeft: 0,
1438				left: 0,
1439				marginRight: 0,
1440				maxWidth: (windowWidth - 4) + 'px'
1441			});
1442
1443			var offset = $this.offset().left,
1444				width = $this.outerWidth(true);
1445
1446			if (offset < 2) {
1447				$this.css('left', (2 - offset) + 'px');
1448			} else if ((offset + width + 2) > windowWidth) {
1449				$this.css('margin-left', (windowWidth - offset - width - 2) + 'px');
1450			}
1451
1452			// Check whether the vertical scrollbar is present.
1453			$this.toggleClass('dropdown-nonscroll', this.scrollHeight === $this.innerHeight());
1454
1455		});
1456		var freeSpace = parent.offset().left - 4;
1457
1458		if (direction === 'left') {
1459			options.dropdown.css('margin-left', '-' + freeSpace + 'px');
1460
1461			// Try to position the notification dropdown correctly in RTL-responsive mode
1462			if (options.dropdown.hasClass('dropdown-extended')) {
1463				var contentWidth,
1464					fullFreeSpace = freeSpace + parent.outerWidth();
1465
1466				options.dropdown.find('.dropdown-contents').each(function() {
1467					contentWidth = parseInt($(this).outerWidth(), 10);
1468					$(this).css({ marginLeft: 0, left: 0 });
1469				});
1470
1471				var maxOffset = Math.min(contentWidth, fullFreeSpace) + 'px';
1472				options.dropdown.css({
1473					width: maxOffset,
1474					marginLeft: -maxOffset
1475				});
1476			}
1477		} else {
1478			options.dropdown.css('margin-right', '-' + (windowWidth + freeSpace) + 'px');
1479		}
1480	}
1481
1482	// Prevent event propagation
1483	if (arguments.length > 0) {
1484		try {
1485			var e = arguments[0];
1486			e.preventDefault();
1487			e.stopPropagation();
1488		} catch (error) { }
1489	}
1490	return false;
1491};
1492
1493/**
1494* Toggle dropdown submenu
1495*/
1496phpbb.toggleSubmenu = function(e) {
1497	$(this).siblings('.dropdown-submenu').toggle();
1498	e.preventDefault();
1499};
1500
1501/**
1502* Register dropdown menu
1503* Shows/hides dropdown, decides which side to open to
1504*
1505* @param {jQuery} toggle Link that toggles dropdown.
1506* @param {jQuery} dropdown Dropdown menu.
1507* @param {Object} options List of options. Optional.
1508*/
1509phpbb.registerDropdown = function(toggle, dropdown, options) {
1510	var ops = {
1511			parent: toggle.parent(), // Parent item to add classes to
1512			direction: 'auto', // Direction of dropdown menu. Possible values: auto, left, right
1513			verticalDirection: 'auto', // Vertical direction. Possible values: auto, up, down
1514			visibleClass: 'visible', // Class to add to parent item when dropdown is visible
1515			leftClass: 'dropdown-left', // Class to add to parent item when dropdown opens to left side
1516			rightClass: 'dropdown-right', // Class to add to parent item when dropdown opens to right side
1517			upClass: 'dropdown-up', // Class to add to parent item when dropdown opens above menu item
1518			downClass: 'dropdown-down' // Class to add to parent item when dropdown opens below menu item
1519		};
1520	if (options) {
1521		ops = $.extend(ops, options);
1522	}
1523	ops.dropdown = dropdown;
1524
1525	ops.parent.addClass('dropdown-container');
1526	toggle.addClass('dropdown-toggle');
1527
1528	toggle.data('dropdown-options', ops);
1529
1530	toggle.click(phpbb.toggleDropdown);
1531	$('.dropdown-toggle-submenu', ops.parent).click(phpbb.toggleSubmenu);
1532};
1533
1534/**
1535* Get the HTML for a color palette table.
1536*
1537* @param {string} dir Palette direction - either v or h
1538* @param {int} width Palette cell width.
1539* @param {int} height Palette cell height.
1540*/
1541phpbb.colorPalette = function(dir, width, height) {
1542	var r, g, b,
1543		numberList = new Array(6),
1544		color = '',
1545		html = '';
1546
1547	numberList[0] = '00';
1548	numberList[1] = '40';
1549	numberList[2] = '80';
1550	numberList[3] = 'BF';
1551	numberList[4] = 'FF';
1552
1553	var tableClass = (dir === 'h') ? 'horizontal-palette' : 'vertical-palette';
1554	html += '<table class="not-responsive colour-palette ' + tableClass + '" style="width: auto;">';
1555
1556	for (r = 0; r < 5; r++) {
1557		if (dir === 'h') {
1558			html += '<tr>';
1559		}
1560
1561		for (g = 0; g < 5; g++) {
1562			if (dir === 'v') {
1563				html += '<tr>';
1564			}
1565
1566			for (b = 0; b < 5; b++) {
1567				color = '' + numberList[r] + numberList[g] + numberList[b];
1568				html += '<td style="background-color: #' + color + '; width: ' + width + 'px; height: ' +
1569					height + 'px;"><a href="#" data-color="' + color + '" style="display: block; width: ' +
1570					width + 'px; height: ' + height + 'px; " alt="#' + color + '" title="#' + color + '"></a>';
1571				html += '</td>';
1572			}
1573
1574			if (dir === 'v') {
1575				html += '</tr>';
1576			}
1577		}
1578
1579		if (dir === 'h') {
1580			html += '</tr>';
1581		}
1582	}
1583	html += '</table>';
1584	return html;
1585};
1586
1587/**
1588* Register a color palette.
1589*
1590* @param {jQuery} el jQuery object for the palette container.
1591*/
1592phpbb.registerPalette = function(el) {
1593	var	orientation	= el.attr('data-color-palette') || el.attr('data-orientation'), // data-orientation kept for backwards compat.
1594		height		= el.attr('data-height'),
1595		width		= el.attr('data-width'),
1596		target		= el.attr('data-target'),
1597		bbcode		= el.attr('data-bbcode');
1598
1599	// Insert the palette HTML into the container.
1600	el.html(phpbb.colorPalette(orientation, width, height));
1601
1602	// Add toggle control.
1603	$('#color_palette_toggle').click(function(e) {
1604		el.toggle();
1605		e.preventDefault();
1606	});
1607
1608	// Attach event handler when a palette cell is clicked.
1609	$(el).on('click', 'a', function(e) {
1610		var color = $(this).attr('data-color');
1611
1612		if (bbcode) {
1613			bbfontstyle('[color=#' + color + ']', '[/color]');
1614		} else {
1615			$(target).val(color);
1616		}
1617		e.preventDefault();
1618	});
1619};
1620
1621/**
1622* Set display of page element
1623*
1624* @param {string} id The ID of the element to change
1625* @param {int} action Set to 0 if element display should be toggled, -1 for
1626*			hiding the element, and 1 for showing it.
1627* @param {string} type Display type that should be used, e.g. inline, block or
1628*			other CSS "display" types
1629*/
1630phpbb.toggleDisplay = function(id, action, type) {
1631	if (!type) {
1632		type = 'block';
1633	}
1634
1635	var $element = $('#' + id);
1636
1637	var display = $element.css('display');
1638	if (!action) {
1639		action = (display === '' || display === type) ? -1 : 1;
1640	}
1641	$element.css('display', ((action === 1) ? type : 'none'));
1642};
1643
1644/**
1645* Toggle additional settings based on the selected
1646* option of select element.
1647*
1648* @param {jQuery} el jQuery select element object.
1649*/
1650phpbb.toggleSelectSettings = function(el) {
1651	el.children().each(function() {
1652		var $this = $(this),
1653			$setting = $($this.data('toggle-setting'));
1654		$setting.toggle($this.is(':selected'));
1655
1656		// Disable any input elements that are not visible right now
1657		if ($this.is(':selected')) {
1658			$($this.data('toggle-setting') + ' input').prop('disabled', false);
1659		} else {
1660			$($this.data('toggle-setting') + ' input').prop('disabled', true);
1661		}
1662	});
1663};
1664
1665/**
1666* Get function from name.
1667* Based on http://stackoverflow.com/a/359910
1668*
1669* @param {string} functionName Function to get.
1670* @returns function
1671*/
1672phpbb.getFunctionByName = function (functionName) {
1673	var namespaces = functionName.split('.'),
1674		func = namespaces.pop(),
1675		context = window;
1676
1677	for (var i = 0; i < namespaces.length; i++) {
1678		context = context[namespaces[i]];
1679	}
1680	return context[func];
1681};
1682
1683/**
1684* Register page dropdowns.
1685*/
1686phpbb.registerPageDropdowns = function() {
1687	var $body = $('body');
1688
1689	$body.find('.dropdown-container').each(function() {
1690		var $this = $(this),
1691			$trigger = $this.find('.dropdown-trigger:first'),
1692			$contents = $this.find('.dropdown'),
1693			options = {
1694				direction: 'auto',
1695				verticalDirection: 'auto'
1696			},
1697			data;
1698
1699		if (!$trigger.length) {
1700			data = $this.attr('data-dropdown-trigger');
1701			$trigger = data ? $this.children(data) : $this.children('a:first');
1702		}
1703
1704		if (!$contents.length) {
1705			data = $this.attr('data-dropdown-contents');
1706			$contents = data ? $this.children(data) : $this.children('div:first');
1707		}
1708
1709		if (!$trigger.length || !$contents.length) {
1710			return;
1711		}
1712
1713		if ($this.hasClass('dropdown-up')) {
1714			options.verticalDirection = 'up';
1715		}
1716		if ($this.hasClass('dropdown-down')) {
1717			options.verticalDirection = 'down';
1718		}
1719		if ($this.hasClass('dropdown-left')) {
1720			options.direction = 'left';
1721		}
1722		if ($this.hasClass('dropdown-right')) {
1723			options.direction = 'right';
1724		}
1725
1726		phpbb.registerDropdown($trigger, $contents, options);
1727	});
1728
1729	// Hide active dropdowns when click event happens outside
1730	$body.click(function(e) {
1731		var $parents = $(e.target).parents();
1732		if (!$parents.is(phpbb.dropdownVisibleContainers)) {
1733			$(phpbb.dropdownHandles).each(phpbb.toggleDropdown);
1734		}
1735	});
1736};
1737
1738/**
1739 * Handle avatars to be lazy loaded.
1740 */
1741phpbb.lazyLoadAvatars = function loadAvatars() {
1742	$('.avatar[data-src]').each(function () {
1743		var $avatar = $(this);
1744
1745		$avatar
1746			.attr('src', $avatar.data('src'))
1747			.removeAttr('data-src');
1748	});
1749};
1750
1751phpbb.recaptcha = {
1752	button: null,
1753	ready: false,
1754
1755	token: $('input[name="recaptcha_token"]'),
1756	form: $('.g-recaptcha').parents('form'),
1757	v3: $('[data-recaptcha-v3]'),
1758
1759	load: function() {
1760		phpbb.recaptcha.bindButton();
1761		phpbb.recaptcha.bindForm();
1762	},
1763	bindButton: function() {
1764		phpbb.recaptcha.form.find('input[type="submit"]').on('click', function() {
1765			// Listen to all the submit buttons for the form that has reCAPTCHA protection,
1766			// and store it so we can click the exact same button later on when we are ready.
1767			phpbb.recaptcha.button = this;
1768		});
1769	},
1770	bindForm: function() {
1771		phpbb.recaptcha.form.on('submit', function(e) {
1772			 // If ready is false, it means the user pressed a submit button.
1773			 // And the form was not submitted by us, after the token was loaded.
1774			if (!phpbb.recaptcha.ready) {
1775				 // If version 3 is used, we need to make a different execution,
1776				 // including the action and the site key.
1777				if (phpbb.recaptcha.v3.length) {
1778					grecaptcha.execute(
1779						phpbb.recaptcha.v3.data('recaptcha-v3'),
1780						{action: phpbb.recaptcha.v3.val()}
1781					).then(function(token) {
1782						// Place the token inside the form
1783						phpbb.recaptcha.token.val(token);
1784
1785						// And now we submit the form.
1786						phpbb.recaptcha.submitForm();
1787					});
1788				} else {
1789					// Regular version 2 execution
1790					grecaptcha.execute();
1791				}
1792
1793				// Do not submit the form
1794				e.preventDefault();
1795			}
1796		});
1797	},
1798	submitForm: function() {
1799		// Now we are ready, so set it to true.
1800		// so the 'submit' event doesn't run multiple times.
1801		phpbb.recaptcha.ready = true;
1802
1803		if (phpbb.recaptcha.button) {
1804			// If there was a specific button pressed initially, trigger the same button
1805			phpbb.recaptcha.button.click();
1806		} else {
1807			if (typeof phpbb.recaptcha.form.submit !== 'function') {
1808				// Rename input[name="submit"] so that we can submit the form
1809				phpbb.recaptcha.form.submit.name = 'submit_btn';
1810			}
1811
1812			phpbb.recaptcha.form.submit();
1813		}
1814	}
1815};
1816
1817// reCAPTCHA v2 doesn't accept callback functions nested inside objects
1818// so we need to make this helper functions here
1819window.phpbbRecaptchaOnLoad = function() {
1820	phpbb.recaptcha.load();
1821};
1822
1823window.phpbbRecaptchaOnSubmit = function() {
1824	phpbb.recaptcha.submitForm();
1825};
1826
1827$(window).on('load', phpbb.lazyLoadAvatars);
1828
1829/**
1830* Apply code editor to all textarea elements with data-bbcode attribute
1831*/
1832$(function() {
1833	// reCAPTCHA v3 needs to be initialized
1834	if (phpbb.recaptcha.v3.length) {
1835		phpbb.recaptcha.load();
1836	}
1837
1838	$('textarea[data-bbcode]').each(function() {
1839		phpbb.applyCodeEditor(this);
1840	});
1841
1842	phpbb.registerPageDropdowns();
1843
1844	$('[data-color-palette], [data-orientation]').each(function() {
1845		phpbb.registerPalette($(this));
1846	});
1847
1848	// Update browser history URL to point to specific post in viewtopic.php
1849	// when using view=unread#unread link.
1850	phpbb.history.replaceUrl($('#unread[data-url]').data('url'));
1851
1852	// Hide settings that are not selected via select element.
1853	$('select[data-togglable-settings]').each(function() {
1854		var $this = $(this);
1855
1856		$this.change(function() {
1857			phpbb.toggleSelectSettings($this);
1858		});
1859		phpbb.toggleSelectSettings($this);
1860	});
1861});
1862
1863})(jQuery); // Avoid conflicts with other libraries

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.