javascript/core.js 0000755 0000000 0000000 00000143176 14516425663 011433 0 ustar /* 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 = $('#loading_indicator');
}
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 = $('
').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('&', '&');
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('&', '&');
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 < 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 += '
';
for (r = 0; r < 5; r++) {
if (dir === 'h') {
html += '
';
}
for (g = 0; g < 5; g++) {
if (dir === 'v') {
html += '
';
}
for (b = 0; b < 5; b++) {
color = '' + numberList[r] + numberList[g] + numberList[b];
html += '
';
html += '
';
}
if (dir === 'v') {
html += '
';
}
}
if (dir === 'h') {
html += '';
}
}
html += '
';
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');
});
};
var recaptchaForm = $('.g-recaptcha').parents('form');
var submitButton = null;
var programaticallySubmitted = false;
phpbb.recaptchaOnLoad = function () {
// Listen to submit buttons in order to know which one was pressed
$('input[type="submit"]').each(function () {
$(this).on('click', function () {
submitButton = this;
});
});
recaptchaForm.on('submit', function (e) {
if (!programaticallySubmitted) {
grecaptcha.execute();
e.preventDefault();
}
});
}
phpbb.recaptchaOnSubmit = function () {
programaticallySubmitted = true;
// If concrete button was clicked (e.g. preview instead of submit),
// let's trigger the same action
if (submitButton) {
submitButton.click();
} else {
// Rename input[name="submit"] so that we can submit the form
if (typeof recaptchaForm.submit !== 'function') {
recaptchaForm.submit.name = 'submit_btn';
}
recaptchaForm.submit();
}
}
// reCAPTCHA doesn't accept callback functions nested inside objects
// so we need to make this helper functions here
window.phpbbRecaptchaOnLoad = function() {
phpbb.recaptchaOnLoad();
}
window.phpbbRecaptchaOnSubmit = function() {
phpbb.recaptchaOnSubmit();
}
$(window).on('load', phpbb.lazyLoadAvatars);
/**
* Apply code editor to all textarea elements with data-bbcode attribute
*/
$(function() {
$('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
javascript/editor.js 0000755 0000000 0000000 00000026653 14516425663 011771 0 ustar /**
* bbCode control by subBlue design [ www.subBlue.com ]
* Includes unixsafe colour palette selector by SHS`
*/
// Startup variables
var imageTag = false;
var theSelection = false;
var bbcodeEnabled = true;
// Check for Browser & Platform for PC & IE specific bits
// More details from: http://www.mozilla.org/docs/web-developer/sniffer/browser_type.html
var clientPC = navigator.userAgent.toLowerCase(); // Get client info
var clientVer = parseInt(navigator.appVersion, 10); // Get browser version
var is_ie = ((clientPC.indexOf('msie') !== -1) && (clientPC.indexOf('opera') === -1));
var is_win = ((clientPC.indexOf('win') !== -1) || (clientPC.indexOf('16bit') !== -1));
var baseHeight;
/**
* Fix a bug involving the TextRange object. From
* http://www.frostjedi.com/terra/scripts/demo/caretBug.html
*/
function initInsertions() {
var doc;
if (document.forms[form_name]) {
doc = document;
} else {
doc = opener.document;
}
var textarea = doc.forms[form_name].elements[text_name];
if (is_ie && typeof(baseHeight) !== 'number') {
textarea.focus();
baseHeight = doc.selection.createRange().duplicate().boundingHeight;
if (!document.forms[form_name]) {
document.body.focus();
}
}
}
/**
* bbstyle
*/
function bbstyle(bbnumber) {
if (bbnumber !== -1) {
bbfontstyle(bbtags[bbnumber], bbtags[bbnumber+1]);
} else {
insert_text('[*]');
document.forms[form_name].elements[text_name].focus();
}
}
/**
* Apply bbcodes
*/
function bbfontstyle(bbopen, bbclose) {
theSelection = false;
var textarea = document.forms[form_name].elements[text_name];
textarea.focus();
if ((clientVer >= 4) && is_ie && is_win) {
// Get text selection
theSelection = document.selection.createRange().text;
if (theSelection) {
// Add tags around selection
document.selection.createRange().text = bbopen + theSelection + bbclose;
textarea.focus();
theSelection = '';
return;
}
} else if (textarea.selectionEnd && (textarea.selectionEnd - textarea.selectionStart > 0)) {
mozWrap(textarea, bbopen, bbclose);
textarea.focus();
theSelection = '';
return;
}
//The new position for the cursor after adding the bbcode
var caret_pos = getCaretPosition(textarea).start;
var new_pos = caret_pos + bbopen.length;
// Open tag
insert_text(bbopen + bbclose);
// Center the cursor when we don't have a selection
// Gecko and proper browsers
if (!isNaN(textarea.selectionStart)) {
textarea.selectionStart = new_pos;
textarea.selectionEnd = new_pos;
}
// IE
else if (document.selection) {
var range = textarea.createTextRange();
range.move("character", new_pos);
range.select();
storeCaret(textarea);
}
textarea.focus();
}
/**
* Insert text at position
*/
function insert_text(text, spaces, popup) {
var textarea;
if (!popup) {
textarea = document.forms[form_name].elements[text_name];
} else {
textarea = opener.document.forms[form_name].elements[text_name];
}
if (spaces) {
text = ' ' + text + ' ';
}
// Since IE9, IE also has textarea.selectionStart, but it still needs to be treated the old way.
// Therefore we simply add a !is_ie here until IE fixes the text-selection completely.
if (!isNaN(textarea.selectionStart) && !is_ie) {
var sel_start = textarea.selectionStart;
var sel_end = textarea.selectionEnd;
mozWrap(textarea, text, '');
textarea.selectionStart = sel_start + text.length;
textarea.selectionEnd = sel_end + text.length;
} else if (textarea.createTextRange && textarea.caretPos) {
if (baseHeight !== textarea.caretPos.boundingHeight) {
textarea.focus();
storeCaret(textarea);
}
var caret_pos = textarea.caretPos;
caret_pos.text = caret_pos.text.charAt(caret_pos.text.length - 1) === ' ' ? caret_pos.text + text + ' ' : caret_pos.text + text;
} else {
textarea.value = textarea.value + text;
}
if (!popup) {
textarea.focus();
}
}
/**
* Add inline attachment at position
*/
function attachInline(index, filename) {
insert_text('[attachment=' + index + ']' + filename + '[/attachment]');
document.forms[form_name].elements[text_name].focus();
}
/**
* Add quote text to message
*/
function addquote(post_id, username, l_wrote, attributes) {
var message_name = 'message_' + post_id;
var theSelection = '';
var divarea = false;
var i;
if (l_wrote === undefined) {
// Backwards compatibility
l_wrote = 'wrote';
}
if (typeof attributes !== 'object') {
attributes = {};
}
if (document.all) {
divarea = document.all[message_name];
} else {
divarea = document.getElementById(message_name);
}
// Get text selection - not only the post content :(
// IE9 must use the document.selection method but has the *.getSelection so we just force no IE
if (window.getSelection && !is_ie && !window.opera) {
theSelection = window.getSelection().toString();
} else if (document.getSelection && !is_ie) {
theSelection = document.getSelection();
} else if (document.selection) {
theSelection = document.selection.createRange().text;
}
if (theSelection === '' || typeof theSelection === 'undefined' || theSelection === null) {
if (divarea.innerHTML) {
theSelection = divarea.innerHTML.replace(/ /ig, '\n');
theSelection = theSelection.replace(/ /ig, '\n');
theSelection = theSelection.replace(/<\;/ig, '<');
theSelection = theSelection.replace(/>\;/ig, '>');
theSelection = theSelection.replace(/&\;/ig, '&');
theSelection = theSelection.replace(/ \;/ig, ' ');
} else if (document.all) {
theSelection = divarea.innerText;
} else if (divarea.textContent) {
theSelection = divarea.textContent;
} else if (divarea.firstChild.nodeValue) {
theSelection = divarea.firstChild.nodeValue;
}
}
if (theSelection) {
if (bbcodeEnabled) {
attributes.author = username;
insert_text(generateQuote(theSelection, attributes));
} else {
insert_text(username + ' ' + l_wrote + ':' + '\n');
var lines = split_lines(theSelection);
for (i = 0; i < lines.length; i++) {
insert_text('> ' + lines[i] + '\n');
}
}
}
}
/**
* Create a quote block for given text
*
* Possible attributes:
* - author: author's name (usually a username)
* - post_id: post_id of the post being quoted
* - user_id: user_id of the user being quoted
* - time: timestamp of the original message
*
* @param {!string} text Quote's text
* @param {!Object} attributes Quote's attributes
* @return {!string} Quote block to be used in a new post/text
*/
function generateQuote(text, attributes) {
text = text.replace(/^\s+/, '').replace(/\s+$/, '');
var quote = '[quote';
if (attributes.author) {
// Add the author as the BBCode's default attribute
quote += '=' + formatAttributeValue(attributes.author);
delete attributes.author;
}
for (var name in attributes) {
if (attributes.hasOwnProperty(name)) {
var value = attributes[name];
quote += ' ' + name + '=' + formatAttributeValue(value.toString());
}
}
quote += ']';
var newline = ((quote + text + '[/quote]').length > 80 || text.indexOf('\n') > -1) ? '\n' : '';
quote += newline + text + newline + '[/quote]';
return quote;
}
/**
* Format given string to be used as an attribute value
*
* Will return the string as-is if it can be used in a BBCode without quotes. Otherwise,
* it will use either single- or double- quotes depending on whichever requires less escaping.
* Quotes and backslashes are escaped with backslashes where necessary
*
* @param {!string} str Original string
* @return {!string} Same string if possible, escaped string within quotes otherwise
*/
function formatAttributeValue(str) {
if (!/[ "'\\\]]/.test(str)) {
// Return as-is if it contains none of: space, ' " \ or ]
return str;
}
var singleQuoted = "'" + str.replace(/[\\']/g, '\\$&') + "'",
doubleQuoted = '"' + str.replace(/[\\"]/g, '\\$&') + '"';
return (singleQuoted.length < doubleQuoted.length) ? singleQuoted : doubleQuoted;
}
function split_lines(text) {
var lines = text.split('\n');
var splitLines = new Array();
var j = 0;
var i;
for(i = 0; i < lines.length; i++) {
if (lines[i].length <= 80) {
splitLines[j] = lines[i];
j++;
} else {
var line = lines[i];
var splitAt;
do {
splitAt = line.indexOf(' ', 80);
if (splitAt === -1) {
splitLines[j] = line;
j++;
} else {
splitLines[j] = line.substring(0, splitAt);
line = line.substring(splitAt);
j++;
}
}
while(splitAt !== -1);
}
}
return splitLines;
}
/**
* From http://www.massless.org/mozedit/
*/
function mozWrap(txtarea, open, close) {
var selLength = (typeof(txtarea.textLength) === 'undefined') ? txtarea.value.length : txtarea.textLength;
var selStart = txtarea.selectionStart;
var selEnd = txtarea.selectionEnd;
var scrollTop = txtarea.scrollTop;
var s1 = (txtarea.value).substring(0,selStart);
var s2 = (txtarea.value).substring(selStart, selEnd);
var s3 = (txtarea.value).substring(selEnd, selLength);
txtarea.value = s1 + open + s2 + close + s3;
txtarea.selectionStart = selStart + open.length;
txtarea.selectionEnd = selEnd + open.length;
txtarea.focus();
txtarea.scrollTop = scrollTop;
return;
}
/**
* Insert at Caret position. Code from
* http://www.faqts.com/knowledge_base/view.phtml/aid/1052/fid/130
*/
function storeCaret(textEl) {
if (textEl.createTextRange && document.selection) {
textEl.caretPos = document.selection.createRange().duplicate();
}
}
/**
* Caret Position object
*/
function caretPosition() {
var start = null;
var end = null;
}
/**
* Get the caret position in an textarea
*/
function getCaretPosition(txtarea) {
var caretPos = new caretPosition();
// simple Gecko/Opera way
if (txtarea.selectionStart || txtarea.selectionStart === 0) {
caretPos.start = txtarea.selectionStart;
caretPos.end = txtarea.selectionEnd;
}
// dirty and slow IE way
else if (document.selection) {
// get current selection
var range = document.selection.createRange();
// a new selection of the whole textarea
var range_all = document.body.createTextRange();
range_all.moveToElementText(txtarea);
// calculate selection start point by moving beginning of range_all to beginning of range
var sel_start;
for (sel_start = 0; range_all.compareEndPoints('StartToStart', range) < 0; sel_start++) {
range_all.moveStart('character', 1);
}
txtarea.sel_start = sel_start;
// we ignore the end value for IE, this is already dirty enough and we don't need it
caretPos.start = txtarea.sel_start;
caretPos.end = txtarea.sel_start;
}
return caretPos;
}
/**
* Allow to use tab character when typing code
* Keep indentation of last line of code when typing code
*/
(function($) {
$(document).ready(function() {
var doc, textarea;
// find textarea, make sure browser supports necessary functions
if (document.forms[form_name]) {
doc = document;
} else {
doc = opener.document;
}
if (!doc.forms[form_name]) {
return;
}
textarea = doc.forms[form_name].elements[text_name];
phpbb.applyCodeEditor(textarea);
if ($('#attach-panel').length) {
phpbb.showDragNDrop(textarea);
}
$('textarea').on('keydown', function (e) {
if (e.which === 13 && (e.metaKey || e.ctrlKey)) {
$(this).closest('form').find(':submit').click();
}
});
});
})(jQuery);
javascript/installer.js 0000755 0000000 0000000 00000037336 14516425663 012500 0 ustar /**
* Installer's AJAX frontend handler
*/
(function($) { // Avoid conflicts with other libraries
'use strict';
// Installer variables
var pollTimer = null;
var nextReadPosition = 0;
var progressBarTriggered = false;
var progressTimer = null;
var currentProgress = 0;
var refreshRequested = false;
var transmissionOver = false;
var statusCount = 0;
// Template related variables
var $contentWrapper = $('.install-body').find('.main');
// Intercept form submits
interceptFormSubmit($('#install_install'));
/**
* Creates an XHR object
*
* jQuery cannot be used as the response is streamed, and
* as of now, jQuery does not provide access to the response until
* the connection is not closed.
*
* @return XMLHttpRequest
*/
function createXhrObject() {
return new XMLHttpRequest();
}
/**
* Displays error, warning and log messages
*
* @param type
* @param messages
*/
function addMessage(type, messages) {
// Get message containers
var $errorContainer = $('#error-container');
var $warningContainer = $('#warning-container');
var $logContainer = $('#log-container');
var $title, $description, $msgElement, arraySize = messages.length;
for (var i = 0; i < arraySize; i++) {
$msgElement = $('');
$title = $('');
$title.text(messages[i].title);
$msgElement.append($title);
if (messages[i].hasOwnProperty('description')) {
$description = $('');
$description.html(messages[i].description);
$msgElement.append($description);
}
switch (type) {
case 'error':
$msgElement.addClass('errorbox');
$errorContainer.append($msgElement);
break;
case 'warning':
$msgElement.addClass('warningbox');
$warningContainer.append($msgElement);
break;
case 'log':
$msgElement.addClass('log');
$logContainer.prepend($msgElement);
$logContainer.addClass('show_log_container');
break;
case 'success':
$msgElement.addClass('successbox');
$errorContainer.prepend($msgElement);
break;
}
}
}
/**
* Render a download box
*/
function addDownloadBox(downloadArray)
{
var $downloadContainer = $('#download-wrapper');
var $downloadBox, $title, $content, $link;
for (var i = 0; i < downloadArray.length; i++) {
$downloadBox = $('');
$downloadBox.addClass('download-box');
$title = $('');
$title.text(downloadArray[i].title);
$downloadBox.append($title);
if (downloadArray[i].hasOwnProperty('msg')) {
$content = $('');
$content.text(downloadArray[i].msg);
$downloadBox.append($content);
}
$link = $('');
$link.addClass('button1');
$link.attr('href', downloadArray[i].href);
$link.text(downloadArray[i].download);
$downloadBox.append($link);
$downloadContainer.append($downloadBox);
}
}
/**
* Render update files' status
*/
function addUpdateFileStatus(fileStatus)
{
var $statusContainer = $('#file-status-wrapper');
$statusContainer.html(fileStatus);
}
/**
* Displays a form from the response
*
* @param formHtml
*/
function addForm(formHtml) {
var $formContainer = $('#form-wrapper');
$formContainer.html(formHtml);
var $form = $('#install_install');
interceptFormSubmit($form);
}
/**
* Handles navigation status updates
*
* @param navObj
*/
function updateNavbarStatus(navObj) {
var navID, $stage, $stageListItem, $active;
$active = $('#activemenu');
if (navObj.hasOwnProperty('finished')) {
// This should be an Array
var navItems = navObj.finished;
for (var i = 0; i < navItems.length; i++) {
navID = 'installer-stage-' + navItems[i];
$stage = $('#' + navID);
$stageListItem = $stage.parent();
if ($active.length && $active.is($stageListItem)) {
$active.removeAttr('id');
}
$stage.addClass('completed');
}
}
if (navObj.hasOwnProperty('active')) {
navID = 'installer-stage-' + navObj.active;
$stage = $('#' + navID);
$stageListItem = $stage.parent();
if ($active.length && !$active.is($stageListItem)) {
$active.removeAttr('id');
}
$stageListItem.attr('id', 'activemenu');
}
}
/**
* Renders progress bar
*
* @param progressObject
*/
function setProgress(progressObject) {
var $statusText, $progressBar, $progressText, $progressFiller, $progressFillerText;
if (progressObject.task_name.length) {
if (!progressBarTriggered) {
// Create progress bar
var $progressBarWrapper = $('#progress-bar-container');
// Create progress bar elements
$progressBar = $('');
$progressBar.attr('id', 'progress-bar');
$progressText = $('');
$progressText.attr('id', 'progress-bar-text');
$progressFiller = $('');
$progressFiller.attr('id', 'progress-bar-filler');
$progressFillerText = $('');
$progressFillerText.attr('id', 'progress-bar-filler-text');
$statusText = $('');
$statusText.attr('id', 'progress-status-text');
$progressFiller.append($progressFillerText);
$progressBar.append($progressText);
$progressBar.append($progressFiller);
$progressBarWrapper.append($statusText);
$progressBarWrapper.append($progressBar);
$progressFillerText.css('width', $progressBar.width());
progressBarTriggered = true;
} else if (progressObject.hasOwnProperty('restart')) {
clearInterval(progressTimer);
$progressFiller = $('#progress-bar-filler');
$progressFillerText = $('#progress-bar-filler-text');
$progressText = $('#progress-bar-text');
$statusText = $('#progress-status-text');
$progressText.text('0%');
$progressFillerText.text('0%');
$progressFiller.css('width', '0%');
currentProgress = 0;
} else {
$statusText = $('#progress-status-text');
}
// Update progress bar
$statusText.text(progressObject.task_name + '…');
incrementProgressBar(Math.round(progressObject.task_num / progressObject.task_count * 100));
}
}
// Set cookies
function setCookies(cookies) {
var cookie;
for (var i = 0; i < cookies.length; i++) {
// Set cookie name and value
cookie = encodeURIComponent(cookies[i].name) + '=' + encodeURIComponent(cookies[i].value);
// Set path
cookie += '; path=/';
document.cookie = cookie;
}
}
// Redirects user
function redirect(url, use_ajax) {
if (use_ajax) {
resetPolling();
var xhReq = createXhrObject();
xhReq.open('GET', url, true);
xhReq.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
xhReq.send();
startPolling(xhReq);
} else {
window.location.href = url;
}
}
/**
* Parse messages from the response object
*
* @param messageJSON
*/
function parseMessage(messageJSON) {
$('#loading_indicator').css('display', 'none');
var responseObject;
try {
responseObject = JSON.parse(messageJSON);
} catch (err) {
if (window.console) {
console.log('Failed to parse JSON object\n\nMessage: ' + err.message + '\n\nServer Response: ' + messageJSON);
} else {
alert('Failed to parse JSON object\n\nMessage: ' + err.message + '\n\nServer Response: ' + messageJSON);
}
resetPolling();
return;
}
// Parse object
if (responseObject.hasOwnProperty('errors')) {
addMessage('error', responseObject.errors);
}
if (responseObject.hasOwnProperty('warnings')) {
addMessage('warning', responseObject.warnings);
}
if (responseObject.hasOwnProperty('logs')) {
addMessage('log', responseObject.logs);
}
if (responseObject.hasOwnProperty('success')) {
addMessage('success', responseObject.success);
}
if (responseObject.hasOwnProperty('form')) {
addForm(responseObject.form);
}
if (responseObject.hasOwnProperty('progress')) {
setProgress(responseObject.progress);
}
if (responseObject.hasOwnProperty('download')) {
addDownloadBox(responseObject.download);
}
if (responseObject.hasOwnProperty('file_status')) {
addUpdateFileStatus(responseObject.file_status);
}
if (responseObject.hasOwnProperty('nav')) {
updateNavbarStatus(responseObject.nav);
}
if (responseObject.hasOwnProperty('cookies')) {
setCookies(responseObject.cookies);
}
if (responseObject.hasOwnProperty('refresh')) {
refreshRequested = true;
}
if (responseObject.hasOwnProperty('redirect')) {
redirect(responseObject.redirect.url, responseObject.redirect.use_ajax);
}
if (responseObject.hasOwnProperty('over')) {
if (responseObject.over) {
transmissionOver = true;
}
}
}
/**
* Processes status data
*
* @param status
*/
function processTimeoutResponse(status) {
if (statusCount === 12) { // 1 minute hard cap
status = 'fail';
}
if (status === 'continue') {
refreshRequested = false;
doRefresh();
} else if (status === 'running') {
statusCount++;
$('#loading_indicator').css('display', 'block');
setTimeout(queryInstallerStatus, 5000);
} else {
$('#loading_indicator').css('display', 'none');
addMessage('error',
[{
title: installLang.title,
description: installLang.msg
}]
);
}
}
/**
* Queries the installer's status
*/
function queryInstallerStatus() {
var url = $(location).attr('pathname');
var lookUp = 'install/app.php';
var position = url.indexOf(lookUp);
if (position === -1) {
lookUp = 'install';
position = url.indexOf(lookUp);
if (position === -1) {
return false;
}
}
url = url.substring(0, position) + lookUp + '/installer/status';
$.getJSON(url, function(data) {
processTimeoutResponse(data.status);
});
}
/**
* Process updates in streamed response
*
* @param xhReq XHR object
*/
function pollContent(xhReq) {
var messages = xhReq.responseText;
var msgSeparator = '}\n\n';
var unprocessed, messageEndIndex, endOfMessageIndex, message;
do {
unprocessed = messages.substring(nextReadPosition);
messageEndIndex = unprocessed.indexOf(msgSeparator);
if (messageEndIndex !== -1) {
endOfMessageIndex = messageEndIndex + msgSeparator.length;
message = unprocessed.substring(0, endOfMessageIndex);
parseMessage($.trim(message));
nextReadPosition += endOfMessageIndex;
}
} while (messageEndIndex !== -1);
if (xhReq.readyState === 4) {
$('#loading_indicator').css('display', 'none');
resetPolling();
var timeoutDetected = !transmissionOver;
if (refreshRequested) {
refreshRequested = false;
doRefresh();
}
if (timeoutDetected) {
statusCount = 0;
queryInstallerStatus();
}
}
}
/**
* Animates the progress bar
*
* @param $progressText
* @param $progressFiller
* @param $progressFillerText
* @param progressLimit
*/
function incrementFiller($progressText, $progressFiller, $progressFillerText, progressLimit) {
if (currentProgress >= progressLimit || currentProgress >= 100) {
clearInterval(progressTimer);
return;
}
var $progressBar = $('#progress-bar');
currentProgress++;
$progressFillerText.css('width', $progressBar.width());
$progressFillerText.text(currentProgress + '%');
$progressText.text(currentProgress + '%');
$progressFiller.css('width', currentProgress + '%');
}
/**
* Wrapper function for progress bar rendering and animating
*
* @param progressLimit
*/
function incrementProgressBar(progressLimit) {
var $progressFiller = $('#progress-bar-filler');
var $progressFillerText = $('#progress-bar-filler-text');
var $progressText = $('#progress-bar-text');
var progressStart = $progressFiller.width() / $progressFiller.offsetParent().width() * 100;
currentProgress = Math.floor(progressStart);
clearInterval(progressTimer);
progressTimer = setInterval(function() {
incrementFiller($progressText, $progressFiller, $progressFillerText, progressLimit);
}, 10);
}
/**
* Resets the polling timer
*/
function resetPolling() {
clearInterval(pollTimer);
nextReadPosition = 0;
}
/**
* Sets up timer for processing the streamed HTTP response
*
* @param xhReq
*/
function startPolling(xhReq) {
resetPolling();
transmissionOver = false;
pollTimer = setInterval(function () {
pollContent(xhReq);
}, 250);
}
/**
* Refresh page
*/
function doRefresh() {
resetPolling();
var xhReq = createXhrObject();
xhReq.open('GET', $(location).attr('pathname'), true);
xhReq.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
xhReq.send();
startPolling(xhReq);
}
/**
* Renders the AJAX UI layout
*/
function setupAjaxLayout() {
progressBarTriggered = false;
// Clear content
$contentWrapper.html('');
var $header = $('');
$header.attr('id', 'header-container');
$contentWrapper.append($header);
var $description = $('');
$description.attr('id', 'description-container');
$contentWrapper.append($description);
var $errorContainer = $('');
$errorContainer.attr('id', 'error-container');
$contentWrapper.append($errorContainer);
var $warningContainer = $('');
$warningContainer.attr('id', 'warning-container');
$contentWrapper.append($warningContainer);
var $progressContainer = $('');
$progressContainer.attr('id', 'progress-bar-container');
$contentWrapper.append($progressContainer);
var $logContainer = $('');
$logContainer.attr('id', 'log-container');
$contentWrapper.append($logContainer);
var $installerContentWrapper = $('');
$installerContentWrapper.attr('id', 'content-container');
$contentWrapper.append($installerContentWrapper);
var $installerDownloadWrapper = $('');
$installerDownloadWrapper.attr('id', 'download-wrapper');
$installerContentWrapper.append($installerDownloadWrapper);
var $updaterFileStatusWrapper = $('');
$updaterFileStatusWrapper.attr('id', 'file-status-wrapper');
$installerContentWrapper.append($updaterFileStatusWrapper);
var $formWrapper = $('');
$formWrapper.attr('id', 'form-wrapper');
$installerContentWrapper.append($formWrapper);
var $spinner = $('');
$spinner.attr('id', 'loading_indicator');
$spinner.html(' ');
$contentWrapper.append($spinner);
}
// Submits a form
function submitForm($form, $submitBtn) {
$form.css('display', 'none');
var xhReq = createXhrObject();
xhReq.open('POST', $form.attr('action'), true);
xhReq.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
xhReq.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
xhReq.send(getFormFields($form, $submitBtn));
// Disable language selector
$('#language_selector :input, label').css('display', 'none');
// Clear content
setupAjaxLayout();
$('#loading_indicator').css('display', 'block');
startPolling(xhReq);
}
/**
* Add submit button to the POST information
*
* @param $form
* @param $submitBtn
*
* @returns {*}
*/
function getFormFields($form, $submitBtn) {
var formData = $form.serialize();
formData += ((formData.length) ? '&' : '') + encodeURIComponent($submitBtn.attr('name')) + '=';
formData += encodeURIComponent($submitBtn.attr('value'));
return formData;
}
/**
* Intercept form submit events and determine the submit button used
*
* @param $form
*/
function interceptFormSubmit($form) {
if (!$form.length) {
return;
}
$form.find(':submit').bind('click', function (event) {
event.preventDefault();
submitForm($form, $(this));
});
}
})(jQuery); // Avoid conflicts with other libraries
javascript/jquery-3.4.1.min.js 0000755 0000000 0000000 00000254123 14516425663 013240 0 ustar /*! jQuery v3.4.1 | (c) JS Foundation and other contributors | jquery.org/license */
!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],E=C.document,r=Object.getPrototypeOf,s=t.slice,g=t.concat,u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType},x=function(e){return null!=e&&e===e.window},c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.4.1",k=function(e,t){return new k.fn.init(e,t)},p=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g;function d(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp($),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+$),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\([\\da-f]{1,6}"+M+"?|("+M+")|.)","ig"),ne=function(e,t,n){var r="0x"+t-65536;return r!=r||n?t:r<0?String.fromCharCode(r+65536):String.fromCharCode(r>>10|55296,1023&r|56320)},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(m.childNodes),m.childNodes),t[m.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&((e?e.ownerDocument||e:m)!==C&&T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!A[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&U.test(t)){(s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=k),o=(l=h(t)).length;while(o--)l[o]="#"+s+" "+xe(l[o]);c=l.join(","),f=ee.test(t)&&ye(e.parentNode)||e}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){A(t,!0)}finally{s===k&&e.removeAttribute("id")}}}return g(t.replace(B,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[k]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e.namespaceURI,n=(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:m;return r!==C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),m!==C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=k,!C.getElementsByName||!C.getElementsByName(k).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+k+"-]").length||v.push("~="),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+k+"+*").length||v.push(".#.+[+~]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",$)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},D=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)===(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e===C||e.ownerDocument===m&&y(m,e)?-1:t===C||t.ownerDocument===m&&y(m,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e===C?-1:t===C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]===m?-1:s[r]===m?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if((e.ownerDocument||e)!==C&&T(e),d.matchesSelector&&E&&!A[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){A(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=p[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&p(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?k.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?k.grep(e,function(e){return e===n!==r}):"string"!=typeof n?k.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(k.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||q,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:L.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof k?t[0]:t,k.merge(this,k.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),D.test(r[1])&&k.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(k):k.makeArray(e,this)}).prototype=k.fn,q=k(E);var H=/^(?:parents|prev(?:Until|All))/,O={children:!0,contents:!0,next:!0,prev:!0};function P(e,t){while((e=e[t])&&1!==e.nodeType);return e}k.fn.extend({has:function(e){var t=k(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i,ge={option:[1,""],thead:[1,"