From 1c0f1ed3da6b68886c4078bc83ed98d20f6a7359 Mon Sep 17 00:00:00 2001 From: Matt Friedman Date: Sat, 31 May 2025 10:08:17 -0700 Subject: [PATCH 1/8] [ticket/17517] Eslint all phpbb JS files PHPBB-17517 --- package.json | 10 - phpBB/adm/style/admin.js | 149 +- phpBB/adm/style/ajax.js | 729 ++-- phpBB/adm/style/permissions.js | 136 +- phpBB/adm/style/tooltip.js | 380 ++- phpBB/assets/javascript/core.js | 3349 ++++++++++--------- phpBB/assets/javascript/editor.js | 156 +- phpBB/assets/javascript/installer.js | 193 +- phpBB/assets/javascript/plupload.js | 1289 +++---- phpBB/styles/prosilver/template/ajax.js | 752 +++-- phpBB/styles/prosilver/template/forum_fn.js | 280 +- 11 files changed, 3736 insertions(+), 3687 deletions(-) diff --git a/package.json b/package.json index 4543bc9797..ab6ef87a0c 100644 --- a/package.json +++ b/package.json @@ -9,20 +9,10 @@ "eslintConfig": { "extends": "xo", "ignorePatterns": [ - "phpBB/adm/style/admin.js", - "phpBB/adm/style/ajax.js", - "phpBB/adm/style/permissions.js", - "phpBB/adm/style/tooltip.js", - "phpBB/assets/javascript/core.js", "phpBB/assets/javascript/cropper.js", - "phpBB/assets/javascript/editor.js", "phpBB/assets/javascript/hermite.js", - "phpBB/assets/javascript/installer.js", "phpBB/assets/javascript/jquery-cropper.js", - "phpBB/assets/javascript/plupload.js", "phpBB/ext/**/*.js", - "phpBB/styles/prosilver/template/ajax.js", - "phpBB/styles/prosilver/template/forum_fn.js", "phpBB/**/*.min.js", "phpBB/vendor/**/*.js", "phpBB/vendor-ext/**/*.js", diff --git a/phpBB/adm/style/admin.js b/phpBB/adm/style/admin.js index ff89458ca8..0beaafd2e0 100644 --- a/phpBB/adm/style/admin.js +++ b/phpBB/adm/style/admin.js @@ -7,19 +7,16 @@ /** * Parse document block */ -function parse_document(container) -{ - var test = document.createElement('div'), - oldBrowser = (typeof test.style.borderRadius == 'undefined'); - +function parseDocument(container) { + const test = document.createElement('div'); test.remove(); /** * Navigation */ container.find('#menu').each(function() { - var menu = $(this), - blocks = menu.children('.menu-block'); + const menu = $(this); + const blocks = menu.children('.menu-block'); if (!blocks.length) { return; @@ -27,10 +24,11 @@ function parse_document(container) // Set onclick event blocks.children('a.header').click(function() { - var parent = $(this).parent(); + const parent = $(this).parent(); if (!parent.hasClass('active')) { parent.siblings().removeClass('active'); } + parent.toggleClass('active'); }); @@ -47,23 +45,22 @@ function parse_document(container) * Responsive tables */ container.find('table').not('.not-responsive').each(function() { - var $this = $(this), - th = $this.find('thead > tr > th'), - columns = th.length, - headers = [], - totalHeaders = 0, - i, headersLength; + const $this = $(this); + const th = $this.find('thead > tr > th'); + const headers = []; + let totalHeaders = 0; + let i; // Find columns $this.find('colgroup:first').children().each(function(i) { - var column = $(this); + const column = $(this); $this.find('td:nth-child(' + (i + 1) + ')').addClass(column.prop('className')); }); // Styles table if ($this.hasClass('styles')) { $this.find('td:first-child[style]').each(function() { - var style = $(this).attr('style'); + const style = $(this).attr('style'); if (style.length) { $(this).parent('tr').attr('style', style.toLowerCase().replace('padding', 'margin')).addClass('responsive-style-row'); } @@ -71,21 +68,24 @@ function parse_document(container) } // Find each header - if (!$this.data('no-responsive-header')) - { + if (!$this.data('no-responsive-header')) { th.each(function(column) { - var cell = $(this), - colspan = parseInt(cell.attr('colspan')), - dfn = cell.attr('data-dfn'), - text = dfn ? dfn : $.trim(cell.text()); + const cell = $(this); + let colspan = parseInt(cell.attr('colspan'), 10); + const dfn = cell.attr('data-dfn'); + let text = dfn ? dfn : $.trim(cell.text()); + + if (text === ' ') { + text = ''; + } - if (text == ' ') text = ''; colspan = isNaN(colspan) || colspan < 1 ? 1 : colspan; - for (i=0; i $this.addClass('responsive'); @@ -104,19 +104,19 @@ function parse_document(container) } $this.find('tbody > tr').each(function() { - var row = $(this), - cells = row.children('td'), - column = 0; + const row = $(this); + const cells = row.children('td'); + let column = 0; - if (cells.length == 1) { + if (cells.length === 1) { row.addClass('big-column'); return; } cells.each(function() { - var cell = $(this), - colspan = parseInt(cell.attr('colspan')), - text = $.trim(cell.text()); + const cell = $(this); + let colspan = parseInt(cell.attr('colspan'), 10); + const text = $.trim(cell.text()); if (headersLength <= column) { return; @@ -124,10 +124,9 @@ function parse_document(container) if ((text.length && text !== '-') || cell.children().length) { if (headers[column].length) { - cell.prepend($("").css('display', 'none').text(headers[column])); + cell.prepend($('').css('display', 'none').text(headers[column])); } - } - else { + } else { cell.addClass('empty'); } @@ -144,9 +143,8 @@ function parse_document(container) * Hide empty responsive tables */ container.find('table.responsive > tbody').each(function() { - var items = $(this).children('tr'); - if (!items.length) - { + const items = $(this).children('tr'); + if (!items.length) { $(this).parent('table:first').addClass('responsive-hide'); } }); @@ -155,8 +153,8 @@ function parse_document(container) * Fieldsets with empty */ container.find('fieldset dt > span:last-child').each(function() { - var $this = $(this); - if ($this.html() == ' ') { + const $this = $(this); + if ($this.html() === ' ') { $this.addClass('responsive-hide'); } }); @@ -166,7 +164,7 @@ function parse_document(container) */ container.find('#sitename_short').each(function() { const $this = this; - const maxLength = $this.maxLength; + const { maxLength } = $this; $this.maxLength = maxLength * 2; $this.addEventListener('input', () => { const inputChars = Array.from($this.value); @@ -180,25 +178,25 @@ function parse_document(container) * Responsive tabs */ container.find('#tabs').not('[data-skip-responsive]').each(function() { - var $this = $(this), - $body = $('body'), - ul = $this.children(), - tabs = ul.children().not('[data-skip-responsive]'), - links = tabs.children('a'), - item = ul.append('').find('li.responsive-tab'), - menu = item.find('.dropdown-contents'), - maxHeight = 0, - lastWidth = false, - responsive = false; + const $this = $(this); + const $body = $('body'); + const ul = $this.children(); + const tabs = ul.children().not('[data-skip-responsive]'); + const links = tabs.children('a'); + const item = ul.append('').find('li.responsive-tab'); + const menu = item.find('.dropdown-contents'); + let maxHeight = 0; + let lastWidth = false; + let responsive = false; links.each(function() { - var link = $(this); + const link = $(this); maxHeight = Math.max(maxHeight, Math.max(link.outerHeight(true), link.parent().outerHeight(true))); - }) + }); function check() { - var width = $body.width(), - height = $this.height(); + const width = $body.width(); + let height = $this.height(); if (!arguments.length && (!responsive || width <= lastWidth) && height <= maxHeight) { return; @@ -214,6 +212,7 @@ function parse_document(container) if (item.hasClass('dropdown-visible')) { phpbb.toggleDropdown.call(item.find('a.responsive-tab-link').get(0)); } + return; } @@ -221,23 +220,29 @@ function parse_document(container) item.show(); menu.html(''); - var availableTabs = tabs.filter(':not(.activetab, .responsive-tab)'), - total = availableTabs.length, - i, tab; + const availableTabs = tabs.filter(':not(.activetab, .responsive-tab)'); + const total = availableTabs.length; + let i; + let tab; - for (i = total - 1; i >= 0; i --) { + for (i = total - 1; i >= 0; i--) { tab = availableTabs.eq(i); menu.prepend(tab.clone(true).removeClass('tab')); tab.hide(); if ($this.height() <= maxHeight) { - menu.find('a').click(function() { check(true); }); + menu.find('a').click(() => { + check(true); + }); return; } } - menu.find('a').click(function() { check(true); }); + + menu.find('a').click(() => { + check(true); + }); } - phpbb.registerDropdown(item.find('a.responsive-tab-link'), item.find('.dropdown'), {visibleClass: 'activetab', verticalDirection: 'down'}); + phpbb.registerDropdown(item.find('a.responsive-tab-link'), item.find('.dropdown'), { visibleClass: 'activetab', verticalDirection: 'down' }); check(true); $(window).resize(check); @@ -248,7 +253,7 @@ function parse_document(container) * Run onload functions */ (function($) { - $(document).ready(function() { + $(document).ready(() => { // Swap .nojs and .hasjs $('body.nojs').toggleClass('nojs hasjs'); @@ -257,13 +262,13 @@ function parse_document(container) $('#' + this.getAttribute('data-focus')).focus(); }); - parse_document($('body')); + parseDocument($('body')); $('#questionnaire-form').css('display', 'none'); - var $triggerConfiglist = $('#trigger-configlist'); + const $triggerConfiglist = $('#trigger-configlist'); - $triggerConfiglist.on('click', function () { - var $configlist = $('#configlist'); + $triggerConfiglist.on('click', function() { + const $configlist = $('#configlist'); $configlist.closest('.send-stats-data-row').toggleClass('send-stats-data-hidden'); $configlist.closest('.send-stats-row').find('.send-stats-data-row:first-child').toggleClass('send-stats-data-only-row'); $(this).find('i').toggleClass('fa-angle-down fa-angle-up'); @@ -272,8 +277,8 @@ function parse_document(container) $('#configlist').closest('.send-stats-data-row').addClass('send-stats-data-hidden'); // Do not underline actions icons on hover (could not be done via CSS) - $('.actions a:has(i.acp-icon)').mouseover(function () { - $(this).css("text-decoration", "none"); + $('.actions a:has(i.acp-icon)').mouseover(function() { + $(this).css('text-decoration', 'none'); }); // Live update BBCode font icon preview @@ -296,11 +301,11 @@ function parse_document(container) const pageIconFont = document.getElementById('bbcode_font_icon'); if (pageIconFont) { - pageIconFont.addEventListener('keyup', function () { + pageIconFont.addEventListener('keyup', function() { updateIconClass(this.nextElementSibling, this.value); }); - pageIconFont.addEventListener('blur', function () { + pageIconFont.addEventListener('blur', function() { updateIconClass(this.nextElementSibling, this.value); }); } diff --git a/phpBB/adm/style/ajax.js b/phpBB/adm/style/ajax.js index 2c364bcd86..1f0a2fa623 100644 --- a/phpBB/adm/style/ajax.js +++ b/phpBB/adm/style/ajax.js @@ -1,424 +1,421 @@ /* global phpbb, statsData */ -(function($) { // Avoid conflicts with other libraries +(function($) { // Avoid conflicts with other libraries + 'use strict'; -'use strict'; + phpbb.prepareSendStats = function() { + const $form = $('#acp_help_phpbb'); + const $dark = $('#darkenwrapper'); + let $loadingIndicator; + $form.on('submit', function(event) { + const $this = $(this); + const currentTime = Math.floor(new Date().getTime() / 1000); + const statsTime = parseInt($this.find('input[name=help_send_statistics_time]').val(), 10); -phpbb.prepareSendStats = function () { - var $form = $('#acp_help_phpbb'); - var $dark = $('#darkenwrapper'); - var $loadingIndicator; + event.preventDefault(); + $this.unbind('submit'); - $form.on('submit', function (event) { - var $this = $(this), - currentTime = Math.floor(new Date().getTime() / 1000), - statsTime = parseInt($this.find('input[name=help_send_statistics_time]').val(), 10); - - event.preventDefault(); - $this.unbind('submit'); - - // Skip ajax request if form is submitted too early or send stats - // checkbox is not checked - if (!$this.find('input[name=help_send_statistics]').is(':checked') || - statsTime > currentTime) { - $form.find('input[type=submit]').click(); - setTimeout(function () { + // Skip ajax request if form is submitted too early or send stats + // checkbox is not checked + if (!$this.find('input[name=help_send_statistics]').is(':checked') + || statsTime > currentTime) { $form.find('input[type=submit]').click(); - }, 300); + setTimeout(() => { + $form.find('input[type=submit]').click(); + }, 300); + 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(); + let errorText = ''; + + 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) { + phpbb.clearLoadingTimeout(); + + // If a confirmation is not required, display an alert and call the + // callbacks. + $dark.fadeOut(phpbb.alertTime); + + if ($loadingIndicator) { + $loadingIndicator.fadeOut(phpbb.alertTime); + } + + const $sendStatisticsSuccess = $('', { + type: 'hidden', + name: 'send_statistics_response', + value: JSON.stringify(res), + }); + $sendStatisticsSuccess.appendTo('p.submit-buttons'); + + // Finish actual form submission + $form.find('input[type=submit]').click(); + } + + $loadingIndicator = phpbb.loadingIndicator(); + + $.ajax({ + url: $this.attr('data-ajax-action').replace('&', '&'), + type: 'POST', + data: statsData, + success: returnHandler, + error: errorHandler, + cache: false, + }).always(() => { + if ($loadingIndicator && $loadingIndicator.is(':visible')) { + $loadingIndicator.fadeOut(phpbb.alertTime); + } + }); + }); + }; + + /** + * The following callbacks are for reording items. row_down + * is triggered when an item is moved down, and row_up is triggered when + * an item is moved up. It moves the row up or down, and deactivates / + * activates any up / down icons that require it (the ones at the top or bottom). + */ + phpbb.addAjaxCallback('row_down', function(res) { + if (typeof res.success === 'undefined' || !res.success) { 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 errorText = ''; + const $firstTr = $(this).parents('tr'); + const $secondTr = $firstTr.next(); - 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); + $firstTr.insertAfter($secondTr); + }); + + phpbb.addAjaxCallback('row_up', function(res) { + if (typeof res.success === 'undefined' || !res.success) { + return; } - /** - * 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) { - phpbb.clearLoadingTimeout(); + const $secondTr = $(this).parents('tr'); + const $firstTr = $secondTr.prev(); - // If a confirmation is not required, display an alert and call the - // callbacks. - $dark.fadeOut(phpbb.alertTime); + $secondTr.insertBefore($firstTr); + }); - if ($loadingIndicator) { - $loadingIndicator.fadeOut(phpbb.alertTime); - } + /** + * This callback replaces activate links with deactivate links and vice versa. + * It does this by replacing the text, and replacing all instances of "activate" + * in the href with "deactivate", and vice versa. + */ + phpbb.addAjaxCallback('activate_deactivate', function(res) { + const $this = $(this); + let newHref = $this.attr('href'); - var $sendStatisticsSuccess = $('', { - type: 'hidden', - name: 'send_statistics_response', - value: JSON.stringify(res) - }); - $sendStatisticsSuccess.appendTo('p.submit-buttons'); + $this.text(res.text); - // Finish actual form submission - $form.find('input[type=submit]').click(); + if (newHref.indexOf('deactivate') === -1) { + newHref = newHref.replace('activate', 'deactivate'); + } else { + newHref = newHref.replace('deactivate', 'activate'); } - $loadingIndicator = phpbb.loadingIndicator(); + $this.attr('href', newHref); + }); - $.ajax({ - url: $this.attr('data-ajax-action').replace('&', '&'), - type: 'POST', - data: statsData, - success: returnHandler, - error: errorHandler, - cache: false - }).always(function() { - if ($loadingIndicator && $loadingIndicator.is(':visible')) { - $loadingIndicator.fadeOut(phpbb.alertTime); + /** + * The removes the parent row of the link or form that triggered the callback, + * and is good for stuff like the removal of forums. + */ + phpbb.addAjaxCallback('row_delete', function(res) { + if (res.SUCCESS !== false) { + $(this).parents('tr').remove(); + } + }); + + /** + * This callback generates the VAPID keys for the web push notification service. + */ + phpbb.addAjaxCallback('generate_vapid_keys', () => { + /** + * Generate VAPID keypair with public and private key string + * + * @returns {Promise<{privateKey: string, publicKey: string}|null>} + */ + async function generateVAPIDKeys() { + try { + // Generate a new key pair using the Subtle Crypto API + const keyPair = await crypto.subtle.generateKey( + { + name: 'ECDH', + namedCurve: 'P-256', + }, + true, + [ 'deriveKey', 'deriveBits' ], + ); + + const privateKeyJwk = await crypto.subtle.exportKey('jwk', keyPair.privateKey); + const privateKeyString = privateKeyJwk.d; + + const publicKeyBuffer = await crypto.subtle.exportKey('raw', keyPair.publicKey); + const publicKeyString = phpbb.base64UrlEncode(phpbb.rawKeyToBase64(publicKeyBuffer)); + + return { + privateKey: privateKeyString, + publicKey: publicKeyString, + }; + } catch (error) { + console.error('Error generating keys with SubtleCrypto:', error); + return null; } + } + + generateVAPIDKeys().then(keyPair => { + if (!keyPair) { + return; + } + + const publicKeyInput = document.querySelector('#webpush_vapid_public'); + const privateKeyInput = document.querySelector('#webpush_vapid_private'); + publicKeyInput.value = keyPair.publicKey; + privateKeyInput.value = keyPair.privateKey; }); }); -}; - -/** - * The following callbacks are for reording items. row_down - * is triggered when an item is moved down, and row_up is triggered when - * an item is moved up. It moves the row up or down, and deactivates / - * activates any up / down icons that require it (the ones at the top or bottom). - */ -phpbb.addAjaxCallback('row_down', function(res) { - if (typeof res.success === 'undefined' || !res.success) { - return; - } - - var $firstTr = $(this).parents('tr'), - $secondTr = $firstTr.next(); - - $firstTr.insertAfter($secondTr); -}); - -phpbb.addAjaxCallback('row_up', function(res) { - if (typeof res.success === 'undefined' || !res.success) { - return; - } - - var $secondTr = $(this).parents('tr'), - $firstTr = $secondTr.prev(); - - $secondTr.insertBefore($firstTr); -}); - -/** - * This callback replaces activate links with deactivate links and vice versa. - * It does this by replacing the text, and replacing all instances of "activate" - * in the href with "deactivate", and vice versa. - */ -phpbb.addAjaxCallback('activate_deactivate', function(res) { - var $this = $(this), - newHref = $this.attr('href'); - - $this.text(res.text); - - if (newHref.indexOf('deactivate') !== -1) { - newHref = newHref.replace('deactivate', 'activate'); - } else { - newHref = newHref.replace('activate', 'deactivate'); - } - - $this.attr('href', newHref); -}); - -/** - * The removes the parent row of the link or form that triggered the callback, - * and is good for stuff like the removal of forums. - */ -phpbb.addAjaxCallback('row_delete', function(res) { - if (res.SUCCESS !== false) { - $(this).parents('tr').remove(); - } -}); - -/** - * This callback generates the VAPID keys for the web push notification service. - */ -phpbb.addAjaxCallback('generate_vapid_keys', () => { /** - * Generate VAPID keypair with public and private key string - * - * @returns {Promise<{privateKey: string, publicKey: string}|null>} - */ - async function generateVAPIDKeys() { - try { - // Generate a new key pair using the Subtle Crypto API - const keyPair = await crypto.subtle.generateKey( - { - name: 'ECDH', - namedCurve: 'P-256', - }, - true, - ['deriveKey', 'deriveBits'] - ); + * Handler for submitting permissions form in chunks + * This call will submit permissions forms in chunks of 5 fieldsets. + */ + function submitPermissions() { + const $form = $('form#set-permissions'); + let fieldsetList = $form.find('fieldset[id^=perm]'); + const formDataSets = []; + let dataSetIndex = 0; + const $submitAllButton = $form.find('input[type=submit][name^=action]')[0]; + const $submitButton = $form.find('input[type=submit][data-clicked=true]')[0]; - const privateKeyJwk = await crypto.subtle.exportKey('jwk', keyPair.privateKey); - const privateKeyString = privateKeyJwk.d; + // Set proper start values for handling refresh of page + let permissionSubmitSize = 0; + let permissionRequestCount = 0; + const forumIds = []; + let permissionSubmitFailed = false; + let clearIndicator = true; - const publicKeyBuffer = await crypto.subtle.exportKey('raw', keyPair.publicKey); - const publicKeyString = phpbb.base64UrlEncode(phpbb.rawKeyToBase64(publicKeyBuffer)); - - return { - privateKey: privateKeyString, - publicKey: publicKeyString - }; - } catch (error) { - console.error('Error generating keys with SubtleCrypto:', error); - return null; - } - } - - generateVAPIDKeys().then(keyPair => { - if (!keyPair) { - return; - } - const publicKeyInput = document.querySelector('#webpush_vapid_public'); - const privateKeyInput = document.querySelector('#webpush_vapid_private'); - publicKeyInput.value = keyPair.publicKey; - privateKeyInput.value = keyPair.privateKey; - }) -}) - -/** - * Handler for submitting permissions form in chunks - * This call will submit permissions forms in chunks of 5 fieldsets. - */ -function submitPermissions() { - var $form = $('form#set-permissions'), - fieldsetList = $form.find('fieldset[id^=perm]'), - formDataSets = [], - dataSetIndex = 0, - $submitAllButton = $form.find('input[type=submit][name^=action]')[0], - $submitButton = $form.find('input[type=submit][data-clicked=true]')[0]; - - // Set proper start values for handling refresh of page - var permissionSubmitSize = 0, - permissionRequestCount = 0, - forumIds = [], - permissionSubmitFailed = false, - clearIndicator = true, - $loadingIndicator; - - if ($submitAllButton !== $submitButton) { - fieldsetList = $form.find('fieldset#' + $submitButton.closest('fieldset.permissions').id); - } - - $.each(fieldsetList, function (key, value) { - dataSetIndex = Math.floor(key / 5); - var $fieldset = $('fieldset#' + value.id); - if (key % 5 === 0) { - formDataSets[dataSetIndex] = $fieldset.find('select:visible, input:not([data-name])').serialize(); - } else { - formDataSets[dataSetIndex] += '&' + $fieldset.find('select:visible, input:not([data-name])').serialize(); + if ($submitAllButton !== $submitButton) { + fieldsetList = $form.find('fieldset#' + $submitButton.closest('fieldset.permissions').id); } - // Find proper role value - var roleInput = $fieldset.find('input[name^=role][data-name]'); - if (roleInput.val()) { - formDataSets[dataSetIndex] += '&' + roleInput.attr('name') + '=' + roleInput.val(); - } else { - formDataSets[dataSetIndex] += '&' + roleInput.attr('name') + '=' + - $fieldset.find('select[name="' + roleInput.attr('name') + '"]').val(); - } - }); + $.each(fieldsetList, (key, value) => { + dataSetIndex = Math.floor(key / 5); + const $fieldset = $('fieldset#' + value.id); + if (key % 5 === 0) { + formDataSets[dataSetIndex] = $fieldset.find('select:visible, input:not([data-name])').serialize(); + } else { + formDataSets[dataSetIndex] += '&' + $fieldset.find('select:visible, input:not([data-name])').serialize(); + } - permissionSubmitSize = formDataSets.length; + // Find proper role value + const roleInput = $fieldset.find('input[name^=role][data-name]'); + if (roleInput.val()) { + formDataSets[dataSetIndex] += '&' + roleInput.attr('name') + '=' + roleInput.val(); + } else { + formDataSets[dataSetIndex] += '&' + roleInput.attr('name') + '=' + + $fieldset.find('select[name="' + roleInput.attr('name') + '"]').val(); + } + }); - // Add each forum ID to forum ID list to preserve selected forums - $.each($form.find('input[type=hidden][name^=forum_id]'), function (key, value) { - if (value.name.match(/^forum_id\[([0-9]+)\]$/)) { - forumIds.push(value.value); - } - }); + permissionSubmitSize = formDataSets.length; - $loadingIndicator = phpbb.loadingIndicator(); + // Add each forum ID to forum ID list to preserve selected forums + $.each($form.find('input[type=hidden][name^=forum_id]'), (key, value) => { + if (value.name.match(/^forum_id\[([0-9]+)\]$/)) { + forumIds.push(value.value); + } + }); - /** - * Handler for submitted permissions form chunk - * - * @param {object} res Object returned by AJAX call - */ - function handlePermissionReturn(res) { - permissionRequestCount++; - var $dark = $('#darkenwrapper'); + const $loadingIndicator = phpbb.loadingIndicator(); - if (res.S_USER_WARNING) { - phpbb.alert(res.MESSAGE_TITLE, res.MESSAGE_TEXT); - permissionSubmitFailed = true; - } else if (!permissionSubmitFailed && res.S_USER_NOTICE) { - // Display success message at the end of submitting the form - if (permissionRequestCount >= permissionSubmitSize) { - clearIndicator = true; + /** + * Handler for submitted permissions form chunk + * + * @param {object} res Object returned by AJAX call + */ + function handlePermissionReturn(res) { + permissionRequestCount++; + const $dark = $('#darkenwrapper'); - var $alert = phpbb.alert(res.MESSAGE_TITLE, res.MESSAGE_TEXT); - var $alertBoxLink = $alert.find('p.alert_text > a'); + if (res.S_USER_WARNING) { + phpbb.alert(res.MESSAGE_TITLE, res.MESSAGE_TEXT); + permissionSubmitFailed = true; + } else if (!permissionSubmitFailed && res.S_USER_NOTICE) { + // Display success message at the end of submitting the form + if (permissionRequestCount >= permissionSubmitSize) { + clearIndicator = true; - // Create form to submit instead of normal "Back to previous page" link - if ($alertBoxLink) { - // Remove forum_id[] from URL - $alertBoxLink.attr('href', $alertBoxLink.attr('href').replace(/(&forum_id\[\]=[0-9]+)/g, '')); - const $previousPageForm = $('
').attr({ - action: $alertBoxLink.attr('href'), - method: 'post' - }); + const $alert = phpbb.alert(res.MESSAGE_TITLE, res.MESSAGE_TEXT); + const $alertBoxLink = $alert.find('p.alert_text > a'); - $.each(forumIds, function (key, value) { - $previousPageForm.append($('').attr({ - type: 'text', - name: 'forum_id[]', - value: value - })); - }); - - $alertBoxLink.on('click', function (e) { - $('body').append($previousPageForm); - e.preventDefault(); - $previousPageForm.submit(); - }); - } - - // Do not allow closing alert - $dark.off('click'); - $alert.find('.alert_close').hide(); - - if (typeof res.REFRESH_DATA !== 'undefined') { - setTimeout(function () { - // Create forum to submit using POST. This will prevent - // exceeding the maximum length of URLs - const $form = $('').attr({ - action: res.REFRESH_DATA.url.replace(/(&forum_id\[\]=[0-9]+)/g, ''), - method: 'post' + // Create form to submit instead of normal "Back to previous page" link + if ($alertBoxLink) { + // Remove forum_id[] from URL + $alertBoxLink.attr('href', $alertBoxLink.attr('href').replace(/(&forum_id\[\]=[0-9]+)/g, '')); + const $previousPageForm = $('').attr({ + action: $alertBoxLink.attr('href'), + method: 'post', }); - $.each(forumIds, function (key, value) { - $form.append($('').attr({ + $.each(forumIds, (key, value) => { + $previousPageForm.append($('').attr({ type: 'text', name: 'forum_id[]', - value: value + value, })); }); - $('body').append($form); - - // 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(); - } + $alertBoxLink.on('click', e => { + $('body').append($previousPageForm); + e.preventDefault(); + $previousPageForm.submit(); }); + } - // Submit form - $form.submit(); - }, res.REFRESH_DATA.time * 1000); // Server specifies time in seconds + // Do not allow closing alert + $dark.off('click'); + $alert.find('.alert_close').hide(); + + if (typeof res.REFRESH_DATA !== 'undefined') { + setTimeout(() => { + // Create forum to submit using POST. This will prevent + // exceeding the maximum length of URLs + const $form = $('').attr({ + action: res.REFRESH_DATA.url.replace(/(&forum_id\[\]=[0-9]+)/g, ''), + method: 'post', + }); + + $.each(forumIds, (key, value) => { + $form.append($('').attr({ + type: 'text', + name: 'forum_id[]', + value, + })); + }); + + $('body').append($form); + + // Hide the alert even if we refresh the page, in case the user + // presses the back button. + $dark.fadeOut(phpbb.alertTime, () => { + if (typeof $alert !== 'undefined') { + $alert.hide(); + } + }); + + // Submit form + $form.submit(); + }, res.REFRESH_DATA.time * 1000); // Server specifies time in seconds + } + } else { + // Still more forms to submit, so do not clear indicator + clearIndicator = false; + } + } + + if (clearIndicator) { + phpbb.clearLoadingTimeout(); + + if ($loadingIndicator) { + $loadingIndicator.fadeOut(phpbb.alertTime); } - } else { - // Still more forms to submit, so do not clear indicator - clearIndicator = false; } } - if (clearIndicator) { - phpbb.clearLoadingTimeout(); - - if ($loadingIndicator) { - $loadingIndicator.fadeOut(phpbb.alertTime); - } - } + // Create AJAX request for each form data set + $.each(formDataSets, (key, formData) => { + $.ajax({ + url: $form.action, + type: 'POST', + data: formData + '&' + $submitButton.name + '=' + encodeURIComponent($submitButton.value) + + '&creation_time=' + $form.find('input[type=hidden][name=creation_time]')[0].value + + '&form_token=' + $form.find('input[type=hidden][name=form_token]')[0].value + + '&' + $form.children('input[type=hidden]').serialize() + + '&' + $form.find('input[type=checkbox][name^=inherit]').serialize(), + success: handlePermissionReturn, + error: handlePermissionReturn, + }); + }); } - // Create AJAX request for each form data set - $.each(formDataSets, function (key, formData) { - $.ajax({ - url: $form.action, - type: 'POST', - data: formData + '&' + $submitButton.name + '=' + encodeURIComponent($submitButton.value) + - '&creation_time=' + $form.find('input[type=hidden][name=creation_time]')[0].value + - '&form_token=' + $form.find('input[type=hidden][name=form_token]')[0].value + - '&' + $form.children('input[type=hidden]').serialize() + - '&' + $form.find('input[type=checkbox][name^=inherit]').serialize(), - success: handlePermissionReturn, - error: handlePermissionReturn - }); + $('[data-ajax]').each(function() { + const $this = $(this); + const ajax = $this.attr('data-ajax'); + + if (ajax !== 'false') { + const fn = (ajax === 'true') ? null : ajax; + phpbb.ajaxify({ + selector: this, + refresh: $this.attr('data-refresh') !== undefined, + callback: fn, + }); + } }); -} -$('[data-ajax]').each(function() { - var $this = $(this), - ajax = $this.attr('data-ajax'); + /** + * Automatically resize textarea + */ + $(() => { + phpbb.resizeTextArea($('textarea:not(.no-auto-resize)'), { minHeight: 75 }); - if (ajax !== 'false') { - var fn = (ajax !== 'true') ? ajax : null; - phpbb.ajaxify({ - selector: this, - refresh: $this.attr('data-refresh') !== undefined, - callback: fn - }); - } -}); - -/** -* Automatically resize textarea -*/ -$(function() { - phpbb.resizeTextArea($('textarea:not(.no-auto-resize)'), {minHeight: 75}); - - var $setPermissionsForm = $('form#set-permissions'); - if ($setPermissionsForm.length) { - $setPermissionsForm.on('submit', function (e) { - submitPermissions(); - e.preventDefault(); - }); - $setPermissionsForm.find('input[type=submit]').click(function() { - $('input[type=submit]', $(this).parents($('form#set-permissions'))).removeAttr('data-clicked'); - $(this).attr('data-clicked', true); - }); - } - - // Handle date option changes - const dateoptionSelect = document.getElementById('dateoptions'); - if (dateoptionSelect) { - dateoptionSelect.addEventListener('change', function() { - const dateoptionInput = document.getElementById(this.getAttribute('data-dateoption')); - if (this.value === 'custom') { - dateoptionInput.value = this.getAttribute('data-dateoption-default'); - } else { - dateoptionInput.value = this.value; - } - }) - } - - if ($('#acp_help_phpbb')) { - phpbb.prepareSendStats(); - } -}); + const $setPermissionsForm = $('form#set-permissions'); + if ($setPermissionsForm.length) { + $setPermissionsForm.on('submit', e => { + submitPermissions(); + e.preventDefault(); + }); + $setPermissionsForm.find('input[type=submit]').click(function() { + $('input[type=submit]', $(this).parents($('form#set-permissions'))).removeAttr('data-clicked'); + $(this).attr('data-clicked', true); + }); + } + // Handle date option changes + const dateoptionSelect = document.getElementById('dateoptions'); + if (dateoptionSelect) { + dateoptionSelect.addEventListener('change', function() { + const dateoptionInput = document.getElementById(this.getAttribute('data-dateoption')); + if (this.value === 'custom') { + dateoptionInput.value = this.getAttribute('data-dateoption-default'); + } else { + dateoptionInput.value = this.value; + } + }); + } + if ($('#acp_help_phpbb')) { + phpbb.prepareSendStats(); + } + }); })(jQuery); // Avoid conflicts with other libraries diff --git a/phpBB/adm/style/permissions.js b/phpBB/adm/style/permissions.js index 04b3fc5c5f..a1371ff7d2 100644 --- a/phpBB/adm/style/permissions.js +++ b/phpBB/adm/style/permissions.js @@ -1,24 +1,26 @@ /* global phpbb */ +/* eslint camelcase: 0 */ +/* eslint no-undef: 0 */ +/* eslint no-unused-vars: 0 */ /** * Hide and show all checkboxes * status = true (show boxes), false (hide boxes) */ function display_checkboxes(status) { - var form = document.getElementById('set-permissions'); - var cb = document.getElementsByTagName('input'); - var display; + const form = document.getElementById('set-permissions'); + const cb = document.getElementsByTagName('input'); + let display; - //show if (status) { + // show display = 'inline'; - } - //hide - else { + } else { + // hide display = 'none'; } - for (var i = 0; i < cb.length; i++ ) { + for (let i = 0; i < cb.length; i++ ) { if (cb[i].className === 'permissions-checkbox') { cb[i].style.display = display; } @@ -31,10 +33,10 @@ function display_checkboxes(status) { * value = 0 (hidden) till 10 (fully visible) */ function set_opacity(e, value) { - e.style.opacity = value/10; + e.style.opacity = value / 10; - //IE opacity currently turned off, because of its astronomical stupidity - //e.style.filter = 'alpha(opacity=' + value*10 + ')'; + // IE opacity currently turned off, because of its astronomical stupidity + // e.style.filter = 'alpha(opacity=' + value*10 + ')'; } /** @@ -42,8 +44,8 @@ function set_opacity(e, value) { * block_id = id of the element that needs to be toggled */ function toggle_opacity(block_id) { - var cb = document.getElementById('checkbox' + block_id); - var fs = document.getElementById('perm' + block_id); + const cb = document.getElementById('checkbox' + block_id); + const fs = document.getElementById('perm' + block_id); if (cb.checked) { set_opacity(fs, 5); @@ -58,25 +60,25 @@ function toggle_opacity(block_id) { * except_id = id of the element not to hide */ function reset_opacity(status, except_id) { - var perm = document.getElementById('set-permissions'); - var fs = perm.getElementsByTagName('fieldset'); - var opacity = 5; + const perm = document.getElementById('set-permissions'); + const fs = perm.getElementsByTagName('fieldset'); + let opacity = 5; if (status) { opacity = 10; } - for (var i = 0; i < fs.length; i++ ) { + for (let i = 0; i < fs.length; i++ ) { if (fs[i].className !== 'quick') { set_opacity(fs[i], opacity); } } - if (typeof(except_id) !== 'undefined') { + if (typeof (except_id) !== 'undefined') { set_opacity(document.getElementById('perm' + except_id), 10); } - //reset checkboxes too + // reset checkboxes too marklist('set-permissions', 'inherit', !status); } @@ -86,13 +88,14 @@ function reset_opacity(status, except_id) { * rb = array of radiobuttons */ function get_radio_status(index, rb) { - for (var i = index; i < rb.length; i = i + 3 ) { + for (let i = index; i < rb.length; i += 3 ) { if (rb[i].checked !== true) { if (i > index) { - //at least one is true, but not all (custom) + // at least one is true, but not all (custom) return 2; } - //first one is not true + + // first one is not true return 0; } } @@ -108,18 +111,18 @@ function get_radio_status(index, rb) { * quick = If no calculation needed, this contains the colour */ function set_colours(id, init, quick) { - var table = document.getElementById('table' + id); - var tab = document.getElementById('tab' + id); + const table = document.getElementById('table' + id); + const tab = document.getElementById('tab' + id); - if (typeof(quick) !== 'undefined') { + if (typeof (quick) !== 'undefined') { tab.className = 'permissions-preset-' + quick + ' activetab'; return; } - var rb = table.getElementsByTagName('input'); - var colour = 'custom'; + const rb = table.getElementsByTagName('input'); + let colour = 'custom'; - var status = get_radio_status(0, rb); + let status = get_radio_status(0, rb); if (status === 1) { colour = 'yes'; @@ -151,17 +154,17 @@ function set_colours(id, init, quick) { * block_id = block that is opened */ function init_colours(block_id) { - var block = document.getElementById('advanced' + block_id); - var panels = block.getElementsByTagName('div'); - var tab = document.getElementById('tab' + id); + const block = document.getElementById('advanced' + block_id); + const panels = block.getElementsByTagName('div'); + const tab = document.getElementById('tab' + id); - for (var i = 0; i < panels.length; i++) { + for (let i = 0; i < panels.length; i++) { if (panels[i].className === 'permissions-panel') { set_colours(panels[i].id.replace(/options/, ''), true); } } - tab.className = tab.className + ' activetab'; + tab.className += ' activetab'; } /** @@ -170,13 +173,14 @@ function init_colours(block_id) { * adv = we are opening advanced permissions * view = called from view permissions */ +// eslint-disable-next-line max-params function swap_options(pmask, fmask, cat, adv, view) { id = pmask + fmask + cat; active_option = active_pmask + active_fmask + active_cat; - var old_tab = document.getElementById('tab' + active_option); - var new_tab = document.getElementById('tab' + id); - var adv_block = document.getElementById('advanced' + pmask + fmask); + const old_tab = document.getElementById('tab' + active_option); + const new_tab = document.getElementById('tab' + id); + const adv_block = document.getElementById('advanced' + pmask + fmask); if (adv_block.style.display === 'block' && adv === true) { phpbb.toggleDisplay('advanced' + pmask + fmask, -1); @@ -196,14 +200,14 @@ function swap_options(pmask, fmask, cat, adv, view) { display_checkboxes(true); reset_opacity(1); } else if (adv) { - //Checkbox might have been clicked, but we need full visibility + // Checkbox might have been clicked, but we need full visibility display_checkboxes(true); reset_opacity(1); } // set active tab - old_tab.className = old_tab.className.replace(/\ activetab/g, ''); - new_tab.className = new_tab.className + ' activetab'; + old_tab.className = old_tab.className.replace(/ activetab/g, ''); + new_tab.className += ' activetab'; if (id === active_option && adv !== true) { return; @@ -211,7 +215,7 @@ function swap_options(pmask, fmask, cat, adv, view) { phpbb.toggleDisplay('options' + active_option, -1); - //hiding and showing the checkbox + // hiding and showing the checkbox if (document.getElementById('checkbox' + active_pmask + active_fmask)) { phpbb.toggleDisplay('checkbox' + pmask + fmask, -1); @@ -227,6 +231,7 @@ function swap_options(pmask, fmask, cat, adv, view) { if (!view) { phpbb.toggleDisplay('advanced' + pmask + fmask, 1); } + phpbb.toggleDisplay('options' + id, 1); active_pmask = pmask; @@ -239,32 +244,32 @@ function swap_options(pmask, fmask, cat, adv, view) { * id = table ID container, s = status ['y'/'u'/'n'] */ function mark_options(id, s) { - var t = document.getElementById(id); + const t = document.getElementById(id); if (!t) { return; } - var rb = t.getElementsByTagName('input'); + const rb = t.getElementsByTagName('input'); - for (var r = 0; r < rb.length; r++) { - if (rb[r].id.substr(rb[r].id.length-1) === s) { + for (let r = 0; r < rb.length; r++) { + if (rb[r].id.substr(rb[r].id.length - 1) === s) { rb[r].checked = true; } } } function mark_one_option(id, field_name, s) { - var t = document.getElementById(id); + const t = document.getElementById(id); if (!t) { return; } - var rb = t.getElementsByTagName('input'); + const rb = t.getElementsByTagName('input'); - for (var r = 0; r < rb.length; r++) { - if (rb[r].id.substr(rb[r].id.length-field_name.length-3, field_name.length) === field_name && rb[r].id.substr(rb[r].id.length-1) === s) { + for (let r = 0; r < rb.length; r++) { + if (rb[r].id.substr(rb[r].id.length - field_name.length - 3, field_name.length) === field_name && rb[r].id.substr(rb[r].id.length - 1) === s) { rb[r].checked = true; } } @@ -280,21 +285,21 @@ function mark_one_option(id, field_name, s) { * @returns {void} */ function reset_role(id) { - var t = document.getElementById(id); + const t = document.getElementById(id); if (!t) { return; } // Before resetting the role dropdown, try and match any permission role - var parent = t.parentNode, - roleId = match_role_settings(id.replace('role', 'perm')), - text = no_role_assigned, - index = 0; + const parent = t.parentNode; + const roleId = match_role_settings(id.replace('role', 'perm')); + let text = no_role_assigned; + let index = 0; // If a role permissions was matched, grab that option's value and index if (roleId) { - for (var i = 0; i < t.options.length; i++) { + for (let i = 0; i < t.options.length; i++) { if (parseInt(t.options[i].value, 10) === roleId) { text = t.options[i].text; index = i; @@ -316,7 +321,7 @@ function reset_role(id) { * Load role and set options accordingly */ function set_role_settings(role_id, target_id) { - var settings = role_options[role_id]; + const settings = role_options[role_id]; if (!settings) { return; @@ -325,8 +330,10 @@ function set_role_settings(role_id, target_id) { // Mark all options to no (unset) first... mark_options(target_id, 'u'); - for (var r in settings) { - mark_one_option(target_id, r, (settings[r] === 1) ? 'y' : 'n'); + for (const r in settings) { + if (Object.prototype.hasOwnProperty.call(settings, r)) { + mark_one_option(target_id, r, (settings[r] === 1) ? 'y' : 'n'); + } } } @@ -337,13 +344,13 @@ function set_role_settings(role_id, target_id) { * @return {number} The permission role identifier */ function match_role_settings(id) { - var fieldset = document.getElementById(id), - radios = fieldset.getElementsByTagName('input'), - set = {}; + const fieldset = document.getElementById(id); + const radios = fieldset.getElementsByTagName('input'); + let set = {}; // Iterate over all the radio buttons - for (var i = 0; i < radios.length; i++) { - var matches = radios[i].id.match(/setting\[\d+]\[\d+]\[([a-z_]+)]/); + for (let i = 0; i < radios.length; i++) { + const matches = radios[i].id.match(/setting\[\d+]\[\d+]\[([a-z_]+)]/); // Make sure the name attribute matches, the radio is checked and it is not the "No" (-1) value. if (matches !== null && radios[i].checked && radios[i].value !== '-1') { @@ -355,8 +362,7 @@ function match_role_settings(id) { set = sort_and_stringify(set); // Iterate over the available role options and return the first match - for (var r in role_options) - { + for (const r in role_options) { if (sort_and_stringify(role_options[r]) === set) { return parseInt(r, 10); } @@ -372,7 +378,7 @@ function match_role_settings(id) { * @return {string} The sorted object as a string */ function sort_and_stringify(obj) { - return JSON.stringify(Object.keys(obj).sort().reduce(function (result, key) { + return JSON.stringify(Object.keys(obj).sort().reduce((result, key) => { result[key] = obj[key]; return result; }, {})); diff --git a/phpBB/adm/style/tooltip.js b/phpBB/adm/style/tooltip.js index 562bc99dc3..51aaaafd8c 100644 --- a/phpBB/adm/style/tooltip.js +++ b/phpBB/adm/style/tooltip.js @@ -13,213 +13,209 @@ phpBB Development Team: */ (function($) { // Avoid conflicts with other libraries + 'use strict'; -'use strict'; + const tooltips = []; -var tooltips = []; + /** + * Enable tooltip replacements for selects + * @param {string} id ID tag of select + * @param {string} headline Text that should appear on top of tooltip + * @param {string} [subId] Sub ID that should only be using tooltips (optional) + */ + phpbb.enableTooltipsSelect = function(id, headline, subId) { + let $links; -/** - * Enable tooltip replacements for selects - * @param {string} id ID tag of select - * @param {string} headline Text that should appear on top of tooltip - * @param {string} [subId] Sub ID that should only be using tooltips (optional) -*/ -phpbb.enableTooltipsSelect = function (id, headline, subId) { - var $links, hold; + const hold = $('', { + id: '_tooltip_container', + css: { + position: 'absolute', + }, + }); - hold = $('', { - id: '_tooltip_container', - css: { - position: 'absolute' + $('body').append(hold); + + if (id) { + $links = $('.roles-options li', '#' + id); + } else { + $links = $('.roles-options li'); } - }); - $('body').append(hold); + $links.each(function() { + const $this = $(this); - if (!id) { - $links = $('.roles-options li'); - } else { - $links = $('.roles-options li', '#' + id); - } - - $links.each(function () { - var $this = $(this); - - if (subId) { - if ($this.parent().attr('id').substr(0, subId.length) === subId) { + if (subId) { + if ($this.parent().attr('id').substr(0, subId.length) === subId) { + phpbb.prepareTooltips($this, headline); + } + } else { phpbb.prepareTooltips($this, headline); } + }); + }; + + /** + * Prepare elements to replace + * + * @param {jQuery} $element Element to prepare for tooltips + * @param {string} headText Text heading to display + */ + phpbb.prepareTooltips = function($element, headText) { + const text = $element.attr('data-title'); + + if (text === null || text.length === 0) { + return; + } + + const $title = $('', { + class: 'top', + css: { + display: 'block', + }, + }) + .append(document.createTextNode(headText)); + + const $desc = $('', { + class: 'bottom', + html: text, + css: { + display: 'block', + }, + }); + + const $tooltip = $('', { + class: 'tooltip', + css: { + display: 'block', + }, + }) + .append($title) + .append($desc); + + tooltips[$element.attr('data-id')] = $tooltip; + $element.on('mouseover', phpbb.showTooltip); + $element.on('mouseout', phpbb.hideTooltip); + }; + + /** + * Show tooltip + * + * @param {object} $element Element passed by .on() + */ + phpbb.showTooltip = function($element) { + const $this = $($element.target); + $('#_tooltip_container').append(tooltips[$this.attr('data-id')]); + phpbb.positionTooltip($this); + }; + + /** + * Hide tooltip + */ + phpbb.hideTooltip = function() { + const d = document.getElementById('_tooltip_container'); + if (d.childNodes.length > 0) { + d.removeChild(d.firstChild); + } + }; + + /** + * Correct positioning of tooltip container + * + * @param {jQuery} $element Tooltip element that should be positioned + */ + phpbb.positionTooltip = function($element) { + $element = $element.parent(); + const offset = $element.offset(); + + if ($('body').hasClass('rtl')) { + $('#_tooltip_container').css({ + top: offset.top + 30, + left: offset.left + 255, + }); } else { - phpbb.prepareTooltips($this, headline); - } - }); -}; - -/** - * Prepare elements to replace - * - * @param {jQuery} $element Element to prepare for tooltips - * @param {string} headText Text heading to display -*/ -phpbb.prepareTooltips = function ($element, headText) { - var $tooltip, text, $desc, $title; - - text = $element.attr('data-title'); - - if (text === null || text.length === 0) { - return; - } - - $title = $('', { - class: 'top', - css: { - display: 'block' - } - }) - .append(document.createTextNode(headText)); - - $desc = $('', { - class: 'bottom', - html: text, - css: { - display: 'block' - } - }); - - $tooltip = $('', { - class: 'tooltip', - css: { - display: 'block' - } - }) - .append($title) - .append($desc); - - tooltips[$element.attr('data-id')] = $tooltip; - $element.on('mouseover', phpbb.showTooltip); - $element.on('mouseout', phpbb.hideTooltip); -}; - -/** - * Show tooltip - * - * @param {object} $element Element passed by .on() -*/ -phpbb.showTooltip = function ($element) { - var $this = $($element.target); - $('#_tooltip_container').append(tooltips[$this.attr('data-id')]); - phpbb.positionTooltip($this); -}; - -/** - * Hide tooltip -*/ -phpbb.hideTooltip = function () { - var d = document.getElementById('_tooltip_container'); - if (d.childNodes.length > 0) { - d.removeChild(d.firstChild); - } -}; - -/** - * Correct positioning of tooltip container - * - * @param {jQuery} $element Tooltip element that should be positioned -*/ -phpbb.positionTooltip = function ($element) { - var offset; - - $element = $element.parent(); - offset = $element.offset(); - - if ($('body').hasClass('rtl')) { - $('#_tooltip_container').css({ - top: offset.top + 30, - left: offset.left + 255 - }); - } else { - $('#_tooltip_container').css({ - top: offset.top + 30, - left: offset.left - 205 - }); - } -}; - -/** - * Prepare roles drop down select - */ -phpbb.prepareRolesDropdown = function () { - var $options = $('.roles-options li'); - - // Display span and hide select - $('.roles-options > span').css('display', 'block'); - $('.roles-options > select').hide(); - $('.roles-options > input[type=hidden]').each(function () { - var $this = $(this); - - if ($this.attr('data-name') && !$this.attr('name')) { - $this.attr('name', $this.attr('data-name')); - } - }); - - // Prepare highlighting of select options and settings update - $options.each(function () { - var $this = $(this); - var $rolesOptions = $this.closest('.roles-options'); - var $span = $rolesOptions.children('span'); - - // Correctly show selected option - if (typeof $this.attr('data-selected') !== 'undefined') { - $rolesOptions - .children('span') - .text($this.text()) - .attr('data-default', $this.text()) - .attr('data-default-val', $this.attr('data-id')); - - // Save default text of drop down if there is no default set yet - if (typeof $span.attr('data-default') === 'undefined') { - $span.attr('data-default', $span.text()); - } - - // Prepare resetting drop down on form reset - $this.closest('form').on('reset', function () { - $span.text($span.attr('data-default')); - $rolesOptions.children('input[type=hidden]') - .val($span.attr('data-default-val')); + $('#_tooltip_container').css({ + top: offset.top + 30, + left: offset.left - 205, }); } + }; - $this.on('mouseover', function () { - var $this = $(this); - $options.removeClass('roles-highlight'); - $this.addClass('roles-highlight'); - }).on('click', function () { - var $this = $(this); - var $rolesOptions = $this.closest('.roles-options'); + /** + * Prepare roles drop down select + */ + phpbb.prepareRolesDropdown = function() { + const $options = $('.roles-options li'); - // Update settings - set_role_settings($this.attr('data-id'), $this.attr('data-target-id')); - init_colours($this.attr('data-target-id').replace('advanced', '')); + // Display span and hide select + $('.roles-options > span').css('display', 'block'); + $('.roles-options > select').hide(); + $('.roles-options > input[type=hidden]').each(function() { + const $this = $(this); - // Set selected setting - $rolesOptions.children('span') - .text($this.text()); - $rolesOptions.children('input[type=hidden]') - .val($this.attr('data-id')); - - // Trigger hiding of selection options - $('body').trigger('click'); + if ($this.attr('data-name') && !$this.attr('name')) { + $this.attr('name', $this.attr('data-name')); + } }); + + // Prepare highlighting of select options and settings update + $options.each(function() { + const $this = $(this); + const $rolesOptions = $this.closest('.roles-options'); + const $span = $rolesOptions.children('span'); + + // Correctly show selected option + if (typeof $this.attr('data-selected') !== 'undefined') { + $rolesOptions + .children('span') + .text($this.text()) + .attr('data-default', $this.text()) + .attr('data-default-val', $this.attr('data-id')); + + // Save default text of drop down if there is no default set yet + if (typeof $span.attr('data-default') === 'undefined') { + $span.attr('data-default', $span.text()); + } + + // Prepare resetting drop down on form reset + $this.closest('form').on('reset', () => { + $span.text($span.attr('data-default')); + $rolesOptions.children('input[type=hidden]') + .val($span.attr('data-default-val')); + }); + } + + $this.on('mouseover', function() { + const $this = $(this); + $options.removeClass('roles-highlight'); + $this.addClass('roles-highlight'); + }).on('click', function() { + const $this = $(this); + const $rolesOptions = $this.closest('.roles-options'); + + // Update settings + // eslint-disable-next-line no-undef + set_role_settings($this.attr('data-id'), $this.attr('data-target-id')); + // eslint-disable-next-line no-undef + init_colours($this.attr('data-target-id').replace('advanced', '')); + + // Set selected setting + $rolesOptions.children('span') + .text($this.text()); + $rolesOptions.children('input[type=hidden]') + .val($this.attr('data-id')); + + // Trigger hiding of selection options + $('body').trigger('click'); + }); + }); + }; + + // Run onload functions for RolesDropdown and tooltips + $(() => { + // Enable tooltips + phpbb.enableTooltipsSelect('set-permissions', $('#set-permissions').attr('data-role-description'), 'role'); + + // Prepare dropdown + phpbb.prepareRolesDropdown(); }); -}; - -// Run onload functions for RolesDropdown and tooltips -$(function() { - // Enable tooltips - phpbb.enableTooltipsSelect('set-permissions', $('#set-permissions').attr('data-role-description'), 'role'); - - // Prepare dropdown - phpbb.prepareRolesDropdown(); -}); - })(jQuery); // Avoid conflicts with other libraries diff --git a/phpBB/assets/javascript/core.js b/phpBB/assets/javascript/core.js index c0b225214d..95e66254c8 100644 --- a/phpBB/assets/javascript/core.js +++ b/phpBB/assets/javascript/core.js @@ -1,757 +1,767 @@ /* global bbfontstyle */ -var phpbb = {}; +const phpbb = {}; phpbb.alertTime = 100; -(function($) { // Avoid conflicts with other libraries +(function($) { // Avoid conflicts with other libraries + 'use strict'; -'use strict'; + // define a couple constants for keydown functions. + const keymap = { + TAB: 9, + ENTER: 13, + ESC: 27, + ARROW_UP: 38, + ARROW_DOWN: 40, + }; -// define a couple constants for keydown functions. -var keymap = { - TAB: 9, - ENTER: 13, - ESC: 27, - ARROW_UP: 38, - ARROW_DOWN: 40 -}; + const $dark = $('#darkenwrapper'); + let $loadingIndicator; + let phpbbAlertTimer = null; -var $dark = $('#darkenwrapper'); -var $loadingIndicator; -var phpbbAlertTimer = null; + phpbb.isTouch = (window && typeof window.ontouchstart !== 'undefined'); -phpbb.isTouch = (window && typeof window.ontouchstart !== 'undefined'); + // Add ajax pre-filter to prevent cross-domain script execution + $.ajaxPrefilter(s => { + if (s.crossDomain) { + s.contents.script = false; + } + }); -// 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'); + } -/** - * 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(() => { + phpbb.showTimeoutMessage(); + }, 60000); + } - 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; + }; - return $loadingIndicator; -}; + /** + * Show timeout message + */ + phpbb.showTimeoutMessage = function() { + const $alert = $('#phpbb_alert'); -/** - * 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')); + } + }; - 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; + } + }; -/** - * 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(() => { + $('#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) { + const $alert = $('#phpbb_alert'); + $alert.find('.alert_title').html(title); + $alert.find('.alert_text').html(msg); -/** -* 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); -}; + $(document).on('keydown.phpbb.alert', e => { + if (e.keyCode === keymap.ENTER || e.keyCode === keymap.ESC) { + phpbb.alert.close($alert, true); + e.preventDefault(); + e.stopPropagation(); + } + }); + phpbb.alert.open($alert); -/** - * 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); + return $alert; + }; - $(document).on('keydown.phpbb.alert', function(e) { - if (e.keyCode === keymap.ENTER || e.keyCode === keymap.ESC) { + /** + * 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, () => { + $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', e => { + e.stopPropagation(); + }); + + $dark.one('click', e => { 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(); - }); + $alert.find('.alert_close').one('click', e => { + phpbb.alert.close($alert, true); + e.preventDefault(); + }); + }; - $dark.one('click', function(e) { - phpbb.alert.close($alert, true); - e.preventDefault(); - e.stopPropagation(); - }); + /** + * 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) { + const $fade = (fadedark) ? $dark : $alert; - $alert.find('.alert_close').one('click', function(e) { - phpbb.alert.close($alert, true); - e.preventDefault(); - }); -}; + $fade.fadeOut(phpbb.alertTime, () => { + $alert.hide(); + }); -/** -* 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; + $alert.find('.alert_close').off('click'); + $(document).off('keydown.phpbb.alert'); + }; - $fade.fadeOut(phpbb.alertTime, function() { - $alert.hide(); - }); + /** + * 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) { + const $confirmDiv = $('#phpbb_confirm'); + $confirmDiv.find('.alert_text').html(msg); + fadedark = typeof fadedark === 'undefined' ? true : fadedark; - $alert.find('.alert_close').off('click'); - $(document).off('keydown.phpbb.alert'); -}; + $(document).on('keydown.phpbb.alert', e => { + if (e.keyCode === keymap.ENTER || e.keyCode === keymap.ESC) { + const name = (e.keyCode === keymap.ENTER) ? 'confirm' : 'cancel'; -/** - * 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; + $('input[name="' + name + '"]').trigger('click'); + e.preventDefault(); + e.stopPropagation(); + } + }); - $(document).on('keydown.phpbb.alert', function(e) { - if (e.keyCode === keymap.ENTER || e.keyCode === keymap.ESC) { - var name = (e.keyCode === keymap.ENTER) ? 'confirm' : 'cancel'; + $confirmDiv.find('input[type="button"]').one('click.phpbb.confirmbox', function(e) { + const confirmed = this.name === 'confirm'; + + callback(confirmed); + $confirmDiv.find('input[type="button"]').off('click.phpbb.confirmbox'); + phpbb.alert.close($confirmDiv, fadedark || !confirmed); - $('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'; + phpbb.alert.open($confirmDiv); - callback(confirmed); - $confirmDiv.find('input[type="button"]').off('click.phpbb.confirmbox'); - phpbb.alert.close($confirmDiv, fadedark || !confirmed); + return $confirmDiv; + }; - e.preventDefault(); - e.stopPropagation(); - }); + /** + * Turn a querystring into an array. + * + * @argument {string} string The querystring to parse. + * @returns {object} The object created. + */ + phpbb.parseQuerystring = function(string) { + const params = {}; + let i; + let split; - 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; + string = string.split('&'); + for (i = 0; i < string.length; i++) { + split = string[i].split('='); + params[split[0]] = decodeURIComponent(split[1]); } - /** - * 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); - } + return params; + }; - /** - * 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 = {}; + /** + * 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) { + const $elements = $(options.selector); + let { refresh } = options; + const { callback } = options; + const overlay = typeof options.overlay === 'undefined' ? true : options.overlay; + const isForm = $elements.is('form'); + const isText = $elements.is('input[type="text"], textarea'); + let eventName; 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() - }); - } + eventName = 'submit'; } else if (isText) { - var name = $this.attr('data-name') || this.name; - action = $this.attr('data-url').replace('&', '&'); - data[name] = this.value; - method = 'POST'; + eventName = 'keyup'; } else { - action = this.href; - data = null; - method = 'GET'; + eventName = 'click'; } - var sendRequest = function() { - var dataOverlay = $this.attr('data-overlay'); - if (overlay && (typeof dataOverlay === 'undefined' || dataOverlay === 'true')) { - phpbb.loadingIndicator(); + $elements.on(eventName, function(event) { + let action; + let method; + let data; + let submit; + const that = this; + const $this = $(this); + + if ($this.find('input[type="submit"][data-clicked]').attr('data-ajax') === 'false') { + return; } - 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); + /** + * Handler for AJAX errors + */ + function errorHandler(jqXHR, textStatus, errorThrown) { + if (typeof console !== 'undefined' && console.log) { + console.log('AJAX error. status: ' + textStatus + ', message: ' + errorThrown); } - }); - }; - // 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)) { + phpbb.clearLoadingTimeout(); + let responseText; + let errorText = false; + try { + responseText = JSON.parse(jqXHR.responseText); + responseText = responseText.message; + } catch {} + + 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) { + let 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') { + $dark.fadeOut(phpbb.alertTime); + + if ($loadingIndicator) { + $loadingIndicator.fadeOut(phpbb.alertTime); + } + } else { + alert = phpbb.alert(res.MESSAGE_TITLE, res.MESSAGE_TEXT); + } + + 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(() => { + 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, () => { + 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, 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. + const 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) { + const 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'; + } + + const sendRequest = function() { + const dataOverlay = $this.attr('data-overlay'); + if (overlay && (typeof dataOverlay === 'undefined' || dataOverlay === 'true')) { + phpbb.loadingIndicator(); + } + + const request = $.ajax({ + url: action, + type: method, + data, + success: returnHandler, + error: errorHandler, + cache: false, + }); + + request.always(() => { + 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() { + const $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) { + const 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) { + const { selectionStart } = $textarea.get(0); + 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) { + const line = phpbb.search.getKeywordLine($input); + const 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(() => { + 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) { + const $this = $(this); + const dataName = $this.attr('data-name') === undefined ? $this.attr('name') : $this.attr('data-name'); + const minLength = parseInt($this.attr('data-min-length'), 10); + const searchID = $this.attr('data-results'); + const keyword = phpbb.search.getKeyword($this, data[dataName], $this.attr('data-multiline')); + const cache = phpbb.search.cache.get(searchID); + const key = event.keyCode || event.which; + let 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); + } + + const 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]) { + const response = { + 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; } - sendRequest(); - event.preventDefault(); - }); + const searchID = $input.attr('data-results'); + const $container = $(searchID); - if (isForm) { - $elements.find('input:submit').click(function () { - var $this = $(this); + if (this.cache.get(searchID).callback) { + callback = this.cache.get(searchID).callback; + } else if (typeof callback === 'function') { + this.cache.set(searchID, 'callback', callback); + } - // Remove data-clicked attribute from any submit button of form - $this.parents('form:first').find('input:submit[data-clicked]').removeAttr('data-clicked'); + if (!fromCache) { + this.cache.setResults(searchID, res.keyword, res.results); + } - $this.attr('data-clicked', 'true'); - }); - } + this.cache.set(searchID, 'lastSearch', res.keyword); + this.showResults(res.results, $input, $container, callback); + }; - return this; -}; + /** + * 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) { + const $resultContainer = $('.search-results', $container); + this.clearResults($resultContainer); -phpbb.search = { - cache: { - data: [] - }, - tpl: [], - container: [] -}; + if (!results.length) { + $container.hide(); + return; + } -/** - * 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; -}; + const searchID = $container.attr('id'); + let tpl; + let row; -/** - * 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; -}; + if (!this.tpl[searchID]) { + tpl = $('.search-result-tpl', $container); + this.tpl[searchID] = tpl.clone().removeClass('search-result-tpl'); + tpl.remove(); + } -/** - * 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; -}; + tpl = this.tpl[searchID]; -/** - * 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(); -}; + $.each(results, function(i, item) { + row = tpl.clone(); + row.find('.search-result').html(item.display); -/** - * 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 (typeof callback === 'function') { + callback.call(this, $input, item, row, $container); } - } - if (proceed) { - sendRequest.call(this); - } - }, 350); - phpbb.search.cache.set(searchID, 'timeout', timeout); + row.appendTo($resultContainer).show(); + }); + $container.show(); - return false; -}; + phpbb.search.navigateResults($input, $container, $resultContainer); + }; -/** - * 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; - } + /** + * Clear search results. + * + * @param {jQuery} $container Search results container. + */ + phpbb.search.clearResults = function($container) { + $container.children(':not(.search-result-tpl)').remove(); + }; - 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) { + /** + * 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(); - 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) { + /** + * 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'); + $input.off('.phpbb.search'); + $input.on('keydown.phpbb.search', event => { + const key = event.keyCode || event.which; + const $active = $resultContainer.children('.active'); - switch (key) { - // Close the results - case keymap.ESC: + if (key === keymap.ESC) { phpbb.search.closeResults($input, $container); - break; - - // Set the value for the selected result - case keymap.ENTER: + } else if (key === keymap.ENTER) { if ($active.length) { - var value = $active.find('.search-result > span').text(); + const value = $active.find('.search-result > span').text(); phpbb.search.setValue($input, value, $input.attr('data-multiline')); } @@ -760,13 +770,9 @@ phpbb.search.navigateResults = function($input, $container, $resultContainer) { // 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(); + } else if (key === keymap.ARROW_DOWN || key === keymap.ARROW_UP) { + const up = key === keymap.ARROW_UP; + const $children = $resultContainer.children(); if (!$active.length) { if (up) { @@ -794,472 +800,473 @@ phpbb.search.navigateResults = function($input, $container, $resultContainer) { // Do not change cursor position in the input element event.preventDefault(); - break; + } + }); + }; + + $('#phpbb').click(function() { + const $this = $(this); + + if (!$this.is('.live-search') && !$this.parents().is('.live-search')) { + phpbb.search.closeResults($('input, textarea'), $('.live-search')); } }); -}; -$('#phpbb').click(function() { - var $this = $(this); + phpbb.history = {}; - if (!$this.is('.live-search') && !$this.parents().is('.live-search')) { - phpbb.search.closeResults($('input, textarea'), $('.live-search')); - } -}); + /** + * 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'); + }; -phpbb.history = {}; + /** + * 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) { + const fn = mode + 'State'; -/** -* 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'); -}; + if (!url || !phpbb.history.isSupported(fn)) { + return; + } -/** -* 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 (!title) { + title = document.title; + } - if (!url || !phpbb.history.isSupported(fn)) { - return; - } - if (!title) { - title = document.title; - } - if (!obj) { - obj = null; - } + if (!obj) { + obj = null; + } - history[fn](obj, title, url); -}; + 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.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); -}; + /** + * 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'); + /** + * 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) { + const $timezoneCopy = $('#timezone_copy'); + const $timezone = $('#timezone'); + const $tzDate = $('#tz_date'); + const $tzSelectDateSuggest = $('#tz_select_date_suggest'); - if ($timezoneCopy.length === 0) { + 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 { + $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()); - } + $timezone.html($timezoneCopy.html()); + } - if ($tzDate.val() !== '') { - $timezone.children('optgroup').remove(':not([data-tz-value="' + $tzDate.val() + '"])'); - } + 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'); - } + 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'); + const $tzOptions = $timezone.children('optgroup[data-tz-value="' + $tzDate.val() + '"]').children('option'); - if ($tzOptions.length === 1) { + 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); + $tzOptions.prop('selected', true); + keepSelection = 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'); + if (typeof keepSelection !== 'undefined' && !keepSelection) { + const $timezoneOptions = $timezone.find('optgroup option'); + if ($timezoneOptions.filter(':selected').length <= 0) { + $timezoneOptions.filter(':first').prop('selected', true); } - - 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; - var updateAll = $(this).data('update-all'); - var toggleText; - var toggleUrl; - var toggleIcon; - - if (updateAll !== undefined && updateAll.length) { - $anchor = $(updateAll); - } else { - $anchor = $(this); - } - - $anchor.each(function() { - var $this = $(this); - - // Toggle link text - toggleText = $.trim($this.attr('data-toggle-text')); - $this.attr('data-toggle-text', $.trim($this.children('span').text())); - $this.attr('title', toggleText); - $this.children('span').last().text(toggleText); - - // Toggle link url - toggleUrl = $this.attr('data-toggle-url'); - $this.attr('data-toggle-url', $this.attr('href')); - $this.attr('href', toggleUrl); - - // Toggle Icon - $this.children().first().toggleClass('is-active').next().toggleClass('is-active') - }); -}); - -/** -* 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); - } + /** + * 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. + let offset = (new Date()).getTimezoneOffset(); + let sign = '-'; + + if (offset < 0) { + sign = '+'; + offset = -offset; + } + + let minutes = offset % 60; + let 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(); + } + + const prefix = 'UTC' + sign + hours + ':' + minutes; + const prefixLength = prefix.length; + const selectorOptions = $('option', '#tz_date'); + let i; + + const $tzSelectDateSuggest = $('#tz_select_date_suggest'); + + for (i = 0; i < selectorOptions.length; ++i) { + const 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'); + } + + const 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() { + let $anchor; + const updateAll = $(this).data('update-all'); + let altText; + + if (updateAll !== undefined && updateAll.length) { + $anchor = $(updateAll); + } else { + $anchor = $(this); + } + + $anchor.each(function() { + const $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() { + let $anchor; + const updateAll = $(this).data('update-all'); + let toggleText; + let toggleUrl; + + if (updateAll !== undefined && updateAll.length) { + $anchor = $(updateAll); + } else { + $anchor = $(this); + } + + $anchor.each(function() { + const $this = $(this); + + // Toggle link text + toggleText = $.trim($this.attr('data-toggle-text')); + $this.attr('data-toggle-text', $.trim($this.children('span').text())); + $this.attr('title', toggleText); + $this.children('span').last().text(toggleText); + + // Toggle link url + toggleUrl = $this.attr('data-toggle-url'); + $this.attr('data-toggle-url', $this.attr('href')); + $this.attr('href', toggleUrl); + + // Toggle Icon + $this.children().first().toggleClass('is-active').next().toggleClass('is-active'); + }); + }); + + /** + * 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 + let configuration = { + minWindowHeight: 500, + minHeight: 200, + maxHeight: 500, + heightDiff: 200, + resizeCallback() {}, + resetCallback() {}, + }; + + if (phpbb.isTouch) { + return; + } + + if (arguments.length > 1) { + configuration = $.extend(configuration, options); + } + + function resetAutoResize(item) { + const $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); + } + + const windowHeight = $(window).height(); + + if (windowHeight < configuration.minWindowHeight) { + resetAutoResize(item); + return; + } + + const maxHeight = Math.min( + Math.max(windowHeight - configuration.heightDiff, configuration.minHeight), + configuration.maxHeight, + ); + const $item = $(item); + const height = parseInt($item.innerHeight(), 10); + const 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(() => { + $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) { + const start = textarea.selectionStart; + let lastEnd = -1; + let lastStart = -1; + let i; + let index; + + if (typeof start !== 'number') { + return false; + } + + const value = textarea.value.toLowerCase(); + + for (i = 0; i < startTags.length; i++) { + const 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 + const startTags = [ '[code]', '[code=' ]; + const startTagsEnd = ']'; + const 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 @@ -1267,244 +1274,249 @@ phpbb.applyCodeEditor = function(textarea) { * * @returns {string} Line of text */ - function getLastLine(stripCodeStart) { - var start = textarea.selectionStart, - value = textarea.value, - index = value.lastIndexOf('\n', start - 1); + function getLastLine(stripCodeStart) { + const start = textarea.selectionStart; + let { value } = textarea; + let index = value.lastIndexOf('\n', start - 1); - value = value.substring(index + 1, start); + 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; + if (stripCodeStart) { + for (let i = 0; i < startTags.length; i++) { + index = value.lastIndexOf(startTags[i]); + if (index >= 0) { + const tagLength = startTags[i].length; - value = value.substring(index + tagLength); - if (startTags[i].lastIndexOf(startTagsEnd) !== tagLength) { - index = value.indexOf(startTagsEnd); + value = value.substring(index + tagLength); + if (startTags[i].lastIndexOf(startTagsEnd) !== tagLength) { + index = value.indexOf(startTagsEnd); - if (index >= 0) { - value = value.substr(index + 1); + // eslint-disable-next-line max-depth + if (index >= 0) { + value = value.substr(index + 1); + } } } } } + + return value; } - 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; + function appendText(text) { + const start = textarea.selectionStart; + const end = textarea.selectionEnd; + const { value } = textarea; - 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; - } + textarea.value = value.substr(0, start) + text + value.substr(end); + textarea.selectionStart = start + text.length; + textarea.selectionEnd = textarea.selectionStart; } - // intercept new line characters - if (key === keymap.ENTER) { - if (inTag()) { - var lastLine = getLastLine(true), - code = '' + /^\s*/g.exec(lastLine); + $(textarea).data('code-editor', true).on('keydown', event => { + const key = event.keyCode || event.which; - if (code.length > 0) { - appendText('\n' + code); + // intercept tabs + if (key === keymap.TAB + && !event.ctrlKey + && !event.shiftKey + && !event.altKey + && !event.metaKey) { + if (inTag()) { + appendText('\t'); event.preventDefault(); + return; } } - } - }); -}; -/** - * 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; - } + // intercept new line characters + if (key === keymap.ENTER) { + if (inTag()) { + const lastLine = getLastLine(true); + const code = String(/^\s*/g.exec(lastLine)); - $('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(event_) { - var $this = $(this), - options = $this.data('dropdown-options'), - parent = options.parent, - visible = parent.hasClass('dropdown-visible'), - direction; - - if (!visible) { - // Prevent link default action - event_.preventDefault(); - event_.stopPropagation(); - // 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'; + if (code.length > 0) { + appendText('\n' + code); + event.preventDefault(); + } + } } - } - 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'); + /** + * 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; } - } - // Prevent event propagation - if (arguments.length > 0) { - try { - var e = arguments[0]; - e.preventDefault(); - e.stopPropagation(); - } catch (error) { } - } - return false; -}; + $('body').on('dragenter dragover', () => { + $(textarea).addClass('drag-n-drop'); + }).on('dragleave dragout dragend drop', () => { + $(textarea).removeClass('drag-n-drop'); + }); + $(textarea).on('dragenter dragover', () => { + $(textarea).addClass('drag-n-drop-highlight'); + }).on('dragleave dragout dragend drop', () => { + $(textarea).removeClass('drag-n-drop-highlight'); + }); + }; -/** -* Toggle dropdown submenu -*/ -phpbb.toggleSubmenu = function(e) { - $(this).siblings('.dropdown-submenu').toggle(); - e.preventDefault(); -}; + /** + * 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'; -/** -* 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 = { + /** + * Dropdown toggle event handler + * This handler is used by phpBB.registerDropdown() and other functions + */ + phpbb.toggleDropdown = function(event_) { + const $this = $(this); + const options = $this.data('dropdown-options'); + const { parent } = options; + const visible = parent.hasClass('dropdown-visible'); + let direction; + + if (!visible) { + // Prevent link default action + event_.preventDefault(); + event_.stopPropagation(); + // Hide other dropdown menus + $(phpbb.dropdownHandles).each(phpbb.toggleDropdown); + + // Figure out direction of dropdown + direction = options.direction; + let { verticalDirection } = options; + const 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') { + const height = $(window).height(); + const 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) { + const windowWidth = $(window).width(); + + options.dropdown.find('.dropdown-contents').each(function() { + const $this = $(this); + + $this.css({ + marginLeft: 0, + left: 0, + marginRight: 0, + maxWidth: (windowWidth - 4) + 'px', + }); + + const offset = $this.offset().left; + const 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()); + }); + const 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')) { + let contentWidth; + const fullFreeSpace = freeSpace + parent.outerWidth(); + + options.dropdown.find('.dropdown-contents').each(function() { + contentWidth = parseInt($(this).outerWidth(), 10); + $(this).css({ marginLeft: 0, left: 0 }); + }); + + const 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 { + // eslint-disable-next-line prefer-rest-params + const e = arguments[0]; + e.preventDefault(); + e.stopPropagation(); + } catch { } + } + + 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) { + let 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 @@ -1512,401 +1524,410 @@ phpbb.registerDropdown = function(toggle, dropdown, options) { 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 + 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 += ''; + if (options) { + ops = $.extend(ops, options); } - for (g = 0; g < 5; g++) { - if (dir === 'v') { + 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) { + let r; + let g; + let b; + const numberList = new Array(6); + let color = ''; + let html = ''; + + numberList[0] = '00'; + numberList[1] = '40'; + numberList[2] = '80'; + numberList[3] = 'BF'; + numberList[4] = 'FF'; + + const tableClass = (dir === 'h') ? 'horizontal-palette' : 'vertical-palette'; + html += '
'; + + for (r = 0; r < 5; r++) { + if (dir === 'h') { html += ''; } - for (b = 0; b < 5; b++) { - color = '' + numberList[r] + numberList[g] + numberList[b]; - html += ''; + for (g = 0; g < 5; g++) { + if (dir === 'v') { + html += ''; + } + + for (b = 0; b < 5; b++) { + color = String(numberList[r]) + numberList[g] + numberList[b]; + html += ''; + } + + if (dir === 'v') { + html += ''; + } } - if (dir === 'v') { + if (dir === 'h') { html += ''; } } - if (dir === 'h') { - html += ''; + html += '
'; - html += '
'; + html += '
'; + return html; + }; + + /** + * Register a color palette. + * + * @param {jQuery} el jQuery object for the palette container. + */ + phpbb.registerPalette = function(el) { + const orientation = el.attr('data-color-palette') || el.attr('data-orientation'); // data-orientation kept for backwards compat. + const height = el.attr('data-height'); + const width = el.attr('data-width'); + const target = el.attr('data-target'); + const 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(e => { + el.toggle(); + e.preventDefault(); + }); + + // Attach event handler when a palette cell is clicked. + $(el).on('click', 'a', function(e) { + const 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'; } - } - 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'); + const $element = $('#' + id); - // 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); + const display = $element.css('display'); + if (!action) { + action = (display === '' || display === type) ? -1 : 1; } - 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'; - } + $element.css('display', ((action === 1) ? type : 'none')); + }; - var $element = $('#' + id); + /** + * 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() { + const $this = $(this); + const $setting = $($this.data('toggle-setting')); + $setting.toggle($this.is(':selected')); - var display = $element.css('display'); - if (!action) { - action = (display === '' || display === type) ? -1 : 1; - } - $element.css('display', ((action === 1) ? type : 'none')); -}; + // 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); + } + }); + }; -/** -* 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')); + /** + * Get function from name. + * Based on http://stackoverflow.com/a/359910 + * + * @param {string} functionName Function to get. + * @returns function + */ + phpbb.getFunctionByName = function(functionName) { + const namespaces = functionName.split('.'); + const func = namespaces.pop(); + let context = window; - // 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); + for (let i = 0; i < namespaces.length; i++) { + context = context[namespaces[i]]; } - }); -}; -/** -* 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; + return context[func]; + }; - for (var i = 0; i < namespaces.length; i++) { - context = context[namespaces[i]]; - } - return context[func]; -}; + /** + * Convert raw key ArrayBuffer to base64 string. + * + * @param {ArrayBuffer} rawKey Raw key array buffer as exported by SubtleCrypto exportKey() + * @returns {string} Base64 encoded raw key string + */ + phpbb.rawKeyToBase64 = rawKey => { + const keyBuffer = new Uint8Array(rawKey); + let keyText = ''; + const keyLength = keyBuffer.byteLength; + for (let i = 0; i < keyLength; i++) { + keyText += String.fromCharCode(keyBuffer[i]); + } -/** - * Convert raw key ArrayBuffer to base64 string. - * - * @param {ArrayBuffer} rawKey Raw key array buffer as exported by SubtleCrypto exportKey() - * @returns {string} Base64 encoded raw key string - */ -phpbb.rawKeyToBase64 = (rawKey) => { - const keyBuffer = new Uint8Array(rawKey); - let keyText = ''; - const keyLength = keyBuffer.byteLength; - for (let i = 0; i < keyLength; i++) { - keyText += String.fromCharCode(keyBuffer[i]); - } + return window.btoa(keyText); + }; - return window.btoa(keyText); -}; + /** + * Base64URL encode base64 encoded string + * + * @param {string} base64String Base64 encoded string + * @returns {string} Base64URL encoded string + */ + phpbb.base64UrlEncode = base64String => base64String.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); -/** - * Base64URL encode base64 encoded string - * - * @param {string} base64String Base64 encoded string - * @returns {string} Base64URL encoded string - */ -phpbb.base64UrlEncode = (base64String) => { - return base64String.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); -}; + /** + * Register page dropdowns. + */ + phpbb.registerPageDropdowns = function() { + const $body = $('body'); -/** -* 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 = { + $body.find('.dropdown-container').each(function() { + const $this = $(this); + let $trigger = $this.find('.dropdown-trigger:first'); + let $contents = $this.find('.dropdown'); + const options = { direction: 'auto', - verticalDirection: 'auto' - }, - data; + verticalDirection: 'auto', + }; + let data; - if (!$trigger.length) { - data = $this.attr('data-dropdown-trigger'); - $trigger = data ? $this.children(data) : $this.children('a:first'); + 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(e => { + const $parents = $(e.target).parents(); + if (!$parents.is(phpbb.dropdownVisibleContainers)) { + $(phpbb.dropdownHandles).each(phpbb.toggleDropdown); + } + }); + }; + + /** + * Handle avatars to be lazy loaded. + */ + phpbb.lazyLoadAvatars = () => { + $('.avatar[data-src]').each(function() { + const $avatar = $(this); + + $avatar + .attr('src', $avatar.data('src')) + .removeAttr('data-src'); + }); + }; + + /** + * Get editor text area element + * + * @param {string} formName Name of form + * @param {string} textareaName Textarea name + * + * @return {HTMLElement|null} Text area element or null if textarea couldn't be found + */ + phpbb.getEditorTextArea = function(formName, textareaName) { + let doc; + + // find textarea, make sure browser supports necessary functions + if (document.forms[formName]) { + doc = document; + } else { + doc = opener.document; } - if (!$contents.length) { - data = $this.attr('data-dropdown-contents'); - $contents = data ? $this.children(data) : $this.children('div:first'); - } - - if (!$trigger.length || !$contents.length) { + if (!doc.forms[formName]) { 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'; - } + return doc.forms[formName].elements[textareaName]; + }; - phpbb.registerDropdown($trigger, $contents, options); - }); + phpbb.recaptcha = { + button: null, + ready: false, - // 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); - } - }); -}; + token: $('input[name="recaptcha_token"]'), + form: $('.g-recaptcha').parents('form'), + v3: $('[data-recaptcha-v3]'), -/** - * 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'); - }); -}; - -/** - * Get editor text area element - * - * @param {string} formName Name of form - * @param {string} textareaName Textarea name - * - * @return {HTMLElement|null} Text area element or null if textarea couldn't be found - */ -phpbb.getEditorTextArea = function(formName, textareaName) { - let doc; - - // find textarea, make sure browser supports necessary functions - if (document.forms[formName]) { - doc = document; - } else { - doc = opener.document; - } - - if (!doc.forms[formName]) { - return; - } - - return doc.forms[formName].elements[textareaName]; -} - -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() { + load() { + phpbb.recaptcha.bindButton(); + phpbb.recaptcha.bindForm(); + }, + bindButton() { + 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) { + phpbb.recaptcha.button = this; + }); + }, + bindForm() { + phpbb.recaptcha.form.on('submit', 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) { + // eslint-disable-next-line no-undef + grecaptcha.execute( + phpbb.recaptcha.v3.data('recaptcha-v3'), + { action: phpbb.recaptcha.v3.val() }, + ).then(token => { // Place the token inside the form - phpbb.recaptcha.token.val(token); + phpbb.recaptcha.token.val(token); - // And now we submit the form. - phpbb.recaptcha.submitForm(); - }); - } else { + // And now we submit the form. + phpbb.recaptcha.submitForm(); + }); + } else { // Regular version 2 execution - grecaptcha.execute(); - } + // eslint-disable-next-line no-undef + grecaptcha.execute(); + } - // Do not submit the form - e.preventDefault(); - } - }); - }, - submitForm: function() { + // Do not submit the form + e.preventDefault(); + } + }); + }, + submitForm() { // Now we are ready, so set it to true. // so the 'submit' event doesn't run multiple times. - phpbb.recaptcha.ready = true; + phpbb.recaptcha.ready = true; - if (phpbb.recaptcha.button) { + 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') { + 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.name = 'submit_btn'; + } + + phpbb.recaptcha.form.submit(); } + }, + }; - 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) { + // 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(); - } + }; - $('textarea[data-bbcode]').each(function() { - phpbb.applyCodeEditor(this); - }); + window.phpbbRecaptchaOnSubmit = function() { + phpbb.recaptcha.submitForm(); + }; - phpbb.registerPageDropdowns(); + $(window).on('load', phpbb.lazyLoadAvatars); - $('[data-color-palette], [data-orientation]').each(function() { - phpbb.registerPalette($(this)); - }); + /** + * Apply code editor to all textarea elements with data-bbcode attribute + */ + $(() => { + // reCAPTCHA v3 needs to be initialized + if (phpbb.recaptcha.v3.length) { + phpbb.recaptcha.load(); + } - // 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')); + $('textarea[data-bbcode]').each(function() { + phpbb.applyCodeEditor(this); + }); - // Hide settings that are not selected via select element. - $('select[data-togglable-settings]').each(function() { - var $this = $(this); + phpbb.registerPageDropdowns(); - $this.change(function() { + $('[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() { + const $this = $(this); + + $this.change(() => { + phpbb.toggleSelectSettings($this); + }); phpbb.toggleSelectSettings($this); }); - phpbb.toggleSelectSettings($this); }); -}); - })(jQuery); // Avoid conflicts with other libraries diff --git a/phpBB/assets/javascript/editor.js b/phpBB/assets/javascript/editor.js index 3b86ca380d..72a982db42 100644 --- a/phpBB/assets/javascript/editor.js +++ b/phpBB/assets/javascript/editor.js @@ -1,4 +1,7 @@ /* global phpbb */ +/* eslint camelcase: 0 */ +/* eslint no-undef: 0 */ +/* eslint no-unused-vars: 0 */ /** * bbCode control by subBlue design [ www.subBlue.com ] @@ -6,25 +9,25 @@ */ // Startup variables -var imageTag = false; -var theSelection = false; -var bbcodeEnabled = true; +const imageTag = false; +let theSelection = false; +const 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 +const clientPC = navigator.userAgent.toLowerCase(); // Get client info +const 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; +const is_ie = ((clientPC.indexOf('msie') !== -1) && (clientPC.indexOf('opera') === -1)); +const is_win = ((clientPC.indexOf('win') !== -1) || (clientPC.indexOf('16bit') !== -1)); +let baseHeight; /** * Fix a bug involving the TextRange object. From * http://www.frostjedi.com/terra/scripts/demo/caretBug.html */ function initInsertions() { - var doc; + let doc; if (document.forms[form_name]) { doc = document; @@ -32,9 +35,9 @@ function initInsertions() { doc = opener.document; } - var textarea = doc.forms[form_name].elements[text_name]; + const textarea = doc.forms[form_name].elements[text_name]; - if (is_ie && typeof(baseHeight) !== 'number') { + if (is_ie && typeof (baseHeight) !== 'number') { textarea.focus(); baseHeight = doc.selection.createRange().duplicate().boundingHeight; @@ -48,11 +51,11 @@ function initInsertions() { * bbstyle */ function bbstyle(bbnumber) { - if (bbnumber !== -1) { - bbfontstyle(bbtags[bbnumber], bbtags[bbnumber+1]); - } else { + if (bbnumber === -1) { insert_text('[*]'); document.forms[form_name].elements[text_name].focus(); + } else { + bbfontstyle(bbtags[bbnumber], bbtags[bbnumber + 1]); } } @@ -62,7 +65,7 @@ function bbstyle(bbnumber) { function bbfontstyle(bbopen, bbclose) { theSelection = false; - var textarea = document.forms[form_name].elements[text_name]; + const textarea = document.forms[form_name].elements[text_name]; textarea.focus(); @@ -84,9 +87,9 @@ function bbfontstyle(bbopen, bbclose) { return; } - //The new position for the cursor after adding the bbcode - var caret_pos = getCaretPosition(textarea).start; - var new_pos = caret_pos + bbopen.length; + // The new position for the cursor after adding the bbcode + const caret_pos = getCaretPosition(textarea).start; + const new_pos = caret_pos + bbopen.length; // Open tag insert_text(bbopen + bbclose); @@ -96,11 +99,10 @@ function bbfontstyle(bbopen, bbclose) { 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); + } else if (document.selection) { + // IE + const range = textarea.createTextRange(); + range.move('character', new_pos); range.select(); storeCaret(textarea); } @@ -112,12 +114,12 @@ function bbfontstyle(bbopen, bbclose) { * Insert text at position */ function insert_text(text, spaces, popup) { - var textarea; + let textarea; - if (!popup) { - textarea = document.forms[form_name].elements[text_name]; - } else { + if (popup) { textarea = opener.document.forms[form_name].elements[text_name]; + } else { + textarea = document.forms[form_name].elements[text_name]; } if (spaces) { @@ -127,8 +129,8 @@ function insert_text(text, spaces, popup) { // 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; + const sel_start = textarea.selectionStart; + const sel_end = textarea.selectionEnd; mozWrap(textarea, text, ''); textarea.selectionStart = sel_start + text.length; @@ -139,10 +141,10 @@ function insert_text(text, spaces, popup) { storeCaret(textarea); } - var caret_pos = textarea.caretPos; + const 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; + textarea.value += text; } if (!popup) { @@ -162,15 +164,16 @@ function attachInline(index, filename) { * 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; + const message_name = 'message_' + post_id; + let theSelection = ''; + let divarea = false; + let i; if (l_wrote === undefined) { // Backwards compatibility l_wrote = 'wrote'; } + if (typeof attributes !== 'object') { attributes = {}; } @@ -195,10 +198,10 @@ function addquote(post_id, username, l_wrote, attributes) { 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, ' '); + 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) { @@ -213,8 +216,8 @@ function addquote(post_id, username, l_wrote, attributes) { attributes.author = username; insert_text(generateQuote(theSelection, attributes)); } else { - insert_text(username + ' ' + l_wrote + ':' + '\n'); - var lines = split_lines(theSelection); + insert_text(username + ' ' + l_wrote + ':\n'); + const lines = split_lines(theSelection); for (i = 0; i < lines.length; i++) { insert_text('> ' + lines[i] + '\n'); } @@ -237,20 +240,22 @@ function addquote(post_id, username, l_wrote, attributes) { */ function generateQuote(text, attributes) { text = text.replace(/^\s+/, '').replace(/\s+$/, ''); - var quote = '[quote'; + let 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]; + + for (const name in attributes) { + if (Object.hasOwn(attributes, name)) { + const value = attributes[name]; quote += ' ' + name + '=' + formatAttributeValue(value.toString()); } } + quote += ']'; - var newline = ((quote + text + '[/quote]').length > 80 || text.indexOf('\n') > -1) ? '\n' : ''; + const newline = ((quote + text + '[/quote]').length > 80 || text.indexOf('\n') > -1) ? '\n' : ''; quote += newline + text + newline + '[/quote]'; return quote; @@ -271,25 +276,26 @@ function formatAttributeValue(str) { // Return as-is if it contains none of: space, ' " \ or ] return str; } - var singleQuoted = "'" + str.replace(/[\\']/g, '\\$&') + "'", - doubleQuoted = '"' + str.replace(/[\\"]/g, '\\$&') + '"'; + + const singleQuoted = '\'' + str.replace(/[\\']/g, '\\$&') + '\''; + const 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; + const lines = text.split('\n'); + const splitLines = []; + let j = 0; + let i; - for(i = 0; i < lines.length; 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; + let line = lines[i]; + let splitAt; do { splitAt = line.indexOf(' ', 80); @@ -302,9 +308,10 @@ function split_lines(text) { j++; } } - while(splitAt !== -1); + while (splitAt !== -1); } } + return splitLines; } @@ -312,22 +319,20 @@ function split_lines(text) { * 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; + const selLength = (typeof (txtarea.textLength) === 'undefined') ? txtarea.value.length : txtarea.textLength; + const selStart = txtarea.selectionStart; + const selEnd = txtarea.selectionEnd; + const { scrollTop } = txtarea; - var s1 = (txtarea.value).substring(0,selStart); - var s2 = (txtarea.value).substring(selStart, selEnd); - var s3 = (txtarea.value).substring(selEnd, selLength); + const s1 = (txtarea.value).substring(0, selStart); + const s2 = (txtarea.value).substring(selStart, selEnd); + const 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; } /** @@ -343,33 +348,32 @@ function storeCaret(textEl) { /** * Caret Position object */ -function caretPosition() { - var start = null; - var end = null; +function CaretPosition() { + const start = null; + const end = null; } /** * Get the caret position in an textarea */ function getCaretPosition(txtarea) { - var caretPos = new caretPosition(); + const 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) { + } else if (document.selection) { + // dirty and slow IE way // get current selection - var range = document.selection.createRange(); + const range = document.selection.createRange(); // a new selection of the whole textarea - var range_all = document.body.createTextRange(); + const 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; + let sel_start; for (sel_start = 0; range_all.compareEndPoints('StartToStart', range) < 0; sel_start++) { range_all.moveStart('character', 1); } @@ -404,7 +408,7 @@ function getCaretPosition(txtarea) { phpbb.showDragNDrop(textarea); } - $('textarea').on('keydown', function (e) { + $('textarea').on('keydown', function(e) { if (e.which === 13 && (e.metaKey || e.ctrlKey)) { $(this).closest('form').find(':submit').click(); } diff --git a/phpBB/assets/javascript/installer.js b/phpBB/assets/javascript/installer.js index cfe685f88b..cf7820b26d 100644 --- a/phpBB/assets/javascript/installer.js +++ b/phpBB/assets/javascript/installer.js @@ -2,21 +2,22 @@ * Installer's AJAX frontend handler */ +/* eslint no-prototype-builtins: 0 */ (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; + let pollTimer = null; + let nextReadPosition = 0; + let progressBarTriggered = false; + let progressTimer = null; + let currentProgress = 0; + let refreshRequested = false; + let transmissionOver = false; + let statusCount = 0; // Template related variables - var $contentWrapper = $('.install-body').find('.main'); + const $contentWrapper = $('.install-body').find('.main'); // Intercept form submits interceptFormSubmit($('#install_install')); @@ -42,12 +43,15 @@ */ function addMessage(type, messages) { // Get message containers - var $errorContainer = $('#error-container'); - var $warningContainer = $('#warning-container'); - var $logContainer = $('#log-container'); + const $errorContainer = $('#error-container'); + const $warningContainer = $('#warning-container'); + const $logContainer = $('#log-container'); - var $title, $description, $msgElement, arraySize = messages.length; - for (var i = 0; i < arraySize; i++) { + let $title; + let $description; + let $msgElement; + const arraySize = messages.length; + for (let i = 0; i < arraySize; i++) { $msgElement = $('
'); $title = $(''); $title.text(messages[i].title); @@ -59,24 +63,19 @@ $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; + if (type === 'error') { + $msgElement.addClass('errorbox'); + $errorContainer.append($msgElement); + } else if (type === 'warning') { + $msgElement.addClass('warningbox'); + $warningContainer.append($msgElement); + } else if (type === 'log') { + $msgElement.addClass('log'); + $logContainer.prepend($msgElement); + $logContainer.addClass('show_log_container'); + } else if (type === 'success') { + $msgElement.addClass('successbox'); + $errorContainer.prepend($msgElement); } } } @@ -84,12 +83,14 @@ /** * Render a download box */ - function addDownloadBox(downloadArray) - { - var $downloadContainer = $('#download-wrapper'); - var $downloadBox, $title, $content, $link; + function addDownloadBox(downloadArray) { + const $downloadContainer = $('#download-wrapper'); + let $downloadBox; + let $title; + let $content; + let $link; - for (var i = 0; i < downloadArray.length; i++) { + for (let i = 0; i < downloadArray.length; i++) { $downloadBox = $('
'); $downloadBox.addClass('download-box'); @@ -116,9 +117,8 @@ /** * Render update files' status */ - function addUpdateFileStatus(fileStatus) - { - var $statusContainer = $('#file-status-wrapper'); + function addUpdateFileStatus(fileStatus) { + const $statusContainer = $('#file-status-wrapper'); $statusContainer.html(fileStatus); } @@ -128,9 +128,9 @@ * @param formHtml */ function addForm(formHtml) { - var $formContainer = $('#form-wrapper'); + const $formContainer = $('#form-wrapper'); $formContainer.html(formHtml); - var $form = $('#install_install'); + const $form = $('#install_install'); interceptFormSubmit($form); } @@ -140,14 +140,16 @@ * @param navObj */ function updateNavbarStatus(navObj) { - var navID, $stage, $stageListItem, $active; - $active = $('#activemenu'); + let navID; + let $stage; + let $stageListItem; + const $active = $('#activemenu'); if (navObj.hasOwnProperty('finished')) { // This should be an Array - var navItems = navObj.finished; + const navItems = navObj.finished; - for (var i = 0; i < navItems.length; i++) { + for (let i = 0; i < navItems.length; i++) { navID = 'installer-stage-' + navItems[i]; $stage = $('#' + navID); $stageListItem = $stage.parent(); @@ -179,12 +181,16 @@ * @param progressObject */ function setProgress(progressObject) { - var $statusText, $progressBar, $progressText, $progressFiller, $progressFillerText; + let $statusText; + let $progressBar; + let $progressText; + let $progressFiller; + let $progressFillerText; if (progressObject.task_name.length) { if (!progressBarTriggered) { // Create progress bar - var $progressBarWrapper = $('#progress-bar-container'); + const $progressBarWrapper = $('#progress-bar-container'); // Create progress bar elements $progressBar = $('
'); @@ -234,9 +240,9 @@ // Set cookies function setCookies(cookies) { - var cookie; + let cookie; - for (var i = 0; i < cookies.length; i++) { + for (let i = 0; i < cookies.length; i++) { // Set cookie name and value cookie = encodeURIComponent(cookies[i].name) + '=' + encodeURIComponent(cookies[i].value); // Set path @@ -246,11 +252,11 @@ } // Redirects user - function redirect(url, use_ajax) { - if (use_ajax) { + function redirect(url, useAjax) { + if (useAjax) { resetPolling(); - var xhReq = createXhrObject(); + const xhReq = createXhrObject(); xhReq.open('GET', url, true); xhReq.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); xhReq.send(); @@ -268,7 +274,7 @@ */ function parseMessage(messageJSON) { $('#loading_indicator').css('display', 'none'); - var responseObject; + let responseObject; try { responseObject = JSON.parse(messageJSON); @@ -276,6 +282,7 @@ if (window.console) { console.log('Failed to parse JSON object\n\nMessage: ' + err.message + '\n\nServer Response: ' + messageJSON); } else { + // eslint-disable-next-line no-alert alert('Failed to parse JSON object\n\nMessage: ' + err.message + '\n\nServer Response: ' + messageJSON); } @@ -359,10 +366,12 @@ } else { $('#loading_indicator').css('display', 'none'); addMessage('error', - [{ + [ { + // eslint-disable-next-line no-undef title: installLang.title, - description: installLang.msg - }] + // eslint-disable-next-line no-undef + description: installLang.msg, + } ], ); } } @@ -371,9 +380,9 @@ * Queries the installer's status */ function queryInstallerStatus() { - var url = $(location).attr('pathname'); - var lookUp = 'install/app.php'; - var position = url.indexOf(lookUp); + let url = $(location).attr('pathname'); + let lookUp = 'install/app.php'; + let position = url.indexOf(lookUp); if (position === -1) { lookUp = 'install'; @@ -385,7 +394,7 @@ } url = url.substring(0, position) + lookUp + '/installer/status'; - $.getJSON(url, function(data) { + $.getJSON(url, data => { processTimeoutResponse(data.status); }); } @@ -396,9 +405,12 @@ * @param xhReq XHR object */ function pollContent(xhReq) { - var messages = xhReq.responseText; - var msgSeparator = '}\n\n'; - var unprocessed, messageEndIndex, endOfMessageIndex, message; + const messages = xhReq.responseText; + const msgSeparator = '}\n\n'; + let unprocessed; + let messageEndIndex; + let endOfMessageIndex; + let message; do { unprocessed = messages.substring(nextReadPosition); @@ -416,7 +428,7 @@ $('#loading_indicator').css('display', 'none'); resetPolling(); - var timeoutDetected = !transmissionOver; + const timeoutDetected = !transmissionOver; if (refreshRequested) { refreshRequested = false; @@ -444,7 +456,7 @@ return; } - var $progressBar = $('#progress-bar'); + const $progressBar = $('#progress-bar'); currentProgress++; $progressFillerText.css('width', $progressBar.width()); @@ -459,14 +471,14 @@ * @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; + const $progressFiller = $('#progress-bar-filler'); + const $progressFillerText = $('#progress-bar-filler-text'); + const $progressText = $('#progress-bar-text'); + const progressStart = $progressFiller.width() / $progressFiller.offsetParent().width() * 100; currentProgress = Math.floor(progressStart); clearInterval(progressTimer); - progressTimer = setInterval(function() { + progressTimer = setInterval(() => { incrementFiller($progressText, $progressFiller, $progressFillerText, progressLimit); }, 10); } @@ -487,7 +499,7 @@ function startPolling(xhReq) { resetPolling(); transmissionOver = false; - pollTimer = setInterval(function () { + pollTimer = setInterval(() => { pollContent(xhReq); }, 250); } @@ -498,7 +510,7 @@ function doRefresh() { resetPolling(); - var xhReq = createXhrObject(); + const xhReq = createXhrObject(); xhReq.open('GET', $(location).attr('pathname'), true); xhReq.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); xhReq.send(); @@ -515,47 +527,47 @@ // Clear content $contentWrapper.html(''); - var $header = $('
'); + const $header = $('
'); $header.attr('id', 'header-container'); $contentWrapper.append($header); - var $description = $('
'); + const $description = $('
'); $description.attr('id', 'description-container'); $contentWrapper.append($description); - var $errorContainer = $('
'); + const $errorContainer = $('
'); $errorContainer.attr('id', 'error-container'); $contentWrapper.append($errorContainer); - var $warningContainer = $('
'); + const $warningContainer = $('
'); $warningContainer.attr('id', 'warning-container'); $contentWrapper.append($warningContainer); - var $progressContainer = $('
'); + const $progressContainer = $('
'); $progressContainer.attr('id', 'progress-bar-container'); $contentWrapper.append($progressContainer); - var $logContainer = $('
'); + const $logContainer = $('
'); $logContainer.attr('id', 'log-container'); $contentWrapper.append($logContainer); - var $installerContentWrapper = $('
'); + const $installerContentWrapper = $('
'); $installerContentWrapper.attr('id', 'content-container'); $contentWrapper.append($installerContentWrapper); - var $installerDownloadWrapper = $('
'); + const $installerDownloadWrapper = $('
'); $installerDownloadWrapper.attr('id', 'download-wrapper'); $installerContentWrapper.append($installerDownloadWrapper); - var $updaterFileStatusWrapper = $('
'); + const $updaterFileStatusWrapper = $('
'); $updaterFileStatusWrapper.attr('id', 'file-status-wrapper'); $installerContentWrapper.append($updaterFileStatusWrapper); - var $formWrapper = $('
'); + const $formWrapper = $('
'); $formWrapper.attr('id', 'form-wrapper'); $installerContentWrapper.append($formWrapper); - var $spinner = $('
'); + const $spinner = $('
'); $spinner.attr('id', 'loading_indicator'); $spinner.html(' '); $contentWrapper.append($spinner); @@ -565,7 +577,7 @@ function submitForm($form, $submitBtn) { $form.css('display', 'none'); - var xhReq = createXhrObject(); + const xhReq = createXhrObject(); xhReq.open('POST', $form.attr('action'), true); xhReq.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); xhReq.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); @@ -590,7 +602,7 @@ * @returns {*} */ function getFormFields($form, $submitBtn) { - var formData = $form.serialize(); + let formData = $form.serialize(); formData += ((formData.length) ? '&' : '') + encodeURIComponent($submitBtn.attr('name')) + '='; formData += encodeURIComponent($submitBtn.attr('value')); @@ -605,11 +617,13 @@ function interceptFormSubmit($form) { if (!$form.length) { return; - } else if ($form.find('input[name="admin_name"]').length > 0) { + } + + if ($form.find('input[name="admin_name"]').length > 0) { setAdminTimezone($form); } - $form.find(':submit').bind('click', function (event) { + $form.find(':submit').bind('click', function(event) { event.preventDefault(); submitForm($form, $(this)); }); @@ -623,7 +637,8 @@ function setAdminTimezone($form) { // Set admin timezone if it does not exist yet if ($form.find('input[name="admin_timezone"]').length === 0) { - const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; + // eslint-disable-next-line new-cap + const { timeZone } = Intl.DateTimeFormat().resolvedOptions(); // Add timezone as form entry const timezoneEntry = $(''); diff --git a/phpBB/assets/javascript/plupload.js b/phpBB/assets/javascript/plupload.js index 8c52aae819..f2f0fb5497 100644 --- a/phpBB/assets/javascript/plupload.js +++ b/phpBB/assets/javascript/plupload.js @@ -1,714 +1,723 @@ -/* global phpbb, plupload, attachInline */ +/* global phpbb, plupload, attachInline, activateSubPanel */ +/* eslint camelcase: 0 */ plupload.addI18n(phpbb.plupload.i18n); phpbb.plupload.ids = []; -(function($) { // Avoid conflicts with other libraries +(function($) { // Avoid conflicts with other libraries + 'use strict'; -'use strict'; - -/** - * Set up the uploader. - */ -phpbb.plupload.initialize = function() { + /** + * Set up the uploader. + */ + phpbb.plupload.initialize = function() { // Initialize the Plupload uploader. - phpbb.plupload.uploader.init(); + phpbb.plupload.uploader.init(); - // Set attachment data. - phpbb.plupload.setData(phpbb.plupload.data); - phpbb.plupload.updateMultipartParams(phpbb.plupload.getSerializedData()); + // Set attachment data. + phpbb.plupload.setData(phpbb.plupload.data); + phpbb.plupload.updateMultipartParams(phpbb.plupload.getSerializedData()); - // Only execute if Plupload initialized successfully. - phpbb.plupload.uploader.bind('Init', function() { - phpbb.plupload.form = $(phpbb.plupload.config.form_hook)[0]; - let $attachRowTemplate = $('#attach-row-tpl'); - $attachRowTemplate.removeClass('attach-row-tpl'); - phpbb.plupload.rowTpl = $attachRowTemplate[0].outerHTML; + // Only execute if Plupload initialized successfully. + phpbb.plupload.uploader.bind('Init', () => { + phpbb.plupload.form = $(phpbb.plupload.config.form_hook)[0]; + const $attachRowTemplate = $('#attach-row-tpl'); + $attachRowTemplate.removeClass('attach-row-tpl'); + phpbb.plupload.rowTpl = $attachRowTemplate[0].outerHTML; - // Hide the basic upload panel and remove the attach row template. - $('#attach-row-tpl, #attach-panel-basic').remove(); - // Show multi-file upload options. - $('#attach-panel-multi').show(); - }); - - phpbb.plupload.uploader.bind('PostInit', function() { - // Point out the drag-and-drop zone if it's supported. - if (phpbb.plupload.uploader.features.dragdrop) { - $('#drag-n-drop-message').show(); - } - - // Ensure "Add files" button position is correctly calculated. - if ($('#attach-panel-multi').is(':visible')) { - phpbb.plupload.uploader.refresh(); - } - $('[data-subpanel="attach-panel"]').one('click', function() { - phpbb.plupload.uploader.refresh(); + // Hide the basic upload panel and remove the attach row template. + $('#attach-row-tpl, #attach-panel-basic').remove(); + // Show multi-file upload options. + $('#attach-panel-multi').show(); }); - }); -}; -/** - * Unsets all elements in the object uploader.settings.multipart_params whose keys - * begin with 'attachment_data[' - */ -phpbb.plupload.clearParams = function() { - var obj = phpbb.plupload.uploader.settings.multipart_params; - for (var key in obj) { - if (!obj.hasOwnProperty(key) || key.indexOf('attachment_data[') !== 0) { - continue; - } + phpbb.plupload.uploader.bind('PostInit', () => { + // Point out the drag-and-drop zone if it's supported. + if (phpbb.plupload.uploader.features.dragdrop) { + $('#drag-n-drop-message').show(); + } - delete phpbb.plupload.uploader.settings.multipart_params[key]; - } -}; + // Ensure "Add files" button position is correctly calculated. + if ($('#attach-panel-multi').is(':visible')) { + phpbb.plupload.uploader.refresh(); + } -/** - * Update uploader.settings.multipart_params object with new data. - * - * @param {object} obj - */ -phpbb.plupload.updateMultipartParams = function(obj) { - var settings = phpbb.plupload.uploader.settings; - settings.multipart_params = $.extend(settings.multipart_params, obj); -}; + $('[data-subpanel="attach-panel"]').one('click', () => { + phpbb.plupload.uploader.refresh(); + }); + }); + }; -/** - * Convert the array of attachment objects into an object that PHP would expect as POST data. - * - * @returns {object} An object in the form 'attachment_data[i][key]': value as - * expected by the server - */ -phpbb.plupload.getSerializedData = function() { - var obj = {}; - for (var i = 0; i < phpbb.plupload.data.length; i++) { - var datum = phpbb.plupload.data[i]; - for (var key in datum) { - if (!datum.hasOwnProperty(key)) { + /** + * Unsets all elements in the object uploader.settings.multipart_params whose keys + * begin with 'attachment_data[' + */ + phpbb.plupload.clearParams = function() { + const obj = phpbb.plupload.uploader.settings.multipart_params; + for (const key in obj) { + if (!Object.prototype.hasOwnProperty.call(obj, key) || key.indexOf('attachment_data[') !== 0) { continue; } - obj['attachment_data[' + i + '][' + key + ']'] = datum[key]; + delete phpbb.plupload.uploader.settings.multipart_params[key]; } - } - - // Insert form data - var $pluploadForm = $(phpbb.plupload.config.form_hook).first(); - obj.creation_time = $pluploadForm.find('input[type=hidden][name="creation_time"]').val(); - obj.form_token = $pluploadForm.find('input[type=hidden][name="form_token"]').val(); - - return obj; -}; - -/** - * Get the index from the phpbb.plupload.data array where the given - * attachment id appears. - * - * @param {int} attachId The attachment id of the file. - * @returns {bool|int} Index of the file if exists, otherwise false. - */ -phpbb.plupload.getIndex = function(attachId) { - var index = $.inArray(Number(attachId), phpbb.plupload.ids); - return (index !== -1) ? index : false; -}; - -/** - * Set the data in phpbb.plupload.data and phpbb.plupload.ids arrays. - * - * @param {Array} data Array containing the new data to use. In the form of - * array(index => object(property: value). Requires attach_id to be one of the object properties. - */ -phpbb.plupload.setData = function(data) { - // Make sure that the array keys are reset. - phpbb.plupload.ids = phpbb.plupload.data = []; - phpbb.plupload.data = data; - - for (var i = 0; i < data.length; i++) { - phpbb.plupload.ids.push(Number(data[i].attach_id)); - } -}; - -/** - * Update the attachment data in the HTML and the phpbb & phpbb.plupload objects. - * - * @param {Array} data Array containing the new data to use. - * @param {string} action The action that required the update. Used to update the inline attachment bbcodes. - * @param {int} index The index from phpbb.plupload_ids that was affected by the action. - * @param {Array} downloadUrl Optional array of download urls to update. - */ -phpbb.plupload.update = function(data, action, index, downloadUrl) { - - phpbb.plupload.updateBbcode(action, index); - phpbb.plupload.setData(data); - phpbb.plupload.updateRows(downloadUrl); - phpbb.plupload.clearParams(); - phpbb.plupload.updateMultipartParams(phpbb.plupload.getSerializedData()); -}; - -/** - * Update the relevant elements and hidden data for all attachments. - * - * @param {Array} downloadUrl Optional array of download urls to update. - */ -phpbb.plupload.updateRows = function(downloadUrl) { - for (var i = 0; i < phpbb.plupload.ids.length; i++) { - phpbb.plupload.updateRow(i, downloadUrl); - } -}; - -/** - * Insert a row for a new attachment. This expects an HTML snippet in the HTML - * using the id "attach-row-tpl" to be present. This snippet is cloned and the - * data for the file inserted into it. The row is then appended or prepended to - * #file-list based on the attach_order setting. - * - * @param {object} file Plupload file object for the new attachment. - */ -phpbb.plupload.insertRow = function(file) { - var row = $(phpbb.plupload.rowTpl); - - row.attr('id', file.id); - row.find('.file-name').html(plupload.xmlEncode(file.name)); - row.find('.file-size').html(plupload.formatSize(file.size)); - - if (phpbb.plupload.order === 'desc') { - $('#file-list').prepend(row); - } else { - $('#file-list').append(row); - } -}; - -/** - * Update the relevant elements and hidden data for an attachment. - * - * @param {int} index The index from phpbb.plupload.ids of the attachment to edit. - * @param {Array} downloadUrl Optional array of download urls to update. - */ -phpbb.plupload.updateRow = function(index, downloadUrl) { - var attach = phpbb.plupload.data[index], - row = $('[data-attach-id="' + attach.attach_id + '"]'); - - // Add the link to the file - if (typeof downloadUrl !== 'undefined' && typeof downloadUrl[index] !== 'undefined') { - var url = downloadUrl[index].replace('&', '&'), - link = $(''); - - link.attr('href', url).html(attach.real_filename); - row.find('.file-name').html(link); - } - - row.find('textarea').attr('name', 'comment_list[' + index + ']'); - phpbb.plupload.updateHiddenData(row, attach, index); -}; - -/** - * Update hidden input data for an attachment. - * - * @param {object} row jQuery object for the attachment row. - * @param {object} attach Attachment data object from phpbb.plupload.data - * @param {int} index Attachment index from phpbb.plupload.ids - */ -phpbb.plupload.updateHiddenData = function(row, attach, index) { - row.find('input[type="hidden"]').remove(); - - for (var key in attach) { - if (!attach.hasOwnProperty(key)) { - return; - } - - var input = $('') - .attr('type', 'hidden') - .attr('name', 'attachment_data[' + index + '][' + key + ']') - .attr('value', attach[key]); - $(row).append(input); - } -}; - -/** - * Deleting a file removes it from the queue and fires an AJAX event to the - * server to tell it to remove the temporary attachment. The server - * responds with the updated attachment data list so that any future - * uploads can maintain state with the server - * - * @param {object} row jQuery object for the attachment row. - * @param {int} attachId Attachment id of the file to be removed. - */ -phpbb.plupload.deleteFile = function(row, attachId) { - // If there's no attach id, then the file hasn't been uploaded. Simply delete the row. - if (typeof attachId === 'undefined') { - var file = phpbb.plupload.uploader.getFile(row.attr('id')); - phpbb.plupload.uploader.removeFile(file); - - row.slideUp(100, function() { - row.remove(); - phpbb.plupload.hideEmptyList(); - }); - } - - var index = phpbb.plupload.getIndex(attachId); - row.find('.file-status').toggleClass('file-uploaded file-working'); - - if (index === false) { - return; - } - var fields = {}; - fields['delete_file[' + index + ']'] = 1; - - var always = function() { - row.find('.file-status').removeClass('file-working'); }; - var done = function(response) { - if (typeof response !== 'object') { - return; - } - - // trigger_error() was called which likely means a permission error was encountered. - if (typeof response.title !== 'undefined') { - phpbb.plupload.uploader.trigger('Error', { message: response.message }); - // We will have to assume that the deletion failed. So leave the file status as uploaded. - row.find('.file-status').toggleClass('file-uploaded'); - - return; - } - - // Handle errors while deleting file - if (typeof response.error !== 'undefined') { - phpbb.alert(phpbb.plupload.lang.ERROR, response.error.message); - - // We will have to assume that the deletion failed. So leave the file status as uploaded. - row.find('.file-status').toggleClass('file-uploaded'); - - return; - } - - phpbb.plupload.update(response, 'removal', index); - // Check if the user can upload files now if he had reached the max files limit. - phpbb.plupload.handleMaxFilesReached(); - - if (row.attr('id')) { - var file = phpbb.plupload.uploader.getFile(row.attr('id')); - phpbb.plupload.uploader.removeFile(file); - } - row.slideUp(100, function() { - row.remove(); - // Hide the file list if it's empty now. - phpbb.plupload.hideEmptyList(); - }); - phpbb.plupload.uploader.trigger('FilesRemoved'); + /** + * Update uploader.settings.multipart_params object with new data. + * + * @param {object} obj + */ + phpbb.plupload.updateMultipartParams = function(obj) { + const { settings } = phpbb.plupload.uploader; + settings.multipart_params = $.extend(settings.multipart_params, obj); }; - $.ajax(phpbb.plupload.config.url, { - type: 'POST', - data: $.extend(fields, phpbb.plupload.getSerializedData()), - headers: phpbb.plupload.config.headers - }) - .always(always) - .done(done); -}; + /** + * Convert the array of attachment objects into an object that PHP would expect as POST data. + * + * @returns {object} An object in the form 'attachment_data[i][key]': value as + * expected by the server + */ + phpbb.plupload.getSerializedData = function() { + const obj = {}; + for (let i = 0; i < phpbb.plupload.data.length; i++) { + const datum = phpbb.plupload.data[i]; + for (const key in datum) { + if (!Object.prototype.hasOwnProperty.call(datum, key)) { + continue; + } -/** - * Check the attachment list and hide its container if it's empty. - */ -phpbb.plupload.hideEmptyList = function() { - if (!$('#file-list').children().length) { - $('#file-list-container').slideUp(100); - } -}; - -/** - * Update the indices used in inline attachment bbcodes. This ensures that the - * bbcodes correspond to the correct file after a file is added or removed. - * This should be called before the phpbb.plupload,data and phpbb.plupload.ids - * arrays are updated, otherwise it will not work correctly. - * - * @param {string} action The action that occurred -- either "addition" or "removal" - * @param {int} index The index of the attachment from phpbb.plupload.ids that was affected. - */ -phpbb.plupload.updateBbcode = function(action, index) { - var textarea = $('#message', phpbb.plupload.form), - text = textarea.val(), - removal = (action === 'removal'); - - // Return if the bbcode isn't used at all. - if (text.indexOf('[attachment=') === -1) { - return; - } - - function runUpdate(i) { - var regex = new RegExp('\\[attachment=' + i + '\\](.*?)\\[\\/attachment\\]', 'g'); - text = text.replace(regex, function updateBbcode(_, fileName) { - // Remove the bbcode if the file was removed. - if (removal && index === i) { - return ''; + obj['attachment_data[' + i + '][' + key + ']'] = datum[key]; } - var newIndex = i + ((removal) ? -1 : 1); - return '[attachment=' + newIndex + ']' + fileName + '[/attachment]'; - }); - } - - // Loop forwards when removing and backwards when adding ensures we don't - // corrupt the bbcode index. - var i; - if (removal) { - for (i = index; i < phpbb.plupload.ids.length; i++) { - runUpdate(i); } - } else { - for (i = phpbb.plupload.ids.length - 1; i >= index; i--) { - runUpdate(i); + + // Insert form data + const $pluploadForm = $(phpbb.plupload.config.form_hook).first(); + obj.creation_time = $pluploadForm.find('input[type=hidden][name="creation_time"]').val(); + obj.form_token = $pluploadForm.find('input[type=hidden][name="form_token"]').val(); + + return obj; + }; + + /** + * Get the index from the phpbb.plupload.data array where the given + * attachment id appears. + * + * @param {int} attachId The attachment id of the file. + * @returns {bool|int} Index of the file if exists, otherwise false. + */ + phpbb.plupload.getIndex = function(attachId) { + const index = $.inArray(Number(attachId), phpbb.plupload.ids); + return index === -1 ? false : index; + }; + + /** + * Set the data in phpbb.plupload.data and phpbb.plupload.ids arrays. + * + * @param {Array} data Array containing the new data to use. In the form of + * array(index => object(property: value). Requires attach_id to be one of the object properties. + */ + phpbb.plupload.setData = function(data) { + // Make sure that the array keys are reset. + phpbb.plupload.ids = []; + phpbb.plupload.data = []; + phpbb.plupload.data = data; + + for (let i = 0; i < data.length; i++) { + phpbb.plupload.ids.push(Number(data[i].attach_id)); } - } + }; - textarea.val(text); -}; + /** + * Update the attachment data in the HTML and the phpbb & phpbb.plupload objects. + * + * @param {Array} data Array containing the new data to use. + * @param {string} action The action that required the update. Used to update the inline attachment bbcodes. + * @param {int} index The index from phpbb.plupload_ids that was affected by the action. + * @param {Array} downloadUrl Optional array of download urls to update. + */ + phpbb.plupload.update = function(data, action, index, downloadUrl) { + phpbb.plupload.updateBbcode(action, index); + phpbb.plupload.setData(data); + phpbb.plupload.updateRows(downloadUrl); + phpbb.plupload.clearParams(); + phpbb.plupload.updateMultipartParams(phpbb.plupload.getSerializedData()); + }; -/** - * Get Plupload file objects based on their upload status. - * - * @param {int} status Plupload status - plupload.DONE, plupload.FAILED, - * plupload.QUEUED, plupload.STARTED, plupload.STOPPED - * - * @returns {Array} The Plupload file objects matching the status. - */ -phpbb.plupload.getFilesByStatus = function(status) { - var files = []; - - $.each(phpbb.plupload.uploader.files, function(i, file) { - if (file.status === status) { - files.push(file); + /** + * Update the relevant elements and hidden data for all attachments. + * + * @param {Array} downloadUrl Optional array of download urls to update. + */ + phpbb.plupload.updateRows = function(downloadUrl) { + for (let i = 0; i < phpbb.plupload.ids.length; i++) { + phpbb.plupload.updateRow(i, downloadUrl); } - }); - return files; -}; + }; -/** - * Check whether the user has reached the maximun number of files that he's allowed - * to upload. If so, disables the uploader and marks the queued files as failed. Otherwise - * makes sure that the uploader is enabled. - * - * @returns {bool} True if the limit has been reached. False if otherwise. - */ -phpbb.plupload.handleMaxFilesReached = function() { - // If there is no limit, the user is an admin or moderator. - if (!phpbb.plupload.maxFiles) { - return false; - } + /** + * Insert a row for a new attachment. This expects an HTML snippet in the HTML + * using the id "attach-row-tpl" to be present. This snippet is cloned and the + * data for the file inserted into it. The row is then appended or prepended to + * #file-list based on the attach_order setting. + * + * @param {object} file Plupload file object for the new attachment. + */ + phpbb.plupload.insertRow = function(file) { + const row = $(phpbb.plupload.rowTpl); - if (phpbb.plupload.maxFiles <= phpbb.plupload.ids.length) { - // Fail the rest of the queue. - phpbb.plupload.markQueuedFailed(phpbb.plupload.lang.TOO_MANY_ATTACHMENTS); - // Disable the uploader. - phpbb.plupload.disableUploader(); - phpbb.plupload.uploader.trigger('Error', { message: phpbb.plupload.lang.TOO_MANY_ATTACHMENTS }); + row.attr('id', file.id); + row.find('.file-name').html(plupload.xmlEncode(file.name)); + row.find('.file-size').html(plupload.formatSize(file.size)); - return true; - } else if (phpbb.plupload.maxFiles > phpbb.plupload.ids.length) { - // Enable the uploader if the user is under the limit - phpbb.plupload.enableUploader(); - } - return false; -}; + if (phpbb.plupload.order === 'desc') { + $('#file-list').prepend(row); + } else { + $('#file-list').append(row); + } + }; -/** - * Disable the uploader - */ -phpbb.plupload.disableUploader = function() { - $('#add_files').addClass('disabled'); - phpbb.plupload.uploader.disableBrowse(); -}; + /** + * Update the relevant elements and hidden data for an attachment. + * + * @param {int} index The index from phpbb.plupload.ids of the attachment to edit. + * @param {Array} downloadUrl Optional array of download urls to update. + */ + phpbb.plupload.updateRow = function(index, downloadUrl) { + const attach = phpbb.plupload.data[index]; + const row = $('[data-attach-id="' + attach.attach_id + '"]'); -/** - * Enable the uploader - */ -phpbb.plupload.enableUploader = function() { - $('#add_files').removeClass('disabled'); - phpbb.plupload.uploader.disableBrowse(false); -}; + // Add the link to the file + if (typeof downloadUrl !== 'undefined' && typeof downloadUrl[index] !== 'undefined') { + const url = downloadUrl[index].replace('&', '&'); + const link = $(''); -/** - * Mark all queued files as failed. - * - * @param {string} error Error message to present to the user. - */ -phpbb.plupload.markQueuedFailed = function(error) { - var files = phpbb.plupload.getFilesByStatus(plupload.QUEUED); + link.attr('href', url).html(attach.real_filename); + row.find('.file-name').html(link); + } - $.each(files, function(i, file) { - $('#' + file.id).find('.file-progress').hide(); - phpbb.plupload.fileError(file, error); - }); -}; + row.find('textarea').attr('name', 'comment_list[' + index + ']'); + phpbb.plupload.updateHiddenData(row, attach, index); + }; -/** - * Marks a file as failed and sets the error message for it. - * - * @param {object} file Plupload file object that failed. - * @param {string} error Error message to present to the user. - */ -phpbb.plupload.fileError = function(file, error) { - file.status = plupload.FAILED; - file.error = error; - $('#' + file.id).find('.file-status') - .addClass('file-error') - .attr({ - 'data-error-title': phpbb.plupload.lang.ERROR, - 'data-error-message': error - }); -}; + /** + * Update hidden input data for an attachment. + * + * @param {object} row jQuery object for the attachment row. + * @param {object} attach Attachment data object from phpbb.plupload.data + * @param {int} index Attachment index from phpbb.plupload.ids + */ + phpbb.plupload.updateHiddenData = function(row, attach, index) { + row.find('input[type="hidden"]').remove(); + for (const key in attach) { + if (!Object.prototype.hasOwnProperty.call(attach, key)) { + continue; + } -/** - * Set up the Plupload object and get some basic data. - */ -phpbb.plupload.uploader = new plupload.Uploader(phpbb.plupload.config); -phpbb.plupload.initialize(); + const input = $('') + .attr('type', 'hidden') + .attr('name', 'attachment_data[' + index + '][' + key + ']') + .attr('value', attach[key]); + $(row).append(input); + } + }; -/** - * Add a file filter to check for max file sizes per mime type. - */ -plupload.addFileFilter('mime_types_max_file_size', function(types, file, callback) { - if (file.size !== 'undefined') { - $(types).each(function(i, type) { - let extensions = [], - extsArray = type.extensions.split(','); + /** + * Deleting a file removes it from the queue and fires an AJAX event to the + * server to tell it to remove the temporary attachment. The server + * responds with the updated attachment data list so that any future + * uploads can maintain state with the server + * + * @param {object} row jQuery object for the attachment row. + * @param {int} attachId Attachment id of the file to be removed. + */ + phpbb.plupload.deleteFile = function(row, attachId) { + // If there's no attach id, then the file hasn't been uploaded. Simply delete the row. + if (typeof attachId === 'undefined') { + const file = phpbb.plupload.uploader.getFile(row.attr('id')); + phpbb.plupload.uploader.removeFile(file); - $(extsArray).each(function(i, extension) { - /^\s*\*\s*$/.test(extension) ? extensions.push("\\.*") : extensions.push("\\." + extension.replace(new RegExp("[" + "/^$.*+?|()[]{}\\".replace(/./g, "\\$&") + "]", "g"), "\\$&")); + row.slideUp(100, () => { + row.remove(); + phpbb.plupload.hideEmptyList(); }); + } - let regex = new RegExp("(" + extensions.join("|") + ")$", "i"); + const index = phpbb.plupload.getIndex(attachId); + row.find('.file-status').toggleClass('file-uploaded file-working'); - if (regex.test(file.name)) { - if (type.max_file_size !== 'undefined' && type.max_file_size) { - if (file.size > type.max_file_size) { - phpbb.plupload.uploader.trigger('Error', { - code: plupload.FILE_SIZE_ERROR, - message: plupload.translate('File size error.'), - file: file - }); + if (index === false) { + return; + } - callback(false); + const fields = {}; + fields['delete_file[' + index + ']'] = 1; + + const always = function() { + row.find('.file-status').removeClass('file-working'); + }; + + const done = function(response) { + if (typeof response !== 'object') { + return; + } + + // trigger_error() was called which likely means a permission error was encountered. + if (typeof response.title !== 'undefined') { + phpbb.plupload.uploader.trigger('Error', { message: response.message }); + // We will have to assume that the deletion failed. So leave the file status as uploaded. + row.find('.file-status').toggleClass('file-uploaded'); + + return; + } + + // Handle errors while deleting file + if (typeof response.error !== 'undefined') { + phpbb.alert(phpbb.plupload.lang.ERROR, response.error.message); + + // We will have to assume that the deletion failed. So leave the file status as uploaded. + row.find('.file-status').toggleClass('file-uploaded'); + + return; + } + + phpbb.plupload.update(response, 'removal', index); + // Check if the user can upload files now if he had reached the max files limit. + phpbb.plupload.handleMaxFilesReached(); + + if (row.attr('id')) { + const file = phpbb.plupload.uploader.getFile(row.attr('id')); + phpbb.plupload.uploader.removeFile(file); + } + + row.slideUp(100, () => { + row.remove(); + // Hide the file list if it's empty now. + phpbb.plupload.hideEmptyList(); + }); + phpbb.plupload.uploader.trigger('FilesRemoved'); + }; + + $.ajax(phpbb.plupload.config.url, { + type: 'POST', + data: $.extend(fields, phpbb.plupload.getSerializedData()), + headers: phpbb.plupload.config.headers, + }) + .always(always) + .done(done); + }; + + /** + * Check the attachment list and hide its container if it's empty. + */ + phpbb.plupload.hideEmptyList = function() { + if (!$('#file-list').children().length) { + $('#file-list-container').slideUp(100); + } + }; + + /** + * Update the indices used in inline attachment bbcodes. This ensures that the + * bbcodes correspond to the correct file after a file is added or removed. + * This should be called before the phpbb.plupload,data and phpbb.plupload.ids + * arrays are updated, otherwise it will not work correctly. + * + * @param {string} action The action that occurred -- either "addition" or "removal" + * @param {int} index The index of the attachment from phpbb.plupload.ids that was affected. + */ + phpbb.plupload.updateBbcode = function(action, index) { + const textarea = $('#message', phpbb.plupload.form); + let text = textarea.val(); + const removal = (action === 'removal'); + + // Return if the bbcode isn't used at all. + if (text.indexOf('[attachment=') === -1) { + return; + } + + function runUpdate(i) { + const regex = new RegExp('\\[attachment=' + i + '\\](.*?)\\[\\/attachment\\]', 'g'); + text = text.replace(regex, (_, fileName) => { + // Remove the bbcode if the file was removed. + if (removal && index === i) { + return ''; + } + + const newIndex = i + ((removal) ? -1 : 1); + return '[attachment=' + newIndex + ']' + fileName + '[/attachment]'; + }); + } + + // Loop forwards when removing and backwards when adding ensures we don't + // corrupt the bbcode index. + let i; + if (removal) { + for (i = index; i < phpbb.plupload.ids.length; i++) { + runUpdate(i); + } + } else { + for (i = phpbb.plupload.ids.length - 1; i >= index; i--) { + runUpdate(i); + } + } + + textarea.val(text); + }; + + /** + * Get Plupload file objects based on their upload status. + * + * @param {int} status Plupload status - plupload.DONE, plupload.FAILED, + * plupload.QUEUED, plupload.STARTED, plupload.STOPPED + * + * @returns {Array} The Plupload file objects matching the status. + */ + phpbb.plupload.getFilesByStatus = function(status) { + const files = []; + + $.each(phpbb.plupload.uploader.files, (i, file) => { + if (file.status === status) { + files.push(file); + } + }); + return files; + }; + + /** + * Check whether the user has reached the maximun number of files that he's allowed + * to upload. If so, disables the uploader and marks the queued files as failed. Otherwise + * makes sure that the uploader is enabled. + * + * @returns {bool} True if the limit has been reached. False if otherwise. + */ + phpbb.plupload.handleMaxFilesReached = function() { + // If there is no limit, the user is an admin or moderator. + if (!phpbb.plupload.maxFiles) { + return false; + } + + if (phpbb.plupload.maxFiles <= phpbb.plupload.ids.length) { + // Fail the rest of the queue. + phpbb.plupload.markQueuedFailed(phpbb.plupload.lang.TOO_MANY_ATTACHMENTS); + // Disable the uploader. + phpbb.plupload.disableUploader(); + phpbb.plupload.uploader.trigger('Error', { message: phpbb.plupload.lang.TOO_MANY_ATTACHMENTS }); + + return true; + } + + if (phpbb.plupload.maxFiles > phpbb.plupload.ids.length) { + // Enable the uploader if the user is under the limit + phpbb.plupload.enableUploader(); + } + + return false; + }; + + /** + * Disable the uploader + */ + phpbb.plupload.disableUploader = function() { + $('#add_files').addClass('disabled'); + phpbb.plupload.uploader.disableBrowse(); + }; + + /** + * Enable the uploader + */ + phpbb.plupload.enableUploader = function() { + $('#add_files').removeClass('disabled'); + phpbb.plupload.uploader.disableBrowse(false); + }; + + /** + * Mark all queued files as failed. + * + * @param {string} error Error message to present to the user. + */ + phpbb.plupload.markQueuedFailed = function(error) { + const files = phpbb.plupload.getFilesByStatus(plupload.QUEUED); + + $.each(files, (i, file) => { + $('#' + file.id).find('.file-progress').hide(); + phpbb.plupload.fileError(file, error); + }); + }; + + /** + * Marks a file as failed and sets the error message for it. + * + * @param {object} file Plupload file object that failed. + * @param {string} error Error message to present to the user. + */ + phpbb.plupload.fileError = function(file, error) { + file.status = plupload.FAILED; + file.error = error; + $('#' + file.id).find('.file-status') + .addClass('file-error') + .attr({ + 'data-error-title': phpbb.plupload.lang.ERROR, + 'data-error-message': error, + }); + }; + + /** + * Set up the Plupload object and get some basic data. + */ + phpbb.plupload.uploader = new plupload.Uploader(phpbb.plupload.config); + phpbb.plupload.initialize(); + + /** + * Add a file filter to check for max file sizes per mime type. + */ + plupload.addFileFilter('mime_types_max_file_size', (types, file, callback) => { + if (file.size !== 'undefined') { + $(types).each((i, type) => { + const extensions = []; + const extsArray = type.extensions.split(','); + + $(extsArray).each((i, extension) => { + if (/^\s*\*\s*$/.test(extension)) { + extensions.push('\\.*'); + } else { + extensions.push('\\.' + extension.replace(new RegExp('[' + '/^$.*+?|()[]{}\\'.replace(/./g, '\\$&') + ']', 'g'), '\\$&')); + } + }); + + const regex = new RegExp('(' + extensions.join('|') + ')$', 'i'); + + if (regex.test(file.name)) { + if (type.max_file_size !== 'undefined' && type.max_file_size) { + if (file.size > type.max_file_size) { + phpbb.plupload.uploader.trigger('Error', { + code: plupload.FILE_SIZE_ERROR, + message: plupload.translate('File size error.'), + file, + }); + + callback(false); + } else { + callback(true); + } } else { callback(true); } - } else { - callback(true); + + return false; } + }); + } + }); - return false; - } - }); - } -}); + const $fileList = $('#file-list'); -var $fileList = $('#file-list'); + /** + * Insert inline attachment bbcode. + */ + $fileList.on('click', '.file-inline-bbcode', function(e) { + const attachId = $(this).parents('.attach-row').attr('data-attach-id'); + const index = phpbb.plupload.getIndex(attachId); -/** - * Insert inline attachment bbcode. - */ -$fileList.on('click', '.file-inline-bbcode', function(e) { - var attachId = $(this).parents('.attach-row').attr('data-attach-id'), - index = phpbb.plupload.getIndex(attachId); + attachInline(index, phpbb.plupload.data[index].real_filename); + e.preventDefault(); + }); - attachInline(index, phpbb.plupload.data[index].real_filename); - e.preventDefault(); -}); + /** + * Delete a file. + */ + $fileList.on('click', '.file-delete', function(e) { + const row = $(this).parents('.attach-row'); + const attachId = row.attr('data-attach-id'); -/** - * Delete a file. - */ -$fileList.on('click', '.file-delete', function(e) { - var row = $(this).parents('.attach-row'), - attachId = row.attr('data-attach-id'); + phpbb.plupload.deleteFile(row, attachId); + e.preventDefault(); + }); - phpbb.plupload.deleteFile(row, attachId); - e.preventDefault(); -}); + /** + * Display the error message for a particular file when the error icon is clicked. + */ + $fileList.on('click', '.file-error', function(e) { + phpbb.alert($(this).attr('data-error-title'), $(this).attr('data-error-message')); + e.preventDefault(); + }); -/** - * Display the error message for a particular file when the error icon is clicked. - */ -$fileList.on('click', '.file-error', function(e) { - phpbb.alert($(this).attr('data-error-title'), $(this).attr('data-error-message')); - e.preventDefault(); -}); + /** + * Fires when an error occurs. + */ + phpbb.plupload.uploader.bind('Error', (up, error) => { + error.file.name = plupload.xmlEncode(error.file.name); -/** - * Fires when an error occurs. - */ -phpbb.plupload.uploader.bind('Error', function(up, error) { - error.file.name = plupload.xmlEncode(error.file.name); + // The error message that Plupload provides for these is vague, so we'll be more specific. + if (error.code === plupload.FILE_EXTENSION_ERROR) { + error.message = plupload.translate('Invalid file extension:') + ' ' + error.file.name; + } else if (error.code === plupload.FILE_SIZE_ERROR) { + error.message = plupload.translate('File too large:') + ' ' + error.file.name; + } - // The error message that Plupload provides for these is vague, so we'll be more specific. - if (error.code === plupload.FILE_EXTENSION_ERROR) { - error.message = plupload.translate('Invalid file extension:') + ' ' + error.file.name; - } else if (error.code === plupload.FILE_SIZE_ERROR) { - error.message = plupload.translate('File too large:') + ' ' + error.file.name; - } - phpbb.alert(phpbb.plupload.lang.ERROR, error.message); -}); + phpbb.alert(phpbb.plupload.lang.ERROR, error.message); + }); -/** - * Fires before a given file is about to be uploaded. This allows us to - * send the real filename along with the chunk. This is necessary because - * for some reason the filename is set to 'blob' whenever a file is chunked - * - * @param {object} up The plupload.Uploader object - * @param {object} file The plupload.File object that is about to be uploaded - */ -phpbb.plupload.uploader.bind('BeforeUpload', function(up, file) { - if (phpbb.plupload.handleMaxFilesReached()) { - return; - } + /** + * Fires before a given file is about to be uploaded. This allows us to + * send the real filename along with the chunk. This is necessary because + * for some reason the filename is set to 'blob' whenever a file is chunked + * + * @param {object} up The plupload.Uploader object + * @param {object} file The plupload.File object that is about to be uploaded + */ + phpbb.plupload.uploader.bind('BeforeUpload', (up, file) => { + if (phpbb.plupload.handleMaxFilesReached()) { + return; + } - phpbb.plupload.updateMultipartParams({ real_filename: file.name }); -}); + phpbb.plupload.updateMultipartParams({ real_filename: file.name }); + }); -/** - * Fired when a single chunk of any given file is uploaded. This parses the - * response from the server and checks for an error. If an error occurs it - * is reported to the user and the upload of this particular file is halted - * - * @param {object} up The plupload.Uploader object - * @param {object} file The plupload.File object whose chunk has just - * been uploaded - * @param {object} response The response object from the server - */ -phpbb.plupload.uploader.bind('ChunkUploaded', function(up, file, response) { - if (response.chunk >= response.chunks - 1) { - return; - } + /** + * Fired when a single chunk of any given file is uploaded. This parses the + * response from the server and checks for an error. If an error occurs it + * is reported to the user and the upload of this particular file is halted + * + * @param {object} up The plupload.Uploader object + * @param {object} file The plupload.File object whose chunk has just + * been uploaded + * @param {object} response The response object from the server + */ + phpbb.plupload.uploader.bind('ChunkUploaded', (up, file, response) => { + if (response.chunk >= response.chunks - 1) { + return; + } - var json = {}; - try { - json = $.parseJSON(response.response); - } catch (e) { - file.status = plupload.FAILED; - up.trigger('FileUploaded', file, { - response: JSON.stringify({ - error: { - message: 'Error parsing server response.' - } - }) - }); - } + let json = {}; + try { + json = $.parseJSON(response.response); + } catch { + file.status = plupload.FAILED; + up.trigger('FileUploaded', file, { + response: JSON.stringify({ + error: { + message: 'Error parsing server response.', + }, + }), + }); + } - // If trigger_error() was called, then a permission error likely occurred. - if (typeof json.title !== 'undefined') { - json.error = { message: json.message }; - } + // If trigger_error() was called, then a permission error likely occurred. + if (typeof json.title !== 'undefined') { + json.error = { message: json.message }; + } - if (json.error) { - file.status = plupload.FAILED; - up.trigger('FileUploaded', file, { - response: JSON.stringify({ - error: { - message: json.error.message - } - }) - }); - } -}); + if (json.error) { + file.status = plupload.FAILED; + up.trigger('FileUploaded', file, { + response: JSON.stringify({ + error: { + message: json.error.message, + }, + }), + }); + } + }); -/** - * Fires when files are added to the queue. - */ -phpbb.plupload.uploader.bind('FilesAdded', function(up, files) { + /** + * Fires when files are added to the queue. + */ + phpbb.plupload.uploader.bind('FilesAdded', (up, files) => { // Prevent unnecessary requests to the server if the user already uploaded // the maximum number of files allowed. - if (phpbb.plupload.handleMaxFilesReached()) { - return; - } + if (phpbb.plupload.handleMaxFilesReached()) { + return; + } - // Switch the active tab if the style supports it - if (typeof activateSubPanel === 'function') { - activateSubPanel('attach-panel'); // jshint ignore: line - } + // Switch the active tab if the style supports it + if (typeof activateSubPanel === 'function') { + activateSubPanel('attach-panel'); // jshint ignore: line + } - // Show the file list if there aren't any files currently. - var $fileListContainer = $('#file-list-container'); - if (!$fileListContainer.is(':visible')) { - $fileListContainer.show(100); - } + // Show the file list if there aren't any files currently. + const $fileListContainer = $('#file-list-container'); + if (!$fileListContainer.is(':visible')) { + $fileListContainer.show(100); + } - $.each(files, function(i, file) { - phpbb.plupload.insertRow(file); - }); - - up.bind('UploadProgress', function(up, file) { - $('.file-progress-bar', '#' + file.id).css('width', file.percent + '%'); - $('#file-total-progress-bar').css('width', up.total.percent + '%'); - }); - - // Do not allow more files to be added to the running queue. - phpbb.plupload.disableUploader(); - - // Start uploading the files once the user has selected them. - up.start(); -}); - - -/** - * Fires when an entire file has been uploaded. It checks for errors - * returned by the server otherwise parses the list of attachment data and - * appends it to the next file upload so that the server can maintain state - * with regards to the attachments in a given post - * - * @param {object} up The plupload.Uploader object - * @param {object} file The plupload.File object that has just been - * uploaded - * @param {string} response The response string from the server - */ -phpbb.plupload.uploader.bind('FileUploaded', function(up, file, response) { - var json = {}, - row = $('#' + file.id), - error; - - // Hide the progress indicator. - row.find('.file-progress').hide(); - - try { - json = JSON.parse(response.response); - } catch (e) { - error = 'Error parsing server response.'; - } - - // If trigger_error() was called, then a permission error likely occurred. - if (typeof json.title !== 'undefined') { - error = json.message; - up.trigger('Error', { message: error }); - - // The rest of the queue will fail. - phpbb.plupload.markQueuedFailed(error); - } else if (json.error) { - error = json.error.message; - } - - if (typeof error !== 'undefined') { - phpbb.plupload.fileError(file, error); - } else if (file.status === plupload.DONE) { - file.attachment_data = json.data[0]; - - row.attr('data-attach-id', file.attachment_data.attach_id); - row.find('.file-inline-bbcode').show(); - row.find('.file-status').addClass('file-uploaded'); - phpbb.plupload.update(json.data, 'addition', 0, [json.download_url]); - } -}); - -/** - * Fires when the entire queue of files have been uploaded. - */ -phpbb.plupload.uploader.bind('UploadComplete', function() { - // Hide the progress bar - setTimeout(function() { - $('#file-total-progress-bar').fadeOut(500, function() { - $(this).css('width', 0).show(); + $.each(files, (i, file) => { + phpbb.plupload.insertRow(file); }); - }, 2000); - // Re-enable the uploader - phpbb.plupload.enableUploader(); -}); + up.bind('UploadProgress', (up, file) => { + $('.file-progress-bar', '#' + file.id).css('width', file.percent + '%'); + $('#file-total-progress-bar').css('width', up.total.percent + '%'); + }); + // Do not allow more files to be added to the running queue. + phpbb.plupload.disableUploader(); + + // Start uploading the files once the user has selected them. + up.start(); + }); + + /** + * Fires when an entire file has been uploaded. It checks for errors + * returned by the server otherwise parses the list of attachment data and + * appends it to the next file upload so that the server can maintain state + * with regards to the attachments in a given post + * + * @param {object} up The plupload.Uploader object + * @param {object} file The plupload.File object that has just been + * uploaded + * @param {string} response The response string from the server + */ + phpbb.plupload.uploader.bind('FileUploaded', (up, file, response) => { + let json = {}; + const row = $('#' + file.id); + let error; + + // Hide the progress indicator. + row.find('.file-progress').hide(); + + try { + json = JSON.parse(response.response); + } catch { + error = 'Error parsing server response.'; + } + + // If trigger_error() was called, then a permission error likely occurred. + if (typeof json.title !== 'undefined') { + error = json.message; + up.trigger('Error', { message: error }); + + // The rest of the queue will fail. + phpbb.plupload.markQueuedFailed(error); + } else if (json.error) { + error = json.error.message; + } + + if (typeof error !== 'undefined') { + phpbb.plupload.fileError(file, error); + } else if (file.status === plupload.DONE) { + file.attachment_data = json.data[0]; + + row.attr('data-attach-id', file.attachment_data.attach_id); + row.find('.file-inline-bbcode').show(); + row.find('.file-status').addClass('file-uploaded'); + phpbb.plupload.update(json.data, 'addition', 0, [ json.download_url ]); + } + }); + + /** + * Fires when the entire queue of files have been uploaded. + */ + phpbb.plupload.uploader.bind('UploadComplete', () => { + // Hide the progress bar + setTimeout(() => { + $('#file-total-progress-bar').fadeOut(500, function() { + $(this).css('width', 0).show(); + }); + }, 2000); + + // Re-enable the uploader + phpbb.plupload.enableUploader(); + }); })(jQuery); // Avoid conflicts with other libraries diff --git a/phpBB/styles/prosilver/template/ajax.js b/phpBB/styles/prosilver/template/ajax.js index 93e8fcef80..c5a0778c8e 100644 --- a/phpBB/styles/prosilver/template/ajax.js +++ b/phpBB/styles/prosilver/template/ajax.js @@ -1,427 +1,425 @@ /* global phpbb */ +/* eslint camelcase: 0 */ -(function($) { // Avoid conflicts with other libraries +(function($) { // Avoid conflicts with other libraries + 'use strict'; -'use strict'; + // This callback will mark all forum icons read + phpbb.addAjaxCallback('mark_forums_read', function(res) { + const readTitle = res.NO_UNREAD_POSTS; + const unreadTitle = res.UNREAD_POSTS; + const iconsArray = { + forum_unread: 'forum_read', + forum_unread_subforum: 'forum_read_subforum', + forum_unread_locked: 'forum_read_locked', + }; -// This callback will mark all forum icons read -phpbb.addAjaxCallback('mark_forums_read', function(res) { - var readTitle = res.NO_UNREAD_POSTS; - var unreadTitle = res.UNREAD_POSTS; - var iconsArray = { - forum_unread: 'forum_read', - forum_unread_subforum: 'forum_read_subforum', - forum_unread_locked: 'forum_read_locked' - }; + $('li.row').find('dl[class*="forum_unread"]').each(function() { + const $this = $(this); - $('li.row').find('dl[class*="forum_unread"]').each(function() { - var $this = $(this); - - $.each(iconsArray, function(unreadClass, readClass) { - if ($this.hasClass(unreadClass)) { - $this.removeClass(unreadClass).addClass(readClass); - } + $.each(iconsArray, (unreadClass, readClass) => { + if ($this.hasClass(unreadClass)) { + $this.removeClass(unreadClass).addClass(readClass); + } + }); + $this.children('dt[title="' + unreadTitle + '"]').attr('title', readTitle); }); - $this.children('dt[title="' + unreadTitle + '"]').attr('title', readTitle); - }); - // Mark subforums read - $('a.subforum[class*="unread"]').removeClass('unread').addClass('read').children('.icon.icon-red').removeClass('icon-red').addClass('icon-blue'); + // Mark subforums read + $('a.subforum[class*="unread"]').removeClass('unread').addClass('read').children('.icon.icon-red').removeClass('icon-red').addClass('icon-blue'); - // Mark topics read if we are watching a category and showing active topics - if ($('#active_topics').length) { - phpbb.ajaxCallbacks.mark_topics_read.call(this, res, false); - } + // Mark topics read if we are watching a category and showing active topics + if ($('#active_topics').length) { + phpbb.ajaxCallbacks.mark_topics_read.call(this, res, false); + } - // Update mark forums read links - $('[data-ajax="mark_forums_read"]').attr('href', res.U_MARK_FORUMS); + // Update mark forums read links + $('[data-ajax="mark_forums_read"]').attr('href', res.U_MARK_FORUMS); - phpbb.closeDarkenWrapper(3000); -}); - -/** -* This callback will mark all topic icons read -* -* @param {bool} [update_topic_links=true] Whether "Mark topics read" links -* should be updated. Defaults to true. -*/ -phpbb.addAjaxCallback('mark_topics_read', function(res, updateTopicLinks) { - var readTitle = res.NO_UNREAD_POSTS; - var unreadTitle = res.UNREAD_POSTS; - var iconsArray = { - global_unread: 'global_read', - announce_unread: 'announce_read', - sticky_unread: 'sticky_read', - topic_unread: 'topic_read' - }; - var iconsState = ['', '_hot', '_hot_mine', '_locked', '_locked_mine', '_mine']; - var unreadClassSelectors; - var classMap = {}; - var classNames = []; - - if (typeof updateTopicLinks === 'undefined') { - updateTopicLinks = true; - } - - $.each(iconsArray, function(unreadClass, readClass) { - $.each(iconsState, function(key, value) { - // Only topics can be hot - if ((value === '_hot' || value === '_hot_mine') && unreadClass !== 'topic_unread') { - return true; - } - classMap[unreadClass + value] = readClass + value; - classNames.push(unreadClass + value); - }); - }); - - unreadClassSelectors = '.' + classNames.join(',.'); - - $('li.row').find(unreadClassSelectors).each(function() { - var $this = $(this); - $.each(classMap, function(unreadClass, readClass) { - if ($this.hasClass(unreadClass)) { - $this.removeClass(unreadClass).addClass(readClass); - } - }); - $this.children('dt[title="' + unreadTitle + '"]').attr('title', readTitle); - }); - - // Remove link to first unread post - $('a.unread').has('.icon-red').remove(); - - // Update mark topics read links - if (updateTopicLinks) { - $('[data-ajax="mark_topics_read"]').attr('href', res.U_MARK_TOPICS); - } - - phpbb.closeDarkenWrapper(3000); -}); - -// This callback will mark all notifications read -phpbb.addAjaxCallback('notification.mark_all_read', function(res) { - if (typeof res.success !== 'undefined') { - phpbb.markNotifications($('[data-notification-unread="true"]'), 0); - phpbb.toggleDropdown.call($('#notification-button')); phpbb.closeDarkenWrapper(3000); - } -}); + }); -// This callback will mark a notification read -phpbb.addAjaxCallback('notification.mark_read', function(res) { - if (typeof res.success !== 'undefined') { - var unreadCount = Number($('#notification-button strong').html()) - 1; - phpbb.markNotifications($(this).parent('[data-notification-unread="true"]'), unreadCount); - } -}); + /** + * This callback will mark all topic icons read + * + * @param {bool} [update_topic_links=true] Whether "Mark topics read" links + * should be updated. Defaults to true. + */ + phpbb.addAjaxCallback('mark_topics_read', (res, updateTopicLinks) => { + const readTitle = res.NO_UNREAD_POSTS; + const unreadTitle = res.UNREAD_POSTS; + const iconsArray = { + global_unread: 'global_read', + announce_unread: 'announce_read', + sticky_unread: 'sticky_read', + topic_unread: 'topic_read', + }; + const iconsState = [ '', '_hot', '_hot_mine', '_locked', '_locked_mine', '_mine' ]; + const classMap = {}; + const classNames = []; -/** - * Mark notification popup rows as read. - * - * @param {jQuery} $popup jQuery object(s) to mark read. - * @param {int} unreadCount The new unread notifications count. - */ -phpbb.markNotifications = function($popup, unreadCount) { + if (typeof updateTopicLinks === 'undefined') { + updateTopicLinks = true; + } + + $.each(iconsArray, (unreadClass, readClass) => { + $.each(iconsState, (key, value) => { + // Only topics can be hot + if ((value === '_hot' || value === '_hot_mine') && unreadClass !== 'topic_unread') { + return true; + } + + classMap[unreadClass + value] = readClass + value; + classNames.push(unreadClass + value); + }); + }); + + const unreadClassSelectors = '.' + classNames.join(',.'); + + $('li.row').find(unreadClassSelectors).each(function() { + const $this = $(this); + $.each(classMap, (unreadClass, readClass) => { + if ($this.hasClass(unreadClass)) { + $this.removeClass(unreadClass).addClass(readClass); + } + }); + $this.children('dt[title="' + unreadTitle + '"]').attr('title', readTitle); + }); + + // Remove link to first unread post + $('a.unread').has('.icon-red').remove(); + + // Update mark topics read links + if (updateTopicLinks) { + $('[data-ajax="mark_topics_read"]').attr('href', res.U_MARK_TOPICS); + } + + phpbb.closeDarkenWrapper(3000); + }); + + // This callback will mark all notifications read + phpbb.addAjaxCallback('notification.mark_all_read', res => { + if (typeof res.success !== 'undefined') { + phpbb.markNotifications($('[data-notification-unread="true"]'), 0); + phpbb.toggleDropdown.call($('#notification-button')); + phpbb.closeDarkenWrapper(3000); + } + }); + + // This callback will mark a notification read + phpbb.addAjaxCallback('notification.mark_read', function(res) { + if (typeof res.success !== 'undefined') { + const unreadCount = Number($('#notification-button strong').html()) - 1; + phpbb.markNotifications($(this).parent('[data-notification-unread="true"]'), unreadCount); + } + }); + + /** + * Mark notification popup rows as read. + * + * @param {jQuery} $popup jQuery object(s) to mark read. + * @param {int} unreadCount The new unread notifications count. + */ + phpbb.markNotifications = function($popup, unreadCount) { // Remove the unread status. - $popup.removeClass('bg2'); - $popup.find('a.mark_read').remove(); + $popup.removeClass('bg2'); + $popup.find('a.mark_read').remove(); - // Update the notification link to the real URL. - $popup.each(function() { - var link = $(this).find('a'); - link.attr('href', link.attr('data-real-url')); + // Update the notification link to the real URL. + $popup.each(function() { + const link = $(this).find('a'); + link.attr('href', link.attr('data-real-url')); + }); + + // Update the unread count. + $('strong', '#notification-button').html(unreadCount); + // Remove the Mark all read link and hide notification count if there are no unread notifications. + if (!unreadCount) { + $('#mark_all_notifications').remove(); + $('#notification-button > strong').addClass('hidden'); + } + + // Update page title + const $title = $('title'); + const originalTitle = $title.text().replace(/(\((\d+)\))/, ''); + $title.text((unreadCount ? '(' + unreadCount + ')' : '') + originalTitle); + }; + + // This callback finds the post from the delete link, and removes it. + phpbb.addAjaxCallback('post_delete', function() { + const $this = $(this); + let postId; + + if ($this.attr('data-refresh') === undefined) { + postId = $this[0].href.split('&p=')[1]; + const post = $this.parents('#p' + postId).css('pointer-events', 'none'); + if (post.hasClass('bg1') || post.hasClass('bg2')) { + const posts1 = post.nextAll('.bg1'); + post.nextAll('.bg2').removeClass('bg2').addClass('bg1'); + posts1.removeClass('bg1').addClass('bg2'); + } + + post.fadeOut(function() { + $(this).remove(); + }); + } }); - // Update the unread count. - $('strong', '#notification-button').html(unreadCount); - // Remove the Mark all read link and hide notification count if there are no unread notifications. - if (!unreadCount) { - $('#mark_all_notifications').remove(); - $('#notification-button > strong').addClass('hidden'); - } - - // Update page title - var $title = $('title'); - var originalTitle = $title.text().replace(/(\((\d+)\))/, ''); - $title.text((unreadCount ? '(' + unreadCount + ')' : '') + originalTitle); -}; - -// This callback finds the post from the delete link, and removes it. -phpbb.addAjaxCallback('post_delete', function() { - var $this = $(this), - postId; - - if ($this.attr('data-refresh') === undefined) { - postId = $this[0].href.split('&p=')[1]; - var post = $this.parents('#p' + postId).css('pointer-events', 'none'); - if (post.hasClass('bg1') || post.hasClass('bg2')) { - var posts1 = post.nextAll('.bg1'); - post.nextAll('.bg2').removeClass('bg2').addClass('bg1'); - posts1.removeClass('bg1').addClass('bg2'); - } - post.fadeOut(function() { + // This callback removes the approve / disapprove div or link. + phpbb.addAjaxCallback('post_visibility', function(res) { + const remove = (res.visible) ? $(this) : $(this).parents('.post'); + $(remove).css('pointer-events', 'none').fadeOut(function() { $(this).remove(); }); - } -}); -// This callback removes the approve / disapprove div or link. -phpbb.addAjaxCallback('post_visibility', function(res) { - var remove = (res.visible) ? $(this) : $(this).parents('.post'); - $(remove).css('pointer-events', 'none').fadeOut(function() { - $(this).remove(); - }); - - if (res.visible) { + if (res.visible) { // Remove the "Deleted by" message from the post on restoring. - remove.parents('.post').find('.post_deleted_msg').css('pointer-events', 'none').fadeOut(function() { - $(this).remove(); - }); - } -}); - -// This removes the parent row of the link or form that fired the callback. -phpbb.addAjaxCallback('row_delete', function() { - $(this).parents('tr').remove(); -}); - -// This handles friend / foe additions removals. -phpbb.addAjaxCallback('zebra', function(res) { - var zebra; - - if (res.success) { - zebra = $('.zebra'); - zebra.first().html(res.MESSAGE_TEXT); - zebra.not(':first').html(' ').prev().html(' '); - } -}); - -/** - * This callback updates the poll results after voting. - */ -phpbb.addAjaxCallback('vote_poll', function(res) { - if (typeof res.success !== 'undefined') { - var poll = $(this).closest('.topic_poll'); - var panel = poll.find('.panel'); - var resultsVisible = poll.find('dl:first-child .resultbar').is(':visible'); - var mostVotes = 0; - - // Set min-height to prevent the page from jumping when the content changes - var updatePanelHeight = function (height) { - height = (typeof height === 'undefined') ? panel.find('.inner').outerHeight() : height; - panel.css('min-height', height); - }; - updatePanelHeight(); - - // Remove the View results link - if (!resultsVisible) { - poll.find('.poll_view_results').hide(500); - } - - if (!res.can_vote) { - poll.find('.polls, .poll_max_votes, .poll_vote, .poll_option_select').fadeOut(500, function () { - poll.find('.resultbar, .poll_option_percent, .poll_total_votes').show(); + remove.parents('.post').find('.post_deleted_msg').css('pointer-events', 'none').fadeOut(function() { + $(this).remove(); }); - } else { - // If the user can still vote, simply slide down the results - poll.find('.resultbar, .poll_option_percent, .poll_total_votes').show(500); } + }); - // Get the votes count of the highest poll option - poll.find('[data-poll-option-id]').each(function() { - var option = $(this); - var optionId = option.attr('data-poll-option-id'); - mostVotes = (res.vote_counts[optionId] >= mostVotes) ? res.vote_counts[optionId] : mostVotes; - }); + // This removes the parent row of the link or form that fired the callback. + phpbb.addAjaxCallback('row_delete', function() { + $(this).parents('tr').remove(); + }); - // Update the total votes count - poll.find('.poll_total_vote_cnt').html(res.total_votes); + // This handles friend / foe additions removals. + phpbb.addAjaxCallback('zebra', res => { + let zebra; - // Update each option - poll.find('[data-poll-option-id]').each(function() { - var $this = $(this); - var optionId = $this.attr('data-poll-option-id'); - var voted = (typeof res.user_votes[optionId] !== 'undefined'); - var mostVoted = (res.vote_counts[optionId] === mostVotes); - var percent = (!res.total_votes) ? 0 : Math.round((res.vote_counts[optionId] / res.total_votes) * 100); - var percentRel = (mostVotes === 0) ? 0 : Math.round((res.vote_counts[optionId] / mostVotes) * 100); - var altText; + if (res.success) { + zebra = $('.zebra'); + zebra.first().html(res.MESSAGE_TEXT); + zebra.not(':first').html(' ').prev().html(' '); + } + }); - altText = $this.attr('data-alt-text'); - if (voted) { - $this.attr('title', $.trim(altText)); - } else { - $this.attr('title', ''); + /** + * This callback updates the poll results after voting. + */ + phpbb.addAjaxCallback('vote_poll', function(res) { + if (typeof res.success !== 'undefined') { + const poll = $(this).closest('.topic_poll'); + const panel = poll.find('.panel'); + const resultsVisible = poll.find('dl:first-child .resultbar').is(':visible'); + let mostVotes = 0; + + // Set min-height to prevent the page from jumping when the content changes + const updatePanelHeight = function(height) { + height = (typeof height === 'undefined') ? panel.find('.inner').outerHeight() : height; + panel.css('min-height', height); }; - $this.toggleClass('voted', voted); - $this.toggleClass('most-votes', mostVoted); - // Update the bars - var bar = $this.find('.resultbar div'); - var barTimeLapse = (res.can_vote) ? 500 : 1500; - var newBarClass = (percent === 100) ? 'pollbar5' : 'pollbar' + (Math.floor(percent / 20) + 1); + updatePanelHeight(); - setTimeout(function () { - bar.animate({ width: percentRel + '%' }, 500) - .removeClass('pollbar1 pollbar2 pollbar3 pollbar4 pollbar5') - .addClass(newBarClass) - .html(res.vote_counts[optionId]); - - var percentText = percent ? percent + '%' : res.NO_VOTES; - $this.find('.poll_option_percent').html(percentText); - }, barTimeLapse); - }); - - if (!res.can_vote) { - poll.find('.polls').delay(400).fadeIn(500); - } - - // Display "Your vote has been cast." message. Disappears after 5 seconds. - var confirmationDelay = (res.can_vote) ? 300 : 900; - poll.find('.vote-submitted').delay(confirmationDelay).slideDown(200, function() { - if (resultsVisible) { - updatePanelHeight(); + // Remove the View results link + if (!resultsVisible) { + poll.find('.poll_view_results').hide(500); } - $(this).delay(5000).fadeOut(500, function() { - resizePanel(300); + if (res.can_vote) { + // If the user can still vote, simply slide down the results + poll.find('.resultbar, .poll_option_percent, .poll_total_votes').show(500); + } else { + poll.find('.polls, .poll_max_votes, .poll_vote, .poll_option_select').fadeOut(500, () => { + poll.find('.resultbar, .poll_option_percent, .poll_total_votes').show(); + }); + } + + // Get the votes count of the highest poll option + poll.find('[data-poll-option-id]').each(function() { + const option = $(this); + const optionId = option.attr('data-poll-option-id'); + mostVotes = (res.vote_counts[optionId] >= mostVotes) ? res.vote_counts[optionId] : mostVotes; }); - }); - // Remove the gap resulting from removing options - setTimeout(function() { - resizePanel(500); - }, 1500); + // Update the total votes count + poll.find('.poll_total_vote_cnt').html(res.total_votes); - var resizePanel = function (time) { - var panelHeight = panel.height(); - var innerHeight = panel.find('.inner').outerHeight(); + // Update each option + poll.find('[data-poll-option-id]').each(function() { + const $this = $(this); + const optionId = $this.attr('data-poll-option-id'); + const voted = (typeof res.user_votes[optionId] !== 'undefined'); + const mostVoted = (res.vote_counts[optionId] === mostVotes); + const percent = res.total_votes ? Math.round((res.vote_counts[optionId] / res.total_votes) * 100) : 0; + const percentRel = (mostVotes === 0) ? 0 : Math.round((res.vote_counts[optionId] / mostVotes) * 100); - if (panelHeight !== innerHeight) { - panel.css({ minHeight: '', height: panelHeight }) - .animate({ height: innerHeight }, time, function () { - panel.css({ minHeight: innerHeight, height: '' }); - }); + const altText = $this.attr('data-alt-text'); + if (voted) { + $this.attr('title', $.trim(altText)); + } else { + $this.attr('title', ''); + } + + $this.toggleClass('voted', voted); + $this.toggleClass('most-votes', mostVoted); + + // Update the bars + const bar = $this.find('.resultbar div'); + const barTimeLapse = (res.can_vote) ? 500 : 1500; + const newBarClass = (percent === 100) ? 'pollbar5' : 'pollbar' + (Math.floor(percent / 20) + 1); + + setTimeout(() => { + bar.animate({ width: percentRel + '%' }, 500) + .removeClass('pollbar1 pollbar2 pollbar3 pollbar4 pollbar5') + .addClass(newBarClass) + .html(res.vote_counts[optionId]); + + const percentText = percent ? percent + '%' : res.NO_VOTES; + $this.find('.poll_option_percent').html(percentText); + }, barTimeLapse); + }); + + if (!res.can_vote) { + poll.find('.polls').delay(400).fadeIn(500); } - }; - } -}); -/** - * Show poll results when clicking View results link. - */ -$('.poll_view_results a').click(function(e) { - // Do not follow the link - e.preventDefault(); + // Display "Your vote has been cast." message. Disappears after 5 seconds. + const confirmationDelay = (res.can_vote) ? 300 : 900; + poll.find('.vote-submitted').delay(confirmationDelay).slideDown(200, function() { + if (resultsVisible) { + updatePanelHeight(); + } - var $poll = $(this).parents('.topic_poll'); + $(this).delay(5000).fadeOut(500, () => { + resizePanel(300); + }); + }); - $poll.find('.resultbar, .poll_option_percent, .poll_total_votes').show(500); - $poll.find('.poll_view_results').hide(500); -}); + // Remove the gap resulting from removing options + setTimeout(() => { + resizePanel(500); + }, 1500); -$('[data-ajax]').each(function() { - var $this = $(this); - var ajax = $this.attr('data-ajax'); - var filter = $this.attr('data-filter'); + const resizePanel = function(time) { + const panelHeight = panel.height(); + const innerHeight = panel.find('.inner').outerHeight(); - if (ajax !== 'false') { - var fn = (ajax !== 'true') ? ajax : null; - filter = (filter !== undefined) ? phpbb.getFunctionByName(filter) : null; - - phpbb.ajaxify({ - selector: this, - refresh: $this.attr('data-refresh') !== undefined, - filter: filter, - callback: fn - }); - } -}); - -/** - * This simply appends #preview to the action of the - * QR action when you click the Full Editor & Preview button - */ -$('#qr_full_editor').click(function() { - $('#qr_postform').attr('action', function(i, val) { - return val + '#preview'; - }); -}); - - -/** - * Make the display post links to use JS - */ -$('.display_post').click(function(e) { - // Do not follow the link - e.preventDefault(); - - var postId = $(this).attr('data-post-id'); - $('#post_content' + postId).show(); - $('#profile' + postId).show(); - $('#post_hidden' + postId).hide(); -}); - -/** - * Display hidden post on post review page - */ -$('.display_post_review').on('click', function(e) { - e.preventDefault(); - - let $displayPostLink = $(this); - $displayPostLink.closest('.post-ignore').removeClass('post-ignore'); - $displayPostLink.hide(); -}); - -/** -* Toggle the member search panel in memberlist.php. -* -* If user returns to search page after viewing results the search panel is automatically displayed. -* In any case the link will toggle the display status of the search panel and link text will be -* appropriately changed based on the status of the search panel. -*/ -$('#member_search').click(function () { - var $memberlistSearch = $('#memberlist_search'); - - $memberlistSearch.slideToggle('fast'); - phpbb.ajaxCallbacks.alt_text.call(this); - - // Focus on the username textbox if it's available and displayed - if ($memberlistSearch.is(':visible')) { - $('#username').focus(); - } - return false; -}); - -/** - * Show to top button if available on page - */ -const $scrollTopButton = $('.to-top-button'); - -if ($scrollTopButton.length) { - // Show or hide the button based on scroll position - $(window).scroll(function () { - if ($(this).scrollTop() > 300) { - $scrollTopButton.fadeIn(); // Fade in the button - } else { - $scrollTopButton.fadeOut(); // Fade out the button + if (panelHeight !== innerHeight) { + panel.css({ minHeight: '', height: panelHeight }) + .animate({ height: innerHeight }, time, () => { + panel.css({ minHeight: innerHeight, height: '' }); + }); + } + }; } }); - // Scroll smoothly to the top when the button is clicked - $scrollTopButton.click(function (e) { - e.preventDefault(); // Prevent the default anchor link behavior - $('html, body').animate({scrollTop: 0}, 500); // Smooth scroll to top + /** + * Show poll results when clicking View results link. + */ + $('.poll_view_results a').click(function(e) { + // Do not follow the link + e.preventDefault(); + + const $poll = $(this).parents('.topic_poll'); + + $poll.find('.resultbar, .poll_option_percent, .poll_total_votes').show(500); + $poll.find('.poll_view_results').hide(500); }); -} -/** -* Automatically resize textarea -*/ -$(function() { - var $textarea = $('textarea:not(#message-box textarea, .no-auto-resize)'); - phpbb.resizeTextArea($textarea, { minHeight: 75, maxHeight: 250 }); - phpbb.resizeTextArea($('textarea', '#message-box')); -}); + $('[data-ajax]').each(function() { + const $this = $(this); + const ajax = $this.attr('data-ajax'); + let filter = $this.attr('data-filter'); + if (ajax !== 'false') { + const fn = ajax === 'true' ? null : ajax; + filter = filter === undefined ? null : phpbb.getFunctionByName(filter); + phpbb.ajaxify({ + selector: this, + refresh: $this.attr('data-refresh') !== undefined, + filter, + callback: fn, + }); + } + }); + + /** + * This simply appends #preview to the action of the + * QR action when you click the Full Editor & Preview button + */ + $('#qr_full_editor').click(() => { + $('#qr_postform').attr('action', (i, val) => val + '#preview'); + }); + + /** + * Make the display post links to use JS + */ + $('.display_post').click(function(e) { + // Do not follow the link + e.preventDefault(); + + const postId = $(this).attr('data-post-id'); + $('#post_content' + postId).show(); + $('#profile' + postId).show(); + $('#post_hidden' + postId).hide(); + }); + + /** + * Display hidden post on post review page + */ + $('.display_post_review').on('click', function(e) { + e.preventDefault(); + + const $displayPostLink = $(this); + $displayPostLink.closest('.post-ignore').removeClass('post-ignore'); + $displayPostLink.hide(); + }); + + /** + * Toggle the member search panel in memberlist.php. + * + * If user returns to search page after viewing results the search panel is automatically displayed. + * In any case the link will toggle the display status of the search panel and link text will be + * appropriately changed based on the status of the search panel. + */ + $('#member_search').click(function() { + const $memberlistSearch = $('#memberlist_search'); + + $memberlistSearch.slideToggle('fast'); + phpbb.ajaxCallbacks.alt_text.call(this); + + // Focus on the username textbox if it's available and displayed + if ($memberlistSearch.is(':visible')) { + $('#username').focus(); + } + + return false; + }); + + /** + * Show to top button if available on page + */ + const $scrollTopButton = $('.to-top-button'); + + if ($scrollTopButton.length) { + // Show or hide the button based on scroll position + $(window).scroll(function() { + if ($(this).scrollTop() > 300) { + $scrollTopButton.fadeIn(); // Fade in the button + } else { + $scrollTopButton.fadeOut(); // Fade out the button + } + }); + + // Scroll smoothly to the top when the button is clicked + $scrollTopButton.click(e => { + e.preventDefault(); // Prevent the default anchor link behavior + $('html, body').animate({ scrollTop: 0 }, 500); // Smooth scroll to top + }); + } + + /** + * Automatically resize textarea + */ + $(() => { + const $textarea = $('textarea:not(#message-box textarea, .no-auto-resize)'); + phpbb.resizeTextArea($textarea, { minHeight: 75, maxHeight: 250 }); + phpbb.resizeTextArea($('textarea', '#message-box')); + }); })(jQuery); // Avoid conflicts with other libraries diff --git a/phpBB/styles/prosilver/template/forum_fn.js b/phpBB/styles/prosilver/template/forum_fn.js index 7ac04a5f39..ff8acb5f00 100644 --- a/phpBB/styles/prosilver/template/forum_fn.js +++ b/phpBB/styles/prosilver/template/forum_fn.js @@ -1,4 +1,6 @@ /* global phpbb */ +/* eslint camelcase: 0 */ +/* eslint no-unused-vars: 0 */ /** * phpBB3 forum functions @@ -34,10 +36,10 @@ function popup(url, width, height, name) { function pageJump(item) { 'use strict'; - var page = parseInt(item.val(), 10), - perPage = item.attr('data-per-page'), - baseUrl = item.attr('data-base-url'), - startName = item.attr('data-start-name'); + const page = parseInt(item.val(), 10); + const perPage = item.attr('data-per-page'); + const baseUrl = item.attr('data-base-url'); + const startName = item.attr('data-start-name'); if (page !== null && !isNaN(page) && page === Math.floor(page) && page > 0) { if (baseUrl.indexOf('?') === -1) { @@ -56,7 +58,7 @@ function marklist(id, name, state) { 'use strict'; jQuery('#' + id + ' input[type=checkbox][name]').each(function() { - var $this = jQuery(this); + const $this = jQuery(this); if ($this.attr('name').substr(0, name.length) === name && !$this.prop('disabled')) { $this.prop('checked', state); } @@ -78,39 +80,38 @@ function viewableArea(e, itself) { e = e.parentNode; } - if (!e.vaHeight) { + if (e.vaHeight) { + // Restore viewable area height to the default + e.style.height = e.vaHeight + 'px'; + e.style.overflow = 'auto'; + e.style.maxHeight = e.vaMaxHeight; + e.vaHeight = false; + } else { // Store viewable area height before changing style to auto e.vaHeight = e.offsetHeight; e.vaMaxHeight = e.style.maxHeight; e.style.height = 'auto'; e.style.maxHeight = 'none'; e.style.overflow = 'visible'; - } else { - // Restore viewable area height to the default - e.style.height = e.vaHeight + 'px'; - e.style.overflow = 'auto'; - e.style.maxHeight = e.vaMaxHeight; - e.vaHeight = false; } } /** * Alternate display of subPanels */ -jQuery(function($) { +jQuery($ => { 'use strict'; $('.sub-panels').each(function() { - - var $childNodes = $('a[data-subpanel]', this), - panels = $childNodes.map(function () { - return this.getAttribute('data-subpanel'); - }), - showPanel = this.getAttribute('data-show-panel'); + const $childNodes = $('a[data-subpanel]', this); + const panels = $childNodes.map(function() { + return this.getAttribute('data-subpanel'); + }); + const showPanel = this.getAttribute('data-show-panel'); if (panels.length) { activateSubPanel(showPanel, panels); - $childNodes.click(function () { + $childNodes.click(function() { activateSubPanel(this.getAttribute('data-subpanel'), panels); return false; }); @@ -124,11 +125,13 @@ jQuery(function($) { function activateSubPanel(p, panels) { 'use strict'; - var i, showPanel; + let i; + let showPanel; if (typeof p === 'string') { showPanel = p; } + $('input[name="show_panel"]').val(showPanel); if (typeof panels === 'undefined') { @@ -147,15 +150,16 @@ function selectCode(a) { 'use strict'; // Get ID of code block - var e = a.parentNode.parentNode.getElementsByTagName('CODE')[0]; - var s, r; + const e = a.parentNode.parentNode.getElementsByTagName('CODE')[0]; + let s; + let r; // Not IE and IE9+ if (window.getSelection) { s = window.getSelection(); // Safari and Chrome if (s.setBaseAndExtent) { - var l = (e.innerText.length > 1) ? e.innerText.length - 1 : 1; + const l = (e.innerText.length > 1) ? e.innerText.length - 1 : 1; try { s.setBaseAndExtent(e, 0, e, l); } catch (error) { @@ -164,12 +168,11 @@ function selectCode(a) { s.removeAllRanges(); s.addRange(r); } - } - // Firefox and Opera - else { + } else { + // Firefox and Opera // workaround for bug # 42885 if (window.opera && e.innerHTML.substring(e.innerHTML.length - 4) === '
') { - e.innerHTML = e.innerHTML + ' '; + e.innerHTML += ' '; } r = document.createRange(); @@ -177,25 +180,23 @@ function selectCode(a) { s.removeAllRanges(); s.addRange(r); } - } - // Some older browsers - else if (document.getSelection) { + } else if (document.getSelection) { + // Some older browsers s = document.getSelection(); r = document.createRange(); r.selectNodeContents(e); s.removeAllRanges(); s.addRange(r); - } - // IE - else if (document.selection) { + } else if (document.selection) { + // IE r = document.body.createTextRange(); r.moveToElementText(e); r.select(); } } -var inAutocomplete = false; -var lastKeyEntered = ''; +let inAutocomplete = false; +let lastKeyEntered = ''; /** * Check event key @@ -229,11 +230,11 @@ function phpbbCheckKey(event) { /** * Apply onkeypress event for forcing default submit button on ENTER key press */ -jQuery(function($) { +jQuery($ => { 'use strict'; - $('form input[type=text], form input[type=password]').on('keypress', function (e) { - var defaultButton = $(this).parents('form').find('input[type=submit].default-submit-action'); + $('form input[type=text], form input[type=password]').on('keypress', function(e) { + const defaultButton = $(this).parents('form').find('input[type=submit].default-submit-action'); if (!defaultButton || defaultButton.length <= 0) { return true; @@ -258,10 +259,10 @@ jQuery(function($) { function insertUser(formId, value) { 'use strict'; - var $form = jQuery(formId), - formName = $form.attr('data-form-name'), - fieldName = $form.attr('data-field-name'), - item = opener.document.forms[formName][fieldName]; + const $form = jQuery(formId); + const formName = $form.attr('data-form-name'); + const fieldName = $form.attr('data-field-name'); + const item = opener.document.forms[formName][fieldName]; if (item.value.length && item.type === 'textarea') { value = item.value + '\n' + value; @@ -293,9 +294,9 @@ function insert_single_user(formId, user) { function parseDocument($container) { 'use strict'; - var test = document.createElement('div'), - oldBrowser = (typeof test.style.borderRadius === 'undefined'), - $body = $('body'); + const test = document.createElement('div'); + const oldBrowser = (typeof test.style.borderRadius === 'undefined'); + const $body = $('body'); /** * Reset avatar dimensions when changing URL or EMAIL @@ -308,7 +309,7 @@ function parseDocument($container) { * Pagination */ $container.find('.pagination .page-jump-form :button').click(function() { - var $input = $(this).siblings('input.inputbox'); + const $input = $(this).siblings('input.inputbox'); pageJump($input); }); @@ -320,9 +321,9 @@ function parseDocument($container) { }); $container.find('.pagination .dropdown-trigger').click(function() { - var $dropdownContainer = $(this).parent(); + const $dropdownContainer = $(this).parent(); // Wait a little bit to make sure the dropdown has activated - setTimeout(function() { + setTimeout(() => { if ($dropdownContainer.hasClass('dropdown-visible')) { $dropdownContainer.find('input.inputbox').focus(); } @@ -333,28 +334,27 @@ function parseDocument($container) { * Resize navigation (breadcrumbs) block to keep all links on same line */ $container.find('.navlinks').each(function() { - var $this = $(this), - $left = $this.children().not('.rightside'), - $right = $this.children('.rightside'); + const $this = $(this); + const $left = $this.children().not('.rightside'); + const $right = $this.children('.rightside'); if ($left.length !== 1 || !$right.length) { return; } function resize() { - var width = 0, - diff = $left.outerWidth(true) - $left.width(), - minWidth = Math.max($this.width() / 3, 240), - maxWidth; + let width = 0; + const diff = $left.outerWidth(true) - $left.width(); + const minWidth = Math.max($this.width() / 3, 240); $right.each(function() { - var $this = $(this); + const $this = $(this); if ($this.is(':visible')) { width += $this.outerWidth(true); } }); - maxWidth = $this.width() - width - diff; + const maxWidth = $this.width() - width - diff; $left.css('max-width', Math.floor(Math.max(maxWidth, minWidth)) + 'px'); } @@ -366,25 +366,25 @@ function parseDocument($container) { * Makes breadcrumbs responsive */ $container.find('.breadcrumbs:not([data-skip-responsive])').each(function() { - var $this = $(this), - $links = $this.find('.crumb'), - length = $links.length, - classes = ['wrapped-max', 'wrapped-wide', 'wrapped-medium', 'wrapped-small', 'wrapped-tiny'], - classesLength = classes.length, - maxHeight = 0, - lastWidth = false, - wrapped = false; + const $this = $(this); + const $links = $this.find('.crumb'); + const { length } = $links; + const classes = [ 'wrapped-max', 'wrapped-wide', 'wrapped-medium', 'wrapped-small', 'wrapped-tiny' ]; + const classesLength = classes.length; + let maxHeight = 0; + let lastWidth = false; + let wrapped = false; // Set tooltips $this.find('a').each(function() { - var $link = $(this); + const $link = $(this); $link.attr('title', $link.text()); }); // Function that checks breadcrumbs function check() { - var height = $this.height(), - width; + const height = $this.height(); + let width; // Test max-width set in code for .navlinks above width = parseInt($this.css('max-width'), 10); @@ -404,6 +404,7 @@ function parseDocument($container) { return; } } + lastWidth = width; if (wrapped) { @@ -419,8 +420,8 @@ function parseDocument($container) { return; } - for (var i = 0; i < classesLength; i++) { - for (var j = length - 1; j >= 0; j--) { + for (let i = 0; i < classesLength; i++) { + for (let j = length - 1; j >= 0; j--) { $links.eq(j).addClass('wrapped ' + classes[i]); if ($this.height() <= maxHeight) { return; @@ -454,23 +455,23 @@ function parseDocument($container) { * responsive-show-all to list of classes */ $container.find('.topiclist.responsive-show-all > li > dl').each(function() { - var $this = $(this), - $block = $this.find('dt .responsive-show:last-child'), - first = true; + const $this = $(this); + let $block = $this.find('dt .responsive-show:last-child'); + let first = true; // Create block that is visible only on mobile devices - if (!$block.length) { + if ($block.length) { + first = ($.trim($block.text()).length === 0); + } else { $this.find('dt > .list-inner').append('