diff --git a/docs/reference-guides/core-blocks.md b/docs/reference-guides/core-blocks.md index e65853a6fbdf71..396d8ef40e059d 100644 --- a/docs/reference-guides/core-blocks.md +++ b/docs/reference-guides/core-blocks.md @@ -378,7 +378,7 @@ Insert an image to make a visual statement. ([Source](https://github.com/WordPre - **Name:** core/image - **Category:** media -- **Supports:** anchor, color (~~background~~, ~~text~~), filter (duotone) +- **Supports:** anchor, color (~~background~~, ~~text~~), filter (duotone), interactivity - **Attributes:** align, alt, aspectRatio, caption, height, href, id, lightbox, linkClass, linkDestination, linkTarget, rel, scale, sizeSlug, title, url, width ## Latest Comments diff --git a/lib/experimental/interactivity-api/modules.php b/lib/experimental/interactivity-api/modules.php new file mode 100644 index 00000000000000..a15f6c915f4317 --- /dev/null +++ b/lib/experimental/interactivity-api/modules.php @@ -0,0 +1,35 @@ + defined( 'GUTENBERG_VERSION' ) ? GUTENBERG_VERSION : get_bloginfo( 'version' ), + ) + ); + + // TODO: Move this to a local file and replace with a simpler version that + // only provides support for import maps. + wp_enqueue_script( + 'es-module-shims', + 'https://ga.jspm.io/npm:es-module-shims@1.8.2/dist/es-module-shims.js', + array(), + null, + array( + 'strategy' => 'defer', + ) + ); +} + +add_action( 'wp_enqueue_scripts', 'gutenberg_register_interactivity_module' ); diff --git a/lib/experimental/interactivity-api/scripts.php b/lib/experimental/interactivity-api/scripts.php deleted file mode 100644 index ed1fca85500701..00000000000000 --- a/lib/experimental/interactivity-api/scripts.php +++ /dev/null @@ -1,40 +0,0 @@ -=' ); - if ( $supports_defer ) { - // Defer execution of @wordpress/interactivity package but continue loading in head. - wp_script_add_data( 'wp-interactivity', 'strategy', 'defer' ); - wp_script_add_data( 'wp-interactivity', 'group', 0 ); - } else { - // Move the @wordpress/interactivity package to the footer. - wp_script_add_data( 'wp-interactivity', 'group', 1 ); - } - - // Move all the view scripts of the interactive blocks to the footer. - $registered_blocks = \WP_Block_Type_Registry::get_instance()->get_all_registered(); - foreach ( array_values( $registered_blocks ) as $block ) { - if ( isset( $block->supports['interactivity'] ) && $block->supports['interactivity'] ) { - foreach ( $block->view_script_handles as $handle ) { - // Note that all block view scripts are already made defer by default. - wp_script_add_data( $handle, 'group', $supports_defer ? 0 : 1 ); - } - } - } -} -add_action( 'wp_enqueue_scripts', 'gutenberg_interactivity_move_interactive_scripts_to_the_footer', 11 ); diff --git a/lib/experimental/modules/class-gutenberg-modules.php b/lib/experimental/modules/class-gutenberg-modules.php new file mode 100644 index 00000000000000..a33ec176914dc3 --- /dev/null +++ b/lib/experimental/modules/class-gutenberg-modules.php @@ -0,0 +1,229 @@ + $src, + 'usage' => $usage, + 'version' => isset( $args['version'] ) ? $args['version'] : '', + 'dependencies' => isset( $args['dependencies'] ) ? $args['dependencies'] : array(), + ); + } + } + + /** + * Enqueues a module in the page. + * + * @param string $module_identifier The identifier of the module. + */ + public static function enqueue( $module_identifier ) { + // Add the module to the queue if it's not already there. + if ( ! in_array( $module_identifier, self::$enqueued, true ) ) { + self::$enqueued[] = $module_identifier; + } + } + + /** + * Returns the import map array. + * + * @return string The import map. + */ + public static function get_import_map() { + $import_map = array( + 'imports' => array(), + ); + + foreach ( self::$registered as $module_identifier => $module ) { + if ( self::get_appropriate_usage( $module['usage'] ) ) { + $import_map['imports'][ $module_identifier ] = $module['src'] . self::get_module_version( $module ); + } + } + + return $import_map; + } + + /** + * Prints the import map. + */ + public static function print_import_map() { + echo ''; + } + + /** + * Prints all the enqueued modules using '; + } + } + } + + /** + * Prints the link tag with rel="modulepreload" for all the static + * dependencies of the enqueued modules. + */ + public static function print_module_preloads() { + foreach ( self::get_dependencies( self::$enqueued ) as $dependency_identifier => $module ) { + $version = self::get_module_version( $module ); + echo ''; + } + } + + /** + * Determines if the usage is appropriate for the current context. + * + * @param array $usage Specifies the usage of the module. Can contain 'admin' or 'frontend'. + * @return bool Returns true if it's appropriate to load the module in the current WP context. + */ + private static function get_appropriate_usage( $usage ) { + if ( in_array( 'admin', $usage, true ) && is_admin() ) { + return true; + } + if ( in_array( 'frontend', $usage, true ) && ! is_admin() ) { + return true; + } + return false; + } + + /** + * Gets the module's version. It either returns a timestamp (if SCRIPT_DEBUG + * is true), the explicit version of the module if it is set and not false, or + * an empty string if none of the above conditions are met. + * + * @param array $module The data of the module. + * @return string A string presenting the version. + */ + private static function get_module_version( $module ) { + if ( SCRIPT_DEBUG ) { + return '?ver=' . time(); + } elseif ( $module['version'] ) { + return '?ver=' . $module['version']; + } + return ''; + } + + /** + * Returns all unique static dependencies for the received modules. It's + * recursive, so it will also get the static dependencies of the dependencies. + * + * @param array $module_identifiers The identifiers of the modules to get dependencies for. + * @return array The array containing the unique dependencies of the modules. + */ + public static function get_dependencies( $module_identifiers ) { + return array_reduce( + $module_identifiers, + function ( $dependency_modules, $module_identifier ) { + if ( + ! isset( self::$registered[ $module_identifier ] ) || + ! self::get_appropriate_usage( self::$registered[ $module_identifier ]['usage'] ) || + empty( self::$registered[ $module_identifier ]['dependencies'] ) + ) { + return $dependency_modules; + } + + $filtered_dependencies = array_filter( + self::$registered[ $module_identifier ]['dependencies'], + function ( $dependency_identifier ) { + return isset( self::$registered[ $dependency_identifier ] ) && self::get_appropriate_usage( self::$registered[ $dependency_identifier ]['usage'] ); + } + ); + + $dependency_data = array_intersect_key( self::$registered, array_flip( $filtered_dependencies ) ); + return array_merge( $dependency_modules, $dependency_data, self::get_dependencies( $filtered_dependencies ) ); + }, + array() + ); + } +} + +/** + * Registers a JavaScript module. It will be added to the import map. + * + * @param string $module_identifier The identifier of the module. Should be unique. It will be used in the final import map. + * @param string $src Full URL of the module, or path of the script relative to the WordPress root directory. + * @param string $usage Specifies where the module would be used. Can be 'admin', 'frontend', or 'both'. + * @param array $args { + * Optional array of arguments. + * + * @type string|bool $ver Optional. String specifying script version number, if it has one, it is added to the URL + * as a query string for cache busting purposes. If version is set to false, a version + * number is automatically added equal to current installed WordPress version. If SCRIPT_DEBUG + * is set to true, it uses the timestamp instead. + * @type array $dependencies Optional. An array of module identifiers of the static dependencies of this module. + * } + */ +function gutenberg_register_module( $module_identifier, $src, $usage, $args = array() ) { + Gutenberg_Modules::register( $module_identifier, $src, $usage, $args ); +} + +/** + * Enqueues a JavaScript module. It will be added to both the import map and a + * script tag with the "module" type. + * + * @param string $module_identifier The identifier of the module. Should be unique. It will be used in the final import map. + */ +function gutenberg_enqueue_module( $module_identifier ) { + Gutenberg_Modules::enqueue( $module_identifier ); +} + +// Prints the import map in the head tag. +add_action( 'wp_head', array( 'Gutenberg_Modules', 'print_import_map' ) ); + +// Prints the enqueued modules in the head tag. +add_action( 'wp_head', array( 'Gutenberg_Modules', 'print_enqueued_modules' ) ); + +// Prints the preloaded modules in the head tag. +add_action( 'wp_head', array( 'Gutenberg_Modules', 'print_module_preloads' ) ); diff --git a/lib/load.php b/lib/load.php index 711766dec7dfbd..88e7ec2ae073a0 100644 --- a/lib/load.php +++ b/lib/load.php @@ -138,7 +138,7 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/experimental/interactivity-api/class-wp-interactivity-store.php'; require __DIR__ . '/experimental/interactivity-api/store.php'; -require __DIR__ . '/experimental/interactivity-api/scripts.php'; +require __DIR__ . '/experimental/interactivity-api/modules.php'; require __DIR__ . '/experimental/interactivity-api/class-wp-directive-processor.php'; require __DIR__ . '/experimental/interactivity-api/class-wp-directive-context.php'; require __DIR__ . '/experimental/interactivity-api/directive-processing.php'; @@ -148,6 +148,8 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/experimental/interactivity-api/directives/wp-style.php'; require __DIR__ . '/experimental/interactivity-api/directives/wp-text.php'; +require __DIR__ . '/experimental/modules/class-gutenberg-modules.php'; + // Fonts API / Font Face. remove_action( 'plugins_loaded', '_wp_theme_json_webfonts_handler' ); // Turns off WordPress 6.0's stopgap handler. diff --git a/packages/block-library/src/file/block.json b/packages/block-library/src/file/block.json index 0cc20b3f501e9b..1b74b46dacdbc3 100644 --- a/packages/block-library/src/file/block.json +++ b/packages/block-library/src/file/block.json @@ -72,7 +72,6 @@ }, "interactivity": true }, - "viewScript": "file:./view.min.js", "editorStyle": "wp-block-file-editor", "style": "wp-block-file" } diff --git a/packages/block-library/src/file/index.php b/packages/block-library/src/file/index.php index 042ea899707360..876b1db34b5cc9 100644 --- a/packages/block-library/src/file/index.php +++ b/packages/block-library/src/file/index.php @@ -16,19 +16,8 @@ */ function render_block_core_file( $attributes, $content, $block ) { $should_load_view_script = ! empty( $attributes['displayPreview'] ); - $view_js_file = 'wp-block-file-view'; - // If the script already exists, there is no point in removing it from viewScript. - if ( ! wp_script_is( $view_js_file ) ) { - $script_handles = $block->block_type->view_script_handles; - - // If the script is not needed, and it is still in the `view_script_handles`, remove it. - if ( ! $should_load_view_script && in_array( $view_js_file, $script_handles, true ) ) { - $block->block_type->view_script_handles = array_diff( $script_handles, array( $view_js_file ) ); - } - // If the script is needed, but it was previously removed, add it again. - if ( $should_load_view_script && ! in_array( $view_js_file, $script_handles, true ) ) { - $block->block_type->view_script_handles = array_merge( $script_handles, array( $view_js_file ) ); - } + if ( $should_load_view_script ) { + gutenberg_enqueue_module( '@wordpress/block-library/file-block' ); } // Update object's aria-label attribute if present in block HTML. @@ -96,5 +85,15 @@ function register_block_core_file() { 'render_callback' => 'render_block_core_file', ) ); + + gutenberg_register_module( + '@wordpress/block-library/file-block', + '/wp-content/plugins/gutenberg/build/interactivity/file.min.js', + 'frontend', + array( + 'version' => defined( 'GUTENBERG_VERSION' ) ? GUTENBERG_VERSION : get_bloginfo( 'version' ), + 'dependencies' => array( '@wordpress/interactivity' ), + ) + ); } add_action( 'init', 'register_block_core_file' ); diff --git a/packages/block-library/src/image/block.json b/packages/block-library/src/image/block.json index d665a8a8f77085..bd94f3b43796dc 100644 --- a/packages/block-library/src/image/block.json +++ b/packages/block-library/src/image/block.json @@ -95,6 +95,7 @@ } }, "supports": { + "interactivity": true, "anchor": true, "color": { "text": false, @@ -130,6 +131,5 @@ { "name": "rounded", "label": "Rounded" } ], "editorStyle": "wp-block-image-editor", - "style": "wp-block-image", - "viewScript": "file:./view.min.js" + "style": "wp-block-image" } diff --git a/packages/block-library/src/image/index.php b/packages/block-library/src/image/index.php index acefd5714bbd47..c4bbb1cecc60da 100644 --- a/packages/block-library/src/image/index.php +++ b/packages/block-library/src/image/index.php @@ -37,9 +37,6 @@ function render_block_core_image( $attributes, $content, $block ) { $link_destination = isset( $attributes['linkDestination'] ) ? $attributes['linkDestination'] : 'none'; $lightbox_settings = block_core_image_get_lightbox_settings( $block->parsed_block ); - $view_js_file_handle = 'wp-block-image-view'; - $script_handles = $block->block_type->view_script_handles; - /* * If the lightbox is enabled and the image is not linked, add the filter * and the JavaScript view file. @@ -50,11 +47,7 @@ function render_block_core_image( $attributes, $content, $block ) { isset( $lightbox_settings['enabled'] ) && true === $lightbox_settings['enabled'] ) { - $block->block_type->supports['interactivity'] = true; - - if ( ! in_array( $view_js_file_handle, $script_handles, true ) ) { - $block->block_type->view_script_handles = array_merge( $script_handles, array( $view_js_file_handle ) ); - } + gutenberg_enqueue_module( '@wordpress/block-library/image' ); /* * This render needs to happen in a filter with priority 15 to ensure @@ -71,10 +64,6 @@ function render_block_core_image( $attributes, $content, $block ) { * other Image blocks. */ remove_filter( 'render_block_core/image', 'block_core_image_render_lightbox', 15 ); - // If the script is not needed, and it is still in the `view_script_handles`, remove it. - if ( in_array( $view_js_file_handle, $script_handles, true ) ) { - $block->block_type->view_script_handles = array_diff( $script_handles, array( $view_js_file_handle ) ); - } } return $processor->get_updated_html(); @@ -362,5 +351,15 @@ function register_block_core_image() { 'render_callback' => 'render_block_core_image', ) ); + + gutenberg_register_module( + '@wordpress/block-library/image', + '/wp-content/plugins/gutenberg/build/interactivity/image.min.js', + 'frontend', + array( + 'version' => defined( 'GUTENBERG_VERSION' ) ? GUTENBERG_VERSION : get_bloginfo( 'version' ), + 'dependencies' => array( '@wordpress/interactivity' ), + ) + ); } add_action( 'init', 'register_block_core_image' ); diff --git a/packages/block-library/src/navigation/block.json b/packages/block-library/src/navigation/block.json index 9ec919ae38d1fa..36817a5e1c35b1 100644 --- a/packages/block-library/src/navigation/block.json +++ b/packages/block-library/src/navigation/block.json @@ -136,7 +136,6 @@ "interactivity": true, "renaming": false }, - "viewScript": "file:./view.min.js", "editorStyle": "wp-block-navigation-editor", "style": "wp-block-navigation" } diff --git a/packages/block-library/src/navigation/index.php b/packages/block-library/src/navigation/index.php index 209a8b1c17c16f..a6f55774d4cc8d 100644 --- a/packages/block-library/src/navigation/index.php +++ b/packages/block-library/src/navigation/index.php @@ -637,20 +637,10 @@ function render_block_core_navigation( $attributes, $content, $block ) { } $should_load_view_script = ( $has_submenus && ( $attributes['openSubmenusOnClick'] || $attributes['showSubmenuIcon'] ) ) || $is_responsive_menu; - $view_js_file = 'wp-block-navigation-view'; - // If the script already exists, there is no point in removing it from viewScript. - if ( ! wp_script_is( $view_js_file ) ) { - $script_handles = $block->block_type->view_script_handles; - - // If the script is not needed, and it is still in the `view_script_handles`, remove it. - if ( ! $should_load_view_script && in_array( $view_js_file, $script_handles, true ) ) { - $block->block_type->view_script_handles = array_diff( $script_handles, array( $view_js_file ) ); - } - // If the script is needed, but it was previously removed, add it again. - if ( $should_load_view_script && ! in_array( $view_js_file, $script_handles, true ) ) { - $block->block_type->view_script_handles = array_merge( $script_handles, array( $view_js_file ) ); - } + // Load the module. + if ( $should_load_view_script ) { + gutenberg_enqueue_module( '@wordpress/block-library/navigation-block' ); } // Add directives to the submenu if needed. @@ -791,6 +781,16 @@ function register_block_core_navigation() { 'render_callback' => 'render_block_core_navigation', ) ); + + gutenberg_register_module( + '@wordpress/block-library/navigation-block', + '/wp-content/plugins/gutenberg/build/interactivity/navigation.min.js', + 'frontend', + array( + 'version' => defined( 'GUTENBERG_VERSION' ) ? GUTENBERG_VERSION : get_bloginfo( 'version' ), + 'dependencies' => array( '@wordpress/interactivity' ), + ) + ); } add_action( 'init', 'register_block_core_navigation' ); diff --git a/packages/block-library/src/query/block.json b/packages/block-library/src/query/block.json index d30eccf3765792..6189f6f0189b79 100644 --- a/packages/block-library/src/query/block.json +++ b/packages/block-library/src/query/block.json @@ -52,6 +52,5 @@ "layout": true }, "editorStyle": "wp-block-query-editor", - "style": "wp-block-query", - "viewScript": "file:./view.min.js" + "style": "wp-block-query" } diff --git a/packages/block-library/src/query/index.php b/packages/block-library/src/query/index.php index b6a5733632ff44..d96058c98c5e05 100644 --- a/packages/block-library/src/query/index.php +++ b/packages/block-library/src/query/index.php @@ -18,6 +18,8 @@ */ function render_block_core_query( $attributes, $content, $block ) { if ( $attributes['enhancedPagination'] && isset( $attributes['queryId'] ) ) { + gutenberg_enqueue_module( '@wordpress/block-library/query' ); + $p = new WP_HTML_Tag_Processor( $content ); if ( $p->next_tag() ) { // Add the necessary directives. @@ -67,22 +69,6 @@ class="wp-block-query__enhanced-pagination-animation" } } - $view_asset = 'wp-block-query-view'; - if ( ! wp_script_is( $view_asset ) ) { - $script_handles = $block->block_type->view_script_handles; - // If the script is not needed, and it is still in the `view_script_handles`, remove it. - if ( - ( ! $attributes['enhancedPagination'] || ! isset( $attributes['queryId'] ) ) - && in_array( $view_asset, $script_handles, true ) - ) { - $block->block_type->view_script_handles = array_diff( $script_handles, array( $view_asset ) ); - } - // If the script is needed, but it was previously removed, add it again. - if ( $attributes['enhancedPagination'] && isset( $attributes['queryId'] ) && ! in_array( $view_asset, $script_handles, true ) ) { - $block->block_type->view_script_handles = array_merge( $script_handles, array( $view_asset ) ); - } - } - $style_asset = 'wp-block-query'; if ( ! wp_style_is( $style_asset ) ) { $style_handles = $block->block_type->style_handles; @@ -131,6 +117,16 @@ function register_block_core_query() { 'render_callback' => 'render_block_core_query', ) ); + + gutenberg_register_module( + '@wordpress/block-library/query', + '/wp-content/plugins/gutenberg/build/interactivity/query.min.js', + 'frontend', + array( + 'version' => defined( 'GUTENBERG_VERSION' ) ? GUTENBERG_VERSION : get_bloginfo( 'version' ), + 'dependencies' => array( '@wordpress/interactivity' ), + ) + ); } add_action( 'init', 'register_block_core_query' ); diff --git a/packages/block-library/src/search/block.json b/packages/block-library/src/search/block.json index 5669a9089d0e03..93018c2c29f4cc 100644 --- a/packages/block-library/src/search/block.json +++ b/packages/block-library/src/search/block.json @@ -91,7 +91,6 @@ }, "html": false }, - "viewScript": "file:./view.min.js", "editorStyle": "wp-block-search-editor", "style": "wp-block-search" } diff --git a/packages/block-library/src/search/index.php b/packages/block-library/src/search/index.php index f00ecfe6abe1cc..6d46635e186c3b 100644 --- a/packages/block-library/src/search/index.php +++ b/packages/block-library/src/search/index.php @@ -85,21 +85,9 @@ function render_block_core_search( $attributes, $content, $block ) { // Adding these attributes manually is needed until the Interactivity API SSR logic is added to core. $input->set_attribute( 'aria-hidden', 'true' ); $input->set_attribute( 'tabindex', '-1' ); - } - - // If the script already exists, there is no point in removing it from viewScript. - $view_js_file = 'wp-block-search-view'; - if ( ! wp_script_is( $view_js_file ) ) { - $script_handles = $block->block_type->view_script_handles; - // If the script is not needed, and it is still in the `view_script_handles`, remove it. - if ( ! $is_expandable_searchfield && in_array( $view_js_file, $script_handles, true ) ) { - $block->block_type->view_script_handles = array_diff( $script_handles, array( $view_js_file ) ); - } - // If the script is needed, but it was previously removed, add it again. - if ( $is_expandable_searchfield && ! in_array( $view_js_file, $script_handles, true ) ) { - $block->block_type->view_script_handles = array_merge( $script_handles, array( $view_js_file ) ); - } + // Load the module. + gutenberg_enqueue_module( '@wordpress/block-library/search-block' ); } } @@ -203,27 +191,18 @@ function register_block_core_search() { 'render_callback' => 'render_block_core_search', ) ); -} -add_action( 'init', 'register_block_core_search' ); -/** - * Ensure that the view script has the `wp-interactivity` dependency. - * - * @since 6.4.0 - * - * @global WP_Scripts $wp_scripts - */ -function block_core_search_ensure_interactivity_dependency() { - global $wp_scripts; - if ( - isset( $wp_scripts->registered['wp-block-search-view'] ) && - ! in_array( 'wp-interactivity', $wp_scripts->registered['wp-block-search-view']->deps, true ) - ) { - $wp_scripts->registered['wp-block-search-view']->deps[] = 'wp-interactivity'; - } + gutenberg_register_module( + '@wordpress/block-library/search-block', + '/wp-content/plugins/gutenberg/build/interactivity/search.min.js', + 'frontend', + array( + 'version' => defined( 'GUTENBERG_VERSION' ) ? GUTENBERG_VERSION : get_bloginfo( 'version' ), + 'dependencies' => array( '@wordpress/interactivity' ), + ) + ); } - -add_action( 'wp_print_scripts', 'block_core_search_ensure_interactivity_dependency' ); +add_action( 'init', 'register_block_core_search' ); /** * Builds the correct top level classnames for the 'core/search' block. diff --git a/tools/webpack/interactivity.js b/tools/webpack/interactivity.js index 26e49966ad40c3..6f43caafee2272 100644 --- a/tools/webpack/interactivity.js +++ b/tools/webpack/interactivity.js @@ -12,18 +12,28 @@ module.exports = { ...baseConfig, name: 'interactivity', entry: { - index: { - import: `./packages/interactivity/src/index.js`, - library: { - name: [ 'wp', 'interactivity' ], - type: 'window', - }, - }, + index: `./packages/interactivity/src/index.js`, + navigation: './packages/block-library/src/navigation/view.js', + query: './packages/block-library/src/query/view.js', + image: './packages/block-library/src/image/view.js', + file: './packages/block-library/src/file/view.js', + search: './packages/block-library/src/search/view.js', + }, + experiments: { + outputModule: true, }, output: { devtoolNamespace: 'wp', filename: './build/interactivity/[name].min.js', + library: { + type: 'module', + }, path: join( __dirname, '..', '..' ), + environment: { module: true }, + }, + externalsType: 'module', + externals: { + '@wordpress/interactivity': '@wordpress/interactivity', }, module: { rules: [