.=< { Star Gans Tq } >=.

  • Home

  • Killme
  • Download
  • Current Path : /home/m/e/h/meharicl/www/phpBB3/assets/javascript/
    Upload File
    @Command ~ $  
    Current File : /home/m/e/h/meharicl/www/phpBB3/assets/javascript/core.js

    /* global bbfontstyle */
    
    var phpbb = {};
    phpbb.alertTime = 100;
    
    (function($) {  // Avoid conflicts with other libraries
    
    'use strict';
    
    // define a couple constants for keydown functions.
    var keymap = {
    	TAB: 9,
    	ENTER: 13,
    	ESC: 27,
    	ARROW_UP: 38,
    	ARROW_DOWN: 40
    };
    
    var $dark = $('#darkenwrapper');
    var $loadingIndicator;
    var phpbbAlertTimer = null;
    
    phpbb.isTouch = (window && typeof window.ontouchstart !== 'undefined');
    
    // Add ajax pre-filter to prevent cross-domain script execution
    $.ajaxPrefilter(function(s) {
    	if (s.crossDomain) {
    		s.contents.script = false;
    	}
    });
    
    /**
     * Display a loading screen
     *
     * @returns {object} Returns loadingIndicator.
     */
    phpbb.loadingIndicator = function() {
    	if (!$loadingIndicator) {
    		$loadingIndicator = $('<div />', {
    			'id': 'loading_indicator',
    			'class': 'loading_indicator'
    		});
    		$loadingIndicator.appendTo('#page-footer');
    	}
    
    	if (!$loadingIndicator.is(':visible')) {
    		$loadingIndicator.fadeIn(phpbb.alertTime);
    		// Wait 60 seconds and display an error if nothing has been returned by then.
    		phpbb.clearLoadingTimeout();
    		phpbbAlertTimer = setTimeout(function() {
    			phpbb.showTimeoutMessage();
    		}, 60000);
    	}
    
    	return $loadingIndicator;
    };
    
    /**
     * Show timeout message
     */
    phpbb.showTimeoutMessage = function () {
    	var $alert = $('#phpbb_alert');
    
    	if ($loadingIndicator.is(':visible')) {
    		phpbb.alert($alert.attr('data-l-err'), $alert.attr('data-l-timeout-processing-req'));
    	}
    };
    
    /**
     * Clear loading alert timeout
    */
    phpbb.clearLoadingTimeout = function() {
    	if (phpbbAlertTimer !== null) {
    		clearTimeout(phpbbAlertTimer);
    		phpbbAlertTimer = null;
    	}
    };
    
    
    /**
    * Close popup alert after a specified delay
    *
    * @param {int} delay Delay in ms until darkenwrapper's click event is triggered
    */
    phpbb.closeDarkenWrapper = function(delay) {
    	phpbbAlertTimer = setTimeout(function() {
    		$('#darkenwrapper').trigger('click');
    	}, delay);
    };
    
    /**
     * Display a simple alert similar to JSs native alert().
     *
     * You can only call one alert or confirm box at any one time.
     *
     * @param {string} title Title of the message, eg "Information" (HTML).
     * @param {string} msg Message to display (HTML).
     *
     * @returns {object} Returns the div created.
     */
    phpbb.alert = function(title, msg) {
    	var $alert = $('#phpbb_alert');
    	$alert.find('.alert_title').html(title);
    	$alert.find('.alert_text').html(msg);
    
    	$(document).on('keydown.phpbb.alert', function(e) {
    		if (e.keyCode === keymap.ENTER || e.keyCode === keymap.ESC) {
    			phpbb.alert.close($alert, true);
    			e.preventDefault();
    			e.stopPropagation();
    		}
    	});
    	phpbb.alert.open($alert);
    
    	return $alert;
    };
    
    /**
    * Handler for opening an alert box.
    *
    * @param {jQuery} $alert jQuery object.
    */
    phpbb.alert.open = function($alert) {
    	if (!$dark.is(':visible')) {
    		$dark.fadeIn(phpbb.alertTime);
    	}
    
    	if ($loadingIndicator && $loadingIndicator.is(':visible')) {
    		$loadingIndicator.fadeOut(phpbb.alertTime, function() {
    			$dark.append($alert);
    			$alert.fadeIn(phpbb.alertTime);
    		});
    	} else if ($dark.is(':visible')) {
    		$dark.append($alert);
    		$alert.fadeIn(phpbb.alertTime);
    	} else {
    		$dark.append($alert);
    		$alert.show();
    		$dark.fadeIn(phpbb.alertTime);
    	}
    
    	$alert.on('click', function(e) {
    		e.stopPropagation();
    	});
    
    	$dark.one('click', function(e) {
    		phpbb.alert.close($alert, true);
    		e.preventDefault();
    		e.stopPropagation();
    	});
    
    	$alert.find('.alert_close').one('click', function(e) {
    		phpbb.alert.close($alert, true);
    		e.preventDefault();
    	});
    };
    
    /**
    * Handler for closing an alert box.
    *
    * @param {jQuery} $alert jQuery object.
    * @param {bool} fadedark Whether to remove dark background.
    */
    phpbb.alert.close = function($alert, fadedark) {
    	var $fade = (fadedark) ? $dark : $alert;
    
    	$fade.fadeOut(phpbb.alertTime, function() {
    		$alert.hide();
    	});
    
    	$alert.find('.alert_close').off('click');
    	$(document).off('keydown.phpbb.alert');
    };
    
    /**
     * Display a simple yes / no box to the user.
     *
     * You can only call one alert or confirm box at any one time.
     *
     * @param {string} msg Message to display (HTML).
     * @param {function} callback Callback. Bool param, whether the user pressed
     *     yes or no (or whatever their language is).
     * @param {bool} fadedark Remove the dark background when done? Defaults
     *     to yes.
     *
     * @returns {object} Returns the div created.
     */
    phpbb.confirm = function(msg, callback, fadedark) {
    	var $confirmDiv = $('#phpbb_confirm');
    	$confirmDiv.find('.alert_text').html(msg);
    	fadedark = typeof fadedark !== 'undefined' ? fadedark : true;
    
    	$(document).on('keydown.phpbb.alert', function(e) {
    		if (e.keyCode === keymap.ENTER || e.keyCode === keymap.ESC) {
    			var name = (e.keyCode === keymap.ENTER) ? 'confirm' : 'cancel';
    
    			$('input[name="' + name + '"]').trigger('click');
    			e.preventDefault();
    			e.stopPropagation();
    		}
    	});
    
    	$confirmDiv.find('input[type="button"]').one('click.phpbb.confirmbox', function(e) {
    		var confirmed = this.name === 'confirm';
    
    		callback(confirmed);
    		$confirmDiv.find('input[type="button"]').off('click.phpbb.confirmbox');
    		phpbb.alert.close($confirmDiv, fadedark || !confirmed);
    
    		e.preventDefault();
    		e.stopPropagation();
    	});
    
    	phpbb.alert.open($confirmDiv);
    
    	return $confirmDiv;
    };
    
    /**
     * Turn a querystring into an array.
     *
     * @argument {string} string The querystring to parse.
     * @returns {object} The object created.
     */
    phpbb.parseQuerystring = function(string) {
    	var params = {}, i, split;
    
    	string = string.split('&');
    	for (i = 0; i < string.length; i++) {
    		split = string[i].split('=');
    		params[split[0]] = decodeURIComponent(split[1]);
    	}
    	return params;
    };
    
    
    /**
     * Makes a link use AJAX instead of loading an entire page.
     *
     * This function will work for links (both standard links and links which
     * invoke confirm_box) and forms. It will be called automatically for links
     * and forms with the data-ajax attribute set, and will call the necessary
     * callback.
     *
     * For more info, view the following page on the phpBB wiki:
     * http://wiki.phpbb.com/JavaScript_Function.phpbb.ajaxify
     *
     * @param {object} options Options.
     */
    phpbb.ajaxify = function(options) {
    	var $elements = $(options.selector),
    		refresh = options.refresh,
    		callback = options.callback,
    		overlay = (typeof options.overlay !== 'undefined') ? options.overlay : true,
    		isForm = $elements.is('form'),
    		isText = $elements.is('input[type="text"], textarea'),
    		eventName;
    
    	if (isForm) {
    		eventName = 'submit';
    	} else if (isText) {
    		eventName = 'keyup';
    	} else {
    		eventName = 'click';
    	}
    
    	$elements.on(eventName, function(event) {
    		var action, method, data, submit, that = this, $this = $(this);
    
    		if ($this.find('input[type="submit"][data-clicked]').attr('data-ajax') === 'false') {
    			return;
    		}
    
    		/**
    		 * Handler for AJAX errors
    		 */
    		function errorHandler(jqXHR, textStatus, errorThrown) {
    			if (typeof console !== 'undefined' && console.log) {
    				console.log('AJAX error. status: ' + textStatus + ', message: ' + errorThrown);
    			}
    			phpbb.clearLoadingTimeout();
    			var responseText, errorText = false;
    			try {
    				responseText = JSON.parse(jqXHR.responseText);
    				responseText = responseText.message;
    			} catch (e) {}
    			if (typeof responseText === 'string' && responseText.length > 0) {
    				errorText = responseText;
    			} else if (typeof errorThrown === 'string' && errorThrown.length > 0) {
    				errorText = errorThrown;
    			} else {
    				errorText = $dark.attr('data-ajax-error-text-' + textStatus);
    				if (typeof errorText !== 'string' || !errorText.length) {
    					errorText = $dark.attr('data-ajax-error-text');
    				}
    			}
    			phpbb.alert($dark.attr('data-ajax-error-title'), errorText);
    		}
    
    		/**
    		 * This is a private function used to handle the callbacks, refreshes
    		 * and alert. It calls the callback, refreshes the page if necessary, and
    		 * displays an alert to the user and removes it after an amount of time.
    		 *
    		 * It cannot be called from outside this function, and is purely here to
    		 * avoid repetition of code.
    		 *
    		 * @param {object} res The object sent back by the server.
    		 */
    		function returnHandler(res) {
    			var alert;
    
    			phpbb.clearLoadingTimeout();
    
    			// Is a confirmation required?
    			if (typeof res.S_CONFIRM_ACTION === 'undefined') {
    				// If a confirmation is not required, display an alert and call the
    				// callbacks.
    				if (typeof res.MESSAGE_TITLE !== 'undefined') {
    					alert = phpbb.alert(res.MESSAGE_TITLE, res.MESSAGE_TEXT);
    				} else {
    					$dark.fadeOut(phpbb.alertTime);
    
    					if ($loadingIndicator) {
    						$loadingIndicator.fadeOut(phpbb.alertTime);
    					}
    				}
    
    				if (typeof phpbb.ajaxCallbacks[callback] === 'function') {
    					phpbb.ajaxCallbacks[callback].call(that, res);
    				}
    
    				// If the server says to refresh the page, check whether the page should
    				// be refreshed and refresh page after specified time if required.
    				if (res.REFRESH_DATA) {
    					if (typeof refresh === 'function') {
    						refresh = refresh(res.REFRESH_DATA.url);
    					} else if (typeof refresh !== 'boolean') {
    						refresh = false;
    					}
    
    					phpbbAlertTimer = setTimeout(function() {
    						if (refresh) {
    							window.location = res.REFRESH_DATA.url;
    						}
    
    						// Hide the alert even if we refresh the page, in case the user
    						// presses the back button.
    						$dark.fadeOut(phpbb.alertTime, function() {
    							if (typeof alert !== 'undefined') {
    								alert.hide();
    							}
    						});
    					}, res.REFRESH_DATA.time * 1000); // Server specifies time in seconds
    				}
    			} else {
    				// If confirmation is required, display a dialog to the user.
    				phpbb.confirm(res.MESSAGE_BODY, function(del) {
    					if (!del) {
    						return;
    					}
    
    					phpbb.loadingIndicator();
    					data =  $('<form>' + res.S_HIDDEN_FIELDS + '</form>').serialize();
    					$.ajax({
    						url: res.S_CONFIRM_ACTION,
    						type: 'POST',
    						data: data + '&confirm=' + res.YES_VALUE + '&' + $('form', '#phpbb_confirm').serialize(),
    						success: returnHandler,
    						error: errorHandler
    					});
    				}, false);
    			}
    		}
    
    		// If the element is a form, POST must be used and some extra data must
    		// be taken from the form.
    		var runFilter = (typeof options.filter === 'function');
    		data = {};
    
    		if (isForm) {
    			action = $this.attr('action').replace('&amp;', '&');
    			data = $this.serializeArray();
    			method = $this.attr('method') || 'GET';
    
    			if ($this.find('input[type="submit"][data-clicked]')) {
    				submit = $this.find('input[type="submit"][data-clicked]');
    				data.push({
    					name: submit.attr('name'),
    					value: submit.val()
    				});
    			}
    		} else if (isText) {
    			var name = $this.attr('data-name') || this.name;
    			action = $this.attr('data-url').replace('&amp;', '&');
    			data[name] = this.value;
    			method = 'POST';
    		} else {
    			action = this.href;
    			data = null;
    			method = 'GET';
    		}
    
    		var sendRequest = function() {
    			var dataOverlay = $this.attr('data-overlay');
    			if (overlay && (typeof dataOverlay === 'undefined' || dataOverlay === 'true')) {
    				phpbb.loadingIndicator();
    			}
    
    			var request = $.ajax({
    				url: action,
    				type: method,
    				data: data,
    				success: returnHandler,
    				error: errorHandler,
    				cache: false
    			});
    
    			request.always(function() {
    				if ($loadingIndicator && $loadingIndicator.is(':visible')) {
    					$loadingIndicator.fadeOut(phpbb.alertTime);
    				}
    			});
    		};
    
    		// If filter function returns false, cancel the AJAX functionality,
    		// and return true (meaning that the HTTP request will be sent normally).
    		if (runFilter && !options.filter.call(this, data, event, sendRequest)) {
    			return;
    		}
    
    		sendRequest();
    		event.preventDefault();
    	});
    
    	if (isForm) {
    		$elements.find('input:submit').click(function () {
    			var $this = $(this);
    
    			// Remove data-clicked attribute from any submit button of form
    			$this.parents('form:first').find('input:submit[data-clicked]').removeAttr('data-clicked');
    
    			$this.attr('data-clicked', 'true');
    		});
    	}
    
    	return this;
    };
    
    phpbb.search = {
    	cache: {
    		data: []
    	},
    	tpl: [],
    	container: []
    };
    
    /**
     * Get cached search data.
     *
     * @param {string} id Search ID.
     * @returns {bool|object} Cached data object. Returns false if no data exists.
     */
    phpbb.search.cache.get = function(id) {
    	if (this.data[id]) {
    		return this.data[id];
    	}
    	return false;
    };
    
    /**
     * Set search cache data value.
     *
     * @param {string} id		Search ID.
     * @param {string} key		Data key.
     * @param {string} value	Data value.
     */
    phpbb.search.cache.set = function(id, key, value) {
    	if (!this.data[id]) {
    		this.data[id] = { results: [] };
    	}
    	this.data[id][key] = value;
    };
    
    /**
     * Cache search result.
     *
     * @param {string} id		Search ID.
     * @param {string} keyword	Keyword.
     * @param {Array} results	Search results.
     */
    phpbb.search.cache.setResults = function(id, keyword, results) {
    	this.data[id].results[keyword] = results;
    };
    
    /**
     * Trim spaces from keyword and lower its case.
     *
     * @param {string} keyword Search keyword to clean.
     * @returns {string} Cleaned string.
     */
    phpbb.search.cleanKeyword = function(keyword) {
    	return $.trim(keyword).toLowerCase();
    };
    
    /**
     * Get clean version of search keyword. If textarea supports several keywords
     * (one per line), it fetches the current keyword based on the caret position.
     *
     * @param {jQuery} $input	Search input|textarea.
     * @param {string} keyword	Input|textarea value.
     * @param {bool} multiline	Whether textarea supports multiple search keywords.
     *
     * @returns string Clean string.
     */
    phpbb.search.getKeyword = function($input, keyword, multiline) {
    	if (multiline) {
    		var line = phpbb.search.getKeywordLine($input);
    		keyword = keyword.split('\n').splice(line, 1);
    	}
    	return phpbb.search.cleanKeyword(keyword);
    };
    
    /**
     * Get the textarea line number on which the keyword resides - for textareas
     * that support multiple keywords (one per line).
     *
     * @param {jQuery} $textarea Search textarea.
     * @returns {int} The line number.
     */
    phpbb.search.getKeywordLine = function ($textarea) {
    	var selectionStart = $textarea.get(0).selectionStart;
    	return $textarea.val().substr(0, selectionStart).split('\n').length - 1;
    };
    
    /**
     * Set the value on the input|textarea. If textarea supports multiple
     * keywords, only the active keyword is replaced.
     *
     * @param {jQuery} $input	Search input|textarea.
     * @param {string} value	Value to set.
     * @param {bool} multiline	Whether textarea supports multiple search keywords.
     */
    phpbb.search.setValue = function($input, value, multiline) {
    	if (multiline) {
    		var line = phpbb.search.getKeywordLine($input),
    			lines = $input.val().split('\n');
    		lines[line] = value;
    		value = lines.join('\n');
    	}
    	$input.val(value);
    };
    
    /**
     * Sets the onclick event to set the value on the input|textarea to the
     * selected search result.
     *
     * @param {jQuery} $input		Search input|textarea.
     * @param {object} value		Result object.
     * @param {jQuery} $row			Result element.
     * @param {jQuery} $container	jQuery object for the search container.
     */
    phpbb.search.setValueOnClick = function($input, value, $row, $container) {
    	$row.click(function() {
    		phpbb.search.setValue($input, value.result, $input.attr('data-multiline'));
    		phpbb.search.closeResults($input, $container);
    	});
    };
    
    /**
     * Runs before the AJAX search request is sent and determines whether
     * there is a need to contact the server. If there are cached results
     * already, those are displayed instead. Executes the AJAX request function
     * itself due to the need to use a timeout to limit the number of requests.
     *
     * @param {Array} data				Data to be sent to the server.
     * @param {object} event			Onkeyup event object.
     * @param {function} sendRequest	Function to execute AJAX request.
     *
     * @returns {boolean} Returns false.
     */
    phpbb.search.filter = function(data, event, sendRequest) {
    	var $this = $(this),
    		dataName = ($this.attr('data-name') !== undefined) ? $this.attr('data-name') : $this.attr('name'),
    		minLength = parseInt($this.attr('data-min-length'), 10),
    		searchID = $this.attr('data-results'),
    		keyword = phpbb.search.getKeyword($this, data[dataName], $this.attr('data-multiline')),
    		cache = phpbb.search.cache.get(searchID),
    		key = event.keyCode || event.which,
    		proceed = true;
    	data[dataName] = keyword;
    
    	// No need to search if enter was pressed
    	// for selecting a value from the results.
    	if (key === keymap.ENTER) {
    		return false;
    	}
    
    	if (cache.timeout) {
    		clearTimeout(cache.timeout);
    	}
    
    	var timeout = setTimeout(function() {
    		// Check min length and existence of cache.
    		if (minLength > keyword.length) {
    			proceed = false;
    		} else if (cache.lastSearch) {
    			// Has the keyword actually changed?
    			if (cache.lastSearch === keyword) {
    				proceed = false;
    			} else {
    				// Do we already have results for this?
    				if (cache.results[keyword]) {
    					var response = {
    						keyword: keyword,
    						results: cache.results[keyword]
    					};
    					phpbb.search.handleResponse(response, $this, true);
    					proceed = false;
    				}
    
    				// If the previous search didn't yield results and the string only had characters added to it,
    				// then we won't bother sending a request.
    				if (keyword.indexOf(cache.lastSearch) === 0 && cache.results[cache.lastSearch].length === 0) {
    					phpbb.search.cache.set(searchID, 'lastSearch', keyword);
    					phpbb.search.cache.setResults(searchID, keyword, []);
    					proceed = false;
    				}
    			}
    		}
    
    		if (proceed) {
    			sendRequest.call(this);
    		}
    	}, 350);
    	phpbb.search.cache.set(searchID, 'timeout', timeout);
    
    	return false;
    };
    
    /**
     * Handle search result response.
     *
     * @param {object} res			Data received from server.
     * @param {jQuery} $input		Search input|textarea.
     * @param {bool} fromCache		Whether the results are from the cache.
     * @param {function} callback	Optional callback to run when assigning each search result.
     */
    phpbb.search.handleResponse = function(res, $input, fromCache, callback) {
    	if (typeof res !== 'object') {
    		return;
    	}
    
    	var searchID = $input.attr('data-results'),
    		$container = $(searchID);
    
    	if (this.cache.get(searchID).callback) {
    		callback = this.cache.get(searchID).callback;
    	} else if (typeof callback === 'function') {
    		this.cache.set(searchID, 'callback', callback);
    	}
    
    	if (!fromCache) {
    		this.cache.setResults(searchID, res.keyword, res.results);
    	}
    
    	this.cache.set(searchID, 'lastSearch', res.keyword);
    	this.showResults(res.results, $input, $container, callback);
    };
    
    /**
     * Show search results.
     *
     * @param {Array} results		Search results.
     * @param {jQuery} $input		Search input|textarea.
     * @param {jQuery} $container	Search results container element.
     * @param {function} callback	Optional callback to run when assigning each search result.
     */
    phpbb.search.showResults = function(results, $input, $container, callback) {
    	var $resultContainer = $('.search-results', $container);
    	this.clearResults($resultContainer);
    
    	if (!results.length) {
    		$container.hide();
    		return;
    	}
    
    	var searchID = $container.attr('id'),
    		tpl,
    		row;
    
    	if (!this.tpl[searchID]) {
    		tpl = $('.search-result-tpl', $container);
    		this.tpl[searchID] = tpl.clone().removeClass('search-result-tpl');
    		tpl.remove();
    	}
    	tpl = this.tpl[searchID];
    
    	$.each(results, function(i, item) {
    		row = tpl.clone();
    		row.find('.search-result').html(item.display);
    
    		if (typeof callback === 'function') {
    			callback.call(this, $input, item, row, $container);
    		}
    		row.appendTo($resultContainer).show();
    	});
    	$container.show();
    
    	phpbb.search.navigateResults($input, $container, $resultContainer);
    };
    
    /**
     * Clear search results.
     *
     * @param {jQuery} $container		Search results container.
     */
    phpbb.search.clearResults = function($container) {
    	$container.children(':not(.search-result-tpl)').remove();
    };
    
    /**
     * Close search results.
     *
     * @param {jQuery} $input			Search input|textarea.
     * @param {jQuery} $container		Search results container.
     */
    phpbb.search.closeResults = function($input, $container) {
    	$input.off('.phpbb.search');
    	$container.hide();
    };
    
    /**
     * Navigate search results.
     *
     * @param {jQuery} $input			Search input|textarea.
     * @param {jQuery} $container		Search results container.
     * @param {jQuery} $resultContainer	Search results list container.
     */
    phpbb.search.navigateResults = function($input, $container, $resultContainer) {
    	// Add a namespace to the event (.phpbb.search),
    	// so it can be unbound specifically later on.
    	// Rebind it, to ensure the event is 'dynamic'.
    	$input.off('.phpbb.search');
    	$input.on('keydown.phpbb.search', function(event) {
    		var key = event.keyCode || event.which,
    			$active = $resultContainer.children('.active');
    
    		switch (key) {
    			// Close the results
    			case keymap.ESC:
    				phpbb.search.closeResults($input, $container);
    			break;
    
    			// Set the value for the selected result
    			case keymap.ENTER:
    				if ($active.length) {
    					var value = $active.find('.search-result > span').text();
    
    					phpbb.search.setValue($input, value, $input.attr('data-multiline'));
    				}
    
    				phpbb.search.closeResults($input, $container);
    
    				// Do not submit the form
    				event.preventDefault();
    			break;
    
    			// Navigate the results
    			case keymap.ARROW_DOWN:
    			case keymap.ARROW_UP:
    				var up = key === keymap.ARROW_UP,
    					$children = $resultContainer.children();
    
    				if (!$active.length) {
    					if (up) {
    						$children.last().addClass('active');
    					} else {
    						$children.first().addClass('active');
    					}
    				} else if ($children.length > 1) {
    					if (up) {
    						if ($active.is(':first-child')) {
    							$children.last().addClass('active');
    						} else {
    							$active.prev().addClass('active');
    						}
    					} else {
    						if ($active.is(':last-child')) {
    							$children.first().addClass('active');
    						} else {
    							$active.next().addClass('active');
    						}
    					}
    
    					$active.removeClass('active');
    				}
    
    				// Do not change cursor position in the input element
    				event.preventDefault();
    			break;
    		}
    	});
    };
    
    $('#phpbb').click(function() {
    	var $this = $(this);
    
    	if (!$this.is('.live-search') && !$this.parents().is('.live-search')) {
    		phpbb.search.closeResults($('input, textarea'), $('.live-search'));
    	}
    });
    
    phpbb.history = {};
    
    /**
    * Check whether a method in the native history object is supported.
    *
    * @param {string} fn Method name.
    * @returns {bool} Returns true if the method is supported.
    */
    phpbb.history.isSupported = function(fn) {
    	return !(typeof history === 'undefined' || typeof history[fn] === 'undefined');
    };
    
    /**
    * Wrapper for the pushState and replaceState methods of the
    * native history object.
    *
    * @param {string} mode		Mode. Either push or replace.
    * @param {string} url		New URL.
    * @param {string} [title]	Optional page title.
    * @param {object} [obj]		Optional state object.
    */
    phpbb.history.alterUrl = function(mode, url, title, obj) {
    	var fn = mode + 'State';
    
    	if (!url || !phpbb.history.isSupported(fn)) {
    		return;
    	}
    	if (!title) {
    		title = document.title;
    	}
    	if (!obj) {
    		obj = null;
    	}
    
    	history[fn](obj, title, url);
    };
    
    /**
    * Wrapper for the native history.replaceState method.
    *
    * @param {string} url		New URL.
    * @param {string} [title]	Optional page title.
    * @param {object} [obj]		Optional state object.
    */
    phpbb.history.replaceUrl = function(url, title, obj) {
    	phpbb.history.alterUrl('replace', url, title, obj);
    };
    
    /**
    * Wrapper for the native history.pushState method.
    *
    * @param {string} url		New URL.
    * @param {string} [title]	Optional page title.
    * @param {object} [obj]		Optional state object.
    */
    phpbb.history.pushUrl = function(url, title, obj) {
    	phpbb.history.alterUrl('push', url, title, obj);
    };
    
    /**
    * Hide the optgroups that are not the selected timezone
    *
    * @param {bool} keepSelection Shall we keep the value selected, or shall the
    * 	user be forced to repick one.
    */
    phpbb.timezoneSwitchDate = function(keepSelection) {
    	var $timezoneCopy = $('#timezone_copy');
    	var $timezone = $('#timezone');
    	var $tzDate = $('#tz_date');
    	var $tzSelectDateSuggest = $('#tz_select_date_suggest');
    
    	if ($timezoneCopy.length === 0) {
    		// We make a backup of the original dropdown, so we can remove optgroups
    		// instead of setting display to none, because IE and chrome will not
    		// hide options inside of optgroups and selects via css
    		$timezone.clone()
    			.attr('id', 'timezone_copy')
    			.css('display', 'none')
    			.attr('name', 'tz_copy')
    			.insertAfter('#timezone');
    	} else {
    		// Copy the content of our backup, so we can remove all unneeded options
    		$timezone.html($timezoneCopy.html());
    	}
    
    	if ($tzDate.val() !== '') {
    		$timezone.children('optgroup').remove(':not([data-tz-value="' + $tzDate.val() + '"])');
    	}
    
    	if ($tzDate.val() === $tzSelectDateSuggest.attr('data-suggested-tz')) {
    		$tzSelectDateSuggest.css('display', 'none');
    	} else {
    		$tzSelectDateSuggest.css('display', 'inline');
    	}
    
    	var $tzOptions = $timezone.children('optgroup[data-tz-value="' + $tzDate.val() + '"]').children('option');
    
    	if ($tzOptions.length === 1) {
    		// If there is only one timezone for the selected date, we just select that automatically.
    		$tzOptions.prop('selected', true);
    		keepSelection = true;
    	}
    
    	if (typeof keepSelection !== 'undefined' && !keepSelection) {
    		var $timezoneOptions = $timezone.find('optgroup option');
    		if ($timezoneOptions.filter(':selected').length <= 0) {
    			$timezoneOptions.filter(':first').prop('selected', true);
    		}
    	}
    };
    
    /**
    * Display the date/time select
    */
    phpbb.timezoneEnableDateSelection = function() {
    	$('#tz_select_date').css('display', 'block');
    };
    
    /**
    * Preselect a date/time or suggest one, if it is not picked.
    *
    * @param {bool} forceSelector Shall we select the suggestion?
    */
    phpbb.timezonePreselectSelect = function(forceSelector) {
    
    	// The offset returned here is in minutes and negated.
    	var offset = (new Date()).getTimezoneOffset();
    	var sign = '-';
    
    	if (offset < 0) {
    		sign = '+';
    		offset = -offset;
    	}
    
    	var minutes = offset % 60;
    	var hours = (offset - minutes) / 60;
    
    	if (hours === 0) {
    		hours = '00';
    		sign = '+';
    	} else if (hours < 10) {
    		hours = '0' + hours.toString();
    	} else {
    		hours = hours.toString();
    	}
    
    	if (minutes < 10) {
    		minutes = '0' + minutes.toString();
    	} else {
    		minutes = minutes.toString();
    	}
    
    	var prefix = 'UTC' + sign + hours + ':' + minutes;
    	var prefixLength = prefix.length;
    	var selectorOptions = $('option', '#tz_date');
    	var i;
    
    	var $tzSelectDateSuggest = $('#tz_select_date_suggest');
    
    	for (i = 0; i < selectorOptions.length; ++i) {
    		var option = selectorOptions[i];
    
    		if (option.value.substring(0, prefixLength) === prefix) {
    			if ($('#tz_date').val() !== option.value && !forceSelector) {
    				// We do not select the option for the user, but notify him,
    				// that we would suggest a different setting.
    				phpbb.timezoneSwitchDate(true);
    				$tzSelectDateSuggest.css('display', 'inline');
    			} else {
    				option.selected = true;
    				phpbb.timezoneSwitchDate(!forceSelector);
    				$tzSelectDateSuggest.css('display', 'none');
    			}
    
    			var suggestion = $tzSelectDateSuggest.attr('data-l-suggestion');
    
    			$tzSelectDateSuggest.attr('title', suggestion.replace('%s', option.innerHTML));
    			$tzSelectDateSuggest.attr('value', suggestion.replace('%s', option.innerHTML.substring(0, 9)));
    			$tzSelectDateSuggest.attr('data-suggested-tz', option.innerHTML);
    
    			// Found the suggestion, there cannot be more, so return from here.
    			return;
    		}
    	}
    };
    
    phpbb.ajaxCallbacks = {};
    
    /**
     * Adds an AJAX callback to be used by phpbb.ajaxify.
     *
     * See the phpbb.ajaxify comments for information on stuff like parameters.
     *
     * @param {string} id The name of the callback.
     * @param {function} callback The callback to be called.
     */
    phpbb.addAjaxCallback = function(id, callback) {
    	if (typeof callback === 'function') {
    		phpbb.ajaxCallbacks[id] = callback;
    	}
    	return this;
    };
    
    /**
     * This callback handles live member searches.
     */
    phpbb.addAjaxCallback('member_search', function(res) {
    	phpbb.search.handleResponse(res, $(this), false, phpbb.getFunctionByName('phpbb.search.setValueOnClick'));
    });
    
    /**
     * This callback alternates text - it replaces the current text with the text in
     * the alt-text data attribute, and replaces the text in the attribute with the
     * current text so that the process can be repeated.
     */
    phpbb.addAjaxCallback('alt_text', function() {
    	var $anchor,
    		updateAll = $(this).data('update-all'),
    		altText;
    
    	if (updateAll !== undefined && updateAll.length) {
    		$anchor = $(updateAll);
    	} else {
    		$anchor = $(this);
    	}
    
    	$anchor.each(function() {
    		var $this = $(this);
    		altText = $this.attr('data-alt-text');
    		$this.attr('data-alt-text', $.trim($this.text()));
    		$this.attr('title', altText);
    		$this.children('span').text(altText);
    	});
    });
    
    /**
     * This callback is based on the alt_text callback.
     *
     * It replaces the current text with the text in the alt-text data attribute,
     * and replaces the text in the attribute with the current text so that the
     * process can be repeated.
     * Additionally it replaces the class of the link's parent
     * and changes the link itself.
     */
    phpbb.addAjaxCallback('toggle_link', function() {
    	var $anchor,
    		updateAll = $(this).data('update-all') ,
    		toggleText,
    		toggleUrl,
    		toggleClass;
    
    	if (updateAll !== undefined && updateAll.length) {
    		$anchor = $(updateAll);
    	} else {
    		$anchor = $(this);
    	}
    
    	$anchor.each(function() {
    		var $this = $(this);
    
    		// Toggle link url
    		toggleUrl = $this.attr('data-toggle-url');
    		$this.attr('data-toggle-url', $this.attr('href'));
    		$this.attr('href', toggleUrl);
    
    		// Toggle class of link parent
    		toggleClass = $this.attr('data-toggle-class');
    		$this.attr('data-toggle-class', $this.children().attr('class'));
    		$this.children('.icon').attr('class', toggleClass);
    
    		// Toggle link text
    		toggleText = $this.attr('data-toggle-text');
    		$this.attr('data-toggle-text', $this.children('span').text());
    		$this.attr('title', $.trim(toggleText));
    		$this.children('span').text(toggleText);
    	});
    });
    
    /**
    * Automatically resize textarea
    *
    * This function automatically resizes textarea elements when user
    * types text.
    *
    * @param {jQuery} $items jQuery object(s) to resize
    * @param {object} [options] Optional parameter that adjusts default
    * 	configuration. See configuration variable
    *
    * Optional parameters:
    *	minWindowHeight {number} Minimum browser window height when textareas are resized. Default = 500
    *	minHeight {number} Minimum height of textarea. Default = 200
    *	maxHeight {number} Maximum height of textarea. Default = 500
    *	heightDiff {number} Minimum difference between window and textarea height. Default = 200
    *	resizeCallback {function} Function to call after resizing textarea
    *	resetCallback {function} Function to call when resize has been canceled
    
    *		Callback function format: function(item) {}
    *			this points to DOM object
    *			item is a jQuery object, same as this
    */
    phpbb.resizeTextArea = function($items, options) {
    	// Configuration
    	var configuration = {
    		minWindowHeight: 500,
    		minHeight: 200,
    		maxHeight: 500,
    		heightDiff: 200,
    		resizeCallback: function() {},
    		resetCallback: function() {}
    	};
    
    	if (phpbb.isTouch) {
    		return;
    	}
    
    	if (arguments.length > 1) {
    		configuration = $.extend(configuration, options);
    	}
    
    	function resetAutoResize(item) {
    		var $item = $(item);
    		if ($item.hasClass('auto-resized')) {
    			$(item)
    				.css({ height: '', resize: '' })
    				.removeClass('auto-resized');
    			configuration.resetCallback.call(item, $item);
    		}
    	}
    
    	function autoResize(item) {
    		function setHeight(height) {
    			height += parseInt($item.css('height'), 10) - $item.innerHeight();
    			$item
    				.css({ height: height + 'px', resize: 'none' })
    				.addClass('auto-resized');
    			configuration.resizeCallback.call(item, $item);
    		}
    
    		var windowHeight = $(window).height();
    
    		if (windowHeight < configuration.minWindowHeight) {
    			resetAutoResize(item);
    			return;
    		}
    
    		var maxHeight = Math.min(
    				Math.max(windowHeight - configuration.heightDiff, configuration.minHeight),
    				configuration.maxHeight
    			),
    			$item = $(item),
    			height = parseInt($item.innerHeight(), 10),
    			scrollHeight = (item.scrollHeight) ? item.scrollHeight : 0;
    
    		if (height < 0) {
    			return;
    		}
    
    		if (height > maxHeight) {
    			setHeight(maxHeight);
    		} else if (scrollHeight > (height + 5)) {
    			setHeight(Math.min(maxHeight, scrollHeight));
    		}
    	}
    
    	$items.on('focus change keyup', function() {
    		$(this).each(function() {
    			autoResize(this);
    		});
    	}).change();
    
    	$(window).resize(function() {
    		$items.each(function() {
    			if ($(this).hasClass('auto-resized')) {
    				autoResize(this);
    			}
    		});
    	});
    };
    
    /**
    * Check if cursor in textarea is currently inside a bbcode tag
    *
    * @param {object} textarea Textarea DOM object
    * @param {Array} startTags List of start tags to look for
    *		For example, Array('[code]', '[code=')
    * @param {Array} endTags List of end tags to look for
    *		For example, Array('[/code]')
    *
    * @returns {boolean} True if cursor is in bbcode tag
    */
    phpbb.inBBCodeTag = function(textarea, startTags, endTags) {
    	var start = textarea.selectionStart,
    		lastEnd = -1,
    		lastStart = -1,
    		i, index, value;
    
    	if (typeof start !== 'number') {
    		return false;
    	}
    
    	value = textarea.value.toLowerCase();
    
    	for (i = 0; i < startTags.length; i++) {
    		var tagLength = startTags[i].length;
    		if (start >= tagLength) {
    			index = value.lastIndexOf(startTags[i], start - tagLength);
    			lastStart = Math.max(lastStart, index);
    		}
    	}
    	if (lastStart === -1) {
    		return false;
    	}
    
    	if (start > 0) {
    		for (i = 0; i < endTags.length; i++) {
    			index = value.lastIndexOf(endTags[i], start - 1);
    			lastEnd = Math.max(lastEnd, index);
    		}
    	}
    
    	return (lastEnd < lastStart);
    };
    
    
    /**
    * Adjust textarea to manage code bbcode
    *
    * This function allows to use tab characters when typing code
    * and keeps indentation of previous line of code when adding new
    * line while typing code.
    *
    * Editor's functionality is changed only when cursor is between
    * [code] and [/code] bbcode tags.
    *
    * @param {object} textarea Textarea DOM object to apply editor to
    */
    phpbb.applyCodeEditor = function(textarea) {
    	// list of allowed start and end bbcode code tags, in lower case
    	var startTags = ['[code]', '[code='],
    		startTagsEnd = ']',
    		endTags = ['[/code]'];
    
    	if (!textarea || typeof textarea.selectionStart !== 'number') {
    		return;
    	}
    
    	if ($(textarea).data('code-editor') === true) {
    		return;
    	}
    
    	function inTag() {
    		return phpbb.inBBCodeTag(textarea, startTags, endTags);
    	}
    
    	/**
    	* Get line of text before cursor
    	*
    	* @param {boolean} stripCodeStart If true, only part of line
    	*		after [code] tag will be returned.
    	*
    	* @returns {string} Line of text
    	*/
    	function getLastLine(stripCodeStart) {
    		var start = textarea.selectionStart,
    			value = textarea.value,
    			index = value.lastIndexOf('\n', start - 1);
    
    		value = value.substring(index + 1, start);
    
    		if (stripCodeStart) {
    			for (var i = 0; i < startTags.length; i++) {
    				index = value.lastIndexOf(startTags[i]);
    				if (index >= 0) {
    					var tagLength = startTags[i].length;
    
    					value = value.substring(index + tagLength);
    					if (startTags[i].lastIndexOf(startTagsEnd) !== tagLength) {
    						index = value.indexOf(startTagsEnd);
    
    						if (index >= 0) {
    							value = value.substr(index + 1);
    						}
    					}
    				}
    			}
    		}
    
    		return value;
    	}
    
    	/**
    	* Append text at cursor position
    	*
    	* @param {string} text Text to append
    	*/
    	function appendText(text) {
    		var start = textarea.selectionStart,
    			end = textarea.selectionEnd,
    			value = textarea.value;
    
    		textarea.value = value.substr(0, start) + text + value.substr(end);
    		textarea.selectionStart = textarea.selectionEnd = start + text.length;
    	}
    
    	$(textarea).data('code-editor', true).on('keydown', function(event) {
    		var key = event.keyCode || event.which;
    
    		// intercept tabs
    		if (key === keymap.TAB	&&
    			!event.ctrlKey		&&
    			!event.shiftKey		&&
    			!event.altKey		&&
    			!event.metaKey) {
    			if (inTag()) {
    				appendText('\t');
    				event.preventDefault();
    				return;
    			}
    		}
    
    		// intercept new line characters
    		if (key === keymap.ENTER) {
    			if (inTag()) {
    				var lastLine = getLastLine(true),
    					code = '' + /^\s*/g.exec(lastLine);
    
    				if (code.length > 0) {
    					appendText('\n' + code);
    					event.preventDefault();
    				}
    			}
    		}
    	});
    };
    
    /**
     * Show drag and drop animation when textarea is present
     *
     * This function will enable the drag and drop animation for a specified
     * textarea.
     *
     * @param {HTMLElement} textarea Textarea DOM object to apply editor to
     */
    phpbb.showDragNDrop = function(textarea) {
    	if (!textarea) {
    		return;
    	}
    
    	$('body').on('dragenter dragover', function () {
    		$(textarea).addClass('drag-n-drop');
    	}).on('dragleave dragout dragend drop', function() {
    		$(textarea).removeClass('drag-n-drop');
    	});
    	$(textarea).on('dragenter dragover', function () {
    		$(textarea).addClass('drag-n-drop-highlight');
    	}).on('dragleave dragout dragend drop', function() {
    		$(textarea).removeClass('drag-n-drop-highlight');
    	});
    };
    
    /**
    * List of classes that toggle dropdown menu,
    * list of classes that contain visible dropdown menu
    *
    * Add your own classes to strings with comma (probably you
    * will never need to do that)
    */
    phpbb.dropdownHandles = '.dropdown-container.dropdown-visible .dropdown-toggle';
    phpbb.dropdownVisibleContainers = '.dropdown-container.dropdown-visible';
    
    /**
    * Dropdown toggle event handler
    * This handler is used by phpBB.registerDropdown() and other functions
    */
    phpbb.toggleDropdown = function() {
    	var $this = $(this),
    		options = $this.data('dropdown-options'),
    		parent = options.parent,
    		visible = parent.hasClass('dropdown-visible'),
    		direction;
    
    	if (!visible) {
    		// Hide other dropdown menus
    		$(phpbb.dropdownHandles).each(phpbb.toggleDropdown);
    
    		// Figure out direction of dropdown
    		direction = options.direction;
    		var verticalDirection = options.verticalDirection,
    			offset = $this.offset();
    
    		if (direction === 'auto') {
    			if (($(window).width() - $this.outerWidth(true)) / 2 > offset.left) {
    				direction = 'right';
    			} else {
    				direction = 'left';
    			}
    		}
    		parent.toggleClass(options.leftClass, direction === 'left')
    			.toggleClass(options.rightClass, direction === 'right');
    
    		if (verticalDirection === 'auto') {
    			var height = $(window).height(),
    				top = offset.top - $(window).scrollTop();
    
    			verticalDirection = (top < height * 0.7) ? 'down' : 'up';
    		}
    		parent.toggleClass(options.upClass, verticalDirection === 'up')
    			.toggleClass(options.downClass, verticalDirection === 'down');
    	}
    
    	options.dropdown.toggle();
    	parent.toggleClass(options.visibleClass, !visible)
    		.toggleClass('dropdown-visible', !visible);
    
    	// Check dimensions when showing dropdown
    	// !visible because variable shows state of dropdown before it was toggled
    	if (!visible) {
    		var windowWidth = $(window).width();
    
    		options.dropdown.find('.dropdown-contents').each(function() {
    			var $this = $(this);
    
    			$this.css({
    				marginLeft: 0,
    				left: 0,
    				marginRight: 0,
    				maxWidth: (windowWidth - 4) + 'px'
    			});
    
    			var offset = $this.offset().left,
    				width = $this.outerWidth(true);
    
    			if (offset < 2) {
    				$this.css('left', (2 - offset) + 'px');
    			} else if ((offset + width + 2) > windowWidth) {
    				$this.css('margin-left', (windowWidth - offset - width - 2) + 'px');
    			}
    
    			// Check whether the vertical scrollbar is present.
    			$this.toggleClass('dropdown-nonscroll', this.scrollHeight === $this.innerHeight());
    
    		});
    		var freeSpace = parent.offset().left - 4;
    
    		if (direction === 'left') {
    			options.dropdown.css('margin-left', '-' + freeSpace + 'px');
    
    			// Try to position the notification dropdown correctly in RTL-responsive mode
    			if (options.dropdown.hasClass('dropdown-extended')) {
    				var contentWidth,
    					fullFreeSpace = freeSpace + parent.outerWidth();
    
    				options.dropdown.find('.dropdown-contents').each(function() {
    					contentWidth = parseInt($(this).outerWidth(), 10);
    					$(this).css({ marginLeft: 0, left: 0 });
    				});
    
    				var maxOffset = Math.min(contentWidth, fullFreeSpace) + 'px';
    				options.dropdown.css({
    					width: maxOffset,
    					marginLeft: -maxOffset
    				});
    			}
    		} else {
    			options.dropdown.css('margin-right', '-' + (windowWidth + freeSpace) + 'px');
    		}
    	}
    
    	// Prevent event propagation
    	if (arguments.length > 0) {
    		try {
    			var e = arguments[0];
    			e.preventDefault();
    			e.stopPropagation();
    		} catch (error) { }
    	}
    	return false;
    };
    
    /**
    * Toggle dropdown submenu
    */
    phpbb.toggleSubmenu = function(e) {
    	$(this).siblings('.dropdown-submenu').toggle();
    	e.preventDefault();
    };
    
    /**
    * Register dropdown menu
    * Shows/hides dropdown, decides which side to open to
    *
    * @param {jQuery} toggle Link that toggles dropdown.
    * @param {jQuery} dropdown Dropdown menu.
    * @param {Object} options List of options. Optional.
    */
    phpbb.registerDropdown = function(toggle, dropdown, options) {
    	var ops = {
    			parent: toggle.parent(), // Parent item to add classes to
    			direction: 'auto', // Direction of dropdown menu. Possible values: auto, left, right
    			verticalDirection: 'auto', // Vertical direction. Possible values: auto, up, down
    			visibleClass: 'visible', // Class to add to parent item when dropdown is visible
    			leftClass: 'dropdown-left', // Class to add to parent item when dropdown opens to left side
    			rightClass: 'dropdown-right', // Class to add to parent item when dropdown opens to right side
    			upClass: 'dropdown-up', // Class to add to parent item when dropdown opens above menu item
    			downClass: 'dropdown-down' // Class to add to parent item when dropdown opens below menu item
    		};
    	if (options) {
    		ops = $.extend(ops, options);
    	}
    	ops.dropdown = dropdown;
    
    	ops.parent.addClass('dropdown-container');
    	toggle.addClass('dropdown-toggle');
    
    	toggle.data('dropdown-options', ops);
    
    	toggle.click(phpbb.toggleDropdown);
    	$('.dropdown-toggle-submenu', ops.parent).click(phpbb.toggleSubmenu);
    };
    
    /**
    * Get the HTML for a color palette table.
    *
    * @param {string} dir Palette direction - either v or h
    * @param {int} width Palette cell width.
    * @param {int} height Palette cell height.
    */
    phpbb.colorPalette = function(dir, width, height) {
    	var r, g, b,
    		numberList = new Array(6),
    		color = '',
    		html = '';
    
    	numberList[0] = '00';
    	numberList[1] = '40';
    	numberList[2] = '80';
    	numberList[3] = 'BF';
    	numberList[4] = 'FF';
    
    	var tableClass = (dir === 'h') ? 'horizontal-palette' : 'vertical-palette';
    	html += '<table class="not-responsive colour-palette ' + tableClass + '" style="width: auto;">';
    
    	for (r = 0; r < 5; r++) {
    		if (dir === 'h') {
    			html += '<tr>';
    		}
    
    		for (g = 0; g < 5; g++) {
    			if (dir === 'v') {
    				html += '<tr>';
    			}
    
    			for (b = 0; b < 5; b++) {
    				color = '' + numberList[r] + numberList[g] + numberList[b];
    				html += '<td style="background-color: #' + color + '; width: ' + width + 'px; height: ' +
    					height + 'px;"><a href="#" data-color="' + color + '" style="display: block; width: ' +
    					width + 'px; height: ' + height + 'px; " alt="#' + color + '" title="#' + color + '"></a>';
    				html += '</td>';
    			}
    
    			if (dir === 'v') {
    				html += '</tr>';
    			}
    		}
    
    		if (dir === 'h') {
    			html += '</tr>';
    		}
    	}
    	html += '</table>';
    	return html;
    };
    
    /**
    * Register a color palette.
    *
    * @param {jQuery} el jQuery object for the palette container.
    */
    phpbb.registerPalette = function(el) {
    	var	orientation	= el.attr('data-color-palette') || el.attr('data-orientation'), // data-orientation kept for backwards compat.
    		height		= el.attr('data-height'),
    		width		= el.attr('data-width'),
    		target		= el.attr('data-target'),
    		bbcode		= el.attr('data-bbcode');
    
    	// Insert the palette HTML into the container.
    	el.html(phpbb.colorPalette(orientation, width, height));
    
    	// Add toggle control.
    	$('#color_palette_toggle').click(function(e) {
    		el.toggle();
    		e.preventDefault();
    	});
    
    	// Attach event handler when a palette cell is clicked.
    	$(el).on('click', 'a', function(e) {
    		var color = $(this).attr('data-color');
    
    		if (bbcode) {
    			bbfontstyle('[color=#' + color + ']', '[/color]');
    		} else {
    			$(target).val(color);
    		}
    		e.preventDefault();
    	});
    };
    
    /**
    * Set display of page element
    *
    * @param {string} id The ID of the element to change
    * @param {int} action Set to 0 if element display should be toggled, -1 for
    *			hiding the element, and 1 for showing it.
    * @param {string} type Display type that should be used, e.g. inline, block or
    *			other CSS "display" types
    */
    phpbb.toggleDisplay = function(id, action, type) {
    	if (!type) {
    		type = 'block';
    	}
    
    	var $element = $('#' + id);
    
    	var display = $element.css('display');
    	if (!action) {
    		action = (display === '' || display === type) ? -1 : 1;
    	}
    	$element.css('display', ((action === 1) ? type : 'none'));
    };
    
    /**
    * Toggle additional settings based on the selected
    * option of select element.
    *
    * @param {jQuery} el jQuery select element object.
    */
    phpbb.toggleSelectSettings = function(el) {
    	el.children().each(function() {
    		var $this = $(this),
    			$setting = $($this.data('toggle-setting'));
    		$setting.toggle($this.is(':selected'));
    
    		// Disable any input elements that are not visible right now
    		if ($this.is(':selected')) {
    			$($this.data('toggle-setting') + ' input').prop('disabled', false);
    		} else {
    			$($this.data('toggle-setting') + ' input').prop('disabled', true);
    		}
    	});
    };
    
    /**
    * Get function from name.
    * Based on http://stackoverflow.com/a/359910
    *
    * @param {string} functionName Function to get.
    * @returns function
    */
    phpbb.getFunctionByName = function (functionName) {
    	var namespaces = functionName.split('.'),
    		func = namespaces.pop(),
    		context = window;
    
    	for (var i = 0; i < namespaces.length; i++) {
    		context = context[namespaces[i]];
    	}
    	return context[func];
    };
    
    /**
    * Register page dropdowns.
    */
    phpbb.registerPageDropdowns = function() {
    	var $body = $('body');
    
    	$body.find('.dropdown-container').each(function() {
    		var $this = $(this),
    			$trigger = $this.find('.dropdown-trigger:first'),
    			$contents = $this.find('.dropdown'),
    			options = {
    				direction: 'auto',
    				verticalDirection: 'auto'
    			},
    			data;
    
    		if (!$trigger.length) {
    			data = $this.attr('data-dropdown-trigger');
    			$trigger = data ? $this.children(data) : $this.children('a:first');
    		}
    
    		if (!$contents.length) {
    			data = $this.attr('data-dropdown-contents');
    			$contents = data ? $this.children(data) : $this.children('div:first');
    		}
    
    		if (!$trigger.length || !$contents.length) {
    			return;
    		}
    
    		if ($this.hasClass('dropdown-up')) {
    			options.verticalDirection = 'up';
    		}
    		if ($this.hasClass('dropdown-down')) {
    			options.verticalDirection = 'down';
    		}
    		if ($this.hasClass('dropdown-left')) {
    			options.direction = 'left';
    		}
    		if ($this.hasClass('dropdown-right')) {
    			options.direction = 'right';
    		}
    
    		phpbb.registerDropdown($trigger, $contents, options);
    	});
    
    	// Hide active dropdowns when click event happens outside
    	$body.click(function(e) {
    		var $parents = $(e.target).parents();
    		if (!$parents.is(phpbb.dropdownVisibleContainers)) {
    			$(phpbb.dropdownHandles).each(phpbb.toggleDropdown);
    		}
    	});
    };
    
    /**
     * Handle avatars to be lazy loaded.
     */
    phpbb.lazyLoadAvatars = function loadAvatars() {
    	$('.avatar[data-src]').each(function () {
    		var $avatar = $(this);
    
    		$avatar
    			.attr('src', $avatar.data('src'))
    			.removeAttr('data-src');
    	});
    };
    
    phpbb.recaptcha = {
    	button: null,
    	ready: false,
    
    	token: $('input[name="recaptcha_token"]'),
    	form: $('.g-recaptcha').parents('form'),
    	v3: $('[data-recaptcha-v3]'),
    
    	load: function() {
    		phpbb.recaptcha.bindButton();
    		phpbb.recaptcha.bindForm();
    	},
    	bindButton: function() {
    		phpbb.recaptcha.form.find('input[type="submit"]').on('click', function() {
    			// Listen to all the submit buttons for the form that has reCAPTCHA protection,
    			// and store it so we can click the exact same button later on when we are ready.
    			phpbb.recaptcha.button = this;
    		});
    	},
    	bindForm: function() {
    		phpbb.recaptcha.form.on('submit', function(e) {
    			 // If ready is false, it means the user pressed a submit button.
    			 // And the form was not submitted by us, after the token was loaded.
    			if (!phpbb.recaptcha.ready) {
    				 // If version 3 is used, we need to make a different execution,
    				 // including the action and the site key.
    				if (phpbb.recaptcha.v3.length) {
    					grecaptcha.execute(
    						phpbb.recaptcha.v3.data('recaptcha-v3'),
    						{action: phpbb.recaptcha.v3.val()}
    					).then(function(token) {
    						// Place the token inside the form
    						phpbb.recaptcha.token.val(token);
    
    						// And now we submit the form.
    						phpbb.recaptcha.submitForm();
    					});
    				} else {
    					// Regular version 2 execution
    					grecaptcha.execute();
    				}
    
    				// Do not submit the form
    				e.preventDefault();
    			}
    		});
    	},
    	submitForm: function() {
    		// Now we are ready, so set it to true.
    		// so the 'submit' event doesn't run multiple times.
    		phpbb.recaptcha.ready = true;
    
    		if (phpbb.recaptcha.button) {
    			// If there was a specific button pressed initially, trigger the same button
    			phpbb.recaptcha.button.click();
    		} else {
    			if (typeof phpbb.recaptcha.form.submit !== 'function') {
    				// Rename input[name="submit"] so that we can submit the form
    				phpbb.recaptcha.form.submit.name = 'submit_btn';
    			}
    
    			phpbb.recaptcha.form.submit();
    		}
    	}
    };
    
    // reCAPTCHA v2 doesn't accept callback functions nested inside objects
    // so we need to make this helper functions here
    window.phpbbRecaptchaOnLoad = function() {
    	phpbb.recaptcha.load();
    };
    
    window.phpbbRecaptchaOnSubmit = function() {
    	phpbb.recaptcha.submitForm();
    };
    
    $(window).on('load', phpbb.lazyLoadAvatars);
    
    /**
    * Apply code editor to all textarea elements with data-bbcode attribute
    */
    $(function() {
    	// reCAPTCHA v3 needs to be initialized
    	if (phpbb.recaptcha.v3.length) {
    		phpbb.recaptcha.load();
    	}
    
    	$('textarea[data-bbcode]').each(function() {
    		phpbb.applyCodeEditor(this);
    	});
    
    	phpbb.registerPageDropdowns();
    
    	$('[data-color-palette], [data-orientation]').each(function() {
    		phpbb.registerPalette($(this));
    	});
    
    	// Update browser history URL to point to specific post in viewtopic.php
    	// when using view=unread#unread link.
    	phpbb.history.replaceUrl($('#unread[data-url]').data('url'));
    
    	// Hide settings that are not selected via select element.
    	$('select[data-togglable-settings]').each(function() {
    		var $this = $(this);
    
    		$this.change(function() {
    			phpbb.toggleSelectSettings($this);
    		});
    		phpbb.toggleSelectSettings($this);
    	});
    });
    
    })(jQuery); // Avoid conflicts with other libraries