wp/wp-admin/js/theme-plugin-editor.js
changeset 7 cf61fcea0001
child 9 177826044cd9
equal deleted inserted replaced
6:490d5cc509ed 7:cf61fcea0001
       
     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 );