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


Generated : Thu Apr 23 08:20:11 2026 Cross-referenced by PHPXref