home *** CD-ROM | disk | FTP | other *** search
/ HTML Examples / WP.iso / wordpress / wp-admin / js / theme-plugin-editor.js < prev    next >
Encoding:
JavaScript  |  2017-10-16  |  23.7 KB  |  992 lines

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