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