| [ Index ] |
PHP Cross Reference of WordPress Trunk (Updated Daily) |
[Summary view] [Print] [Text view]
1 /** 2 * @output wp-admin/js/common.js 3 */ 4 5 /* global setUserSetting, ajaxurl, alert, confirm, pagenow */ 6 /* global columns, screenMeta */ 7 8 /** 9 * Adds common WordPress functionality to the window. 10 * 11 * @param {JQueryStatic} $ The jQuery object. 12 * @param {Object} window The window object. 13 * @param {*} undefined Unused. 14 */ 15 ( function( $, window, undefined ) { 16 var $document = $( document ), 17 $window = $( window ), 18 $body = $( document.body ), 19 __ = wp.i18n.__, 20 sprintf = wp.i18n.sprintf; 21 22 /** 23 * Throws an error for a deprecated property. 24 * 25 * @since 5.5.1 26 * 27 * @param {string} propName The property that was used. 28 * @param {string} version The version of WordPress that deprecated the property. 29 * @param {string} replacement The property that should have been used. 30 */ 31 function deprecatedProperty( propName, version, replacement ) { 32 var message; 33 34 if ( 'undefined' !== typeof replacement ) { 35 message = sprintf( 36 /* translators: 1: Deprecated property name, 2: Version number, 3: Alternative property name. */ 37 __( '%1$s is deprecated since version %2$s! Use %3$s instead.' ), 38 propName, 39 version, 40 replacement 41 ); 42 } else { 43 message = sprintf( 44 /* translators: 1: Deprecated property name, 2: Version number. */ 45 __( '%1$s is deprecated since version %2$s with no alternative available.' ), 46 propName, 47 version 48 ); 49 } 50 51 window.console.warn( message ); 52 } 53 54 /** 55 * Deprecate all properties on an object. 56 * 57 * @since 5.5.1 58 * @since 5.6.0 Added the `version` parameter. 59 * 60 * @param {string} name The name of the object, i.e. commonL10n. 61 * @param {Object} l10nObject The object to deprecate the properties on. 62 * @param {string} version The version of WordPress that deprecated the property. 63 * 64 * @return {Object} The object with all its properties deprecated. 65 */ 66 function deprecateL10nObject( name, l10nObject, version ) { 67 var deprecatedObject = {}; 68 69 Object.keys( l10nObject ).forEach( function( key ) { 70 var prop = l10nObject[ key ]; 71 var propName = name + '.' + key; 72 73 if ( 'object' === typeof prop ) { 74 Object.defineProperty( deprecatedObject, key, { get: function() { 75 deprecatedProperty( propName, version, prop.alternative ); 76 return prop.func(); 77 } } ); 78 } else { 79 Object.defineProperty( deprecatedObject, key, { get: function() { 80 deprecatedProperty( propName, version, 'wp.i18n' ); 81 return prop; 82 } } ); 83 } 84 } ); 85 86 return deprecatedObject; 87 } 88 89 window.wp.deprecateL10nObject = deprecateL10nObject; 90 91 /** 92 * Removed in 5.5.0, needed for back-compatibility. 93 * 94 * @since 2.6.0 95 * @deprecated 5.5.0 96 */ 97 window.commonL10n = window.commonL10n || { 98 warnDelete: '', 99 dismiss: '', 100 collapseMenu: '', 101 expandMenu: '' 102 }; 103 104 window.commonL10n = deprecateL10nObject( 'commonL10n', window.commonL10n, '5.5.0' ); 105 106 /** 107 * Removed in 5.5.0, needed for back-compatibility. 108 * 109 * @since 3.3.0 110 * @deprecated 5.5.0 111 */ 112 window.wpPointerL10n = window.wpPointerL10n || { 113 dismiss: '' 114 }; 115 116 window.wpPointerL10n = deprecateL10nObject( 'wpPointerL10n', window.wpPointerL10n, '5.5.0' ); 117 118 /** 119 * Removed in 5.5.0, needed for back-compatibility. 120 * 121 * @since 4.3.0 122 * @deprecated 5.5.0 123 */ 124 window.userProfileL10n = window.userProfileL10n || { 125 warn: '', 126 warnWeak: '', 127 show: '', 128 hide: '', 129 cancel: '', 130 ariaShow: '', 131 ariaHide: '' 132 }; 133 134 window.userProfileL10n = deprecateL10nObject( 'userProfileL10n', window.userProfileL10n, '5.5.0' ); 135 136 /** 137 * Removed in 5.5.0, needed for back-compatibility. 138 * 139 * @since 4.9.6 140 * @deprecated 5.5.0 141 */ 142 window.privacyToolsL10n = window.privacyToolsL10n || { 143 noDataFound: '', 144 foundAndRemoved: '', 145 noneRemoved: '', 146 someNotRemoved: '', 147 removalError: '', 148 emailSent: '', 149 noExportFile: '', 150 exportError: '' 151 }; 152 153 window.privacyToolsL10n = deprecateL10nObject( 'privacyToolsL10n', window.privacyToolsL10n, '5.5.0' ); 154 155 /** 156 * Removed in 5.5.0, needed for back-compatibility. 157 * 158 * @since 3.6.0 159 * @deprecated 5.5.0 160 */ 161 window.authcheckL10n = { 162 beforeunload: '' 163 }; 164 165 window.authcheckL10n = window.authcheckL10n || deprecateL10nObject( 'authcheckL10n', window.authcheckL10n, '5.5.0' ); 166 167 /** 168 * Removed in 5.5.0, needed for back-compatibility. 169 * 170 * @since 2.8.0 171 * @deprecated 5.5.0 172 */ 173 window.tagsl10n = { 174 noPerm: '', 175 broken: '' 176 }; 177 178 window.tagsl10n = window.tagsl10n || deprecateL10nObject( 'tagsl10n', window.tagsl10n, '5.5.0' ); 179 180 /** 181 * Removed in 5.5.0, needed for back-compatibility. 182 * 183 * @since 2.5.0 184 * @deprecated 5.5.0 185 */ 186 window.adminCommentsL10n = window.adminCommentsL10n || { 187 hotkeys_highlight_first: { 188 alternative: 'window.adminCommentsSettings.hotkeys_highlight_first', 189 func: function() { return window.adminCommentsSettings.hotkeys_highlight_first; } 190 }, 191 hotkeys_highlight_last: { 192 alternative: 'window.adminCommentsSettings.hotkeys_highlight_last', 193 func: function() { return window.adminCommentsSettings.hotkeys_highlight_last; } 194 }, 195 replyApprove: '', 196 reply: '', 197 warnQuickEdit: '', 198 warnCommentChanges: '', 199 docTitleComments: '', 200 docTitleCommentsCount: '' 201 }; 202 203 window.adminCommentsL10n = deprecateL10nObject( 'adminCommentsL10n', window.adminCommentsL10n, '5.5.0' ); 204 205 /** 206 * Removed in 5.5.0, needed for back-compatibility. 207 * 208 * @since 2.5.0 209 * @deprecated 5.5.0 210 */ 211 window.tagsSuggestL10n = window.tagsSuggestL10n || { 212 tagDelimiter: '', 213 removeTerm: '', 214 termSelected: '', 215 termAdded: '', 216 termRemoved: '' 217 }; 218 219 window.tagsSuggestL10n = deprecateL10nObject( 'tagsSuggestL10n', window.tagsSuggestL10n, '5.5.0' ); 220 221 /** 222 * Removed in 5.5.0, needed for back-compatibility. 223 * 224 * @since 3.5.0 225 * @deprecated 5.5.0 226 */ 227 window.wpColorPickerL10n = window.wpColorPickerL10n || { 228 clear: '', 229 clearAriaLabel: '', 230 defaultString: '', 231 defaultAriaLabel: '', 232 pick: '', 233 defaultLabel: '' 234 }; 235 236 window.wpColorPickerL10n = deprecateL10nObject( 'wpColorPickerL10n', window.wpColorPickerL10n, '5.5.0' ); 237 238 /** 239 * Removed in 5.5.0, needed for back-compatibility. 240 * 241 * @since 2.7.0 242 * @deprecated 5.5.0 243 */ 244 window.attachMediaBoxL10n = window.attachMediaBoxL10n || { 245 error: '' 246 }; 247 248 window.attachMediaBoxL10n = deprecateL10nObject( 'attachMediaBoxL10n', window.attachMediaBoxL10n, '5.5.0' ); 249 250 /** 251 * Removed in 5.5.0, needed for back-compatibility. 252 * 253 * @since 2.5.0 254 * @deprecated 5.5.0 255 */ 256 window.postL10n = window.postL10n || { 257 ok: '', 258 cancel: '', 259 publishOn: '', 260 publishOnFuture: '', 261 publishOnPast: '', 262 dateFormat: '', 263 showcomm: '', 264 endcomm: '', 265 publish: '', 266 schedule: '', 267 update: '', 268 savePending: '', 269 saveDraft: '', 270 'private': '', 271 'public': '', 272 publicSticky: '', 273 password: '', 274 privatelyPublished: '', 275 published: '', 276 saveAlert: '', 277 savingText: '', 278 permalinkSaved: '' 279 }; 280 281 window.postL10n = deprecateL10nObject( 'postL10n', window.postL10n, '5.5.0' ); 282 283 /** 284 * Removed in 5.5.0, needed for back-compatibility. 285 * 286 * @since 2.7.0 287 * @deprecated 5.5.0 288 */ 289 window.inlineEditL10n = window.inlineEditL10n || { 290 error: '', 291 ntdeltitle: '', 292 notitle: '', 293 comma: '', 294 saved: '' 295 }; 296 297 window.inlineEditL10n = deprecateL10nObject( 'inlineEditL10n', window.inlineEditL10n, '5.5.0' ); 298 299 /** 300 * Removed in 5.5.0, needed for back-compatibility. 301 * 302 * @since 2.7.0 303 * @deprecated 5.5.0 304 */ 305 window.plugininstallL10n = window.plugininstallL10n || { 306 plugin_information: '', 307 plugin_modal_label: '', 308 ays: '' 309 }; 310 311 window.plugininstallL10n = deprecateL10nObject( 'plugininstallL10n', window.plugininstallL10n, '5.5.0' ); 312 313 /** 314 * Removed in 5.5.0, needed for back-compatibility. 315 * 316 * @since 3.0.0 317 * @deprecated 5.5.0 318 */ 319 window.navMenuL10n = window.navMenuL10n || { 320 noResultsFound: '', 321 warnDeleteMenu: '', 322 saveAlert: '', 323 untitled: '' 324 }; 325 326 window.navMenuL10n = deprecateL10nObject( 'navMenuL10n', window.navMenuL10n, '5.5.0' ); 327 328 /** 329 * Removed in 5.5.0, needed for back-compatibility. 330 * 331 * @since 2.5.0 332 * @deprecated 5.5.0 333 */ 334 window.commentL10n = window.commentL10n || { 335 submittedOn: '', 336 dateFormat: '' 337 }; 338 339 window.commentL10n = deprecateL10nObject( 'commentL10n', window.commentL10n, '5.5.0' ); 340 341 /** 342 * Removed in 5.5.0, needed for back-compatibility. 343 * 344 * @since 2.9.0 345 * @deprecated 5.5.0 346 */ 347 window.setPostThumbnailL10n = window.setPostThumbnailL10n || { 348 setThumbnail: '', 349 saving: '', 350 error: '', 351 done: '' 352 }; 353 354 window.setPostThumbnailL10n = deprecateL10nObject( 'setPostThumbnailL10n', window.setPostThumbnailL10n, '5.5.0' ); 355 356 /** 357 * Removed in 6.5.0, needed for back-compatibility. 358 * 359 * @since 4.5.0 360 * @deprecated 6.5.0 361 */ 362 window.uiAutocompleteL10n = window.uiAutocompleteL10n || { 363 noResults: '', 364 oneResult: '', 365 manyResults: '', 366 itemSelected: '' 367 }; 368 369 window.uiAutocompleteL10n = deprecateL10nObject( 'uiAutocompleteL10n', window.uiAutocompleteL10n, '6.5.0' ); 370 371 /** 372 * Removed in 3.3.0, needed for back-compatibility. 373 * 374 * @since 2.7.0 375 * @deprecated 3.3.0 376 */ 377 window.adminMenu = { 378 init : function() {}, 379 fold : function() {}, 380 restoreMenuState : function() {}, 381 toggle : function() {}, 382 favorites : function() {} 383 }; 384 385 // Show/hide/save table columns. 386 window.columns = { 387 388 /** 389 * Initializes the column toggles in the screen options. 390 * 391 * Binds an onClick event to the checkboxes to show or hide the table columns 392 * based on their toggled state. And persists the toggled state. 393 * 394 * @since 2.7.0 395 * 396 * @return {void} 397 */ 398 init : function() { 399 var that = this; 400 $('.hide-column-tog', '#adv-settings').on( 'click', function() { 401 var $t = $(this), column = $t.val(); 402 if ( $t.prop('checked') ) 403 that.checked(column); 404 else 405 that.unchecked(column); 406 407 columns.saveManageColumnsState(); 408 }); 409 }, 410 411 /** 412 * Saves the toggled state for the columns. 413 * 414 * Saves whether the columns should be shown or hidden on a page. 415 * 416 * @since 3.0.0 417 * 418 * @return {void} 419 */ 420 saveManageColumnsState : function() { 421 var hidden = this.hidden(); 422 $.post( 423 ajaxurl, 424 { 425 action: 'hidden-columns', 426 hidden: hidden, 427 screenoptionnonce: $('#screenoptionnonce').val(), 428 page: pagenow 429 }, 430 function() { 431 wp.a11y.speak( __( 'Screen Options updated.' ) ); 432 } 433 ); 434 }, 435 436 /** 437 * Makes a column visible and adjusts the column span for the table. 438 * 439 * @since 3.0.0 440 * @param {string} column The column name. 441 * 442 * @return {void} 443 */ 444 checked : function(column) { 445 $('.column-' + column).removeClass( 'hidden' ); 446 this.colSpanChange(+1); 447 }, 448 449 /** 450 * Hides a column and adjusts the column span for the table. 451 * 452 * @since 3.0.0 453 * @param {string} column The column name. 454 * 455 * @return {void} 456 */ 457 unchecked : function(column) { 458 $('.column-' + column).addClass( 'hidden' ); 459 this.colSpanChange(-1); 460 }, 461 462 /** 463 * Gets all hidden columns. 464 * 465 * @since 3.0.0 466 * 467 * @return {string} The hidden column names separated by a comma. 468 */ 469 hidden : function() { 470 return $( '.manage-column[id]' ).filter( '.hidden' ).map(function() { 471 return this.id; 472 }).get().join( ',' ); 473 }, 474 475 /** 476 * Gets the checked column toggles from the screen options. 477 * 478 * @since 3.0.0 479 */ 480 useCheckboxesForHidden : function() { 481 this.hidden = function(){ 482 return $('.hide-column-tog').not(':checked').map(function() { 483 var id = this.id; 484 return id.substring( id, id.length - 5 ); 485 }).get().join(','); 486 }; 487 }, 488 489 /** 490 * Adjusts the column span for the table. 491 * 492 * @since 3.1.0 493 * 494 * @param {number} diff The modifier for the column span. 495 */ 496 colSpanChange : function(diff) { 497 var $t = $('table').find('.colspanchange'), n; 498 if ( !$t.length ) 499 return; 500 n = parseInt( $t.attr('colspan'), 10 ) + diff; 501 $t.attr('colspan', n.toString()); 502 } 503 }; 504 505 $( function() { columns.init(); } ); 506 507 /** 508 * Validates that the required form fields are not empty. 509 * 510 * @since 2.9.0 511 * 512 * @param {jQuery} form The form to validate. 513 * 514 * @return {boolean} Returns true if all required fields are not an empty string. 515 */ 516 window.validateForm = function( form ) { 517 return !$( form ) 518 .find( '.form-required' ) 519 .filter( function() { return $( ':input:visible', this ).val() === ''; } ) 520 .addClass( 'form-invalid' ) 521 .find( ':input:visible' ) 522 .on( 'change', function() { $( this ).closest( '.form-invalid' ).removeClass( 'form-invalid' ); } ) 523 .length; 524 }; 525 526 // Stub for doing better warnings. 527 /** 528 * Shows message pop-up notice or confirmation message. 529 * 530 * @since 2.7.0 531 * 532 * @type {{warn: showNotice.warn, note: showNotice.note}} 533 * 534 * @return {void} 535 */ 536 window.showNotice = { 537 538 /** 539 * Shows a delete confirmation pop-up message. 540 * 541 * @since 2.7.0 542 * 543 * @return {boolean} Returns true if the message is confirmed. 544 */ 545 warn : function() { 546 if ( confirm( __( 'You are about to permanently delete these items from your site.\nThis action cannot be undone.\n\'Cancel\' to stop, \'OK\' to delete.' ) ) ) { 547 return true; 548 } 549 550 return false; 551 }, 552 553 /** 554 * Shows an alert message. 555 * 556 * @since 2.7.0 557 * 558 * @param {string} text The text to display in the message. 559 */ 560 note : function(text) { 561 alert(text); 562 } 563 }; 564 565 /** 566 * Represents the functions for the meta screen options panel. 567 * 568 * @since 3.2.0 569 * 570 * @type {{element: null, toggles: null, page: null, init: screenMeta.init, 571 * toggleEvent: screenMeta.toggleEvent, open: screenMeta.open, 572 * close: screenMeta.close}} 573 * 574 * @return {void} 575 */ 576 window.screenMeta = { 577 element: null, // #screen-meta 578 toggles: null, // .screen-meta-toggle 579 page: null, // #wpcontent 580 581 /** 582 * Initializes the screen meta options panel. 583 * 584 * @since 3.2.0 585 * 586 * @return {void} 587 */ 588 init: function() { 589 this.element = $('#screen-meta'); 590 this.toggles = $( '#screen-meta-links' ).find( '.show-settings' ); 591 this.page = $('#wpcontent'); 592 593 this.toggles.on( 'click', this.toggleEvent ); 594 }, 595 596 /** 597 * Toggles the screen meta options panel. 598 * 599 * @since 3.2.0 600 * 601 * @return {void} 602 */ 603 toggleEvent: function() { 604 var panel = $( '#' + $( this ).attr( 'aria-controls' ) ); 605 606 if ( !panel.length ) 607 return; 608 609 if ( panel.is(':visible') ) 610 screenMeta.close( panel, $(this) ); 611 else 612 screenMeta.open( panel, $(this) ); 613 }, 614 615 /** 616 * Opens the screen meta options panel. 617 * 618 * @since 3.2.0 619 * 620 * @param {jQuery} panel The screen meta options panel div. 621 * @param {jQuery} button The toggle button. 622 * 623 * @return {void} 624 */ 625 open: function( panel, button ) { 626 627 $( '#screen-meta-links' ).find( '.screen-meta-toggle' ).not( button.parent() ).css( 'visibility', 'hidden' ); 628 629 panel.parent().show(); 630 631 /** 632 * Sets the focus to the meta options panel and adds the necessary CSS classes. 633 * 634 * @since 3.2.0 635 * 636 * @return {void} 637 */ 638 panel.slideDown( 'fast', function() { 639 panel.removeClass( 'hidden' ).trigger( 'focus' ); 640 button.addClass( 'screen-meta-active' ).attr( 'aria-expanded', true ); 641 }); 642 643 $document.trigger( 'screen:options:open' ); 644 }, 645 646 /** 647 * Closes the screen meta options panel. 648 * 649 * @since 3.2.0 650 * 651 * @param {jQuery} panel The screen meta options panel div. 652 * @param {jQuery} button The toggle button. 653 * 654 * @return {void} 655 */ 656 close: function( panel, button ) { 657 /** 658 * Hides the screen meta options panel. 659 * 660 * @since 3.2.0 661 * 662 * @return {void} 663 */ 664 panel.slideUp( 'fast', function() { 665 button.removeClass( 'screen-meta-active' ).attr( 'aria-expanded', false ); 666 $('.screen-meta-toggle').css('visibility', ''); 667 panel.parent().hide(); 668 panel.addClass( 'hidden' ); 669 }); 670 671 $document.trigger( 'screen:options:close' ); 672 } 673 }; 674 675 /** 676 * Initializes the help tabs in the help panel. 677 * 678 * @param {Event} e The event object. 679 * 680 * @return {void} 681 */ 682 $('.contextual-help-tabs').on( 'click', 'a', function(e) { 683 var link = $(this), 684 panel; 685 686 e.preventDefault(); 687 688 // Don't do anything if the click is for the tab already showing. 689 if ( link.is('.active a') ) 690 return false; 691 692 // Links. 693 $('.contextual-help-tabs .active').removeClass('active'); 694 link.parent('li').addClass('active'); 695 696 panel = $( link.attr('href') ); 697 698 // Panels. 699 $('.help-tab-content').not( panel ).removeClass('active').hide(); 700 panel.addClass('active').show(); 701 }); 702 703 /** 704 * Update custom permalink structure via buttons. 705 */ 706 var permalinkStructureFocused = false, 707 $permalinkStructure = $( '#permalink_structure' ), 708 $permalinkStructureInputs = $( '.permalink-structure input:radio' ), 709 $permalinkCustomSelection = $( '#custom_selection' ), 710 $availableStructureTags = $( '.form-table.permalink-structure .available-structure-tags button' ); 711 712 // Change permalink structure input when selecting one of the common structures. 713 $permalinkStructureInputs.on( 'change', function() { 714 if ( 'custom' === this.value ) { 715 return; 716 } 717 718 $permalinkStructure.val( this.value ); 719 720 // Update button states after selection. 721 $availableStructureTags.each( function() { 722 changeStructureTagButtonState( $( this ) ); 723 } ); 724 } ); 725 726 $permalinkStructure.on( 'click input', function() { 727 $permalinkCustomSelection.prop( 'checked', true ); 728 } ); 729 730 // Check if the permalink structure input field has had focus at least once. 731 $permalinkStructure.on( 'focus', function( event ) { 732 permalinkStructureFocused = true; 733 $( this ).off( event ); 734 } ); 735 736 /** 737 * Enables or disables a structure tag button depending on its usage. 738 * 739 * If the structure is already used in the custom permalink structure, 740 * it will be disabled. 741 * 742 * @param {Object} button Button jQuery object. 743 */ 744 function changeStructureTagButtonState( button ) { 745 if ( -1 !== $permalinkStructure.val().indexOf( button.text().trim() ) ) { 746 button.attr( 'data-label', button.attr( 'aria-label' ) ); 747 button.attr( 'aria-label', button.attr( 'data-used' ) ); 748 button.attr( 'aria-pressed', true ); 749 button.addClass( 'active' ); 750 } else if ( button.attr( 'data-label' ) ) { 751 button.attr( 'aria-label', button.attr( 'data-label' ) ); 752 button.attr( 'aria-pressed', false ); 753 button.removeClass( 'active' ); 754 } 755 } 756 757 // Check initial button state. 758 $availableStructureTags.each( function() { 759 changeStructureTagButtonState( $( this ) ); 760 } ); 761 762 // Observe permalink structure field and disable buttons of tags that are already present. 763 $permalinkStructure.on( 'change', function() { 764 $availableStructureTags.each( function() { 765 changeStructureTagButtonState( $( this ) ); 766 } ); 767 } ); 768 769 $availableStructureTags.on( 'click', function() { 770 var permalinkStructureValue = $permalinkStructure.val(), 771 selectionStart = $permalinkStructure[ 0 ].selectionStart, 772 selectionEnd = $permalinkStructure[ 0 ].selectionEnd, 773 textToAppend = $( this ).text().trim(), 774 textToAnnounce, 775 newSelectionStart; 776 777 if ( $( this ).hasClass( 'active' ) ) { 778 textToAnnounce = $( this ).attr( 'data-removed' ); 779 } else { 780 textToAnnounce = $( this ).attr( 'data-added' ); 781 } 782 783 // Remove structure tag if already part of the structure. 784 if ( -1 !== permalinkStructureValue.indexOf( textToAppend ) ) { 785 permalinkStructureValue = permalinkStructureValue.replace( textToAppend + '/', '' ); 786 787 $permalinkStructure.val( '/' === permalinkStructureValue ? '' : permalinkStructureValue ); 788 789 // Announce change to screen readers. 790 $( '#custom_selection_updated' ).text( textToAnnounce ); 791 792 // Disable button. 793 changeStructureTagButtonState( $( this ) ); 794 795 return; 796 } 797 798 // Input field never had focus, move selection to end of input. 799 if ( ! permalinkStructureFocused && 0 === selectionStart && 0 === selectionEnd ) { 800 selectionStart = selectionEnd = permalinkStructureValue.length; 801 } 802 803 $permalinkCustomSelection.prop( 'checked', true ); 804 805 // Prepend and append slashes if necessary. 806 if ( '/' !== permalinkStructureValue.substr( 0, selectionStart ).substr( -1 ) ) { 807 textToAppend = '/' + textToAppend; 808 } 809 810 if ( '/' !== permalinkStructureValue.substr( selectionEnd, 1 ) ) { 811 textToAppend = textToAppend + '/'; 812 } 813 814 // Insert structure tag at the specified position. 815 $permalinkStructure.val( permalinkStructureValue.substr( 0, selectionStart ) + textToAppend + permalinkStructureValue.substr( selectionEnd ) ); 816 817 // Announce change to screen readers. 818 $( '#custom_selection_updated' ).text( textToAnnounce ); 819 820 // Disable button. 821 changeStructureTagButtonState( $( this ) ); 822 823 // If input had focus give it back with cursor right after appended text. 824 if ( permalinkStructureFocused && $permalinkStructure[0].setSelectionRange ) { 825 newSelectionStart = ( permalinkStructureValue.substr( 0, selectionStart ) + textToAppend ).length; 826 $permalinkStructure[0].setSelectionRange( newSelectionStart, newSelectionStart ); 827 $permalinkStructure.trigger( 'focus' ); 828 } 829 } ); 830 831 $( function() { 832 var checks, first, last, checked, sliced, mobileEvent, transitionTimeout, focusedRowActions, 833 lastClicked = false, 834 pageInput = $('input.current-page'), 835 currentPage = pageInput.val(), 836 isIOS = /iPhone|iPad|iPod/.test( navigator.userAgent ), 837 isAndroid = navigator.userAgent.indexOf( 'Android' ) !== -1, 838 $adminMenuWrap = $( '#adminmenuwrap' ), 839 $wpwrap = $( '#wpwrap' ), 840 $adminmenu = $( '#adminmenu' ), 841 $overlay = $( '#wp-responsive-overlay' ), 842 $toolbar = $( '#wp-toolbar' ), 843 $toolbarPopups = $toolbar.find( 'a[aria-haspopup="true"]' ), 844 $sortables = $('.meta-box-sortables'), 845 wpResponsiveActive = false, 846 $adminbar = $( '#wpadminbar' ), 847 lastScrollPosition = 0, 848 pinnedMenuTop = false, 849 pinnedMenuBottom = false, 850 menuTop = 0, 851 menuState, 852 menuIsPinned = false, 853 height = { 854 window: $window.height(), 855 wpwrap: $wpwrap.height(), 856 adminbar: $adminbar.height(), 857 menu: $adminMenuWrap.height() 858 }, 859 $headerEnd = $( '.wp-header-end' ); 860 861 /** 862 * Makes the fly-out submenu header clickable, when the menu is folded. 863 * 864 * @param {Event} e The event object. 865 * 866 * @return {void} 867 */ 868 $adminmenu.on('click.wp-submenu-head', '.wp-submenu-head', function(e){ 869 $(e.target).parent().siblings('a').get(0).click(); 870 }); 871 872 /** 873 * Collapses the admin menu. 874 * 875 * @return {void} 876 */ 877 $( '#collapse-button' ).on( 'click.collapse-menu', function() { 878 var viewportWidth = getViewportWidth() || 961; 879 880 // Reset any compensation for submenus near the bottom of the screen. 881 $('#adminmenu div.wp-submenu').css('margin-top', ''); 882 883 if ( viewportWidth <= 960 ) { 884 if ( $body.hasClass('auto-fold') ) { 885 $body.removeClass('auto-fold').removeClass('folded'); 886 setUserSetting('unfold', 1); 887 setUserSetting('mfold', 'o'); 888 menuState = 'open'; 889 } else { 890 $body.addClass('auto-fold'); 891 setUserSetting('unfold', 0); 892 menuState = 'folded'; 893 } 894 } else { 895 if ( $body.hasClass('folded') ) { 896 $body.removeClass('folded'); 897 setUserSetting('mfold', 'o'); 898 menuState = 'open'; 899 } else { 900 $body.addClass('folded'); 901 setUserSetting('mfold', 'f'); 902 menuState = 'folded'; 903 } 904 } 905 906 $document.trigger( 'wp-collapse-menu', { state: menuState } ); 907 }); 908 909 /** 910 * Ensures an admin submenu is within the visual viewport. 911 * 912 * @since 4.1.0 913 * 914 * @param {jQuery} $menuItem The parent menu item containing the submenu. 915 * 916 * @return {void} 917 */ 918 function adjustSubmenu( $menuItem ) { 919 var bottomOffset, pageHeight, adjustment, theFold, menutop, wintop, maxtop, 920 $submenu = $menuItem.find( '.wp-submenu' ); 921 922 menutop = $menuItem.offset().top; 923 wintop = $window.scrollTop(); 924 maxtop = menutop - wintop - 30; // max = make the top of the sub almost touch admin bar. 925 926 bottomOffset = menutop + $submenu.height() + 1; // Bottom offset of the menu. 927 pageHeight = $wpwrap.height(); // Height of the entire page. 928 adjustment = 60 + bottomOffset - pageHeight; 929 theFold = $window.height() + wintop - 50; // The fold. 930 931 if ( theFold < ( bottomOffset - adjustment ) ) { 932 adjustment = bottomOffset - theFold; 933 } 934 935 if ( adjustment > maxtop ) { 936 adjustment = maxtop; 937 } 938 939 if ( adjustment > 1 && $('#wp-admin-bar-menu-toggle').is(':hidden') ) { 940 $submenu.css( 'margin-top', '-' + adjustment + 'px' ); 941 } else { 942 $submenu.css( 'margin-top', '' ); 943 } 944 } 945 946 if ( 'ontouchstart' in window || /IEMobile\/[1-9]/.test(navigator.userAgent) ) { // Touch screen device. 947 // iOS Safari works with touchstart, the rest work with click. 948 mobileEvent = isIOS ? 'touchstart' : 'click'; 949 950 /** 951 * Closes any open submenus when touch/click is not on the menu. 952 * 953 * @param {Event} e The event object. 954 * 955 * @return {void} 956 */ 957 $body.on( mobileEvent+'.wp-mobile-hover', function(e) { 958 if ( $adminmenu.data('wp-responsive') ) { 959 return; 960 } 961 962 if ( ! $( e.target ).closest( '#adminmenu' ).length ) { 963 $adminmenu.find( 'li.opensub' ).removeClass( 'opensub' ); 964 } 965 }); 966 967 /** 968 * Handles the opening or closing the submenu based on the mobile click|touch event. 969 * 970 * @param {Event} event The event object. 971 * 972 * @return {void} 973 */ 974 $adminmenu.find( 'a.wp-has-submenu' ).on( mobileEvent + '.wp-mobile-hover', function( event ) { 975 var $menuItem = $(this).parent(); 976 977 if ( $adminmenu.data( 'wp-responsive' ) ) { 978 return; 979 } 980 981 /* 982 * Show the sub instead of following the link if: 983 * - the submenu is not open. 984 * - the submenu is not shown inline or the menu is not folded. 985 */ 986 if ( ! $menuItem.hasClass( 'opensub' ) && ( ! $menuItem.hasClass( 'wp-menu-open' ) || $menuItem.width() < 40 ) ) { 987 event.preventDefault(); 988 adjustSubmenu( $menuItem ); 989 $adminmenu.find( 'li.opensub' ).removeClass( 'opensub' ); 990 $menuItem.addClass('opensub'); 991 } 992 }); 993 } 994 995 if ( ! isIOS && ! isAndroid ) { 996 $adminmenu.find( 'li.wp-has-submenu' ).hoverIntent({ 997 998 /** 999 * Opens the submenu when hovered over the menu item for desktops. 1000 * 1001 * @return {void} 1002 */ 1003 over: function() { 1004 var $menuItem = $( this ), 1005 $submenu = $menuItem.find( '.wp-submenu' ), 1006 top = parseInt( $submenu.css( 'top' ), 10 ); 1007 1008 if ( isNaN( top ) || top > -5 ) { // The submenu is visible. 1009 return; 1010 } 1011 1012 if ( $adminmenu.data( 'wp-responsive' ) ) { 1013 // The menu is in responsive mode, bail. 1014 return; 1015 } 1016 1017 adjustSubmenu( $menuItem ); 1018 $adminmenu.find( 'li.opensub' ).removeClass( 'opensub' ); 1019 $menuItem.addClass( 'opensub' ); 1020 }, 1021 1022 /** 1023 * Closes the submenu when no longer hovering the menu item. 1024 * 1025 * @return {void} 1026 */ 1027 out: function(){ 1028 if ( $adminmenu.data( 'wp-responsive' ) ) { 1029 // The menu is in responsive mode, bail. 1030 return; 1031 } 1032 1033 $( this ).removeClass( 'opensub' ).find( '.wp-submenu' ).css( 'margin-top', '' ); 1034 }, 1035 timeout: 200, 1036 sensitivity: 7, 1037 interval: 90 1038 }); 1039 1040 /** 1041 * Opens the submenu on when focused on the menu item. 1042 * 1043 * @param {Event} event The event object. 1044 * 1045 * @return {void} 1046 */ 1047 $adminmenu.on( 'focus.adminmenu', '.wp-submenu a', function( event ) { 1048 if ( $adminmenu.data( 'wp-responsive' ) ) { 1049 // The menu is in responsive mode, bail. 1050 return; 1051 } 1052 1053 $( event.target ).closest( 'li.menu-top' ).addClass( 'opensub' ); 1054 1055 /** 1056 * Closes the submenu on blur from the menu item. 1057 * 1058 * @param {Event} event The event object. 1059 * 1060 * @return {void} 1061 */ 1062 }).on( 'blur.adminmenu', '.wp-submenu a', function( event ) { 1063 if ( $adminmenu.data( 'wp-responsive' ) ) { 1064 return; 1065 } 1066 1067 $( event.target ).closest( 'li.menu-top' ).removeClass( 'opensub' ); 1068 1069 /** 1070 * Adjusts the size for the submenu. 1071 * 1072 * @return {void} 1073 */ 1074 }).find( 'li.wp-has-submenu.wp-not-current-submenu' ).on( 'focusin.adminmenu', function() { 1075 adjustSubmenu( $( this ) ); 1076 }); 1077 } 1078 1079 /* 1080 * The `.below-h2` class is here just for backward compatibility with plugins 1081 * that are (incorrectly) using it. Do not use. Use `.inline` instead. See #34570. 1082 * If '.wp-header-end' is found, append the notices after it otherwise 1083 * after the first h1 or h2 heading found within the main content. 1084 */ 1085 if ( ! $headerEnd.length ) { 1086 $headerEnd = $( '.wrap h1, .wrap h2' ).first(); 1087 } 1088 $( 'div.updated, div.error, div.notice' ).not( '.inline, .below-h2' ).insertAfter( $headerEnd ); 1089 1090 /** 1091 * Makes notices dismissible. 1092 * 1093 * @since 4.4.0 1094 * 1095 * @return {void} 1096 */ 1097 function makeNoticesDismissible() { 1098 $( '.notice.is-dismissible' ).each( function() { 1099 var $el = $( this ), 1100 $button = $( '<button type="button" class="notice-dismiss"><span class="screen-reader-text"></span></button>' ); 1101 1102 if ( $el.find( '.notice-dismiss' ).length ) { 1103 return; 1104 } 1105 1106 // Ensure plain text. 1107 $button.find( '.screen-reader-text' ).text( __( 'Dismiss this notice.' ) ); 1108 $button.on( 'click.wp-dismiss-notice', function( event ) { 1109 event.preventDefault(); 1110 $el.fadeTo( 100, 0, function() { 1111 $el.slideUp( 100, function() { 1112 $el.remove(); 1113 }); 1114 }); 1115 }); 1116 1117 $el.append( $button ); 1118 }); 1119 } 1120 1121 $document.on( 'wp-updates-notice-added wp-plugin-install-error wp-plugin-update-error wp-plugin-delete-error wp-theme-install-error wp-theme-delete-error wp-notice-added', makeNoticesDismissible ); 1122 1123 // Init screen meta. 1124 screenMeta.init(); 1125 1126 /** 1127 * Checks a checkbox. 1128 * 1129 * This event needs to be delegated. Ticket #37973. 1130 * 1131 * @return {boolean} Returns whether a checkbox is checked or not. 1132 */ 1133 $body.on( 'click', 'tbody > tr > .check-column :checkbox', function( event ) { 1134 // Shift click to select a range of checkboxes. 1135 if ( 'undefined' == event.shiftKey ) { return true; } 1136 if ( event.shiftKey ) { 1137 if ( !lastClicked ) { return true; } 1138 checks = $( lastClicked ).closest( 'form' ).find( ':checkbox' ).filter( ':visible:enabled' ); 1139 first = checks.index( lastClicked ); 1140 last = checks.index( this ); 1141 checked = $(this).prop('checked'); 1142 if ( 0 < first && 0 < last && first != last ) { 1143 sliced = ( last > first ) ? checks.slice( first, last ) : checks.slice( last, first ); 1144 sliced.prop( 'checked', function() { 1145 if ( $(this).closest('tr').is(':visible') ) 1146 return checked; 1147 1148 return false; 1149 }); 1150 } 1151 } 1152 lastClicked = this; 1153 1154 // Toggle the "Select all" checkboxes depending if the other ones are all checked or not. 1155 var unchecked = $(this).closest('tbody').find('tr').find(':checkbox').filter(':visible:enabled').not(':checked'); 1156 1157 /** 1158 * Determines if all checkboxes are checked. 1159 * 1160 * @return {boolean} Returns true if there are no unchecked checkboxes. 1161 */ 1162 $(this).closest('table').children('thead, tfoot').find(':checkbox').prop('checked', function() { 1163 return ( 0 === unchecked.length ); 1164 }); 1165 1166 return true; 1167 }); 1168 1169 /** 1170 * Controls all the toggles on bulk toggle change. 1171 * 1172 * When the bulk checkbox is changed, all the checkboxes in the tables are changed accordingly. 1173 * When the shift-button is pressed while changing the bulk checkbox the checkboxes in the table are inverted. 1174 * 1175 * This event needs to be delegated. Ticket #37973. 1176 * 1177 * @param {Event} event The event object. 1178 * 1179 * @return {boolean} 1180 */ 1181 $body.on( 'click.wp-toggle-checkboxes', 'thead .check-column :checkbox, tfoot .check-column :checkbox', function( event ) { 1182 var $this = $(this), 1183 $table = $this.closest( 'table' ), 1184 controlChecked = $this.prop('checked'), 1185 toggle = event.shiftKey || $this.data('wp-toggle'); 1186 1187 $table.children( 'tbody' ).filter(':visible') 1188 .children().children('.check-column').find(':checkbox') 1189 /** 1190 * Updates the checked state on the checkbox in the table. 1191 * 1192 * @return {boolean} True checks the checkbox, False unchecks the checkbox. 1193 */ 1194 .prop('checked', function() { 1195 if ( $(this).is(':hidden,:disabled') ) { 1196 return false; 1197 } 1198 1199 if ( toggle ) { 1200 return ! $(this).prop( 'checked' ); 1201 } else if ( controlChecked ) { 1202 return true; 1203 } 1204 1205 return false; 1206 }); 1207 1208 $table.children('thead, tfoot').filter(':visible') 1209 .children().children('.check-column').find(':checkbox') 1210 1211 /** 1212 * Syncs the bulk checkboxes on the top and bottom of the table. 1213 * 1214 * @return {boolean} True checks the checkbox, False unchecks the checkbox. 1215 */ 1216 .prop('checked', function() { 1217 if ( toggle ) { 1218 return false; 1219 } else if ( controlChecked ) { 1220 return true; 1221 } 1222 1223 return false; 1224 }); 1225 }); 1226 1227 /** 1228 * Marries a secondary control to its primary control. 1229 * 1230 * @param {jQuery} topSelector The top selector element. 1231 * @param {jQuery} topSubmit The top submit element. 1232 * @param {jQuery} bottomSelector The bottom selector element. 1233 * @param {jQuery} bottomSubmit The bottom submit element. 1234 * @return {void} 1235 */ 1236 function marryControls( topSelector, topSubmit, bottomSelector, bottomSubmit ) { 1237 /** 1238 * Updates the primary selector when the secondary selector is changed. 1239 * 1240 * @since 5.7.0 1241 * 1242 * @return {void} 1243 */ 1244 function updateTopSelector() { 1245 topSelector.val($(this).val()); 1246 } 1247 bottomSelector.on('change', updateTopSelector); 1248 1249 /** 1250 * Updates the secondary selector when the primary selector is changed. 1251 * 1252 * @since 5.7.0 1253 * 1254 * @return {void} 1255 */ 1256 function updateBottomSelector() { 1257 bottomSelector.val($(this).val()); 1258 } 1259 topSelector.on('change', updateBottomSelector); 1260 1261 /** 1262 * Triggers the primary submit when then secondary submit is clicked. 1263 * 1264 * @param {SubmitEvent} e The event object. 1265 * @since 5.7.0 1266 * 1267 * @return {void} 1268 */ 1269 function triggerSubmitClick(e) { 1270 e.preventDefault(); 1271 e.stopPropagation(); 1272 1273 topSubmit.trigger('click'); 1274 } 1275 bottomSubmit.on('click', triggerSubmitClick); 1276 } 1277 1278 // Marry the secondary "Bulk actions" controls to the primary controls: 1279 marryControls( $('#bulk-action-selector-top'), $('#doaction'), $('#bulk-action-selector-bottom'), $('#doaction2') ); 1280 1281 // Marry the secondary "Change role to" controls to the primary controls: 1282 marryControls( $('#new_role'), $('#changeit'), $('#new_role2'), $('#changeit2') ); 1283 1284 var addAdminNotice = function( data ) { 1285 var $notice = $( data.selector ), 1286 $headerEnd = $( '.wp-header-end' ), 1287 type, 1288 dismissible, 1289 $adminNotice; 1290 1291 delete data.selector; 1292 1293 dismissible = ( data.dismissible && data.dismissible === true ) ? ' is-dismissible' : ''; 1294 type = ( data.type ) ? data.type : 'info'; 1295 1296 $adminNotice = '<div id="' + data.id + '" class="notice notice-' + data.type + dismissible + '"><p>' + data.message + '</p></div>'; 1297 1298 // Check if this admin notice already exists. 1299 if ( ! $notice.length ) { 1300 $notice = $( '#' + data.id ); 1301 } 1302 1303 if ( $notice.length ) { 1304 $notice.replaceWith( $adminNotice ); 1305 } else if ( $headerEnd.length ) { 1306 $headerEnd.after( $adminNotice ); 1307 } else { 1308 if ( 'customize' === pagenow ) { 1309 $( '.customize-themes-notifications' ).append( $adminNotice ); 1310 } else { 1311 $( '.wrap' ).find( '> h1' ).after( $adminNotice ); 1312 } 1313 } 1314 1315 $document.trigger( 'wp-notice-added' ); 1316 }; 1317 1318 $( '.bulkactions' ).parents( 'form' ).on( 'submit', function( event ) { 1319 var form = this, 1320 submitterName = event.originalEvent && event.originalEvent.submitter ? event.originalEvent.submitter.name : false, 1321 currentPageSelector = form.querySelector( '#current-page-selector' ); 1322 1323 if ( currentPageSelector && currentPageSelector.defaultValue !== currentPageSelector.value ) { 1324 return; // Pagination form submission. 1325 } 1326 1327 // Observe submissions from posts lists for 'bulk_action' or users lists for 'new_role'. 1328 var bulkFieldRelations = { 1329 'bulk_action' : window.bulkActionObserverIds.bulk_action, 1330 'changeit' : window.bulkActionObserverIds.changeit 1331 }; 1332 if ( ! Object.keys( bulkFieldRelations ).includes( submitterName ) ) { 1333 return; 1334 } 1335 1336 var values = new FormData(form); 1337 var value = values.get( bulkFieldRelations[ submitterName ] ) || '-1'; 1338 1339 // Check that the action is not the default one. 1340 if ( value !== '-1' ) { 1341 // Check that at least one item is selected. 1342 var itemsSelected = form.querySelectorAll( '.wp-list-table tbody .check-column input[type="checkbox"]:checked' ); 1343 1344 if ( itemsSelected.length > 0 ) { 1345 return; 1346 } 1347 } 1348 event.preventDefault(); 1349 event.stopPropagation(); 1350 $( 'html, body' ).animate( { scrollTop: 0 } ); 1351 1352 var errorMessage = value !== '-1' ? 1353 __( 'Please select at least one item to perform this action on.' ) : 1354 __( 'Please select a bulk action to perform.' ); 1355 addAdminNotice( { 1356 id: value !== '-1' ? 'no-items-selected' : 'no-bulk-action-selected', 1357 type: 'error', 1358 message: errorMessage, 1359 dismissible: true, 1360 } ); 1361 1362 wp.a11y.speak( errorMessage ); 1363 }); 1364 1365 /** 1366 * Shows row actions on focus of its parent container element or any other elements contained within. 1367 * 1368 * @return {void} 1369 */ 1370 $( '#wpbody-content' ).on({ 1371 focusin: function() { 1372 clearTimeout( transitionTimeout ); 1373 focusedRowActions = $( this ).find( '.row-actions' ); 1374 // transitionTimeout is necessary for Firefox, but Chrome won't remove the CSS class without a little help. 1375 $( '.row-actions' ).not( this ).removeClass( 'visible' ); 1376 focusedRowActions.addClass( 'visible' ); 1377 }, 1378 focusout: function() { 1379 // Tabbing between post title and .row-actions links needs a brief pause, otherwise 1380 // the .row-actions div gets hidden in transit in some browsers (ahem, Firefox). 1381 transitionTimeout = setTimeout( function() { 1382 focusedRowActions.removeClass( 'visible' ); 1383 }, 30 ); 1384 } 1385 }, '.table-view-list .has-row-actions' ); 1386 1387 // Toggle list table rows on small screens. 1388 $( 'tbody' ).on( 'click', '.toggle-row', function() { 1389 $( this ).closest( 'tr' ).toggleClass( 'is-expanded' ); 1390 }); 1391 1392 $('#default-password-nag-no').on( 'click', function() { 1393 setUserSetting('default_password_nag', 'hide'); 1394 $('div.default-password-nag').hide(); 1395 return false; 1396 }); 1397 1398 /** 1399 * Handles tab keypresses in theme and plugin file editor textareas. 1400 * 1401 * @param {Event} e The event object. 1402 * 1403 * @return {void} 1404 */ 1405 $('#newcontent').on('keydown.wpevent_InsertTab', function(e) { 1406 var el = e.target, selStart, selEnd, val, scroll, sel; 1407 1408 // After pressing escape key (keyCode: 27), the tab key should tab out of the textarea. 1409 if ( e.keyCode == 27 ) { 1410 // When pressing Escape: Opera 12 and 27 blur form fields, IE 8 clears them. 1411 e.preventDefault(); 1412 $(el).data('tab-out', true); 1413 return; 1414 } 1415 1416 // Only listen for plain tab key (keyCode: 9) without any modifiers. 1417 if ( e.keyCode != 9 || e.ctrlKey || e.altKey || e.shiftKey ) 1418 return; 1419 1420 // After tabbing out, reset it so next time the tab key can be used again. 1421 if ( $(el).data('tab-out') ) { 1422 $(el).data('tab-out', false); 1423 return; 1424 } 1425 1426 selStart = el.selectionStart; 1427 selEnd = el.selectionEnd; 1428 val = el.value; 1429 1430 // If any text is selected, replace the selection with a tab character. 1431 if ( document.selection ) { 1432 el.focus(); 1433 sel = document.selection.createRange(); 1434 sel.text = '\t'; 1435 } else if ( selStart >= 0 ) { 1436 scroll = this.scrollTop; 1437 el.value = val.substring(0, selStart).concat('\t', val.substring(selEnd) ); 1438 el.selectionStart = el.selectionEnd = selStart + 1; 1439 this.scrollTop = scroll; 1440 } 1441 1442 // Cancel the regular tab functionality, to prevent losing focus of the textarea. 1443 if ( e.stopPropagation ) 1444 e.stopPropagation(); 1445 if ( e.preventDefault ) 1446 e.preventDefault(); 1447 }); 1448 1449 // Reset page number variable for new filters/searches but not for bulk actions. See #17685. 1450 if ( pageInput.length ) { 1451 1452 /** 1453 * Handles pagination variable when filtering the list table. 1454 * 1455 * Set the pagination argument to the first page when the post-filter form is submitted. 1456 * This happens when pressing the 'filter' button on the list table page. 1457 * 1458 * The pagination argument should not be touched when the bulk action dropdowns are set to do anything. 1459 * 1460 * The form closest to the pageInput is the post-filter form. 1461 * 1462 * @return {void} 1463 */ 1464 pageInput.closest('form').on( 'submit', function() { 1465 /* 1466 * action = bulk action dropdown at the top of the table 1467 */ 1468 if ( $('select[name="action"]').val() == -1 && pageInput.val() == currentPage ) 1469 pageInput.val('1'); 1470 }); 1471 } 1472 1473 /** 1474 * Resets the bulk actions when the search button is clicked. 1475 * 1476 * @return {void} 1477 */ 1478 $('.search-box input[type="search"], .search-box input[type="submit"]').on( 'mousedown', function () { 1479 $('select[name^="action"]').val('-1'); 1480 }); 1481 1482 /** 1483 * Scrolls into view when focus.scroll-into-view is triggered. 1484 * 1485 * @param {Event} e The event object. 1486 * 1487 * @return {void} 1488 */ 1489 $('#contextual-help-link, #show-settings-link').on( 'focus.scroll-into-view', function(e){ 1490 if ( e.target.scrollIntoViewIfNeeded ) 1491 e.target.scrollIntoViewIfNeeded(false); 1492 }); 1493 1494 /** 1495 * Disables the submit upload buttons when no data is entered. 1496 * 1497 * @return {void} 1498 */ 1499 (function(){ 1500 var button, input, form = $('form.wp-upload-form'); 1501 1502 // Exit when no upload form is found. 1503 if ( ! form.length ) 1504 return; 1505 1506 button = form.find('input[type="submit"]'); 1507 input = form.find('input[type="file"]'); 1508 1509 /** 1510 * Determines if any data is entered in any file upload input. 1511 * 1512 * @since 3.5.0 1513 * 1514 * @return {void} 1515 */ 1516 function toggleUploadButton() { 1517 // When no inputs have a value, disable the upload buttons. 1518 button.prop('disabled', '' === input.map( function() { 1519 return $(this).val(); 1520 }).get().join('')); 1521 } 1522 1523 // Update the status initially. 1524 toggleUploadButton(); 1525 // Update the status when any file input changes. 1526 input.on('change', toggleUploadButton); 1527 })(); 1528 1529 /** 1530 * Pins the menu while distraction-free writing is enabled. 1531 * 1532 * @param {Event} event Event data. 1533 * 1534 * @since 4.1.0 1535 * 1536 * @return {void} 1537 */ 1538 function pinMenu( event ) { 1539 var windowPos = $window.scrollTop(), 1540 resizing = ! event || event.type !== 'scroll'; 1541 1542 if ( isIOS || $adminmenu.data( 'wp-responsive' ) ) { 1543 return; 1544 } 1545 1546 /* 1547 * When the menu is higher than the window and smaller than the entire page. 1548 * It should be adjusted to be able to see the entire menu. 1549 * 1550 * Otherwise it can be accessed normally. 1551 */ 1552 if ( height.menu + height.adminbar < height.window || 1553 height.menu + height.adminbar + 20 > height.wpwrap ) { 1554 unpinMenu(); 1555 return; 1556 } 1557 1558 menuIsPinned = true; 1559 1560 // If the menu is higher than the window, compensate on scroll. 1561 if ( height.menu + height.adminbar > height.window ) { 1562 // Check for overscrolling, this happens when swiping up at the top of the document in modern browsers. 1563 if ( windowPos < 0 ) { 1564 // Stick the menu to the top. 1565 if ( ! pinnedMenuTop ) { 1566 pinnedMenuTop = true; 1567 pinnedMenuBottom = false; 1568 1569 $adminMenuWrap.css({ 1570 position: 'fixed', 1571 top: '', 1572 bottom: '' 1573 }); 1574 } 1575 1576 return; 1577 } else if ( windowPos + height.window > $document.height() - 1 ) { 1578 // When overscrolling at the bottom, stick the menu to the bottom. 1579 if ( ! pinnedMenuBottom ) { 1580 pinnedMenuBottom = true; 1581 pinnedMenuTop = false; 1582 1583 $adminMenuWrap.css({ 1584 position: 'fixed', 1585 top: '', 1586 bottom: 0 1587 }); 1588 } 1589 1590 return; 1591 } 1592 1593 if ( windowPos > lastScrollPosition ) { 1594 // When a down scroll has been detected. 1595 1596 // If it was pinned to the top, unpin and calculate relative scroll. 1597 if ( pinnedMenuTop ) { 1598 pinnedMenuTop = false; 1599 // Calculate new offset position. 1600 menuTop = $adminMenuWrap.offset().top - height.adminbar - ( windowPos - lastScrollPosition ); 1601 1602 if ( menuTop + height.menu + height.adminbar < windowPos + height.window ) { 1603 menuTop = windowPos + height.window - height.menu - height.adminbar; 1604 } 1605 1606 $adminMenuWrap.css({ 1607 position: 'absolute', 1608 top: menuTop, 1609 bottom: '' 1610 }); 1611 } else if ( ! pinnedMenuBottom && $adminMenuWrap.offset().top + height.menu < windowPos + height.window ) { 1612 // Pin it to the bottom. 1613 pinnedMenuBottom = true; 1614 1615 $adminMenuWrap.css({ 1616 position: 'fixed', 1617 top: '', 1618 bottom: 0 1619 }); 1620 } 1621 } else if ( windowPos < lastScrollPosition ) { 1622 // When a scroll up is detected. 1623 1624 // If it was pinned to the bottom, unpin and calculate relative scroll. 1625 if ( pinnedMenuBottom ) { 1626 pinnedMenuBottom = false; 1627 1628 // Calculate new offset position. 1629 menuTop = $adminMenuWrap.offset().top - height.adminbar + ( lastScrollPosition - windowPos ); 1630 1631 if ( menuTop + height.menu > windowPos + height.window ) { 1632 menuTop = windowPos; 1633 } 1634 1635 $adminMenuWrap.css({ 1636 position: 'absolute', 1637 top: menuTop, 1638 bottom: '' 1639 }); 1640 } else if ( ! pinnedMenuTop && $adminMenuWrap.offset().top >= windowPos + height.adminbar ) { 1641 1642 // Pin it to the top. 1643 pinnedMenuTop = true; 1644 1645 $adminMenuWrap.css({ 1646 position: 'fixed', 1647 top: '', 1648 bottom: '' 1649 }); 1650 } 1651 } else if ( resizing ) { 1652 // Window is being resized. 1653 1654 pinnedMenuTop = pinnedMenuBottom = false; 1655 1656 // Calculate the new offset. 1657 menuTop = windowPos + height.window - height.menu - height.adminbar - 1; 1658 1659 if ( menuTop > 0 ) { 1660 $adminMenuWrap.css({ 1661 position: 'absolute', 1662 top: menuTop, 1663 bottom: '' 1664 }); 1665 } else { 1666 unpinMenu(); 1667 } 1668 } 1669 } 1670 1671 lastScrollPosition = windowPos; 1672 } 1673 1674 /** 1675 * Determines the height of certain elements. 1676 * 1677 * @since 4.1.0 1678 * 1679 * @return {void} 1680 */ 1681 function resetHeights() { 1682 height = { 1683 window: $window.height(), 1684 wpwrap: $wpwrap.height(), 1685 adminbar: $adminbar.height(), 1686 menu: $adminMenuWrap.height() 1687 }; 1688 } 1689 1690 /** 1691 * Unpins the menu. 1692 * 1693 * @since 4.1.0 1694 * 1695 * @return {void} 1696 */ 1697 function unpinMenu() { 1698 if ( isIOS || ! menuIsPinned ) { 1699 return; 1700 } 1701 1702 pinnedMenuTop = pinnedMenuBottom = menuIsPinned = false; 1703 $adminMenuWrap.css({ 1704 position: '', 1705 top: '', 1706 bottom: '' 1707 }); 1708 } 1709 1710 /** 1711 * Pins and unpins the menu when applicable. 1712 * 1713 * @since 4.1.0 1714 * 1715 * @return {void} 1716 */ 1717 function setPinMenu() { 1718 resetHeights(); 1719 1720 if ( $adminmenu.data('wp-responsive') ) { 1721 $body.removeClass( 'sticky-menu' ); 1722 unpinMenu(); 1723 } else if ( height.menu + height.adminbar > height.window ) { 1724 pinMenu(); 1725 $body.removeClass( 'sticky-menu' ); 1726 } else { 1727 $body.addClass( 'sticky-menu' ); 1728 unpinMenu(); 1729 } 1730 } 1731 1732 if ( ! isIOS ) { 1733 $window.on( 'scroll.pin-menu', pinMenu ); 1734 $document.on( 'tinymce-editor-init.pin-menu', function( event, editor ) { 1735 editor.on( 'wp-autoresize', resetHeights ); 1736 }); 1737 } 1738 1739 /** 1740 * Changes the sortables and responsiveness of metaboxes. 1741 * 1742 * @since 3.8.0 1743 * 1744 * @return {void} 1745 */ 1746 window.wpResponsive = { 1747 1748 /** 1749 * Initializes the wpResponsive object. 1750 * 1751 * @since 3.8.0 1752 * 1753 * @return {void} 1754 */ 1755 init: function() { 1756 var self = this; 1757 1758 this.maybeDisableSortables = this.maybeDisableSortables.bind( this ); 1759 1760 // Modify functionality based on custom activate/deactivate event. 1761 $document.on( 'wp-responsive-activate.wp-responsive', function() { 1762 self.activate(); 1763 self.toggleAriaHasPopup( 'add' ); 1764 }).on( 'wp-responsive-deactivate.wp-responsive', function() { 1765 self.deactivate(); 1766 self.toggleAriaHasPopup( 'remove' ); 1767 }); 1768 1769 $( '#wp-admin-bar-menu-toggle a' ).attr( 'aria-expanded', 'false' ); 1770 1771 // Toggle sidebar when toggle is clicked. 1772 $( '#wp-admin-bar-menu-toggle' ).on( 'click.wp-responsive', function( event ) { 1773 event.preventDefault(); 1774 1775 // Close any open toolbar submenus. 1776 $adminbar.find( '.hover' ).removeClass( 'hover' ); 1777 1778 $wpwrap.toggleClass( 'wp-responsive-open' ); 1779 if ( $wpwrap.hasClass( 'wp-responsive-open' ) ) { 1780 $(this).find('a').attr( 'aria-expanded', 'true' ); 1781 $( '#adminmenu a:first' ).trigger( 'focus' ); 1782 } else { 1783 $(this).find('a').attr( 'aria-expanded', 'false' ); 1784 } 1785 } ); 1786 1787 // Close sidebar when target moves outside of toggle and sidebar. 1788 $( document ).on( 'click', function( event ) { 1789 if ( ! $wpwrap.hasClass( 'wp-responsive-open' ) || ! document.hasFocus() ) { 1790 return; 1791 } 1792 1793 var focusIsInToggle = $.contains( $( '#wp-admin-bar-menu-toggle' )[0], event.target ); 1794 var focusIsInSidebar = $.contains( $( '#adminmenuwrap' )[0], event.target ); 1795 1796 if ( ! focusIsInToggle && ! focusIsInSidebar ) { 1797 $( '#wp-admin-bar-menu-toggle' ).trigger( 'click.wp-responsive' ); 1798 } 1799 } ); 1800 1801 // Close sidebar when a keypress completes outside of toggle and sidebar. 1802 $( document ).on( 'keyup', function( event ) { 1803 var toggleButton = $( '#wp-admin-bar-menu-toggle' )[0]; 1804 if ( ! $wpwrap.hasClass( 'wp-responsive-open' ) ) { 1805 return; 1806 } 1807 if ( 27 === event.keyCode ) { 1808 $( toggleButton ).trigger( 'click.wp-responsive' ); 1809 $( toggleButton ).find( 'a' ).trigger( 'focus' ); 1810 } else { 1811 if ( 9 === event.keyCode ) { 1812 var sidebar = $( '#adminmenuwrap' )[0]; 1813 var focusedElement = event.relatedTarget || document.activeElement; 1814 // A brief delay is required to allow focus to switch to another element. 1815 setTimeout( function() { 1816 var focusIsInToggle = $.contains( toggleButton, focusedElement ); 1817 var focusIsInSidebar = $.contains( sidebar, focusedElement ); 1818 1819 if ( ! focusIsInToggle && ! focusIsInSidebar ) { 1820 $( toggleButton ).trigger( 'click.wp-responsive' ); 1821 } 1822 }, 10 ); 1823 } 1824 } 1825 }); 1826 1827 // Add menu events. 1828 $adminmenu.on( 'click.wp-responsive', 'li.wp-has-submenu > a', function( event ) { 1829 if ( ! $adminmenu.data('wp-responsive') ) { 1830 return; 1831 } 1832 let state = ( 'false' === $( this ).attr( 'aria-expanded' ) ) ? 'true' : 'false'; 1833 $( this ).parent( 'li' ).toggleClass( 'selected' ); 1834 $( this ).attr( 'aria-expanded', state ); 1835 $( this ).trigger( 'focus' ); 1836 event.preventDefault(); 1837 }); 1838 1839 self.trigger(); 1840 $document.on( 'wp-window-resized.wp-responsive', this.trigger.bind( this ) ); 1841 1842 // This needs to run later as UI Sortable may be initialized when the document is ready. 1843 $window.on( 'load.wp-responsive', this.maybeDisableSortables ); 1844 $document.on( 'postbox-toggled', this.maybeDisableSortables ); 1845 1846 // When the screen columns are changed, potentially disable sortables. 1847 $( '#screen-options-wrap input' ).on( 'click', this.maybeDisableSortables ); 1848 }, 1849 1850 /** 1851 * Disable sortables if there is only one metabox, or the screen is in one column mode. Otherwise, enable sortables. 1852 * 1853 * @since 5.3.0 1854 * 1855 * @return {void} 1856 */ 1857 maybeDisableSortables: function() { 1858 var width = navigator.userAgent.indexOf('AppleWebKit/') > -1 ? $window.width() : window.innerWidth; 1859 1860 if ( 1861 ( width <= 782 ) || 1862 ( 1 >= $sortables.find( '.ui-sortable-handle:visible' ).length && jQuery( '.columns-prefs-1 input' ).prop( 'checked' ) ) 1863 ) { 1864 this.disableSortables(); 1865 } else { 1866 this.enableSortables(); 1867 } 1868 }, 1869 1870 /** 1871 * Changes properties of body and admin menu. 1872 * 1873 * Pins and unpins the menu and adds the auto-fold class to the body. 1874 * Makes the admin menu responsive and disables the metabox sortables. 1875 * 1876 * @since 3.8.0 1877 * 1878 * @return {void} 1879 */ 1880 activate: function() { 1881 setPinMenu(); 1882 1883 if ( ! $body.hasClass( 'auto-fold' ) ) { 1884 $body.addClass( 'auto-fold' ); 1885 } 1886 1887 $adminmenu.data( 'wp-responsive', 1 ); 1888 this.disableSortables(); 1889 }, 1890 1891 /** 1892 * Changes properties of admin menu and enables metabox sortables. 1893 * 1894 * Pin and unpin the menu. 1895 * Removes the responsiveness of the admin menu and enables the metabox sortables. 1896 * 1897 * @since 3.8.0 1898 * 1899 * @return {void} 1900 */ 1901 deactivate: function() { 1902 setPinMenu(); 1903 $adminmenu.removeData('wp-responsive'); 1904 1905 this.maybeDisableSortables(); 1906 }, 1907 1908 /** 1909 * Toggles the aria-haspopup attribute for the responsive admin menu. 1910 * 1911 * The aria-haspopup attribute is only necessary for the responsive menu. 1912 * See ticket https://core.trac.wordpress.org/ticket/43095 1913 * 1914 * @since 6.6.0 1915 * 1916 * @param {string} action Whether to add or remove the aria-haspopup attribute. 1917 * 1918 * @return {void} 1919 */ 1920 toggleAriaHasPopup: function( action ) { 1921 var elements = $adminmenu.find( '[data-ariahaspopup]' ); 1922 1923 if ( action === 'add' ) { 1924 elements.each( function() { 1925 $( this ).attr( 'aria-haspopup', 'menu' ).attr( 'aria-expanded', 'false' ); 1926 } ); 1927 1928 return; 1929 } 1930 1931 elements.each( function() { 1932 $( this ).removeAttr( 'aria-haspopup' ).removeAttr( 'aria-expanded' ); 1933 } ); 1934 }, 1935 1936 /** 1937 * Sets the responsiveness and enables the overlay based on the viewport width. 1938 * 1939 * @since 3.8.0 1940 * 1941 * @return {void} 1942 */ 1943 trigger: function() { 1944 var viewportWidth = getViewportWidth(); 1945 1946 // Exclude IE < 9, it doesn't support @media CSS rules. 1947 if ( ! viewportWidth ) { 1948 return; 1949 } 1950 1951 if ( viewportWidth <= 782 ) { 1952 if ( ! wpResponsiveActive ) { 1953 $document.trigger( 'wp-responsive-activate' ); 1954 wpResponsiveActive = true; 1955 } 1956 } else { 1957 if ( wpResponsiveActive ) { 1958 $document.trigger( 'wp-responsive-deactivate' ); 1959 wpResponsiveActive = false; 1960 } 1961 } 1962 1963 if ( viewportWidth <= 480 ) { 1964 this.enableOverlay(); 1965 } else { 1966 this.disableOverlay(); 1967 } 1968 1969 this.maybeDisableSortables(); 1970 }, 1971 1972 /** 1973 * Inserts a responsive overlay and toggles the window. 1974 * 1975 * @since 3.8.0 1976 * 1977 * @return {void} 1978 */ 1979 enableOverlay: function() { 1980 if ( $overlay.length === 0 ) { 1981 $overlay = $( '<div id="wp-responsive-overlay"></div>' ) 1982 .insertAfter( '#wpcontent' ) 1983 .hide() 1984 .on( 'click.wp-responsive', function() { 1985 $toolbar.find( '.menupop.hover' ).removeClass( 'hover' ); 1986 $( this ).hide(); 1987 }); 1988 } 1989 1990 $toolbarPopups.on( 'click.wp-responsive', function() { 1991 $overlay.show(); 1992 }); 1993 }, 1994 1995 /** 1996 * Disables the responsive overlay and removes the overlay. 1997 * 1998 * @since 3.8.0 1999 * 2000 * @return {void} 2001 */ 2002 disableOverlay: function() { 2003 $toolbarPopups.off( 'click.wp-responsive' ); 2004 $overlay.hide(); 2005 }, 2006 2007 /** 2008 * Disables sortables. 2009 * 2010 * @since 3.8.0 2011 * 2012 * @return {void} 2013 */ 2014 disableSortables: function() { 2015 if ( $sortables.length ) { 2016 try { 2017 $sortables.sortable( 'disable' ); 2018 $sortables.find( '.ui-sortable-handle' ).addClass( 'is-non-sortable' ); 2019 } catch ( e ) {} 2020 } 2021 }, 2022 2023 /** 2024 * Enables sortables. 2025 * 2026 * @since 3.8.0 2027 * 2028 * @return {void} 2029 */ 2030 enableSortables: function() { 2031 if ( $sortables.length ) { 2032 try { 2033 $sortables.sortable( 'enable' ); 2034 $sortables.find( '.ui-sortable-handle' ).removeClass( 'is-non-sortable' ); 2035 } catch ( e ) {} 2036 } 2037 } 2038 }; 2039 2040 /** 2041 * Add an ARIA role `button` to elements that behave like UI controls when JavaScript is on. 2042 * 2043 * @since 4.5.0 2044 * 2045 * @return {void} 2046 */ 2047 function aria_button_if_js() { 2048 $( '.aria-button-if-js' ).attr( 'role', 'button' ); 2049 } 2050 2051 $( document ).on( 'ajaxComplete', function() { 2052 aria_button_if_js(); 2053 }); 2054 2055 /** 2056 * Get the viewport width. 2057 * 2058 * @since 4.7.0 2059 * 2060 * @return {number|boolean} The current viewport width or false if the 2061 * browser doesn't support innerWidth (IE < 9). 2062 */ 2063 function getViewportWidth() { 2064 var viewportWidth = false; 2065 2066 if ( window.innerWidth ) { 2067 // On phones, window.innerWidth is affected by zooming. 2068 viewportWidth = Math.max( window.innerWidth, document.documentElement.clientWidth ); 2069 } 2070 2071 return viewportWidth; 2072 } 2073 2074 /** 2075 * Sets the admin menu collapsed/expanded state. 2076 * 2077 * Sets the global variable `menuState` and triggers a custom event passing 2078 * the current menu state. 2079 * 2080 * @since 4.7.0 2081 * 2082 * @return {void} 2083 */ 2084 function setMenuState() { 2085 var viewportWidth = getViewportWidth() || 961; 2086 2087 if ( viewportWidth <= 782 ) { 2088 menuState = 'responsive'; 2089 } else if ( $body.hasClass( 'folded' ) || ( $body.hasClass( 'auto-fold' ) && viewportWidth <= 960 && viewportWidth > 782 ) ) { 2090 menuState = 'folded'; 2091 } else { 2092 menuState = 'open'; 2093 } 2094 2095 $document.trigger( 'wp-menu-state-set', { state: menuState } ); 2096 } 2097 2098 // Set the menu state when the window gets resized. 2099 $document.on( 'wp-window-resized.set-menu-state', setMenuState ); 2100 2101 /** 2102 * Sets ARIA attributes on the collapse/expand menu button. 2103 * 2104 * When the admin menu is open or folded, updates the `aria-expanded` and 2105 * `aria-label` attributes of the button to give feedback to assistive 2106 * technologies. In the responsive view, the button is always hidden. 2107 * 2108 * @since 4.7.0 2109 * 2110 * @return {void} 2111 */ 2112 $document.on( 'wp-menu-state-set wp-collapse-menu', function( event, eventData ) { 2113 var $collapseButton = $( '#collapse-button' ), 2114 ariaExpanded, ariaLabelText; 2115 2116 if ( 'folded' === eventData.state ) { 2117 ariaExpanded = 'false'; 2118 ariaLabelText = __( 'Expand Main menu' ); 2119 } else { 2120 ariaExpanded = 'true'; 2121 ariaLabelText = __( 'Collapse Main menu' ); 2122 } 2123 2124 $collapseButton.attr({ 2125 'aria-expanded': ariaExpanded, 2126 'aria-label': ariaLabelText 2127 }); 2128 }); 2129 2130 window.wpResponsive.init(); 2131 setPinMenu(); 2132 setMenuState(); 2133 makeNoticesDismissible(); 2134 aria_button_if_js(); 2135 2136 $document.on( 'wp-pin-menu wp-window-resized.pin-menu postboxes-columnchange.pin-menu postbox-toggled.pin-menu wp-collapse-menu.pin-menu wp-scroll-start.pin-menu', setPinMenu ); 2137 2138 // Set initial focus on a specific element. 2139 $( '.wp-initial-focus' ).trigger( 'focus' ); 2140 2141 // Toggle update details on update-core.php. 2142 $body.on( 'click', '.js-update-details-toggle', function() { 2143 var $updateNotice = $( this ).closest( '.js-update-details' ), 2144 $progressDiv = $( '#' + $updateNotice.data( 'update-details' ) ); 2145 2146 /* 2147 * When clicking on "Show details" move the progress div below the update 2148 * notice. Make sure it gets moved just the first time. 2149 */ 2150 if ( ! $progressDiv.hasClass( 'update-details-moved' ) ) { 2151 $progressDiv.insertAfter( $updateNotice ).addClass( 'update-details-moved' ); 2152 } 2153 2154 // Toggle the progress div visibility. 2155 $progressDiv.toggle(); 2156 // Toggle the Show Details button expanded state. 2157 $( this ).attr( 'aria-expanded', $progressDiv.is( ':visible' ) ); 2158 }); 2159 }); 2160 2161 /** 2162 * Hides the update button for expired plugin or theme uploads. 2163 * 2164 * On the "Update plugin/theme from uploaded zip" screen, once the upload has expired, 2165 * hides the "Replace current with uploaded" button and displays a warning. 2166 * 2167 * @since 5.5.0 2168 */ 2169 $( function( $ ) { 2170 var $overwrite, $warning; 2171 2172 if ( ! $body.hasClass( 'update-php' ) ) { 2173 return; 2174 } 2175 2176 $overwrite = $( 'a.update-from-upload-overwrite' ); 2177 $warning = $( '.update-from-upload-expired' ); 2178 2179 if ( ! $overwrite.length || ! $warning.length ) { 2180 return; 2181 } 2182 2183 window.setTimeout( 2184 function() { 2185 $overwrite.hide(); 2186 $warning.removeClass( 'hidden' ); 2187 2188 if ( window.wp && window.wp.a11y ) { 2189 window.wp.a11y.speak( $warning.text() ); 2190 } 2191 }, 2192 7140000 // 119 minutes. The uploaded file is deleted after 2 hours. 2193 ); 2194 } ); 2195 2196 // Fire a custom jQuery event at the end of window resize. 2197 ( function() { 2198 var timeout; 2199 2200 /** 2201 * Triggers the WP window-resize event. 2202 * 2203 * @since 3.8.0 2204 * 2205 * @return {void} 2206 */ 2207 function triggerEvent() { 2208 $document.trigger( 'wp-window-resized' ); 2209 } 2210 2211 /** 2212 * Fires the trigger event again after 200 ms. 2213 * 2214 * @since 3.8.0 2215 * 2216 * @return {void} 2217 */ 2218 function fireOnce() { 2219 window.clearTimeout( timeout ); 2220 timeout = window.setTimeout( triggerEvent, 200 ); 2221 } 2222 2223 $window.on( 'resize.wp-fire-once', fireOnce ); 2224 }()); 2225 2226 // Make Windows 8 devices play along nicely. 2227 (function(){ 2228 if ( '-ms-user-select' in document.documentElement.style && navigator.userAgent.match(/IEMobile\/10\.0/) ) { 2229 var msViewportStyle = document.createElement( 'style' ); 2230 msViewportStyle.appendChild( 2231 document.createTextNode( '@-ms-viewport{width:auto!important}' ) 2232 ); 2233 document.getElementsByTagName( 'head' )[0].appendChild( msViewportStyle ); 2234 } 2235 })(); 2236 2237 }( jQuery, window )); 2238 2239 /** 2240 * Freeze animated plugin icons when reduced motion is enabled. 2241 * 2242 * When the user has enabled the 'prefers-reduced-motion' setting, this module 2243 * stops animations for all GIFs on the page with the class 'plugin-icon' or 2244 * plugin icon images in the update plugins table. 2245 * 2246 * @since 6.4.0 2247 * 2248 * @return {Object} Public methods. 2249 */ 2250 (function() { 2251 // Private variables and methods. 2252 var priv = {}, 2253 pub = {}, 2254 mediaQuery; 2255 2256 // Initialize pauseAll to false; it will be set to true if reduced motion is preferred. 2257 priv.pauseAll = false; 2258 if ( window.matchMedia ) { 2259 mediaQuery = window.matchMedia( '(prefers-reduced-motion: reduce)' ); 2260 if ( ! mediaQuery || mediaQuery.matches ) { 2261 priv.pauseAll = true; 2262 } 2263 } 2264 2265 // Method to replace animated GIFs with a static frame. 2266 priv.freezeAnimatedPluginIcons = function( img ) { 2267 var coverImage = function() { 2268 var width = img.width; 2269 var height = img.height; 2270 var canvas = document.createElement( 'canvas' ); 2271 2272 // Set canvas dimensions. 2273 canvas.width = width; 2274 canvas.height = height; 2275 2276 // Copy classes from the image to the canvas. 2277 canvas.className = img.className; 2278 2279 // Check if the image is inside a specific table. 2280 var isInsideUpdateTable = img.closest( '#update-plugins-table' ); 2281 2282 if ( isInsideUpdateTable ) { 2283 // Transfer computed styles from image to canvas. 2284 var computedStyles = window.getComputedStyle( img ), 2285 i, max; 2286 for ( i = 0, max = computedStyles.length; i < max; i++ ) { 2287 var propName = computedStyles[ i ]; 2288 var propValue = computedStyles.getPropertyValue( propName ); 2289 canvas.style[ propName ] = propValue; 2290 } 2291 } 2292 2293 // Draw the image onto the canvas. 2294 canvas.getContext( '2d' ).drawImage( img, 0, 0, width, height ); 2295 2296 // Set accessibility attributes on canvas. 2297 canvas.setAttribute( 'aria-hidden', 'true' ); 2298 canvas.setAttribute( 'role', 'presentation' ); 2299 2300 // Insert canvas before the image and set the image to be near-invisible. 2301 var parent = img.parentNode; 2302 parent.insertBefore( canvas, img ); 2303 img.style.opacity = 0.01; 2304 img.style.width = '0px'; 2305 img.style.height = '0px'; 2306 }; 2307 2308 // If the image is already loaded, apply the coverImage function. 2309 if ( img.complete ) { 2310 coverImage(); 2311 } else { 2312 // Otherwise, wait for the image to load. 2313 img.addEventListener( 'load', coverImage, true ); 2314 } 2315 }; 2316 2317 // Public method to freeze all relevant GIFs on the page. 2318 pub.freezeAll = function() { 2319 var images = document.querySelectorAll( '.plugin-icon, #update-plugins-table img' ); 2320 for ( var x = 0; x < images.length; x++ ) { 2321 if ( /\.gif(?:\?|$)/i.test( images[ x ].src ) ) { 2322 priv.freezeAnimatedPluginIcons( images[ x ] ); 2323 } 2324 } 2325 }; 2326 2327 // Only run the freezeAll method if the user prefers reduced motion. 2328 if ( true === priv.pauseAll ) { 2329 pub.freezeAll(); 2330 } 2331 2332 // Listen for jQuery AJAX events. 2333 ( function( $ ) { 2334 if ( window.pagenow === 'plugin-install' ) { 2335 // Only listen for ajaxComplete if this is the plugin-install.php page. 2336 $( document ).ajaxComplete( function( event, xhr, settings ) { 2337 2338 // Check if this is the 'search-install-plugins' request. 2339 if ( settings.data && typeof settings.data === 'string' && settings.data.includes( 'action=search-install-plugins' ) ) { 2340 // Recheck if the user prefers reduced motion. 2341 if ( window.matchMedia ) { 2342 var mediaQuery = window.matchMedia( '(prefers-reduced-motion: reduce)' ); 2343 if ( mediaQuery.matches ) { 2344 pub.freezeAll(); 2345 } 2346 } else { 2347 // Fallback for browsers that don't support matchMedia. 2348 if ( true === priv.pauseAll ) { 2349 pub.freezeAll(); 2350 } 2351 } 2352 } 2353 } ); 2354 } 2355 } )( jQuery ); 2356 2357 // Expose public methods. 2358 return pub; 2359 })(); 2360 2361 /** 2362 * Validate the delete-and-reassign users form and surface an accessible 2363 * error summary instead of disabling the submit button. 2364 * 2365 * Disabled buttons can't be discovered by assistive technology, so rather 2366 * than blocking submission we let the form submit, intercept it when content 2367 * decisions are still missing, and present a focusable error summary that 2368 * lists how many decisions remain and links straight to each one. 2369 * 2370 * Shared by both the single-site (wp-admin/users.php) and multisite/network 2371 * (confirm_delete_users() in wp-admin/includes/ms.php) deletion forms. The two 2372 * differ in markup: single site has one content decision per user, multisite 2373 * has one decision per site a user belongs to (several radio groups per 2374 * fieldset), and their reassign dropdowns use different "no selection" values. 2375 * The logic below works per radio group so it covers both. 2376 * 2377 * @since 7.1.0 2378 */ 2379 (function(){ 2380 const { _n, sprintf } = wp.i18n; 2381 const usersForm = document.querySelector( '.delete-and-reassign-users-form' ); 2382 2383 // Check if the form exists and contains any radio buttons. 2384 if ( ! usersForm || ! usersForm.querySelector( 'input[type="radio"]' ) ) { 2385 return; 2386 } 2387 2388 const summaryId = 'delete-users-error-summary'; 2389 2390 /** 2391 * Whether a reassign dropdown has no user selected. 2392 * 2393 * The "Select a user" placeholder value differs between the forms: the 2394 * single-site dropdown uses an empty string, the multisite one uses the 2395 * wp_dropdown_users() default of '-1'. 2396 * 2397 * @param {HTMLSelectElement} select The reassign dropdown. 2398 * @return {boolean} True when no real user is selected. 2399 */ 2400 function hasNoSelectedUser( select ) { 2401 return '' === select.value || '-1' === select.value; 2402 } 2403 2404 /** 2405 * Builds a human-readable label for a radio group's decision. 2406 * 2407 * Combines the fieldset legend (the user) with the site context that 2408 * precedes the group on multisite, so each summary entry is identifiable. 2409 * 2410 * @param {HTMLElement} group The radio group (<ul>) element. 2411 * @return {string} The composed label. 2412 */ 2413 function getDecisionLabel( group ) { 2414 const fieldset = group.closest( 'fieldset' ); 2415 const legend = fieldset ? fieldset.querySelector( 'legend' ) : null; 2416 const parts = []; 2417 2418 if ( legend ) { 2419 parts.push( legend.textContent.trim() ); 2420 } 2421 2422 // On multisite each radio group is preceded by a "Site: …" paragraph. 2423 const previous = group.previousElementSibling; 2424 if ( previous && previous !== legend && previous.textContent.trim() ) { 2425 parts.push( previous.textContent.trim() ); 2426 } 2427 2428 return parts.join( ' – ' ); 2429 } 2430 2431 // Keep the radio selection in sync with the reassign dropdown. 2432 usersForm.querySelectorAll( 'select' ).forEach( function( selectElement ) { 2433 selectElement.addEventListener( 'change', function( e ) { 2434 const item = e.target.closest( 'li' ); 2435 const radio = item ? item.querySelector( 'input[type="radio"]' ) : null; 2436 if ( radio ) { 2437 radio.checked = ! hasNoSelectedUser( e.target ); 2438 } 2439 }); 2440 }); 2441 2442 /** 2443 * Returns the radio groups whose content decision is still incomplete. 2444 * 2445 * A decision unit is a single radio group (<ul>), which maps to one user on 2446 * single site and one site-per-user on multisite. 2447 * 2448 * @return {Array} Objects describing each incomplete decision. 2449 */ 2450 function getIncompleteDecisions() { 2451 const incomplete = []; 2452 2453 usersForm.querySelectorAll( 'fieldset ul' ).forEach( function( group ) { 2454 const radios = group.querySelectorAll( 'input[type="radio"]' ); 2455 if ( ! radios.length ) { 2456 return; 2457 } 2458 2459 const checked = group.querySelector( 'input[type="radio"]:checked' ); 2460 2461 // No option chosen yet. 2462 if ( ! checked ) { 2463 incomplete.push( { target: radios[ 0 ], label: getDecisionLabel( group ) } ); 2464 return; 2465 } 2466 2467 // "Attribute to another user" chosen, but no user selected. 2468 if ( 'reassign' === checked.value ) { 2469 const select = group.querySelector( 'select' ); 2470 if ( select && hasNoSelectedUser( select ) ) { 2471 incomplete.push( { target: select, label: getDecisionLabel( group ) } ); 2472 } 2473 } 2474 }); 2475 2476 return incomplete; 2477 } 2478 2479 /** 2480 * Builds or refreshes the error summary markup. 2481 * 2482 * @param {Array} incomplete Incomplete decisions from getIncompleteDecisions(). 2483 * @return {string} The summary title, for announcing to assistive technology. 2484 */ 2485 function renderErrorSummary( incomplete ) { 2486 let summary = document.getElementById( summaryId ); 2487 2488 if ( ! summary ) { 2489 summary = document.createElement( 'div' ); 2490 summary.id = summaryId; 2491 summary.className = 'notice notice-error'; 2492 summary.setAttribute( 'tabindex', '-1' ); 2493 2494 // The wrapper contains the form on single site and wraps it on 2495 // multisite; insert the summary right after the page heading. 2496 const wrap = usersForm.querySelector( '.wrap' ) || usersForm.closest( '.wrap' ) || usersForm; 2497 const heading = wrap.querySelector( 'h1' ); 2498 wrap.insertBefore( summary, heading ? heading.nextSibling : wrap.firstChild ); 2499 } 2500 2501 const count = incomplete.length; 2502 const title = sprintf( 2503 /* translators: %s: Number of content decisions still required. */ 2504 _n( 2505 '%s content decision is still required before you can delete.', 2506 '%s content decisions are still required before you can delete.', 2507 count 2508 ), 2509 count 2510 ); 2511 2512 // Clear any previous markup and invalid states before rebuilding, 2513 // so decisions resolved since the last render are no longer flagged. 2514 summary.textContent = ''; 2515 usersForm.querySelectorAll( '[aria-invalid]' ).forEach( function( el ) { 2516 el.removeAttribute( 'aria-invalid' ); 2517 }); 2518 2519 const titleEl = document.createElement( 'p' ); 2520 const strong = document.createElement( 'strong' ); 2521 strong.textContent = title; 2522 titleEl.appendChild( strong ); 2523 summary.appendChild( titleEl ); 2524 2525 const list = document.createElement( 'ul' ); 2526 incomplete.forEach( function( item ) { 2527 const li = document.createElement( 'li' ); 2528 const link = document.createElement( 'a' ); 2529 link.href = '#' + item.target.id; 2530 link.textContent = item.label; 2531 link.addEventListener( 'click', function( e ) { 2532 e.preventDefault(); 2533 item.target.focus(); 2534 }); 2535 li.appendChild( link ); 2536 list.appendChild( li ); 2537 2538 item.target.setAttribute( 'aria-invalid', 'true' ); 2539 }); 2540 summary.appendChild( list ); 2541 2542 return title; 2543 } 2544 2545 /** 2546 * Removes the error summary and clears invalid states. 2547 */ 2548 function clearErrorState() { 2549 const summary = document.getElementById( summaryId ); 2550 if ( summary ) { 2551 summary.remove(); 2552 } 2553 usersForm.querySelectorAll( '[aria-invalid]' ).forEach( function( el ) { 2554 el.removeAttribute( 'aria-invalid' ); 2555 }); 2556 } 2557 2558 /** 2559 * Refreshes the error summary to match the current form state. 2560 * 2561 * @param {boolean} moveFocus Whether to move focus to the summary and 2562 * announce it (used on a failed submit). 2563 * @return {number} The number of incomplete decisions. 2564 */ 2565 function updateSummary( moveFocus ) { 2566 const incomplete = getIncompleteDecisions(); 2567 2568 if ( ! incomplete.length ) { 2569 clearErrorState(); 2570 return 0; 2571 } 2572 2573 const title = renderErrorSummary( incomplete ); 2574 2575 if ( moveFocus ) { 2576 document.getElementById( summaryId ).focus(); 2577 if ( window.wp && window.wp.a11y ) { 2578 window.wp.a11y.speak( title, 'assertive' ); 2579 } 2580 } 2581 2582 return incomplete.length; 2583 } 2584 2585 usersForm.addEventListener( 'submit', function( e ) { 2586 if ( updateSummary( true ) > 0 ) { 2587 e.preventDefault(); 2588 } 2589 }); 2590 2591 // Keep an existing summary current as decisions are resolved, without 2592 // stealing focus on every interaction. 2593 usersForm.addEventListener( 'change', function() { 2594 if ( document.getElementById( summaryId ) ) { 2595 updateSummary( false ); 2596 } 2597 }); 2598 })();
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated : Fri Sep 4 08:20:24 2026 | Cross-referenced by PHPXref |