190 * script tag of type "application/json". Once in the browser, the state will |
200 * script tag of type "application/json". Once in the browser, the state will |
191 * be parsed and used to hydrate the client-side interactivity stores and the |
201 * be parsed and used to hydrate the client-side interactivity stores and the |
192 * configuration will be available using a `getConfig` utility. |
202 * configuration will be available using a `getConfig` utility. |
193 * |
203 * |
194 * @since 6.5.0 |
204 * @since 6.5.0 |
|
205 * |
|
206 * @deprecated 6.7.0 Client data passing is handled by the {@see "script_module_data_{$module_id}"} filter. |
195 */ |
207 */ |
196 public function print_client_interactivity_data() { |
208 public function print_client_interactivity_data() { |
|
209 _deprecated_function( __METHOD__, '6.7.0' ); |
|
210 } |
|
211 |
|
212 /** |
|
213 * Set client-side interactivity-router data. |
|
214 * |
|
215 * Once in the browser, the state will be parsed and used to hydrate the client-side |
|
216 * interactivity stores and the configuration will be available using a `getConfig` utility. |
|
217 * |
|
218 * @since 6.7.0 |
|
219 * |
|
220 * @param array $data Data to filter. |
|
221 * @return array Data for the Interactivity Router script module. |
|
222 */ |
|
223 public function filter_script_module_interactivity_router_data( array $data ): array { |
|
224 if ( ! isset( $data['i18n'] ) ) { |
|
225 $data['i18n'] = array(); |
|
226 } |
|
227 $data['i18n']['loading'] = __( 'Loading page, please wait.' ); |
|
228 $data['i18n']['loaded'] = __( 'Page Loaded.' ); |
|
229 return $data; |
|
230 } |
|
231 |
|
232 /** |
|
233 * Set client-side interactivity data. |
|
234 * |
|
235 * Once in the browser, the state will be parsed and used to hydrate the client-side |
|
236 * interactivity stores and the configuration will be available using a `getConfig` utility. |
|
237 * |
|
238 * @since 6.7.0 |
|
239 * |
|
240 * @param array $data Data to filter. |
|
241 * @return array Data for the Interactivity API script module. |
|
242 */ |
|
243 public function filter_script_module_interactivity_data( array $data ): array { |
197 if ( empty( $this->state_data ) && empty( $this->config_data ) ) { |
244 if ( empty( $this->state_data ) && empty( $this->config_data ) ) { |
198 return; |
245 return $data; |
199 } |
246 } |
200 |
|
201 $interactivity_data = array(); |
|
202 |
247 |
203 $config = array(); |
248 $config = array(); |
204 foreach ( $this->config_data as $key => $value ) { |
249 foreach ( $this->config_data as $key => $value ) { |
205 if ( ! empty( $value ) ) { |
250 if ( ! empty( $value ) ) { |
206 $config[ $key ] = $value; |
251 $config[ $key ] = $value; |
207 } |
252 } |
208 } |
253 } |
209 if ( ! empty( $config ) ) { |
254 if ( ! empty( $config ) ) { |
210 $interactivity_data['config'] = $config; |
255 $data['config'] = $config; |
211 } |
256 } |
212 |
257 |
213 $state = array(); |
258 $state = array(); |
214 foreach ( $this->state_data as $key => $value ) { |
259 foreach ( $this->state_data as $key => $value ) { |
215 if ( ! empty( $value ) ) { |
260 if ( ! empty( $value ) ) { |
216 $state[ $key ] = $value; |
261 $state[ $key ] = $value; |
217 } |
262 } |
218 } |
263 } |
219 if ( ! empty( $state ) ) { |
264 if ( ! empty( $state ) ) { |
220 $interactivity_data['state'] = $state; |
265 $data['state'] = $state; |
221 } |
266 } |
222 |
267 |
223 if ( ! empty( $interactivity_data ) ) { |
268 return $data; |
224 /* |
|
225 * This data will be printed as JSON inside a script tag like this: |
|
226 * <script type="application/json"></script> |
|
227 * |
|
228 * A script tag must be closed by a sequence beginning with `</`. It's impossible to |
|
229 * close a script tag without using `<`. We ensure that `<` is escaped and `/` can |
|
230 * remain unescaped, so `</script>` will be printed as `\u003C/script\u00E3`. |
|
231 * |
|
232 * - JSON_HEX_TAG: All < and > are converted to \u003C and \u003E. |
|
233 * - JSON_UNESCAPED_SLASHES: Don't escape /. |
|
234 * |
|
235 * If the page will use UTF-8 encoding, it's safe to print unescaped unicode: |
|
236 * |
|
237 * - JSON_UNESCAPED_UNICODE: Encode multibyte Unicode characters literally (instead of as `\uXXXX`). |
|
238 * - JSON_UNESCAPED_LINE_TERMINATORS: The line terminators are kept unescaped when |
|
239 * JSON_UNESCAPED_UNICODE is supplied. It uses the same behaviour as it was |
|
240 * before PHP 7.1 without this constant. Available as of PHP 7.1.0. |
|
241 * |
|
242 * The JSON specification requires encoding in UTF-8, so if the generated HTML page |
|
243 * is not encoded in UTF-8 then it's not safe to include those literals. They must |
|
244 * be escaped to avoid encoding issues. |
|
245 * |
|
246 * @see https://www.rfc-editor.org/rfc/rfc8259.html for details on encoding requirements. |
|
247 * @see https://www.php.net/manual/en/json.constants.php for details on these constants. |
|
248 * @see https://html.spec.whatwg.org/#script-data-state for details on script tag parsing. |
|
249 */ |
|
250 $json_encode_flags = JSON_HEX_TAG | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_LINE_TERMINATORS; |
|
251 if ( ! is_utf8_charset() ) { |
|
252 $json_encode_flags = JSON_HEX_TAG | JSON_UNESCAPED_SLASHES; |
|
253 } |
|
254 |
|
255 wp_print_inline_script_tag( |
|
256 wp_json_encode( |
|
257 $interactivity_data, |
|
258 $json_encode_flags |
|
259 ), |
|
260 array( |
|
261 'type' => 'application/json', |
|
262 'id' => 'wp-interactivity-data', |
|
263 ) |
|
264 ); |
|
265 } |
|
266 } |
269 } |
267 |
270 |
268 /** |
271 /** |
269 * Returns the latest value on the context stack with the passed namespace. |
272 * Returns the latest value on the context stack with the passed namespace. |
270 * |
273 * |
304 ? $context[ $store_namespace ] |
307 ? $context[ $store_namespace ] |
305 : array(); |
308 : array(); |
306 } |
309 } |
307 |
310 |
308 /** |
311 /** |
|
312 * Returns an array representation of the current element being processed. |
|
313 * |
|
314 * The returned array contains a copy of the element attributes. |
|
315 * |
|
316 * @since 6.7.0 |
|
317 * |
|
318 * @return array{attributes: array<string, string|bool>}|null Current element. |
|
319 */ |
|
320 public function get_element(): ?array { |
|
321 if ( null === $this->current_element ) { |
|
322 _doing_it_wrong( |
|
323 __METHOD__, |
|
324 __( 'The element can only be read during directive processing.' ), |
|
325 '6.7.0' |
|
326 ); |
|
327 } |
|
328 |
|
329 return $this->current_element; |
|
330 } |
|
331 |
|
332 /** |
309 * Registers the `@wordpress/interactivity` script modules. |
333 * Registers the `@wordpress/interactivity` script modules. |
310 * |
334 * |
|
335 * @deprecated 6.7.0 Script Modules registration is handled by {@see wp_default_script_modules()}. |
|
336 * |
311 * @since 6.5.0 |
337 * @since 6.5.0 |
312 */ |
338 */ |
313 public function register_script_modules() { |
339 public function register_script_modules() { |
314 $suffix = wp_scripts_get_suffix(); |
340 _deprecated_function( __METHOD__, '6.7.0', 'wp_default_script_modules' ); |
315 |
|
316 wp_register_script_module( |
|
317 '@wordpress/interactivity', |
|
318 includes_url( "js/dist/interactivity$suffix.js" ) |
|
319 ); |
|
320 |
|
321 wp_register_script_module( |
|
322 '@wordpress/interactivity-router', |
|
323 includes_url( "js/dist/interactivity-router$suffix.js" ), |
|
324 array( '@wordpress/interactivity' ) |
|
325 ); |
|
326 } |
341 } |
327 |
342 |
328 /** |
343 /** |
329 * Adds the necessary hooks for the Interactivity API. |
344 * Adds the necessary hooks for the Interactivity API. |
330 * |
345 * |
331 * @since 6.5.0 |
346 * @since 6.5.0 |
332 */ |
347 */ |
333 public function add_hooks() { |
348 public function add_hooks() { |
334 add_action( 'wp_enqueue_scripts', array( $this, 'register_script_modules' ) ); |
349 add_filter( 'script_module_data_@wordpress/interactivity', array( $this, 'filter_script_module_interactivity_data' ) ); |
335 add_action( 'wp_footer', array( $this, 'print_client_interactivity_data' ) ); |
350 add_filter( 'script_module_data_@wordpress/interactivity-router', array( $this, 'filter_script_module_interactivity_router_data' ) ); |
336 |
|
337 add_action( 'admin_enqueue_scripts', array( $this, 'register_script_modules' ) ); |
|
338 add_action( 'admin_print_footer_scripts', array( $this, 'print_client_interactivity_data' ) ); |
|
339 } |
351 } |
340 |
352 |
341 /** |
353 /** |
342 * Processes the interactivity directives contained within the HTML content |
354 * Processes the interactivity directives contained within the HTML content |
343 * and updates the markup accordingly. |
355 * and updates the markup accordingly. |
437 } else { |
449 } else { |
438 $directives_prefixes = array(); |
450 $directives_prefixes = array(); |
439 |
451 |
440 // Checks if there is a server directive processor registered for each directive. |
452 // Checks if there is a server directive processor registered for each directive. |
441 foreach ( $p->get_attribute_names_with_prefix( 'data-wp-' ) as $attribute_name ) { |
453 foreach ( $p->get_attribute_names_with_prefix( 'data-wp-' ) as $attribute_name ) { |
|
454 if ( ! preg_match( |
|
455 /* |
|
456 * This must align with the client-side regex used by the interactivity API. |
|
457 * @see https://github.com/WordPress/gutenberg/blob/ca616014255efbb61f34c10917d52a2d86c1c660/packages/interactivity/src/vdom.ts#L20-L32 |
|
458 */ |
|
459 '/' . |
|
460 '^data-wp-' . |
|
461 // Match alphanumeric characters including hyphen-separated |
|
462 // segments. It excludes underscore intentionally to prevent confusion. |
|
463 // E.g., "custom-directive". |
|
464 '([a-z0-9]+(?:-[a-z0-9]+)*)' . |
|
465 // (Optional) Match '--' followed by any alphanumeric charachters. It |
|
466 // excludes underscore intentionally to prevent confusion, but it can |
|
467 // contain multiple hyphens. E.g., "--custom-prefix--with-more-info". |
|
468 '(?:--([a-z0-9_-]+))?$' . |
|
469 '/i', |
|
470 $attribute_name |
|
471 ) ) { |
|
472 continue; |
|
473 } |
442 list( $directive_prefix ) = $this->extract_prefix_and_suffix( $attribute_name ); |
474 list( $directive_prefix ) = $this->extract_prefix_and_suffix( $attribute_name ); |
443 if ( array_key_exists( $directive_prefix, self::$directive_processors ) ) { |
475 if ( array_key_exists( $directive_prefix, self::$directive_processors ) ) { |
444 $directives_prefixes[] = $directive_prefix; |
476 $directives_prefixes[] = $directive_prefix; |
445 } |
477 } |
446 } |
478 } |
464 |
496 |
465 // Directive processing might be different depending on if it is entering the tag or exiting it. |
497 // Directive processing might be different depending on if it is entering the tag or exiting it. |
466 $modes = array( |
498 $modes = array( |
467 'enter' => ! $p->is_tag_closer(), |
499 'enter' => ! $p->is_tag_closer(), |
468 'exit' => $p->is_tag_closer() || ! $p->has_and_visits_its_closer_tag(), |
500 'exit' => $p->is_tag_closer() || ! $p->has_and_visits_its_closer_tag(), |
|
501 ); |
|
502 |
|
503 // Get the element attributes to include them in the element representation. |
|
504 $element_attrs = array(); |
|
505 $attr_names = $p->get_attribute_names_with_prefix( '' ) ?? array(); |
|
506 |
|
507 foreach ( $attr_names as $name ) { |
|
508 $element_attrs[ $name ] = $p->get_attribute( $name ); |
|
509 } |
|
510 |
|
511 // Assign the current element right before running its directive processors. |
|
512 $this->current_element = array( |
|
513 'attributes' => $element_attrs, |
469 ); |
514 ); |
470 |
515 |
471 foreach ( $modes as $mode => $should_run ) { |
516 foreach ( $modes as $mode => $should_run ) { |
472 if ( ! $should_run ) { |
517 if ( ! $should_run ) { |
473 continue; |
518 continue; |
549 |
597 |
550 // Extracts the value from the store using the reference path. |
598 // Extracts the value from the store using the reference path. |
551 $path_segments = explode( '.', $path ); |
599 $path_segments = explode( '.', $path ); |
552 $current = $store; |
600 $current = $store; |
553 foreach ( $path_segments as $path_segment ) { |
601 foreach ( $path_segments as $path_segment ) { |
|
602 /* |
|
603 * Special case for numeric arrays and strings. Add length |
|
604 * property mimicking JavaScript behavior. |
|
605 * |
|
606 * @since 6.8.0 |
|
607 */ |
|
608 if ( 'length' === $path_segment ) { |
|
609 if ( is_array( $current ) && array_is_list( $current ) ) { |
|
610 $current = count( $current ); |
|
611 break; |
|
612 } |
|
613 |
|
614 if ( is_string( $current ) ) { |
|
615 /* |
|
616 * Differences in encoding between PHP strings and |
|
617 * JavaScript mean that it's complicated to calculate |
|
618 * the string length JavaScript would see from PHP. |
|
619 * `strlen` is a reasonable approximation. |
|
620 * |
|
621 * Users that desire a more precise length likely have |
|
622 * more precise needs than "bytelength" and should |
|
623 * implement their own length calculation in derived |
|
624 * state taking into account encoding and their desired |
|
625 * output (codepoints, graphemes, bytes, etc.). |
|
626 */ |
|
627 $current = strlen( $current ); |
|
628 break; |
|
629 } |
|
630 } |
|
631 |
554 if ( ( is_array( $current ) || $current instanceof ArrayAccess ) && isset( $current[ $path_segment ] ) ) { |
632 if ( ( is_array( $current ) || $current instanceof ArrayAccess ) && isset( $current[ $path_segment ] ) ) { |
555 $current = $current[ $path_segment ]; |
633 $current = $current[ $path_segment ]; |
556 } elseif ( is_object( $current ) && isset( $current->$path_segment ) ) { |
634 } elseif ( is_object( $current ) && isset( $current->$path_segment ) ) { |
557 $current = $current->$path_segment; |
635 $current = $current->$path_segment; |
558 } else { |
636 } else { |
559 return null; |
637 $current = null; |
|
638 break; |
560 } |
639 } |
561 |
640 |
562 if ( $current instanceof Closure ) { |
641 if ( $current instanceof Closure ) { |
563 /* |
642 /* |
564 * This state getter's namespace is added to the stack so that |
643 * This state getter's namespace is added to the stack so that |
1011 } |
1090 } |
1012 CSS; |
1091 CSS; |
1013 } |
1092 } |
1014 |
1093 |
1015 /** |
1094 /** |
1016 * Outputs the markup for the top loading indicator and the screen reader |
1095 * Deprecated. |
1017 * notifications during client-side navigations. |
1096 * |
|
1097 * @since 6.5.0 |
|
1098 * @deprecated 6.7.0 Use {@see WP_Interactivity_API::print_router_markup} instead. |
|
1099 */ |
|
1100 public function print_router_loading_and_screen_reader_markup() { |
|
1101 _deprecated_function( __METHOD__, '6.7.0', 'WP_Interactivity_API::print_router_markup' ); |
|
1102 |
|
1103 // Call the new method. |
|
1104 $this->print_router_markup(); |
|
1105 } |
|
1106 |
|
1107 /** |
|
1108 * Outputs markup for the @wordpress/interactivity-router script module. |
1018 * |
1109 * |
1019 * This method prints a div element representing a loading bar visible during |
1110 * This method prints a div element representing a loading bar visible during |
1020 * navigation, as well as an aria-live region that can be read by screen |
1111 * navigation. |
1021 * readers to announce navigation status. |
1112 * |
1022 * |
1113 * @since 6.7.0 |
1023 * @since 6.5.0 |
1114 */ |
1024 */ |
1115 public function print_router_markup() { |
1025 public function print_router_loading_and_screen_reader_markup() { |
|
1026 echo <<<HTML |
1116 echo <<<HTML |
1027 <div |
1117 <div |
1028 class="wp-interactivity-router-loading-bar" |
1118 class="wp-interactivity-router-loading-bar" |
1029 data-wp-interactive="core/router" |
1119 data-wp-interactive="core/router" |
1030 data-wp-class--start-animation="state.navigation.hasStarted" |
1120 data-wp-class--start-animation="state.navigation.hasStarted" |
1031 data-wp-class--finish-animation="state.navigation.hasFinished" |
1121 data-wp-class--finish-animation="state.navigation.hasFinished" |
1032 ></div> |
1122 ></div> |
1033 <div |
|
1034 class="screen-reader-text" |
|
1035 aria-live="polite" |
|
1036 data-wp-interactive="core/router" |
|
1037 data-wp-text="state.navigation.message" |
|
1038 ></div> |
|
1039 HTML; |
1123 HTML; |
1040 } |
1124 } |
1041 |
1125 |
1042 /** |
1126 /** |
1043 * Processes the `data-wp-router-region` directive. |
1127 * Processes the `data-wp-router-region` directive. |
1054 */ |
1138 */ |
1055 private function data_wp_router_region_processor( WP_Interactivity_API_Directives_Processor $p, string $mode ) { |
1139 private function data_wp_router_region_processor( WP_Interactivity_API_Directives_Processor $p, string $mode ) { |
1056 if ( 'enter' === $mode && ! $this->has_processed_router_region ) { |
1140 if ( 'enter' === $mode && ! $this->has_processed_router_region ) { |
1057 $this->has_processed_router_region = true; |
1141 $this->has_processed_router_region = true; |
1058 |
1142 |
1059 // Initialize the `core/router` store. |
|
1060 $this->state( |
|
1061 'core/router', |
|
1062 array( |
|
1063 'navigation' => array( |
|
1064 'texts' => array( |
|
1065 'loading' => __( 'Loading page, please wait.' ), |
|
1066 'loaded' => __( 'Page Loaded.' ), |
|
1067 ), |
|
1068 ), |
|
1069 ) |
|
1070 ); |
|
1071 |
|
1072 // Enqueues as an inline style. |
1143 // Enqueues as an inline style. |
1073 wp_register_style( 'wp-interactivity-router-animations', false ); |
1144 wp_register_style( 'wp-interactivity-router-animations', false ); |
1074 wp_add_inline_style( 'wp-interactivity-router-animations', $this->get_router_animation_styles() ); |
1145 wp_add_inline_style( 'wp-interactivity-router-animations', $this->get_router_animation_styles() ); |
1075 wp_enqueue_style( 'wp-interactivity-router-animations' ); |
1146 wp_enqueue_style( 'wp-interactivity-router-animations' ); |
1076 |
1147 |
1077 // Adds the necessary markup to the footer. |
1148 // Adds the necessary markup to the footer. |
1078 add_action( 'wp_footer', array( $this, 'print_router_loading_and_screen_reader_markup' ) ); |
1149 add_action( 'wp_footer', array( $this, 'print_router_markup' ) ); |
1079 } |
1150 } |
1080 } |
1151 } |
1081 |
1152 |
1082 /** |
1153 /** |
1083 * Processes the `data-wp-each` directive. |
1154 * Processes the `data-wp-each` directive. |