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