diff --git a/lib/blocks.php b/lib/blocks.php index 320d50bf509500..8e2b25715f1099 100644 --- a/lib/blocks.php +++ b/lib/blocks.php @@ -593,3 +593,103 @@ function gutenberg_multiple_block_styles( $metadata ) { return $metadata; } add_filter( 'block_type_metadata', 'gutenberg_multiple_block_styles' ); + +if ( ! function_exists( 'wp_enqueue_block_view_script' ) ) { + /** + * Enqueue a frontend script for a specific block. + * + * Scripts enqueued using this function will only get printed + * when the block gets rendered on the frontend. + * + * @param string $block_name The block-name, including namespace. + * @param array $args An array of arguments [handle,src,deps,ver,media]. + * + * @return void + */ + function wp_enqueue_block_view_script( $block_name, $args ) { + $args = wp_parse_args( + $args, + array( + 'handle' => '', + 'src' => '', + 'deps' => array(), + 'ver' => false, + 'in_footer' => false, + + // Additional arg to allow translations for the script's textdomain. + 'textdomain' => '', + ) + ); + + /** + * Callback function to register and enqueue scripts. + * + * @param string $content When the callback is used for the render_block filter, + * the content needs to be returned so the function parameter + * is to ensure the content exists. + * @return string Block content. + */ + $callback = static function( $content, $block ) use ( $args, $block_name ) { + + // Sanity check. + if ( empty( $block['blockName'] ) || $block_name !== $block['blockName'] ) { + return $content; + } + + // Register the stylesheet. + if ( ! empty( $args['src'] ) ) { + wp_register_script( $args['handle'], $args['src'], $args['deps'], $args['ver'], $args['in_footer'] ); + } + + // Enqueue the stylesheet. + wp_enqueue_script( $args['handle'] ); + + // If a textdomain is defined, use it to set the script translations. + if ( ! empty( $args['textdomain'] ) && in_array( 'wp-i18n', $args['deps'], true ) ) { + wp_set_script_translations( $args['handle'], $args['textdomain'] ); + } + + return $content; + }; + + /* + * The filter's callback here is an anonymous function because + * using a named function in this case is not possible. + * + * The function cannot be unhooked, however, users are still able + * to dequeue the script registered/enqueued by the callback + * which is why in this case, using an anonymous function + * was deemed acceptable. + */ + add_filter( 'render_block', $callback, 10, 2 ); + } +} + +/** + * Allow multiple view scripts per block. + * + * Filters the metadata provided for registering a block type. + * + * @param array $metadata Metadata for registering a block type. + * + * @return array + */ +function gutenberg_block_type_metadata_multiple_view_scripts( $metadata ) { + + // Early return if viewScript is empty, or not an array. + if ( ! isset( $metadata['viewScript'] ) || ! is_array( $metadata['viewScript'] ) ) { + return $metadata; + } + + // Register all viewScript items. + foreach ( $metadata['viewScript'] as $view_script ) { + $item_metadata = $metadata; + $item_metadata['viewScript'] = $view_script; + gutenberg_block_type_metadata_view_script( array(), $item_metadata ); + } + + // Proceed with the default behavior. + $metadata['viewScript'] = $metadata['viewScript'][0]; + return $metadata; +} +add_filter( 'block_type_metadata', 'gutenberg_block_type_metadata_multiple_view_scripts' ); diff --git a/lib/compat/wordpress-6.0/blocks.php b/lib/compat/wordpress-6.0/blocks.php index 40108be343ec99..842dd50b7f6107 100644 --- a/lib/compat/wordpress-6.0/blocks.php +++ b/lib/compat/wordpress-6.0/blocks.php @@ -122,3 +122,54 @@ function gutenberg_build_query_vars_from_query_block( $block, $page ) { } return $query; } + +/** + * Registers view scripts for core blocks if handling is missing in WordPress core. + * + * This is a temporary solution until the Gutenberg plugin sets + * the required WordPress version to 6.0. + * + * @param array $settings Array of determined settings for registering a block type. + * @param array $metadata Metadata provided for registering a block type. + * + * @return array Array of settings for registering a block type. + */ +function gutenberg_block_type_metadata_view_script( $settings, $metadata ) { + if ( + ! isset( $metadata['viewScript'] ) || + ! empty( $settings['view_script'] ) || + ! isset( $metadata['file'] ) || + strpos( $metadata['file'], gutenberg_dir_path() ) !== 0 + ) { + return $settings; + } + + $view_script_path = realpath( dirname( $metadata['file'] ) . '/' . remove_block_asset_path_prefix( $metadata['viewScript'] ) ); + + if ( file_exists( $view_script_path ) ) { + $view_script_id = str_replace( array( '.min.js', '.js' ), '', basename( remove_block_asset_path_prefix( $metadata['viewScript'] ) ) ); + $view_script_handle = str_replace( 'core/', 'wp-block-', $metadata['name'] ) . '-' . $view_script_id; + wp_deregister_script( $view_script_handle ); + + // Replace suffix and extension with `.asset.php` to find the generated dependencies file. + $view_asset_file = substr( $view_script_path, 0, -( strlen( '.js' ) ) ) . '.asset.php'; + $view_asset = file_exists( $view_asset_file ) ? require( $view_asset_file ) : null; + $view_script_dependencies = isset( $view_asset['dependencies'] ) ? $view_asset['dependencies'] : array(); + $view_script_version = isset( $view_asset['version'] ) ? $view_asset['version'] : false; + $result = wp_register_script( + $view_script_handle, + gutenberg_url( str_replace( gutenberg_dir_path(), '', $view_script_path ) ), + $view_script_dependencies, + $view_script_version + ); + if ( $result ) { + $settings['view_script'] = $view_script_handle; + + if ( ! empty( $metadata['textdomain'] ) && in_array( 'wp-i18n', $view_script_dependencies, true ) ) { + wp_set_script_translations( $view_script_handle, $metadata['textdomain'] ); + } + } + } + return $settings; +} +add_filter( 'block_type_metadata_settings', 'gutenberg_block_type_metadata_view_script', 10, 2 ); diff --git a/packages/block-library/README.md b/packages/block-library/README.md index 92cf2c6a351843..45f923af9f8e3e 100644 --- a/packages/block-library/README.md +++ b/packages/block-library/README.md @@ -14,7 +14,7 @@ _This package assumes that your code will run in an **ES2015+** environment. If ## Building JavaScript for the browser -If a `view.js` file is present in the block's directory, this file will be built along other assets, making it available to load from the browser. +If a `view.js` file (or a file prefixed with `view`, e.g. `view-example.js`) is present in the block's directory, this file will be built along other assets, making it available to load from the browser. This enables us to, for instance, load this file when the block is present on the page in two ways: diff --git a/schemas/json/block.json b/schemas/json/block.json index 99217d43c376fc..43e24b28f91ee0 100644 --- a/schemas/json/block.json +++ b/schemas/json/block.json @@ -421,8 +421,18 @@ "description": "Block type frontend and editor script definition. It will be enqueued both in the editor and when viewing the content on the front of the site." }, "viewScript": { - "type": "string", - "description": "Block type frontend script definition. It will be enqueued only when viewing the content on the front of the site." + "description": "Block type frontend script definition. It will be enqueued only when viewing the content on the front of the site.", + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] }, "editorStyle": { "description": "Block type editor style definition. It will only be enqueued in the context of the editor.", diff --git a/tools/webpack/blocks.js b/tools/webpack/blocks.js index 11d4635af96324..c9a5af0b631ce1 100644 --- a/tools/webpack/blocks.js +++ b/tools/webpack/blocks.js @@ -17,10 +17,11 @@ const DependencyExtractionWebpackPlugin = require( '@wordpress/dependency-extrac const { baseConfig, plugins, stylesTransform } = require( './shared' ); /* - * Matches a block's name in paths in the form - * build-module//view.js + * Matches a block's filepaths in the form build-module/.js */ -const blockNameRegex = new RegExp( /(?<=build-module\/).*(?=(\/view))/g ); +const blockViewRegex = new RegExp( + /build-module\/(?.*\/view.*).js$/ +); /** * We need to automatically rename some functions when they are called inside block files, @@ -31,14 +32,14 @@ const prefixFunctions = [ 'build_query_vars_from_query_block' ]; const createEntrypoints = () => { /* - * Returns an array of paths to view.js files within the `@wordpress/block-library` package. - * These paths can be matched by the regex `blockNameRegex` in order to extract - * the block's name. + * Returns an array of paths to block view files within the `@wordpress/block-library` package. + * These paths can be matched by the regex `blockViewRegex` in order to extract + * the block's filename. * * Returns an empty array if no files were found. */ const blockViewScriptPaths = fastGlob.sync( - './packages/block-library/build-module/**/view.js' + './packages/block-library/build-module/**/view*.js' ); /* @@ -46,11 +47,14 @@ const createEntrypoints = () => { * each block's view.js file. */ return blockViewScriptPaths.reduce( ( entries, scriptPath ) => { - const [ blockName ] = scriptPath.match( blockNameRegex ); + const result = scriptPath.match( blockViewRegex ); + if ( ! result?.groups?.filename ) { + return entries; + } return { ...entries, - [ 'blocks/' + blockName ]: scriptPath, + [ result.groups.filename ]: scriptPath, }; }, {} ); }; @@ -61,7 +65,7 @@ module.exports = { entry: createEntrypoints(), output: { devtoolNamespace: 'wp', - filename: './build/block-library/[name]/view.min.js', + filename: './build/block-library/blocks/[name].min.js', path: join( __dirname, '..', '..' ), }, plugins: [