wp/wp-includes/class-wp-script-modules.php
changeset 22 8c2e4d02f4ef
parent 21 48c4eec2b7e6
equal deleted inserted replaced
21:48c4eec2b7e6 22:8c2e4d02f4ef
    27 	 *
    27 	 *
    28 	 * @since 6.5.0
    28 	 * @since 6.5.0
    29 	 * @var array<string, true>
    29 	 * @var array<string, true>
    30 	 */
    30 	 */
    31 	private $enqueued_before_registered = array();
    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;
    32 
    43 
    33 	/**
    44 	/**
    34 	 * Registers the script module if no script module with that script module
    45 	 * Registers the script module if no script module with that script module
    35 	 * identifier has already been registered.
    46 	 * identifier has already been registered.
    36 	 *
    47 	 *
   180 		add_action( $position, array( $this, 'print_script_module_preloads' ) );
   191 		add_action( $position, array( $this, 'print_script_module_preloads' ) );
   181 
   192 
   182 		add_action( 'admin_print_footer_scripts', array( $this, 'print_import_map' ) );
   193 		add_action( 'admin_print_footer_scripts', array( $this, 'print_import_map' ) );
   183 		add_action( 'admin_print_footer_scripts', array( $this, 'print_enqueued_script_modules' ) );
   194 		add_action( 'admin_print_footer_scripts', array( $this, 'print_enqueued_script_modules' ) );
   184 		add_action( 'admin_print_footer_scripts', array( $this, 'print_script_module_preloads' ) );
   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 );
   185 	}
   201 	}
   186 
   202 
   187 	/**
   203 	/**
   188 	 * Prints the enqueued script modules using script tags with type="module"
   204 	 * Prints the enqueued script modules using script tags with type="module"
   189 	 * attributes.
   205 	 * attributes.
   225 
   241 
   226 	/**
   242 	/**
   227 	 * Prints the import map using a script tag with a type="importmap" attribute.
   243 	 * Prints the import map using a script tag with a type="importmap" attribute.
   228 	 *
   244 	 *
   229 	 * @since 6.5.0
   245 	 * @since 6.5.0
   230 	 *
       
   231 	 * @global WP_Scripts $wp_scripts The WP_Scripts object for printing the polyfill.
       
   232 	 */
   246 	 */
   233 	public function print_import_map() {
   247 	public function print_import_map() {
   234 		$import_map = $this->get_import_map();
   248 		$import_map = $this->get_import_map();
   235 		if ( ! empty( $import_map['imports'] ) ) {
   249 		if ( ! empty( $import_map['imports'] ) ) {
   236 			global $wp_scripts;
       
   237 			if ( isset( $wp_scripts ) ) {
       
   238 				wp_print_inline_script_tag(
       
   239 					wp_get_script_polyfill(
       
   240 						$wp_scripts,
       
   241 						array(
       
   242 							'HTMLScriptElement.supports && HTMLScriptElement.supports("importmap")' => 'wp-polyfill-importmap',
       
   243 						)
       
   244 					),
       
   245 					array(
       
   246 						'id' => 'wp-load-polyfill-importmap',
       
   247 					)
       
   248 				);
       
   249 			}
       
   250 			wp_print_inline_script_tag(
   250 			wp_print_inline_script_tag(
   251 				wp_json_encode( $import_map, JSON_HEX_TAG | JSON_HEX_AMP ),
   251 				wp_json_encode( $import_map, JSON_HEX_TAG | JSON_HEX_AMP ),
   252 				array(
   252 				array(
   253 					'type' => 'importmap',
   253 					'type' => 'importmap',
   254 					'id'   => 'wp-importmap',
   254 					'id'   => 'wp-importmap',
   361 		 */
   361 		 */
   362 		$src = apply_filters( 'script_module_loader_src', $src, $id );
   362 		$src = apply_filters( 'script_module_loader_src', $src, $id );
   363 
   363 
   364 		return $src;
   364 		return $src;
   365 	}
   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 	}
   366 }
   506 }