ru-se.com

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

theme-plugin-editor.js (25386B)


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