/* 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 = $('
', { '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 = $('
' + res.S_HIDDEN_FIELDS + '
').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 === 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 += ''; 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 += ''; } if (dir === 'v') { html += ''; } } if (dir === 'h') { html += ''; } } 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'); }); }; 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 var banner = document.createElement("div"); banner.className = "archivebanner"; banner.innerHTML = "This is an archived, static version of the forums. No data can be changed"; document.body.insertBefore(banner, document.body.childNodes[0]);