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