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