[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

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


Generated: Sat Nov 23 20:47:33 2019 Cross-referenced by PHPXref 0.7