Verzeichnisstruktur phpBB-3.3.15
- Veröffentlicht
- 28.08.2024
So funktioniert es
|
|
Auf das letzte Element klicken. Dies geht jeweils ein Schritt zurück |
Auf das Icon klicken, dies öffnet das Verzeichnis. Nochmal klicken schließt das Verzeichnis. |
|
|
(Beispiel Datei-Icons)
|
Auf das Icon klicken um den Quellcode anzuzeigen |
core.js
0001 /* global bbfontstyle */
0002
0003 var phpbb = {};
0004 phpbb.alertTime = 100;
0005
0006 (function($) { // Avoid conflicts with other libraries
0007
0008 'use strict';
0009
0010 // define a couple constants for keydown functions.
0011 var keymap = {
0012 TAB: 9,
0013 ENTER: 13,
0014 ESC: 27,
0015 ARROW_UP: 38,
0016 ARROW_DOWN: 40
0017 };
0018
0019 var $dark = $('#darkenwrapper');
0020 var $loadingIndicator;
0021 var phpbbAlertTimer = null;
0022
0023 phpbb.isTouch = (window && typeof window.ontouchstart !== 'undefined');
0024
0025 // Add ajax pre-filter to prevent cross-domain script execution
0026 $.ajaxPrefilter(function(s) {
0027 if (s.crossDomain) {
0028 s.contents.script = false;
0029 }
0030 });
0031
0032 /**
0033 * Display a loading screen
0034 *
0035 * @returns {object} Returns loadingIndicator.
0036 */
0037 phpbb.loadingIndicator = function() {
0038 if (!$loadingIndicator) {
0039 $loadingIndicator = $('<div />', {
0040 'id': 'loading_indicator',
0041 'class': 'loading_indicator'
0042 });
0043 $loadingIndicator.appendTo('#page-footer');
0044 }
0045
0046 if (!$loadingIndicator.is(':visible')) {
0047 $loadingIndicator.fadeIn(phpbb.alertTime);
0048 // Wait 60 seconds and display an error if nothing has been returned by then.
0049 phpbb.clearLoadingTimeout();
0050 phpbbAlertTimer = setTimeout(function() {
0051 phpbb.showTimeoutMessage();
0052 }, 60000);
0053 }
0054
0055 return $loadingIndicator;
0056 };
0057
0058 /**
0059 * Show timeout message
0060 */
0061 phpbb.showTimeoutMessage = function () {
0062 var $alert = $('#phpbb_alert');
0063
0064 if ($loadingIndicator.is(':visible')) {
0065 phpbb.alert($alert.attr('data-l-err'), $alert.attr('data-l-timeout-processing-req'));
0066 }
0067 };
0068
0069 /**
0070 * Clear loading alert timeout
0071 */
0072 phpbb.clearLoadingTimeout = function() {
0073 if (phpbbAlertTimer !== null) {
0074 clearTimeout(phpbbAlertTimer);
0075 phpbbAlertTimer = null;
0076 }
0077 };
0078
0079
0080 /**
0081 * Close popup alert after a specified delay
0082 *
0083 * @param {int} delay Delay in ms until darkenwrapper's click event is triggered
0084 */
0085 phpbb.closeDarkenWrapper = function(delay) {
0086 phpbbAlertTimer = setTimeout(function() {
0087 $('#darkenwrapper').trigger('click');
0088 }, delay);
0089 };
0090
0091 /**
0092 * Display a simple alert similar to JSs native alert().
0093 *
0094 * You can only call one alert or confirm box at any one time.
0095 *
0096 * @param {string} title Title of the message, eg "Information" (HTML).
0097 * @param {string} msg Message to display (HTML).
0098 *
0099 * @returns {object} Returns the div created.
0100 */
0101 phpbb.alert = function(title, msg) {
0102 var $alert = $('#phpbb_alert');
0103 $alert.find('.alert_title').html(title);
0104 $alert.find('.alert_text').html(msg);
0105
0106 $(document).on('keydown.phpbb.alert', function(e) {
0107 if (e.keyCode === keymap.ENTER || e.keyCode === keymap.ESC) {
0108 phpbb.alert.close($alert, true);
0109 e.preventDefault();
0110 e.stopPropagation();
0111 }
0112 });
0113 phpbb.alert.open($alert);
0114
0115 return $alert;
0116 };
0117
0118 /**
0119 * Handler for opening an alert box.
0120 *
0121 * @param {jQuery} $alert jQuery object.
0122 */
0123 phpbb.alert.open = function($alert) {
0124 if (!$dark.is(':visible')) {
0125 $dark.fadeIn(phpbb.alertTime);
0126 }
0127
0128 if ($loadingIndicator && $loadingIndicator.is(':visible')) {
0129 $loadingIndicator.fadeOut(phpbb.alertTime, function() {
0130 $dark.append($alert);
0131 $alert.fadeIn(phpbb.alertTime);
0132 });
0133 } else if ($dark.is(':visible')) {
0134 $dark.append($alert);
0135 $alert.fadeIn(phpbb.alertTime);
0136 } else {
0137 $dark.append($alert);
0138 $alert.show();
0139 $dark.fadeIn(phpbb.alertTime);
0140 }
0141
0142 $alert.on('click', function(e) {
0143 e.stopPropagation();
0144 });
0145
0146 $dark.one('click', function(e) {
0147 phpbb.alert.close($alert, true);
0148 e.preventDefault();
0149 e.stopPropagation();
0150 });
0151
0152 $alert.find('.alert_close').one('click', function(e) {
0153 phpbb.alert.close($alert, true);
0154 e.preventDefault();
0155 });
0156 };
0157
0158 /**
0159 * Handler for closing an alert box.
0160 *
0161 * @param {jQuery} $alert jQuery object.
0162 * @param {bool} fadedark Whether to remove dark background.
0163 */
0164 phpbb.alert.close = function($alert, fadedark) {
0165 var $fade = (fadedark) ? $dark : $alert;
0166
0167 $fade.fadeOut(phpbb.alertTime, function() {
0168 $alert.hide();
0169 });
0170
0171 $alert.find('.alert_close').off('click');
0172 $(document).off('keydown.phpbb.alert');
0173 };
0174
0175 /**
0176 * Display a simple yes / no box to the user.
0177 *
0178 * You can only call one alert or confirm box at any one time.
0179 *
0180 * @param {string} msg Message to display (HTML).
0181 * @param {function} callback Callback. Bool param, whether the user pressed
0182 * yes or no (or whatever their language is).
0183 * @param {bool} fadedark Remove the dark background when done? Defaults
0184 * to yes.
0185 *
0186 * @returns {object} Returns the div created.
0187 */
0188 phpbb.confirm = function(msg, callback, fadedark) {
0189 var $confirmDiv = $('#phpbb_confirm');
0190 $confirmDiv.find('.alert_text').html(msg);
0191 fadedark = typeof fadedark !== 'undefined' ? fadedark : true;
0192
0193 $(document).on('keydown.phpbb.alert', function(e) {
0194 if (e.keyCode === keymap.ENTER || e.keyCode === keymap.ESC) {
0195 var name = (e.keyCode === keymap.ENTER) ? 'confirm' : 'cancel';
0196
0197 $('input[name="' + name + '"]').trigger('click');
0198 e.preventDefault();
0199 e.stopPropagation();
0200 }
0201 });
0202
0203 $confirmDiv.find('input[type="button"]').one('click.phpbb.confirmbox', function(e) {
0204 var confirmed = this.name === 'confirm';
0205
0206 callback(confirmed);
0207 $confirmDiv.find('input[type="button"]').off('click.phpbb.confirmbox');
0208 phpbb.alert.close($confirmDiv, fadedark || !confirmed);
0209
0210 e.preventDefault();
0211 e.stopPropagation();
0212 });
0213
0214 phpbb.alert.open($confirmDiv);
0215
0216 return $confirmDiv;
0217 };
0218
0219 /**
0220 * Turn a querystring into an array.
0221 *
0222 * @argument {string} string The querystring to parse.
0223 * @returns {object} The object created.
0224 */
0225 phpbb.parseQuerystring = function(string) {
0226 var params = {}, i, split;
0227
0228 string = string.split('&');
0229 for (i = 0; i < string.length; i++) {
0230 split = string[i].split('=');
0231 params[split[0]] = decodeURIComponent(split[1]);
0232 }
0233 return params;
0234 };
0235
0236
0237 /**
0238 * Makes a link use AJAX instead of loading an entire page.
0239 *
0240 * This function will work for links (both standard links and links which
0241 * invoke confirm_box) and forms. It will be called automatically for links
0242 * and forms with the data-ajax attribute set, and will call the necessary
0243 * callback.
0244 *
0245 * For more info, view the following page on the phpBB wiki:
0246 * http://wiki.phpbb.com/JavaScript_Function.phpbb.ajaxify
0247 *
0248 * @param {object} options Options.
0249 */
0250 phpbb.ajaxify = function(options) {
0251 var $elements = $(options.selector),
0252 refresh = options.refresh,
0253 callback = options.callback,
0254 overlay = (typeof options.overlay !== 'undefined') ? options.overlay : true,
0255 isForm = $elements.is('form'),
0256 isText = $elements.is('input[type="text"], textarea'),
0257 eventName;
0258
0259 if (isForm) {
0260 eventName = 'submit';
0261 } else if (isText) {
0262 eventName = 'keyup';
0263 } else {
0264 eventName = 'click';
0265 }
0266
0267 $elements.on(eventName, function(event) {
0268 var action, method, data, submit, that = this, $this = $(this);
0269
0270 if ($this.find('input[type="submit"][data-clicked]').attr('data-ajax') === 'false') {
0271 return;
0272 }
0273
0274 /**
0275 * Handler for AJAX errors
0276 */
0277 function errorHandler(jqXHR, textStatus, errorThrown) {
0278 if (typeof console !== 'undefined' && console.log) {
0279 console.log('AJAX error. status: ' + textStatus + ', message: ' + errorThrown);
0280 }
0281 phpbb.clearLoadingTimeout();
0282 var responseText, errorText = false;
0283 try {
0284 responseText = JSON.parse(jqXHR.responseText);
0285 responseText = responseText.message;
0286 } catch (e) {}
0287 if (typeof responseText === 'string' && responseText.length > 0) {
0288 errorText = responseText;
0289 } else if (typeof errorThrown === 'string' && errorThrown.length > 0) {
0290 errorText = errorThrown;
0291 } else {
0292 errorText = $dark.attr('data-ajax-error-text-' + textStatus);
0293 if (typeof errorText !== 'string' || !errorText.length) {
0294 errorText = $dark.attr('data-ajax-error-text');
0295 }
0296 }
0297 phpbb.alert($dark.attr('data-ajax-error-title'), errorText);
0298 }
0299
0300 /**
0301 * This is a private function used to handle the callbacks, refreshes
0302 * and alert. It calls the callback, refreshes the page if necessary, and
0303 * displays an alert to the user and removes it after an amount of time.
0304 *
0305 * It cannot be called from outside this function, and is purely here to
0306 * avoid repetition of code.
0307 *
0308 * @param {object} res The object sent back by the server.
0309 */
0310 function returnHandler(res) {
0311 var alert;
0312
0313 phpbb.clearLoadingTimeout();
0314
0315 // Is a confirmation required?
0316 if (typeof res.S_CONFIRM_ACTION === 'undefined') {
0317 // If a confirmation is not required, display an alert and call the
0318 // callbacks.
0319 if (typeof res.MESSAGE_TITLE !== 'undefined') {
0320 alert = phpbb.alert(res.MESSAGE_TITLE, res.MESSAGE_TEXT);
0321 } else {
0322 $dark.fadeOut(phpbb.alertTime);
0323
0324 if ($loadingIndicator) {
0325 $loadingIndicator.fadeOut(phpbb.alertTime);
0326 }
0327 }
0328
0329 if (typeof phpbb.ajaxCallbacks[callback] === 'function') {
0330 phpbb.ajaxCallbacks[callback].call(that, res);
0331 }
0332
0333 // If the server says to refresh the page, check whether the page should
0334 // be refreshed and refresh page after specified time if required.
0335 if (res.REFRESH_DATA) {
0336 if (typeof refresh === 'function') {
0337 refresh = refresh(res.REFRESH_DATA.url);
0338 } else if (typeof refresh !== 'boolean') {
0339 refresh = false;
0340 }
0341
0342 phpbbAlertTimer = setTimeout(function() {
0343 if (refresh) {
0344 window.location = res.REFRESH_DATA.url;
0345 }
0346
0347 // Hide the alert even if we refresh the page, in case the user
0348 // presses the back button.
0349 $dark.fadeOut(phpbb.alertTime, function() {
0350 if (typeof alert !== 'undefined') {
0351 alert.hide();
0352 }
0353 });
0354 }, res.REFRESH_DATA.time * 1000); // Server specifies time in seconds
0355 }
0356 } else {
0357 // If confirmation is required, display a dialog to the user.
0358 phpbb.confirm(res.MESSAGE_BODY, function(del) {
0359 if (!del) {
0360 return;
0361 }
0362
0363 phpbb.loadingIndicator();
0364 data = $('<form>' + res.S_HIDDEN_FIELDS + '</form>').serialize();
0365 $.ajax({
0366 url: res.S_CONFIRM_ACTION,
0367 type: 'POST',
0368 data: data + '&confirm=' + res.YES_VALUE + '&' + $('form', '#phpbb_confirm').serialize(),
0369 success: returnHandler,
0370 error: errorHandler
0371 });
0372 }, false);
0373 }
0374 }
0375
0376 // If the element is a form, POST must be used and some extra data must
0377 // be taken from the form.
0378 var runFilter = (typeof options.filter === 'function');
0379 data = {};
0380
0381 if (isForm) {
0382 action = $this.attr('action').replace('&', '&');
0383 data = $this.serializeArray();
0384 method = $this.attr('method') || 'GET';
0385
0386 if ($this.find('input[type="submit"][data-clicked]')) {
0387 submit = $this.find('input[type="submit"][data-clicked]');
0388 data.push({
0389 name: submit.attr('name'),
0390 value: submit.val()
0391 });
0392 }
0393 } else if (isText) {
0394 var name = $this.attr('data-name') || this.name;
0395 action = $this.attr('data-url').replace('&', '&');
0396 data[name] = this.value;
0397 method = 'POST';
0398 } else {
0399 action = this.href;
0400 data = null;
0401 method = 'GET';
0402 }
0403
0404 var sendRequest = function() {
0405 var dataOverlay = $this.attr('data-overlay');
0406 if (overlay && (typeof dataOverlay === 'undefined' || dataOverlay === 'true')) {
0407 phpbb.loadingIndicator();
0408 }
0409
0410 var request = $.ajax({
0411 url: action,
0412 type: method,
0413 data: data,
0414 success: returnHandler,
0415 error: errorHandler,
0416 cache: false
0417 });
0418
0419 request.always(function() {
0420 if ($loadingIndicator && $loadingIndicator.is(':visible')) {
0421 $loadingIndicator.fadeOut(phpbb.alertTime);
0422 }
0423 });
0424 };
0425
0426 // If filter function returns false, cancel the AJAX functionality,
0427 // and return true (meaning that the HTTP request will be sent normally).
0428 if (runFilter && !options.filter.call(this, data, event, sendRequest)) {
0429 return;
0430 }
0431
0432 sendRequest();
0433 event.preventDefault();
0434 });
0435
0436 if (isForm) {
0437 $elements.find('input:submit').click(function () {
0438 var $this = $(this);
0439
0440 // Remove data-clicked attribute from any submit button of form
0441 $this.parents('form:first').find('input:submit[data-clicked]').removeAttr('data-clicked');
0442
0443 $this.attr('data-clicked', 'true');
0444 });
0445 }
0446
0447 return this;
0448 };
0449
0450 phpbb.search = {
0451 cache: {
0452 data: []
0453 },
0454 tpl: [],
0455 container: []
0456 };
0457
0458 /**
0459 * Get cached search data.
0460 *
0461 * @param {string} id Search ID.
0462 * @returns {bool|object} Cached data object. Returns false if no data exists.
0463 */
0464 phpbb.search.cache.get = function(id) {
0465 if (this.data[id]) {
0466 return this.data[id];
0467 }
0468 return false;
0469 };
0470
0471 /**
0472 * Set search cache data value.
0473 *
0474 * @param {string} id Search ID.
0475 * @param {string} key Data key.
0476 * @param {string} value Data value.
0477 */
0478 phpbb.search.cache.set = function(id, key, value) {
0479 if (!this.data[id]) {
0480 this.data[id] = { results: [] };
0481 }
0482 this.data[id][key] = value;
0483 };
0484
0485 /**
0486 * Cache search result.
0487 *
0488 * @param {string} id Search ID.
0489 * @param {string} keyword Keyword.
0490 * @param {Array} results Search results.
0491 */
0492 phpbb.search.cache.setResults = function(id, keyword, results) {
0493 this.data[id].results[keyword] = results;
0494 };
0495
0496 /**
0497 * Trim spaces from keyword and lower its case.
0498 *
0499 * @param {string} keyword Search keyword to clean.
0500 * @returns {string} Cleaned string.
0501 */
0502 phpbb.search.cleanKeyword = function(keyword) {
0503 return $.trim(keyword).toLowerCase();
0504 };
0505
0506 /**
0507 * Get clean version of search keyword. If textarea supports several keywords
0508 * (one per line), it fetches the current keyword based on the caret position.
0509 *
0510 * @param {jQuery} $input Search input|textarea.
0511 * @param {string} keyword Input|textarea value.
0512 * @param {bool} multiline Whether textarea supports multiple search keywords.
0513 *
0514 * @returns string Clean string.
0515 */
0516 phpbb.search.getKeyword = function($input, keyword, multiline) {
0517 if (multiline) {
0518 var line = phpbb.search.getKeywordLine($input);
0519 keyword = keyword.split('\n').splice(line, 1);
0520 }
0521 return phpbb.search.cleanKeyword(keyword);
0522 };
0523
0524 /**
0525 * Get the textarea line number on which the keyword resides - for textareas
0526 * that support multiple keywords (one per line).
0527 *
0528 * @param {jQuery} $textarea Search textarea.
0529 * @returns {int} The line number.
0530 */
0531 phpbb.search.getKeywordLine = function ($textarea) {
0532 var selectionStart = $textarea.get(0).selectionStart;
0533 return $textarea.val().substr(0, selectionStart).split('\n').length - 1;
0534 };
0535
0536 /**
0537 * Set the value on the input|textarea. If textarea supports multiple
0538 * keywords, only the active keyword is replaced.
0539 *
0540 * @param {jQuery} $input Search input|textarea.
0541 * @param {string} value Value to set.
0542 * @param {bool} multiline Whether textarea supports multiple search keywords.
0543 */
0544 phpbb.search.setValue = function($input, value, multiline) {
0545 if (multiline) {
0546 var line = phpbb.search.getKeywordLine($input),
0547 lines = $input.val().split('\n');
0548 lines[line] = value;
0549 value = lines.join('\n');
0550 }
0551 $input.val(value);
0552 };
0553
0554 /**
0555 * Sets the onclick event to set the value on the input|textarea to the
0556 * selected search result.
0557 *
0558 * @param {jQuery} $input Search input|textarea.
0559 * @param {object} value Result object.
0560 * @param {jQuery} $row Result element.
0561 * @param {jQuery} $container jQuery object for the search container.
0562 */
0563 phpbb.search.setValueOnClick = function($input, value, $row, $container) {
0564 $row.click(function() {
0565 phpbb.search.setValue($input, value.result, $input.attr('data-multiline'));
0566 phpbb.search.closeResults($input, $container);
0567 });
0568 };
0569
0570 /**
0571 * Runs before the AJAX search request is sent and determines whether
0572 * there is a need to contact the server. If there are cached results
0573 * already, those are displayed instead. Executes the AJAX request function
0574 * itself due to the need to use a timeout to limit the number of requests.
0575 *
0576 * @param {Array} data Data to be sent to the server.
0577 * @param {object} event Onkeyup event object.
0578 * @param {function} sendRequest Function to execute AJAX request.
0579 *
0580 * @returns {boolean} Returns false.
0581 */
0582 phpbb.search.filter = function(data, event, sendRequest) {
0583 var $this = $(this),
0584 dataName = ($this.attr('data-name') !== undefined) ? $this.attr('data-name') : $this.attr('name'),
0585 minLength = parseInt($this.attr('data-min-length'), 10),
0586 searchID = $this.attr('data-results'),
0587 keyword = phpbb.search.getKeyword($this, data[dataName], $this.attr('data-multiline')),
0588 cache = phpbb.search.cache.get(searchID),
0589 key = event.keyCode || event.which,
0590 proceed = true;
0591 data[dataName] = keyword;
0592
0593 // No need to search if enter was pressed
0594 // for selecting a value from the results.
0595 if (key === keymap.ENTER) {
0596 return false;
0597 }
0598
0599 if (cache.timeout) {
0600 clearTimeout(cache.timeout);
0601 }
0602
0603 var timeout = setTimeout(function() {
0604 // Check min length and existence of cache.
0605 if (minLength > keyword.length) {
0606 proceed = false;
0607 } else if (cache.lastSearch) {
0608 // Has the keyword actually changed?
0609 if (cache.lastSearch === keyword) {
0610 proceed = false;
0611 } else {
0612 // Do we already have results for this?
0613 if (cache.results[keyword]) {
0614 var response = {
0615 keyword: keyword,
0616 results: cache.results[keyword]
0617 };
0618 phpbb.search.handleResponse(response, $this, true);
0619 proceed = false;
0620 }
0621
0622 // If the previous search didn't yield results and the string only had characters added to it,
0623 // then we won't bother sending a request.
0624 if (keyword.indexOf(cache.lastSearch) === 0 && cache.results[cache.lastSearch].length === 0) {
0625 phpbb.search.cache.set(searchID, 'lastSearch', keyword);
0626 phpbb.search.cache.setResults(searchID, keyword, []);
0627 proceed = false;
0628 }
0629 }
0630 }
0631
0632 if (proceed) {
0633 sendRequest.call(this);
0634 }
0635 }, 350);
0636 phpbb.search.cache.set(searchID, 'timeout', timeout);
0637
0638 return false;
0639 };
0640
0641 /**
0642 * Handle search result response.
0643 *
0644 * @param {object} res Data received from server.
0645 * @param {jQuery} $input Search input|textarea.
0646 * @param {bool} fromCache Whether the results are from the cache.
0647 * @param {function} callback Optional callback to run when assigning each search result.
0648 */
0649 phpbb.search.handleResponse = function(res, $input, fromCache, callback) {
0650 if (typeof res !== 'object') {
0651 return;
0652 }
0653
0654 var searchID = $input.attr('data-results'),
0655 $container = $(searchID);
0656
0657 if (this.cache.get(searchID).callback) {
0658 callback = this.cache.get(searchID).callback;
0659 } else if (typeof callback === 'function') {
0660 this.cache.set(searchID, 'callback', callback);
0661 }
0662
0663 if (!fromCache) {
0664 this.cache.setResults(searchID, res.keyword, res.results);
0665 }
0666
0667 this.cache.set(searchID, 'lastSearch', res.keyword);
0668 this.showResults(res.results, $input, $container, callback);
0669 };
0670
0671 /**
0672 * Show search results.
0673 *
0674 * @param {Array} results Search results.
0675 * @param {jQuery} $input Search input|textarea.
0676 * @param {jQuery} $container Search results container element.
0677 * @param {function} callback Optional callback to run when assigning each search result.
0678 */
0679 phpbb.search.showResults = function(results, $input, $container, callback) {
0680 var $resultContainer = $('.search-results', $container);
0681 this.clearResults($resultContainer);
0682
0683 if (!results.length) {
0684 $container.hide();
0685 return;
0686 }
0687
0688 var searchID = $container.attr('id'),
0689 tpl,
0690 row;
0691
0692 if (!this.tpl[searchID]) {
0693 tpl = $('.search-result-tpl', $container);
0694 this.tpl[searchID] = tpl.clone().removeClass('search-result-tpl');
0695 tpl.remove();
0696 }
0697 tpl = this.tpl[searchID];
0698
0699 $.each(results, function(i, item) {
0700 row = tpl.clone();
0701 row.find('.search-result').html(item.display);
0702
0703 if (typeof callback === 'function') {
0704 callback.call(this, $input, item, row, $container);
0705 }
0706 row.appendTo($resultContainer).show();
0707 });
0708 $container.show();
0709
0710 phpbb.search.navigateResults($input, $container, $resultContainer);
0711 };
0712
0713 /**
0714 * Clear search results.
0715 *
0716 * @param {jQuery} $container Search results container.
0717 */
0718 phpbb.search.clearResults = function($container) {
0719 $container.children(':not(.search-result-tpl)').remove();
0720 };
0721
0722 /**
0723 * Close search results.
0724 *
0725 * @param {jQuery} $input Search input|textarea.
0726 * @param {jQuery} $container Search results container.
0727 */
0728 phpbb.search.closeResults = function($input, $container) {
0729 $input.off('.phpbb.search');
0730 $container.hide();
0731 };
0732
0733 /**
0734 * Navigate search results.
0735 *
0736 * @param {jQuery} $input Search input|textarea.
0737 * @param {jQuery} $container Search results container.
0738 * @param {jQuery} $resultContainer Search results list container.
0739 */
0740 phpbb.search.navigateResults = function($input, $container, $resultContainer) {
0741 // Add a namespace to the event (.phpbb.search),
0742 // so it can be unbound specifically later on.
0743 // Rebind it, to ensure the event is 'dynamic'.
0744 $input.off('.phpbb.search');
0745 $input.on('keydown.phpbb.search', function(event) {
0746 var key = event.keyCode || event.which,
0747 $active = $resultContainer.children('.active');
0748
0749 switch (key) {
0750 // Close the results
0751 case keymap.ESC:
0752 phpbb.search.closeResults($input, $container);
0753 break;
0754
0755 // Set the value for the selected result
0756 case keymap.ENTER:
0757 if ($active.length) {
0758 var value = $active.find('.search-result > span').text();
0759
0760 phpbb.search.setValue($input, value, $input.attr('data-multiline'));
0761 }
0762
0763 phpbb.search.closeResults($input, $container);
0764
0765 // Do not submit the form
0766 event.preventDefault();
0767 break;
0768
0769 // Navigate the results
0770 case keymap.ARROW_DOWN:
0771 case keymap.ARROW_UP:
0772 var up = key === keymap.ARROW_UP,
0773 $children = $resultContainer.children();
0774
0775 if (!$active.length) {
0776 if (up) {
0777 $children.last().addClass('active');
0778 } else {
0779 $children.first().addClass('active');
0780 }
0781 } else if ($children.length > 1) {
0782 if (up) {
0783 if ($active.is(':first-child')) {
0784 $children.last().addClass('active');
0785 } else {
0786 $active.prev().addClass('active');
0787 }
0788 } else {
0789 if ($active.is(':last-child')) {
0790 $children.first().addClass('active');
0791 } else {
0792 $active.next().addClass('active');
0793 }
0794 }
0795
0796 $active.removeClass('active');
0797 }
0798
0799 // Do not change cursor position in the input element
0800 event.preventDefault();
0801 break;
0802 }
0803 });
0804 };
0805
0806 $('#phpbb').click(function() {
0807 var $this = $(this);
0808
0809 if (!$this.is('.live-search') && !$this.parents().is('.live-search')) {
0810 phpbb.search.closeResults($('input, textarea'), $('.live-search'));
0811 }
0812 });
0813
0814 phpbb.history = {};
0815
0816 /**
0817 * Check whether a method in the native history object is supported.
0818 *
0819 * @param {string} fn Method name.
0820 * @returns {bool} Returns true if the method is supported.
0821 */
0822 phpbb.history.isSupported = function(fn) {
0823 return !(typeof history === 'undefined' || typeof history[fn] === 'undefined');
0824 };
0825
0826 /**
0827 * Wrapper for the pushState and replaceState methods of the
0828 * native history object.
0829 *
0830 * @param {string} mode Mode. Either push or replace.
0831 * @param {string} url New URL.
0832 * @param {string} [title] Optional page title.
0833 * @param {object} [obj] Optional state object.
0834 */
0835 phpbb.history.alterUrl = function(mode, url, title, obj) {
0836 var fn = mode + 'State';
0837
0838 if (!url || !phpbb.history.isSupported(fn)) {
0839 return;
0840 }
0841 if (!title) {
0842 title = document.title;
0843 }
0844 if (!obj) {
0845 obj = null;
0846 }
0847
0848 history[fn](obj, title, url);
0849 };
0850
0851 /**
0852 * Wrapper for the native history.replaceState method.
0853 *
0854 * @param {string} url New URL.
0855 * @param {string} [title] Optional page title.
0856 * @param {object} [obj] Optional state object.
0857 */
0858 phpbb.history.replaceUrl = function(url, title, obj) {
0859 phpbb.history.alterUrl('replace', url, title, obj);
0860 };
0861
0862 /**
0863 * Wrapper for the native history.pushState method.
0864 *
0865 * @param {string} url New URL.
0866 * @param {string} [title] Optional page title.
0867 * @param {object} [obj] Optional state object.
0868 */
0869 phpbb.history.pushUrl = function(url, title, obj) {
0870 phpbb.history.alterUrl('push', url, title, obj);
0871 };
0872
0873 /**
0874 * Hide the optgroups that are not the selected timezone
0875 *
0876 * @param {bool} keepSelection Shall we keep the value selected, or shall the
0877 * user be forced to repick one.
0878 */
0879 phpbb.timezoneSwitchDate = function(keepSelection) {
0880 var $timezoneCopy = $('#timezone_copy');
0881 var $timezone = $('#timezone');
0882 var $tzDate = $('#tz_date');
0883 var $tzSelectDateSuggest = $('#tz_select_date_suggest');
0884
0885 if ($timezoneCopy.length === 0) {
0886 // We make a backup of the original dropdown, so we can remove optgroups
0887 // instead of setting display to none, because IE and chrome will not
0888 // hide options inside of optgroups and selects via css
0889 $timezone.clone()
0890 .attr('id', 'timezone_copy')
0891 .css('display', 'none')
0892 .attr('name', 'tz_copy')
0893 .insertAfter('#timezone');
0894 } else {
0895 // Copy the content of our backup, so we can remove all unneeded options
0896 $timezone.html($timezoneCopy.html());
0897 }
0898
0899 if ($tzDate.val() !== '') {
0900 $timezone.children('optgroup').remove(':not([data-tz-value="' + $tzDate.val() + '"])');
0901 }
0902
0903 if ($tzDate.val() === $tzSelectDateSuggest.attr('data-suggested-tz')) {
0904 $tzSelectDateSuggest.css('display', 'none');
0905 } else {
0906 $tzSelectDateSuggest.css('display', 'inline');
0907 }
0908
0909 var $tzOptions = $timezone.children('optgroup[data-tz-value="' + $tzDate.val() + '"]').children('option');
0910
0911 if ($tzOptions.length === 1) {
0912 // If there is only one timezone for the selected date, we just select that automatically.
0913 $tzOptions.prop('selected', true);
0914 keepSelection = true;
0915 }
0916
0917 if (typeof keepSelection !== 'undefined' && !keepSelection) {
0918 var $timezoneOptions = $timezone.find('optgroup option');
0919 if ($timezoneOptions.filter(':selected').length <= 0) {
0920 $timezoneOptions.filter(':first').prop('selected', true);
0921 }
0922 }
0923 };
0924
0925 /**
0926 * Display the date/time select
0927 */
0928 phpbb.timezoneEnableDateSelection = function() {
0929 $('#tz_select_date').css('display', 'block');
0930 };
0931
0932 /**
0933 * Preselect a date/time or suggest one, if it is not picked.
0934 *
0935 * @param {bool} forceSelector Shall we select the suggestion?
0936 */
0937 phpbb.timezonePreselectSelect = function(forceSelector) {
0938
0939 // The offset returned here is in minutes and negated.
0940 var offset = (new Date()).getTimezoneOffset();
0941 var sign = '-';
0942
0943 if (offset < 0) {
0944 sign = '+';
0945 offset = -offset;
0946 }
0947
0948 var minutes = offset % 60;
0949 var hours = (offset - minutes) / 60;
0950
0951 if (hours === 0) {
0952 hours = '00';
0953 sign = '+';
0954 } else if (hours < 10) {
0955 hours = '0' + hours.toString();
0956 } else {
0957 hours = hours.toString();
0958 }
0959
0960 if (minutes < 10) {
0961 minutes = '0' + minutes.toString();
0962 } else {
0963 minutes = minutes.toString();
0964 }
0965
0966 var prefix = 'UTC' + sign + hours + ':' + minutes;
0967 var prefixLength = prefix.length;
0968 var selectorOptions = $('option', '#tz_date');
0969 var i;
0970
0971 var $tzSelectDateSuggest = $('#tz_select_date_suggest');
0972
0973 for (i = 0; i < selectorOptions.length; ++i) {
0974 var option = selectorOptions[i];
0975
0976 if (option.value.substring(0, prefixLength) === prefix) {
0977 if ($('#tz_date').val() !== option.value && !forceSelector) {
0978 // We do not select the option for the user, but notify him,
0979 // that we would suggest a different setting.
0980 phpbb.timezoneSwitchDate(true);
0981 $tzSelectDateSuggest.css('display', 'inline');
0982 } else {
0983 option.selected = true;
0984 phpbb.timezoneSwitchDate(!forceSelector);
0985 $tzSelectDateSuggest.css('display', 'none');
0986 }
0987
0988 var suggestion = $tzSelectDateSuggest.attr('data-l-suggestion');
0989
0990 $tzSelectDateSuggest.attr('title', suggestion.replace('%s', option.innerHTML));
0991 $tzSelectDateSuggest.attr('value', suggestion.replace('%s', option.innerHTML.substring(0, 9)));
0992 $tzSelectDateSuggest.attr('data-suggested-tz', option.innerHTML);
0993
0994 // Found the suggestion, there cannot be more, so return from here.
0995 return;
0996 }
0997 }
0998 };
0999
1000 phpbb.ajaxCallbacks = {};
1001
1002 /**
1003 * Adds an AJAX callback to be used by phpbb.ajaxify.
1004 *
1005 * See the phpbb.ajaxify comments for information on stuff like parameters.
1006 *
1007 * @param {string} id The name of the callback.
1008 * @param {function} callback The callback to be called.
1009 */
1010 phpbb.addAjaxCallback = function(id, callback) {
1011 if (typeof callback === 'function') {
1012 phpbb.ajaxCallbacks[id] = callback;
1013 }
1014 return this;
1015 };
1016
1017 /**
1018 * This callback handles live member searches.
1019 */
1020 phpbb.addAjaxCallback('member_search', function(res) {
1021 phpbb.search.handleResponse(res, $(this), false, phpbb.getFunctionByName('phpbb.search.setValueOnClick'));
1022 });
1023
1024 /**
1025 * This callback alternates text - it replaces the current text with the text in
1026 * the alt-text data attribute, and replaces the text in the attribute with the
1027 * current text so that the process can be repeated.
1028 */
1029 phpbb.addAjaxCallback('alt_text', function() {
1030 var $anchor,
1031 updateAll = $(this).data('update-all'),
1032 altText;
1033
1034 if (updateAll !== undefined && updateAll.length) {
1035 $anchor = $(updateAll);
1036 } else {
1037 $anchor = $(this);
1038 }
1039
1040 $anchor.each(function() {
1041 var $this = $(this);
1042 altText = $this.attr('data-alt-text');
1043 $this.attr('data-alt-text', $.trim($this.text()));
1044 $this.attr('title', altText);
1045 $this.children('span').text(altText);
1046 });
1047 });
1048
1049 /**
1050 * This callback is based on the alt_text callback.
1051 *
1052 * It replaces the current text with the text in the alt-text data attribute,
1053 * and replaces the text in the attribute with the current text so that the
1054 * process can be repeated.
1055 * Additionally it replaces the class of the link's parent
1056 * and changes the link itself.
1057 */
1058 phpbb.addAjaxCallback('toggle_link', function() {
1059 var $anchor,
1060 updateAll = $(this).data('update-all') ,
1061 toggleText,
1062 toggleUrl,
1063 toggleClass;
1064
1065 if (updateAll !== undefined && updateAll.length) {
1066 $anchor = $(updateAll);
1067 } else {
1068 $anchor = $(this);
1069 }
1070
1071 $anchor.each(function() {
1072 var $this = $(this);
1073
1074 // Toggle link url
1075 toggleUrl = $this.attr('data-toggle-url');
1076 $this.attr('data-toggle-url', $this.attr('href'));
1077 $this.attr('href', toggleUrl);
1078
1079 // Toggle class of link parent
1080 toggleClass = $this.attr('data-toggle-class');
1081 $this.attr('data-toggle-class', $this.children().attr('class'));
1082 $this.children('.icon').attr('class', toggleClass);
1083
1084 // Toggle link text
1085 toggleText = $this.attr('data-toggle-text');
1086 $this.attr('data-toggle-text', $this.children('span').text());
1087 $this.attr('title', $.trim(toggleText));
1088 $this.children('span').text(toggleText);
1089 });
1090 });
1091
1092 /**
1093 * Automatically resize textarea
1094 *
1095 * This function automatically resizes textarea elements when user
1096 * types text.
1097 *
1098 * @param {jQuery} $items jQuery object(s) to resize
1099 * @param {object} [options] Optional parameter that adjusts default
1100 * configuration. See configuration variable
1101 *
1102 * Optional parameters:
1103 * minWindowHeight {number} Minimum browser window height when textareas are resized. Default = 500
1104 * minHeight {number} Minimum height of textarea. Default = 200
1105 * maxHeight {number} Maximum height of textarea. Default = 500
1106 * heightDiff {number} Minimum difference between window and textarea height. Default = 200
1107 * resizeCallback {function} Function to call after resizing textarea
1108 * resetCallback {function} Function to call when resize has been canceled
1109
1110 * Callback function format: function(item) {}
1111 * this points to DOM object
1112 * item is a jQuery object, same as this
1113 */
1114 phpbb.resizeTextArea = function($items, options) {
1115 // Configuration
1116 var configuration = {
1117 minWindowHeight: 500,
1118 minHeight: 200,
1119 maxHeight: 500,
1120 heightDiff: 200,
1121 resizeCallback: function() {},
1122 resetCallback: function() {}
1123 };
1124
1125 if (phpbb.isTouch) {
1126 return;
1127 }
1128
1129 if (arguments.length > 1) {
1130 configuration = $.extend(configuration, options);
1131 }
1132
1133 function resetAutoResize(item) {
1134 var $item = $(item);
1135 if ($item.hasClass('auto-resized')) {
1136 $(item)
1137 .css({ height: '', resize: '' })
1138 .removeClass('auto-resized');
1139 configuration.resetCallback.call(item, $item);
1140 }
1141 }
1142
1143 function autoResize(item) {
1144 function setHeight(height) {
1145 height += parseInt($item.css('height'), 10) - $item.innerHeight();
1146 $item
1147 .css({ height: height + 'px', resize: 'none' })
1148 .addClass('auto-resized');
1149 configuration.resizeCallback.call(item, $item);
1150 }
1151
1152 var windowHeight = $(window).height();
1153
1154 if (windowHeight < configuration.minWindowHeight) {
1155 resetAutoResize(item);
1156 return;
1157 }
1158
1159 var maxHeight = Math.min(
1160 Math.max(windowHeight - configuration.heightDiff, configuration.minHeight),
1161 configuration.maxHeight
1162 ),
1163 $item = $(item),
1164 height = parseInt($item.innerHeight(), 10),
1165 scrollHeight = (item.scrollHeight) ? item.scrollHeight : 0;
1166
1167 if (height < 0) {
1168 return;
1169 }
1170
1171 if (height > maxHeight) {
1172 setHeight(maxHeight);
1173 } else if (scrollHeight > (height + 5)) {
1174 setHeight(Math.min(maxHeight, scrollHeight));
1175 }
1176 }
1177
1178 $items.on('focus change keyup', function() {
1179 $(this).each(function() {
1180 autoResize(this);
1181 });
1182 }).change();
1183
1184 $(window).resize(function() {
1185 $items.each(function() {
1186 if ($(this).hasClass('auto-resized')) {
1187 autoResize(this);
1188 }
1189 });
1190 });
1191 };
1192
1193 /**
1194 * Check if cursor in textarea is currently inside a bbcode tag
1195 *
1196 * @param {object} textarea Textarea DOM object
1197 * @param {Array} startTags List of start tags to look for
1198 * For example, Array('[code]', '[code=')
1199 * @param {Array} endTags List of end tags to look for
1200 * For example, Array('[/code]')
1201 *
1202 * @returns {boolean} True if cursor is in bbcode tag
1203 */
1204 phpbb.inBBCodeTag = function(textarea, startTags, endTags) {
1205 var start = textarea.selectionStart,
1206 lastEnd = -1,
1207 lastStart = -1,
1208 i, index, value;
1209
1210 if (typeof start !== 'number') {
1211 return false;
1212 }
1213
1214 value = textarea.value.toLowerCase();
1215
1216 for (i = 0; i < startTags.length; i++) {
1217 var tagLength = startTags[i].length;
1218 if (start >= tagLength) {
1219 index = value.lastIndexOf(startTags[i], start - tagLength);
1220 lastStart = Math.max(lastStart, index);
1221 }
1222 }
1223 if (lastStart === -1) {
1224 return false;
1225 }
1226
1227 if (start > 0) {
1228 for (i = 0; i < endTags.length; i++) {
1229 index = value.lastIndexOf(endTags[i], start - 1);
1230 lastEnd = Math.max(lastEnd, index);
1231 }
1232 }
1233
1234 return (lastEnd < lastStart);
1235 };
1236
1237
1238 /**
1239 * Adjust textarea to manage code bbcode
1240 *
1241 * This function allows to use tab characters when typing code
1242 * and keeps indentation of previous line of code when adding new
1243 * line while typing code.
1244 *
1245 * Editor's functionality is changed only when cursor is between
1246 * [code] and [/code] bbcode tags.
1247 *
1248 * @param {object} textarea Textarea DOM object to apply editor to
1249 */
1250 phpbb.applyCodeEditor = function(textarea) {
1251 // list of allowed start and end bbcode code tags, in lower case
1252 var startTags = ['[code]', '[code='],
1253 startTagsEnd = ']',
1254 endTags = ['[/code]'];
1255
1256 if (!textarea || typeof textarea.selectionStart !== 'number') {
1257 return;
1258 }
1259
1260 if ($(textarea).data('code-editor') === true) {
1261 return;
1262 }
1263
1264 function inTag() {
1265 return phpbb.inBBCodeTag(textarea, startTags, endTags);
1266 }
1267
1268 /**
1269 * Get line of text before cursor
1270 *
1271 * @param {boolean} stripCodeStart If true, only part of line
1272 * after [code] tag will be returned.
1273 *
1274 * @returns {string} Line of text
1275 */
1276 function getLastLine(stripCodeStart) {
1277 var start = textarea.selectionStart,
1278 value = textarea.value,
1279 index = value.lastIndexOf('\n', start - 1);
1280
1281 value = value.substring(index + 1, start);
1282
1283 if (stripCodeStart) {
1284 for (var i = 0; i < startTags.length; i++) {
1285 index = value.lastIndexOf(startTags[i]);
1286 if (index >= 0) {
1287 var tagLength = startTags[i].length;
1288
1289 value = value.substring(index + tagLength);
1290 if (startTags[i].lastIndexOf(startTagsEnd) !== tagLength) {
1291 index = value.indexOf(startTagsEnd);
1292
1293 if (index >= 0) {
1294 value = value.substr(index + 1);
1295 }
1296 }
1297 }
1298 }
1299 }
1300
1301 return value;
1302 }
1303
1304 /**
1305 * Append text at cursor position
1306 *
1307 * @param {string} text Text to append
1308 */
1309 function appendText(text) {
1310 var start = textarea.selectionStart,
1311 end = textarea.selectionEnd,
1312 value = textarea.value;
1313
1314 textarea.value = value.substr(0, start) + text + value.substr(end);
1315 textarea.selectionStart = textarea.selectionEnd = start + text.length;
1316 }
1317
1318 $(textarea).data('code-editor', true).on('keydown', function(event) {
1319 var key = event.keyCode || event.which;
1320
1321 // intercept tabs
1322 if (key === keymap.TAB &&
1323 !event.ctrlKey &&
1324 !event.shiftKey &&
1325 !event.altKey &&
1326 !event.metaKey) {
1327 if (inTag()) {
1328 appendText('\t');
1329 event.preventDefault();
1330 return;
1331 }
1332 }
1333
1334 // intercept new line characters
1335 if (key === keymap.ENTER) {
1336 if (inTag()) {
1337 var lastLine = getLastLine(true),
1338 code = '' + /^\s*/g.exec(lastLine);
1339
1340 if (code.length > 0) {
1341 appendText('\n' + code);
1342 event.preventDefault();
1343 }
1344 }
1345 }
1346 });
1347 };
1348
1349 /**
1350 * Show drag and drop animation when textarea is present
1351 *
1352 * This function will enable the drag and drop animation for a specified
1353 * textarea.
1354 *
1355 * @param {HTMLElement} textarea Textarea DOM object to apply editor to
1356 */
1357 phpbb.showDragNDrop = function(textarea) {
1358 if (!textarea) {
1359 return;
1360 }
1361
1362 $('body').on('dragenter dragover', function () {
1363 $(textarea).addClass('drag-n-drop');
1364 }).on('dragleave dragout dragend drop', function() {
1365 $(textarea).removeClass('drag-n-drop');
1366 });
1367 $(textarea).on('dragenter dragover', function () {
1368 $(textarea).addClass('drag-n-drop-highlight');
1369 }).on('dragleave dragout dragend drop', function() {
1370 $(textarea).removeClass('drag-n-drop-highlight');
1371 });
1372 };
1373
1374 /**
1375 * List of classes that toggle dropdown menu,
1376 * list of classes that contain visible dropdown menu
1377 *
1378 * Add your own classes to strings with comma (probably you
1379 * will never need to do that)
1380 */
1381 phpbb.dropdownHandles = '.dropdown-container.dropdown-visible .dropdown-toggle';
1382 phpbb.dropdownVisibleContainers = '.dropdown-container.dropdown-visible';
1383
1384 /**
1385 * Dropdown toggle event handler
1386 * This handler is used by phpBB.registerDropdown() and other functions
1387 */
1388 phpbb.toggleDropdown = function() {
1389 var $this = $(this),
1390 options = $this.data('dropdown-options'),
1391 parent = options.parent,
1392 visible = parent.hasClass('dropdown-visible'),
1393 direction;
1394
1395 if (!visible) {
1396 // Hide other dropdown menus
1397 $(phpbb.dropdownHandles).each(phpbb.toggleDropdown);
1398
1399 // Figure out direction of dropdown
1400 direction = options.direction;
1401 var verticalDirection = options.verticalDirection,
1402 offset = $this.offset();
1403
1404 if (direction === 'auto') {
1405 if (($(window).width() - $this.outerWidth(true)) / 2 > offset.left) {
1406 direction = 'right';
1407 } else {
1408 direction = 'left';
1409 }
1410 }
1411 parent.toggleClass(options.leftClass, direction === 'left')
1412 .toggleClass(options.rightClass, direction === 'right');
1413
1414 if (verticalDirection === 'auto') {
1415 var height = $(window).height(),
1416 top = offset.top - $(window).scrollTop();
1417
1418 verticalDirection = (top < height * 0.7) ? 'down' : 'up';
1419 }
1420 parent.toggleClass(options.upClass, verticalDirection === 'up')
1421 .toggleClass(options.downClass, verticalDirection === 'down');
1422 }
1423
1424 options.dropdown.toggle();
1425 parent.toggleClass(options.visibleClass, !visible)
1426 .toggleClass('dropdown-visible', !visible);
1427
1428 // Check dimensions when showing dropdown
1429 // !visible because variable shows state of dropdown before it was toggled
1430 if (!visible) {
1431 var windowWidth = $(window).width();
1432
1433 options.dropdown.find('.dropdown-contents').each(function() {
1434 var $this = $(this);
1435
1436 $this.css({
1437 marginLeft: 0,
1438 left: 0,
1439 marginRight: 0,
1440 maxWidth: (windowWidth - 4) + 'px'
1441 });
1442
1443 var offset = $this.offset().left,
1444 width = $this.outerWidth(true);
1445
1446 if (offset < 2) {
1447 $this.css('left', (2 - offset) + 'px');
1448 } else if ((offset + width + 2) > windowWidth) {
1449 $this.css('margin-left', (windowWidth - offset - width - 2) + 'px');
1450 }
1451
1452 // Check whether the vertical scrollbar is present.
1453 $this.toggleClass('dropdown-nonscroll', this.scrollHeight === $this.innerHeight());
1454
1455 });
1456 var freeSpace = parent.offset().left - 4;
1457
1458 if (direction === 'left') {
1459 options.dropdown.css('margin-left', '-' + freeSpace + 'px');
1460
1461 // Try to position the notification dropdown correctly in RTL-responsive mode
1462 if (options.dropdown.hasClass('dropdown-extended')) {
1463 var contentWidth,
1464 fullFreeSpace = freeSpace + parent.outerWidth();
1465
1466 options.dropdown.find('.dropdown-contents').each(function() {
1467 contentWidth = parseInt($(this).outerWidth(), 10);
1468 $(this).css({ marginLeft: 0, left: 0 });
1469 });
1470
1471 var maxOffset = Math.min(contentWidth, fullFreeSpace) + 'px';
1472 options.dropdown.css({
1473 width: maxOffset,
1474 marginLeft: -maxOffset
1475 });
1476 }
1477 } else {
1478 options.dropdown.css('margin-right', '-' + (windowWidth + freeSpace) + 'px');
1479 }
1480 }
1481
1482 // Prevent event propagation
1483 if (arguments.length > 0) {
1484 try {
1485 var e = arguments[0];
1486 e.preventDefault();
1487 e.stopPropagation();
1488 } catch (error) { }
1489 }
1490 return false;
1491 };
1492
1493 /**
1494 * Toggle dropdown submenu
1495 */
1496 phpbb.toggleSubmenu = function(e) {
1497 $(this).siblings('.dropdown-submenu').toggle();
1498 e.preventDefault();
1499 };
1500
1501 /**
1502 * Register dropdown menu
1503 * Shows/hides dropdown, decides which side to open to
1504 *
1505 * @param {jQuery} toggle Link that toggles dropdown.
1506 * @param {jQuery} dropdown Dropdown menu.
1507 * @param {Object} options List of options. Optional.
1508 */
1509 phpbb.registerDropdown = function(toggle, dropdown, options) {
1510 var ops = {
1511 parent: toggle.parent(), // Parent item to add classes to
1512 direction: 'auto', // Direction of dropdown menu. Possible values: auto, left, right
1513 verticalDirection: 'auto', // Vertical direction. Possible values: auto, up, down
1514 visibleClass: 'visible', // Class to add to parent item when dropdown is visible
1515 leftClass: 'dropdown-left', // Class to add to parent item when dropdown opens to left side
1516 rightClass: 'dropdown-right', // Class to add to parent item when dropdown opens to right side
1517 upClass: 'dropdown-up', // Class to add to parent item when dropdown opens above menu item
1518 downClass: 'dropdown-down' // Class to add to parent item when dropdown opens below menu item
1519 };
1520 if (options) {
1521 ops = $.extend(ops, options);
1522 }
1523 ops.dropdown = dropdown;
1524
1525 ops.parent.addClass('dropdown-container');
1526 toggle.addClass('dropdown-toggle');
1527
1528 toggle.data('dropdown-options', ops);
1529
1530 toggle.click(phpbb.toggleDropdown);
1531 $('.dropdown-toggle-submenu', ops.parent).click(phpbb.toggleSubmenu);
1532 };
1533
1534 /**
1535 * Get the HTML for a color palette table.
1536 *
1537 * @param {string} dir Palette direction - either v or h
1538 * @param {int} width Palette cell width.
1539 * @param {int} height Palette cell height.
1540 */
1541 phpbb.colorPalette = function(dir, width, height) {
1542 var r, g, b,
1543 numberList = new Array(6),
1544 color = '',
1545 html = '';
1546
1547 numberList[0] = '00';
1548 numberList[1] = '40';
1549 numberList[2] = '80';
1550 numberList[3] = 'BF';
1551 numberList[4] = 'FF';
1552
1553 var tableClass = (dir === 'h') ? 'horizontal-palette' : 'vertical-palette';
1554 html += '<table class="not-responsive colour-palette ' + tableClass + '" style="width: auto;">';
1555
1556 for (r = 0; r < 5; r++) {
1557 if (dir === 'h') {
1558 html += '<tr>';
1559 }
1560
1561 for (g = 0; g < 5; g++) {
1562 if (dir === 'v') {
1563 html += '<tr>';
1564 }
1565
1566 for (b = 0; b < 5; b++) {
1567 color = '' + numberList[r] + numberList[g] + numberList[b];
1568 html += '<td style="background-color: #' + color + '; width: ' + width + 'px; height: ' +
1569 height + 'px;"><a href="#" data-color="' + color + '" style="display: block; width: ' +
1570 width + 'px; height: ' + height + 'px; " alt="#' + color + '" title="#' + color + '"></a>';
1571 html += '</td>';
1572 }
1573
1574 if (dir === 'v') {
1575 html += '</tr>';
1576 }
1577 }
1578
1579 if (dir === 'h') {
1580 html += '</tr>';
1581 }
1582 }
1583 html += '</table>';
1584 return html;
1585 };
1586
1587 /**
1588 * Register a color palette.
1589 *
1590 * @param {jQuery} el jQuery object for the palette container.
1591 */
1592 phpbb.registerPalette = function(el) {
1593 var orientation = el.attr('data-color-palette') || el.attr('data-orientation'), // data-orientation kept for backwards compat.
1594 height = el.attr('data-height'),
1595 width = el.attr('data-width'),
1596 target = el.attr('data-target'),
1597 bbcode = el.attr('data-bbcode');
1598
1599 // Insert the palette HTML into the container.
1600 el.html(phpbb.colorPalette(orientation, width, height));
1601
1602 // Add toggle control.
1603 $('#color_palette_toggle').click(function(e) {
1604 el.toggle();
1605 e.preventDefault();
1606 });
1607
1608 // Attach event handler when a palette cell is clicked.
1609 $(el).on('click', 'a', function(e) {
1610 var color = $(this).attr('data-color');
1611
1612 if (bbcode) {
1613 bbfontstyle('[color=#' + color + ']', '[/color]');
1614 } else {
1615 $(target).val(color);
1616 }
1617 e.preventDefault();
1618 });
1619 };
1620
1621 /**
1622 * Set display of page element
1623 *
1624 * @param {string} id The ID of the element to change
1625 * @param {int} action Set to 0 if element display should be toggled, -1 for
1626 * hiding the element, and 1 for showing it.
1627 * @param {string} type Display type that should be used, e.g. inline, block or
1628 * other CSS "display" types
1629 */
1630 phpbb.toggleDisplay = function(id, action, type) {
1631 if (!type) {
1632 type = 'block';
1633 }
1634
1635 var $element = $('#' + id);
1636
1637 var display = $element.css('display');
1638 if (!action) {
1639 action = (display === '' || display === type) ? -1 : 1;
1640 }
1641 $element.css('display', ((action === 1) ? type : 'none'));
1642 };
1643
1644 /**
1645 * Toggle additional settings based on the selected
1646 * option of select element.
1647 *
1648 * @param {jQuery} el jQuery select element object.
1649 */
1650 phpbb.toggleSelectSettings = function(el) {
1651 el.children().each(function() {
1652 var $this = $(this),
1653 $setting = $($this.data('toggle-setting'));
1654 $setting.toggle($this.is(':selected'));
1655
1656 // Disable any input elements that are not visible right now
1657 if ($this.is(':selected')) {
1658 $($this.data('toggle-setting') + ' input').prop('disabled', false);
1659 } else {
1660 $($this.data('toggle-setting') + ' input').prop('disabled', true);
1661 }
1662 });
1663 };
1664
1665 /**
1666 * Get function from name.
1667 * Based on http://stackoverflow.com/a/359910
1668 *
1669 * @param {string} functionName Function to get.
1670 * @returns function
1671 */
1672 phpbb.getFunctionByName = function (functionName) {
1673 var namespaces = functionName.split('.'),
1674 func = namespaces.pop(),
1675 context = window;
1676
1677 for (var i = 0; i < namespaces.length; i++) {
1678 context = context[namespaces[i]];
1679 }
1680 return context[func];
1681 };
1682
1683 /**
1684 * Register page dropdowns.
1685 */
1686 phpbb.registerPageDropdowns = function() {
1687 var $body = $('body');
1688
1689 $body.find('.dropdown-container').each(function() {
1690 var $this = $(this),
1691 $trigger = $this.find('.dropdown-trigger:first'),
1692 $contents = $this.find('.dropdown'),
1693 options = {
1694 direction: 'auto',
1695 verticalDirection: 'auto'
1696 },
1697 data;
1698
1699 if (!$trigger.length) {
1700 data = $this.attr('data-dropdown-trigger');
1701 $trigger = data ? $this.children(data) : $this.children('a:first');
1702 }
1703
1704 if (!$contents.length) {
1705 data = $this.attr('data-dropdown-contents');
1706 $contents = data ? $this.children(data) : $this.children('div:first');
1707 }
1708
1709 if (!$trigger.length || !$contents.length) {
1710 return;
1711 }
1712
1713 if ($this.hasClass('dropdown-up')) {
1714 options.verticalDirection = 'up';
1715 }
1716 if ($this.hasClass('dropdown-down')) {
1717 options.verticalDirection = 'down';
1718 }
1719 if ($this.hasClass('dropdown-left')) {
1720 options.direction = 'left';
1721 }
1722 if ($this.hasClass('dropdown-right')) {
1723 options.direction = 'right';
1724 }
1725
1726 phpbb.registerDropdown($trigger, $contents, options);
1727 });
1728
1729 // Hide active dropdowns when click event happens outside
1730 $body.click(function(e) {
1731 var $parents = $(e.target).parents();
1732 if (!$parents.is(phpbb.dropdownVisibleContainers)) {
1733 $(phpbb.dropdownHandles).each(phpbb.toggleDropdown);
1734 }
1735 });
1736 };
1737
1738 /**
1739 * Handle avatars to be lazy loaded.
1740 */
1741 phpbb.lazyLoadAvatars = function loadAvatars() {
1742 $('.avatar[data-src]').each(function () {
1743 var $avatar = $(this);
1744
1745 $avatar
1746 .attr('src', $avatar.data('src'))
1747 .removeAttr('data-src');
1748 });
1749 };
1750
1751 phpbb.recaptcha = {
1752 button: null,
1753 ready: false,
1754
1755 token: $('input[name="recaptcha_token"]'),
1756 form: $('.g-recaptcha').parents('form'),
1757 v3: $('[data-recaptcha-v3]'),
1758
1759 load: function() {
1760 phpbb.recaptcha.bindButton();
1761 phpbb.recaptcha.bindForm();
1762 },
1763 bindButton: function() {
1764 phpbb.recaptcha.form.find('input[type="submit"]').on('click', function() {
1765 // Listen to all the submit buttons for the form that has reCAPTCHA protection,
1766 // and store it so we can click the exact same button later on when we are ready.
1767 phpbb.recaptcha.button = this;
1768 });
1769 },
1770 bindForm: function() {
1771 phpbb.recaptcha.form.on('submit', function(e) {
1772 // If ready is false, it means the user pressed a submit button.
1773 // And the form was not submitted by us, after the token was loaded.
1774 if (!phpbb.recaptcha.ready) {
1775 // If version 3 is used, we need to make a different execution,
1776 // including the action and the site key.
1777 if (phpbb.recaptcha.v3.length) {
1778 grecaptcha.execute(
1779 phpbb.recaptcha.v3.data('recaptcha-v3'),
1780 {action: phpbb.recaptcha.v3.val()}
1781 ).then(function(token) {
1782 // Place the token inside the form
1783 phpbb.recaptcha.token.val(token);
1784
1785 // And now we submit the form.
1786 phpbb.recaptcha.submitForm();
1787 });
1788 } else {
1789 // Regular version 2 execution
1790 grecaptcha.execute();
1791 }
1792
1793 // Do not submit the form
1794 e.preventDefault();
1795 }
1796 });
1797 },
1798 submitForm: function() {
1799 // Now we are ready, so set it to true.
1800 // so the 'submit' event doesn't run multiple times.
1801 phpbb.recaptcha.ready = true;
1802
1803 if (phpbb.recaptcha.button) {
1804 // If there was a specific button pressed initially, trigger the same button
1805 phpbb.recaptcha.button.click();
1806 } else {
1807 if (typeof phpbb.recaptcha.form.submit !== 'function') {
1808 // Rename input[name="submit"] so that we can submit the form
1809 phpbb.recaptcha.form.submit.name = 'submit_btn';
1810 }
1811
1812 phpbb.recaptcha.form.submit();
1813 }
1814 }
1815 };
1816
1817 // reCAPTCHA v2 doesn't accept callback functions nested inside objects
1818 // so we need to make this helper functions here
1819 window.phpbbRecaptchaOnLoad = function() {
1820 phpbb.recaptcha.load();
1821 };
1822
1823 window.phpbbRecaptchaOnSubmit = function() {
1824 phpbb.recaptcha.submitForm();
1825 };
1826
1827 $(window).on('load', phpbb.lazyLoadAvatars);
1828
1829 /**
1830 * Apply code editor to all textarea elements with data-bbcode attribute
1831 */
1832 $(function() {
1833 // reCAPTCHA v3 needs to be initialized
1834 if (phpbb.recaptcha.v3.length) {
1835 phpbb.recaptcha.load();
1836 }
1837
1838 $('textarea[data-bbcode]').each(function() {
1839 phpbb.applyCodeEditor(this);
1840 });
1841
1842 phpbb.registerPageDropdowns();
1843
1844 $('[data-color-palette], [data-orientation]').each(function() {
1845 phpbb.registerPalette($(this));
1846 });
1847
1848 // Update browser history URL to point to specific post in viewtopic.php
1849 // when using view=unread#unread link.
1850 phpbb.history.replaceUrl($('#unread[data-url]').data('url'));
1851
1852 // Hide settings that are not selected via select element.
1853 $('select[data-togglable-settings]').each(function() {
1854 var $this = $(this);
1855
1856 $this.change(function() {
1857 phpbb.toggleSelectSettings($this);
1858 });
1859 phpbb.toggleSelectSettings($this);
1860 });
1861 });
1862
1863 })(jQuery); // Avoid conflicts with other libraries
1864