theme-plugin-editor.js 25 KB

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