PageSourceSearch

http://elan-kazak.ru/forum/assets/javascript/core.js?assets_version=12

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