wp/wp-includes/interactivity-api/class-wp-interactivity-api.php
changeset 22 8c2e4d02f4ef
parent 21 48c4eec2b7e6
equal deleted inserted replaced
21:48c4eec2b7e6 22:8c2e4d02f4ef
    94 	 * @var array<array<mixed>>|null
    94 	 * @var array<array<mixed>>|null
    95 	 */
    95 	 */
    96 	private $context_stack = null;
    96 	private $context_stack = null;
    97 
    97 
    98 	/**
    98 	/**
       
    99 	 * Representation in array format of the element currently being processed.
       
   100 	 *
       
   101 	 * This is only available during directive processing, otherwise it is `null`.
       
   102 	 *
       
   103 	 * @since 6.7.0
       
   104 	 * @var array{attributes: array<string, string|bool>}|null
       
   105 	 */
       
   106 	private $current_element = null;
       
   107 
       
   108 	/**
    99 	 * Gets and/or sets the initial state of an Interactivity API store for a
   109 	 * Gets and/or sets the initial state of an Interactivity API store for a
   100 	 * given namespace.
   110 	 * given namespace.
   101 	 *
   111 	 *
   102 	 * If state for that store namespace already exists, it merges the new
   112 	 * If state for that store namespace already exists, it merges the new
   103 	 * provided state with the existing one.
   113 	 * provided state with the existing one.
   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;
   487 						: array( $this, self::$directive_processors[ $directive_prefix ] );
   532 						: array( $this, self::$directive_processors[ $directive_prefix ] );
   488 
   533 
   489 					call_user_func_array( $func, array( $p, $mode, &$tag_stack ) );
   534 					call_user_func_array( $func, array( $p, $mode, &$tag_stack ) );
   490 				}
   535 				}
   491 			}
   536 			}
       
   537 
       
   538 			// Clear the current element.
       
   539 			$this->current_element = null;
   492 		}
   540 		}
   493 
   541 
   494 		if ( $unbalanced ) {
   542 		if ( $unbalanced ) {
   495 			// Reset the namespace and context stacks to their previous values.
   543 			// Reset the namespace and context stacks to their previous values.
   496 			array_splice( $this->namespace_stack, $namespace_stack_size );
   544 			array_splice( $this->namespace_stack, $namespace_stack_size );
   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.