[ Index ]

PHP Cross Reference of phpBB-3.3.14-deutsch

title

Body

[close]

/assets/javascript/ -> core.js (source)

   1  /* global bbfontstyle */
   2  
   3  var phpbb = {};
   4  phpbb.alertTime = 100;
   5  
   6  (function($) {  // Avoid conflicts with other libraries
   7  
   8  'use strict';
   9  
  10  // define a couple constants for keydown functions.
  11  var keymap = {
  12      TAB: 9,
  13      ENTER: 13,
  14      ESC: 27,
  15      ARROW_UP: 38,
  16      ARROW_DOWN: 40
  17  };
  18  
  19  var $dark = $('#darkenwrapper');
  20  var $loadingIndicator;
  21  var phpbbAlertTimer = null;
  22  
  23  phpbb.isTouch = (window && typeof window.ontouchstart !== 'undefined');
  24  
  25  // Add ajax pre-filter to prevent cross-domain script execution
  26  $.ajaxPrefilter(function(s) {
  27      if (s.crossDomain) {
  28          s.contents.script = false;
  29      }
  30  });
  31  
  32  /**
  33   * Display a loading screen
  34   *
  35   * @returns {object} Returns loadingIndicator.
  36   */
  37  phpbb.loadingIndicator = function() {
  38      if (!$loadingIndicator) {
  39          $loadingIndicator = $('<div />', {
  40              'id': 'loading_indicator',
  41              'class': 'loading_indicator'
  42          });
  43          $loadingIndicator.appendTo('#page-footer');
  44      }
  45  
  46      if (!$loadingIndicator.is(':visible')) {
  47          $loadingIndicator.fadeIn(phpbb.alertTime);
  48          // Wait 60 seconds and display an error if nothing has been returned by then.
  49          phpbb.clearLoadingTimeout();
  50          phpbbAlertTimer = setTimeout(function() {
  51              phpbb.showTimeoutMessage();
  52          }, 60000);
  53      }
  54  
  55      return $loadingIndicator;
  56  };
  57  
  58  /**
  59   * Show timeout message
  60   */
  61  phpbb.showTimeoutMessage = function () {
  62      var $alert = $('#phpbb_alert');
  63  
  64      if ($loadingIndicator.is(':visible')) {
  65          phpbb.alert($alert.attr('data-l-err'), $alert.attr('data-l-timeout-processing-req'));
  66      }
  67  };
  68  
  69  /**
  70   * Clear loading alert timeout
  71  */
  72  phpbb.clearLoadingTimeout = function() {
  73      if (phpbbAlertTimer !== null) {
  74          clearTimeout(phpbbAlertTimer);
  75          phpbbAlertTimer = null;
  76      }
  77  };
  78  
  79  
  80  /**
  81  * Close popup alert after a specified delay
  82  *
  83  * @param {int} delay Delay in ms until darkenwrapper's click event is triggered
  84  */
  85  phpbb.closeDarkenWrapper = function(delay) {
  86      phpbbAlertTimer = setTimeout(function() {
  87          $('#darkenwrapper').trigger('click');
  88      }, delay);
  89  };
  90  
  91  /**
  92   * Display a simple alert similar to JSs native alert().
  93   *
  94   * You can only call one alert or confirm box at any one time.
  95   *
  96   * @param {string} title Title of the message, eg "Information" (HTML).
  97   * @param {string} msg Message to display (HTML).
  98   *
  99   * @returns {object} Returns the div created.
 100   */
 101  phpbb.alert = function(title, msg) {
 102      var $alert = $('#phpbb_alert');
 103      $alert.find('.alert_title').html(title);
 104      $alert.find('.alert_text').html(msg);
 105  
 106      $(document).on('keydown.phpbb.alert', function(e) {
 107          if (e.keyCode === keymap.ENTER || e.keyCode === keymap.ESC) {
 108              phpbb.alert.close($alert, true);
 109              e.preventDefault();
 110              e.stopPropagation();
 111          }
 112      });
 113      phpbb.alert.open($alert);
 114  
 115      return $alert;
 116  };
 117  
 118  /**
 119  * Handler for opening an alert box.
 120  *
 121  * @param {jQuery} $alert jQuery object.
 122  */
 123  phpbb.alert.open = function($alert) {
 124      if (!$dark.is(':visible')) {
 125          $dark.fadeIn(phpbb.alertTime);
 126      }
 127  
 128      if ($loadingIndicator && $loadingIndicator.is(':visible')) {
 129          $loadingIndicator.fadeOut(phpbb.alertTime, function() {
 130              $dark.append($alert);
 131              $alert.fadeIn(phpbb.alertTime);
 132          });
 133      } else if ($dark.is(':visible')) {
 134          $dark.append($alert);
 135          $alert.fadeIn(phpbb.alertTime);
 136      } else {
 137          $dark.append($alert);
 138          $alert.show();
 139          $dark.fadeIn(phpbb.alertTime);
 140      }
 141  
 142      $alert.on('click', function(e) {
 143          e.stopPropagation();
 144      });
 145  
 146      $dark.one('click', function(e) {
 147          phpbb.alert.close($alert, true);
 148          e.preventDefault();
 149          e.stopPropagation();
 150      });
 151  
 152      $alert.find('.alert_close').one('click', function(e) {
 153          phpbb.alert.close($alert, true);
 154          e.preventDefault();
 155      });
 156  };
 157  
 158  /**
 159  * Handler for closing an alert box.
 160  *
 161  * @param {jQuery} $alert jQuery object.
 162  * @param {bool} fadedark Whether to remove dark background.
 163  */
 164  phpbb.alert.close = function($alert, fadedark) {
 165      var $fade = (fadedark) ? $dark : $alert;
 166  
 167      $fade.fadeOut(phpbb.alertTime, function() {
 168          $alert.hide();
 169      });
 170  
 171      $alert.find('.alert_close').off('click');
 172      $(document).off('keydown.phpbb.alert');
 173  };
 174  
 175  /**
 176   * Display a simple yes / no box to the user.
 177   *
 178   * You can only call one alert or confirm box at any one time.
 179   *
 180   * @param {string} msg Message to display (HTML).
 181   * @param {function} callback Callback. Bool param, whether the user pressed
 182   *     yes or no (or whatever their language is).
 183   * @param {bool} fadedark Remove the dark background when done? Defaults
 184   *     to yes.
 185   *
 186   * @returns {object} Returns the div created.
 187   */
 188  phpbb.confirm = function(msg, callback, fadedark) {
 189      var $confirmDiv = $('#phpbb_confirm');
 190      $confirmDiv.find('.alert_text').html(msg);
 191      fadedark = typeof fadedark !== 'undefined' ? fadedark : true;
 192  
 193      $(document).on('keydown.phpbb.alert', function(e) {
 194          if (e.keyCode === keymap.ENTER || e.keyCode === keymap.ESC) {
 195              var name = (e.keyCode === keymap.ENTER) ? 'confirm' : 'cancel';
 196  
 197              $('input[name="' + name + '"]').trigger('click');
 198              e.preventDefault();
 199              e.stopPropagation();
 200          }
 201      });
 202  
 203      $confirmDiv.find('input[type="button"]').one('click.phpbb.confirmbox', function(e) {
 204          var confirmed = this.name === 'confirm';
 205  
 206          callback(confirmed);
 207          $confirmDiv.find('input[type="button"]').off('click.phpbb.confirmbox');
 208          phpbb.alert.close($confirmDiv, fadedark || !confirmed);
 209  
 210          e.preventDefault();
 211          e.stopPropagation();
 212      });
 213  
 214      phpbb.alert.open($confirmDiv);
 215  
 216      return $confirmDiv;
 217  };
 218  
 219  /**
 220   * Turn a querystring into an array.
 221   *
 222   * @argument {string} string The querystring to parse.
 223   * @returns {object} The object created.
 224   */
 225  phpbb.parseQuerystring = function(string) {
 226      var params = {}, i, split;
 227  
 228      string = string.split('&');
 229      for (i = 0; i < string.length; i++) {
 230          split = string[i].split('=');
 231          params[split[0]] = decodeURIComponent(split[1]);
 232      }
 233      return params;
 234  };
 235  
 236  
 237  /**
 238   * Makes a link use AJAX instead of loading an entire page.
 239   *
 240   * This function will work for links (both standard links and links which
 241   * invoke confirm_box) and forms. It will be called automatically for links
 242   * and forms with the data-ajax attribute set, and will call the necessary
 243   * callback.
 244   *
 245   * For more info, view the following page on the phpBB wiki:
 246   * http://wiki.phpbb.com/JavaScript_Function.phpbb.ajaxify
 247   *
 248   * @param {object} options Options.
 249   */
 250  phpbb.ajaxify = function(options) {
 251      var $elements = $(options.selector),
 252          refresh = options.refresh,
 253          callback = options.callback,
 254          overlay = (typeof options.overlay !== 'undefined') ? options.overlay : true,
 255          isForm = $elements.is('form'),
 256          isText = $elements.is('input[type="text"], textarea'),
 257          eventName;
 258  
 259      if (isForm) {
 260          eventName = 'submit';
 261      } else if (isText) {
 262          eventName = 'keyup';
 263      } else {
 264          eventName = 'click';
 265      }
 266  
 267      $elements.on(eventName, function(event) {
 268          var action, method, data, submit, that = this, $this = $(this);
 269  
 270          if ($this.find('input[type="submit"][data-clicked]').attr('data-ajax') === 'false') {
 271              return;
 272          }
 273  
 274          /**
 275           * Handler for AJAX errors
 276           */
 277  		function errorHandler(jqXHR, textStatus, errorThrown) {
 278              if (typeof console !== 'undefined' && console.log) {
 279                  console.log('AJAX error. status: ' + textStatus + ', message: ' + errorThrown);
 280              }
 281              phpbb.clearLoadingTimeout();
 282              var responseText, errorText = false;
 283              try {
 284                  responseText = JSON.parse(jqXHR.responseText);
 285                  responseText = responseText.message;
 286              } catch (e) {}
 287              if (typeof responseText === 'string' && responseText.length > 0) {
 288                  errorText = responseText;
 289              } else if (typeof errorThrown === 'string' && errorThrown.length > 0) {
 290                  errorText = errorThrown;
 291              } else {
 292                  errorText = $dark.attr('data-ajax-error-text-' + textStatus);
 293                  if (typeof errorText !== 'string' || !errorText.length) {
 294                      errorText = $dark.attr('data-ajax-error-text');
 295                  }
 296              }
 297              phpbb.alert($dark.attr('data-ajax-error-title'), errorText);
 298          }
 299  
 300          /**
 301           * This is a private function used to handle the callbacks, refreshes
 302           * and alert. It calls the callback, refreshes the page if necessary, and
 303           * displays an alert to the user and removes it after an amount of time.
 304           *
 305           * It cannot be called from outside this function, and is purely here to
 306           * avoid repetition of code.
 307           *
 308           * @param {object} res The object sent back by the server.
 309           */
 310  		function returnHandler(res) {
 311              var alert;
 312  
 313              phpbb.clearLoadingTimeout();
 314  
 315              // Is a confirmation required?
 316              if (typeof res.S_CONFIRM_ACTION === 'undefined') {
 317                  // If a confirmation is not required, display an alert and call the
 318                  // callbacks.
 319                  if (typeof res.MESSAGE_TITLE !== 'undefined') {
 320                      alert = phpbb.alert(res.MESSAGE_TITLE, res.MESSAGE_TEXT);
 321                  } else {
 322                      $dark.fadeOut(phpbb.alertTime);
 323  
 324                      if ($loadingIndicator) {
 325                          $loadingIndicator.fadeOut(phpbb.alertTime);
 326                      }
 327                  }
 328  
 329                  if (typeof phpbb.ajaxCallbacks[callback] === 'function') {
 330                      phpbb.ajaxCallbacks[callback].call(that, res);
 331                  }
 332  
 333                  // If the server says to refresh the page, check whether the page should
 334                  // be refreshed and refresh page after specified time if required.
 335                  if (res.REFRESH_DATA) {
 336                      if (typeof refresh === 'function') {
 337                          refresh = refresh(res.REFRESH_DATA.url);
 338                      } else if (typeof refresh !== 'boolean') {
 339                          refresh = false;
 340                      }
 341  
 342                      phpbbAlertTimer = setTimeout(function() {
 343                          if (refresh) {
 344                              window.location = res.REFRESH_DATA.url;
 345                          }
 346  
 347                          // Hide the alert even if we refresh the page, in case the user
 348                          // presses the back button.
 349                          $dark.fadeOut(phpbb.alertTime, function() {
 350                              if (typeof alert !== 'undefined') {
 351                                  alert.hide();
 352                              }
 353                          });
 354                      }, res.REFRESH_DATA.time * 1000); // Server specifies time in seconds
 355                  }
 356              } else {
 357                  // If confirmation is required, display a dialog to the user.
 358                  phpbb.confirm(res.MESSAGE_BODY, function(del) {
 359                      if (!del) {
 360                          return;
 361                      }
 362  
 363                      phpbb.loadingIndicator();
 364                      data =  $('<form>' + res.S_HIDDEN_FIELDS + '</form>').serialize();
 365                      $.ajax({
 366                          url: res.S_CONFIRM_ACTION,
 367                          type: 'POST',
 368                          data: data + '&confirm=' + res.YES_VALUE + '&' + $('form', '#phpbb_confirm').serialize(),
 369                          success: returnHandler,
 370                          error: errorHandler
 371                      });
 372                  }, false);
 373              }
 374          }
 375  
 376          // If the element is a form, POST must be used and some extra data must
 377          // be taken from the form.
 378          var runFilter = (typeof options.filter === 'function');
 379          data = {};
 380  
 381          if (isForm) {
 382              action = $this.attr('action').replace('&amp;', '&');
 383              data = $this.serializeArray();
 384              method = $this.attr('method') || 'GET';
 385  
 386              if ($this.find('input[type="submit"][data-clicked]')) {
 387                  submit = $this.find('input[type="submit"][data-clicked]');
 388                  data.push({
 389                      name: submit.attr('name'),
 390                      value: submit.val()
 391                  });
 392              }
 393          } else if (isText) {
 394              var name = $this.attr('data-name') || this.name;
 395              action = $this.attr('data-url').replace('&amp;', '&');
 396              data[name] = this.value;
 397              method = 'POST';
 398          } else {
 399              action = this.href;
 400              data = null;
 401              method = 'GET';
 402          }
 403  
 404          var sendRequest = function() {
 405              var dataOverlay = $this.attr('data-overlay');
 406              if (overlay && (typeof dataOverlay === 'undefined' || dataOverlay === 'true')) {
 407                  phpbb.loadingIndicator();
 408              }
 409  
 410              var request = $.ajax({
 411                  url: action,
 412                  type: method,
 413                  data: data,
 414                  success: returnHandler,
 415                  error: errorHandler,
 416                  cache: false
 417              });
 418  
 419              request.always(function() {
 420                  if ($loadingIndicator && $loadingIndicator.is(':visible')) {
 421                      $loadingIndicator.fadeOut(phpbb.alertTime);
 422                  }
 423              });
 424          };
 425  
 426          // If filter function returns false, cancel the AJAX functionality,
 427          // and return true (meaning that the HTTP request will be sent normally).
 428          if (runFilter && !options.filter.call(this, data, event, sendRequest)) {
 429              return;
 430          }
 431  
 432          sendRequest();
 433          event.preventDefault();
 434      });
 435  
 436      if (isForm) {
 437          $elements.find('input:submit').click(function () {
 438              var $this = $(this);
 439  
 440              // Remove data-clicked attribute from any submit button of form
 441              $this.parents('form:first').find('input:submit[data-clicked]').removeAttr('data-clicked');
 442  
 443              $this.attr('data-clicked', 'true');
 444          });
 445      }
 446  
 447      return this;
 448  };
 449  
 450  phpbb.search = {
 451      cache: {
 452          data: []
 453      },
 454      tpl: [],
 455      container: []
 456  };
 457  
 458  /**
 459   * Get cached search data.
 460   *
 461   * @param {string} id Search ID.
 462   * @returns {bool|object} Cached data object. Returns false if no data exists.
 463   */
 464  phpbb.search.cache.get = function(id) {
 465      if (this.data[id]) {
 466          return this.data[id];
 467      }
 468      return false;
 469  };
 470  
 471  /**
 472   * Set search cache data value.
 473   *
 474   * @param {string} id        Search ID.
 475   * @param {string} key        Data key.
 476   * @param {string} value    Data value.
 477   */
 478  phpbb.search.cache.set = function(id, key, value) {
 479      if (!this.data[id]) {
 480          this.data[id] = { results: [] };
 481      }
 482      this.data[id][key] = value;
 483  };
 484  
 485  /**
 486   * Cache search result.
 487   *
 488   * @param {string} id        Search ID.
 489   * @param {string} keyword    Keyword.
 490   * @param {Array} results    Search results.
 491   */
 492  phpbb.search.cache.setResults = function(id, keyword, results) {
 493      this.data[id].results[keyword] = results;
 494  };
 495  
 496  /**
 497   * Trim spaces from keyword and lower its case.
 498   *
 499   * @param {string} keyword Search keyword to clean.
 500   * @returns {string} Cleaned string.
 501   */
 502  phpbb.search.cleanKeyword = function(keyword) {
 503      return $.trim(keyword).toLowerCase();
 504  };
 505  
 506  /**
 507   * Get clean version of search keyword. If textarea supports several keywords
 508   * (one per line), it fetches the current keyword based on the caret position.
 509   *
 510   * @param {jQuery} $input    Search input|textarea.
 511   * @param {string} keyword    Input|textarea value.
 512   * @param {bool} multiline    Whether textarea supports multiple search keywords.
 513   *
 514   * @returns string Clean string.
 515   */
 516  phpbb.search.getKeyword = function($input, keyword, multiline) {
 517      if (multiline) {
 518          var line = phpbb.search.getKeywordLine($input);
 519          keyword = keyword.split('\n').splice(line, 1);
 520      }
 521      return phpbb.search.cleanKeyword(keyword);
 522  };
 523  
 524  /**
 525   * Get the textarea line number on which the keyword resides - for textareas
 526   * that support multiple keywords (one per line).
 527   *
 528   * @param {jQuery} $textarea Search textarea.
 529   * @returns {int} The line number.
 530   */
 531  phpbb.search.getKeywordLine = function ($textarea) {
 532      var selectionStart = $textarea.get(0).selectionStart;
 533      return $textarea.val().substr(0, selectionStart).split('\n').length - 1;
 534  };
 535  
 536  /**
 537   * Set the value on the input|textarea. If textarea supports multiple
 538   * keywords, only the active keyword is replaced.
 539   *
 540   * @param {jQuery} $input    Search input|textarea.
 541   * @param {string} value    Value to set.
 542   * @param {bool} multiline    Whether textarea supports multiple search keywords.
 543   */
 544  phpbb.search.setValue = function($input, value, multiline) {
 545      if (multiline) {
 546          var line = phpbb.search.getKeywordLine($input),
 547              lines = $input.val().split('\n');
 548          lines[line] = value;
 549          value = lines.join('\n');
 550      }
 551      $input.val(value);
 552  };
 553  
 554  /**
 555   * Sets the onclick event to set the value on the input|textarea to the
 556   * selected search result.
 557   *
 558   * @param {jQuery} $input        Search input|textarea.
 559   * @param {object} value        Result object.
 560   * @param {jQuery} $row            Result element.
 561   * @param {jQuery} $container    jQuery object for the search container.
 562   */
 563  phpbb.search.setValueOnClick = function($input, value, $row, $container) {
 564      $row.click(function() {
 565          phpbb.search.setValue($input, value.result, $input.attr('data-multiline'));
 566          phpbb.search.closeResults($input, $container);
 567      });
 568  };
 569  
 570  /**
 571   * Runs before the AJAX search request is sent and determines whether
 572   * there is a need to contact the server. If there are cached results
 573   * already, those are displayed instead. Executes the AJAX request function
 574   * itself due to the need to use a timeout to limit the number of requests.
 575   *
 576   * @param {Array} data                Data to be sent to the server.
 577   * @param {object} event            Onkeyup event object.
 578   * @param {function} sendRequest    Function to execute AJAX request.
 579   *
 580   * @returns {boolean} Returns false.
 581   */
 582  phpbb.search.filter = function(data, event, sendRequest) {
 583      var $this = $(this),
 584          dataName = ($this.attr('data-name') !== undefined) ? $this.attr('data-name') : $this.attr('name'),
 585          minLength = parseInt($this.attr('data-min-length'), 10),
 586          searchID = $this.attr('data-results'),
 587          keyword = phpbb.search.getKeyword($this, data[dataName], $this.attr('data-multiline')),
 588          cache = phpbb.search.cache.get(searchID),
 589          key = event.keyCode || event.which,
 590          proceed = true;
 591      data[dataName] = keyword;
 592  
 593      // No need to search if enter was pressed
 594      // for selecting a value from the results.
 595      if (key === keymap.ENTER) {
 596          return false;
 597      }
 598  
 599      if (cache.timeout) {
 600          clearTimeout(cache.timeout);
 601      }
 602  
 603      var timeout = setTimeout(function() {
 604          // Check min length and existence of cache.
 605          if (minLength > keyword.length) {
 606              proceed = false;
 607          } else if (cache.lastSearch) {
 608              // Has the keyword actually changed?
 609              if (cache.lastSearch === keyword) {
 610                  proceed = false;
 611              } else {
 612                  // Do we already have results for this?
 613                  if (cache.results[keyword]) {
 614                      var response = {
 615                          keyword: keyword,
 616                          results: cache.results[keyword]
 617                      };
 618                      phpbb.search.handleResponse(response, $this, true);
 619                      proceed = false;
 620                  }
 621  
 622                  // If the previous search didn't yield results and the string only had characters added to it,
 623                  // then we won't bother sending a request.
 624                  if (keyword.indexOf(cache.lastSearch) === 0 && cache.results[cache.lastSearch].length === 0) {
 625                      phpbb.search.cache.set(searchID, 'lastSearch', keyword);
 626                      phpbb.search.cache.setResults(searchID, keyword, []);
 627                      proceed = false;
 628                  }
 629              }
 630          }
 631  
 632          if (proceed) {
 633              sendRequest.call(this);
 634          }
 635      }, 350);
 636      phpbb.search.cache.set(searchID, 'timeout', timeout);
 637  
 638      return false;
 639  };
 640  
 641  /**
 642   * Handle search result response.
 643   *
 644   * @param {object} res            Data received from server.
 645   * @param {jQuery} $input        Search input|textarea.
 646   * @param {bool} fromCache        Whether the results are from the cache.
 647   * @param {function} callback    Optional callback to run when assigning each search result.
 648   */
 649  phpbb.search.handleResponse = function(res, $input, fromCache, callback) {
 650      if (typeof res !== 'object') {
 651          return;
 652      }
 653  
 654      var searchID = $input.attr('data-results'),
 655          $container = $(searchID);
 656  
 657      if (this.cache.get(searchID).callback) {
 658          callback = this.cache.get(searchID).callback;
 659      } else if (typeof callback === 'function') {
 660          this.cache.set(searchID, 'callback', callback);
 661      }
 662  
 663      if (!fromCache) {
 664          this.cache.setResults(searchID, res.keyword, res.results);
 665      }
 666  
 667      this.cache.set(searchID, 'lastSearch', res.keyword);
 668      this.showResults(res.results, $input, $container, callback);
 669  };
 670  
 671  /**
 672   * Show search results.
 673   *
 674   * @param {Array} results        Search results.
 675   * @param {jQuery} $input        Search input|textarea.
 676   * @param {jQuery} $container    Search results container element.
 677   * @param {function} callback    Optional callback to run when assigning each search result.
 678   */
 679  phpbb.search.showResults = function(results, $input, $container, callback) {
 680      var $resultContainer = $('.search-results', $container);
 681      this.clearResults($resultContainer);
 682  
 683      if (!results.length) {
 684          $container.hide();
 685          return;
 686      }
 687  
 688      var searchID = $container.attr('id'),
 689          tpl,
 690          row;
 691  
 692      if (!this.tpl[searchID]) {
 693          tpl = $('.search-result-tpl', $container);
 694          this.tpl[searchID] = tpl.clone().removeClass('search-result-tpl');
 695          tpl.remove();
 696      }
 697      tpl = this.tpl[searchID];
 698  
 699      $.each(results, function(i, item) {
 700          row = tpl.clone();
 701          row.find('.search-result').html(item.display);
 702  
 703          if (typeof callback === 'function') {
 704              callback.call(this, $input, item, row, $container);
 705          }
 706          row.appendTo($resultContainer).show();
 707      });
 708      $container.show();
 709  
 710      phpbb.search.navigateResults($input, $container, $resultContainer);
 711  };
 712  
 713  /**
 714   * Clear search results.
 715   *
 716   * @param {jQuery} $container        Search results container.
 717   */
 718  phpbb.search.clearResults = function($container) {
 719      $container.children(':not(.search-result-tpl)').remove();
 720  };
 721  
 722  /**
 723   * Close search results.
 724   *
 725   * @param {jQuery} $input            Search input|textarea.
 726   * @param {jQuery} $container        Search results container.
 727   */
 728  phpbb.search.closeResults = function($input, $container) {
 729      $input.off('.phpbb.search');
 730      $container.hide();
 731  };
 732  
 733  /**
 734   * Navigate search results.
 735   *
 736   * @param {jQuery} $input            Search input|textarea.
 737   * @param {jQuery} $container        Search results container.
 738   * @param {jQuery} $resultContainer    Search results list container.
 739   */
 740  phpbb.search.navigateResults = function($input, $container, $resultContainer) {
 741      // Add a namespace to the event (.phpbb.search),
 742      // so it can be unbound specifically later on.
 743      // Rebind it, to ensure the event is 'dynamic'.
 744      $input.off('.phpbb.search');
 745      $input.on('keydown.phpbb.search', function(event) {
 746          var key = event.keyCode || event.which,
 747              $active = $resultContainer.children('.active');
 748  
 749          switch (key) {
 750              // Close the results
 751              case keymap.ESC:
 752                  phpbb.search.closeResults($input, $container);
 753              break;
 754  
 755              // Set the value for the selected result
 756              case keymap.ENTER:
 757                  if ($active.length) {
 758                      var value = $active.find('.search-result > span').text();
 759  
 760                      phpbb.search.setValue($input, value, $input.attr('data-multiline'));
 761                  }
 762  
 763                  phpbb.search.closeResults($input, $container);
 764  
 765                  // Do not submit the form
 766                  event.preventDefault();
 767              break;
 768  
 769              // Navigate the results
 770              case keymap.ARROW_DOWN:
 771              case keymap.ARROW_UP:
 772                  var up = key === keymap.ARROW_UP,
 773                      $children = $resultContainer.children();
 774  
 775                  if (!$active.length) {
 776                      if (up) {
 777                          $children.last().addClass('active');
 778                      } else {
 779                          $children.first().addClass('active');
 780                      }
 781                  } else if ($children.length > 1) {
 782                      if (up) {
 783                          if ($active.is(':first-child')) {
 784                              $children.last().addClass('active');
 785                          } else {
 786                              $active.prev().addClass('active');
 787                          }
 788                      } else {
 789                          if ($active.is(':last-child')) {
 790                              $children.first().addClass('active');
 791                          } else {
 792                              $active.next().addClass('active');
 793                          }
 794                      }
 795  
 796                      $active.removeClass('active');
 797                  }
 798  
 799                  // Do not change cursor position in the input element
 800                  event.preventDefault();
 801              break;
 802          }
 803      });
 804  };
 805  
 806  $('#phpbb').click(function() {
 807      var $this = $(this);
 808  
 809      if (!$this.is('.live-search') && !$this.parents().is('.live-search')) {
 810          phpbb.search.closeResults($('input, textarea'), $('.live-search'));
 811      }
 812  });
 813  
 814  phpbb.history = {};
 815  
 816  /**
 817  * Check whether a method in the native history object is supported.
 818  *
 819  * @param {string} fn Method name.
 820  * @returns {bool} Returns true if the method is supported.
 821  */
 822  phpbb.history.isSupported = function(fn) {
 823      return !(typeof history === 'undefined' || typeof history[fn] === 'undefined');
 824  };
 825  
 826  /**
 827  * Wrapper for the pushState and replaceState methods of the
 828  * native history object.
 829  *
 830  * @param {string} mode        Mode. Either push or replace.
 831  * @param {string} url        New URL.
 832  * @param {string} [title]    Optional page title.
 833  * @param {object} [obj]        Optional state object.
 834  */
 835  phpbb.history.alterUrl = function(mode, url, title, obj) {
 836      var fn = mode + 'State';
 837  
 838      if (!url || !phpbb.history.isSupported(fn)) {
 839          return;
 840      }
 841      if (!title) {
 842          title = document.title;
 843      }
 844      if (!obj) {
 845          obj = null;
 846      }
 847  
 848      history[fn](obj, title, url);
 849  };
 850  
 851  /**
 852  * Wrapper for the native history.replaceState method.
 853  *
 854  * @param {string} url        New URL.
 855  * @param {string} [title]    Optional page title.
 856  * @param {object} [obj]        Optional state object.
 857  */
 858  phpbb.history.replaceUrl = function(url, title, obj) {
 859      phpbb.history.alterUrl('replace', url, title, obj);
 860  };
 861  
 862  /**
 863  * Wrapper for the native history.pushState method.
 864  *
 865  * @param {string} url        New URL.
 866  * @param {string} [title]    Optional page title.
 867  * @param {object} [obj]        Optional state object.
 868  */
 869  phpbb.history.pushUrl = function(url, title, obj) {
 870      phpbb.history.alterUrl('push', url, title, obj);
 871  };
 872  
 873  /**
 874  * Hide the optgroups that are not the selected timezone
 875  *
 876  * @param {bool} keepSelection Shall we keep the value selected, or shall the
 877  *     user be forced to repick one.
 878  */
 879  phpbb.timezoneSwitchDate = function(keepSelection) {
 880      var $timezoneCopy = $('#timezone_copy');
 881      var $timezone = $('#timezone');
 882      var $tzDate = $('#tz_date');
 883      var $tzSelectDateSuggest = $('#tz_select_date_suggest');
 884  
 885      if ($timezoneCopy.length === 0) {
 886          // We make a backup of the original dropdown, so we can remove optgroups
 887          // instead of setting display to none, because IE and chrome will not
 888          // hide options inside of optgroups and selects via css
 889          $timezone.clone()
 890              .attr('id', 'timezone_copy')
 891              .css('display', 'none')
 892              .attr('name', 'tz_copy')
 893              .insertAfter('#timezone');
 894      } else {
 895          // Copy the content of our backup, so we can remove all unneeded options
 896          $timezone.html($timezoneCopy.html());
 897      }
 898  
 899      if ($tzDate.val() !== '') {
 900          $timezone.children('optgroup').remove(':not([data-tz-value="' + $tzDate.val() + '"])');
 901      }
 902  
 903      if ($tzDate.val() === $tzSelectDateSuggest.attr('data-suggested-tz')) {
 904          $tzSelectDateSuggest.css('display', 'none');
 905      } else {
 906          $tzSelectDateSuggest.css('display', 'inline');
 907      }
 908  
 909      var $tzOptions = $timezone.children('optgroup[data-tz-value="' + $tzDate.val() + '"]').children('option');
 910  
 911      if ($tzOptions.length === 1) {
 912          // If there is only one timezone for the selected date, we just select that automatically.
 913          $tzOptions.prop('selected', true);
 914          keepSelection = true;
 915      }
 916  
 917      if (typeof keepSelection !== 'undefined' && !keepSelection) {
 918          var $timezoneOptions = $timezone.find('optgroup option');
 919          if ($timezoneOptions.filter(':selected').length <= 0) {
 920              $timezoneOptions.filter(':first').prop('selected', true);
 921          }
 922      }
 923  };
 924  
 925  /**
 926  * Display the date/time select
 927  */
 928  phpbb.timezoneEnableDateSelection = function() {
 929      $('#tz_select_date').css('display', 'block');
 930  };
 931  
 932  /**
 933  * Preselect a date/time or suggest one, if it is not picked.
 934  *
 935  * @param {bool} forceSelector Shall we select the suggestion?
 936  */
 937  phpbb.timezonePreselectSelect = function(forceSelector) {
 938  
 939      // The offset returned here is in minutes and negated.
 940      var offset = (new Date()).getTimezoneOffset();
 941      var sign = '-';
 942  
 943      if (offset < 0) {
 944          sign = '+';
 945          offset = -offset;
 946      }
 947  
 948      var minutes = offset % 60;
 949      var hours = (offset - minutes) / 60;
 950  
 951      if (hours === 0) {
 952          hours = '00';
 953          sign = '+';
 954      } else if (hours < 10) {
 955          hours = '0' + hours.toString();
 956      } else {
 957          hours = hours.toString();
 958      }
 959  
 960      if (minutes < 10) {
 961          minutes = '0' + minutes.toString();
 962      } else {
 963          minutes = minutes.toString();
 964      }
 965  
 966      var prefix = 'UTC' + sign + hours + ':' + minutes;
 967      var prefixLength = prefix.length;
 968      var selectorOptions = $('option', '#tz_date');
 969      var i;
 970  
 971      var $tzSelectDateSuggest = $('#tz_select_date_suggest');
 972  
 973      for (i = 0; i < selectorOptions.length; ++i) {
 974          var option = selectorOptions[i];
 975  
 976          if (option.value.substring(0, prefixLength) === prefix) {
 977              if ($('#tz_date').val() !== option.value && !forceSelector) {
 978                  // We do not select the option for the user, but notify him,
 979                  // that we would suggest a different setting.
 980                  phpbb.timezoneSwitchDate(true);
 981                  $tzSelectDateSuggest.css('display', 'inline');
 982              } else {
 983                  option.selected = true;
 984                  phpbb.timezoneSwitchDate(!forceSelector);
 985                  $tzSelectDateSuggest.css('display', 'none');
 986              }
 987  
 988              var suggestion = $tzSelectDateSuggest.attr('data-l-suggestion');
 989  
 990              $tzSelectDateSuggest.attr('title', suggestion.replace('%s', option.innerHTML));
 991              $tzSelectDateSuggest.attr('value', suggestion.replace('%s', option.innerHTML.substring(0, 9)));
 992              $tzSelectDateSuggest.attr('data-suggested-tz', option.innerHTML);
 993  
 994              // Found the suggestion, there cannot be more, so return from here.
 995              return;
 996          }
 997      }
 998  };
 999  
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


Generated: Mon Nov 25 19:05:08 2024 Cross-referenced by PHPXref 0.7.1