[ Index ] |
PHP Cross Reference of WordPress Trunk (Updated Daily) |
[Summary view] [Print] [Text view]
1 <?php 2 /** 3 * Script Modules API: WP_Script_Modules class. 4 * 5 * Native support for ES Modules and Import Maps. 6 * 7 * @package WordPress 8 * @subpackage Script Modules 9 */ 10 11 /** 12 * Core class used to register script modules. 13 * 14 * @since 6.5.0 15 */ 16 class WP_Script_Modules { 17 /** 18 * Holds the registered script modules, keyed by script module identifier. 19 * 20 * @since 6.5.0 21 * @var array[] 22 */ 23 private $registered = array(); 24 25 /** 26 * Holds the script module identifiers that were enqueued before registered. 27 * 28 * @since 6.5.0 29 * @var array<string, true> 30 */ 31 private $enqueued_before_registered = array(); 32 33 /** 34 * Tracks whether the @wordpress/a11y script module is available. 35 * 36 * Some additional HTML is required on the page for the module to work. Track 37 * whether it's available to print at the appropriate time. 38 * 39 * @since 6.7.0 40 * @var bool 41 */ 42 private $a11y_available = false; 43 44 /** 45 * Registers the script module if no script module with that script module 46 * identifier has already been registered. 47 * 48 * @since 6.5.0 49 * 50 * @param string $id The identifier of the script module. Should be unique. It will be used in the 51 * final import map. 52 * @param string $src Optional. Full URL of the script module, or path of the script module relative 53 * to the WordPress root directory. If it is provided and the script module has 54 * not been registered yet, it will be registered. 55 * @param array $deps { 56 * Optional. List of dependencies. 57 * 58 * @type string|array ...$0 { 59 * An array of script module identifiers of the dependencies of this script 60 * module. The dependencies can be strings or arrays. If they are arrays, 61 * they need an `id` key with the script module identifier, and can contain 62 * an `import` key with either `static` or `dynamic`. By default, 63 * dependencies that don't contain an `import` key are considered static. 64 * 65 * @type string $id The script module identifier. 66 * @type string $import Optional. Import type. May be either `static` or 67 * `dynamic`. Defaults to `static`. 68 * } 69 * } 70 * @param string|false|null $version Optional. String specifying the script module version number. Defaults to false. 71 * It is added to the URL as a query string for cache busting purposes. If $version 72 * is set to false, the version number is the currently installed WordPress version. 73 * If $version is set to null, no version is added. 74 */ 75 public function register( string $id, string $src, array $deps = array(), $version = false ) { 76 if ( ! isset( $this->registered[ $id ] ) ) { 77 $dependencies = array(); 78 foreach ( $deps as $dependency ) { 79 if ( is_array( $dependency ) ) { 80 if ( ! isset( $dependency['id'] ) ) { 81 _doing_it_wrong( __METHOD__, __( 'Missing required id key in entry among dependencies array.' ), '6.5.0' ); 82 continue; 83 } 84 $dependencies[] = array( 85 'id' => $dependency['id'], 86 'import' => isset( $dependency['import'] ) && 'dynamic' === $dependency['import'] ? 'dynamic' : 'static', 87 ); 88 } elseif ( is_string( $dependency ) ) { 89 $dependencies[] = array( 90 'id' => $dependency, 91 'import' => 'static', 92 ); 93 } else { 94 _doing_it_wrong( __METHOD__, __( 'Entries in dependencies array must be either strings or arrays with an id key.' ), '6.5.0' ); 95 } 96 } 97 98 $this->registered[ $id ] = array( 99 'src' => $src, 100 'version' => $version, 101 'enqueue' => isset( $this->enqueued_before_registered[ $id ] ), 102 'dependencies' => $dependencies, 103 ); 104 } 105 } 106 107 /** 108 * Marks the script module to be enqueued in the page. 109 * 110 * If a src is provided and the script module has not been registered yet, it 111 * will be registered. 112 * 113 * @since 6.5.0 114 * 115 * @param string $id The identifier of the script module. Should be unique. It will be used in the 116 * final import map. 117 * @param string $src Optional. Full URL of the script module, or path of the script module relative 118 * to the WordPress root directory. If it is provided and the script module has 119 * not been registered yet, it will be registered. 120 * @param array $deps { 121 * Optional. List of dependencies. 122 * 123 * @type string|array ...$0 { 124 * An array of script module identifiers of the dependencies of this script 125 * module. The dependencies can be strings or arrays. If they are arrays, 126 * they need an `id` key with the script module identifier, and can contain 127 * an `import` key with either `static` or `dynamic`. By default, 128 * dependencies that don't contain an `import` key are considered static. 129 * 130 * @type string $id The script module identifier. 131 * @type string $import Optional. Import type. May be either `static` or 132 * `dynamic`. Defaults to `static`. 133 * } 134 * } 135 * @param string|false|null $version Optional. String specifying the script module version number. Defaults to false. 136 * It is added to the URL as a query string for cache busting purposes. If $version 137 * is set to false, the version number is the currently installed WordPress version. 138 * If $version is set to null, no version is added. 139 */ 140 public function enqueue( string $id, string $src = '', array $deps = array(), $version = false ) { 141 if ( isset( $this->registered[ $id ] ) ) { 142 $this->registered[ $id ]['enqueue'] = true; 143 } elseif ( $src ) { 144 $this->register( $id, $src, $deps, $version ); 145 $this->registered[ $id ]['enqueue'] = true; 146 } else { 147 $this->enqueued_before_registered[ $id ] = true; 148 } 149 } 150 151 /** 152 * Unmarks the script module so it will no longer be enqueued in the page. 153 * 154 * @since 6.5.0 155 * 156 * @param string $id The identifier of the script module. 157 */ 158 public function dequeue( string $id ) { 159 if ( isset( $this->registered[ $id ] ) ) { 160 $this->registered[ $id ]['enqueue'] = false; 161 } 162 unset( $this->enqueued_before_registered[ $id ] ); 163 } 164 165 /** 166 * Removes a registered script module. 167 * 168 * @since 6.5.0 169 * 170 * @param string $id The identifier of the script module. 171 */ 172 public function deregister( string $id ) { 173 unset( $this->registered[ $id ] ); 174 unset( $this->enqueued_before_registered[ $id ] ); 175 } 176 177 /** 178 * Adds the hooks to print the import map, enqueued script modules and script 179 * module preloads. 180 * 181 * In classic themes, the script modules used by the blocks are not yet known 182 * when the `wp_head` actions is fired, so it needs to print everything in the 183 * footer. 184 * 185 * @since 6.5.0 186 */ 187 public function add_hooks() { 188 $position = wp_is_block_theme() ? 'wp_head' : 'wp_footer'; 189 add_action( $position, array( $this, 'print_import_map' ) ); 190 add_action( $position, array( $this, 'print_enqueued_script_modules' ) ); 191 add_action( $position, array( $this, 'print_script_module_preloads' ) ); 192 193 add_action( 'admin_print_footer_scripts', array( $this, 'print_import_map' ) ); 194 add_action( 'admin_print_footer_scripts', array( $this, 'print_enqueued_script_modules' ) ); 195 add_action( 'admin_print_footer_scripts', array( $this, 'print_script_module_preloads' ) ); 196 197 add_action( 'wp_footer', array( $this, 'print_script_module_data' ) ); 198 add_action( 'admin_print_footer_scripts', array( $this, 'print_script_module_data' ) ); 199 add_action( 'wp_footer', array( $this, 'print_a11y_script_module_html' ), 20 ); 200 add_action( 'admin_print_footer_scripts', array( $this, 'print_a11y_script_module_html' ), 20 ); 201 } 202 203 /** 204 * Prints the enqueued script modules using script tags with type="module" 205 * attributes. 206 * 207 * @since 6.5.0 208 */ 209 public function print_enqueued_script_modules() { 210 foreach ( $this->get_marked_for_enqueue() as $id => $script_module ) { 211 wp_print_script_tag( 212 array( 213 'type' => 'module', 214 'src' => $this->get_src( $id ), 215 'id' => $id . '-js-module', 216 ) 217 ); 218 } 219 } 220 221 /** 222 * Prints the the static dependencies of the enqueued script modules using 223 * link tags with rel="modulepreload" attributes. 224 * 225 * If a script module is marked for enqueue, it will not be preloaded. 226 * 227 * @since 6.5.0 228 */ 229 public function print_script_module_preloads() { 230 foreach ( $this->get_dependencies( array_keys( $this->get_marked_for_enqueue() ), array( 'static' ) ) as $id => $script_module ) { 231 // Don't preload if it's marked for enqueue. 232 if ( true !== $script_module['enqueue'] ) { 233 echo sprintf( 234 '<link rel="modulepreload" href="%s" id="%s">', 235 esc_url( $this->get_src( $id ) ), 236 esc_attr( $id . '-js-modulepreload' ) 237 ); 238 } 239 } 240 } 241 242 /** 243 * Prints the import map using a script tag with a type="importmap" attribute. 244 * 245 * @since 6.5.0 246 */ 247 public function print_import_map() { 248 $import_map = $this->get_import_map(); 249 if ( ! empty( $import_map['imports'] ) ) { 250 wp_print_inline_script_tag( 251 wp_json_encode( $import_map, JSON_HEX_TAG | JSON_HEX_AMP ), 252 array( 253 'type' => 'importmap', 254 'id' => 'wp-importmap', 255 ) 256 ); 257 } 258 } 259 260 /** 261 * Returns the import map array. 262 * 263 * @since 6.5.0 264 * 265 * @return array Array with an `imports` key mapping to an array of script module identifiers and their respective 266 * URLs, including the version query. 267 */ 268 private function get_import_map(): array { 269 $imports = array(); 270 foreach ( $this->get_dependencies( array_keys( $this->get_marked_for_enqueue() ) ) as $id => $script_module ) { 271 $imports[ $id ] = $this->get_src( $id ); 272 } 273 return array( 'imports' => $imports ); 274 } 275 276 /** 277 * Retrieves the list of script modules marked for enqueue. 278 * 279 * @since 6.5.0 280 * 281 * @return array[] Script modules marked for enqueue, keyed by script module identifier. 282 */ 283 private function get_marked_for_enqueue(): array { 284 $enqueued = array(); 285 foreach ( $this->registered as $id => $script_module ) { 286 if ( true === $script_module['enqueue'] ) { 287 $enqueued[ $id ] = $script_module; 288 } 289 } 290 return $enqueued; 291 } 292 293 /** 294 * Retrieves all the dependencies for the given script module identifiers, 295 * filtered by import types. 296 * 297 * It will consolidate an array containing a set of unique dependencies based 298 * on the requested import types: 'static', 'dynamic', or both. This method is 299 * recursive and also retrieves dependencies of the dependencies. 300 * 301 * @since 6.5.0 302 * 303 * @param string[] $ids The identifiers of the script modules for which to gather dependencies. 304 * @param string[] $import_types Optional. Import types of dependencies to retrieve: 'static', 'dynamic', or both. 305 * Default is both. 306 * @return array[] List of dependencies, keyed by script module identifier. 307 */ 308 private function get_dependencies( array $ids, array $import_types = array( 'static', 'dynamic' ) ) { 309 return array_reduce( 310 $ids, 311 function ( $dependency_script_modules, $id ) use ( $import_types ) { 312 $dependencies = array(); 313 foreach ( $this->registered[ $id ]['dependencies'] as $dependency ) { 314 if ( 315 in_array( $dependency['import'], $import_types, true ) && 316 isset( $this->registered[ $dependency['id'] ] ) && 317 ! isset( $dependency_script_modules[ $dependency['id'] ] ) 318 ) { 319 $dependencies[ $dependency['id'] ] = $this->registered[ $dependency['id'] ]; 320 } 321 } 322 return array_merge( $dependency_script_modules, $dependencies, $this->get_dependencies( array_keys( $dependencies ), $import_types ) ); 323 }, 324 array() 325 ); 326 } 327 328 /** 329 * Gets the versioned URL for a script module src. 330 * 331 * If $version is set to false, the version number is the currently installed 332 * WordPress version. If $version is set to null, no version is added. 333 * Otherwise, the string passed in $version is used. 334 * 335 * @since 6.5.0 336 * 337 * @param string $id The script module identifier. 338 * @return string The script module src with a version if relevant. 339 */ 340 private function get_src( string $id ): string { 341 if ( ! isset( $this->registered[ $id ] ) ) { 342 return ''; 343 } 344 345 $script_module = $this->registered[ $id ]; 346 $src = $script_module['src']; 347 348 if ( false === $script_module['version'] ) { 349 $src = add_query_arg( 'ver', get_bloginfo( 'version' ), $src ); 350 } elseif ( null !== $script_module['version'] ) { 351 $src = add_query_arg( 'ver', $script_module['version'], $src ); 352 } 353 354 /** 355 * Filters the script module source. 356 * 357 * @since 6.5.0 358 * 359 * @param string $src Module source URL. 360 * @param string $id Module identifier. 361 */ 362 $src = apply_filters( 'script_module_loader_src', $src, $id ); 363 364 return $src; 365 } 366 367 /** 368 * Print data associated with Script Modules. 369 * 370 * The data will be embedded in the page HTML and can be read by Script Modules on page load. 371 * 372 * @since 6.7.0 373 * 374 * Data can be associated with a Script Module via the 375 * {@see "script_module_data_{$module_id}"} filter. 376 * 377 * The data for a Script Module will be serialized as JSON in a script tag with an ID of the 378 * form `wp-script-module-data-{$module_id}`. 379 */ 380 public function print_script_module_data(): void { 381 $modules = array(); 382 foreach ( array_keys( $this->get_marked_for_enqueue() ) as $id ) { 383 if ( '@wordpress/a11y' === $id ) { 384 $this->a11y_available = true; 385 } 386 $modules[ $id ] = true; 387 } 388 foreach ( array_keys( $this->get_import_map()['imports'] ) as $id ) { 389 if ( '@wordpress/a11y' === $id ) { 390 $this->a11y_available = true; 391 } 392 $modules[ $id ] = true; 393 } 394 395 foreach ( array_keys( $modules ) as $module_id ) { 396 /** 397 * Filters data associated with a given Script Module. 398 * 399 * Script Modules may require data that is required for initialization or is essential 400 * to have immediately available on page load. These are suitable use cases for 401 * this data. 402 * 403 * The dynamic portion of the hook name, `$module_id`, refers to the Script Module ID 404 * that the data is associated with. 405 * 406 * This is best suited to pass essential data that must be available to the module for 407 * initialization or immediately on page load. It does not replace the REST API or 408 * fetching data from the client. 409 * 410 * Example: 411 * 412 * add_filter( 413 * 'script_module_data_MyScriptModuleID', 414 * function ( array $data ): array { 415 * $data['dataForClient'] = 'ok'; 416 * return $data; 417 * } 418 * ); 419 * 420 * If the filter returns no data (an empty array), nothing will be embedded in the page. 421 * 422 * The data for a given Script Module, if provided, will be JSON serialized in a script 423 * tag with an ID of the form `wp-script-module-data-{$module_id}`. 424 * 425 * The data can be read on the client with a pattern like this: 426 * 427 * Example: 428 * 429 * const dataContainer = document.getElementById( 'wp-script-module-data-MyScriptModuleID' ); 430 * let data = {}; 431 * if ( dataContainer ) { 432 * try { 433 * data = JSON.parse( dataContainer.textContent ); 434 * } catch {} 435 * } 436 * // data.dataForClient === 'ok'; 437 * initMyScriptModuleWithData( data ); 438 * 439 * @since 6.7.0 440 * 441 * @param array $data The data associated with the Script Module. 442 */ 443 $data = apply_filters( "script_module_data_{$module_id}", array() ); 444 445 if ( is_array( $data ) && array() !== $data ) { 446 /* 447 * This data will be printed as JSON inside a script tag like this: 448 * <script type="application/json"></script> 449 * 450 * A script tag must be closed by a sequence beginning with `</`. It's impossible to 451 * close a script tag without using `<`. We ensure that `<` is escaped and `/` can 452 * remain unescaped, so `</script>` will be printed as `\u003C/script\u00E3`. 453 * 454 * - JSON_HEX_TAG: All < and > are converted to \u003C and \u003E. 455 * - JSON_UNESCAPED_SLASHES: Don't escape /. 456 * 457 * If the page will use UTF-8 encoding, it's safe to print unescaped unicode: 458 * 459 * - JSON_UNESCAPED_UNICODE: Encode multibyte Unicode characters literally (instead of as `\uXXXX`). 460 * - JSON_UNESCAPED_LINE_TERMINATORS: The line terminators are kept unescaped when 461 * JSON_UNESCAPED_UNICODE is supplied. It uses the same behaviour as it was 462 * before PHP 7.1 without this constant. Available as of PHP 7.1.0. 463 * 464 * The JSON specification requires encoding in UTF-8, so if the generated HTML page 465 * is not encoded in UTF-8 then it's not safe to include those literals. They must 466 * be escaped to avoid encoding issues. 467 * 468 * @see https://www.rfc-editor.org/rfc/rfc8259.html for details on encoding requirements. 469 * @see https://www.php.net/manual/en/json.constants.php for details on these constants. 470 * @see https://html.spec.whatwg.org/#script-data-state for details on script tag parsing. 471 */ 472 $json_encode_flags = JSON_HEX_TAG | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_LINE_TERMINATORS; 473 if ( ! is_utf8_charset() ) { 474 $json_encode_flags = JSON_HEX_TAG | JSON_UNESCAPED_SLASHES; 475 } 476 477 wp_print_inline_script_tag( 478 wp_json_encode( 479 $data, 480 $json_encode_flags 481 ), 482 array( 483 'type' => 'application/json', 484 'id' => "wp-script-module-data-{$module_id}", 485 ) 486 ); 487 } 488 } 489 } 490 491 /** 492 * @access private This is only intended to be called by the registered actions. 493 * 494 * @since 6.7.0 495 */ 496 public function print_a11y_script_module_html() { 497 if ( ! $this->a11y_available ) { 498 return; 499 } 500 echo '<div style="position:absolute;margin:-1px;padding:0;height:1px;width:1px;overflow:hidden;clip-path:inset(50%);border:0;word-wrap:normal !important;">' 501 . '<p id="a11y-speak-intro-text" class="a11y-speak-intro-text" hidden>' . esc_html__( 'Notifications' ) . '</p>' 502 . '<div id="a11y-speak-assertive" class="a11y-speak-region" aria-live="assertive" aria-relevant="additions text" aria-atomic="true"></div>' 503 . '<div id="a11y-speak-polite" class="a11y-speak-region" aria-live="polite" aria-relevant="additions text" aria-atomic="true"></div>' 504 . '</div>'; 505 } 506 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated : Tue Dec 24 08:20:01 2024 | Cross-referenced by PHPXref |