Textpattern PHP Cross Reference Content Management Systems

Source: /textpattern/textpattern.js - 2141 lines - 56616 bytes - Summary - Text - Print

Description: Collection of client-side tools.

   1  /*
   2   * Textpattern Content Management System
   3   * http://textpattern.com
   4   *
   5   * Copyright (C) 2016 The Textpattern Development Team
   6   *
   7   * This file is part of Textpattern.
   8   *
   9   * Textpattern is free software; you can redistribute it and/or
  10   * modify it under the terms of the GNU General Public License
  11   * as published by the Free Software Foundation, version 2.
  12   *
  13   * Textpattern is distributed in the hope that it will be useful,
  14   * but WITHOUT ANY WARRANTY; without even the implied warranty of
  15   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16   * GNU General Public License for more details.
  17   *
  18   * You should have received a copy of the GNU General Public License
  19   * along with Textpattern. If not, see <http://www.gnu.org/licenses/>.
  20   */
  21  
  22  /**
  23   * Collection of client-side tools.
  24   */
  25  
  26  /**
  27   * Ascertain the page direction (LTR or RTL) as a variable.
  28   */
  29  
  30  var langdir = document.documentElement.dir;
  31  
  32  /**
  33   * Checks if HTTP cookies are enabled.
  34   *
  35   * @return {boolean}
  36   */
  37  
  38  function checkCookies()
  39  {
  40      var date = new Date();
  41  
  42      date.setTime(date.getTime() + (60 * 1000));
  43  
  44      document.cookie = 'testcookie=enabled; expired=' + date.toGMTString() + '; path=/';
  45  
  46      cookieEnabled = (document.cookie.length > 2) ? true : false;
  47  
  48      date.setTime(date.getTime() - (60 * 1000));
  49  
  50      document.cookie = 'testcookie=; expires=' + date.toGMTString() + '; path=/';
  51  
  52      return cookieEnabled;
  53  }
  54  
  55  /**
  56   * Spawns a centred popup window.
  57   *
  58   * @param {string}  url     The location
  59   * @param {integer} width   The width
  60   * @param {integer} height  The height
  61   * @param {string}  options A list of options
  62   */
  63  
  64  function popWin(url, width, height, options)
  65  {
  66      var w = (width) ? width : 400;
  67      var h = (height) ? height : 400;
  68  
  69      var t = (screen.height) ? (screen.height - h) / 2 : 0;
  70      var l = (screen.width) ? (screen.width - w) / 2 : 0;
  71  
  72      var opt = (options) ? options : 'toolbar = no, location = no, directories = no, ' +
  73          'status = yes, menubar = no, scrollbars = yes, copyhistory = no, resizable = yes';
  74  
  75      var popped = window.open(url, 'popupwindow',
  76          'top = ' + t + ', left = ' + l + ', width = ' + w + ', height = ' + h + ',' + opt);
  77  
  78      popped.focus();
  79  }
  80  
  81  /**
  82   * Legacy multi-edit tool.
  83   *
  84   * @param      {object} elm
  85   * @deprecated in 4.6.0
  86   */
  87  
  88  function poweredit(elm)
  89  {
  90      var something = elm.options[elm.selectedIndex].value;
  91  
  92      // Add another chunk of HTML
  93      var pjs = document.getElementById('js');
  94  
  95      if (pjs == null) {
  96          var br = document.createElement('br');
  97          elm.parentNode.appendChild(br);
  98  
  99          pjs = document.createElement('P');
 100          pjs.setAttribute('id', 'js');
 101          elm.parentNode.appendChild(pjs);
 102      }
 103  
 104      if (pjs.style.display == 'none' || pjs.style.display == '') {
 105          pjs.style.display = 'block';
 106      }
 107  
 108      if (something != '') {
 109          switch (something) {
 110              default:
 111                  pjs.style.display = 'none';
 112                  break;
 113          }
 114      }
 115  
 116      return false;
 117  }
 118  
 119  /**
 120   * Basic confirmation for potentially powerful choices (like deletion,
 121   * for example).
 122   *
 123   * @param  {string}  msg The message
 124   * @return {boolean} TRUE if user confirmed the action
 125   */
 126  
 127  function verify(msg)
 128  {
 129      return confirm(msg);
 130  }
 131  
 132  /**
 133   * Selects all multi-edit checkboxes.
 134   *
 135   * @deprecated in 4.5.0
 136   */
 137  
 138  function selectall()
 139  {
 140      $('form[name=longform] input[type=checkbox][name="selected[]"]').prop('checked', true);
 141  }
 142  
 143  /**
 144   * De-selects all multi-edit checkboxes.
 145   *
 146   * @deprecated in 4.5.0
 147   */
 148  
 149  function deselectall()
 150  {
 151      $('form[name=longform] input[type=checkbox][name="selected[]"]').prop('checked', false);
 152  }
 153  
 154  /**
 155   * Selects a range of multi-edit checkboxes.
 156   *
 157   * @deprecated in 4.5.0
 158   */
 159  
 160  function selectrange()
 161  {
 162      var inrange = false;
 163  
 164      $('form[name=longform] input[type=checkbox][name="selected[]"]').each(function ()
 165      {
 166          var $this = $(this);
 167  
 168          if ($this.is(':checked')) {
 169              inrange = (!inrange) ? true : false;
 170          }
 171  
 172          if (inrange) {
 173              $this.prop('checked', true);
 174          }
 175      });
 176  }
 177  
 178  /**
 179   * ?
 180   *
 181   * @deprecated in 4.5.0
 182   */
 183  
 184  function cleanSelects()
 185  {
 186      var withsel = document.getElementById('withselected');
 187  
 188      if (withsel && withsel.options[withsel.selectedIndex].value != '') {
 189          return (withsel.selectedIndex = 0);
 190      }
 191  }
 192  
 193  /**
 194   * Multi-edit functions.
 195   *
 196   * @param  {string|object} method Called method, or options
 197   * @param  {object}        opt    Options if method is a method
 198   * @return {object}        this
 199   * @since  4.5.0
 200   */
 201  
 202  jQuery.fn.txpMultiEditForm = function (method, opt)
 203  {
 204      var args = {};
 205  
 206      var defaults = {
 207          'checkbox'      : 'input[name="selected[]"][type=checkbox]',
 208          'row'           : 'tbody td',
 209          'highlighted'   : 'tr',
 210          'selectedClass' : 'selected',
 211          'actions'       : 'select[name=edit_method]',
 212          'submitButton'  : '.multi-edit input[type=submit]',
 213          'selectAll'     : 'input[name=select_all][type=checkbox]',
 214          'rowClick'      : true,
 215          'altClick'      : true,
 216          'confirmation'  : textpattern.gTxt('are_you_sure')
 217      };
 218  
 219      if ($.type(method) !== 'string') {
 220          opt = method;
 221          method = null;
 222      } else {
 223          args = opt;
 224      }
 225  
 226      this.closest('form').each(function ()
 227      {
 228          var $this = $(this), form = {}, methods = {}, lib = {};
 229  
 230          if ($this.data('_txpMultiEdit')) {
 231              form = $this.data('_txpMultiEdit');
 232              opt = $.extend(form.opt, opt);
 233          } else {
 234              opt = $.extend(defaults, opt);
 235              form.boxes = opt.checkbox;
 236              form.editMethod = $this.find(opt.actions);
 237              form.lastCheck = null;
 238              form.opt = opt;
 239              form.selectAll = $this.find(opt.selectAll);
 240              form.button = $this.find(opt.submitButton);
 241          }
 242  
 243          /**
 244           * Registers a multi-edit option.
 245           *
 246           * @param  {object} options
 247           * @param  {string} options.label The option's label
 248           * @param  {string} options.value The option's value
 249           * @param  {string} options.html  The second step HTML
 250           * @return {object} methods
 251           */
 252  
 253          methods.addOption = function (options)
 254          {
 255              var settings = $.extend({
 256                  'label' : null,
 257                  'value' : null,
 258                  'html'  : null
 259              }, options);
 260  
 261              if (!settings.value) {
 262                  return methods;
 263              }
 264  
 265              var option = form.editMethod.find('option').filter(function ()
 266              {
 267                  return $(this).val() === settings.value;
 268              });
 269  
 270              var exists = (option.length > 0);
 271              form.editMethod.val('');
 272  
 273              if (!exists) {
 274                  option = $('<option />');
 275              }
 276  
 277              if (!option.data('_txpMultiMethod')) {
 278                  if (!option.val()) {
 279                      option.val(settings.value);
 280                  }
 281  
 282                  if (!option.text() && settings.label) {
 283                      option.text(settings.label);
 284                  }
 285  
 286                  option.data('_txpMultiMethod', settings.html);
 287              }
 288  
 289              if (!exists) {
 290                  form.editMethod.append(option);
 291              }
 292  
 293              return methods;
 294          };
 295  
 296          /**
 297           * Selects rows based on supplied arguments.
 298           *
 299           * Only one of the filters applies at a time.
 300           *
 301           * @param  {object}  options
 302           * @param  {array}   options.index   Indexes to select
 303           * @param  {array}   options.range   Select index range, takes [min, max]
 304           * @param  {array}   options.value   Values to select
 305           * @param  {boolean} options.checked TRUE to check, FALSE to uncheck
 306           * @return {object}  methods
 307           */
 308  
 309          methods.select = function (options)
 310          {
 311              var settings = $.extend({
 312                  'index'   : null,
 313                  'range'   : null,
 314                  'value'   : null,
 315                  'checked' : true
 316              }, options);
 317  
 318              var obj = $this.find(form.boxes);
 319  
 320              if (settings.value !== null) {
 321                  obj = obj.filter(function ()
 322                  {
 323                      return $.inArray($(this).val(), settings.value) !== -1;
 324                  });
 325              } else if (settings.index !== null) {
 326                  obj = obj.filter(function (index)
 327                  {
 328                      return $.inArray(index, settings.index) !== -1;
 329                  });
 330              } else if (settings.range !== null) {
 331                  obj = obj.slice(settings.range[0], settings.range[1]);
 332              }
 333  
 334              obj.prop('checked', settings.checked).change();
 335  
 336              return methods;
 337          };
 338  
 339          /**
 340           * Highlights selected rows.
 341           *
 342           * @return {object} lib
 343           */
 344  
 345          lib.highlight = function ()
 346          {
 347              var element = $this.find(form.boxes);
 348              element.filter(':checked').closest(opt.highlighted).addClass(opt.selectedClass);
 349              element.filter(':not(:checked)').closest(opt.highlighted).removeClass(opt.selectedClass);
 350              return lib;
 351          };
 352  
 353          /**
 354           * Extends click region to whole row.
 355           *
 356           * @return {object} lib
 357           */
 358  
 359          lib.extendedClick = function ()
 360          {
 361              if (opt.rowClick) {
 362                  var selector = opt.row;
 363              } else {
 364                  var selector = form.boxes;
 365              }
 366  
 367              $this.on('click', selector, function (e)
 368              {
 369                  var self = ($(e.target).is(form.boxes) || $(this).is(form.boxes));
 370  
 371                  if (!self && (e.target != this || $(this).is('a, :input') || $(e.target).is('a, :input'))) {
 372                      return;
 373                  }
 374  
 375                  if (!self && opt.altClick && !e.altKey && !e.ctrlKey) {
 376                      return;
 377                  }
 378  
 379                  var box = $(this).closest(opt.highlighted).find(form.boxes);
 380  
 381                  if (box.length < 1) {
 382                      return;
 383                  }
 384  
 385                  var checked = box.prop('checked');
 386  
 387                  if (self) {
 388                      checked = !checked;
 389                  }
 390  
 391                  if (e.shiftKey && form.lastCheck) {
 392                      var boxes = $this.find(form.boxes);
 393                      var start = boxes.index(box);
 394                      var end = boxes.index(form.lastCheck);
 395  
 396                      methods.select({
 397                          'range'   : [Math.min(start, end), Math.max(start, end) + 1],
 398                          'checked' : !checked
 399                      });
 400                  } else if (!self) {
 401                      box.prop('checked', !checked).change();
 402                  }
 403  
 404                  if (checked === false) {
 405                      form.lastCheck = box;
 406                  } else {
 407                      form.lastCheck = null;
 408                  }
 409              });
 410  
 411              return lib;
 412          };
 413  
 414          /**
 415           * Tracks row checks.
 416           *
 417           * @return {object} lib
 418           */
 419  
 420          lib.checked = function ()
 421          {
 422              $this.on('change', form.boxes, function (e)
 423              {
 424                  var box = $(this);
 425                  var boxes = $this.find(form.boxes);
 426  
 427                  if (box.prop('checked')) {
 428                      $(this).closest(opt.highlighted).addClass(opt.selectedClass);
 429                      $this.find(opt.selectAll).prop('checked', boxes.filter(':checked').length === boxes.length);
 430                  } else {
 431                      $(this).closest(opt.highlighted).removeClass(opt.selectedClass);
 432                      $this.find(opt.selectAll).prop('checked', false);
 433                  }
 434              });
 435  
 436              return lib;
 437          };
 438  
 439          /**
 440           * Handles edit method selecting.
 441           *
 442           * @return {object} lib
 443           */
 444  
 445          lib.changeMethod = function ()
 446          {
 447              form.button.hide();
 448  
 449              form.editMethod.val('').change(function (e)
 450              {
 451                  var selected = $(this).find('option:selected');
 452                  $this.find('.multi-step').remove();
 453  
 454                  if (selected.length < 1 || selected.val() === '') {
 455                      form.button.hide();
 456                      return lib;
 457                  }
 458  
 459                  if (selected.data('_txpMultiMethod')) {
 460                      $(this).after($('<div />').attr('class', 'multi-step multi-option').html(selected.data('_txpMultiMethod')));
 461                      form.button.show();
 462                  } else {
 463                      form.button.hide();
 464                      $(this).parents('form').submit();
 465                  }
 466              });
 467  
 468              return lib;
 469          };
 470  
 471          /**
 472           * Handles sending.
 473           *
 474           * @return {object} lib
 475           */
 476  
 477          lib.sendForm = function ()
 478          {
 479              $this.submit(function ()
 480              {
 481                  if (opt.confirmation !== false && verify(opt.confirmation) === false) {
 482                      form.editMethod.val('').change();
 483  
 484                      return false;
 485                  }
 486              });
 487  
 488              return lib;
 489          };
 490  
 491          if (!$this.data('_txpMultiEdit')) {
 492              lib.highlight().extendedClick().checked().changeMethod().sendForm();
 493  
 494              (function ()
 495              {
 496                  var multiOptions = $this.find('.multi-option:not(.multi-step)');
 497  
 498                  form.editMethod.find('option[value!=""]').each(function ()
 499                  {
 500                      var value = $(this).val();
 501  
 502                      var option = multiOptions.filter(function ()
 503                      {
 504                          return $(this).data('multi-option') === value;
 505                      });
 506  
 507                      if (option.length > 0) {
 508                          methods.addOption({
 509                              'label' : null,
 510                              'html'  : option.eq(0).contents(),
 511                              'value' : $(this).val()
 512                          });
 513                      }
 514                  });
 515  
 516                  multiOptions.remove();
 517              })();
 518  
 519              $this.on('change', opt.selectAll, function (e)
 520              {
 521                  methods.select({
 522                      'checked' : $(this).prop('checked')
 523                  });
 524              });
 525          }
 526  
 527          if (method && methods[method]) {
 528              methods[method].call($this, args);
 529          }
 530  
 531          $this.data('_txpMultiEdit', form);
 532      });
 533  
 534      return this;
 535  };
 536  
 537  /**
 538   * Adds an event handler.
 539   *
 540   * See jQuery before trying to use this.
 541   *
 542   * @author S.Andrew http://www.scottandrew.com/
 543   * @param {object}  elm        The element to attach to
 544   * @param {string}  evType     The event
 545   * @param {object}  fn         The callback function
 546   * @param {boolean} useCapture Initiate capture
 547   */
 548  
 549  function addEvent(elm, evType, fn, useCapture)
 550  {
 551      if (elm.addEventListener) {
 552          elm.addEventListener(evType, fn, useCapture);
 553          return true;
 554      } else if (elm.attachEvent) {
 555          var r = elm.attachEvent('on' + evType, fn);
 556          return r;
 557      } else {
 558          elm['on' + evType] = fn;
 559      }
 560  }
 561  
 562  /**
 563   * Sets a HTTP cookie.
 564   *
 565   * @param {string}  name  The name
 566   * @param {string}  value The value
 567   * @param {integer} days  Expires in
 568   */
 569  
 570  function setCookie(name, value, days)
 571  {
 572      if (days) {
 573          var date = new Date();
 574  
 575          date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
 576  
 577          var expires = '; expires=' + date.toGMTString();
 578      } else {
 579          var expires = '';
 580      }
 581  
 582      document.cookie = name + '=' + value + expires + '; path=/';
 583  }
 584  
 585  /**
 586   * Gets a HTTP cookie's value.
 587   *
 588   * @param  {string} name The name
 589   * @return {string} The cookie
 590   */
 591  
 592  function getCookie(name)
 593  {
 594      var nameEQ = name + '=';
 595      var ca = document.cookie.split(';');
 596  
 597      for (var i = 0; i < ca.length; i++) {
 598          var c = ca[i];
 599  
 600          while (c.charAt(0) == ' ') {
 601              c = c.substring(1, c.length);
 602          }
 603  
 604          if (c.indexOf(nameEQ) == 0) {
 605              return c.substring(nameEQ.length, c.length);
 606          }
 607      }
 608  
 609      return null;
 610  }
 611  
 612  /**
 613   * Deletes a HTTP cookie.
 614   *
 615   * @param {string} name The cookie
 616   */
 617  
 618  function deleteCookie(name)
 619  {
 620      setCookie(name, '', -1);
 621  }
 622  
 623  /**
 624   * Gets element by class.
 625   *
 626   * See jQuery before trying to use this.
 627   *
 628   * @param  {string} classname The HTML class
 629   * @param  {object} node      The node, defaults to the document
 630   * @return {object} Matching nodes
 631   * @see    http://www.snook.ca/archives/javascript/your_favourite_1/
 632   */
 633  
 634  function getElementsByClass(classname, node)
 635  {
 636      var a = [];
 637      var re = new RegExp('(^|\\s)' + classname + '(\\s|$)');
 638  
 639      if (node == null) {
 640          node = document;
 641      }
 642  
 643      var els = node.getElementsByTagName("*");
 644  
 645      for (var i = 0, j = els.length; i < j; i++) {
 646          if (re.test(els[i].className)) {
 647              a.push(els[i]);
 648          }
 649      }
 650  
 651      return a;
 652  }
 653  
 654  /**
 655   * Toggles panel's visibility and saves the state to the server.
 656   *
 657   * @param  {string}  id The element ID
 658   * @return {boolean} Returns FALSE
 659   */
 660  
 661  function toggleDisplay(id)
 662  {
 663      var obj = $('#' + id);
 664  
 665      if (obj.length) {
 666          obj.toggle();
 667  
 668          // Send state of toggle pane to localStorage or server.
 669          if ($(this).data('txp-pane')) {
 670              var pane = $(this).data('txp-pane');
 671  
 672              if (!window.localStorage && $(this).data('txp-token')) {
 673                  sendAsyncEvent({
 674                      event   : 'pane',
 675                      step    : 'visible',
 676                      pane    : $(this).data('txp-pane'),
 677                      visible : obj.is(':visible'),
 678                      origin  : textpattern.event,
 679                      token   : $(this).data('txp-token')
 680                  });
 681              }
 682          } else {
 683              var pane = obj.attr('id');
 684  
 685              if (!window.localStorage) {
 686                  sendAsyncEvent({
 687                      event   : textpattern.event,
 688                      step    : 'save_pane_state',
 689                      pane    : obj.attr('id'),
 690                      visible : obj.is(':visible')
 691                  });
 692              }
 693          }
 694  
 695          var data = new Object;
 696          data[pane] = obj.is(':visible');
 697          textpattern.storage.update(data);
 698      }
 699  
 700      return false;
 701  }
 702  
 703  /**
 704   * Direct show/hide referred #segment; decorate parent lever.
 705   */
 706  
 707  function toggleDisplayHref()
 708  {
 709      var $this = $(this);
 710      var href = $this.attr('href');
 711      var lever = $this.parent('.txp-summary');
 712  
 713      if (href) {
 714          toggleDisplay.call(this, href.substr(1));
 715      }
 716  
 717      if (lever.length) {
 718          var vis = $(href).is(':visible');
 719          lever.toggleClass('expanded', vis);
 720          $this.attr('aria-pressed', vis.toString());
 721          $(href).attr('aria-expanded', vis.toString());
 722      }
 723  
 724      return false;
 725  }
 726  
 727  /**
 728   * Shows/hides matching elements.
 729   *
 730   * @param {string}  className Targeted element's class
 731   * @param {boolean} show      TRUE to display
 732   */
 733  
 734  function setClassDisplay(className, show)
 735  {
 736      $('.' + className).toggle(show);
 737  }
 738  
 739  /**
 740   * Toggles panel's visibility and saves the state to a HTTP cookie.
 741   *
 742   * @param {string} classname The HTML class
 743   */
 744  
 745  function toggleClassRemember(className)
 746  {
 747      var v = getCookie('toggle_' + className);
 748      v = (v == 1 ? 0 : 1);
 749  
 750      setCookie('toggle_' + className, v, 365);
 751      setClassDisplay(className, v);
 752      setClassDisplay(className + '_neg', 1 - v);
 753  }
 754  
 755  /**
 756   * Toggle visibility of matching elements based on a cookie value.
 757   *
 758   * @param {string}  className The HTML class
 759   * @param {string}  force     The value
 760   */
 761  
 762  function setClassRemember(className, force)
 763  {
 764      if (typeof(force) != 'undefined') {
 765          setCookie('toggle_' + className, force, 365);
 766      }
 767  
 768      var v = getCookie('toggle_' + className);
 769      setClassDisplay(className, v);
 770      setClassDisplay(className + '_neg', 1 - v);
 771  }
 772  
 773  /**
 774   * Load data from the server using a HTTP POST request.
 775   *
 776   * @param  {object} data   POST payload
 777   * @param  {object} fn     Success handler
 778   * @param  {string} format Response data format, defaults to 'xml'
 779   * @return {object} this
 780   * @see    http://api.jquery.com/jQuery.post/
 781   */
 782  
 783  function sendAsyncEvent (data, fn, format)
 784  {
 785      var formdata = false;
 786      if ($.type(data) === 'string' && data.length > 0) {
 787          // Got serialized data.
 788          data = data + '&app_mode=async&_txp_token=' + textpattern._txp_token;
 789      } else if (data instanceof FormData) {
 790          formdata = true;
 791          data.append("app_mode", 'async');
 792          data.append("_txp_token", textpattern._txp_token);
 793      } else {
 794          data.app_mode = 'async';
 795          data._txp_token = textpattern._txp_token;
 796      }
 797  
 798      format = format || 'xml';
 799  
 800      return formdata ?
 801          $.ajax({
 802              type: "POST",
 803              url: 'index.php',
 804              data: data,
 805              success: fn,
 806              dataType: format,
 807              processData: false,
 808              contentType: false
 809          }) :
 810          $.post('index.php', data, fn, format);
 811  }
 812  
 813  /**
 814   * A pub/sub hub for client-side events.
 815   *
 816   * @since 4.5.0
 817   */
 818  
 819  textpattern.Relay =
 820  {
 821      /**
 822       * Publishes an event to all registered subscribers.
 823       *
 824       * @param  {string} event The event
 825       * @param  {object} data  The data passed to registered subscribers
 826       * @return {object} The Relay object
 827       * @example
 828       * textpattern.Relay.callback('newEvent', {'name1' : 'value1', 'name2' : 'value2'});
 829       */
 830  
 831      callback: function (event, data)
 832      {
 833          return $(this).trigger(event, data);
 834      },
 835  
 836      /**
 837       * Subscribes to an event.
 838       *
 839       * @param  {string} The event
 840       * @param  {object} fn  The callback function
 841       * @return {object} The Relay object
 842       * @example
 843       * textpattern.Relay.register('event',
 844       *     function (event, data)
 845       *     {
 846       *         alert(data);
 847       *     }
 848       * );
 849       */
 850  
 851      register: function (event, fn)
 852      {
 853          $(this).on(event, fn);
 854          return this;
 855      }
 856  };
 857  
 858  /**
 859   * Textpattern localStorage.
 860   *
 861   * @since 4.6.0
 862   */
 863  
 864  textpattern.storage =
 865  {
 866      /**
 867       * Textpattern localStorage data.
 868       */
 869  
 870      data : (window.localStorage ? JSON.parse(window.localStorage.getItem("textpattern")) : null) || {},
 871  
 872      /**
 873       * Updates data.
 874       *
 875       * @param   data The message
 876       * @example
 877       * textpattern.update({prefs : "site"});
 878       */
 879  
 880      update : function (data) {
 881  
 882          if (!window.localStorage) {
 883              return;
 884          }
 885  
 886          if (data) {
 887              $.extend(textpattern.storage.data, data);
 888              window.localStorage.setItem("textpattern", JSON.stringify(textpattern.storage.data));
 889          }
 890      }
 891  };
 892  
 893  /**
 894   * Logs debugging messages.
 895   *
 896   * @since 4.6.0
 897   */
 898  
 899  textpattern.Console =
 900  {
 901      /**
 902       * Stores an array of invoked messages.
 903       */
 904  
 905      history : [],
 906  
 907      /**
 908       * Logs a message.
 909       *
 910       * @param  message The message
 911       * @return textpattern.Console
 912       * @example
 913       * textpattern.Console.log('Some message');
 914       */
 915  
 916      log : function (message)
 917      {
 918          if (textpattern.production_status === 'debug') {
 919              textpattern.Console.history.push(message);
 920  
 921              textpattern.Relay.callback('txpConsoleLog', {
 922                  'message' : message
 923              });
 924          }
 925  
 926          return this;
 927      }
 928  };
 929  
 930  /**
 931   * Console API module for textpattern.Console.
 932   *
 933   * Passes invoked messages to Web/JavaScript Console
 934   * using console.log().
 935   *
 936   * Uses a namespaced 'txpConsoleLog.ConsoleAPI' event.
 937   */
 938  
 939  textpattern.Relay.register('txpConsoleLog.ConsoleAPI', function (event, data)
 940  {
 941      if ($.type(console) === 'object' && $.type(console.log) === 'function') {
 942          console.log(data.message);
 943      }
 944  });
 945  
 946  /**
 947   * Script routing.
 948   *
 949   * @since 4.6.0
 950   */
 951  
 952  textpattern.Route =
 953  {
 954      /**
 955       * An array of attached listeners.
 956       */
 957  
 958      attached : [],
 959  
 960      /**
 961       * Attaches a listener.
 962       *
 963       * @param {string} pages The page
 964       * @param {object} fn    The callback
 965       */
 966  
 967      add : function (pages, fn)
 968      {
 969          $.each(pages.split(','), function (index, page)
 970          {
 971              textpattern.Route.attached.push({
 972                  'page' : $.trim(page),
 973                  'fn'   : fn
 974              });
 975          });
 976      },
 977  
 978      /**
 979       * Initializes attached listeners.
 980       *
 981       * @param {object} options       Options
 982       * @param {string} options.event The event
 983       * @param {string} options.step  The step
 984       */
 985  
 986      init : function (options)
 987      {
 988          var options = $.extend({
 989              'event' : textpattern.event,
 990              'step'  : textpattern.step
 991          }, options);
 992  
 993          $.each(textpattern.Route.attached, function (index, data)
 994          {
 995              if (data.page === '' || data.page === options.event || data.page === options.event + '.' + options.step) {
 996                  data.fn({
 997                      'event' : options.event,
 998                      'step'  : options.step,
 999                      'route' : data.page
1000                  });
1001              }
1002          });
1003      }
1004  };
1005  
1006  /**
1007   * Sends a form using AJAX and processes the response.
1008   *
1009   * @param  {object} options          Options
1010   * @param  {string} options.dataType The response data type
1011   * @param  {object} options.success  The success callback
1012   * @param  {object} options.error    The error callback
1013   * @return {object} this
1014   * @since  4.5.0
1015   */
1016  
1017  jQuery.fn.txpAsyncForm = function (options)
1018  {
1019      options = $.extend({
1020          dataType : 'script',
1021          success  : null,
1022          error    : null
1023      }, options);
1024  
1025      // Send form data to application, process response as script.
1026      this.on('submit.txpAsyncForm', function (event)
1027      {
1028          event.preventDefault();
1029  
1030          var $this = $(this);
1031          var form =
1032          {
1033              button  : $this.find('input[type="submit"]:focus').eq(0),
1034              data    : ( window.FormData === undefined ? $this.serialize() : new FormData(this) ),
1035              spinner : $('<span />').addClass('spinner')
1036          };
1037  
1038          // Show feedback while processing.
1039          $this.addClass('busy');
1040          $('body').addClass('busy');
1041  
1042          // WebKit does not set :focus on button-click: use first submit input as a fallback.
1043          if (!form.button.length) {
1044              form.button = $this.find('input[type="submit"]').eq(0);
1045          }
1046  
1047          form.button.attr('disabled', true).after(form.spinner);
1048  
1049          if (form.data)
1050              if ( form.data instanceof FormData ) {
1051                  form.data.append(form.button.attr('name') || '_txp_submit' , form.button.val() || '_txp_submit');
1052              } else {
1053                  form.data += '&' + (form.button.attr('name') || '_txp_submit') + '=' + (form.button.val() || '_txp_submit');
1054              }
1055  
1056          sendAsyncEvent(form.data, function () {}, options.dataType)
1057              .done(function (data, textStatus, jqXHR)
1058              {
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              {
1073                  if (options.error) {
1074                      options.error($this, event, jqXHR, $.ajaxSetup(), errorThrown);
1075                  }
1076  
1077                  textpattern.Relay.callback('txpAsyncForm.error', {
1078                      'this'         : $this,
1079                      'event'        : event,
1080                      'jqXHR'        : jqXHR,
1081                      'ajaxSettings' : $.ajaxSetup(),
1082                      'thrownError'  : errorThrown
1083                  });
1084              })
1085              .always(function ()
1086              {
1087                  $this.removeClass('busy');
1088                  form.button.removeAttr('disabled');
1089                  form.spinner.remove();
1090                  $('body').removeClass('busy');
1091              });
1092      });
1093  
1094      return this;
1095  };
1096  
1097  /**
1098   * Sends a link using AJAX and processes the plain text response.
1099   *
1100   * @param  {object} options          Options
1101   * @param  {string} options.dataType The response data type
1102   * @param  {object} options.success  The success callback
1103   * @param  {object} options.error    The error callback
1104   * @return {object} this
1105   * @since  4.5.0
1106   */
1107  
1108  jQuery.fn.txpAsyncHref = function (options)
1109  {
1110      options = $.extend({
1111          dataType : 'text',
1112          success  : null,
1113          error    : null
1114      }, options);
1115  
1116      this.on('click.txpAsyncHref', function (event)
1117      {
1118          event.preventDefault();
1119          var $this = $(this);
1120          var url = this.search.replace('?', '') + '&' + $.param({value : $this.text()});
1121  
1122          // Show feedback while processing.
1123          $this.addClass('busy');
1124          $('body').addClass('busy');
1125  
1126          sendAsyncEvent(url, function () {}, options.dataType)
1127              .done(function (data, textStatus, jqXHR)
1128              {
1129                  if (options.dataType === 'text') {
1130                      $this.html(data);
1131                  }
1132  
1133                  if (options.success) {
1134                      options.success($this, event, data, textStatus, jqXHR);
1135                  }
1136  
1137                  textpattern.Relay.callback('txpAsyncHref.success', {
1138                      'this'       : $this,
1139                      'event'      : event,
1140                      'data'       : data,
1141                      'textStatus' : textStatus,
1142                      'jqXHR'      : jqXHR
1143                  });
1144              })
1145              .fail(function (jqXHR, textStatus, errorThrown)
1146              {
1147                  if (options.error) {
1148                      options.error($this, event, jqXHR, $.ajaxSetup(), errorThrown);
1149                  }
1150  
1151                  textpattern.Relay.callback('txpAsyncHref.error', {
1152                      'this'         : $this,
1153                      'event'        : event,
1154                      'jqXHR'        : jqXHR,
1155                      'ajaxSettings' : $.ajaxSetup(),
1156                      'thrownError'  : errorThrown
1157                  });
1158              })
1159              .always(function ()
1160              {
1161                  $this.removeClass('busy');
1162                  $('body').removeClass('busy');
1163              });
1164      });
1165  
1166      return this;
1167  };
1168  
1169  /**
1170   * Sends a link using AJAX and processes the HTML response.
1171   *
1172   * @param  {object} options          Options
1173   * @param  {string} options.dataType The response data type
1174   * @param  {object} options.success  The success callback
1175   * @param  {object} options.error    The error callback
1176   * @return {object} this
1177   * @since  4.6.0
1178   */
1179  
1180  function txpAsyncLink(event)
1181  {
1182      event.preventDefault();
1183      var $this = $(event.target);
1184      var url = $this.attr('href').replace('?', '');
1185  
1186      // Show feedback while processing.
1187      $this.addClass('busy');
1188      $('body').addClass('busy');
1189  
1190      sendAsyncEvent(url, function () {}, 'html')
1191          .done(function (data, textStatus, jqXHR)
1192          {
1193              textpattern.Relay.callback('txpAsyncLink.success', {
1194                  'this'       : $this,
1195                  'event'      : event,
1196                  'data'       : data,
1197                  'textStatus' : textStatus,
1198                  'jqXHR'      : jqXHR
1199              });
1200          })
1201          .fail(function (jqXHR, textStatus, errorThrown)
1202          {
1203              textpattern.Relay.callback('txpAsyncLink.error', {
1204                  'this'         : $this,
1205                  'event'        : event,
1206                  'jqXHR'        : jqXHR,
1207                  'ajaxSettings' : $.ajaxSetup(),
1208                  'thrownError'  : errorThrown
1209              });
1210          })
1211          .always(function ()
1212          {
1213              $this.removeClass('busy');
1214              $('body').removeClass('busy');
1215          });
1216  
1217      return this;
1218  };
1219  
1220  /**
1221   * Creates a UI dialog.
1222   *
1223   * @param  {object} options Options
1224   * @return {object} this
1225   * @since  4.6.0
1226   */
1227  
1228  jQuery.fn.txpDialog = function (options)
1229  {
1230      options = $.extend({
1231          autoOpen : false,
1232          buttons  : [
1233              {
1234                  text  : textpattern.gTxt('ok'),
1235                  click : function ()
1236                  {
1237                      // callbacks?
1238  
1239                      if ($(this).is('form')) {
1240                          $(this).submit();
1241                      }
1242  
1243                      $(this).dialog('close');
1244                  }
1245              }
1246          ]
1247      }, options);
1248  
1249      this.dialog(options);
1250  
1251      return this;
1252  };
1253  
1254  /**
1255   * Creates a date picker.
1256   *
1257   * @param  {object} options Options
1258   * @return {object} this
1259   * @since  4.6.0
1260   */
1261  
1262  jQuery.fn.txpDatepicker = function (options)
1263  {
1264      // TODO $.datepicker.regional[ "en" ];
1265      // TODO support from RTL languages
1266      this.datepicker(options);
1267  
1268      return this;
1269  };
1270  
1271  /**
1272   * Creates a sortable element.
1273   *
1274   * This method creates a sortable widget, allowing to
1275   * reorder elements in a list and synchronizes the updated
1276   * order with the server.
1277   *
1278   * @param  {object}  options
1279   * @param  {string}  options.dataType The response datatype
1280   * @param  {object}  options.success  The sync success callback
1281   * @param  {object}  options.error    The sync error callback
1282   * @param  {string}  options.event    The event
1283   * @param  {string}  options.step     The step
1284   * @param  {string}  options.cancel   Prevents sorting if you start on elements matching the selector
1285   * @param  {integer} options.delay    Sorting delay
1286   * @param  {integer} options.distance Tolerance, in pixels, for when sorting should start
1287   * @return this
1288   * @since  4.6.0
1289   */
1290  
1291  jQuery.fn.txpSortable = function (options)
1292  {
1293      options = $.extend({
1294          dataType : 'script',
1295          success  : null,
1296          error    : null,
1297          event    : textpattern.event,
1298          step     : 'sortable_save',
1299          cancel   : ':input, button',
1300          delay    : 0,
1301          distance : 15,
1302          items    : '[data-txp-sortable-id]'
1303      }, options);
1304  
1305      var methods =
1306      {
1307          /**
1308           * Sends updated order to the server.
1309           */
1310  
1311          update : function ()
1312          {
1313              var ids = [], $this = $(this);
1314  
1315              $this.children('[data-txp-sortable-id]').each(function ()
1316              {
1317                  ids.push($(this).data('txp-sortable-id'));
1318              });
1319  
1320              if (ids) {
1321                  sendAsyncEvent({
1322                      event : options.event,
1323                      step  : options.step,
1324                      order : ids
1325                  }, function () {}, options.dataType)
1326                      .done(function (data, textStatus, jqXHR)
1327                      {
1328                          if (options.success) {
1329                              options.success.call($this, data, textStatus, jqXHR);
1330                          }
1331  
1332                          textpattern.Relay.callback('txpSortable.success', {
1333                              'this'       : $this,
1334                              'data'       : data,
1335                              'textStatus' : textStatus,
1336                              'jqXHR'      : jqXHR
1337                          });
1338                      })
1339                      .fail(function (jqXHR, textStatus, errorThrown)
1340                      {
1341                          if (options.error) {
1342                              options.error.call($this, jqXHR, $.ajaxSetup(), errorThrown);
1343                          }
1344  
1345                          textpattern.Relay.callback('txpSortable.error', {
1346                              'this'         : $this,
1347                              'jqXHR'        : jqXHR,
1348                              'ajaxSettings' : $.ajaxSetup(),
1349                              'thrownError'  : errorThrown
1350                          });
1351                      });
1352              }
1353          }
1354      };
1355  
1356      return this.sortable({
1357          cancel   : options.cancel,
1358          delay    : options.delay,
1359          distance : options.distance,
1360          update   : methods.update,
1361          items    : options.items
1362      });
1363  };
1364  
1365  
1366  /**
1367   * Password strength meter.
1368   *
1369   * @since 4.6.0
1370   * @param  {object}  options
1371   * @param  {array}   options.gtxt_prefix  gTxt() string prefix
1372   * @todo  Pass in name/email via 'options' to be injected in user_inputs[]
1373   */
1374  
1375  textpattern.passwordStrength = function (options)
1376  {
1377      jQuery('form').on('keyup', 'input.txp-strength-hint', function() {
1378          var settings = $.extend({
1379              'gtxt_prefix' : ''
1380          }, options);
1381  
1382          var me = jQuery(this);
1383          var pass = me.val();
1384          var passResult = zxcvbn(pass, user_inputs=[]);
1385          var strengthMap = {
1386              "0": {
1387                  "width": "5"
1388              },
1389              "1": {
1390                  "width": "28"
1391              },
1392              "2": {
1393                  "width": "50"
1394              },
1395              "3": {
1396                  "width": "75"
1397              },
1398              "4": {
1399                  "width": "100"
1400              }
1401          };
1402  
1403          var offset = strengthMap[passResult.score];
1404          var meter = me.siblings('.strength-meter');
1405          meter.empty();
1406  
1407          if (pass.length > 0) {
1408              meter.append('<div class="bar"></div><div class="indicator">' + textpattern.gTxt(settings.gtxt_prefix+'password_strength_'+passResult.score) + '</div>');
1409          }
1410  
1411          meter
1412              .find('.bar')
1413              .attr('class', 'bar password-strength-'+passResult.score)
1414              .css('width', offset.width+'%');
1415      });
1416  }
1417  
1418  /**
1419   * Mask/unmask password input field.
1420   *
1421   * @since  4.6.0
1422   */
1423  
1424  textpattern.passwordMask = function()
1425  {
1426      $('form').on('click', '#show_password', function() {
1427          var inputBox = $(this).closest('form').find('input.txp-maskable');
1428          var newType = (inputBox.attr('type') === 'password') ? 'text' : 'password';
1429          textpattern.changeType(inputBox, newType);
1430      });
1431  }
1432  
1433  /**
1434   * Change the type of an input element.
1435   *
1436   * @param  {object} elem The <input/> element
1437   * @param  {string} type The desired type
1438   *
1439   * @see    https://gist.github.com/3559343 for original
1440   * @since  4.6.0
1441   */
1442  
1443  textpattern.changeType = function(elem, type)
1444  {
1445      if (elem.prop('type') === type) {
1446          // Already the correct type.
1447          return elem;
1448      }
1449  
1450      try {
1451          // May fail if browser prevents it.
1452          return elem.prop('type', type);
1453      } catch(e) {
1454          // Create the element by hand.
1455          // Clone it via a div (jQuery has no html() method for an element).
1456          var html = $("<div>").append(elem.clone()).html();
1457  
1458          // Match existing attributes of type=text or type="text".
1459          var regex = /type=(\")?([^\"\s]+)(\")?/;
1460  
1461          // If no match, add the type attribute to the end; otherwise, replace it.
1462          var tmp = $(html.match(regex) == null ?
1463              html.replace(">", ' type="' + type + '">') :
1464              html.replace(regex, 'type="' + type + '"'));
1465  
1466          // Copy data from old element.
1467          tmp.data('type', elem.data('type'));
1468          var events = elem.data('events');
1469          var cb = function(events) {
1470              return function() {
1471                  // Re-bind all prior events.
1472                  for(var idx in events) {
1473                      var ydx = events[idx];
1474  
1475                      for(var jdx in ydx) {
1476                          tmp.bind(idx, ydx[jdx].handler);
1477                      }
1478                  }
1479              }
1480          }(events);
1481  
1482          elem.replaceWith(tmp);
1483  
1484          // Wait a smidge before firing callback.
1485          setTimeout(cb, 10);
1486  
1487          return tmp;
1488      }
1489  }
1490  
1491  /**
1492   * Encodes a string for a use in HTML.
1493   *
1494   * @param  {string} string The string
1495   * @return {string} Encoded string
1496   * @since  4.6.0
1497   */
1498  
1499  textpattern.encodeHTML = function (string)
1500  {
1501      return $('<div/>').text(string).html();
1502  };
1503  
1504  /**
1505   * Translates given substrings.
1506   *
1507   * @param  {string} string       The string being translated
1508   * @param  {object} replacements Translated substrings
1509   * @return string   Translated string
1510   * @since  4.6.0
1511   * @example
1512   * textpattern.tr('hello world, and bye!', {'hello' : 'bye', 'bye' : 'hello'});
1513   */
1514  
1515  textpattern.tr = function (string, replacements)
1516  {
1517      var match, position, output = '', replacement;
1518  
1519      for (position = 0; position < string.length; position++) {
1520          match = false;
1521  
1522          $.each(replacements, function (from, to)
1523          {
1524              if (string.substr(position, from.length) === from) {
1525                  match = true;
1526                  replacement = to;
1527                  position = (position + from.length) - 1;
1528  
1529                  return;
1530              }
1531          });
1532  
1533          if (match) {
1534              output += replacement;
1535          } else {
1536              output += string.charAt(position);
1537          }
1538      }
1539  
1540      return output;
1541  };
1542  
1543  /**
1544   * Returns an i18n string.
1545   *
1546   * @param  {string}  i18n   The i18n string
1547   * @param  {object}  atts   Replacement map
1548   * @param  {boolean} escape TRUE to escape HTML in atts
1549   * @return {string}  The string
1550   * @example
1551   * textpattern.gTxt('string', {'{name}' : 'example'}, true);
1552   */
1553  
1554  textpattern.gTxt = function (i18n, atts, escape)
1555  {
1556      var tags = atts || {};
1557      var string = i18n;
1558      var name = string.toLowerCase();
1559  
1560      if ($.type(textpattern.textarray[name]) !== 'undefined') {
1561          string = textpattern.textarray[name];
1562      }
1563  
1564      if (escape !== false) {
1565          string = textpattern.encodeHTML(string);
1566  
1567          $.each(tags, function (key, value)
1568          {
1569              tags[key] = textpattern.encodeHTML(value);
1570          });
1571      }
1572  
1573      string = textpattern.tr(string, tags);
1574  
1575      return string;
1576  };
1577  
1578  /**
1579   * Replaces HTML contents of each matched with i18n string.
1580   *
1581   * This is a jQuery plugin for textpattern.gTxt().
1582   *
1583   * @param  {object|string}  options        Options or the i18n string
1584   * @param  {string}         options.string The i18n string
1585   * @param  {object}         options.tags   Replacement map
1586   * @param  {boolean}        options.escape TRUE to escape HTML in tags
1587   * @param  {object}         tags           Replacement map
1588   * @param  {boolean}        escape         TRUE to escape HTML in tags
1589   * @return {object}         this
1590   * @see    textpattern.gTxt()
1591   * @example
1592   * $('p').gTxt('string').class('alert-block warning');
1593   */
1594  
1595  jQuery.fn.gTxt = function (opts, tags, escape)
1596  {
1597      var options = $.extend({
1598          'string' : opts,
1599          'tags'   : tags,
1600          'escape' : escape
1601      }, opts);
1602  
1603      this.html(textpattern.gTxt(options.string, options.tags, options.escape));
1604  
1605      return this;
1606  };
1607  
1608  /**
1609   * ESC button closes alert messages.
1610   *
1611   * @since 4.5.0
1612   */
1613  
1614  $(document).keyup(function (e)
1615  {
1616      if (e.keyCode == 27) {
1617          $('.close').parent().remove();
1618      }
1619  });
1620  
1621  /**
1622   * Search tool.
1623   *
1624   * @since 4.6.0
1625   */
1626  
1627  function txp_search()
1628  {
1629      var $ui = $('.txp-search');
1630  
1631      $ui.find('.txp-search-button').button({
1632          showLabel: false,
1633          icon: 'ui-icon-search'
1634      }).click(function ()
1635      {
1636          $ui.submit();
1637      });
1638  
1639      $ui.find('.txp-search-options').button({
1640          showLabel: false,
1641          icon: 'ui-icon-triangle-1-s'
1642      }).on('click', function (e)
1643      {
1644          if (langdir === 'rtl') {
1645              var menu = $ui.find('.txp-dropdown').toggle().position(
1646              {
1647                  my: "left top",
1648                  at: "left bottom",
1649                  of: this
1650              });
1651          } else {
1652              var menu = $ui.find('.txp-dropdown').toggle().position(
1653              {
1654                  my: "right top",
1655                  at: "right bottom",
1656                  of: this
1657              });
1658          };
1659  
1660          $(document).one('click blur', function ()
1661          {
1662              menu.hide();
1663          });
1664  
1665          return false;
1666      });
1667  
1668      $ui.find('.txp-search-buttons').controlgroup();
1669      $ui.find('.txp-dropdown').hide().menu().click(function (e) {
1670          e.stopPropagation();
1671      });
1672  
1673      $ui.txpMultiEditForm({
1674          'checkbox'    : 'input[name="search_method[]"][type=checkbox]',
1675          'row'         : '.txp-dropdown li',
1676          'highlighted' : '.txp-dropdown li',
1677          'confirmation': false
1678      });
1679  }
1680  
1681  /**
1682   * Set expanded/collapsed nature of all twisty boxes in a panel.
1683   *
1684   * The direction can either be 'expand' or 'collapse', passed
1685   * in as an argument to the handler.
1686   *
1687   * @param  {event} ev Event that triggered the function
1688   * @since  4.6.0
1689   */
1690  
1691  function txp_expand_collapse_all(ev) {
1692      ev.preventDefault();
1693  
1694      var direction = ev.data.direction,
1695          container = ev.data.container || (ev.delegateTarget == ev.target ? 'body' : ev.delegateTarget);
1696  
1697      $(container).find('.txp-summary a').each(function (i, elm) {
1698          var $elm = $(elm);
1699  
1700          if (direction === 'collapse') {
1701              if ($elm.parent(".txp-summary").hasClass("expanded")) {
1702                  $elm.click();
1703              }
1704          } else {
1705              if (!$elm.parent(".txp-summary").hasClass("expanded")) {
1706                  $elm.click();
1707              }
1708          }
1709      });
1710  }
1711  
1712  /**
1713   * Restore sub-panel twistys to their as-stored state.
1714   *
1715   * @return {[type]} [description]
1716   */
1717  jQuery.fn.restorePanes = function ()
1718  {
1719      // Initialize dynamic WAI-ARIA attributes.
1720      $(this).find('.txp-summary a').each(function (i, elm)
1721      {
1722          // Get id of toggled <section> region.
1723          var $elm = $(elm), region = $elm.attr('href');
1724  
1725          if (region) {
1726  
1727              var $region = $(region);
1728              region = region.substr(1);
1729  
1730              var pane = $elm.data("txp-pane");
1731  
1732              if (pane === undefined) {
1733                  pane = region;
1734              }
1735  
1736              if (textpattern.storage.data[pane] !== undefined) {
1737                  if (textpattern.storage.data[pane]) {
1738                      $elm.parent(".txp-summary").addClass("expanded");
1739                      $region.show();
1740                  } else {
1741                      $elm.parent(".txp-summary").removeClass("expanded");
1742                      $region.hide();
1743                  }
1744              }
1745  
1746              var vis = $region.is(':visible').toString();
1747              $elm.attr('aria-controls', region).attr('aria-pressed', vis);
1748              $region.attr('aria-expanded', vis);
1749          }
1750      });
1751  }
1752  
1753  /**
1754   * Cookie status.
1755   *
1756   * @deprecated in 4.6.0
1757   */
1758  
1759  var cookieEnabled = true;
1760  
1761  // Setup panel.
1762  
1763  textpattern.Route.add('setup', function ()
1764  {
1765      textpattern.passwordMask();
1766      textpattern.passwordStrength({
1767          'gtxt_prefix' : 'setup_'
1768      });
1769  });
1770  
1771  // Login panel.
1772  
1773  textpattern.Route.add('login', function ()
1774  {
1775      // Check cookies.
1776      if (!checkCookies()) {
1777          cookieEnabled = false;
1778          $('main').prepend($('<p class="alert-block warning" />').text(textpattern.gTxt('cookies_must_be_enabled')));
1779      }
1780  
1781      // Focus on either username or password when empty.
1782      $('#login_form input').each(function() {
1783          if (this.value === '') {
1784              this.focus();
1785              return false;
1786          }
1787      });
1788  
1789      textpattern.passwordMask();
1790      textpattern.passwordStrength();
1791  });
1792  
1793  // Write panel.
1794  
1795  textpattern.Route.add('article', function ()
1796  {
1797      // Assume users would not change the timestamp if they wanted to
1798      // 'publish now'/'reset time'.
1799      $(document).on('change',
1800          '#write-timestamp input.year,' +
1801          '#write-timestamp input.month,' +
1802          '#write-timestamp input.day,' +
1803          '#write-timestamp input.hour,' +
1804          '#write-timestamp input.minute,' +
1805          '#write-timestamp input.second',
1806          function ()
1807          {
1808              $('#publish_now').prop('checked', false);
1809              $('#reset_time').prop('checked', false);
1810          }
1811      );
1812  
1813      var status = $('select[name=Status]'), form = status.parents('form'), submitButton = form.find('input[type=submit]');
1814  
1815      status.change(function ()
1816      {
1817          if (!form.hasClass('published')) {
1818              if ($(this).val() < 4) {
1819                  submitButton.val(textpattern.gTxt('save'));
1820              } else {
1821                  submitButton.val(textpattern.gTxt('publish'));
1822              }
1823          }
1824      });
1825  
1826      $('.txp-actions').on('click', '.txp-clone', function (e)
1827      {
1828          e.preventDefault();
1829          form.append('<input type="hidden" name="copy" value="1" />'+
1830              '<input type="hidden" name="publish" value="1" />');
1831          form.off('submit.txpAsyncForm').trigger('submit');
1832      });
1833  
1834      // Switch to Text/HTML/Preview mode.
1835      $(document).on('click',
1836          '[data-view-mode]',
1837          function (e)
1838          {
1839              e.preventDefault();
1840              $('input[name="view"]').val($(this).data('view-mode'));
1841              document.article_form.submit();
1842          }
1843      );
1844  });
1845  
1846  // Uncheck reset on timestamp change.
1847  
1848  textpattern.Route.add('article, file', function ()
1849  {
1850      $(document).on('change', '.posted input', function (e)
1851      {
1852          $('#publish_now, #reset_time').prop('checked', false);
1853      });
1854  });
1855  
1856  // 'Clone' button on Pages, Forms, Styles panels.
1857  
1858  textpattern.Route.add('css, page, form', function ()
1859  {
1860      $('.txp-clone').click(function (e)
1861      {
1862          e.preventDefault();
1863          var target = $(this).data('form');
1864          if (target) {
1865              $('#'+target).append('<input type="hidden" name="copy" value="1" />');
1866              $('.txp-save input').click();
1867          }
1868      });
1869  });
1870  
1871  // Tagbuilder.
1872  
1873  textpattern.Route.add('page, form, file, image', function ()
1874  {
1875      // Set up asynchronous tag builder links.
1876      textpattern.Relay.register('txpAsyncLink.success', function (event, data)
1877      {
1878          $('#tagbuild_links').dialog('close').html($(data['data'])).dialog('open').restorePanes();
1879          $('#txp-tagbuilder-output').select();
1880      });
1881  
1882      textpattern.Relay.register('txpAsyncForm.success', function (event, data)
1883      {
1884          $('#tagbuild_links').html($(data['data']));
1885          $('#txp-tagbuilder-output').select();
1886      });
1887  
1888      $('#tagbuild_links, .files_detail, .images_detail').on('click', '.txp-tagbuilder-link', function(ev) {
1889          txpAsyncLink(ev);
1890      });
1891  
1892      $('#tagbuild_links').dialog({
1893          dialogClass: 'txp-tagbuilder-container',
1894          autoOpen: false,
1895          focus: function(ev, ui) {
1896              $(ev.target).closest('.ui-dialog').focus();
1897          }
1898      });
1899  
1900      $('.txp-tagbuilder-dialog').on('click', function(ev) {
1901          ev.preventDefault();
1902          if ($("#tagbuild_links").dialog('isOpen')) {
1903              $("#tagbuild_links").dialog('close');
1904          } else {
1905              $("#tagbuild_links").dialog('open');
1906          }
1907      });
1908  
1909      // Set up delegated asynchronous tagbuilder form submission.
1910      $('#tagbuild_links').on('click', 'form.asynchtml input[type="submit"]', function(ev) {
1911          $(this).closest('form.asynchtml').txpAsyncForm({
1912              dataType: 'html',
1913              error: function ()
1914              {
1915                  window.alert(textpattern.gTxt('form_submission_error'));
1916              },
1917              success: function()
1918              {
1919              }
1920          });
1921      });
1922  });
1923  
1924  // Forms panel.
1925  
1926  textpattern.Route.add('form', function ()
1927  {
1928      $('#allforms_form').txpMultiEditForm({
1929          'checkbox'    : 'input[name="selected_forms[]"][type=checkbox]',
1930          'row'         : '.switcher-list li, .form-list-name',
1931          'highlighted' : '.switcher-list li'
1932      });
1933  });
1934  
1935  // Admin panel.
1936  
1937  textpattern.Route.add('admin', function ()
1938  {
1939      textpattern.passwordMask();
1940      textpattern.passwordStrength();
1941  });
1942  
1943  // Plugins panel.
1944  
1945  textpattern.Route.add('plugin', function ()
1946  {
1947      textpattern.Relay.register('txpAsyncHref.success', function (event, data)
1948      {
1949          $(data['this']).closest('tr').toggleClass('active');
1950      });
1951  });
1952  
1953  // All panels?
1954  
1955  textpattern.Route.add('', function ()
1956  {
1957      // Collapse/Expand all support.
1958      $('#supporting_content, #tagbuild_links, #content_switcher').on('click', '.txp-collapse-all', {direction: 'collapse'}, txp_expand_collapse_all)
1959          .on('click', '.txp-expand-all', {direction: 'expand'}, txp_expand_collapse_all);
1960  
1961      // Pane states
1962      var prefsGroup = $('form:has(.switcher-list li a[data-txp-pane])');
1963  
1964      if (prefsGroup.length == 0) {
1965          return;
1966      }
1967  
1968      var prefTabs = prefsGroup.find('.switcher-list li');
1969      var $switchers = prefTabs.children('a[data-txp-pane]');
1970      var $section = window.location.hash ? prefsGroup.find($(window.location.hash).closest('section')) : [];
1971  
1972      if ($section.length) {
1973          selectedTab = $section.index();
1974      }
1975      else if (textpattern.storage.data[textpattern.event] !== undefined) {
1976          $switchers.each(function (i, elm) {
1977              if ($(elm).data('txp-pane') == textpattern.storage.data[textpattern.event]) {
1978                  selectedTab = i;
1979                  $(elm).parent().addClass('ui-tabs-active ui-state-active');
1980              } else {
1981                  $(elm).parent().removeClass('ui-tabs-active ui-state-active');
1982              }
1983          });
1984      }
1985  
1986      if (selectedTab === undefined) {
1987          selectedTab = 0;
1988      }
1989  
1990      prefsGroup.tabs({active: selectedTab}).removeClass('ui-widget ui-widget-content ui-corner-all').addClass('ui-tabs-vertical');
1991      prefsGroup.find('.switcher-list').removeClass('ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all');
1992      prefTabs.removeClass('ui-state-default ui-corner-top');
1993      prefsGroup.find('.txp-prefs-group').removeClass('ui-widget-content ui-corner-bottom');
1994  
1995      prefTabs.on('click focus', function(ev)
1996      {
1997          var me = $(this).children('a[data-txp-pane]');
1998  
1999          if (!window.localStorage) sendAsyncEvent({
2000              event  : 'pane',
2001              step   : 'tabVisible',
2002              pane   : me.data('txp-pane'),
2003              origin : textpattern.event,
2004              token  : me.data('txp-token')
2005          });
2006  
2007          var data = new Object;
2008          data[textpattern.event] = me.data('txp-pane');
2009          textpattern.storage.update(data);
2010      });
2011  });
2012  
2013  // Initialize JavaScript.
2014  
2015  $(document).ready(function ()
2016  {
2017      // Confirmation dialogs.
2018      $(document).on('click.txpVerify', 'a[data-verify]', function (e)
2019      {
2020          return verify($(this).data('verify'));
2021      });
2022  
2023      $(document).on('submit.txpVerify', 'form[data-verify]', function (e)
2024      {
2025          return verify($(this).data('verify'));
2026      });
2027  
2028      // Disable spellchecking on all elements of class "code" in capable browsers.
2029      var c = $(".code")[0];
2030  
2031      if (c && "spellcheck" in c) {
2032          $(".code").prop("spellcheck", false);
2033      }
2034  
2035      // Enable spellcheck for all elements mentioned in textpattern.do_spellcheck.
2036      c = $(textpattern.do_spellcheck)[0];
2037  
2038      if (c && "spellcheck" in c) {
2039          $(textpattern.do_spellcheck).prop("spellcheck", true);
2040      }
2041  
2042      // Attach toggle behaviours.
2043      $(document).on('click', '.txp-summary a[class!=pophelp]', toggleDisplayHref);
2044  
2045      // Attach multi-edit form.
2046      $('.multi_edit_form').txpMultiEditForm();
2047  
2048      // Establish AJAX timeout from prefs.
2049      if ($.ajaxSetup().timeout === undefined) {
2050          $.ajaxSetup({timeout : textpattern.ajax_timeout});
2051      }
2052  
2053      // Set up asynchronous forms.
2054      $('form.async').txpAsyncForm({
2055          error: function ()
2056          {
2057              window.alert(textpattern.gTxt('form_submission_error'));
2058          }
2059      });
2060  
2061      // Set up asynchronous links.
2062      $('a.async:not(.script)').txpAsyncHref({
2063          error: function ()
2064          {
2065              window.alert(textpattern.gTxt('form_submission_error'));
2066          }
2067      });
2068  
2069      $('a.async.script').txpAsyncHref({
2070          dataType : 'script',
2071          error    : function ()
2072          {
2073              window.alert(textpattern.gTxt('form_submission_error'));
2074          }
2075      });
2076  
2077      // Close button on the announce pane.
2078      $(document).on('click', '.close', function (e)
2079      {
2080          e.preventDefault();
2081          $(this).parent().remove();
2082      });
2083  
2084      $('body').restorePanes();
2085  
2086      // Hide popup elements.
2087      $('.txp-dropdown').hide();
2088  
2089      // Event handling and automation.
2090      $(document).on('change.txpAutoSubmit', 'form [data-submit-on="change"]', function (e)
2091      {
2092          $(this).parents('form').submit();
2093      });
2094  
2095      // Polyfills.
2096      // Add support for form attribute in submit buttons.
2097      if ($('html').hasClass('no-formattribute')) {
2098          $('.txp-save input[form]').click(function(e) {
2099              var targetForm = $(this).attr('form');
2100              $('form[id='+targetForm+']').submit();
2101          });
2102      }
2103  
2104      // Establish UI defaults.
2105      $('.txp-dialog').txpDialog();
2106      $('.txp-dialog.modal').dialog('option', 'modal', true);
2107      $('.txp-datepicker').txpDatepicker();
2108      $('.txp-sortable').txpSortable();
2109  
2110  
2111  
2112      // TODO: integrate jQuery UI stuff properly --------------------------------
2113  
2114  
2115      // Selectmenu
2116      $('.jquery-ui-selectmenu').selectmenu();
2117  
2118      // Button
2119      $('.jquery-ui-button').button();
2120  
2121      // Button set
2122      $('.jquery-ui-controlgroup').controlgroup();
2123  
2124  
2125      // TODO: end integrate jQuery UI stuff properly ----------------------------
2126  
2127  
2128  
2129      // Find and open associated dialogs.
2130      $(document).on('click.txpDialog', '[data-txp-dialog]', function (e)
2131      {
2132          $($(this).data('txp-dialog')).dialog('open');
2133          e.preventDefault();
2134      });
2135  
2136      // Initialize panel specific JavaScript.
2137      textpattern.Route.init();
2138  
2139      // Arm UI.
2140      $('body').removeClass('not-ready');
2141  });

title

Description

title

Description

title

Description

title

title

Body