| [ Index ] |
PHP Cross Reference of WordPress Trunk (Updated Daily) |
[Summary view] [Print] [Text view]
1 /** 2 * @output wp-admin/js/site-health.js 3 */ 4 5 /* global ajaxurl, ClipboardJS, SiteHealth, wp */ 6 7 /** 8 * Handles the interactions used by the Site Health modules in WordPress. 9 * 10 * @param {JQueryStatic} $ The jQuery object. 11 */ 12 jQuery( function( $ ) { 13 14 var __ = wp.i18n.__, 15 _n = wp.i18n._n, 16 sprintf = wp.i18n.sprintf, 17 clipboard = new ClipboardJS( '.site-health-copy-buttons .copy-button' ), 18 isStatusTab = $( '.health-check-body.health-check-status-tab' ).length, 19 isDebugTab = $( '.health-check-body.health-check-debug-tab' ).length, 20 pathsSizesSection = $( '#health-check-accordion-block-wp-paths-sizes' ), 21 menuCounterWrapper = $( '#adminmenu .site-health-counter' ), 22 menuCounter = $( '#adminmenu .site-health-counter .count' ), 23 successTimeout; 24 25 // Debug information copy section. 26 clipboard.on( 'success', function( e ) { 27 var triggerElement = $( e.trigger ), 28 successElement = $( '.success', triggerElement.closest( 'div' ) ); 29 30 // Clear the selection and move focus back to the trigger. 31 e.clearSelection(); 32 33 // Show success visual feedback. 34 clearTimeout( successTimeout ); 35 successElement.removeClass( 'hidden' ); 36 37 // Hide success visual feedback after 3 seconds since last success. 38 successTimeout = setTimeout( function() { 39 successElement.addClass( 'hidden' ); 40 }, 3000 ); 41 42 // Handle success audible feedback. 43 wp.a11y.speak( __( 'Site information has been copied to your clipboard.' ) ); 44 } ); 45 46 // Accordion handling in various areas. 47 $( '.health-check-accordion' ).on( 'click', '.health-check-accordion-trigger', function() { 48 var isExpanded = ( 'true' === $( this ).attr( 'aria-expanded' ) ); 49 50 if ( $( this ).prop( 'id' ) ) { 51 window.location.hash = $( this ).prop( 'id' ); 52 } 53 54 if ( isExpanded ) { 55 $( this ).attr( 'aria-expanded', 'false' ); 56 $( '#' + $( this ).attr( 'aria-controls' ) ).attr( 'hidden', true ); 57 } else { 58 $( this ).attr( 'aria-expanded', 'true' ); 59 $( '#' + $( this ).attr( 'aria-controls' ) ).attr( 'hidden', false ); 60 } 61 } ); 62 63 /* global setTimeout */ 64 wp.domReady( function() { 65 // Get hash from query string and open the related accordion. 66 var hash = window.location.hash; 67 68 if ( hash ) { 69 var requestedPanel = $( hash ); 70 71 if ( requestedPanel.is( '.health-check-accordion-trigger' ) ) { 72 requestedPanel.trigger( 'click' ); 73 } 74 } 75 } ); 76 77 // Site Health test handling. 78 79 $( '.site-health-view-passed' ).on( 'click', function() { 80 var goodIssuesWrapper = $( '#health-check-issues-good' ); 81 82 goodIssuesWrapper.toggleClass( 'hidden' ); 83 $( this ).attr( 'aria-expanded', ! goodIssuesWrapper.hasClass( 'hidden' ) ); 84 } ); 85 86 /** 87 * Validates the Site Health test result format. 88 * 89 * @since 5.6.0 90 * 91 * @param {Object} issue The issue data to validate. 92 * 93 * @return {boolean} True if the issue data is valid, false otherwise. 94 */ 95 function validateIssueData( issue ) { 96 // Expected minimum format of a valid SiteHealth test response. 97 var minimumExpected = { 98 test: 'string', 99 label: 'string', 100 description: 'string' 101 }, 102 passed = true, 103 key, value, subKey, subValue; 104 105 // If the issue passed is not an object, return a `false` state early. 106 if ( 'object' !== typeof( issue ) ) { 107 return false; 108 } 109 110 // Loop over expected data and match the data types. 111 for ( key in minimumExpected ) { 112 value = minimumExpected[ key ]; 113 114 if ( 'object' === typeof( value ) ) { 115 for ( subKey in value ) { 116 subValue = value[ subKey ]; 117 118 if ( 'undefined' === typeof( issue[ key ] ) || 119 'undefined' === typeof( issue[ key ][ subKey ] ) || 120 subValue !== typeof( issue[ key ][ subKey ] ) 121 ) { 122 passed = false; 123 } 124 } 125 } else { 126 if ( 'undefined' === typeof( issue[ key ] ) || 127 value !== typeof( issue[ key ] ) 128 ) { 129 passed = false; 130 } 131 } 132 } 133 134 return passed; 135 } 136 137 /** 138 * Appends a new issue to the issue list. 139 * 140 * @since 5.2.0 141 * 142 * @param {Object} issue The issue data. 143 * @return {void|boolean} True if the issue was appended, false otherwise. 144 */ 145 function appendIssue( issue ) { 146 var template = wp.template( 'health-check-issue' ), 147 issueWrapper = $( '#health-check-issues-' + issue.status ), 148 heading, 149 count; 150 151 /* 152 * Validate the issue data format before using it. 153 * If the output is invalid, discard it. 154 */ 155 if ( ! validateIssueData( issue ) ) { 156 return false; 157 } 158 159 SiteHealth.site_status.issues[ issue.status ]++; 160 161 count = SiteHealth.site_status.issues[ issue.status ]; 162 163 // If no test name is supplied, append a placeholder for markup references. 164 if ( typeof issue.test === 'undefined' ) { 165 issue.test = issue.status + count; 166 } 167 168 if ( 'critical' === issue.status ) { 169 heading = sprintf( 170 _n( '%s critical issue', '%s critical issues', count ), 171 '<span class="issue-count">' + count + '</span>' 172 ); 173 } else if ( 'recommended' === issue.status ) { 174 heading = sprintf( 175 _n( '%s recommended improvement', '%s recommended improvements', count ), 176 '<span class="issue-count">' + count + '</span>' 177 ); 178 } else if ( 'good' === issue.status ) { 179 heading = sprintf( 180 _n( '%s item with no issues detected', '%s items with no issues detected', count ), 181 '<span class="issue-count">' + count + '</span>' 182 ); 183 } 184 185 if ( heading ) { 186 $( '.site-health-issue-count-title', issueWrapper ).html( heading ); 187 } 188 189 menuCounter.text( SiteHealth.site_status.issues.critical ); 190 191 if ( 0 < parseInt( SiteHealth.site_status.issues.critical, 0 ) ) { 192 $( '#health-check-issues-critical' ).removeClass( 'hidden' ); 193 194 menuCounterWrapper.removeClass( 'count-0' ); 195 } else { 196 menuCounterWrapper.addClass( 'count-0' ); 197 } 198 if ( 0 < parseInt( SiteHealth.site_status.issues.recommended, 0 ) ) { 199 $( '#health-check-issues-recommended' ).removeClass( 'hidden' ); 200 } 201 202 $( '.issues', '#health-check-issues-' + issue.status ).append( template( issue ) ); 203 } 204 205 /** 206 * Updates site health status indicator as asynchronous tests are run and returned. 207 * 208 * @since 5.2.0 209 */ 210 function recalculateProgression() { 211 var r, c, pct; 212 var $progress = $( '.site-health-progress' ); 213 var $wrapper = $progress.closest( '.site-health-progress-wrapper' ); 214 var $progressLabel = $( '.site-health-progress-label', $wrapper ); 215 var $circle = $( '.site-health-progress svg #bar' ); 216 var totalTests = parseInt( SiteHealth.site_status.issues.good, 0 ) + 217 parseInt( SiteHealth.site_status.issues.recommended, 0 ) + 218 ( parseInt( SiteHealth.site_status.issues.critical, 0 ) * 1.5 ); 219 var failedTests = ( parseInt( SiteHealth.site_status.issues.recommended, 0 ) * 0.5 ) + 220 ( parseInt( SiteHealth.site_status.issues.critical, 0 ) * 1.5 ); 221 var val = 100 - Math.ceil( ( failedTests / totalTests ) * 100 ); 222 223 if ( 0 === totalTests ) { 224 $progress.addClass( 'hidden' ); 225 return; 226 } 227 228 $wrapper.removeClass( 'loading' ); 229 230 r = $circle.attr( 'r' ); 231 c = Math.PI * ( r * 2 ); 232 233 if ( 0 > val ) { 234 val = 0; 235 } 236 if ( 100 < val ) { 237 val = 100; 238 } 239 240 pct = ( ( 100 - val ) / 100 ) * c + 'px'; 241 242 $circle.css( { strokeDashoffset: pct } ); 243 244 if ( 80 <= val && 0 === parseInt( SiteHealth.site_status.issues.critical, 0 ) ) { 245 $wrapper.addClass( 'green' ).removeClass( 'orange' ); 246 247 $progressLabel.text( __( 'Good' ) ); 248 announceTestsProgression( 'good' ); 249 } else { 250 $wrapper.addClass( 'orange' ).removeClass( 'green' ); 251 252 $progressLabel.text( __( 'Should be improved' ) ); 253 announceTestsProgression( 'improvable' ); 254 } 255 256 if ( isStatusTab ) { 257 $.post( 258 ajaxurl, 259 { 260 'action': 'health-check-site-status-result', 261 '_wpnonce': SiteHealth.nonce.site_status_result, 262 'counts': SiteHealth.site_status.issues 263 } 264 ); 265 266 if ( 100 === val ) { 267 $( '.site-status-all-clear' ).removeClass( 'hide' ); 268 $( '.site-status-has-issues' ).addClass( 'hide' ); 269 } 270 } 271 } 272 273 /** 274 * Queues the next asynchronous test when we're ready to run it. 275 * 276 * @since 5.2.0 277 */ 278 function maybeRunNextAsyncTest() { 279 var doCalculation = true; 280 281 if ( 1 <= SiteHealth.site_status.async.length ) { 282 $.each( SiteHealth.site_status.async, function() { 283 var data = { 284 'action': 'health-check-' + this.test.replace( '_', '-' ), 285 '_wpnonce': SiteHealth.nonce.site_status 286 }; 287 288 if ( this.completed ) { 289 return true; 290 } 291 292 doCalculation = false; 293 294 this.completed = true; 295 296 if ( 'undefined' !== typeof( this.has_rest ) && this.has_rest ) { 297 wp.apiRequest( { 298 url: wp.url.addQueryArgs( this.test, { _locale: 'user' } ), 299 headers: this.headers 300 } ) 301 .done( function( response ) { 302 /** This filter is documented in wp-admin/includes/class-wp-site-health.php */ 303 appendIssue( wp.hooks.applyFilters( 'site_status_test_result', response ) ); 304 } ) 305 .fail( function( response ) { 306 var description; 307 308 if ( 'undefined' !== typeof( response.responseJSON ) && 'undefined' !== typeof( response.responseJSON.message ) ) { 309 description = response.responseJSON.message; 310 } else { 311 description = __( 'No details available' ); 312 } 313 314 addFailedSiteHealthCheckNotice( this.url, description ); 315 } ) 316 .always( function() { 317 maybeRunNextAsyncTest(); 318 } ); 319 } else { 320 $.post( 321 ajaxurl, 322 data 323 ).done( function( response ) { 324 /** This filter is documented in wp-admin/includes/class-wp-site-health.php */ 325 appendIssue( wp.hooks.applyFilters( 'site_status_test_result', response.data ) ); 326 } ).fail( function( response ) { 327 var description; 328 329 if ( 'undefined' !== typeof( response.responseJSON ) && 'undefined' !== typeof( response.responseJSON.message ) ) { 330 description = response.responseJSON.message; 331 } else { 332 description = __( 'No details available' ); 333 } 334 335 addFailedSiteHealthCheckNotice( this.url, description ); 336 } ).always( function() { 337 maybeRunNextAsyncTest(); 338 } ); 339 } 340 341 return false; 342 } ); 343 } 344 345 if ( doCalculation ) { 346 recalculateProgression(); 347 } 348 } 349 350 /** 351 * Add the details of a failed asynchronous test to the list of test results. 352 * 353 * @param {string} url The URL of the failed test. 354 * @param {string} description The description of the failed test. 355 * @since 5.6.0 356 */ 357 function addFailedSiteHealthCheckNotice( url, description ) { 358 var issue; 359 360 issue = { 361 'status': 'recommended', 362 'label': __( 'A test is unavailable' ), 363 'badge': { 364 'color': 'red', 365 'label': __( 'Unavailable' ) 366 }, 367 'description': '<p>' + url + '</p><p>' + description + '</p>', 368 'actions': '' 369 }; 370 371 /** This filter is documented in wp-admin/includes/class-wp-site-health.php */ 372 appendIssue( wp.hooks.applyFilters( 'site_status_test_result', issue ) ); 373 } 374 375 if ( 'undefined' !== typeof SiteHealth ) { 376 if ( 0 === SiteHealth.site_status.direct.length && 0 === SiteHealth.site_status.async.length ) { 377 recalculateProgression(); 378 } else { 379 SiteHealth.site_status.issues = { 380 'good': 0, 381 'recommended': 0, 382 'critical': 0 383 }; 384 } 385 386 if ( 0 < SiteHealth.site_status.direct.length ) { 387 $.each( SiteHealth.site_status.direct, function() { 388 appendIssue( this ); 389 } ); 390 } 391 392 if ( 0 < SiteHealth.site_status.async.length ) { 393 maybeRunNextAsyncTest(); 394 } else { 395 recalculateProgression(); 396 } 397 } 398 399 /** 400 * Get the sizes of the directories in the Site Health Info section. 401 */ 402 function getDirectorySizes() { 403 var timestamp = ( new Date().getTime() ); 404 405 // After 3 seconds announce that we're still waiting for directory sizes. 406 var timeout = window.setTimeout( function() { 407 announceTestsProgression( 'waiting-for-directory-sizes' ); 408 }, 3000 ); 409 410 wp.apiRequest( { 411 path: '/wp-site-health/v1/directory-sizes' 412 } ).done( function( response ) { 413 updateDirSizes( response || {} ); 414 } ).always( function() { 415 var delay = ( new Date().getTime() ) - timestamp; 416 417 $( '.health-check-wp-paths-sizes.spinner' ).css( 'visibility', 'hidden' ); 418 419 if ( delay > 3000 ) { 420 /* 421 * We have announced that we're waiting. 422 * Announce that we're ready after giving at least 3 seconds 423 * for the first announcement to be read out, or the two may collide. 424 */ 425 if ( delay > 6000 ) { 426 delay = 0; 427 } else { 428 delay = 6500 - delay; 429 } 430 431 window.setTimeout( function() { 432 recalculateProgression(); 433 }, delay ); 434 } else { 435 // Cancel the announcement. 436 window.clearTimeout( timeout ); 437 } 438 439 $( document ).trigger( 'site-health-info-dirsizes-done' ); 440 } ); 441 } 442 443 /** 444 * Updates the directory sizes in the Site Health Info section. 445 * 446 * @param {Object} data The directory sizes data. 447 */ 448 function updateDirSizes( data ) { 449 var copyButton = $( 'button.button.copy-button' ); 450 var clipboardText = copyButton.attr( 'data-clipboard-text' ); 451 452 $.each( data, function( name, value ) { 453 var text = value.debug || value.size; 454 455 if ( typeof text !== 'undefined' ) { 456 clipboardText = clipboardText.replace( name + ': loading...', name + ': ' + text ); 457 } 458 } ); 459 460 copyButton.attr( 'data-clipboard-text', clipboardText ); 461 462 pathsSizesSection.find( 'td[class]' ).each( function( i, element ) { 463 var td = $( element ); 464 var name = td.attr( 'class' ); 465 466 if ( data.hasOwnProperty( name ) && data[ name ].size ) { 467 td.text( data[ name ].size ); 468 } 469 } ); 470 } 471 472 if ( isDebugTab ) { 473 if ( pathsSizesSection.length ) { 474 getDirectorySizes(); 475 } else { 476 recalculateProgression(); 477 } 478 } 479 480 // Trigger a class toggle when the extended menu button is clicked. 481 $( '.health-check-offscreen-nav-wrapper' ).on( 'click', function() { 482 $( this ).toggleClass( 'visible' ); 483 } ); 484 485 /** 486 * Announces to assistive technologies the tests progression status. 487 * 488 * @since 6.4.0 489 * 490 * @param {string} type The type of message to be announced. 491 * 492 * @return {void} 493 */ 494 function announceTestsProgression( type ) { 495 // Only announce the messages in the Site Health pages. 496 if ( 'site-health' !== SiteHealth.screen ) { 497 return; 498 } 499 500 switch ( type ) { 501 case 'good': 502 wp.a11y.speak( __( 'All site health tests have finished running. Your site is looking good.' ) ); 503 break; 504 case 'improvable': 505 wp.a11y.speak( __( 'All site health tests have finished running. There are items that should be addressed.' ) ); 506 break; 507 case 'waiting-for-directory-sizes': 508 wp.a11y.speak( __( 'Running additional tests... please wait.' ) ); 509 break; 510 default: 511 return; 512 } 513 } 514 } );
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated : Sun Sep 13 08:20:28 2026 | Cross-referenced by PHPXref |