Textpattern PHP Cross Reference Content Management Systems

Source: /textpattern/textpattern.js - 2720 lines - 82402 bytes - Summary - Text - Print

Description: Collection of client-side tools.

   1  /* eslint-env jquery */
   2  
   3  /*
   4   * Textpattern Content Management System
   5   * https://textpattern.com/
   6   *
   7   * Copyright (C) 2020 The Textpattern Development Team
   8   *
   9   * This file is part of Textpattern.
  10   *
  11   * Textpattern is free software; you can redistribute it and/or
  12   * modify it under the terms of the GNU General Public License
  13   * as published by the Free Software Foundation, version 2.
  14   *
  15   * Textpattern is distributed in the hope that it will be useful,
  16   * but WITHOUT ANY WARRANTY; without even the implied warranty of
  17   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  18   * GNU General Public License for more details.
  19   *
  20   * You should have received a copy of the GNU General Public License
  21   * along with Textpattern. If not, see <https://www.gnu.org/licenses/>.
  22   */
  23  
  24  'use strict';
  25  
  26  /**
  27   * Collection of client-side tools.
  28   */
  29  
  30  textpattern.version = '4.8.4';
  31  
  32  /**
  33   * Ascertain the page direction (LTR or RTL) as a variable.
  34   */
  35  
  36  var langdir = document.documentElement.dir,
  37      dir = langdir === 'rtl' ? 'left' : 'right';
  38  
  39  /**
  40   * Checks if HTTP cookies are enabled.
  41   *
  42   * @return {boolean}
  43   */
  44  
  45  function checkCookies()
  46  {
  47      cookieEnabled = navigator.cookieEnabled && (document.cookie.indexOf('txp_test_cookie') >= 0 || document.cookie.indexOf('txp_login') >= 0);
  48  
  49      if (!cookieEnabled) {
  50          textpattern.Console.addMessage([textpattern.gTxt('cookies_must_be_enabled'), 1]);
  51      } else {
  52          document.cookie = 'txp_test_cookie=; Max-Age=0; SameSite=Lax';
  53      }
  54  }
  55  
  56  /**
  57   * Basic confirmation for potentially powerful choices (like deletion,
  58   * for example).
  59   *
  60   * @param  {string}  msg The message
  61   * @return {boolean} TRUE if user confirmed the action
  62   */
  63  
  64  function verify(msg)
  65  {
  66      return confirm(msg);
  67  }
  68  
  69  /**
  70   * Multi-edit functions.
  71   *
  72   * @param  {string|object} method Called method, or options
  73   * @param  {object}        opt    Options if method is a method
  74   * @return {object}        this
  75   * @since  4.5.0
  76   */
  77  
  78  jQuery.fn.txpMultiEditForm = function (method, opt) {
  79      var args = {};
  80  
  81      var defaults = {
  82          'checkbox'     : 'input[name="selected[]"][type=checkbox]',
  83          'row'          : 'tbody td',
  84          'highlighted'  : 'tr',
  85          'filteredClass': 'filtered',
  86          'selectedClass': 'selected',
  87          'actions'      : 'select[name=edit_method]',
  88          'submitButton' : '.multi-edit input[type=submit]',
  89          'selectAll'    : 'input[name=select_all][type=checkbox]',
  90          'rowClick'     : true,
  91          'altClick'     : true,
  92          'confirmation' : textpattern.gTxt('are_you_sure')
  93      };
  94  
  95      if ($.type(method) !== 'string') {
  96          opt = method;
  97          method = null;
  98      } else {
  99          args = opt;
 100      }
 101  
 102      this./*closest('form').*/each(function () {
 103          var $this = $(this), form = {}, methods = {}, lib = {};
 104  
 105          if ($this.data('_txpMultiEdit')) {
 106              form = $this.data('_txpMultiEdit');
 107              opt = $.extend(form.opt, opt);
 108          } else {
 109              opt = $.extend(defaults, opt);
 110              form.editMethod = $this.find(opt.actions);
 111              form.lastCheck = null;
 112              form.opt = opt;
 113              form.selectAll = $this.find(opt.selectAll);
 114              form.button = $this.find(opt.submitButton);
 115          }
 116  
 117          form.boxes = $this.find(opt.checkbox);
 118  
 119          /**
 120           * Registers a multi-edit option.
 121           *
 122           * @param  {object} options
 123           * @param  {string} options.label The option's label
 124           * @param  {string} options.value The option's value
 125           * @param  {string} options.html  The second step HTML
 126           * @return {object} methods
 127           */
 128  
 129          methods.addOption = function (options) {
 130              var settings = $.extend({
 131                  'label': null,
 132                  'value': null,
 133                  'html' : null
 134              }, options);
 135  
 136              if (!settings.value) {
 137                  return methods;
 138              }
 139  
 140              var option = form.editMethod.find('option').filter(function () {
 141                  return $(this).val() === settings.value;
 142              });
 143  
 144              var exists = (option.length > 0);
 145              form.editMethod.val('');
 146  
 147              if (!exists) {
 148                  option = $('<option />');
 149              }
 150  
 151              if (!option.data('_txpMultiMethod')) {
 152                  if (!option.val()) {
 153                      option.val(settings.value);
 154                  }
 155  
 156                  if (!option.text() && settings.label) {
 157                      option.text(settings.label);
 158                  }
 159  
 160                  option.data('_txpMultiMethod', settings.html);
 161              }
 162  
 163              if (!exists) {
 164                  form.editMethod.append(option);
 165              }
 166  
 167              return methods;
 168          };
 169  
 170          /**
 171           * Selects rows based on supplied arguments.
 172           *
 173           * Only one of the filters applies at a time.
 174           *
 175           * @param  {object}  options
 176           * @param  {array}   options.index   Indexes to select
 177           * @param  {array}   options.range   Select index range, takes [min, max]
 178           * @param  {array}   options.value   Values to select
 179           * @param  {boolean} options.checked TRUE to check, FALSE to uncheck
 180           * @return {object}  methods
 181           */
 182  
 183          methods.select = function (options) {
 184              var settings = $.extend({
 185                  'index'  : null,
 186                  'range'  : null,
 187                  'value'  : null,
 188                  'checked': true
 189              }, options);
 190  
 191              var obj = form.boxes;//$this.find(opt.checkbox);
 192  
 193              if (settings.value !== null) {
 194                  obj = obj.filter(function () {
 195                      return $.inArray($(this).val(), settings.value) !== -1;
 196                  });
 197              } else if (settings.index !== null) {
 198                  obj = obj.filter(function (index) {
 199                      return $.inArray(index, settings.index) !== -1;
 200                  });
 201              } else if (settings.range !== null) {
 202                  obj = obj.slice(settings.range[0], settings.range[1]);
 203              }
 204  
 205              obj.filter(settings.checked ? ':not(:checked)' : ':checked').prop('checked', settings.checked).change();
 206              lib.highlight();
 207  
 208              return methods;
 209          };
 210  
 211          /**
 212           * Highlights selected rows.
 213           *
 214           * @return {object} lib
 215           */
 216  
 217          lib.highlight = function () {
 218              var checked = form.boxes.filter(':checked'), count = checked.length,
 219                  option = form.editMethod.find('[value=""]');
 220              checked.closest(opt.highlighted).addClass(opt.selectedClass);
 221              form.boxes.filter(':not(:checked)').closest(opt.highlighted).removeClass(opt.selectedClass);
 222  
 223              option.gTxt('with_selected_option', {
 224                  '{count}': count
 225              });
 226              form.selectAll.prop('checked', count === form.boxes.length).change();
 227              form.editMethod.prop('disabled', !count);
 228  
 229              if (!count) {
 230                  form.editMethod.val('').change();
 231              }
 232  
 233              return lib;
 234          };
 235  
 236          /**
 237           * Extends click region to whole row.
 238           *
 239           * @return {object} lib
 240           */
 241  
 242          lib.extendedClick = function () {
 243              var selector = opt.rowClick ? opt.row : opt.checkbox;
 244  
 245              $this.on('click', selector, function (e) {
 246                  var self = ($(e.target).is(opt.checkbox) || $(this).is(opt.checkbox));
 247  
 248                  if (!self && (e.target != this || $(this).is('a, :input') || $(e.target).is('a, :input'))) {
 249                      return;
 250                  }
 251  
 252                  if (!self && opt.altClick && !e.altKey && !e.ctrlKey) {
 253                      return;
 254                  }
 255  
 256                  var box = $(this).closest(opt.highlighted).find(opt.checkbox);
 257  
 258                  if (box.length < 1) {
 259                      return;
 260                  }
 261  
 262                  var checked = box.prop('checked');
 263  
 264                  if (self) {
 265                      checked = !checked;
 266                  }
 267  
 268                  if (e.shiftKey && form.lastCheck) {
 269                      var boxes = form.boxes;
 270                      var start = boxes.index(box);
 271                      var end = boxes.index(form.lastCheck);
 272  
 273                      methods.select({
 274                          'range'  : [Math.min(start, end), Math.max(start, end) + 1],
 275                          'checked': !checked
 276                      });
 277                  } else if (!self) {
 278                      box.prop('checked', !checked).change();
 279                  }
 280  
 281                  form.lastCheck = box;
 282              });
 283  
 284              return lib;
 285          };
 286  
 287          /**
 288           * Tracks row checks.
 289           *
 290           * @return {object} lib
 291           */
 292  
 293          lib.checked = function () {
 294              $this.on('change', opt.checkbox, function (e) {
 295                  var box = $(this);
 296  
 297                  if (box.prop('checked')) {
 298                      if (-1 == $.inArray(box.val(), textpattern.Relay.data.selected)) {
 299                          textpattern.Relay.data.selected.push(box.val());
 300                      }
 301                  } else {
 302                      textpattern.Relay.data.selected = $.grep(textpattern.Relay.data.selected, function(value) {
 303                          return value != box.val();
 304                      });
 305                  }
 306  
 307                  if (typeof(e.originalEvent) != 'undefined') {
 308                      lib.highlight();
 309                  }
 310              });
 311  
 312              return lib;
 313          };
 314  
 315          /**
 316           * Handles edit method selecting.
 317           *
 318           * @return {object} lib
 319           */
 320  
 321          lib.changeMethod = function () {
 322              form.button.hide();
 323  
 324              form.editMethod.val('').change(function (e) {
 325                  var selected = $(this).find('option:selected');
 326                  $this.find('.multi-step').remove();
 327  
 328                  if (selected.length < 1 || selected.val() === '') {
 329                      form.button.hide();
 330                      return lib;
 331                  }
 332  
 333                  if (selected.data('_txpMultiMethod')) {
 334                      $(this).after($('<div />').attr('class', 'multi-step multi-option').html(selected.data('_txpMultiMethod')));
 335                      form.button.show();
 336                  } else {
 337                      form.button.hide();
 338                      $(this).parents('form').submit();
 339                  }
 340              });
 341  
 342              return lib;
 343          };
 344  
 345          /**
 346           * Handles sending.
 347           *
 348           * @return {object} lib
 349           */
 350  
 351          lib.sendForm = function () {
 352              $this.submit(function () {
 353                  if (opt.confirmation !== false && verify(opt.confirmation) === false) {
 354                      form.editMethod.val('').change();
 355  
 356                      return false;
 357                  }
 358              });
 359  
 360              return lib;
 361          };
 362  
 363          if (!$this.data('_txpMultiEdit')) {
 364              lib.highlight().extendedClick().checked().changeMethod().sendForm();
 365  
 366              (function () {
 367                  var multiOptions = $this.find('.multi-option:not(.multi-step)');
 368  
 369                  form.editMethod.find('option[value!=""]').each(function () {
 370                      var value = $(this).val();
 371  
 372                      var option = multiOptions.filter(function () {
 373                          return $(this).data('multi-option') === value;
 374                      });
 375  
 376                      if (option.length > 0) {
 377                          methods.addOption({
 378                              'label': null,
 379                              'html' : option.eq(0).contents(),
 380                              'value': $(this).val()
 381                          });
 382                      }
 383                  });
 384  
 385                  multiOptions.remove();
 386              })();
 387  
 388              form.selectAll.on('change', function (e) {
 389                  if (typeof(e.originalEvent) != 'undefined') {
 390                      methods.select({
 391                          'checked': $(this).prop('checked')
 392                      });
 393                  }
 394  
 395                  $this.toggleClass(opt.filteredClass, !$(this).prop('checked'));
 396              }).change();
 397          }
 398  
 399          if (method && methods[method]) {
 400              methods[method].call($this, args);
 401          }
 402  
 403          $this.data('_txpMultiEdit', form);
 404      });
 405  
 406      return this;
 407  };
 408  
 409  /**
 410   * Sets a HTTP cookie.
 411   *
 412   * @param {string}  name  The name
 413   * @param {string}  value The value
 414   * @param {integer} days  Expires in
 415   */
 416  
 417  function setCookie(name, value, days)
 418  {
 419      var expires = '';
 420  
 421      if (days) {
 422          var date = new Date();
 423  
 424          date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
 425  
 426          expires = '; expires=' + date.toGMTString();
 427      }
 428  
 429      document.cookie = name + '=' + value + expires + '; path=/; SameSite=Lax';
 430  }
 431  
 432  /**
 433   * Gets a HTTP cookie's value.
 434   *
 435   * @param  {string} name The name
 436   * @return {string} The cookie
 437   */
 438  
 439  function getCookie(name)
 440  {
 441      var nameEQ = name + '=';
 442      var ca = document.cookie.split(';');
 443  
 444      for (var i = 0; i < ca.length; i++) {
 445          var c = ca[i];
 446  
 447          while (c.charAt(0) == ' ') {
 448              c = c.substring(1, c.length);
 449          }
 450  
 451          if (c.indexOf(nameEQ) == 0) {
 452              return c.substring(nameEQ.length, c.length);
 453          }
 454      }
 455  
 456      return null;
 457  }
 458  
 459  /**
 460   * Deletes a HTTP cookie.
 461   *
 462   * @param {string} name The cookie
 463   */
 464  
 465  function deleteCookie(name)
 466  {
 467      setCookie(name, '', -1);
 468  }
 469  
 470  /**
 471   * Toggles column's visibility and saves the state.
 472   *
 473   * @param  {string}  sel The column selector object
 474   * @return {boolean} Returns FALSE
 475   * @since  4.7.0
 476   */
 477  
 478  function toggleColumn(sel, $sel, vis)
 479  {
 480  //    $sel = $(sel);
 481      if ($sel.length) {
 482          $sel.toggle(!!vis);
 483  
 484          // Send state of toggle pane to localStorage.
 485          var data = new Object;
 486  
 487          data[textpattern.event] = {'columns':{}};
 488          data[textpattern.event]['columns'][sel] = !!vis ? null : false;
 489          textpattern.storage.update(data);
 490      }
 491  
 492      return false;
 493  }
 494  
 495  /**
 496   * Toggles panel's visibility and saves the state.
 497   *
 498   * @param  {string}  id The element ID
 499   * @return {boolean} Returns FALSE
 500   */
 501  
 502  function toggleDisplay(id)
 503  {
 504      var obj = $('#' + id);
 505  
 506      if (obj.length) {
 507          obj.toggle();
 508  
 509          // Send state of toggle pane to localStorage.
 510          var pane = $(this).data('txp-pane') || obj.attr('id');
 511          var data = new Object;
 512  
 513          data[textpattern.event] = {'panes':{}};
 514          data[textpattern.event]['panes'][pane] = obj.is(':visible') ? true : null;
 515          textpattern.storage.update(data);
 516      }
 517  
 518      return false;
 519  }
 520  
 521  /**
 522   * Direct show/hide referred #segment; decorate parent lever.
 523   */
 524  
 525  function toggleDisplayHref()
 526  {
 527      var $this = $(this);
 528      var href = $this.attr('href');
 529      var lever = $this.parent('.txp-summary');
 530  
 531      if (href) {
 532          toggleDisplay.call(this, href.substr(1));
 533      }
 534  
 535      if (lever.length) {
 536          var vis = $(href).is(':visible');
 537          lever.toggleClass('expanded', vis);
 538          $this.attr('aria-pressed', vis.toString());
 539          $(href).attr('aria-expanded', vis.toString());
 540      }
 541  
 542      return false;
 543  }
 544  
 545  /**
 546   * Shows/hides matching elements.
 547   *
 548   * @param {string}  className Targeted element's class
 549   * @param {boolean} show      TRUE to display
 550   */
 551  
 552  function setClassDisplay(className, show)
 553  {
 554      $('.' + className).toggle(show);
 555  }
 556  
 557  /**
 558   * Toggles panel's visibility and saves the state to a HTTP cookie.
 559   *
 560   * @param {string} classname The HTML class
 561   */
 562  
 563  function toggleClassRemember(className)
 564  {
 565      var v = getCookie('toggle_' + className);
 566      v = (v == 1 ? 0 : 1);
 567  
 568      setCookie('toggle_' + className, v, 365);
 569      setClassDisplay(className, v);
 570      setClassDisplay(className + '_neg', 1 - v);
 571  }
 572  
 573  /**
 574   * Toggle visibility of matching elements based on a cookie value.
 575   *
 576   * @param {string}  className The HTML class
 577   * @param {string}  force     The value
 578   */
 579  
 580  function setClassRemember(className, force)
 581  {
 582      if (typeof(force) != 'undefined') {
 583          setCookie('toggle_' + className, force, 365);
 584      }
 585  
 586      var v = getCookie('toggle_' + className);
 587      setClassDisplay(className, v);
 588      setClassDisplay(className + '_neg', 1 - v);
 589  }
 590  
 591  /**
 592   * Load data from the server using a HTTP POST request.
 593   *
 594   * @param  {object} data   POST payload
 595   * @param  {object} fn     Success handler
 596   * @param  {string} format Response data format, defaults to 'xml'
 597   * @return {object} this
 598   * @see    https://api.jquery.com/jQuery.post/
 599   */
 600  
 601  function sendAsyncEvent(data, fn, format)
 602  {
 603      var formdata = false;
 604  
 605      if ($.type(data) === 'string' && data.length > 0) {
 606          // Got serialized data.
 607          data = data + '&app_mode=async&_txp_token=' + textpattern._txp_token;
 608      } else if (data instanceof FormData) {
 609          formdata = true;
 610          data.append('app_mode', 'async');
 611          data.append('_txp_token', textpattern._txp_token);
 612      } else {
 613          data.app_mode = 'async';
 614          data._txp_token = textpattern._txp_token;
 615      }
 616  
 617      format = format || 'xml';
 618  
 619      return formdata ?
 620          $.ajax({
 621              type: 'POST',
 622              url: 'index.php',
 623              data: data,
 624              success: fn,
 625              dataType: format,
 626              processData: false,
 627              contentType: false
 628          }) :
 629          $.post('index.php', data, fn, format);
 630  }
 631  
 632  /**
 633   * A pub/sub hub for client-side events.
 634   *
 635   * @since 4.5.0
 636   */
 637  
 638  textpattern.Relay =
 639  {
 640      /**
 641       * Publishes an event to all registered subscribers.
 642       *
 643       * @param  {string} event The event
 644       * @param  {object} data  The data passed to registered subscribers
 645       * @return {object} The Relay object
 646       * @example
 647       * textpattern.Relay.callback('newEvent', {'name1': 'value1', 'name2': 'value2'});
 648       */
 649  
 650      callback: function (event, data, timeout) {
 651          clearTimeout(textpattern.Relay.timeouts[event]);
 652          timeout = !timeout ? 0 : parseInt(timeout, 10);
 653  
 654          if (!timeout || isNaN(timeout)) {
 655              return $(this).trigger(event, data);
 656          }
 657  
 658          textpattern.Relay.timeouts[event] = setTimeout(
 659              $.proxy(function() {
 660                  return textpattern.Relay.callback(event, data);
 661              }, this),
 662              parseInt(timeout, 10)
 663          );
 664      },
 665  
 666      /**
 667       * Subscribes to an event.
 668       *
 669       * @param  {string} The event
 670       * @param  {object} fn  The callback function
 671       * @return {object} The Relay object
 672       * @example
 673       * textpattern.Relay.register('event',
 674       *     function (event, data)
 675       *     {
 676       *         alert(data);
 677       *     }
 678       * );
 679       */
 680  
 681      register: function (event, fn) {
 682          if (fn) {
 683              $(this).on(event, fn);
 684          } else {
 685              $(this).off(event);
 686          }
 687  
 688          return this;
 689      },
 690  
 691      timeouts: {},
 692      data: {selected: []}
 693  };
 694  
 695  /**
 696   * Textpattern localStorage.
 697   *
 698   * @since 4.6.0
 699   */
 700  
 701  textpattern.storage =
 702  {
 703      /**
 704       * Textpattern localStorage data.
 705       */
 706  
 707      data: (!navigator.cookieEnabled || !window.localStorage ? null : JSON.parse(window.localStorage.getItem('textpattern.' + textpattern._txp_uid))) || {},
 708  
 709      /**
 710       * Updates data.
 711       *
 712       * @param   data The message
 713       * @example
 714       * textpattern.update({prefs: 'site'});
 715       */
 716  
 717      update: function (data) {
 718          $.extend(true, textpattern.storage.data, data);
 719          textpattern.storage.clean(textpattern.storage.data);
 720  
 721          if (navigator.cookieEnabled && window.localStorage) {
 722              window.localStorage.setItem('textpattern.' + textpattern._txp_uid, JSON.stringify(textpattern.storage.data));
 723          }
 724      },
 725  
 726      clean: function (obj) {
 727          Object.keys(obj).forEach(function (key) {
 728              if (obj[key] && typeof obj[key] === 'object') {
 729                  textpattern.storage.clean(obj[key]);
 730              } else if (obj[key] === null) {
 731                  delete obj[key];
 732              }
 733          });
 734      }
 735  };
 736  
 737  /**
 738   * Logs debugging messages.
 739   *
 740   * @since 4.6.0
 741   */
 742  
 743  textpattern.Console =
 744  {
 745      /**
 746       * Stores an array of invoked messages.
 747       */
 748  
 749      history: [],
 750  
 751      /**
 752       * Stores an array of messages to announce.
 753       */
 754  
 755      messages: {},
 756  
 757      queue: {},
 758  
 759      /**
 760       * Clear.
 761       *
 762       * @param  {string} The event
 763       * @return textpattern.Console
 764       */
 765  
 766      clear: function (event, reset) {
 767          event = event || textpattern.event;
 768          textpattern.Console.messages[event] = [];
 769  
 770          if (!!reset) {
 771              textpattern.Console.queue[event] = false;
 772          }
 773  
 774          return this;
 775      },
 776  
 777      /**
 778       * Add message to announce.
 779       *
 780       * @param  {string} The event
 781       * @param  {string} The message
 782       * @return textpattern.Console
 783       */
 784  
 785      addMessage: function (message, event) {
 786          event = event || textpattern.event;
 787  
 788          if (typeof textpattern.Console.messages[event] === 'undefined') {
 789              textpattern.Console.messages[event] = [];
 790          }
 791  
 792          textpattern.Console.messages[event].push(message);
 793  
 794          return this;
 795      },
 796  
 797      /**
 798       * Announce.
 799       *
 800       * @param  {string} The event
 801       * @return textpattern.Console
 802       */
 803  
 804      announce: function (event, options) {
 805          event = event || textpattern.event;
 806  
 807          if (!!textpattern.Console.queue[event]) {
 808              return this;
 809          } else {
 810              textpattern.Console.queue[event] = true;
 811          }
 812  
 813          $(document).ready(function() {
 814              var c = 0, message = [], status = 0;
 815  
 816              if (textpattern.Console.messages[event] && textpattern.Console.messages[event].length) {
 817                  var container = textpattern.prefs.message || '{message}';
 818  
 819                  textpattern.Console.messages[event].forEach (function(pair) {
 820                      message.push(textpattern.mustache(container, {
 821                          status: pair[1] != 1 && pair[1] != 2 ? 'check' : 'alert',
 822                          message: pair[0]
 823                      }));
 824                      c += 2*(pair[1] == 1) + 1*(pair[1] == 2);
 825                  });
 826  
 827                  status = !c ? 'success' : (c == 2*textpattern.Console.messages[event].length ? 'error' : 'warning');
 828              }
 829  
 830              textpattern.Relay.callback('announce', {event: event, message: message, status: status});
 831              textpattern.Console.clear(event, true);
 832          });
 833  
 834          return this;
 835      },
 836  
 837      /**
 838       * Logs a message.
 839       *
 840       * @param  message The message
 841       * @return textpattern.Console
 842       * @example
 843       * textpattern.Console.log('Some message');
 844       */
 845  
 846      log: function (message) {
 847          if (textpattern.prefs.production_status === 'debug') {
 848              textpattern.Console.history.push(message);
 849  
 850              textpattern.Relay.callback('txpConsoleLog', {
 851                  'message': message
 852              });
 853          }
 854  
 855          return this;
 856      }
 857  };
 858  
 859  /**
 860   * Console API module for textpattern.Console.
 861   *
 862   * Passes invoked messages to Web/JavaScript Console
 863   * using console.log().
 864   *
 865   * Uses a namespaced 'txpConsoleLog.ConsoleAPI' event.
 866   */
 867  
 868  textpattern.Relay.register('txpConsoleLog.ConsoleAPI', function (event, data) {
 869      if ($.type(console) === 'object' && $.type(console.log) === 'function') {
 870          console.log(data.message);
 871      }
 872  }).register('uploadProgress', function (event, data) {
 873      $('progress.txp-upload-progress').val(data.loaded / data.total);
 874  }).register('uploadStart', function (event, data) {
 875      $('progress.txp-upload-progress').val(0).show();
 876  }).register('uploadEnd', function (event, data) {
 877      $('progress.txp-upload-progress').hide();
 878  }).register('updateList', function (event, data) {
 879      var list = data.list || '#messagepane, .txp-async-update',
 880          url = data.url || 'index.php',
 881          callback = data.callback || function(event) {
 882              textpattern.Console.announce(event);
 883          },
 884          handle = function(html) {
 885              if (html) {
 886                  var $html = $(html);
 887                  $.each(list.split(','), function(index, value) {
 888                      $(value).each(function() {
 889                          var id = this.id;
 890                          if (id) {
 891                              $(this).replaceWith($html.find('#'+id)).remove();
 892                              $('#'+id).trigger('updateList');
 893                          }
 894                      });
 895                  });
 896  
 897                  $html.remove();
 898              }
 899  
 900              callback(data.event);
 901          };
 902  
 903      $(list).addClass('disabled');
 904  
 905      if (typeof data.html == 'undefined') {
 906          $('<html />').load(url, data.data, function(responseText, textStatus, jqXHR) {
 907              handle(this);
 908          });
 909      } else {
 910          handle(data.html);
 911      }
 912  }).register('announce', function(event, data) {
 913      var container = textpattern.prefs.messagePane || '',
 914          message = container && data.message.length ? textpattern.mustache(container, {message: data.message.join('<br />'), status: data.status, close: textpattern.gTxt('close')}) : '';
 915  
 916      if (message) {
 917          $('#messagepane').html(message);
 918      }
 919  });
 920  
 921  /**
 922   * Script routing.
 923   *
 924   * @since 4.6.0
 925   */
 926  
 927  textpattern.Route =
 928  {
 929      /**
 930       * An array of attached listeners.
 931       */
 932  
 933      attached: [],
 934  
 935      /**
 936       * Attaches a listener.
 937       *
 938       * @param {string} pages The page
 939       * @param {object} fn    The callback
 940       */
 941  
 942      add: function (pages, fn) {
 943          $.each(pages.split(','), function (index, page) {
 944              textpattern.Route.attached.push({
 945                  'page': $.trim(page),
 946                  'fn'  : fn
 947              });
 948          });
 949  
 950          return this;
 951      },
 952  
 953      /**
 954       * Initializes attached listeners.
 955       *
 956       * @param {object} options       Options
 957       * @param {string} options.event The event
 958       * @param {string} options.step  The step
 959       */
 960  
 961      init: function (options) {
 962          var custom = !!options;
 963          var options = $.extend({
 964              'event': textpattern.event,
 965              'step' : textpattern.step
 966          }, options);
 967  
 968          textpattern.Route.attached = textpattern.Route.attached.filter(function (elt) {return !!elt});
 969          textpattern.Route.attached.forEach(function (data, index) {
 970              if (!custom && data.page === '' || data.page === options.event || data.page === '.' + options.step || data.page === options.event + '.' + options.step) {
 971                  data.fn({
 972                      'event': options.event,
 973                      'step' : options.step,
 974                      'route': data.page
 975                  });
 976  
 977                  delete(textpattern.Route.attached[index]);
 978              }
 979          });
 980  
 981          return this;
 982      }
 983  };
 984  
 985  /**
 986   * Sends a form using AJAX and processes the response.
 987   *
 988   * @param  {object} options          Options
 989   * @param  {string} options.dataType The response data type
 990   * @param  {object} options.success  The success callback
 991   * @param  {object} options.error    The error callback
 992   * @return {object} this
 993   * @since  4.5.0
 994   */
 995  
 996  jQuery.fn.txpAsyncForm = function (options) {
 997      options = $.extend({
 998          dataType: 'script',
 999          success : null,
1000          error   : null
1001      }, options);
1002  
1003      // Send form data to application, process response as script.
1004      this.off('submit.txpAsyncForm').on('submit.txpAsyncForm', function (event, extra) {
1005          event.preventDefault();
1006  
1007          if (typeof extra === 'undefined') extra = new Object;
1008  
1009          var $this = $(this);
1010  
1011          // Safari workaround?
1012          var $inputs = $('input[type="file"]:not([disabled])', $this);
1013          $inputs.each(function(i, input) {
1014              if (input.files.length > 0) return;
1015              $(input).prop('disabled', true);
1016          });
1017  
1018          var form =
1019          {
1020              data   : typeof extra.form !== 'undefined' ? extra.form : ( typeof window.FormData === 'undefined' ? $this.serialize() : new FormData(this) ),
1021              extra  : new Object,
1022              spinner: typeof extra['_txp_spinner'] !== 'undefined' ? $(extra['_txp_spinner']) : $('<span />').addClass('spinner ui-icon ui-icon-refresh')
1023          };
1024  
1025          $inputs.prop('disabled', false);// Safari workaround.
1026  
1027          if (typeof extra['_txp_submit'] !== 'undefined') {
1028              form.button = $this.find(extra['_txp_submit']).eq(0);
1029          } else {
1030              form.button = $this.find('input[type="submit"]:focus').eq(0);
1031  
1032              // WebKit does not set :focus on button-click: use first submit input as a fallback.
1033              if (!form.button.length) {
1034                  form.button = $this.find('input[type="submit"]').eq(0);
1035              }
1036          }
1037  
1038          form.extra[form.button.attr('name') || '_txp_submit'] = form.button.val() || '_txp_submit';
1039          $.extend(true, form.extra, options.data, extra.data);
1040          // Show feedback while processing.
1041          form.button.attr('disabled', true).after(form.spinner.val(0));
1042          $this.addClass('busy');
1043          $('body').addClass('busy');
1044  
1045          if (form.data) {
1046              if ( form.data instanceof FormData ) {
1047                  $.each(form.extra, function(key, val) {
1048                      form.data.append(key, val);
1049                  });
1050              } else {
1051                  $.each(form.extra, function(key, val) {
1052                      form.data += '&'+key+'='+val;
1053                  });
1054              }
1055          }
1056  
1057          sendAsyncEvent(form.data, function () {}, options.dataType)
1058              .done(function (data, textStatus, jqXHR) {
1059                  if (options.success) {
1060                      options.success($this, event, data, textStatus, jqXHR);
1061                  }
1062  
1063                  textpattern.Relay.callback('txpAsyncForm.success', {
1064                      'this'      : $this,
1065                      'event'     : event,
1066                      'data'      : data,
1067                      'textStatus': textStatus,
1068                      'jqXHR'     : jqXHR
1069                  });
1070              })
1071              .fail(function (jqXHR, textStatus, errorThrown) {
1072                  if (options.error) {
1073                      options.error($this, event, jqXHR, $.ajaxSetup(), errorThrown);
1074                  }
1075  
1076                  textpattern.Relay.callback('txpAsyncForm.error', {
1077                      'this'        : $this,
1078                      'event'       : event,
1079                      'jqXHR'       : jqXHR,
1080                      'ajaxSettings': $.ajaxSetup(),
1081                      'thrownError' : errorThrown
1082                  });
1083              })
1084              .always(function () {
1085                  $this.removeClass('busy');
1086                  form.button.removeAttr('disabled');
1087                  form.spinner.remove();
1088                  $('body').removeClass('busy');
1089                  textpattern.Console.announce();
1090              });
1091      });
1092  
1093      return this;
1094  };
1095  
1096  /**
1097   * Sends a link using AJAX and processes the plain text response.
1098   *
1099   * @param  {object} options          Options
1100   * @param  {string} options.dataType The response data type
1101   * @param  {object} options.success  The success callback
1102   * @param  {object} options.error    The error callback
1103   * @return {object} this
1104   * @since  4.5.0
1105   */
1106  
1107  jQuery.fn.txpAsyncHref = function (options, selector) {
1108      options = $.extend({
1109          dataType: 'text',
1110          success : null,
1111          error   : null
1112      }, options);
1113  
1114      selector = !!selector ? selector : null;
1115  
1116      this.on('click.txpAsyncHref', selector, function (event) {
1117          event.preventDefault();
1118          var $this = $(this);
1119          var url = this.search.replace('?', '') + '&' + $.param({value: $this.text()});
1120  
1121          // Show feedback while processing.
1122          $this.addClass('busy');
1123          $('body').addClass('busy');
1124  
1125          sendAsyncEvent(url, function () {}, options.dataType)
1126              .done(function (data, textStatus, jqXHR) {
1127                  if (options.dataType === 'text') {
1128                      $this.html(data);
1129                  }
1130  
1131                  if (options.success) {
1132                      options.success($this, event, data, textStatus, jqXHR);
1133                  }
1134  
1135                  textpattern.Relay.callback('txpAsyncHref.success', {
1136                      'this'      : $this,
1137                      'event'     : event,
1138                      'data'      : data,
1139                      'textStatus': textStatus,
1140                      'jqXHR'     : jqXHR
1141                  });
1142              })
1143              .fail(function (jqXHR, textStatus, errorThrown) {
1144                  if (options.error) {
1145                      options.error($this, event, jqXHR, $.ajaxSetup(), errorThrown);
1146                  }
1147  
1148                  textpattern.Relay.callback('txpAsyncHref.error', {
1149                      'this'        : $this,
1150                      'event'       : event,
1151                      'jqXHR'       : jqXHR,
1152                      'ajaxSettings': $.ajaxSetup(),
1153                      'thrownError' : errorThrown
1154                  });
1155              })
1156              .always(function () {
1157                  $this.removeClass('busy');
1158                  $('body').removeClass('busy');
1159              });
1160      });
1161  
1162      return this;
1163  };
1164  
1165  /**
1166   * Sends a link using AJAX and processes the HTML response.
1167   *
1168   * @param  {object} options          Options
1169   * @param  {string} options.dataType The response data type
1170   * @param  {object} options.success  The success callback
1171   * @param  {object} options.error    The error callback
1172   * @return {object} this
1173   * @since  4.6.0
1174   */
1175  
1176  function txpAsyncLink(event, txpEvent)
1177  {
1178      event.preventDefault();
1179      var $this = $(event.target);
1180      if ($this.attr('href') === undefined) {
1181          $this = $this.parent();
1182      }
1183      var url = $this.attr('href').replace('?', '');
1184  
1185      // Show feedback while processing.
1186      $this.addClass('busy');
1187      $('body').addClass('busy');
1188  
1189      sendAsyncEvent(url, function () {}, 'html')
1190          .done(function (data, textStatus, jqXHR) {
1191              textpattern.Relay.callback('txpAsyncLink.'+txpEvent+'.success', {
1192                  'this'      : $this,
1193                  'event'     : event,
1194                  'data'      : data,
1195                  'textStatus': textStatus,
1196                  'jqXHR'     : jqXHR
1197              });
1198          })
1199          .fail(function (jqXHR, textStatus, errorThrown) {
1200              textpattern.Relay.callback('txpAsyncLink.'+txpEvent+'.error', {
1201                  'this'        : $this,
1202                  'event'       : event,
1203                  'jqXHR'       : jqXHR,
1204                  'ajaxSettings': $.ajaxSetup(),
1205                  'thrownError' : errorThrown
1206              });
1207          })
1208          .always(function () {
1209              $this.removeClass('busy');
1210              $('body').removeClass('busy');
1211          });
1212  
1213      return this;
1214  }
1215  
1216  /**
1217   * Creates a UI dialog.
1218   *
1219   * @param  {object} options Options
1220   * @return {object} this
1221   * @since  4.6.0
1222   */
1223  
1224  jQuery.fn.txpDialog = function (options) {
1225      options = $.extend({
1226          autoOpen: false,
1227          buttons: [{
1228              text: textpattern.gTxt('ok'),
1229              click: function () {
1230                  // callbacks?
1231  
1232                  if ($(this).is('form')) {
1233                      $(this).submit();
1234                  }
1235  
1236                  $(this).dialog('close');
1237              }
1238          }],
1239          width: 440
1240      }, options, $(this).data());
1241  
1242      this.dialog(options);
1243  
1244      return this;
1245  };
1246  
1247  /**
1248   * Creates a date picker.
1249   *
1250   * @param  {object} options Options
1251   * @return {object} this
1252   * @since  4.6.0
1253   */
1254  
1255  jQuery.fn.txpDatepicker = function (options) {
1256      // TODO $.datepicker.regional[ 'en' ];
1257      // TODO support from RTL languages
1258      this.datepicker(options);
1259  
1260      return this;
1261  };
1262  
1263  /**
1264   * Creates a sortable element.
1265   *
1266   * This method creates a sortable widget, allowing to
1267   * reorder elements in a list and synchronizes the updated
1268   * order with the server.
1269   *
1270   * @param  {object}  options
1271   * @param  {string}  options.dataType The response datatype
1272   * @param  {object}  options.success  The sync success callback
1273   * @param  {object}  options.error    The sync error callback
1274   * @param  {string}  options.event    The event
1275   * @param  {string}  options.step     The step
1276   * @param  {string}  options.cancel   Prevents sorting if you start on elements matching the selector
1277   * @param  {integer} options.delay    Sorting delay
1278   * @param  {integer} options.distance Tolerance, in pixels, for when sorting should start
1279   * @return this
1280   * @since  4.6.0
1281   */
1282  
1283  jQuery.fn.txpSortable = function (options) {
1284      options = $.extend({
1285          dataType: 'script',
1286          success : null,
1287          error   : null,
1288          event   : textpattern.event,
1289          step    : 'sortable_save',
1290          cancel  : ':input, button',
1291          delay   : 0,
1292          distance: 15,
1293          items   : '[data-txp-sortable-id]'
1294      }, options);
1295  
1296      var methods =
1297      {
1298          /**
1299           * Sends updated order to the server.
1300           */
1301  
1302          update: function () {
1303              var ids = [], $this = $(this);
1304  
1305              $this.children('[data-txp-sortable-id]').each(function () {
1306                  ids.push($(this).data('txp-sortable-id'));
1307              });
1308  
1309              if (ids) {
1310                  sendAsyncEvent({
1311                      event: options.event,
1312                      step : options.step,
1313                      order: ids
1314                  }, function () {}, options.dataType)
1315                      .done(function (data, textStatus, jqXHR) {
1316                          if (options.success) {
1317                              options.success.call($this, data, textStatus, jqXHR);
1318                          }
1319  
1320                          textpattern.Relay.callback('txpSortable.success', {
1321                              'this'      : $this,
1322                              'data'      : data,
1323                              'textStatus': textStatus,
1324                              'jqXHR'     : jqXHR
1325                          });
1326                      })
1327                      .fail(function (jqXHR, textStatus, errorThrown) {
1328                          if (options.error) {
1329                              options.error.call($this, jqXHR, $.ajaxSetup(), errorThrown);
1330                          }
1331  
1332                          textpattern.Relay.callback('txpSortable.error', {
1333                              'this'        : $this,
1334                              'jqXHR'       : jqXHR,
1335                              'ajaxSettings': $.ajaxSetup(),
1336                              'thrownError' : errorThrown
1337                          });
1338                      });
1339              }
1340          }
1341      };
1342  
1343      return this.sortable({
1344          cancel  : options.cancel,
1345          delay   : options.delay,
1346          distance: options.distance,
1347          update  : methods.update,
1348          items   : options.items
1349      });
1350  };
1351  
1352  /**
1353   * Mask/unmask password input field.
1354   *
1355   * @since  4.6.0
1356   */
1357  
1358  textpattern.passwordMask = function () {
1359      $('form').on('click', '#show_password', function () {
1360          var inputBox = $(this).closest('form').find('input.txp-maskable');
1361          var newType = (inputBox.attr('type') === 'password') ? 'text' : 'password';
1362          textpattern.changeType(inputBox, newType);
1363          $(this).attr('checked', newType === 'text' ? 'checked' : null).prop('checked', newType === 'text');
1364      }).find('#show_password').prop('checked', false);
1365  };
1366  
1367  /**
1368   * Change the type of an input element.
1369   *
1370   * @param  {object} elem The <input/> element
1371   * @param  {string} type The desired type
1372   *
1373   * @see    https://gist.github.com/3559343 for original
1374   * @since  4.6.0
1375   */
1376  
1377  textpattern.changeType = function (elem, type) {
1378      if (elem.prop('type') === type) {
1379          // Already the correct type.
1380          return elem;
1381      }
1382  
1383      try {
1384          // May fail if browser prevents it.
1385          return elem.prop('type', type);
1386      } catch (e) {
1387          // Create the element by hand.
1388          // Clone it via a div (jQuery has no html() method for an element).
1389          var html = $('<div>').append(elem.clone()).html();
1390  
1391          // Match existing attributes of type=text or type="text".
1392          var regex = /type=(\")?([^\"\s]+)(\")?/;
1393  
1394          // If no match, add the type attribute to the end; otherwise, replace it.
1395          var tmp = $(html.match(regex) == null ?
1396              html.replace('>', ' type="' + type + '">') :
1397              html.replace(regex, 'type="' + type + '"'));
1398  
1399          // Copy data from old element.
1400          tmp.data('type', elem.data('type'));
1401          var events = elem.data('events');
1402          var cb = function (events) {
1403              return function () {
1404                  // Re-bind all prior events.
1405                  for (var idx in events) {
1406                      var ydx = events[idx];
1407  
1408                      for (var jdx in ydx) {
1409                          tmp.bind(idx, ydx[jdx].handler);
1410                      }
1411                  }
1412              };
1413          }(events);
1414  
1415          elem.replaceWith(tmp);
1416  
1417          // Wait a smidge before firing callback.
1418          setTimeout(cb, 10);
1419  
1420          return tmp;
1421      }
1422  };
1423  
1424  /**
1425   * Encodes a string for a use in HTML.
1426   *
1427   * @param  {string} string The string
1428   * @return {string} Encoded string
1429   * @since  4.6.0
1430   */
1431  
1432  textpattern.encodeHTML = function (string) {
1433      return $('<div/>').text(string).html();
1434  };
1435  
1436  /**
1437   * Decodes a string as HTML.
1438   *
1439   * @param  {string} string The string
1440   * @return {string} Encoded string
1441   * @since  4.8.0
1442   */
1443  
1444  textpattern.decodeHTML = function (string) {
1445      let div = document.createElement('template');
1446      div.innerHTML = string.trim();
1447  
1448      return div.content;
1449  }
1450  
1451  /**
1452   * Translates given substrings.
1453   *
1454   * @param  {string} string        The mustached string
1455   * @param  {object} replacements  Translated substrings
1456   * @return string   Translated string
1457   * @since  4.7.0
1458   * @example
1459   * textpattern.mustache('{hello} world, and {bye|thanks}!', {hello: 'bye'});
1460   */
1461  
1462  textpattern.mustache = function(string, replacements)
1463  {
1464      return string.replace(/\{([^\{\|\}]+)(\|[^\{\}]*)?\}/g, function(match, p1, p2) {
1465          return typeof replacements[p1] != 'undefined' ? replacements[p1] : (typeof p2 == 'undefined' ? match : p2.replace('|', ''));
1466      });
1467  };
1468  
1469  /**
1470   * Translates given substrings.
1471   *
1472   * @param  {string} string       The string being translated
1473   * @param  {object} replacements Translated substrings
1474   * @return string   Translated string
1475   * @since  4.6.0
1476   * @example
1477   * textpattern.tr('hello world, and bye!', {'hello': 'bye', 'bye': 'hello'});
1478   */
1479  
1480  textpattern.tr = function (string, replacements) {
1481      var match, position, output = '', replacement;
1482  
1483      for (position = 0; position < string.length; position++) {
1484          match = false;
1485  
1486          $.each(replacements, function (from, to) {
1487              if (string.substr(position, from.length) === from) {
1488                  match = true;
1489                  replacement = to;
1490                  position = (position + from.length) - 1;
1491  
1492                  return;
1493              }
1494          });
1495  
1496          if (match) {
1497              output += replacement;
1498          } else {
1499              output += string.charAt(position);
1500          }
1501      }
1502  
1503      return output;
1504  };
1505  
1506  /**
1507   * Returns an i18n string.
1508   *
1509   * @param  {string}  i18n   The i18n string
1510   * @param  {object}  atts   Replacement map
1511   * @param  {boolean} escape TRUE to escape HTML in atts
1512   * @return {string}  The string
1513   * @example
1514   * textpattern.gTxt('string', {'{name}': 'example'}, true);
1515   */
1516  
1517  textpattern.gTxt = function (i18n, atts, escape) {
1518      var tags = atts || {};
1519      var string = i18n;
1520      var name = string.toLowerCase();
1521  
1522      if ($.type(textpattern.textarray[name]) !== 'undefined') {
1523          string = textpattern.textarray[name];
1524      }
1525  
1526      if (escape !== false) {
1527          string = textpattern.encodeHTML(string);
1528  
1529          $.each(tags, function (key, value) {
1530              tags[key] = textpattern.encodeHTML(value);
1531          });
1532      }
1533  
1534      string = textpattern.tr(string, tags);
1535  
1536      return string;
1537  };
1538  
1539  /**
1540   * Replaces HTML contents of each matched with i18n string.
1541   *
1542   * This is a jQuery plugin for textpattern.gTxt().
1543   *
1544   * @param  {object|string}  options        Options or the i18n string
1545   * @param  {string}         options.string The i18n string
1546   * @param  {object}         options.tags   Replacement map
1547   * @param  {boolean}        options.escape TRUE to escape HTML in tags
1548   * @param  {object}         tags           Replacement map
1549   * @param  {boolean}        escape         TRUE to escape HTML in tags
1550   * @return {object}         this
1551   * @see    textpattern.gTxt()
1552   * @example
1553   * $('p').gTxt('string').class('alert-block warning');
1554   */
1555  
1556  jQuery.fn.gTxt = function (opts, tags, escape) {
1557      var options = $.extend({
1558          'string': opts,
1559          'tags'  : tags,
1560          'escape': escape
1561      }, opts);
1562  
1563      this.html(textpattern.gTxt(options.string, options.tags, options.escape));
1564  
1565      return this;
1566  };
1567  
1568  /**
1569   * ESC button closes alert messages.
1570   * CTRL+S triggers Save buttons click.
1571   *
1572   * @since 4.7.0
1573   */
1574  
1575  $(document).keydown(function (e) {
1576      var key = e.which;
1577  
1578      if (key === 27) {
1579          $('.close').parent().toggle();
1580      } else if (key === 19 || (!e.altKey && (e.metaKey || e.ctrlKey) && String.fromCharCode(key).toLowerCase() === 's'))
1581      {
1582          var obj = $('input.publish');
1583  
1584          if (obj.length)
1585          {
1586              e.preventDefault();
1587              obj.eq(0).closest('form').submit();
1588          }
1589      }
1590  });
1591  
1592  jQuery.fn.txpMenu = function(button) {
1593      var menu = this;
1594  
1595      menu.on('click focusin', function (e) {
1596          e.stopPropagation();
1597      }).menu({
1598          select: function(e, ui) {
1599              menu.menu('focus', null, ui.item);
1600              if (e.originalEvent.type !== 'click') {
1601                  ui.item.find('input[type="checkbox"]').click();
1602              }
1603          }
1604      }).find('input[type="checkbox"]').keyup(function(e) {
1605          e.preventDefault();
1606      });
1607  
1608      !button || button.on('click', function (e) {
1609          menu.toggle().position({
1610              my: dir+' top',
1611              at: dir+' bottom',
1612              of: this
1613          }).focus().menu('focus', null, menu.find('.ui-menu-item:first'));
1614  
1615          if (menu.is(':visible')) {
1616              $(document).one('blur click focusin', function (e) {
1617                  menu.hide();
1618              });
1619          }
1620  
1621          return false;
1622      }).on('focusin', function(e) {
1623          e.stopPropagation();
1624      });
1625  
1626      return this;
1627  };
1628  
1629  /**
1630   * Search tool.
1631   *
1632   * @since 4.6.0
1633   */
1634  
1635  function txp_search()
1636  {
1637      var $ui = $('.txp-search'),
1638          button = $ui.find('.txp-search-options').button({
1639              showLabel: false,
1640              icon: 'ui-icon-triangle-1-s'
1641          }),
1642          menu = $ui.find('.txp-dropdown'),
1643          crit = $ui.find('input[name="crit"]');
1644  
1645      menu.hide().txpMenu(button);
1646  
1647      $ui.find('.txp-search-button').button({
1648          showLabel: false,
1649          icon: 'ui-icon-search'
1650      }).click(function (e) {
1651          e.stopPropagation();
1652          e.preventDefault();
1653          $ui.submit();
1654      });
1655  
1656      $ui.find('.txp-search-buttons').controlgroup();
1657  
1658      $ui.find('.txp-search-clear').click(function(e) {
1659          e.preventDefault();
1660          crit.val('');
1661          $ui.submit();
1662      });
1663  
1664      $ui.txpMultiEditForm({
1665          'checkbox'   : 'input[name="search_method[]"][type=checkbox]',
1666          'row'        : '.txp-dropdown li',
1667          'highlighted': '.txp-dropdown li',
1668          'confirmation': false
1669      });
1670  
1671      $ui.submit(function(e) {
1672          var empty = crit.val() !== '';
1673  
1674          if (empty) {
1675              menu.find('input[name="search_method[]"]').each(function() {
1676                  empty = empty && !$(this).is(':checked');
1677              });
1678          }
1679  
1680          if(empty) {
1681              button.click();
1682              return false;
1683          }
1684      });
1685  }
1686  
1687  /**
1688   * Column manipulation tool.
1689   *
1690   * @since 4.7.0
1691   */
1692  
1693  var uniqueID = (function() {
1694      var id = 0;
1695      return function() {
1696          return id++;
1697      };
1698  })(); // Invoke the outer function after defining it.
1699  
1700  jQuery.fn.txpColumnize = function ()
1701  {
1702      var $table = $(this), items = [], selectAll = true, stored = true,
1703          $headers = $table.find('thead tr>th');
1704  
1705      $headers.each(function (index) {
1706          var $this = $(this), $title = $this.text().trim(), $id = $this.data('col');
1707  
1708          if (!$title) {
1709              return;
1710          }
1711  
1712          if ($id == undefined) {
1713              if ($id = this.className.match(/\btxp-list-col-([\w\-]+)\b/)) {
1714                  $id = $id[1];
1715              } else {
1716                  return;
1717              }
1718          }
1719  
1720          var disabled = $this.hasClass('asc') || $this.hasClass('desc');
1721          var $li = $('<li />').addClass(disabled ? 'ui-state-disabled' : null);
1722          var $box = $('<input type="checkbox" tabindex="-1" class="checkbox active" data-name="list_options" checked="checked" />')
1723              .attr('value', $id)
1724              .attr('data-index', index)
1725              .prop('disabled', disabled);
1726  
1727          $li.html($('<div role="menuitem" />')
1728              .append($('<label />').text($title).prepend($box)));
1729  
1730  
1731          var $target = $table.find('tr>*:nth-child(' + (index + 1) + ')');
1732          var me = $li.find('input').on('change', function (ev) {
1733              toggleColumn($id, $target, $(this).prop('checked'));
1734          });
1735  
1736          if (stored) {
1737              try {
1738                  if (textpattern.storage.data[textpattern.event]['columns'][$id] == false) {
1739                      selectAll = false;
1740                      $target.hide();
1741                      me.prop('checked', false);
1742                  }
1743              } catch (e) {
1744                  stored = false;
1745              }
1746          }
1747  
1748          items.push($li);
1749      });
1750  
1751      if (!items.length) {
1752          return this;
1753      }
1754  
1755      var $menu = $('<ul class="txp-dropdown" role="menu" />').hide(),
1756          $button = $('<a class="txp-list-options-button" href="#" />').text(textpattern.gTxt('list_options')).prepend('<span class="ui-icon ui-icon-gear"></span> ');
1757  
1758      var $li = $('<li class="txp-dropdown-toggle-all" />'),
1759          $box = $('<input tabindex="-1" class="checkbox active" data-name="select_all" type="checkbox" />').attr('checked', selectAll);
1760  
1761      $li.html($('<div role="menuitem" />')
1762          .append($('<label />').html(textpattern.gTxt('toggle_all_selected')).prepend($box)));
1763  
1764      $menu.html($li).append(items);
1765  
1766      var $container = $table.closest('.txp-layout-1col');
1767      var $ui = $container.find('.txp-list-options');
1768      var $panel = $container.find('.txp-control-panel');
1769  
1770      if (!$ui.length) {
1771          $ui = $('<div class="txp-list-options"></div>');
1772      } else {
1773          $ui.find('a.txp-list-options-button, ul.txp-dropdown').remove();
1774          $panel = false;
1775      }
1776  
1777      $ui.append($button).append($menu);
1778      $menu.txpMenu($button);
1779  
1780      $ui.data('_txpMultiEdit', null).txpMultiEditForm({
1781          'checkbox'   : 'input:not(:disabled)[data-name="list_options"][type=checkbox]',
1782          'selectAll'  : 'input[data-name="select_all"][type=checkbox]',
1783          'row'        : '.txp-dropdown li',
1784          'highlighted': '.txp-dropdown li',
1785          'confirmation': false
1786      });
1787  
1788      if ($panel.length) {
1789          $panel.after($ui);
1790      } else if ($panel !== false) {
1791          $table.closest('form').prepend($ui);
1792      }
1793  
1794      return this;
1795  };
1796  
1797  /**
1798   * Set expanded/collapsed nature of all twisty boxes in a panel.
1799   *
1800   * The direction can either be 'expand' or 'collapse', passed
1801   * in as an argument to the handler.
1802   *
1803   * @param  {event} ev Event that triggered the function
1804   * @since  4.6.0
1805   */
1806  
1807  function txp_expand_collapse_all(ev)
1808  {
1809      ev.preventDefault();
1810  
1811      var direction = ev.data.direction,
1812          container = ev.data.container || (ev.delegateTarget == ev.target ? 'body' : ev.delegateTarget);
1813  
1814      $(container).find('.txp-summary a').each(function (i, elm) {
1815          var $elm = $(elm);
1816  
1817          if (direction === 'collapse') {
1818              if ($elm.parent('.txp-summary').hasClass('expanded')) {
1819                  $elm.click();
1820              }
1821          } else {
1822              if (!$elm.parent('.txp-summary').hasClass('expanded')) {
1823                  $elm.click();
1824              }
1825          }
1826      });
1827  }
1828  
1829  /**
1830   * Restore sub-panel twisties to their as-stored state.
1831   *
1832   * @return {[type]} [description]
1833   */
1834  
1835  jQuery.fn.restorePanes = function () {
1836      var $this = $(this), stored = true;
1837      // Initialize dynamic WAI-ARIA attributes.
1838      $this.find('.txp-summary a').each(function (i, elm) {
1839          // Get id of toggled <section> region.
1840          var $elm = $(elm), region = this.hash;
1841  
1842          if (region) {
1843              var $region = $this.find(region);
1844              region = region.substr(1);
1845  
1846              var pane = $elm.data('txp-pane');
1847  
1848              if (pane === undefined) {
1849                  pane = region;
1850              }
1851  
1852              if (stored) {
1853                  try {
1854                      if (textpattern.storage.data[textpattern.event]['panes'][pane] == true) {
1855                          $elm.parent('.txp-summary').addClass('expanded');
1856                          $region.show();
1857                      }
1858                  } catch (e) {
1859                      stored = false;
1860                  }
1861              }
1862  
1863              var vis = $region.is(':visible').toString();
1864              $elm.attr('aria-controls', region).attr('aria-pressed', vis);
1865              $region.attr('aria-expanded', vis);
1866          }
1867      });
1868  
1869      return $this;
1870  };
1871  
1872  /**
1873   * Manage file uploads.
1874   *
1875   * @since 4.7.0
1876   */
1877  
1878  jQuery.fn.txpFileupload = function (options) {
1879      if (!jQuery.fn.fileupload) return this;
1880  
1881      var form = this, fileInput = this.find('input[type="file"]'),
1882          maxChunkSize = Math.min(parseFloat(textpattern.prefs.max_upload_size || 1000000), Number.MAX_SAFE_INTEGER),
1883          maxFileSize = Math.min(parseFloat(textpattern.prefs.max_file_size || 1000000), Number.MAX_SAFE_INTEGER);
1884  
1885      form.fileupload($.extend({
1886          paramName: fileInput.attr('name'),
1887          dataType: 'script',
1888          maxFileSize: maxFileSize,
1889          maxChunkSize: maxChunkSize,
1890          singleFileUploads: true,
1891          formData: null,
1892          fileInput: null,
1893          dropZone: null,
1894          replaceFileInput: false,/*
1895          add: function (e, data) {
1896              form.uploadCount++;
1897              data.submit();
1898          },*/
1899          progressall: function (e, data) {
1900              textpattern.Relay.callback('uploadProgress', data);
1901          },
1902          start: function (e) {
1903              textpattern.Relay.callback('uploadStart', e);
1904          },
1905          stop: function (e) {
1906              textpattern.Relay.callback('uploadEnd', e);
1907          }
1908      }, options)).off('submit').submit(function (e) {
1909          e.preventDefault();
1910          form.uploadCount = 0;
1911          var files = [];
1912  
1913          for (let file of fileInput.prop('files')) {
1914              if (file.size > maxFileSize) {
1915                  textpattern.Console.addMessage(['<strong>'+textpattern.encodeHTML(file['name'])+'</strong> - '+textpattern.gTxt('upload_err_form_size'), 1], 'uploadEnd');
1916              } else {
1917                  form.uploadCount++;
1918                  file['order'] = form.uploadCount;
1919                  files.push(file);
1920              }
1921          }
1922  
1923          if (!files.length) {
1924              textpattern.Console.announce('uploadEnd');
1925          } else {
1926              form.fileupload('add', {
1927                  files: files
1928              });
1929          }
1930          fileInput.val('');
1931      }).bind('fileuploadsubmit', function (e, data) {
1932          data.formData = $.merge([{
1933              'name' : 'fileInputOrder', 'value' : data.files[0].order+'/'+form.uploadCount
1934          }], options.formData);
1935          $.merge(data.formData, form.serializeArray());
1936  
1937          // Reduce maxChunkSize by extra data size (?)
1938          var res = typeof data.formData.entries !== 'undefined'
1939              ? Array.from(data.formData.entries(), function(prop) {
1940                  return prop[1].name.length + prop[1].value.length;
1941              }).reduce(function(a, b) {return a + b + 2;}, 0)
1942              : 256;
1943  
1944          var chunkSize = form.fileupload('option', 'maxChunkSize');
1945          form.fileupload('option', 'maxChunkSize', Math.min(maxChunkSize - 8*(res + 255), chunkSize));
1946      });
1947  /*
1948      fileInput.on('change', function(e) {
1949          var singleFileUploads = false;
1950  
1951          $(this.files).each(function () {
1952              if (this.size > maxChunkSize) {
1953                  singleFileUploads = true;
1954              }
1955          });
1956  
1957          form.fileupload('option', 'singleFileUploads', singleFileUploads);
1958      })*/
1959      return this;
1960  };
1961  
1962  jQuery.fn.txpUploadPreview = function(template) {
1963      if (!(template = template || textpattern.prefs.uploadPreview)) {
1964          return this;
1965      }
1966  
1967      var form = $(this), last = form.children(':last-child'), maxSize = textpattern.prefs.max_file_size;
1968      var createObjectURL = (window.URL || window.webkitURL || {}).createObjectURL;
1969  
1970      form.find('input[type="reset"]').on('click', function (e) {
1971          last.nextAll().remove();
1972      });
1973  
1974      form.find('input[type="file"]').on('change', function (e) {
1975          last.nextAll().remove();
1976  
1977          $(this.files).each(function (index) {
1978              var preview = '', mime = this.type.split('/'), hash = typeof(md5) == 'function' ? md5(this.name) : index, status = this.size > maxSize ? 'alert' : '';
1979  
1980              if (createObjectURL) {
1981                  switch (mime[0]) {
1982                  case 'image':
1983                      preview = '<img src="' + createObjectURL(this) + '" />';
1984                      break;
1985                  // TODO case 'video':?
1986                  case 'audio':
1987                      preview = '<'+mime[0]+' controls src="' + createObjectURL(this) + '" />';
1988                      break;
1989                  }
1990              }
1991  
1992              preview = textpattern.mustache(template, $.extend(this, {
1993                  hash: hash,
1994                  preview: preview,
1995                  status: status,
1996                  title: textpattern.encodeHTML(this.name.replace(/\.[^\.]*$/, ''))
1997              }));
1998              form.append(preview);
1999          });
2000      }).change();
2001  
2002      return this;
2003  };
2004  
2005  
2006  /**
2007   * Cookie status.
2008   *
2009   * @deprecated in 4.6.0
2010   */
2011  
2012  var cookieEnabled = true;
2013  
2014  // Setup panel.
2015  
2016  textpattern.Route.add('setup', function () {
2017      textpattern.passwordMask();
2018      $('#setup_admin_theme').prop('required',true);
2019      $('#setup_public_theme').prop('required',true);
2020  
2021      if ($('textarea[name=config]').length) {
2022          $('.txp-config-download').on('click', function (e) {
2023              var text = $('textarea[name=config]').val();
2024              var text = 'data:text/plain;charset=utf-8,' + encodeURIComponent(text);
2025              var el = e.currentTarget;
2026              el.href = text;
2027              el.download = 'config.php';
2028          });
2029      }
2030  });
2031  
2032  // Login panel.
2033  
2034  textpattern.Route.add('login', function () {
2035      // Check cookies.
2036      cookieEnabled = checkCookies();
2037  
2038      // Focus on either username or password when empty.
2039      $('#login_form input').filter(function() {
2040          return !this.value;
2041      }).first().focus();
2042  
2043      textpattern.passwordMask();
2044  });
2045  
2046  // Write panel.
2047  
2048  textpattern.Route.add('article', function () {
2049      // Assume users would not change the timestamp if they wanted to
2050      // 'publish now'/'reset time'.
2051      $(document).on('change',
2052          '#write-timestamp input.year,' +
2053          '#write-timestamp input.month,' +
2054          '#write-timestamp input.day,' +
2055          '#write-timestamp input.hour,' +
2056          '#write-timestamp input.minute,' +
2057          '#write-timestamp input.second',
2058          function () {
2059              $('#publish_now').prop('checked', false);
2060              $('#reset_time').prop('checked', false);
2061          }
2062      );
2063  
2064      textpattern.Relay.register('article.section_changed',
2065          function (event, data) {
2066              var $overrideForm = $('#override-form');
2067              var override_sel = $overrideForm.val();
2068  
2069              $overrideForm.empty().append('<option></option>');
2070  
2071              $.each(data.data, function(key, item) {
2072                  var $option = $('<option />');
2073                  $option.text(item).attr('dir', 'auto').prop('selected', item == override_sel);
2074                  $overrideForm.append($option);
2075              });
2076          }
2077      );
2078  
2079      $('#txp-write-sort-group').on('change', '#section',
2080          function () {
2081              if (typeof allForms !== 'undefined') {
2082                  textpattern.Relay.callback('article.section_changed', {
2083                          data: allForms[$(this).find(':selected').data('skin')]
2084                  });
2085              }
2086          }
2087      ).change();
2088  
2089      var status = 'select[name=Status]', form = $(status).parents('form'), submitButton = form.find('input[type=submit]');
2090  
2091      $('#article_form').on('change', status, function () {
2092          if (!form.hasClass('published')) {
2093              if ($(this).val() < 4) {
2094                  submitButton.val(textpattern.gTxt('save'));
2095              } else {
2096                  submitButton.val(textpattern.gTxt('publish'));
2097              }
2098          }
2099      }).on('submit.txpAsyncForm', function (e) {
2100          if ($pane.dialog('isOpen') && !$('#live-preview').is(':checked')) {
2101              $viewMode.click();
2102          }
2103      }).on('click', '.txp-clone', function (e) {
2104          e.preventDefault();
2105          form.trigger('submit', {data: {copy:1, publish:1}});
2106      });
2107  
2108      // Switch to Text/HTML/Preview mode.
2109      var $pane = $('#pane-view').closest('.txp-dialog'),
2110          $field = '',
2111          $viewMode = $('#view_modes li.active [data-view-mode]');
2112          if (!$viewMode.length) $viewMode = $('#view_modes [data-view-mode]').first();
2113  
2114      $pane.dialog({
2115          dialogClass: 'txp-preview-container',
2116          buttons: [],
2117          closeOnEscape: false,
2118          maxWidth: '100%'
2119      });
2120  
2121      $pane.on( 'dialogopen', function( event, ui ) {
2122          $('#live-preview').trigger('change');
2123      }).on( 'dialogclose', function( event, ui ) {
2124          $('#body, #excerpt').off('input', txp_article_preview);
2125      });
2126  
2127      $('#live-preview').on('change', function() {
2128          if ($(this).is(':checked')) {
2129              $('#body, #excerpt').on('input', txp_article_preview);
2130          } else {
2131              $('#body, #excerpt').off('input', txp_article_preview);
2132          }
2133      })
2134  
2135      textpattern.Relay.register('article.preview',
2136          function (e) {
2137              var data = form.serializeArray();
2138              data.push({name: 'app_mode', value: 'async'});
2139              data.push({name: 'preview', value: $field});
2140              data.push({name: 'view', value: $viewMode.data('view-mode')});
2141              textpattern.Relay.callback('updateList', {
2142                  url: 'index.php #pane-view',
2143                  data: data,
2144                  list: '#pane-view',
2145                  callback: function () {
2146                      $pane.dialog('open');
2147                  }
2148              });
2149          }
2150      );
2151  
2152      $(document).on('click', '[data-view-mode]', function(e) {
2153          e.preventDefault();
2154          $viewMode = $(this);
2155          let $view = $viewMode.data('view-mode');
2156          $viewMode.closest('ul').children('li').removeClass('active').filter('#tab-'+$view).addClass('active');
2157          textpattern.Relay.callback('article.preview');
2158      }).on('click', '[data-preview-link]', function(e) {
2159          e.preventDefault();
2160          $field = $(this).data('preview-link');
2161          $pane.dialog('option', 'title', $(this).text());
2162          $viewMode.click();
2163      }).on('updateList', '#pane-view.html', function() {
2164          Prism.highlightAllUnder(this);
2165      });
2166  
2167      function txp_article_preview() {
2168          $field = this.id;
2169          textpattern.Relay.callback('article.preview', null, 1000);
2170      }
2171  
2172      // Handle Textfilter options.
2173      var $listoptions = $('.txp-textfilter-options .jquery-ui-selectmenu');
2174  
2175      $listoptions.on('selectmenuchange', function (e) {
2176          var me = $('option:selected', this);
2177  
2178          var wrapper = me.closest('.txp-textfilter-options');
2179          var thisHelp = me.data('help');
2180          var renderHelp = (typeof thisHelp === 'undefined') ? '' : thisHelp;
2181  
2182          wrapper.find('.textfilter-value').val(me.data('id')).trigger('change');
2183          wrapper.find('.textfilter-help').html(renderHelp);
2184  
2185          if ($pane.dialog('isOpen')) {
2186              wrapper.find('[data-preview-link]').click();
2187          }
2188      });
2189  
2190      $listoptions.hide().menu();
2191  });
2192  
2193  
2194  textpattern.Route.add('article.init', function () {
2195      $('.txp-textfilter-options .jquery-ui-selectmenu').trigger('selectmenuchange')
2196  })
2197  
2198  textpattern.Route.add('file, image', function () {
2199      if (!$('#txp-list-container').length) return;
2200  
2201      textpattern.Relay.register('uploadStart', function(event) {
2202          textpattern.Relay.data.fileid = [];
2203      }).register('uploadEnd', function(event) {
2204          var callback = function() {
2205              textpattern.Console.clear().announce(event.type);
2206          };
2207  
2208          $(document).ready(function() {
2209              $.merge(textpattern.Relay.data.selected, textpattern.Relay.data.fileid);
2210  
2211              if (textpattern.Relay.data.fileid.length) {
2212                  textpattern.Relay.callback('updateList', {
2213                      data: $('nav.prev-next form').serializeArray(),
2214                      list: '#txp-list-container',
2215                      event: event.type,
2216                      callback: callback
2217                  });
2218              } else {
2219                  callback();
2220              }
2221          });
2222      });
2223  
2224      $('form.upload-form.async').txpUploadPreview()
2225          .txpFileupload({formData: [{name: 'app_mode', value: 'async'}]});
2226  });
2227  
2228  // Uncheck reset on timestamp change.
2229  
2230  textpattern.Route.add('article, file', function () {
2231      $(document).on('change', '.posted input', function (e) {
2232          $('#publish_now, #reset_time').prop('checked', false);
2233      });
2234  });
2235  
2236  // 'Clone' button on Pages, Forms, Styles panels.
2237  
2238  textpattern.Route.add('skin, css, page, form', function () {
2239      $('.txp-clone').click(function (e) {
2240          e.preventDefault();
2241          var target = $(this).data('form');
2242          if (target) {
2243              var $target = $('#' + target);
2244              $target.append('<input type="hidden" name="copy" value="1" />');
2245              $target.off('submit.txpAsyncForm').trigger('submit');
2246          }
2247      });
2248  });
2249  
2250  // Tagbuilder.
2251  
2252  textpattern.Route.add('page, form, file, image', function () {
2253      // Set up asynchronous tag builder links.
2254      textpattern.Relay.register('txpAsyncLink.tag.success', function (event, data) {
2255          $('#tagbuild_links').dialog('close').html($(data['data'])).dialog('open').restorePanes();
2256          $('#txp-tagbuilder-output').select();
2257      });
2258  
2259      $(document).on('click', '.txp-tagbuilder-link', function (ev) {
2260          txpAsyncLink(ev, 'tag');
2261      });
2262  
2263      // Set up asynchronous tag builder launcher.
2264      textpattern.Relay.register('txpAsyncLink.tagbuilder.success', function (event, data) {
2265          $('#tagbuild_links').dialog('close').html($(data['data'])).dialog('open').restorePanes();
2266      });
2267  
2268      $(document).on('click', '.txp-tagbuilder-dialog', function (ev) {
2269          txpAsyncLink(ev, 'tagbuilder');
2270      });
2271  
2272      $('#tagbuild_links').dialog({
2273          dialogClass: 'txp-tagbuilder-container',
2274          autoOpen: false,
2275          focus: function (ev, ui) {
2276              $(ev.target).closest('.ui-dialog').focus();
2277          }
2278      });
2279  
2280      // Set up delegated asynchronous tagbuilder form submission.
2281      $('#tagbuild_links').on('click', 'form.asynchtml input[type="submit"]', function (ev) {
2282          $(this).closest('form.asynchtml').txpAsyncForm({
2283              dataType: 'html',
2284              error: function () {
2285                  window.alert(textpattern.gTxt('form_submission_error'));
2286              },
2287              success: function ($this, event, data) {
2288                  $('#tagbuild_links').html(data);
2289                  $('#txp-tagbuilder-output').select();
2290              }
2291          });
2292      });
2293  });
2294  
2295  // Pophelp.
2296  
2297  textpattern.Route.add('', function () {
2298      textpattern.Relay.register('txpAsyncLink.pophelp.success', function (event, data) {
2299          $(data.event.target).parent().attr('data-item', encodeURIComponent(data.data) );
2300          $('#pophelp_dialog').dialog('close').html(data.data).dialog('open');
2301      });
2302  
2303      $('body').on('click','.pophelp', function (ev) {
2304          var $pophelp = $('#pophelp_dialog');
2305  
2306          if ($pophelp.length == 0) {
2307              $pophelp = $('<div id="pophelp_dialog"></div>');
2308              $('body').append($pophelp);
2309              $pophelp.dialog({
2310                  classes: {'ui-dialog': 'txp-dialog-container'},
2311                  autoOpen: false,
2312                  width: 440,
2313                  title: textpattern.gTxt('help'),
2314                  focus: function (ev, ui) {
2315                      $(ev.target).closest('.ui-dialog').focus();
2316                  }
2317              });
2318          }
2319  
2320          var item = $(ev.target).parent().attr('data-item') || $(ev.target).attr('data-item');
2321          if (typeof item === 'undefined' ) {
2322              txpAsyncLink(ev, 'pophelp');
2323          } else {
2324              $pophelp.dialog('close').html(decodeURIComponent(item)).dialog('open');
2325          }
2326          return false;
2327      });
2328  });
2329  
2330  // Forms panel.
2331  
2332  textpattern.Route.add('form', function () {
2333      $('#allforms_form').txpMultiEditForm({
2334          'checkbox'   : 'input[name="selected_forms[]"][type=checkbox]',
2335          'row'        : '.switcher-list li, .form-list-name',
2336          'highlighted': '.switcher-list li'
2337      });
2338  
2339      textpattern.Relay.register('txpAsyncForm.success', function () {
2340          $('#allforms_form').txpMultiEditForm('select', {value: textpattern.Relay.data.selected});
2341          $('#allforms_form_sections').restorePanes();
2342      });
2343  });
2344  
2345  // Users panel.
2346  
2347  textpattern.Route.add('admin', function () {
2348      textpattern.passwordMask();
2349  });
2350  
2351  // Plugins panel.
2352  
2353  textpattern.Route.add('plugin', function () {
2354      textpattern.Relay.register('txpAsyncHref.success', function (event, data) {
2355          $(data['this']).closest('tr').toggleClass('active');
2356      });
2357  });
2358  
2359  // Diagnostics panel.
2360  
2361  textpattern.Route.add('diag', function () {
2362      $('#diag_clear_private').change(function () {
2363          var diag_data = $('#diagnostics-data').val();
2364          if ($('#diag_clear_private').is(':checked')) {
2365              var regex = new RegExp($('#diagnostics-data').attr('data-txproot'), 'g');
2366              diag_data = diag_data.replace(/^===.*\s/gm, '').replace(regex, '__TXP-ROOT');
2367          } else {
2368              diag_data = diag_data.replace(/^=== +/gm, '');
2369          }
2370          $('#diagnostics-detail').val(diag_data);
2371      });
2372      $('#diag_clear_private').change();
2373  });
2374  
2375  // Languages panel.
2376  
2377  textpattern.Route.add('lang', function () {
2378      $('.txp-grid-lang').on('click', 'button', function (ev) {
2379          ev.preventDefault();
2380          var $me = $(this), $form = $me.closest('form');
2381          $form.find('input[name=step]').val($me.attr('name'));
2382          $(ev.delegateTarget).addClass('disabled').find('button').attr('disabled', true);
2383          $form.submit();
2384      });
2385  });
2386  
2387  // Images edit panel.
2388  
2389  textpattern.Route.add('image', function () {
2390      $('.thumbnail-swap-size').button({
2391          showLabel: false,
2392          icon: 'ui-icon-transfer-e-w'
2393      }).on('click', function (ev) {
2394          var $w = $('#width');
2395          var $h = $('#height');
2396          var width = $w.val();
2397          var height = $h.val();
2398          $w.val(height);
2399          $h.val(width);
2400      });
2401  });
2402  
2403  // Sections panel. Used for edit panel and multiedit change of page+style.
2404  // This can probably be cleaned up / optimised.
2405  
2406  textpattern.Route.add('section', function ()
2407  {
2408      /**
2409       * Display assets based on the selected theme.
2410       *
2411       * @param  string skin The theme name from which to show assets
2412       */
2413      function section_theme_show(skin) {
2414          $('#section_page, #section_css, #multiedit_page, #multiedit_css, #multiedit_dev_page, #multiedit_dev_css').empty();
2415          var $pageSelect = $('[name=section_page], #multiedit_dev_page');
2416          var $styleSelect = $('[name=css], #multiedit_dev_css');
2417  
2418          if (skin in skin_page) {
2419              $pageSelect.append('<option></option>');
2420  
2421              $.each(skin_page[skin], function(key, item) {
2422                  var isSelected = (item == page_sel) ? ' selected' : '';
2423                  $pageSelect.append('<option'+isSelected+'>'+item+'</option>');
2424              });
2425  
2426              if (page_sel === null) {
2427                  $pageSelect.append('<option selected>*</option>');
2428              }
2429          }
2430  
2431          if (skin in skin_style) {
2432              $styleSelect.append('<option></option>');
2433  
2434              $.each(skin_style[skin], function(key, item) {
2435                  var isSelected = (item == style_sel) ? ' selected' : '';
2436                  $styleSelect.append('<option'+isSelected+'>'+item+'</option>');
2437              });
2438  
2439              if (style_sel === null) {
2440                  $styleSelect.append('<option selected>*</option>');
2441              }
2442          }
2443      }
2444  
2445      $('main').on('change', '#section_skin, #multiedit_skin, #multiedit_dev_skin', function() {
2446          section_theme_show($(this).val());
2447      }).on('change', 'select[name=edit_method]', function() {
2448          if ($(this).val() === 'changepagestyle') {
2449              $('#multiedit_skin').change();
2450          } else if ($(this).val() === 'changepagestyledev') {
2451              $('#multiedit_dev_skin').change();
2452          }
2453      });
2454  
2455      // Invoke the handler now to set things on initial page load.
2456      $('#section_skin').change();
2457  });
2458  
2459  // Plugin help panel.
2460  
2461  textpattern.Route.add('plugin.plugin_help', function ()
2462  {
2463      var $helpWrap = $(document.body).children('main');
2464      var $helpTxt = $helpWrap.children('.txp-layout-textbox');
2465      var $head = $helpTxt.children(':first');
2466      var $sectHeads = $helpTxt.children('h2');
2467      var $intro = $head.nextUntil($sectHeads);
2468      var sectIdPrefix = 'plugin_help_section_';
2469  
2470      if ($head.prop('tagName') != 'H1'
2471          || $intro.length && !$sectHeads.length
2472          || !$intro.length && $sectHeads.length < 2
2473          || $helpTxt.find('h1').length > 1
2474          || $helpTxt.find('script, style, [style], [id^="' + sectIdPrefix + '"], [id*=" ' + sectIdPrefix + '"], [class^="txp-layout"], [class*=" txp-layout"], [class^="txp-grid"], [class*=" txp-grid"]').length
2475      ) {
2476          return;
2477      }
2478  
2479      $helpTxt.detach();
2480  
2481      var $sects = $();
2482      var tabs = '';
2483  
2484      if ($intro.length) {
2485          $intro = $intro.wrapAll('<section class="txp-tabs-vertical-group" id="' + sectIdPrefix + 'intro" aria-labelledby="intro-label" />').parent();
2486          $sects = $sects.add($intro);
2487          tabs += '<li><a data-txp-pane="intro" href="#' + sectIdPrefix + 'intro">' + textpattern.gTxt('documentation') + '</a></li>';
2488      }
2489  
2490      $sectHeads.each(function(i, sectHead) {
2491          var $sectHead = $(sectHead);
2492          var $tabHead = $sectHead.clone();
2493  
2494          $tabHead.find('a').each(function(i, anchor) {
2495              $(anchor).contents().unwrap();
2496          });
2497  
2498          // Grab the heading, strip out markup, then sanitize.
2499          var tabTitle = $("<div>").html($tabHead.html()).text();
2500          var tabName = tabTitle.replace(/[^a-z0-9\s]/gi, '').replace(/[_\s]/g, '_').toLowerCase();
2501          var sectId = sectIdPrefix + tabName;
2502  
2503          $sects = $sects.add($sectHead.nextUntil(sectHead).addBack().wrapAll('<section class="txp-tabs-vertical-group" id="' + sectId + '" aria-labelledby="' + sectId + '-label" />').parent());
2504          tabs += '<li><a data-txp-pane="' + tabName + '" href="#' + sectId + '">' + tabTitle + '</a></li>';
2505      });
2506  
2507      $head.addClass('txp-heading').wrap('<div class="txp-layout-1col"></div>');
2508      $sects.wrapAll('<div class="txp-layout-4col-3span" />');
2509      $sects.parent().before('<div class="txp-layout-4col-alt"><section class="txp-details" id="all_sections" aria-labelledby="all_sections-label"><h3 id="all_sections-label">' + textpattern.gTxt('plugin_help') + '</h3><div role="group"><ul class="switcher-list">' + tabs + '</ul></div></section></div>');
2510      $helpTxt.wrap('<div class="txp-layout" />').contents().unwrap().parent().appendTo($helpWrap);
2511  });
2512  
2513  // All panels?
2514  
2515  textpattern.Route.add('', function () {
2516      // Pane states
2517      var hasTabs = $('.txp-layout:has(.switcher-list li a[data-txp-pane])');
2518  
2519      if (hasTabs.length == 0) {
2520          return;
2521      }
2522  
2523      var tabs = hasTabs.find('.switcher-list li');
2524      var $switchers = tabs.children('a[data-txp-pane]');
2525      var $section = window.location.hash ? hasTabs.find($(window.location.hash).closest('section')) : [];
2526      var selectedTab = 1;
2527  
2528      if (textpattern.event === 'plugin') {
2529          var nameParam = new RegExp('[\?&]name=([^&#]*)').exec(window.location.href);
2530          var dataItem = nameParam[1];
2531      } else {
2532          dataItem = textpattern.event;
2533      }
2534  
2535      tabs.on('click focus', function (ev) {
2536          var me = $(this).children('a[data-txp-pane]');
2537          var data = new Object;
2538  
2539          data[dataItem] = {'tab':me.data('txp-pane')};
2540          textpattern.storage.update(data);
2541      });
2542  
2543      hasTabs.find('a:not([data-txp-pane], .pophelp)').click(function() {
2544          $section = hasTabs.find($(this.hash).closest('section'));
2545  
2546          if ($section.length) {
2547              selectedTab = $section.index();
2548              $switchers.eq(selectedTab).click();
2549          }
2550      });
2551  
2552      if ($section.length) {
2553          selectedTab = $section.index();
2554          $switchers.eq(selectedTab).click();
2555      } else if (textpattern.storage.data[dataItem] !== undefined && textpattern.storage.data[dataItem]['tab'] !== undefined) {
2556          $switchers.each(function (i, elm) {
2557              if ($(elm).data('txp-pane') == textpattern.storage.data[dataItem]['tab']) {
2558                  selectedTab = i;
2559              }
2560          });
2561      } else {
2562          selectedTab = 0;
2563      }
2564  
2565      if (typeof selectedTab === 'undefined') {
2566          selectedTab = 0;
2567      }
2568  
2569      hasTabs.tabs({active: selectedTab}).removeClass('ui-widget ui-widget-content ui-corner-all').addClass('ui-tabs-vertical');
2570      hasTabs.find('.switcher-list').removeClass('ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all');
2571      tabs.removeClass('ui-state-default ui-corner-top');
2572      hasTabs.find('.txp-tabs-vertical-group').removeClass('ui-widget-content ui-corner-bottom');
2573  });
2574  
2575  // Initialize JavaScript.
2576  $(document).ready(function () {
2577      $('body').restorePanes();
2578  
2579      // Collapse/Expand all support.
2580      $('#supporting_content, #tagbuild_links, #content_switcher').on('click', '.txp-collapse-all', {direction: 'collapse'}, txp_expand_collapse_all)
2581          .on('click', '.txp-expand-all', {direction: 'expand'}, txp_expand_collapse_all);
2582  
2583      // Confirmation dialogs.
2584      $(document).on('click.txpVerify', 'a[data-verify]', function (e) {
2585          return verify($(this).data('verify'));
2586      });
2587  
2588      $(document).on('submit.txpVerify', 'form[data-verify]', function (e) {
2589          return verify($(this).data('verify'));
2590      });
2591  
2592      // Disable spellchecking on all elements of class 'code' in capable browsers.
2593      var c = $('.code')[0];
2594  
2595      if (c && 'spellcheck' in c) {
2596          $('.code').prop('spellcheck', false);
2597      }
2598  
2599      // Enable spellcheck for all elements mentioned in textpattern.prefs.do_spellcheck.
2600      $(textpattern.prefs.do_spellcheck).each(function(i, c) {
2601          if ('spellcheck' in c) {
2602              $(c).prop('spellcheck', true);
2603          }
2604      });
2605  
2606      // Attach toggle behaviours.
2607      $(document).on('click', '.txp-summary a[class!=pophelp]', toggleDisplayHref);
2608  
2609      // Establish AJAX timeout from prefs.
2610      if ($.ajaxSetup().timeout === undefined) {
2611          $.ajaxSetup({timeout: textpattern.ajax_timeout});
2612      }
2613  
2614      // Set up asynchronous forms.
2615      $('form.async').txpAsyncForm({
2616          error: function () {
2617              window.alert(textpattern.gTxt('form_submission_error'));
2618          }
2619      });
2620  
2621      // Set up asynchronous links.
2622      $('body').txpAsyncHref($.extend({
2623          error: function () {
2624              window.alert(textpattern.gTxt('form_submission_error'));
2625          }
2626      }, $(this).hasClass('script') ? {dataType: 'script'} : {}), 'a.async');
2627  
2628      // Close button on the announce pane.
2629      $(document).on('click', '.close', function (e) {
2630          e.preventDefault();
2631          $(this).parent().remove();
2632      });
2633  
2634      // Event handling and automation.
2635      $(document).on('change.txpAutoSubmit', 'form [data-submit-on="change"]', function (e) {
2636          $(this).parents('form').submit();
2637      });
2638  
2639      // Polyfills.
2640      // Add support for form attribute in submit buttons.
2641      if ($('html').hasClass('no-formattribute')) {
2642          $('.txp-save input[form]').click(function (e) {
2643              var targetForm = $(this).attr('form');
2644              $('form[id=' + targetForm + ']').submit();
2645          });
2646      }
2647  
2648      // Establish UI defaults.
2649      $('.txp-dropdown').hide();
2650      $('.txp-dialog').txpDialog();
2651      $('.txp-dialog.modal').dialog('option', 'modal', true);
2652      $('.txp-datepicker').txpDatepicker();
2653      $('.txp-sortable').txpSortable();
2654  
2655  
2656      // TODO: integrate jQuery UI stuff properly --------------------------------
2657  
2658  
2659      // Selectmenu
2660      $('.jquery-ui-selectmenu').selectmenu({
2661          position: { my: dir+' top', at: dir+' bottom' }
2662      });
2663  
2664      // Button
2665      $('.jquery-ui-button').button();
2666  
2667      // Button set
2668      $('.jquery-ui-controlgroup').controlgroup();
2669  
2670  
2671      // TODO: end integrate jQuery UI stuff properly ----------------------------
2672  
2673  
2674      // Async lists navigation
2675      $('#txp-list-container').closest('main').on('submit', 'nav.prev-next form', function(e) {
2676          e.preventDefault();
2677          textpattern.Relay.callback('updateList', {data: $(this).serializeArray()});
2678      }).on('click', '.txp-navigation a', function(e) {
2679          if ($(this).hasClass('pophelp')) return;
2680          e.preventDefault();
2681          textpattern.Relay.callback('updateList', {url: $(this).attr('href'), data: $('nav.prev-next form').serializeArray()});
2682          scroll(0, 0);
2683      }).on('click', '.txp-list thead th a', function(e) {
2684          e.preventDefault();
2685          textpattern.Relay.callback('updateList', {list: '#txp-list-container', url: $(this).attr('href'), data: $('nav.prev-next form').serializeArray()});
2686      }).on('submit', 'form[name="longform"]', function(e) {
2687          e.preventDefault();
2688          textpattern.Relay.callback('updateList', {data: $(this).serializeArray()});
2689      }).on('submit', 'form.txp-search', function(e) {
2690          e.preventDefault();
2691          if ($(this).find('input[name="crit"]').val()) $(this).find('.txp-search-clear').removeClass('ui-helper-hidden');
2692          else $(this).find('.txp-search-clear').addClass('ui-helper-hidden');
2693          textpattern.Relay.callback('updateList', {data: $(this).serializeArray()});
2694      }).on('updateList', '#txp-list-container', function() {
2695          if ($(this).find('.multi_edit_form').txpMultiEditForm('select', {value: textpattern.Relay.data.selected}).find('table.txp-list').txpColumnize().length == 0) {
2696              $(this).closest('.txp-layout-1col').find('.txp-list-options-button').hide();
2697          }
2698      });
2699  
2700      // Find and open associated dialogs.
2701      $(document).on('click.txpDialog', '[data-txp-dialog]', function (e) {
2702          $($(this).data('txp-dialog')).dialog('open');
2703          e.preventDefault();
2704      });
2705  
2706      // Attach multi-edit form.
2707      $('.multi_edit_form').txpMultiEditForm();
2708      $('table.txp-list').txpColumnize();
2709  
2710      $('a.txp-logout, .txp-logout a').attr('href', 'index.php?logout=1&lang='+textpattern.prefs.language_ui+'&_txp_token='+textpattern._txp_token);
2711  
2712      // Initialize panel specific JavaScript.
2713      textpattern.Route.init();
2714      // Trigger post init events.
2715      textpattern.Route.init({'step':'init'});
2716  
2717      // Arm UI.
2718      $('.not-ready').removeClass('not-ready');
2719      textpattern.Console.announce();
2720  });

title

Description

title

Description

title

Description

title

title

Body