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.
Auf den Verzeichnisnamen klicken, dies zeigt nur das Verzeichnis mit Inhalt an

(Beispiel Datei-Icons)

Auf das Icon klicken um den Quellcode anzuzeigen

core.js

Zuletzt modifiziert: 02.04.2025, 15:01 - Dateigröße: 49.15 KiB


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('&amp;', '&');
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('&amp;', '&');
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