Seditio Source
Root |
./othercms/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