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


Generated : Sun Sep 13 08:20:28 2026 Cross-referenced by PHPXref