[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-admin/js/ -> theme-plugin-editor.js (source)

   1  /**
   2   * @output wp-admin/js/theme-plugin-editor.js
   3   */
   4  
   5  /* eslint no-magic-numbers: ["error", { "ignore": [-1, 0, 1] }] */
   6  
   7  if ( ! window.wp ) {
   8      window.wp = {};
   9  }
  10  
  11  wp.themePluginEditor = (function( $ ) {
  12      'use strict';
  13      var component, TreeLinks,
  14          __ = wp.i18n.__, _n = wp.i18n._n, sprintf = wp.i18n.sprintf;
  15  
  16      component = {
  17          codeEditor: {},
  18          instance: null,
  19          noticeElements: {},
  20          dirty: false,
  21          lintErrors: []
  22      };
  23  
  24      /**
  25       * Initialize component.
  26       *
  27       * @since 4.9.0
  28       *
  29       * @param {jQuery}         form - Form element.
  30       * @param {Object}         settings - Settings.
  31       * @param {Object|boolean} settings.codeEditor - Code editor settings (or `false` if syntax highlighting is disabled).
  32       * @return {void}
  33       */
  34      component.init = function init( form, settings ) {
  35  
  36          component.form = form;
  37          if ( settings ) {
  38              $.extend( component, settings );
  39          }
  40  
  41          component.noticeTemplate = wp.template( 'wp-file-editor-notice' );
  42          component.noticesContainer = component.form.find( '.editor-notices' );
  43          component.submitButton = component.form.find( ':input[name=submit]' );
  44          component.spinner = component.form.find( '.submit .spinner' );
  45          component.form.on( 'submit', component.submit );
  46          component.textarea = component.form.find( '#newcontent' );
  47          component.textarea.on( 'change', component.onChange );
  48          component.warning = $( '.file-editor-warning' );
  49          component.docsLookUpButton = component.form.find( '#docs-lookup' );
  50          component.docsLookUpList = component.form.find( '#docs-list' );
  51  
  52          if ( component.warning.length > 0 ) {
  53              component.showWarning();
  54          }
  55  
  56          if ( false !== component.codeEditor ) {
  57              /*
  58               * Defer adding notices until after DOM ready as workaround for WP Admin injecting
  59               * its own managed dismiss buttons and also to prevent the editor from showing a notice
  60               * when the file had linting errors to begin with.
  61               */
  62              _.defer( function() {
  63                  component.initCodeEditor();
  64              } );
  65          }
  66  
  67          $( component.initFileBrowser );
  68  
  69          $( window ).on( 'beforeunload', function() {
  70              if ( component.dirty ) {
  71                  return __( 'The changes you made will be lost if you navigate away from this page.' );
  72              }
  73              return undefined;
  74          } );
  75  
  76          component.docsLookUpList.on( 'change', function() {
  77              var option = $( this ).val();
  78              if ( '' === option ) {
  79                  component.docsLookUpButton.prop( 'disabled', true );
  80              } else {
  81                  component.docsLookUpButton.prop( 'disabled', false );
  82              }
  83          } );
  84      };
  85  
  86      /**
  87       * Set up and display the warning modal.
  88       *
  89       * @since 4.9.0
  90       * @return {void}
  91       */
  92      component.showWarning = function() {
  93          // Get the text within the modal.
  94          var rawMessage = component.warning.find( '.file-editor-warning-message' ).text();
  95          // Hide all the #wpwrap content from assistive technologies.
  96          $( '#wpwrap' ).attr( 'aria-hidden', 'true' );
  97          // Detach the warning modal from its position and append it to the body.
  98          $( document.body )
  99              .addClass( 'modal-open' )
 100              .append( component.warning.detach() );
 101          // Reveal the modal and set focus on the go back button.
 102          component.warning
 103              .removeClass( 'hidden' )
 104              .find( '.file-editor-warning-go-back' ).trigger( 'focus' );
 105          // Get the links and buttons within the modal.
 106          component.warningTabbables = component.warning.find( 'a, button' );
 107          // Attach event handlers.
 108          component.warningTabbables.on( 'keydown', component.constrainTabbing );
 109          component.warning.on( 'click', '.file-editor-warning-dismiss', component.dismissWarning );
 110          // Make screen readers announce the warning message after a short delay (necessary for some screen readers).
 111          setTimeout( function() {
 112              wp.a11y.speak( wp.sanitize.stripTags( rawMessage.replace( /\s+/g, ' ' ) ), 'assertive' );
 113          }, 1000 );
 114      };
 115  
 116      /**
 117       * Constrain tabbing within the warning modal.
 118       *
 119       * @since 4.9.0
 120       * @param {Object} event jQuery event object.
 121       * @return {void}
 122       */
 123      component.constrainTabbing = function( event ) {
 124          var firstTabbable, lastTabbable;
 125  
 126          if ( 9 !== event.which ) {
 127              return;
 128          }
 129  
 130          firstTabbable = component.warningTabbables.first()[0];
 131          lastTabbable = component.warningTabbables.last()[0];
 132  
 133          if ( lastTabbable === event.target && ! event.shiftKey ) {
 134              firstTabbable.focus();
 135              event.preventDefault();
 136          } else if ( firstTabbable === event.target && event.shiftKey ) {
 137              lastTabbable.focus();
 138              event.preventDefault();
 139          }
 140      };
 141  
 142      /**
 143       * Dismiss the warning modal.
 144       *
 145       * @since 4.9.0
 146       * @return {void}
 147       */
 148      component.dismissWarning = function() {
 149  
 150          wp.ajax.post( 'dismiss-wp-pointer', {
 151              pointer: component.themeOrPlugin + '_editor_notice'
 152          });
 153  
 154          // Hide modal.
 155          component.warning.remove();
 156          $( '#wpwrap' ).removeAttr( 'aria-hidden' );
 157          $( 'body' ).removeClass( 'modal-open' );
 158      };
 159  
 160      /**
 161       * Callback for when a change happens.
 162       *
 163       * @since 4.9.0
 164       * @return {void}
 165       */
 166      component.onChange = function() {
 167          component.dirty = true;
 168          component.removeNotice( 'file_saved' );
 169      };
 170  
 171      /**
 172       * Submit file via Ajax.
 173       *
 174       * @since 4.9.0
 175       * @param {jQuery.Event} event - Event.
 176       * @return {void}
 177       */
 178      component.submit = function( event ) {
 179          var data = {}, request;
 180          event.preventDefault(); // Prevent form submission in favor of Ajax below.
 181          $.each( component.form.serializeArray(), function() {
 182              data[ this.name ] = this.value;
 183          } );
 184  
 185          // Use value from codemirror if present.
 186          if ( component.instance ) {
 187              data.newcontent = component.instance.codemirror.getValue();
 188          }
 189  
 190          if ( component.isSaving ) {
 191              return;
 192          }
 193  
 194          // Scroll to the line that has the error.
 195          if ( component.lintErrors.length ) {
 196              component.instance.codemirror.setCursor( component.lintErrors[0].from.line );
 197              return;
 198          }
 199  
 200          component.isSaving = true;
 201          component.textarea.prop( 'readonly', true );
 202          if ( component.instance ) {
 203              component.instance.codemirror.setOption( 'readOnly', true );
 204          }
 205  
 206          component.spinner.addClass( 'is-active' );
 207          request = wp.ajax.post( 'edit-theme-plugin-file', data );
 208  
 209          // Remove previous save notice before saving.
 210          if ( component.lastSaveNoticeCode ) {
 211              component.removeNotice( component.lastSaveNoticeCode );
 212          }
 213  
 214          request.done( function( response ) {
 215              component.lastSaveNoticeCode = 'file_saved';
 216              component.addNotice({
 217                  code: component.lastSaveNoticeCode,
 218                  type: 'success',
 219                  message: response.message,
 220                  dismissible: true
 221              });
 222              component.dirty = false;
 223          } );
 224  
 225          request.fail( function( response ) {
 226              var notice = $.extend(
 227                  {
 228                      code: 'save_error',
 229                      message: __( 'Something went wrong. Your change may not have been saved. Please try again. There is also a chance that you may need to manually fix and upload the file over FTP.' )
 230                  },
 231                  response,
 232                  {
 233                      type: 'error',
 234                      dismissible: true
 235                  }
 236              );
 237              component.lastSaveNoticeCode = notice.code;
 238              component.addNotice( notice );
 239          } );
 240  
 241          request.always( function() {
 242              component.spinner.removeClass( 'is-active' );
 243              component.isSaving = false;
 244  
 245              component.textarea.prop( 'readonly', false );
 246              if ( component.instance ) {
 247                  component.instance.codemirror.setOption( 'readOnly', false );
 248              }
 249          } );
 250      };
 251  
 252      /**
 253       * Add notice.
 254       *
 255       * @since 4.9.0
 256       *
 257       * @param {Object}   notice - Notice.
 258       * @param {string}   notice.code - Code.
 259       * @param {string}   notice.type - Type.
 260       * @param {string}   notice.message - Message.
 261       * @param {boolean}  [notice.dismissible=false] - Dismissible.
 262       * @param {Function} [notice.onDismiss] - Callback for when a user dismisses the notice.
 263       * @return {jQuery} Notice element.
 264       */
 265      component.addNotice = function( notice ) {
 266          var noticeElement;
 267  
 268          if ( ! notice.code ) {
 269              throw new Error( 'Missing code.' );
 270          }
 271  
 272          // Only let one notice of a given type be displayed at a time.
 273          component.removeNotice( notice.code );
 274  
 275          noticeElement = $( component.noticeTemplate( notice ) );
 276          noticeElement.hide();
 277  
 278          noticeElement.find( '.notice-dismiss' ).on( 'click', function() {
 279              component.removeNotice( notice.code );
 280              if ( notice.onDismiss ) {
 281                  notice.onDismiss( notice );
 282              }
 283          } );
 284  
 285          wp.a11y.speak( notice.message );
 286  
 287          component.noticesContainer.append( noticeElement );
 288          noticeElement.slideDown( 'fast' );
 289          component.noticeElements[ notice.code ] = noticeElement;
 290          return noticeElement;
 291      };
 292  
 293      /**
 294       * Remove notice.
 295       *
 296       * @since 4.9.0
 297       *
 298       * @param {string} code - Notice code.
 299       * @return {boolean} Whether a notice was removed.
 300       */
 301      component.removeNotice = function( code ) {
 302          if ( component.noticeElements[ code ] ) {
 303              component.noticeElements[ code ].slideUp( 'fast', function() {
 304                  $( this ).remove();
 305              } );
 306              delete component.noticeElements[ code ];
 307              return true;
 308          }
 309          return false;
 310      };
 311  
 312      /**
 313       * Initialize code editor.
 314       *
 315       * @since 4.9.0
 316       * @return {void}
 317       */
 318      component.initCodeEditor = function initCodeEditor() {
 319          var codeEditorSettings, editor;
 320  
 321          codeEditorSettings = $.extend( {}, component.codeEditor );
 322  
 323          /**
 324           * Handle tabbing to the field before the editor.
 325           *
 326           * @since 4.9.0
 327           *
 328           * @return {void}
 329           */
 330          codeEditorSettings.onTabPrevious = function() {
 331              $( '#templateside' ).find( ':tabbable' ).last().trigger( 'focus' );
 332          };
 333  
 334          /**
 335           * Handle tabbing to the field after the editor.
 336           *
 337           * @since 4.9.0
 338           *
 339           * @return {void}
 340           */
 341          codeEditorSettings.onTabNext = function() {
 342              $( '#template' ).find( ':tabbable:not(.CodeMirror-code)' ).first().trigger( 'focus' );
 343          };
 344  
 345          /**
 346           * Handle change to the linting errors.
 347           *
 348           * @since 4.9.0
 349           *
 350           * @param {Array} errors - List of linting errors.
 351           * @return {void}
 352           */
 353          codeEditorSettings.onChangeLintingErrors = function( errors ) {
 354              component.lintErrors = errors;
 355  
 356              // Only disable the button in onUpdateErrorNotice when there are errors so users can still feel they can click the button.
 357              if ( 0 === errors.length ) {
 358                  component.submitButton.toggleClass( 'disabled', false );
 359              }
 360          };
 361  
 362          /**
 363           * Update error notice.
 364           *
 365           * @since 4.9.0
 366           *
 367           * @param {Array} errorAnnotations - Error annotations.
 368           * @return {void}
 369           */
 370          codeEditorSettings.onUpdateErrorNotice = function onUpdateErrorNotice( errorAnnotations ) {
 371              var noticeElement;
 372  
 373              component.submitButton.toggleClass( 'disabled', errorAnnotations.length > 0 );
 374  
 375              if ( 0 !== errorAnnotations.length ) {
 376                  noticeElement = component.addNotice({
 377                      code: 'lint_errors',
 378                      type: 'error',
 379                      message: sprintf(
 380                          /* translators: %s: Error count. */
 381                          _n(
 382                              'There is %s error which must be fixed before you can update this file.',
 383                              'There are %s errors which must be fixed before you can update this file.',
 384                              errorAnnotations.length
 385                          ),
 386                          String( errorAnnotations.length )
 387                      ),
 388                      dismissible: false
 389                  });
 390                  noticeElement.find( 'input[type=checkbox]' ).on( 'click', function() {
 391                      codeEditorSettings.onChangeLintingErrors( [] );
 392                      component.removeNotice( 'lint_errors' );
 393                  } );
 394              } else {
 395                  component.removeNotice( 'lint_errors' );
 396              }
 397          };
 398  
 399          editor = wp.codeEditor.initialize( $( '#newcontent' ), codeEditorSettings );
 400          editor.codemirror.on( 'change', component.onChange );
 401  
 402          // Improve the editor accessibility.
 403          $( editor.codemirror.display.lineDiv )
 404              .attr({
 405                  role: 'textbox',
 406                  'aria-multiline': 'true',
 407                  'aria-labelledby': 'theme-plugin-editor-label',
 408                  'aria-describedby': 'editor-keyboard-trap-help-1 editor-keyboard-trap-help-2 editor-keyboard-trap-help-3 editor-keyboard-trap-help-4'
 409              });
 410  
 411          // Focus the editor when clicking on its label.
 412          $( '#theme-plugin-editor-label' ).on( 'click', function() {
 413              editor.codemirror.focus();
 414          });
 415  
 416          component.instance = editor;
 417      };
 418  
 419      /**
 420       * Initialization of the file browser's folder states.
 421       *
 422       * @since 4.9.0
 423       * @return {void}
 424       */
 425      component.initFileBrowser = function initFileBrowser() {
 426  
 427          var $templateside = $( '#templateside' );
 428  
 429          // Collapse all folders.
 430          $templateside.find( '[role="group"]' ).parent().attr( 'aria-expanded', false );
 431  
 432          // Expand ancestors to the current file.
 433          $templateside.find( '.notice' ).parents( '[aria-expanded]' ).attr( 'aria-expanded', true );
 434  
 435          // Find Tree elements and enhance them.
 436          $templateside.find( '[role="tree"]' ).each( function() {
 437              var treeLinks = new TreeLinks( this );
 438              treeLinks.init();
 439          } );
 440  
 441          // Scroll the current file into view.
 442          $templateside.find( '.current-file:first' ).each( function() {
 443              if ( this.scrollIntoViewIfNeeded ) {
 444                  this.scrollIntoViewIfNeeded();
 445              } else {
 446                  this.scrollIntoView( false );
 447              }
 448          } );
 449      };
 450  
 451      /* jshint ignore:start */
 452      /* jscs:disable */
 453      /* eslint-disable */
 454  
 455      /**
 456       * Creates a new TreeitemLink.
 457       *
 458       * @since 4.9.0
 459       * @class
 460       * @private
 461       * @see {@link https://www.w3.org/TR/wai-aria-practices-1.1/examples/treeview/treeview-2/treeview-2b.html|W3C Treeview Example}
 462       * @license W3C-20150513
 463       */
 464      var TreeitemLink = (function () {
 465          /**
 466           *   This content is licensed according to the W3C Software License at
 467           *   https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document
 468           *
 469           *   File:   TreeitemLink.js
 470           *
 471           *   Desc:   Treeitem widget that implements ARIA Authoring Practices
 472           *           for a tree being used as a file viewer
 473           *
 474           *   Author: Jon Gunderson, Ku Ja Eun and Nicholas Hoyt
 475           */
 476  
 477          /**
 478           *   @constructor
 479           *
 480           *   @desc
 481           *       Treeitem object for representing the state and user interactions for a
 482           *       treeItem widget
 483           *
 484           *   @param node
 485           *       An element with the role=tree attribute
 486           */
 487  
 488          var TreeitemLink = function (node, treeObj, group) {
 489  
 490              // Check whether node is a DOM element.
 491              if (typeof node !== 'object') {
 492                  return;
 493              }
 494  
 495              node.tabIndex = -1;
 496              this.tree = treeObj;
 497              this.groupTreeitem = group;
 498              this.domNode = node;
 499              this.label = node.textContent.trim();
 500              this.stopDefaultClick = false;
 501  
 502              if (node.getAttribute('aria-label')) {
 503                  this.label = node.getAttribute('aria-label').trim();
 504              }
 505  
 506              this.isExpandable = false;
 507              this.isVisible = false;
 508              this.inGroup = false;
 509  
 510              if (group) {
 511                  this.inGroup = true;
 512              }
 513  
 514              var elem = node.firstElementChild;
 515  
 516              while (elem) {
 517  
 518                  if (elem.tagName.toLowerCase() == 'ul') {
 519                      elem.setAttribute('role', 'group');
 520                      this.isExpandable = true;
 521                      break;
 522                  }
 523  
 524                  elem = elem.nextElementSibling;
 525              }
 526  
 527              this.keyCode = Object.freeze({
 528                  RETURN: 13,
 529                  SPACE: 32,
 530                  PAGEUP: 33,
 531                  PAGEDOWN: 34,
 532                  END: 35,
 533                  HOME: 36,
 534                  LEFT: 37,
 535                  UP: 38,
 536                  RIGHT: 39,
 537                  DOWN: 40
 538              });
 539          };
 540  
 541          TreeitemLink.prototype.init = function () {
 542              this.domNode.tabIndex = -1;
 543  
 544              if (!this.domNode.getAttribute('role')) {
 545                  this.domNode.setAttribute('role', 'treeitem');
 546              }
 547  
 548              this.domNode.addEventListener('keydown', this.handleKeydown.bind(this));
 549              this.domNode.addEventListener('click', this.handleClick.bind(this));
 550              this.domNode.addEventListener('focus', this.handleFocus.bind(this));
 551              this.domNode.addEventListener('blur', this.handleBlur.bind(this));
 552  
 553              if (this.isExpandable) {
 554                  this.domNode.firstElementChild.addEventListener('mouseover', this.handleMouseOver.bind(this));
 555                  this.domNode.firstElementChild.addEventListener('mouseout', this.handleMouseOut.bind(this));
 556              }
 557              else {
 558                  this.domNode.addEventListener('mouseover', this.handleMouseOver.bind(this));
 559                  this.domNode.addEventListener('mouseout', this.handleMouseOut.bind(this));
 560              }
 561          };
 562  
 563          TreeitemLink.prototype.isExpanded = function () {
 564  
 565              if (this.isExpandable) {
 566                  return this.domNode.getAttribute('aria-expanded') === 'true';
 567              }
 568  
 569              return false;
 570  
 571          };
 572  
 573          /* EVENT HANDLERS */
 574  
 575          TreeitemLink.prototype.handleKeydown = function (event) {
 576              var tgt = event.currentTarget,
 577                  flag = false,
 578                  _char = event.key,
 579                  clickEvent;
 580  
 581  			function isPrintableCharacter(str) {
 582                  return str.length === 1 && str.match(/\S/);
 583              }
 584  
 585  			function printableCharacter(item) {
 586                  if (_char == '*') {
 587                      item.tree.expandAllSiblingItems(item);
 588                      flag = true;
 589                  }
 590                  else {
 591                      if (isPrintableCharacter(_char)) {
 592                          item.tree.setFocusByFirstCharacter(item, _char);
 593                          flag = true;
 594                      }
 595                  }
 596              }
 597  
 598              this.stopDefaultClick = false;
 599  
 600              if (event.altKey || event.ctrlKey || event.metaKey) {
 601                  return;
 602              }
 603  
 604              if (event.shift) {
 605                  if (event.keyCode == this.keyCode.SPACE || event.keyCode == this.keyCode.RETURN) {
 606                      event.stopPropagation();
 607                      this.stopDefaultClick = true;
 608                  }
 609                  else {
 610                      if (isPrintableCharacter(_char)) {
 611                          printableCharacter(this);
 612                      }
 613                  }
 614              }
 615              else {
 616                  switch (event.keyCode) {
 617                      case this.keyCode.SPACE:
 618                      case this.keyCode.RETURN:
 619                          if (this.isExpandable) {
 620                              if (this.isExpanded()) {
 621                                  this.tree.collapseTreeitem(this);
 622                              }
 623                              else {
 624                                  this.tree.expandTreeitem(this);
 625                              }
 626                              flag = true;
 627                          }
 628                          else {
 629                              event.stopPropagation();
 630                              this.stopDefaultClick = true;
 631                          }
 632                          break;
 633  
 634                      case this.keyCode.UP:
 635                          this.tree.setFocusToPreviousItem(this);
 636                          flag = true;
 637                          break;
 638  
 639                      case this.keyCode.DOWN:
 640                          this.tree.setFocusToNextItem(this);
 641                          flag = true;
 642                          break;
 643  
 644                      case this.keyCode.RIGHT:
 645                          if (this.isExpandable) {
 646                              if (this.isExpanded()) {
 647                                  this.tree.setFocusToNextItem(this);
 648                              }
 649                              else {
 650                                  this.tree.expandTreeitem(this);
 651                              }
 652                          }
 653                          flag = true;
 654                          break;
 655  
 656                      case this.keyCode.LEFT:
 657                          if (this.isExpandable && this.isExpanded()) {
 658                              this.tree.collapseTreeitem(this);
 659                              flag = true;
 660                          }
 661                          else {
 662                              if (this.inGroup) {
 663                                  this.tree.setFocusToParentItem(this);
 664                                  flag = true;
 665                              }
 666                          }
 667                          break;
 668  
 669                      case this.keyCode.HOME:
 670                          this.tree.setFocusToFirstItem();
 671                          flag = true;
 672                          break;
 673  
 674                      case this.keyCode.END:
 675                          this.tree.setFocusToLastItem();
 676                          flag = true;
 677                          break;
 678  
 679                      default:
 680                          if (isPrintableCharacter(_char)) {
 681                              printableCharacter(this);
 682                          }
 683                          break;
 684                  }
 685              }
 686  
 687              if (flag) {
 688                  event.stopPropagation();
 689                  event.preventDefault();
 690              }
 691          };
 692  
 693          TreeitemLink.prototype.handleClick = function (event) {
 694  
 695              // Only process click events that directly happened on this treeitem.
 696              if (event.target !== this.domNode && event.target !== this.domNode.firstElementChild) {
 697                  return;
 698              }
 699  
 700              if (this.isExpandable) {
 701                  if (this.isExpanded()) {
 702                      this.tree.collapseTreeitem(this);
 703                  }
 704                  else {
 705                      this.tree.expandTreeitem(this);
 706                  }
 707                  event.stopPropagation();
 708              }
 709          };
 710  
 711          TreeitemLink.prototype.handleFocus = function (event) {
 712              var node = this.domNode;
 713              if (this.isExpandable) {
 714                  node = node.firstElementChild;
 715              }
 716              node.classList.add('focus');
 717          };
 718  
 719          TreeitemLink.prototype.handleBlur = function (event) {
 720              var node = this.domNode;
 721              if (this.isExpandable) {
 722                  node = node.firstElementChild;
 723              }
 724              node.classList.remove('focus');
 725          };
 726  
 727          TreeitemLink.prototype.handleMouseOver = function (event) {
 728              event.currentTarget.classList.add('hover');
 729          };
 730  
 731          TreeitemLink.prototype.handleMouseOut = function (event) {
 732              event.currentTarget.classList.remove('hover');
 733          };
 734  
 735          return TreeitemLink;
 736      })();
 737  
 738      /**
 739       * Creates a new TreeLinks.
 740       *
 741       * @since 4.9.0
 742       * @class
 743       * @private
 744       * @see {@link https://www.w3.org/TR/wai-aria-practices-1.1/examples/treeview/treeview-2/treeview-2b.html|W3C Treeview Example}
 745       * @license W3C-20150513
 746       */
 747      TreeLinks = (function () {
 748          /*
 749           *   This content is licensed according to the W3C Software License at
 750           *   https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document
 751           *
 752           *   File:   TreeLinks.js
 753           *
 754           *   Desc:   Tree widget that implements ARIA Authoring Practices
 755           *           for a tree being used as a file viewer
 756           *
 757           *   Author: Jon Gunderson, Ku Ja Eun and Nicholas Hoyt
 758           */
 759  
 760          /*
 761           *   @constructor
 762           *
 763           *   @desc
 764           *       Tree item object for representing the state and user interactions for a
 765           *       tree widget
 766           *
 767           *   @param node
 768           *       An element with the role=tree attribute
 769           */
 770  
 771          var TreeLinks = function (node) {
 772              // Check whether node is a DOM element.
 773              if (typeof node !== 'object') {
 774                  return;
 775              }
 776  
 777              this.domNode = node;
 778  
 779              this.treeitems = [];
 780              this.firstChars = [];
 781  
 782              this.firstTreeitem = null;
 783              this.lastTreeitem = null;
 784  
 785          };
 786  
 787          TreeLinks.prototype.init = function () {
 788  
 789  			function findTreeitems(node, tree, group) {
 790  
 791                  var elem = node.firstElementChild;
 792                  var ti = group;
 793  
 794                  while (elem) {
 795  
 796                      if ((elem.tagName.toLowerCase() === 'li' && elem.firstElementChild.tagName.toLowerCase() === 'span') || elem.tagName.toLowerCase() === 'a') {
 797                          ti = new TreeitemLink(elem, tree, group);
 798                          ti.init();
 799                          tree.treeitems.push(ti);
 800                          tree.firstChars.push(ti.label.substring(0, 1).toLowerCase());
 801                      }
 802  
 803                      if (elem.firstElementChild) {
 804                          findTreeitems(elem, tree, ti);
 805                      }
 806  
 807                      elem = elem.nextElementSibling;
 808                  }
 809              }
 810  
 811              // Initialize pop up menus.
 812              if (!this.domNode.getAttribute('role')) {
 813                  this.domNode.setAttribute('role', 'tree');
 814              }
 815  
 816              findTreeitems(this.domNode, this, false);
 817  
 818              this.updateVisibleTreeitems();
 819  
 820              this.firstTreeitem.domNode.tabIndex = 0;
 821  
 822          };
 823  
 824          TreeLinks.prototype.setFocusToItem = function (treeitem) {
 825  
 826              for (var i = 0; i < this.treeitems.length; i++) {
 827                  var ti = this.treeitems[i];
 828  
 829                  if (ti === treeitem) {
 830                      ti.domNode.tabIndex = 0;
 831                      ti.domNode.focus();
 832                  }
 833                  else {
 834                      ti.domNode.tabIndex = -1;
 835                  }
 836              }
 837  
 838          };
 839  
 840          TreeLinks.prototype.setFocusToNextItem = function (currentItem) {
 841  
 842              var nextItem = false;
 843  
 844              for (var i = (this.treeitems.length - 1); i >= 0; i--) {
 845                  var ti = this.treeitems[i];
 846                  if (ti === currentItem) {
 847                      break;
 848                  }
 849                  if (ti.isVisible) {
 850                      nextItem = ti;
 851                  }
 852              }
 853  
 854              if (nextItem) {
 855                  this.setFocusToItem(nextItem);
 856              }
 857  
 858          };
 859  
 860          TreeLinks.prototype.setFocusToPreviousItem = function (currentItem) {
 861  
 862              var prevItem = false;
 863  
 864              for (var i = 0; i < this.treeitems.length; i++) {
 865                  var ti = this.treeitems[i];
 866                  if (ti === currentItem) {
 867                      break;
 868                  }
 869                  if (ti.isVisible) {
 870                      prevItem = ti;
 871                  }
 872              }
 873  
 874              if (prevItem) {
 875                  this.setFocusToItem(prevItem);
 876              }
 877          };
 878  
 879          TreeLinks.prototype.setFocusToParentItem = function (currentItem) {
 880  
 881              if (currentItem.groupTreeitem) {
 882                  this.setFocusToItem(currentItem.groupTreeitem);
 883              }
 884          };
 885  
 886          TreeLinks.prototype.setFocusToFirstItem = function () {
 887              this.setFocusToItem(this.firstTreeitem);
 888          };
 889  
 890          TreeLinks.prototype.setFocusToLastItem = function () {
 891              this.setFocusToItem(this.lastTreeitem);
 892          };
 893  
 894          TreeLinks.prototype.expandTreeitem = function (currentItem) {
 895  
 896              if (currentItem.isExpandable) {
 897                  currentItem.domNode.setAttribute('aria-expanded', true);
 898                  this.updateVisibleTreeitems();
 899              }
 900  
 901          };
 902  
 903          TreeLinks.prototype.expandAllSiblingItems = function (currentItem) {
 904              for (var i = 0; i < this.treeitems.length; i++) {
 905                  var ti = this.treeitems[i];
 906  
 907                  if ((ti.groupTreeitem === currentItem.groupTreeitem) && ti.isExpandable) {
 908                      this.expandTreeitem(ti);
 909                  }
 910              }
 911  
 912          };
 913  
 914          TreeLinks.prototype.collapseTreeitem = function (currentItem) {
 915  
 916              var groupTreeitem = false;
 917  
 918              if (currentItem.isExpanded()) {
 919                  groupTreeitem = currentItem;
 920              }
 921              else {
 922                  groupTreeitem = currentItem.groupTreeitem;
 923              }
 924  
 925              if (groupTreeitem) {
 926                  groupTreeitem.domNode.setAttribute('aria-expanded', false);
 927                  this.updateVisibleTreeitems();
 928                  this.setFocusToItem(groupTreeitem);
 929              }
 930  
 931          };
 932  
 933          TreeLinks.prototype.updateVisibleTreeitems = function () {
 934  
 935              this.firstTreeitem = this.treeitems[0];
 936  
 937              for (var i = 0; i < this.treeitems.length; i++) {
 938                  var ti = this.treeitems[i];
 939  
 940                  var parent = ti.domNode.parentNode;
 941  
 942                  ti.isVisible = true;
 943  
 944                  while (parent && (parent !== this.domNode)) {
 945  
 946                      if (parent.getAttribute('aria-expanded') == 'false') {
 947                          ti.isVisible = false;
 948                      }
 949                      parent = parent.parentNode;
 950                  }
 951  
 952                  if (ti.isVisible) {
 953                      this.lastTreeitem = ti;
 954                  }
 955              }
 956  
 957          };
 958  
 959          TreeLinks.prototype.setFocusByFirstCharacter = function (currentItem, _char) {
 960              var start, index;
 961              _char = _char.toLowerCase();
 962  
 963              // Get start index for search based on position of currentItem.
 964              start = this.treeitems.indexOf(currentItem) + 1;
 965              if (start === this.treeitems.length) {
 966                  start = 0;
 967              }
 968  
 969              // Check remaining slots in the menu.
 970              index = this.getIndexFirstChars(start, _char);
 971  
 972              // If not found in remaining slots, check from beginning.
 973              if (index === -1) {
 974                  index = this.getIndexFirstChars(0, _char);
 975              }
 976  
 977              // If match was found...
 978              if (index > -1) {
 979                  this.setFocusToItem(this.treeitems[index]);
 980              }
 981          };
 982  
 983          TreeLinks.prototype.getIndexFirstChars = function (startIndex, _char) {
 984              for (var i = startIndex; i < this.firstChars.length; i++) {
 985                  if (this.treeitems[i].isVisible) {
 986                      if (_char === this.firstChars[i]) {
 987                          return i;
 988                      }
 989                  }
 990              }
 991              return -1;
 992          };
 993  
 994          return TreeLinks;
 995      })();
 996  
 997      /* jshint ignore:end */
 998      /* jscs:enable */
 999      /* eslint-enable */
1000  
1001      return component;
1002  })( jQuery );
1003  
1004  /**
1005   * Removed in 5.5.0, needed for back-compatibility.
1006   *
1007   * @since 4.9.0
1008   * @deprecated 5.5.0
1009   *
1010   * @type {object}
1011   */
1012  wp.themePluginEditor.l10n = wp.themePluginEditor.l10n || {
1013      saveAlert: '',
1014      saveError: '',
1015      lintError: {
1016          alternative: 'wp.i18n',
1017          func: function() {
1018              return {
1019                  singular: '',
1020                  plural: ''
1021              };
1022          }
1023      }
1024  };
1025  
1026  wp.themePluginEditor.l10n = window.wp.deprecateL10nObject( 'wp.themePluginEditor.l10n', wp.themePluginEditor.l10n, '5.5.0' );


Generated : Thu Nov 21 08:20:01 2024 Cross-referenced by PHPXref