diff --git a/.travis.yml b/.travis.yml index 32e41294c57f9..db9acbf2d2174 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,6 +21,7 @@ branches: only: - master - rnmobile/master + - rnmobile/releases - /wp\/.*/ env: diff --git a/README.md b/README.md index e6b4565775def..c11f2a71a5650 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Build Status](https://img.shields.io/travis/com/WordPress/gutenberg/master.svg)](https://travis-ci.com/WordPress/gutenberg) [![lerna](https://img.shields.io/badge/maintained%20with-lerna-cc00ff.svg)](https://lerna.js.org) -![Screenshot of the Gutenberg Editor, editing a post in WordPress](https://cldup.com/H0oKBfpidk.png) +![Screenshot of the Gutenberg Editor, editing a post in WordPress](https://cldup.com/R84R5fNgrI.png) This repo is the development hub for the editor focus in WordPress Core. `Gutenberg` is the project name. @@ -32,7 +32,6 @@ Here's why we're looking at the whole editing screen, as opposed to just the con 4. **A fresh look at content creation.** Considering the whole interface lays a solid foundation for the next focus: full site customization. 5. **Modern tooling.** Looking at the full editor screen also gives WordPress the opportunity to drastically modernize the foundation, and take steps towards a more fluid and JavaScript-powered future that fully leverages the WordPress REST API. -![Writing in Gutenberg 1.6](https://make.wordpress.org/core/files/2017/10/gutenberg-typing-1_6.gif) ## Blocks diff --git a/bin/commander.js b/bin/commander.js index dcc863872f034..95cfc56b25efd 100755 --- a/bin/commander.js +++ b/bin/commander.js @@ -17,7 +17,7 @@ const uuid = require( 'uuid/v4' ); // Config const gitRepoOwner = 'WordPress'; -const gitRepoURL = 'git@github.com:' + gitRepoOwner + '/gutenberg.git'; +const gitRepoURL = 'https://github.com/' + gitRepoOwner + '/gutenberg.git'; const svnRepoURL = 'https://plugins.svn.wordpress.org/gutenberg'; // Working Directories @@ -95,6 +95,7 @@ function runShellScript( script, cwd ) { env: { NO_CHECKS: true, PATH: process.env.PATH, + HOME: process.env.HOME, }, stdio: [ 'inherit', 'ignore', 'inherit' ], } ); diff --git a/bin/get-vendor-scripts.php b/bin/get-vendor-scripts.php index 442e77b0eb435..e7295f556b4cb 100755 --- a/bin/get-vendor-scripts.php +++ b/bin/get-vendor-scripts.php @@ -32,4 +32,13 @@ function wp_add_inline_script() {} require_once dirname( dirname( __FILE__ ) ) . '/lib/client-assets.php'; -gutenberg_register_vendor_scripts(); +/** + * Hi, phpcs + */ +function run_gutenberg_register_vendor_scripts() { + global $wp_scripts; + + gutenberg_register_vendor_scripts( $wp_scripts ); +} + +run_gutenberg_register_vendor_scripts(); diff --git a/docs/designers-developers/developers/block-api/block-registration.md b/docs/designers-developers/developers/block-api/block-registration.md index 6cc028ab8c94f..217bbd27608ac 100644 --- a/docs/designers-developers/developers/block-api/block-registration.md +++ b/docs/designers-developers/developers/block-api/block-registration.md @@ -1,6 +1,6 @@ # Block Registration -## `register_block_type` +## `registerBlockType` * **Type:** `Function` diff --git a/docs/designers-developers/developers/themes/theme-support.md b/docs/designers-developers/developers/themes/theme-support.md index 210072f5d2cf0..62d650eb5aa47 100644 --- a/docs/designers-developers/developers/themes/theme-support.md +++ b/docs/designers-developers/developers/themes/theme-support.md @@ -134,6 +134,46 @@ Themes are responsible for creating the classes that apply the colors in differe The class name is built appending 'has-', followed by the class name _using_ kebab case and ending with the context name. +### Block Gradient Presents + +Different blocks have the possibility of selecting from a list of predined of gradients. The block editor provides a default gradient presets, but a theme can overwrite them and provide its own: + +```php +add_theme_support( + '__experimental-editor-gradient-presets', + array( + array( + 'name' => __( 'Vivid cyan blue to vivid purple', 'themeLangDomain' ), + 'gradient' => 'linear-gradient(135deg,rgba(6,147,227,1) 0%,rgb(155,81,224) 100%)', + 'slug' => 'vivid-cyan-blue-to-vivid-purple' + ), + array( + 'name' => __( 'Vivid green cyan to vivid cyan blue', 'themeLangDomain' ), + 'gradient' => 'linear-gradient(135deg,rgba(0,208,132,1) 0%,rgba(6,147,227,1) 100%)', + 'slug' => 'vivid-green-cyan-to-vivid-cyan-blue', + ), + array( + 'name' => __( 'Light green cyan to vivid green cyan', 'themeLangDomain' ), + 'gradient' => 'linear-gradient(135deg,rgb(122,220,180) 0%,rgb(0,208,130) 100%)', + 'slug' => 'light-green-cyan-to-vivid-green-cyan', + ), + array( + 'name' => __( 'Luminous vivid amber to luminous vivid orange', 'themeLangDomain' ), + 'gradient' => 'linear-gradient(135deg,rgba(252,185,0,1) 0%,rgba(255,105,0,1) 100%)', + 'slug' => 'luminous-vivid-amber-to-luminous-vivid-orange', + ), + array( + 'name' => __( 'Luminous vivid orange to vivid red', 'themeLangDomain' ), + 'gradient' => 'linear-gradient(135deg,rgba(255,105,0,1) 0%,rgb(207,46,46) 100%)', + 'slug' => 'luminous-vivid-orange-to-vivid-red', + ), + ) +); +``` + +`name` is a human-readable label (demonstrated above) that appears in the tooltip and provides a meaningful description of the gradient to users. It is especially important for those who rely on screen readers or would otherwise have difficulty perceiving the color. `gradient` is a CSS value of a gradient applied to a background-image of the block. Details of valid gradient types can be found in the [mozilla documentation](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Images/Using_CSS_gradients). `slug` is a unique identifier for the gradient and is used to generate the CSS classes used by the block editor. + + ### Block Font Sizes: Blocks may allow the user to configure the font sizes they use, e.g., the paragraph block. The block provides a default set of font sizes, but a theme can overwrite it and provide its own: diff --git a/gutenberg.php b/gutenberg.php index 9e80dee2f6a1a..76bf483c8f74b 100644 --- a/gutenberg.php +++ b/gutenberg.php @@ -3,7 +3,7 @@ * Plugin Name: Gutenberg * Plugin URI: https://github.com/WordPress/gutenberg * Description: Printing since 1440. This is the development plugin for the new block editor in core. - * Version: 6.8.0-rc.1 + * Version: 6.8.0 * Author: Gutenberg Team * Text Domain: gutenberg * diff --git a/lib/block-directory.php b/lib/block-directory.php new file mode 100644 index 0000000000000..723db66de2b3c --- /dev/null +++ b/lib/block-directory.php @@ -0,0 +1,21 @@ + instead of in the . * Default 'false'. */ -function gutenberg_override_script( $handle, $src, $deps = array(), $ver = false, $in_footer = false ) { - global $wp_scripts; - - $script = $wp_scripts->query( $handle, 'registered' ); +function gutenberg_override_script( &$scripts, $handle, $src, $deps = array(), $ver = false, $in_footer = false ) { + $script = $scripts->query( $handle, 'registered' ); if ( $script ) { /* * In many ways, this is a reimplementation of `wp_register_script` but @@ -67,6 +66,7 @@ function gutenberg_override_script( $handle, $src, $deps = array(), $ver = false $script->src = $src; $script->deps = $deps; $script->ver = $ver; + $script->args = $in_footer; /* * The script's `group` designation is an indication of whether it is @@ -81,7 +81,7 @@ function gutenberg_override_script( $handle, $src, $deps = array(), $ver = false $script->add_data( 'group', 1 ); } } else { - wp_register_script( $handle, $src, $deps, $ver, $in_footer ); + $scripts->add( $handle, $src, $deps, $ver, $in_footer ); } /* @@ -93,7 +93,7 @@ function gutenberg_override_script( $handle, $src, $deps = array(), $ver = false * See: https://core.trac.wordpress.org/ticket/46089 */ if ( 'wp-i18n' !== $handle && 'wp-polyfill' !== $handle ) { - wp_set_script_translations( $handle, 'default' ); + $scripts->set_translations( $handle, 'default' ); } } @@ -155,6 +155,7 @@ function gutenberg_override_translation_file( $file, $handle ) { * * @since 4.1.0 * + * @param WP_Styles $styles WP_Styles instance (passed by reference). * @param string $handle Name of the stylesheet. Should be unique. * @param string $src Full URL of the stylesheet, or path of the stylesheet relative to the WordPress root directory. * @param array $deps Optional. An array of registered stylesheet handles this stylesheet depends on. Default empty array. @@ -166,18 +167,69 @@ function gutenberg_override_translation_file( $file, $handle ) { * Default 'all'. Accepts media types like 'all', 'print' and 'screen', or media queries like * '(orientation: portrait)' and '(max-width: 640px)'. */ -function gutenberg_override_style( $handle, $src, $deps = array(), $ver = false, $media = 'all' ) { - wp_deregister_style( $handle ); - wp_register_style( $handle, $src, $deps, $ver, $media ); +function gutenberg_override_style( &$styles, $handle, $src, $deps = array(), $ver = false, $media = 'all' ) { + $style = $styles->query( $handle, 'registered' ); + if ( $style ) { + $styles->remove( $handle ); + } + $styles->add( $handle, $src, $deps, $ver, $media ); } +/** + * Registers vendor JavaScript files to be used as dependencies of the editor + * and plugins. + * + * This function is called from a script during the plugin build process, so it + * should not call any WordPress PHP functions. + * + * @since 0.1.0 + * + * @param WP_Scripts $scripts WP_Scripts instance (passed by reference). + */ +function gutenberg_register_vendor_scripts( &$scripts ) { + $suffix = SCRIPT_DEBUG ? '' : '.min'; + + // Vendor Scripts. + $react_suffix = ( SCRIPT_DEBUG ? '.development' : '.production' ) . $suffix; + + // TODO: Overrides for react, react-dom and lodash are necessary + // until WordPress 5.3 is released. + gutenberg_register_vendor_script( + $scripts, + 'react', + 'https://unpkg.com/react@16.9.0/umd/react' . $react_suffix . '.js', + array( 'wp-polyfill' ), + '16.9.0', + true + ); + gutenberg_register_vendor_script( + $scripts, + 'react-dom', + 'https://unpkg.com/react-dom@16.9.0/umd/react-dom' . $react_suffix . '.js', + array( 'react' ), + '16.9.0', + true + ); + gutenberg_register_vendor_script( + $scripts, + 'lodash', + 'https://unpkg.com/lodash@4.17.15/lodash' . $suffix . '.js', + array(), + '4.17.15', + true + ); +} +add_action( 'wp_default_scripts', 'gutenberg_register_vendor_scripts' ); + /** * Registers all the WordPress packages scripts that are in the standardized * `build/` location. * * @since 4.5.0 + * + * @param WP_Scripts $scripts WP_Scripts instance (passed by reference). */ -function gutenberg_register_packages_scripts() { +function gutenberg_register_packages_scripts( &$scripts ) { foreach ( glob( gutenberg_dir_path() . 'build/*/index.js' ) as $path ) { // Prefix `wp-` to package directory to get script handle. // For example, `…/build/a11y/index.js` becomes `wp-a11y`. @@ -206,6 +258,7 @@ function gutenberg_register_packages_scripts() { $gutenberg_path = substr( $path, strlen( gutenberg_dir_path() ) ); gutenberg_override_script( + $scripts, $handle, gutenberg_url( $gutenberg_path ), $dependencies, @@ -214,117 +267,74 @@ function gutenberg_register_packages_scripts() { ); } } +add_action( 'wp_default_scripts', 'gutenberg_register_packages_scripts' ); /** - * Registers common scripts and styles to be used as dependencies of the editor - * and plugins. + * Registers all the WordPress packages styles that are in the standardized + * `build/` location. * - * @since 0.1.0 - */ -function gutenberg_register_scripts_and_styles() { - global $wp_scripts; - - gutenberg_register_vendor_scripts(); - gutenberg_register_packages_scripts(); - - // Add nonce middleware which accounts for the absence of the heartbeat - // listener. This relies on API Fetch implementation running middlewares in - // order of last added, and that the original nonce middleware would defer - // to an X-WP-Nonce header already being present. This inline script should - // be removed once the following Core ticket is resolved in assigning the - // nonce received from heartbeat to the created middleware. - // - // See: https://core.trac.wordpress.org/ticket/46107 . - // See: https://github.com/WordPress/gutenberg/pull/13451 . - if ( isset( $wp_scripts->registered['wp-api-fetch'] ) ) { - $wp_scripts->registered['wp-api-fetch']->deps[] = 'wp-hooks'; - } - wp_add_inline_script( - 'wp-api-fetch', - sprintf( - 'wp.apiFetch.nonceMiddleware = wp.apiFetch.createNonceMiddleware( "%s" );' . - 'wp.apiFetch.use( wp.apiFetch.nonceMiddleware );' . - 'wp.apiFetch.nonceEndpoint = "%s";' . - 'wp.apiFetch.use( wp.apiFetch.mediaUploadMiddleware );', - ( wp_installing() && ! is_multisite() ) ? '' : wp_create_nonce( 'wp_rest' ), - admin_url( 'admin-ajax.php?action=gutenberg_rest_nonce' ) - ), - 'after' - ); - - // TEMPORARY: Core does not (yet) provide persistence migration from the - // introduction of the block editor and still calls the data plugins. - // We unset the existing inline scripts first. - $wp_scripts->registered['wp-data']->extra['after'] = array(); - wp_add_inline_script( - 'wp-data', - implode( - "\n", - array( - '( function() {', - ' var userId = ' . get_current_user_ID() . ';', - ' var storageKey = "WP_DATA_USER_" + userId;', - ' wp.data', - ' .use( wp.data.plugins.persistence, { storageKey: storageKey } );', - ' wp.data.plugins.persistence.__unstableMigrate( { storageKey: storageKey } );', - '} )();', - ) - ) - ); + * @since 6.7.0 + * @param WP_Styles $styles WP_Styles instance (passed by reference). + */ +function gutenberg_register_packages_styles( &$styles ) { // Editor Styles. - // This empty stylesheet is defined to ensure backward compatibility. - gutenberg_override_style( 'wp-blocks', false ); - gutenberg_override_style( + $styles, 'wp-block-editor', gutenberg_url( 'build/block-editor/style.css' ), array( 'wp-components', 'wp-editor-font' ), filemtime( gutenberg_dir_path() . 'build/editor/style.css' ) ); - wp_style_add_data( 'wp-block-editor', 'rtl', 'replace' ); + $styles->add_data( 'wp-block-editor', 'rtl', 'replace' ); gutenberg_override_style( + $styles, 'wp-editor', gutenberg_url( 'build/editor/style.css' ), - array( 'wp-components', 'wp-block-editor', 'wp-nux', 'wp-block-directory' ), + array( 'wp-components', 'wp-block-editor', 'wp-nux' ), filemtime( gutenberg_dir_path() . 'build/editor/style.css' ) ); - wp_style_add_data( 'wp-editor', 'rtl', 'replace' ); + $styles->add_data( 'wp-editor', 'rtl', 'replace' ); gutenberg_override_style( + $styles, 'wp-edit-post', gutenberg_url( 'build/edit-post/style.css' ), array( 'wp-components', 'wp-block-editor', 'wp-editor', 'wp-edit-blocks', 'wp-block-library', 'wp-nux' ), filemtime( gutenberg_dir_path() . 'build/edit-post/style.css' ) ); - wp_style_add_data( 'wp-edit-post', 'rtl', 'replace' ); + $styles->add_data( 'wp-edit-post', 'rtl', 'replace' ); gutenberg_override_style( + $styles, 'wp-components', gutenberg_url( 'build/components/style.css' ), array(), filemtime( gutenberg_dir_path() . 'build/components/style.css' ) ); - wp_style_add_data( 'wp-components', 'rtl', 'replace' ); + $styles->add_data( 'wp-components', 'rtl', 'replace' ); gutenberg_override_style( + $styles, 'wp-block-library', gutenberg_url( 'build/block-library/style.css' ), array(), filemtime( gutenberg_dir_path() . 'build/block-library/style.css' ) ); - wp_style_add_data( 'wp-block-library', 'rtl', 'replace' ); + $styles->add_data( 'wp-block-library', 'rtl', 'replace' ); gutenberg_override_style( + $styles, 'wp-format-library', gutenberg_url( 'build/format-library/style.css' ), array( 'wp-block-editor', 'wp-components' ), filemtime( gutenberg_dir_path() . 'build/format-library/style.css' ) ); - wp_style_add_data( 'wp-format-library', 'rtl', 'replace' ); + $styles->add_data( 'wp-format-library', 'rtl', 'replace' ); gutenberg_override_style( + $styles, 'wp-edit-blocks', gutenberg_url( 'build/block-library/editor.css' ), array( @@ -336,92 +346,107 @@ function gutenberg_register_scripts_and_styles() { ), filemtime( gutenberg_dir_path() . 'build/block-library/editor.css' ) ); - wp_style_add_data( 'wp-edit-blocks', 'rtl', 'replace' ); + $styles->add_data( 'wp-edit-blocks', 'rtl', 'replace' ); gutenberg_override_style( + $styles, 'wp-nux', gutenberg_url( 'build/nux/style.css' ), array( 'wp-components' ), filemtime( gutenberg_dir_path() . 'build/nux/style.css' ) ); - wp_style_add_data( 'wp-nux', 'rtl', 'replace' ); + $styles->add_data( 'wp-nux', 'rtl', 'replace' ); gutenberg_override_style( + $styles, 'wp-block-library-theme', gutenberg_url( 'build/block-library/theme.css' ), array(), filemtime( gutenberg_dir_path() . 'build/block-library/theme.css' ) ); - wp_style_add_data( 'wp-block-library-theme', 'rtl', 'replace' ); + $styles->add_data( 'wp-block-library-theme', 'rtl', 'replace' ); gutenberg_override_style( + $styles, 'wp-list-reusable-blocks', gutenberg_url( 'build/list-reusable-blocks/style.css' ), array( 'wp-components' ), filemtime( gutenberg_dir_path() . 'build/list-reusable-blocks/style.css' ) ); - wp_style_add_data( 'wp-list-reusable-block', 'rtl', 'replace' ); + $styles->add_data( 'wp-list-reusable-block', 'rtl', 'replace' ); gutenberg_override_style( + $styles, 'wp-edit-widgets', gutenberg_url( 'build/edit-widgets/style.css' ), array( 'wp-components', 'wp-block-editor', 'wp-edit-blocks' ), filemtime( gutenberg_dir_path() . 'build/edit-widgets/style.css' ) ); - wp_style_add_data( 'wp-edit-widgets', 'rtl', 'replace' ); + $styles->add_data( 'wp-edit-widgets', 'rtl', 'replace' ); gutenberg_override_style( + $styles, 'wp-block-directory', gutenberg_url( 'build/block-directory/style.css' ), - array( 'wp-components' ), + array( 'wp-block-editor', 'wp-components' ), filemtime( gutenberg_dir_path() . 'build/block-directory/style.css' ) ); - wp_style_add_data( 'wp-block-directory', 'rtl', 'replace' ); - - if ( defined( 'GUTENBERG_LIVE_RELOAD' ) && GUTENBERG_LIVE_RELOAD ) { - $live_reload_url = ( GUTENBERG_LIVE_RELOAD === true ) ? 'http://localhost:35729/livereload.js' : GUTENBERG_LIVE_RELOAD; - - wp_enqueue_script( - 'gutenberg-live-reload', - $live_reload_url - ); - } + $styles->add_data( 'wp-block-directory', 'rtl', 'replace' ); } -add_action( 'wp_enqueue_scripts', 'gutenberg_register_scripts_and_styles', 5 ); -add_action( 'admin_enqueue_scripts', 'gutenberg_register_scripts_and_styles', 5 ); +add_action( 'wp_default_styles', 'gutenberg_register_packages_styles' ); /** - * Registers vendor JavaScript files to be used as dependencies of the editor + * Registers common scripts and styles to be used as dependencies of the editor * and plugins. * - * This function is called from a script during the plugin build process, so it - * should not call any WordPress PHP functions. - * * @since 0.1.0 */ -function gutenberg_register_vendor_scripts() { - $suffix = SCRIPT_DEBUG ? '' : '.min'; - - // Vendor Scripts. - $react_suffix = ( SCRIPT_DEBUG ? '.development' : '.production' ) . $suffix; +function gutenberg_enqueue_block_editor_assets() { + global $wp_scripts; - // TODO: Overrides for react, react-dom and lodash are necessary - // until WordPress 5.3 is released. - gutenberg_register_vendor_script( - 'react', - 'https://unpkg.com/react@16.9.0/umd/react' . $react_suffix . '.js', - array( 'wp-polyfill' ) - ); - gutenberg_register_vendor_script( - 'react-dom', - 'https://unpkg.com/react-dom@16.9.0/umd/react-dom' . $react_suffix . '.js', - array( 'react' ) + wp_add_inline_script( + 'wp-api-fetch', + sprintf( + 'wp.apiFetch.nonceMiddleware = wp.apiFetch.createNonceMiddleware( "%s" );' . + 'wp.apiFetch.use( wp.apiFetch.nonceMiddleware );' . + 'wp.apiFetch.nonceEndpoint = "%s";' . + 'wp.apiFetch.use( wp.apiFetch.mediaUploadMiddleware );', + ( wp_installing() && ! is_multisite() ) ? '' : wp_create_nonce( 'wp_rest' ), + admin_url( 'admin-ajax.php?action=gutenberg_rest_nonce' ) + ), + 'after' ); - gutenberg_register_vendor_script( - 'lodash', - 'https://unpkg.com/lodash@4.17.15/lodash' . $suffix . '.js' + + // TEMPORARY: Core does not (yet) provide persistence migration from the + // introduction of the block editor and still calls the data plugins. + // We unset the existing inline scripts first. + $wp_scripts->registered['wp-data']->extra['after'] = array(); + wp_add_inline_script( + 'wp-data', + implode( + "\n", + array( + '( function() {', + ' var userId = ' . get_current_user_ID() . ';', + ' var storageKey = "WP_DATA_USER_" + userId;', + ' wp.data', + ' .use( wp.data.plugins.persistence, { storageKey: storageKey } );', + ' wp.data.plugins.persistence.__unstableMigrate( { storageKey: storageKey } );', + '} )();', + ) + ) ); + + if ( defined( 'GUTENBERG_LIVE_RELOAD' ) && GUTENBERG_LIVE_RELOAD ) { + $live_reload_url = ( GUTENBERG_LIVE_RELOAD === true ) ? 'http://localhost:35729/livereload.js' : GUTENBERG_LIVE_RELOAD; + + wp_enqueue_script( + 'gutenberg-live-reload', + $live_reload_url + ); + } } +add_action( 'enqueue_block_editor_assets', 'gutenberg_enqueue_block_editor_assets' ); /** * Retrieves a unique and reasonably short and human-friendly filename for a @@ -459,14 +484,21 @@ function gutenberg_vendor_script_filename( $handle, $src ) { * possible, or downloading it if the cached version is unavailable or * outdated. * - * @param string $handle Name of the script. - * @param string $src Full URL of the external script. - * @param array $deps Optional. An array of registered script handles this - * script depends on. + * @param WP_Scripts $scripts WP_Scripts instance (passed by reference). + * @param string $handle Name of the script. + * @param string $src Full URL of the external script. + * @param array $deps Optional. An array of registered script handles this + * script depends on. + * @param string|bool|null $ver Optional. String specifying script version number, if it has one, which 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 set to null, no version is added. + * @param bool $in_footer Optional. Whether to enqueue the script before instead of in the . + * Default 'false'. * * @since 0.1.0 */ -function gutenberg_register_vendor_script( $handle, $src, $deps = array() ) { +function gutenberg_register_vendor_script( &$scripts, $handle, $src, $deps = array(), $ver = null, $in_footer = false ) { if ( defined( 'GUTENBERG_LOAD_VENDOR_SCRIPTS' ) && ! GUTENBERG_LOAD_VENDOR_SCRIPTS ) { return; } @@ -496,7 +528,7 @@ function gutenberg_register_vendor_script( $handle, $src, $deps = array() ) { if ( ! $f ) { // Failed to open the file for writing, probably due to server // permissions. Enqueue the script directly from the URL instead. - gutenberg_override_script( $handle, $src, $deps, null ); + gutenberg_override_script( $scripts, $handle, $src, $deps, $ver, $in_footer ); return; } fclose( $f ); @@ -509,16 +541,18 @@ function gutenberg_register_vendor_script( $handle, $src, $deps = array() ) { // The request failed. If the file is already cached, continue to // use this file. If not, then unlink the 0 byte file, and enqueue // the script directly from the URL. - gutenberg_override_script( $handle, $src, $deps, null ); + gutenberg_override_script( $scripts, $handle, $src, $deps, $ver, $in_footer ); unlink( $full_path ); return; } } gutenberg_override_script( + $scripts, $handle, gutenberg_url( 'vendor/' . $filename ), $deps, - null + $ver, + $in_footer ); } diff --git a/lib/customizer.php b/lib/customizer.php index 771ced523f5c9..1f27db00ee308 100644 --- a/lib/customizer.php +++ b/lib/customizer.php @@ -55,7 +55,7 @@ function gutenberg_customize_register( $wp_customize ) { 'sanitize_callback' => 'gutenberg_customize_sanitize', ) ); - if ( get_option( 'gutenberg-experiments' ) && array_key_exists( 'gutenberg-widget-experiments', get_option( 'gutenberg-experiments' ) ) ) { + if ( gutenberg_is_experiment_enabled( 'gutenberg-widget-experiments' ) ) { $wp_customize->add_section( 'gutenberg_widget_blocks', array( 'title' => __( 'Widget Blocks (Experimental)', 'gutenberg' ) ) diff --git a/lib/experiments-page.php b/lib/experiments-page.php index bbc04650a732e..f88fcca5c1611 100644 --- a/lib/experiments-page.php +++ b/lib/experiments-page.php @@ -130,14 +130,19 @@ function gutenberg_display_experiment_section() { * @return array Filtered editor settings. */ function gutenberg_experiments_editor_settings( $settings ) { - $experiments_exist = get_option( 'gutenberg-experiments' ); $experiments_settings = array( - '__experimentalEnableLegacyWidgetBlock' => $experiments_exist ? array_key_exists( 'gutenberg-widget-experiments', get_option( 'gutenberg-experiments' ) ) : false, - '__experimentalEnableMenuBlock' => $experiments_exist ? array_key_exists( 'gutenberg-menu-block', get_option( 'gutenberg-experiments' ) ) : false, - '__experimentalBlockDirectory' => $experiments_exist ? array_key_exists( 'gutenberg-block-directory', get_option( 'gutenberg-experiments' ) ) : false, - '__experimentalEnableFullSiteEditing' => $experiments_exist ? array_key_exists( 'gutenberg-full-site-editing', get_option( 'gutenberg-experiments' ) ) : false, + '__experimentalEnableLegacyWidgetBlock' => gutenberg_is_experiment_enabled( 'gutenberg-widget-experiments' ), + '__experimentalEnableMenuBlock' => gutenberg_is_experiment_enabled( 'gutenberg-menu-block' ), + '__experimentalBlockDirectory' => gutenberg_is_experiment_enabled( 'gutenberg-block-directory' ), + '__experimentalEnableFullSiteEditing' => gutenberg_is_experiment_enabled( 'gutenberg-full-site-editing' ), ); + + $gradient_presets = current( (array) get_theme_support( '__experimental-editor-gradient-presets' ) ); + if ( false !== $gradient_presets ) { + $experiments_settings['gradients'] = $gradient_presets; + } + return array_merge( $settings, $experiments_settings ); } add_filter( 'block_editor_settings', 'gutenberg_experiments_editor_settings' ); diff --git a/lib/load.php b/lib/load.php index 35b49b2ead5b9..58e545b38a6f7 100644 --- a/lib/load.php +++ b/lib/load.php @@ -9,6 +9,20 @@ die( 'Silence is golden.' ); } +/** + * Checks whether the Gutenberg experiment is enabled. + * + * @since 6.7.0 + * + * @param string $name The name of the experiment. + * + * @return bool True when the experiment is enabled. + */ +function gutenberg_is_experiment_enabled( $name ) { + $experiments = get_option( 'gutenberg-experiments' ); + return ! empty( $experiments[ $name ] ); +} + // These files only need to be loaded if within a rest server instance // which this class will exist if that is the case. if ( class_exists( 'WP_REST_Controller' ) ) { @@ -22,13 +36,12 @@ require dirname( __FILE__ ) . '/class-experimental-wp-widget-blocks-manager.php'; require dirname( __FILE__ ) . '/class-wp-rest-widget-areas-controller.php'; } - /** - * End: Include for phase 2 - */ - if ( ! class_exists( 'WP_REST_Block_Directory_Controller' ) ) { require dirname( __FILE__ ) . '/class-wp-rest-block-directory-controller.php'; } + /** + * End: Include for phase 2 + */ require dirname( __FILE__ ) . '/rest-api.php'; } @@ -43,6 +56,7 @@ require dirname( __FILE__ ) . '/templates.php'; require dirname( __FILE__ ) . '/template-loader.php'; require dirname( __FILE__ ) . '/client-assets.php'; +require dirname( __FILE__ ) . '/block-directory.php'; require dirname( __FILE__ ) . '/demo.php'; require dirname( __FILE__ ) . '/widgets.php'; require dirname( __FILE__ ) . '/widgets-page.php'; diff --git a/lib/rest-api.php b/lib/rest-api.php index 5aa85b6d83e48..6ad5e6d0e6f0e 100644 --- a/lib/rest-api.php +++ b/lib/rest-api.php @@ -85,6 +85,10 @@ function gutenberg_register_rest_widget_areas() { * @since 6.5.0 */ function gutenberg_register_rest_block_directory() { + if ( ! gutenberg_is_experiment_enabled( 'gutenberg-block-directory' ) ) { + return; + } + $block_directory_controller = new WP_REST_Block_Directory_Controller(); $block_directory_controller->register_routes(); } diff --git a/lib/template-loader.php b/lib/template-loader.php index 1c6923091ce32..30ea5fae54f8e 100644 --- a/lib/template-loader.php +++ b/lib/template-loader.php @@ -80,7 +80,7 @@ function gutenberg_override_query_template( $template, $type, array $templates = * @return string Path to the canvas file to include. */ function gutenberg_find_template( $template_file ) { - global $_wp_current_template_post, $_wp_current_template_hierarchy; + global $_wp_current_template_content, $_wp_current_template_hierarchy; // Bail if no relevant template hierarchy was determined, or if the template file // was overridden another way. @@ -105,8 +105,56 @@ function gutenberg_find_template( $template_file ) { ); if ( $template_query->have_posts() ) { - $template_posts = $template_query->get_posts(); - $_wp_current_template_post = array_shift( $template_posts ); + $template_posts = $template_query->get_posts(); + $current_template_post = array_shift( $template_posts ); + + // Build map of template slugs to their priority in the current hierarchy. + $slug_priorities = array_flip( $slugs ); + + // See if there is a theme block template with higher priority than the resolved template post. + $higher_priority_block_template_path = null; + $higher_priority_block_template_priority = PHP_INT_MAX; + $block_template_files = glob( get_stylesheet_directory() . '/block-templates/*.html', 1 ); + if ( is_child_theme() ) { + $block_template_files = array_merge( $block_template_files, glob( get_template_directory() . '/block-templates/*.html', 1 ) ); + } + foreach ( $block_template_files as $path ) { + $theme_block_template_priority = $slug_priorities[ basename( $path, '.html' ) ]; + if ( + isset( $theme_block_template_priority ) && + $theme_block_template_priority < $higher_priority_block_template_priority && + $theme_block_template_priority < $slug_priorities[ $current_template_post->post_name ] + ) { + $higher_priority_block_template_path = $path; + $higher_priority_block_template_priority = $theme_block_template_priority; + } + } + + // If there is, use it instead. + if ( isset( $higher_priority_block_template_path ) ) { + $post_name = basename( $path, '.html' ); + $current_template_post = array( + 'post_content' => file_get_contents( $higher_priority_block_template_path ), + 'post_title' => ucfirst( $post_name ), + 'post_status' => 'auto-draft', + 'post_type' => 'wp_template', + 'post_name' => $post_name, + ); + if ( is_admin() ) { + // Only create auto-draft of block template for editing + // in admin screens, similarly to how we do it for new + // posts in the editor. + $current_template_post = get_post( + wp_insert_post( $current_template_post ) + ); + } else { + $current_template_post = new WP_Post( + (object) $current_template_post + ); + } + } + + $_wp_current_template_content = $current_template_post->post_content; } // Add extra hooks for template canvas. @@ -131,17 +179,15 @@ function gutenberg_render_title_tag() { * Renders the markup for the current template. */ function gutenberg_render_the_template() { - global $_wp_current_template_post; + global $_wp_current_template_content; global $wp_embed; - if ( ! $_wp_current_template_post || 'wp_template' !== $_wp_current_template_post->post_type ) { + if ( ! $_wp_current_template_content ) { echo '

' . esc_html__( 'No matching template found', 'gutenberg' ) . '

'; return; } - $content = $_wp_current_template_post->post_content; - - $content = $wp_embed->run_shortcode( $content ); + $content = $wp_embed->run_shortcode( $_wp_current_template_content ); $content = $wp_embed->autoembed( $content ); $content = do_blocks( $content ); $content = wptexturize( $content ); diff --git a/lib/templates.php b/lib/templates.php index fe7a4177400db..3eb4df27e1c0e 100644 --- a/lib/templates.php +++ b/lib/templates.php @@ -9,10 +9,7 @@ * Registers block editor 'wp_template' post type. */ function gutenberg_register_template_post_type() { - if ( - ! get_option( 'gutenberg-experiments' ) || - ! array_key_exists( 'gutenberg-full-site-editing', get_option( 'gutenberg-experiments' ) ) - ) { + if ( ! gutenberg_is_experiment_enabled( 'gutenberg-full-site-editing' ) ) { return; } diff --git a/package-lock.json b/package-lock.json index ea4b7a76328ff..e489d2de0e393 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "6.8.0-rc.1", + "version": "6.8.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -7228,6 +7228,7 @@ "@wordpress/data": "file:packages/data", "@wordpress/element": "file:packages/element", "@wordpress/i18n": "file:packages/i18n", + "@wordpress/plugins": "file:packages/plugins", "lodash": "^4.17.15" } }, @@ -7849,8 +7850,8 @@ "js-yaml": "^3.13.1", "lodash": "^4.17.15", "minimist": "^1.2.0", - "npm-package-json-lint": "^4.0.2", - "puppeteer": "^1.20.0", + "npm-package-json-lint": "^4.0.3", + "puppeteer": "^2.0.0", "read-pkg-up": "^1.0.1", "request": "^2.88.0", "resolve-bin": "^0.4.0", @@ -8853,9 +8854,9 @@ "dev": true }, "axe-core": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-3.1.2.tgz", - "integrity": "sha512-e1WVs0SQu3tM29J9a/mISGvlo2kdCStE+yffIAJF6eb42FS+eUFEVz9j4rgDeV2TAfPJmuOZdRetWYycIbK7Vg==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-3.4.0.tgz", + "integrity": "sha512-5C0OdgxPv/DrQguO6Taj5F1dY5OlkWg4SVmZIVABFYKWlnAc5WTLPzG+xJSgIwf2fmY+NiNGiZXhXx2qT0u/9Q==", "dev": true }, "axe-puppeteer": { @@ -15275,6 +15276,15 @@ } } }, + "expand-tilde": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-1.2.2.tgz", + "integrity": "sha1-C4HrqJflo9MdHD0QL48BRB5VlEk=", + "dev": true, + "requires": { + "os-homedir": "^1.0.1" + } + }, "expect": { "version": "24.7.1", "resolved": "https://registry.npmjs.org/expect/-/expect-24.7.1.tgz", @@ -15859,55 +15869,6 @@ "requires": { "fs-exists-sync": "^0.1.0", "resolve-dir": "^0.1.0" - }, - "dependencies": { - "expand-tilde": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-1.2.2.tgz", - "integrity": "sha1-C4HrqJflo9MdHD0QL48BRB5VlEk=", - "dev": true, - "requires": { - "os-homedir": "^1.0.1" - } - }, - "global-modules": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-0.2.3.tgz", - "integrity": "sha1-6lo77ULG1s6ZWk+KEmm12uIjgo0=", - "dev": true, - "requires": { - "global-prefix": "^0.1.4", - "is-windows": "^0.2.0" - } - }, - "global-prefix": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-0.1.5.tgz", - "integrity": "sha1-jTvGuNo8qBEqFg2NSW/wRiv+948=", - "dev": true, - "requires": { - "homedir-polyfill": "^1.0.0", - "ini": "^1.3.4", - "is-windows": "^0.2.0", - "which": "^1.2.12" - } - }, - "is-windows": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-0.2.0.tgz", - "integrity": "sha1-3hqm1j6indJIc3tp8f+LgALSEIw=", - "dev": true - }, - "resolve-dir": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-0.1.1.tgz", - "integrity": "sha1-shklmlYC+sXFxJatiUpujMQwJh4=", - "dev": true, - "requires": { - "expand-tilde": "^1.2.2", - "global-modules": "^0.2.3" - } - } } }, "find-parent-dir": { @@ -17730,9 +17691,9 @@ "integrity": "sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw==" }, "homedir-polyfill": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.1.tgz", - "integrity": "sha1-TCu8inWJmP7r9e1oWA921GdotLw=", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", + "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", "dev": true, "requires": { "parse-passwd": "^1.0.0" @@ -19788,27 +19749,28 @@ "wait-on": "^3.3.0" }, "dependencies": { - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } + "kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true }, "prompts": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.1.0.tgz", - "integrity": "sha512-+x5TozgqYdOwWsQFZizE/Tra3fKvAoy037kOyU6cgz84n8f6zxngLOV4O32kTwt9FcLCxAqw0P/c8rOr9y+Gfg==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.2.1.tgz", + "integrity": "sha512-VObPvJiWPhpZI6C5m60XOzTfnYg/xc/an+r9VYymj9WJW3B/DIH+REzjpAACPf8brwPeP+7vz3bIim3S+AaMjw==", "dev": true, "requires": { - "kleur": "^3.0.2", - "sisteransi": "^1.0.0" + "kleur": "^3.0.3", + "sisteransi": "^1.0.3" } }, + "sisteransi": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.3.tgz", + "integrity": "sha512-SbEG75TzH8G7eVXFSN5f9EExILKfly7SUvVY5DhhYLvfhKqhDFY0OzevWa/zwak0RLRfWS5AvfMWpd9gJvr5Yg==", + "dev": true + }, "tree-kill": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.1.tgz", @@ -19896,19 +19858,6 @@ "cwd": "^0.10.0", "jest-dev-server": "^4.3.0", "merge-deep": "^3.0.2" - }, - "dependencies": { - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - } } }, "jest-get-type": { @@ -24342,9 +24291,9 @@ } }, "npm-package-json-lint": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/npm-package-json-lint/-/npm-package-json-lint-4.0.2.tgz", - "integrity": "sha512-xlahvfRgZcsawUPzKaJvcQSs4K0EzDgQ9YEe7VODo1XbLMWwS0IEXiywHlKPAvo1fmWBvovIdoAqhe0Gk4InHA==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/npm-package-json-lint/-/npm-package-json-lint-4.0.3.tgz", + "integrity": "sha512-cuvTR2l5dOjjlRR3a1CCp+mh2A2HyQRxydwdcYi0Z77NRlADpf7wF3Jf8XFLGZM7J6afXNRBofBjQ1UWFyOtKA==", "dev": true, "requires": { "ajv": "^6.10.2", @@ -27056,14 +27005,14 @@ "dev": true }, "puppeteer": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-1.20.0.tgz", - "integrity": "sha512-bt48RDBy2eIwZPrkgbcwHtb51mj2nKvHOPMaSH2IsWiv7lOG9k9zhaRzpDZafrk05ajMc3cu+lSQYYOfH2DkVQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-2.0.0.tgz", + "integrity": "sha512-t3MmTWzQxPRP71teU6l0jX47PHXlc4Z52sQv4LJQSZLq1ttkKS2yGM3gaI57uQwZkNaoGd0+HPPMELZkcyhlqA==", "dev": true, "requires": { "debug": "^4.1.0", "extract-zip": "^1.6.6", - "https-proxy-agent": "^2.2.1", + "https-proxy-agent": "^3.0.0", "mime": "^2.0.3", "progress": "^2.0.1", "proxy-from-env": "^1.0.0", @@ -27080,6 +27029,27 @@ "ms": "^2.1.1" } }, + "https-proxy-agent": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-3.0.1.tgz", + "integrity": "sha512-+ML2Rbh6DAuee7d07tYGEKOEi2voWPUGan+ExdPbPW6Z3svq+JCqr0v8WmKPOkz1vOVykPCBSuobe7G8GJUtVg==", + "dev": true, + "requires": { + "agent-base": "^4.3.0", + "debug": "^3.1.0" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -29364,6 +29334,46 @@ "resolve-from": "^3.0.0" } }, + "resolve-dir": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-0.1.1.tgz", + "integrity": "sha1-shklmlYC+sXFxJatiUpujMQwJh4=", + "dev": true, + "requires": { + "expand-tilde": "^1.2.2", + "global-modules": "^0.2.3" + }, + "dependencies": { + "global-modules": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-0.2.3.tgz", + "integrity": "sha1-6lo77ULG1s6ZWk+KEmm12uIjgo0=", + "dev": true, + "requires": { + "global-prefix": "^0.1.4", + "is-windows": "^0.2.0" + } + }, + "global-prefix": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-0.1.5.tgz", + "integrity": "sha1-jTvGuNo8qBEqFg2NSW/wRiv+948=", + "dev": true, + "requires": { + "homedir-polyfill": "^1.0.0", + "ini": "^1.3.4", + "is-windows": "^0.2.0", + "which": "^1.2.12" + } + }, + "is-windows": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-0.2.0.tgz", + "integrity": "sha1-3hqm1j6indJIc3tp8f+LgALSEIw=", + "dev": true + } + } + }, "resolve-from": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", @@ -33559,107 +33569,12 @@ "rx": "^4.1.0" }, "dependencies": { - "ajv": { - "version": "6.10.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", - "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", - "dev": true, - "requires": { - "fast-deep-equal": "^2.0.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "aws4": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", - "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", - "dev": true - }, "core-js": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.9.tgz", - "integrity": "sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A==", - "dev": true - }, - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true - }, - "fast-deep-equal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", - "dev": true - }, - "har-validator": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", - "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", - "dev": true, - "requires": { - "ajv": "^6.5.5", - "har-schema": "^2.0.0" - } - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "mime-db": { - "version": "1.40.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", - "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==", - "dev": true - }, - "mime-types": { - "version": "2.1.24", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", - "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", - "dev": true, - "requires": { - "mime-db": "1.40.0" - } - }, - "oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "version": "2.6.10", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.10.tgz", + "integrity": "sha512-I39t74+4t+zau64EN1fE5v2W31Adtc/REhzWN+gWRRXg6WH5qAsZm62DHpQ1+Yhe4047T55jvzz7MUqF/dBBlA==", "dev": true }, - "request": { - "version": "2.88.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", - "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", - "dev": true, - "requires": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.0", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.4.3", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - } - }, "rx": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/rx/-/rx-4.1.0.tgz", @@ -33669,63 +33584,35 @@ } }, "wait-port": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/wait-port/-/wait-port-0.2.2.tgz", - "integrity": "sha1-1RpJHkhKF791qUfnEaLwErTm8uM=", + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/wait-port/-/wait-port-0.2.6.tgz", + "integrity": "sha512-nXE5Yp0Zs1obhFVc0Da7WVJc3y0LxoCq3j4mtV0NdI5P/ZvRdKp5yhuojvMOcOxSwpQL1hGbOgMNQ+4wpRpwCA==", "dev": true, "requires": { - "chalk": "^1.1.3", - "commander": "^2.9.0", - "debug": "^2.6.6" + "chalk": "^2.4.2", + "commander": "^3.0.2", + "debug": "^4.1.1" }, "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "commander": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/commander/-/commander-3.0.2.tgz", + "integrity": "sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow==", "dev": true }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", "dev": true, "requires": { - "ansi-regex": "^2.0.0" + "ms": "^2.1.1" } }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true } } diff --git a/package.json b/package.json index 87179bff38b78..0409deb1372b7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "6.8.0-rc.1", + "version": "6.8.0", "private": true, "description": "A new WordPress editor experience.", "author": "The WordPress Contributors", diff --git a/packages/block-directory/package.json b/packages/block-directory/package.json index 7736869f8100c..22ff8e4e8f8c2 100644 --- a/packages/block-directory/package.json +++ b/packages/block-directory/package.json @@ -29,6 +29,7 @@ "@wordpress/data": "file:../data", "@wordpress/element": "file:../element", "@wordpress/i18n": "file:../i18n", + "@wordpress/plugins": "file:../plugins", "lodash": "^4.17.15" }, "publishConfig": { diff --git a/packages/block-directory/src/index.js b/packages/block-directory/src/index.js index 5c088e8d5928d..3af7027bf74d0 100644 --- a/packages/block-directory/src/index.js +++ b/packages/block-directory/src/index.js @@ -2,5 +2,4 @@ * Internal dependencies */ import './store'; - -export { default as DownloadableBlocksPanel } from './components/downloadable-blocks-panel'; +import './plugins'; diff --git a/packages/block-directory/src/plugins/index.js b/packages/block-directory/src/plugins/index.js new file mode 100644 index 0000000000000..917b8567c2d87 --- /dev/null +++ b/packages/block-directory/src/plugins/index.js @@ -0,0 +1,15 @@ +/** + * WordPress dependencies + */ +import { registerPlugin } from '@wordpress/plugins'; + +/** + * Internal dependencies + */ +import InserterMenuDownloadableBlocksPanel from './inserter-menu-downloadable-blocks-panel'; + +registerPlugin( 'block-directory', { + render() { + return ; + }, +} ); diff --git a/packages/editor/src/components/inserter-menu-downloadable-blocks-panel/index.js b/packages/block-directory/src/plugins/inserter-menu-downloadable-blocks-panel/index.js similarity index 89% rename from packages/editor/src/components/inserter-menu-downloadable-blocks-panel/index.js rename to packages/block-directory/src/plugins/inserter-menu-downloadable-blocks-panel/index.js index d19d76ba42ad8..9fb2cc8105dc0 100644 --- a/packages/editor/src/components/inserter-menu-downloadable-blocks-panel/index.js +++ b/packages/block-directory/src/plugins/inserter-menu-downloadable-blocks-panel/index.js @@ -7,9 +7,13 @@ import { debounce } from 'lodash'; * WordPress dependencies */ import { __experimentalInserterMenuExtension } from '@wordpress/block-editor'; -import { DownloadableBlocksPanel } from '@wordpress/block-directory'; import { useState } from '@wordpress/element'; +/** + * Internal dependencies + */ +import DownloadableBlocksPanel from '../../components/downloadable-blocks-panel'; + function InserterMenuDownloadableBlocksPanel() { const [ debouncedFilterValue, setFilterValue ] = useState( '' ); diff --git a/packages/block-editor/src/components/autocomplete/index.native.js b/packages/block-editor/src/components/autocomplete/index.native.js new file mode 100644 index 0000000000000..461f67a0a4bcb --- /dev/null +++ b/packages/block-editor/src/components/autocomplete/index.native.js @@ -0,0 +1 @@ +export default () => null; diff --git a/packages/block-editor/src/components/block-list/block-async-mode-provider.js b/packages/block-editor/src/components/block-list/block-async-mode-provider.js index aaa2e709db92c..3cdde80050761 100644 --- a/packages/block-editor/src/components/block-list/block-async-mode-provider.js +++ b/packages/block-editor/src/components/block-list/block-async-mode-provider.js @@ -1,10 +1,7 @@ /** * WordPress dependencies */ -import { - __experimentalAsyncModeProvider as AsyncModeProvider, - useSelect, -} from '@wordpress/data'; +import { AsyncModeProvider, useSelect } from '@wordpress/data'; const BlockAsyncModeProvider = ( { children, clientId, isBlockInSelection } ) => { const isParentOfSelectedBlock = useSelect( ( select ) => { diff --git a/packages/block-editor/src/components/block-list/block-mobile-toolbar.js b/packages/block-editor/src/components/block-list/block-mobile-toolbar.js index f35a7add73641..8af64ec12c021 100644 --- a/packages/block-editor/src/components/block-list/block-mobile-toolbar.js +++ b/packages/block-editor/src/components/block-list/block-mobile-toolbar.js @@ -9,11 +9,11 @@ import { ifViewportMatches } from '@wordpress/viewport'; import BlockMover from '../block-mover'; import VisualEditorInserter from '../inserter'; -function BlockMobileToolbar( { clientId } ) { +function BlockMobileToolbar( { clientId, moverDirection } ) { return (
- +
); } diff --git a/packages/block-editor/src/components/block-list/block.js b/packages/block-editor/src/components/block-list/block.js index 777cb54933a80..48b5506e452f2 100644 --- a/packages/block-editor/src/components/block-list/block.js +++ b/packages/block-editor/src/components/block-list/block.js @@ -67,6 +67,7 @@ function BlockListBlock( { mode, isFocusMode, hasFixedToolbar, + moverDirection, isLocked, clientId, rootClientId, @@ -450,6 +451,20 @@ function BlockListBlock( { }; } const blockElementId = `block-${ clientId }`; + const blockMover = ( + + ); // We wrap the BlockEdit component in a div that hides it when editing in // HTML mode. This allows us to render all of the ancillary pieces @@ -511,22 +526,18 @@ function BlockListBlock( { rootClientId={ rootClientId } /> { isFirstMultiSelected && ( - + ) } -
- { shouldRenderMovers && ( - +
+ { shouldRenderMovers && ( moverDirection === 'vertical' ) && blockMover } { shouldShowBreadcrumb && ( ) } + { shouldRenderMovers && ( moverDirection === 'horizontal' ) && blockMover } { ! isValid && [ { !! hasError && } { shouldShowMobileToolbar && ( - + ) }
diff --git a/packages/block-editor/src/components/block-list/index.js b/packages/block-editor/src/components/block-list/index.js index 25386d5039c12..54511ac941ecc 100644 --- a/packages/block-editor/src/components/block-list/index.js +++ b/packages/block-editor/src/components/block-list/index.js @@ -17,7 +17,7 @@ import { Component } from '@wordpress/element'; import { withSelect, withDispatch, - __experimentalAsyncModeProvider as AsyncModeProvider, + AsyncModeProvider, } from '@wordpress/data'; import { compose } from '@wordpress/compose'; @@ -195,6 +195,7 @@ class BlockList extends Component { className, blockClientIds, rootClientId, + __experimentalMoverDirection: moverDirection = 'vertical', isDraggable, selectedBlockClientId, multiSelectedBlockClientIds, @@ -227,6 +228,7 @@ class BlockList extends Component { blockRef={ this.setBlockRef } onSelectionStart={ this.onSelectionStart } isDraggable={ isDraggable } + moverDirection={ moverDirection } // This prop is explicitely computed and passed down // to avoid being impacted by the async mode diff --git a/packages/block-editor/src/components/block-list/index.native.js b/packages/block-editor/src/components/block-list/index.native.js index 05c2898899ca3..b925a724809ec 100644 --- a/packages/block-editor/src/components/block-list/index.native.js +++ b/packages/block-editor/src/components/block-list/index.native.js @@ -96,9 +96,6 @@ export class BlockList extends Component { ListHeaderComponent={ header } ListEmptyComponent={ this.renderDefaultBlockAppender } ListFooterComponent={ withFooter && this.renderBlockListFooter } - getItemLayout={ ( data, index ) => { - return { length: 0, offset: 0, index }; - } } /> { renderAppender && blockClientIds.length > 0 && diff --git a/packages/block-editor/src/components/block-list/multi-controls.js b/packages/block-editor/src/components/block-list/multi-controls.js index e4dd88aeda030..2f49ca2ca0a3b 100644 --- a/packages/block-editor/src/components/block-list/multi-controls.js +++ b/packages/block-editor/src/components/block-list/multi-controls.js @@ -11,6 +11,7 @@ import BlockMover from '../block-mover'; function BlockListMultiControls( { multiSelectedBlockClientIds, isSelecting, + moverDirection, } ) { if ( isSelecting ) { return null; @@ -19,6 +20,7 @@ function BlockListMultiControls( { return ( ); } diff --git a/packages/block-editor/src/components/block-list/style.scss b/packages/block-editor/src/components/block-list/style.scss index 761632c49d7c7..f70a789f4e704 100644 --- a/packages/block-editor/src/components/block-list/style.scss +++ b/packages/block-editor/src/components/block-list/style.scss @@ -101,6 +101,9 @@ .block-editor-block-list__block-edit { position: relative; + &.has-mover-inside > [data-block] { + display: flex; + } &::before { z-index: z-index(".block-editor-block-list__block-edit::before"); @@ -708,37 +711,6 @@ &.is-multi-selected > .block-editor-block-mover { left: -$block-side-ui-width - $block-side-ui-clearance; } - - // For floats, show block mover when block is selected, and never on hover. - &[data-align="left"], - &[data-align="right"] { - // Show always when the block is selected. - &.is-selected > .block-editor-block-list__block-edit > .block-editor-block-mover { - // Don't show on mobile, allow the special mobile toolbar to work there. - display: none; - @include break-small() { - display: block; - opacity: 1; - animation: none; - - // Make wider and taller to make "safe" hover area bigger. - // The intent is to make it less likely that you hover float-adjacent - // blocks that visually appear below the block. - width: $block-side-ui-width + $block-side-ui-clearance + $block-padding + $border-width; - height: auto; - padding-bottom: $block-padding; - - // Unset the negative top margin, or it might overlap the block toolbar. - margin-top: 0; - } - } - - // Don't show on hover, or on the "ghost" when dragging. - &.is-hovered > .block-editor-block-list__block-edit > .block-editor-block-mover, - &.is-dragging > .block-editor-block-list__block-edit > .block-editor-block-mover { - display: none; - } - } } diff --git a/packages/block-editor/src/components/block-mover/icons.js b/packages/block-editor/src/components/block-mover/icons.js index 06bf5601f439d..2a15b8a6159d5 100644 --- a/packages/block-editor/src/components/block-mover/icons.js +++ b/packages/block-editor/src/components/block-mover/icons.js @@ -9,12 +9,24 @@ export const upArrow = ( ); +export const leftArrow = ( + + + +); + export const downArrow = ( ); +export const rightArrow = ( + + + +); + export const dragHandle = ( @@ -107,6 +141,8 @@ export class BlockMover extends Component { isFirst, isLast, 1, + orientation, + isRTL, ) } @@ -125,12 +161,17 @@ export default compose( const blockOrder = getBlockOrder( rootClientId ); const firstIndex = getBlockIndex( firstClientId, rootClientId ); const lastIndex = getBlockIndex( last( normalizedClientIds ), rootClientId ); + const { getSettings } = select( 'core/block-editor' ); + const { + isRTL, + } = getSettings(); return { blockType: block ? getBlockType( block.name ) : null, isLocked: getTemplateLock( rootClientId ) === 'all', rootClientId, firstIndex, + isRTL, isFirst: firstIndex === 0, isLast: lastIndex === blockOrder.length - 1, }; diff --git a/packages/block-editor/src/components/block-mover/mover-description.js b/packages/block-editor/src/components/block-mover/mover-description.js index 44174ce85534c..d9a62de176d82 100644 --- a/packages/block-editor/src/components/block-mover/mover-description.js +++ b/packages/block-editor/src/components/block-mover/mover-description.js @@ -14,12 +14,30 @@ import { __, _n, sprintf } from '@wordpress/i18n'; * @param {boolean} isLast This is the last block. * @param {number} dir Direction of movement (> 0 is considered to be going * down, < 0 is up). + * @param {string} orientation The orientation of the block movers, vertical or + * horizontal. + * @param {boolean} isRTL True if current writing system is right to left. * * @return {string} Label for the block movement controls. */ -export function getBlockMoverDescription( selectedCount, type, firstIndex, isFirst, isLast, dir ) { +export function getBlockMoverDescription( selectedCount, type, firstIndex, isFirst, isLast, dir, orientation, isRTL ) { const position = ( firstIndex + 1 ); + const getMovementDirection = ( moveDirection ) => { + if ( moveDirection === 'up' ) { + if ( orientation === 'horizontal' ) { + return isRTL ? 'right' : 'left'; + } + return 'up'; + } else if ( moveDirection === 'down' ) { + if ( orientation === 'horizontal' ) { + return isRTL ? 'left' : 'right'; + } + return 'down'; + } + return null; + }; + if ( selectedCount > 1 ) { return getMultiBlockMoverDescription( selectedCount, firstIndex, isFirst, isLast, dir ); } @@ -32,35 +50,46 @@ export function getBlockMoverDescription( selectedCount, type, firstIndex, isFir if ( dir > 0 && ! isLast ) { // moving down return sprintf( - // translators: 1: Type of block (i.e. Text, Image etc), 2: Position of selected block, 3: New position - __( 'Move %1$s block from position %2$d down to position %3$d' ), + // translators: 1: Type of block (i.e. Text, Image etc), 2: Position of selected block, 3: Direction of movement ( up, down, left, right ), 4: New position + __( 'Move %1$s block from position %2$d %3$s to position %4$d' ), type, position, - ( position + 1 ) + getMovementDirection( 'down' ), + ( position + 1 ), ); } if ( dir > 0 && isLast ) { // moving down, and is the last item - // translators: %s: Type of block (i.e. Text, Image etc) - return sprintf( __( 'Block %s is at the end of the content and can’t be moved down' ), type ); + // translators: 1: Type of block (i.e. Text, Image etc), 2: Direction of movement ( up, down, left, right ) + return sprintf( + __( 'Block %1$s is at the end of the content and can’t be moved %2$s' ), + type, + getMovementDirection( 'down' ), + + ); } if ( dir < 0 && ! isFirst ) { // moving up return sprintf( - // translators: 1: Type of block (i.e. Text, Image etc), 2: Position of selected block, 3: New position - __( 'Move %1$s block from position %2$d up to position %3$d' ), + // translators: 1: Type of block (i.e. Text, Image etc), 2: Position of selected block, 3: Direction of movement ( up, down, left, right ), 4: New position + __( 'Move %1$s block from position %2$d %3$s to position %4$d' ), type, position, - ( position - 1 ) + getMovementDirection( 'up' ), + ( position - 1 ), ); } if ( dir < 0 && isFirst ) { // moving up, and is the first item - // translators: %s: Type of block (i.e. Text, Image etc) - return sprintf( __( 'Block %s is at the beginning of the content and can’t be moved up' ), type ); + // translators: 1: Type of block (i.e. Text, Image etc), 2: Direction of movement ( up, down, left, right ) + return sprintf( + __( 'Block %1$s is at the beginning of the content and can’t be moved %2$s' ), + type, + getMovementDirection( 'up' ), + ); } } diff --git a/packages/block-editor/src/components/block-mover/style.scss b/packages/block-editor/src/components/block-mover/style.scss index a506b3282fad4..260d456c73d3c 100644 --- a/packages/block-editor/src/components/block-mover/style.scss +++ b/packages/block-editor/src/components/block-mover/style.scss @@ -1,5 +1,4 @@ .block-editor-block-mover { - @include break-small() { min-height: $empty-paragraph-height; opacity: 0; @@ -20,13 +19,38 @@ // 24px is the smallest size of a good pressable button. // With 3 pieces of side UI, that comes to a total of 72px. // To vertically center against a 56px paragraph, move upwards 72px - 56px / 2. - // Don't do this for wide, fullwide, or mobile. - .block-editor-block-list__block:not([data-align="wide"]):not([data-align="full"]) & { - margin-top: -$grid-size; + margin-top: -$grid-size; + } + + &.is-horizontal { + margin-top: 5px; // The height of the appender is 36px. This pushes down the mover to be centered according to that. + margin-right: $grid-size; + padding-right: 0; + min-height: auto; + width: ($icon-button-size-small * 2) + ($border-width * 2); + height: $icon-button-size-small + ($border-width * 2); + display: flex; + + .block-editor-block-mover__control { + width: $icon-button-size-small; + height: $icon-button-size-small; + + svg { + width: $icon-button-size-small; + padding: 3px; + } } } } +// Don't add negative vertical margin for wide, fullwide, or mobile. +// @todo: simplify this selector. +@include break-small() { + .block-editor-block-list__block:not([data-align="wide"]):not([data-align="full"]) .editor-block-mover:not(.is-horizontal) { + margin-top: 0; + } +} + // Mover icon buttons. .block-editor-block-mover__control { display: flex; diff --git a/packages/block-editor/src/components/button-block-appender/index.js b/packages/block-editor/src/components/button-block-appender/index.js index 7fd751165f5b4..1cbbb5980903a 100644 --- a/packages/block-editor/src/components/button-block-appender/index.js +++ b/packages/block-editor/src/components/button-block-appender/index.js @@ -6,8 +6,8 @@ import classnames from 'classnames'; /** * WordPress dependencies */ -import { Button, Icon } from '@wordpress/components'; -import { __ } from '@wordpress/i18n'; +import { Button, Icon, Tooltip } from '@wordpress/components'; +import { _x, sprintf } from '@wordpress/i18n'; /** * Internal dependencies @@ -21,17 +21,31 @@ function ButtonBlockAppender( { rootClientId, className } ) { ( - { __( 'Add Block' ) } - - - ) } + renderToggle={ ( { onToggle, disabled, isOpen, blockTitle, hasSingleBlockType } ) => { + let label; + if ( hasSingleBlockType ) { + // translators: %s: the name of the block when there is only one + label = sprintf( _x( 'Add %s', 'directly add the only allowed block' ), blockTitle ); + } else { + label = _x( 'Add block', 'Generic label for block inserter button' ); + } + const isToggleButton = ! hasSingleBlockType; + return ( + + + + ); + } } isAppender /> diff --git a/packages/block-editor/src/components/colors/use-colors.js b/packages/block-editor/src/components/colors/use-colors.js index ee3f37c8b00bf..d69ee1bdbadf9 100644 --- a/packages/block-editor/src/components/colors/use-colors.js +++ b/packages/block-editor/src/components/colors/use-colors.js @@ -2,7 +2,13 @@ * External dependencies */ import memoize from 'memize'; -import { kebabCase, camelCase, startCase } from 'lodash'; +import classnames from 'classnames'; +import { + camelCase, + kebabCase, + map, + startCase, +} from 'lodash'; /** * WordPress dependencies @@ -35,17 +41,17 @@ const ColorPanel = ( { { contrastCheckerProps && - components.map( ( Component, index ) => ( + map( components, ( ( Component, key ) => ( - ) ) } + ) ) ) } { typeof panelChildren === 'function' ? panelChildren( components ) : panelChildren } @@ -89,24 +95,35 @@ export default function __experimentalUseColors( const createComponent = useMemo( () => memoize( - ( property, color, colorValue, customColor ) => ( { children } ) => + ( name, property, className, color, colorValue, customColor ) => ( { + children, + className: componentClassName = '', + style: componentStyle = {}, + } ) => // Clone children, setting the style property from the color configuration, // if not already set explicitly through props. Children.map( children, ( child ) => { - let className = child.props.className; - let style = child.props.style; + let colorStyle = {}; if ( color ) { - className = `${ child.props.className } has-${ kebabCase( - color - ) }-${ kebabCase( property ) }`; - style = { [ property ]: colorValue, ...child.props.style }; + colorStyle = { [ property ]: colorValue }; } else if ( customColor ) { - className = `${ child.props.className } has-${ kebabCase( property ) }`; - style = { [ property ]: customColor, ...child.props.style }; + colorStyle = { [ property ]: customColor }; } + return cloneElement( child, { - className, - style, + className: classnames( + componentClassName, + child.props.className, + { + [ `has-${ kebabCase( color ) }-${ kebabCase( property ) }` ]: color, + [ className || `has-${ kebabCase( name ) }` ]: color || customColor, + } + ), + style: { + ...colorStyle, + ...componentStyle, + ...( child.props.style || {} ), + }, } ); } ), { maxSize: colorConfigs.length } @@ -135,7 +152,7 @@ export default function __experimentalUseColors( ); return useMemo( () => { - const colorSettings = []; + const colorSettings = {}; const components = colorConfigs.reduce( ( acc, colorConfig ) => { if ( typeof colorConfig === 'string' ) { @@ -144,6 +161,7 @@ export default function __experimentalUseColors( const { name, // E.g. 'backgroundColor'. property = name, // E.g. 'backgroundColor'. + className, panelLabel = startCase( name ), // E.g. 'Background Color'. componentName = panelLabel.replace( /\s/g, '' ), // E.g. 'BackgroundColor'. @@ -159,7 +177,9 @@ export default function __experimentalUseColors( // when they are used as props for other components. const _color = colors.find( ( __color ) => __color.slug === color ); acc[ componentName ] = createComponent( + name, property, + className, color, _color && _color.color, attributes[ camelCase( `custom ${ name }` ) ] @@ -168,21 +188,20 @@ export default function __experimentalUseColors( acc[ componentName ].color = color; acc[ componentName ].setColor = createSetColor( name, colors ); - const newSettingIndex = - colorSettings.push( { - value: _color ? - _color.color : - attributes[ camelCase( `custom ${ name }` ) ], - onChange: acc[ componentName ].setColor, - label: panelLabel, - colors, - } ) - 1; + colorSettings[ componentName ] = { + value: _color ? + _color.color : + attributes[ camelCase( `custom ${ name }` ) ], + onChange: acc[ componentName ].setColor, + label: panelLabel, + colors, + }; // These settings will be spread over the `colors` in // `colorPanelProps`, so we need to unset the key here, // if not set to an actual value, to avoid overwriting // an actual value in `colorPanelProps`. if ( ! colors ) { - delete colorSettings[ newSettingIndex ].colors; + delete colorSettings[ componentName ].colors; } return acc; @@ -193,7 +212,7 @@ export default function __experimentalUseColors( colorSettings, colorPanelProps, contrastCheckerProps, - components: Object.values( components ), + components, panelChildren, }; return { diff --git a/packages/block-editor/src/components/default-block-appender/test/__snapshots__/index.js.snap b/packages/block-editor/src/components/default-block-appender/test/__snapshots__/index.js.snap index 9a09dae0d4767..08a0f7df9efe7 100644 --- a/packages/block-editor/src/components/default-block-appender/test/__snapshots__/index.js.snap +++ b/packages/block-editor/src/components/default-block-appender/test/__snapshots__/index.js.snap @@ -29,7 +29,7 @@ exports[`DefaultBlockAppender should append a default block when input focused 1 rows={1} value="Start writing or type / to choose a block" /> - @@ -53,7 +53,7 @@ exports[`DefaultBlockAppender should match snapshot 1`] = ` rows={1} value="Start writing or type / to choose a block" /> - @@ -77,7 +77,7 @@ exports[`DefaultBlockAppender should optionally show without prompt 1`] = ` rows={1} value="" /> - diff --git a/packages/block-editor/src/components/index.js b/packages/block-editor/src/components/index.js index 7ec60098a8c73..c7792608518ac 100644 --- a/packages/block-editor/src/components/index.js +++ b/packages/block-editor/src/components/index.js @@ -26,11 +26,13 @@ export { default as __experimentalGradientPickerPanel } from './gradient-picker/ export { default as InnerBlocks } from './inner-blocks'; export { default as InspectorAdvancedControls } from './inspector-advanced-controls'; export { default as InspectorControls } from './inspector-controls'; +export { default as __experimentalLinkControl } from './link-control'; export { default as MediaPlaceholder } from './media-placeholder'; export { default as MediaUpload } from './media-upload'; export { default as MediaUploadCheck } from './media-upload/check'; export { default as PanelColorSettings } from './panel-color-settings'; export { default as PlainText } from './plain-text'; +export { default as __experimentalResponsiveBlockControl } from './responsive-block-control'; export { default as RichText, RichTextShortcut, diff --git a/packages/block-editor/src/components/inner-blocks/index.js b/packages/block-editor/src/components/inner-blocks/index.js index 35e1bf10b47ca..ed147bfe736ad 100644 --- a/packages/block-editor/src/components/inner-blocks/index.js +++ b/packages/block-editor/src/components/inner-blocks/index.js @@ -107,6 +107,7 @@ class InnerBlocks extends Component { hasOverlay, renderAppender, template, + __experimentalMoverDirection: moverDirection, __experimentalTemplateOptions: templateOptions, __experimentalOnSelectTemplateOption: onSelectTemplateOption, __experimentalAllowTemplateOptionSkip: allowTemplateOptionSkip, @@ -131,6 +132,7 @@ class InnerBlocks extends Component { ) }
diff --git a/packages/block-editor/src/components/inner-blocks/index.native.js b/packages/block-editor/src/components/inner-blocks/index.native.js index 2b6a6c9320c92..6d2d4290772fa 100644 --- a/packages/block-editor/src/components/inner-blocks/index.native.js +++ b/packages/block-editor/src/components/inner-blocks/index.native.js @@ -122,6 +122,7 @@ class InnerBlocks extends Component { rootClientId={ clientId } renderAppender={ renderAppender } withFooter={ false } + isFullyBordered={ true } /> ) } diff --git a/packages/block-editor/src/components/inserter/index.js b/packages/block-editor/src/components/inserter/index.js index 5abb1ae4ef923..012ca34ef2eef 100644 --- a/packages/block-editor/src/components/inserter/index.js +++ b/packages/block-editor/src/components/inserter/index.js @@ -1,29 +1,45 @@ +/** + * External dependencies + */ +import { get } from 'lodash'; /** * WordPress dependencies */ -import { __ } from '@wordpress/i18n'; +import { __, _x, sprintf } from '@wordpress/i18n'; import { Dropdown, IconButton } from '@wordpress/components'; import { Component } from '@wordpress/element'; -import { withSelect } from '@wordpress/data'; +import { withDispatch, withSelect } from '@wordpress/data'; import { compose, ifCondition } from '@wordpress/compose'; +import { + createBlock, +} from '@wordpress/blocks'; /** * Internal dependencies */ import InserterMenu from './menu'; -const defaultRenderToggle = ( { onToggle, disabled, isOpen } ) => ( - -); +const defaultRenderToggle = ( { onToggle, disabled, isOpen, blockTitle, hasSingleBlockType } ) => { + let label; + if ( hasSingleBlockType ) { + // translators: %s: the name of the block when there is only one + label = sprintf( _x( 'Add %s', 'directly add the only allowed block' ), blockTitle ); + } else { + label = _x( 'Add block', 'Generic label for block inserter button' ); + } + return ( + + ); +}; class Inserter extends Component { constructor() { @@ -56,10 +72,12 @@ class Inserter extends Component { renderToggle( { onToggle, isOpen } ) { const { disabled, + blockTitle, + hasSingleBlockType, renderToggle = defaultRenderToggle, } = this.props; - return renderToggle( { onToggle, isOpen, disabled } ); + return renderToggle( { onToggle, isOpen, disabled, blockTitle, hasSingleBlockType } ); } /** @@ -86,8 +104,10 @@ class Inserter extends Component { } render() { - const { position } = this.props; - + const { position, hasSingleBlockType, insertOnlyAllowedBlock } = this.props; + if ( hasSingleBlockType ) { + return this.renderToggle( { onToggle: insertOnlyAllowedBlock } ); + } return ( { - const { hasInserterItems } = select( 'core/block-editor' ); + const { + hasInserterItems, + __experimentalGetAllowedBlocks, + } = select( 'core/block-editor' ); + + const allowedBlocks = __experimentalGetAllowedBlocks( rootClientId ); + const hasSingleBlockType = allowedBlocks && ( get( allowedBlocks, [ 'length' ], 0 ) === 1 ); + let allowedBlockType = false; + if ( hasSingleBlockType ) { + allowedBlockType = allowedBlocks[ 0 ]; + } return { hasItems: hasInserterItems( rootClientId ), + hasSingleBlockType, + blockTitle: allowedBlockType ? allowedBlockType.title : '', + allowedBlockType, + }; + } ), + withDispatch( ( dispatch, ownProps, { select } ) => { + return { + insertOnlyAllowedBlock() { + const { rootClientId, clientId, isAppender } = ownProps; + const { + hasSingleBlockType, + allowedBlockType, + } = ownProps; + + if ( ! hasSingleBlockType ) { + return; + } + + function getInsertionIndex() { + const { + getBlockIndex, + getBlockSelectionEnd, + getBlockOrder, + } = select( 'core/block-editor' ); + + // If the clientId is defined, we insert at the position of the block. + if ( clientId ) { + return getBlockIndex( clientId, rootClientId ); + } + + // If there a selected block, we insert after the selected block. + const end = getBlockSelectionEnd(); + if ( ! isAppender && end ) { + return getBlockIndex( end, rootClientId ) + 1; + } + + // Otherwise, we insert at the end of the current rootClientId + return getBlockOrder( rootClientId ).length; + } + + const { + insertBlock, + } = dispatch( 'core/block-editor' ); + + const blockToInsert = createBlock( allowedBlockType.name ); + insertBlock( + blockToInsert, + getInsertionIndex(), + rootClientId + ); + }, }; } ), ifCondition( ( { hasItems } ) => hasItems ), diff --git a/packages/block-editor/src/components/inserter/menu.native.js b/packages/block-editor/src/components/inserter/menu.native.js index a03e5f818540c..591932dde8ab6 100644 --- a/packages/block-editor/src/components/inserter/menu.native.js +++ b/packages/block-editor/src/components/inserter/menu.native.js @@ -21,6 +21,15 @@ import { BottomSheet, Icon } from '@wordpress/components'; import styles from './style.scss'; export class InserterMenu extends Component { + constructor() { + super( ...arguments ); + + this.onLayout = this.onLayout.bind( this ); + this.state = { + numberOfColumns: this.calculateNumberOfColumns(), + }; + } + componentDidMount() { this.onOpen(); } @@ -47,9 +56,13 @@ export class InserterMenu extends Component { this.props.hideInsertionPoint(); } + onLayout() { + const numberOfColumns = this.calculateNumberOfColumns(); + this.setState( { numberOfColumns } ); + } + render() { const { getStylesFromColorScheme } = this.props; - const numberOfColumns = this.calculateNumberOfColumns(); const bottomPadding = styles.contentBottomPadding; const modalIconWrapperStyle = getStylesFromColorScheme( styles.modalIconWrapper, styles.modalIconWrapperDark ); const modalIconStyle = getStylesFromColorScheme( styles.modalIcon, styles.modalIconDark ); @@ -63,10 +76,11 @@ export class InserterMenu extends Component { hideHeader > diff --git a/packages/block-editor/src/components/link-control/README.md b/packages/block-editor/src/components/link-control/README.md new file mode 100644 index 0000000000000..e21c29974301b --- /dev/null +++ b/packages/block-editor/src/components/link-control/README.md @@ -0,0 +1,50 @@ +# Link Control + +## Props + +### className + +- Type: `String` +- Required: Yes + +### currentLink + +- Type: `Object` +- Required: Yes + +### currentSettings + +- Type: `Object` +- Required: Yes + +### fetchSearchSuggestions + +- Type: `Function` +- Required: No + +## Event handlers + +### onClose + +- Type: `Function` +- Required: No + +### onKeyDown + +- Type: `Function` +- Required: No + +### onKeyPress + +- Type: `Function` +- Required: No + +### onLinkChange + +- Type: `Function` +- Required: No + +### onSettingChange + +- Type: `Function` +- Required: No diff --git a/packages/block-editor/src/components/link-control/index.js b/packages/block-editor/src/components/link-control/index.js new file mode 100644 index 0000000000000..afe72df4f2e93 --- /dev/null +++ b/packages/block-editor/src/components/link-control/index.js @@ -0,0 +1,249 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; +import { isFunction, noop, startsWith } from 'lodash'; + +/** + * WordPress dependencies + */ +import { + Button, + ExternalLink, + Popover, +} from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; + +import { + useCallback, + useState, + useEffect, + Fragment, +} from '@wordpress/element'; + +import { + safeDecodeURI, + filterURLForDisplay, + isURL, + prependHTTP, + getProtocol, +} from '@wordpress/url'; + +import { withInstanceId, compose } from '@wordpress/compose'; +import { withSelect } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import LinkControlSettingsDrawer from './settings-drawer'; +import LinkControlSearchItem from './search-item'; +import LinkControlSearchInput from './search-input'; + +function LinkControl( { + className, + currentLink, + currentSettings, + fetchSearchSuggestions, + instanceId, + onClose = noop, + onKeyDown = noop, + onKeyPress = noop, + onLinkChange = noop, + onSettingsChange = { noop }, +} ) { + // State + const [ inputValue, setInputValue ] = useState( '' ); + const [ isEditingLink, setIsEditingLink ] = useState( false ); + + // Effects + useEffect( () => { + // If we have a link then stop editing mode + if ( currentLink ) { + setIsEditingLink( false ); + } else { + setIsEditingLink( true ); + } + }, [ currentLink ] ); + + // Handlers + + /** + * onChange LinkControlSearchInput event handler + * + * @param {string} value Current value returned by the search. + */ + const onInputChange = ( value = '' ) => { + setInputValue( value ); + }; + + // Utils + const startEditMode = () => { + if ( isFunction( onLinkChange ) ) { + onLinkChange(); + } + }; + + const closeLinkUI = () => { + resetInput(); + onClose(); + }; + + const resetInput = () => { + setInputValue( '' ); + }; + + const handleDirectEntry = ( value ) => { + let type = 'URL'; + + const protocol = getProtocol( value ) || ''; + + if ( protocol.includes( 'mailto' ) ) { + type = 'mailto'; + } + + if ( protocol.includes( 'tel' ) ) { + type = 'tel'; + } + + if ( startsWith( value, '#' ) ) { + type = 'internal'; + } + + return Promise.resolve( + [ { + id: '-1', + title: value, + url: type === 'URL' ? prependHTTP( value ) : value, + type, + } ] + ); + }; + + const handleEntitySearch = async ( value ) => { + const results = await Promise.all( [ + fetchSearchSuggestions( value ), + handleDirectEntry( value ), + ] ); + + const couldBeURL = ! value.includes( ' ' ); + + // If it's potentially a URL search then concat on a URL search suggestion + // just for good measure. That way once the actual results run out we always + // have a URL option to fallback on. + return couldBeURL ? results[ 0 ].concat( results[ 1 ] ) : results[ 0 ]; + }; + + // Effects + const getSearchHandler = useCallback( ( value ) => { + const protocol = getProtocol( value ) || ''; + const isMailto = protocol.includes( 'mailto' ); + const isInternal = startsWith( value, '#' ); + const isTel = protocol.includes( 'tel' ); + + const handleManualEntry = isInternal || isMailto || isTel || isURL( value ) || ( value && value.includes( 'www.' ) ); + + return ( handleManualEntry ) ? handleDirectEntry( value ) : handleEntitySearch( value ); + }, [ handleDirectEntry, fetchSearchSuggestions ] ); + + // Render Components + const renderSearchResults = ( { suggestionsListProps, buildSuggestionItemProps, suggestions, selectedSuggestion, isLoading } ) => { + const resultsListClasses = classnames( 'block-editor-link-control__search-results', { + 'is-loading': isLoading, + } ); + + const manualLinkEntryTypes = [ 'url', 'mailto', 'tel', 'internal' ]; + + return ( +
+
+ { suggestions.map( ( suggestion, index ) => ( + onLinkChange( suggestion ) } + isSelected={ index === selectedSuggestion } + isURL={ manualLinkEntryTypes.includes( suggestion.type.toLowerCase() ) } + searchTerm={ inputValue } + /> + ) ) } +
+
+ ); + }; + + return ( + +
+
+ + { ( ! isEditingLink && currentLink ) && ( + +

+ { __( 'Currently selected' ) }: +

+
+ + + + { currentLink.title } + + { filterURLForDisplay( safeDecodeURI( currentLink.url ) ) || '' } + + + +
+
+ ) } + + { isEditingLink && ( + + ) } + + { ! isEditingLink && ( + + ) } +
+
+
+ ); +} + +export default compose( + withInstanceId, + withSelect( ( select, ownProps ) => { + if ( ownProps.fetchSearchSuggestions && isFunction( ownProps.fetchSearchSuggestions ) ) { + return; + } + + const { getSettings } = select( 'core/block-editor' ); + return { + fetchSearchSuggestions: getSettings().__experimentalFetchLinkSuggestions, + }; + } ) +)( LinkControl ); diff --git a/packages/block-editor/src/components/link-control/search-input.js b/packages/block-editor/src/components/link-control/search-input.js new file mode 100644 index 0000000000000..84fd5db835936 --- /dev/null +++ b/packages/block-editor/src/components/link-control/search-input.js @@ -0,0 +1,69 @@ + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { IconButton } from '@wordpress/components'; +import { ENTER } from '@wordpress/keycodes'; + +/** + * Internal dependencies + */ +import { URLInput } from '../'; + +const LinkControlSearchInput = ( { + value, + onChange, + onSelect, + renderSuggestions, + fetchSuggestions, + onReset, + onKeyDown, + onKeyPress, +} ) => { + const selectItemHandler = ( selection, suggestion ) => { + onChange( selection ); + + if ( suggestion ) { + onSelect( suggestion ); + } + }; + + const stopFormEventsPropagation = ( event ) => { + event.preventDefault(); + event.stopPropagation(); + }; + + return ( +
+ { + if ( event.keyCode === ENTER ) { + return; + } + onKeyDown( event ); + } } + onKeyPress={ onKeyPress } + placeholder={ __( 'Search or type url' ) } + __experimentalRenderSuggestions={ renderSuggestions } + __experimentalFetchLinkSuggestions={ fetchSuggestions } + __experimentalHandleURLSuggestions={ true } + /> + + + + + ); +}; + +export default LinkControlSearchInput; diff --git a/packages/block-editor/src/components/link-control/search-item.js b/packages/block-editor/src/components/link-control/search-item.js new file mode 100644 index 0000000000000..432b4bb3dff17 --- /dev/null +++ b/packages/block-editor/src/components/link-control/search-item.js @@ -0,0 +1,54 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + +/** + * Internal dependencies + */ +import TextHighlight from './text-highlight'; + +/** + * WordPress dependencies + */ +import { safeDecodeURI } from '@wordpress/url'; +import { __ } from '@wordpress/i18n'; + +import { + Icon, +} from '@wordpress/components'; + +export const LinkControlSearchItem = ( { itemProps, suggestion, isSelected = false, onClick, isURL = false, searchTerm = '' } ) => { + return ( + + ); +}; + +export default LinkControlSearchItem; + diff --git a/packages/block-editor/src/components/link-control/settings-drawer.js b/packages/block-editor/src/components/link-control/settings-drawer.js new file mode 100644 index 0000000000000..372426e4e821f --- /dev/null +++ b/packages/block-editor/src/components/link-control/settings-drawer.js @@ -0,0 +1,29 @@ +/** + * External dependencies + */ +import { partial } from 'lodash'; + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { + ToggleControl, +} from '@wordpress/components'; + +const LinkControlSettingsDrawer = ( { settings, onSettingChange } ) => { + if ( ! settings || settings.length ) { + return null; + } + + return ( +
+ +
+ ); +}; + +export default LinkControlSettingsDrawer; diff --git a/packages/block-editor/src/components/link-control/style.scss b/packages/block-editor/src/components/link-control/style.scss new file mode 100644 index 0000000000000..69f87d79fdfe3 --- /dev/null +++ b/packages/block-editor/src/components/link-control/style.scss @@ -0,0 +1,202 @@ +.block-editor-link-control__search { + position: relative; + min-width: $modal-min-width; +} + +.block-editor-link-control__search .block-editor-link-control__search-input { + // Specificity overide + &.block-editor-link-control__search-input > input[type="text"] { + width: calc(100% - #{$grid-size-large*2}); + display: block; + padding: 11px $grid-size-large; + margin: $grid-size-large; + padding-right: 38px; // width of reset button + position: relative; + z-index: 1; + border: 1px solid #e1e1e1; + border-radius: $radius-round-rectangle; + + /* Fonts smaller than 16px causes mobile safari to zoom. */ + font-size: $mobile-text-min-font-size; + + @include break-small { + font-size: $default-font-size; + } + + &:focus { + @include input-style__focus(); + } + } +} + +.block-editor-link-control__search-reset { + position: absolute; + top: 19px; // has to be hard coded as form expands with search suggestions + right: 19px; // push away to avoid focus style obscuring input border + z-index: 10; +} + +.block-editor-link-control__search-results-wrapper { + position: relative; + margin-top: -$grid-size-large + 1px; + + &::before, + &::after { + content: ""; + position: absolute; + left: -1px; + right: $grid-size-large; // avoid overlaying scrollbars + display: block; + pointer-events: none; + z-index: 100; + } + + &::before { + height: $grid-size-large/2; + top: -1px; + bottom: auto; + background: linear-gradient(to bottom, rgba(255, 255, 255, 1) 0%, rgba(255, 255, 255, 0) 100%); + } + + &::after { + height: 20px; + bottom: -1px; + top: auto; + background: linear-gradient(to bottom, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 1) 100%); + } +} + +.block-editor-link-control__search-results { + margin: 0; + padding: $grid-size-large/2 $grid-size-large; + max-height: 200px; + overflow-y: scroll; // allow results list to scroll + + &.is-loading { + opacity: 0.2; + } +} + +.block-editor-link-control__search-item { + position: relative; + display: flex; + align-items: center; + font-size: $default-font-size; + cursor: pointer; + background: $white; + width: 100%; + border: none; + text-align: left; + padding: 10px 15px; + border-radius: 5px; + + &:hover, + &:focus { + background-color: #e9e9e9; + } + + &.is-selected { + background: #f2f2f2; + + .block-editor-link-control__search-item-type { + background: #fff; + } + } + + &.is-current { + background: transparent; + border: 0; + width: 100%; + cursor: default; + padding: $grid-size-large; + padding-left: $grid-size-xlarge; + } + + .block-editor-link-control__search-item-header { + display: block; + margin-right: $grid-size-xlarge; + } + + .block-editor-link-control__search-item-icon { + margin-right: 1em; + min-width: 24px; + } + + .block-editor-link-control__search-item-info, + .block-editor-link-control__search-item-title { + text-overflow: ellipsis; + max-width: 230px; + overflow: hidden; + white-space: nowrap; + } + + .block-editor-link-control__search-item-title { + display: block; + margin-bottom: 0.2em; + font-weight: 500; + + mark { + font-weight: 700; + color: #000; + background-color: transparent; + } + + span { + font-weight: normal; + } + } + + .block-editor-link-control__search-item-info { + display: block; + color: #999; + font-size: 0.9em; + line-height: 1.3; + } + + .block-editor-link-control__search-item-type { + display: block; + padding: 3px 8px; + margin-left: auto; + font-size: 0.9em; + background-color: #f3f4f5; + border-radius: 2px; + } +} + +// Specificity overide +.block-editor-link-control__search-results div[role="menu"] > .block-editor-link-control__search-item.block-editor-link-control__search-item { + padding: 10px; +} + +.block-editor-link-control__settings { + border-top: 1px solid #e1e1e1; + margin: 0; + padding: $grid-size-large $grid-size-xlarge; + + :last-child { + margin-bottom: 0; + } +} + +.block-editor-link-control .block-editor-link-control__search-input .components-spinner { + display: block; + z-index: 100; + float: none; + + &.components-spinner { // Specificity overide + position: absolute; + top: 70px; + left: 50%; + right: auto; + bottom: auto; + margin: 0 auto 16px auto; + transform: translateX(-50%); + } + + +} + +.block-editor-link-control__search-item-action { + margin-left: auto; // push to far right hand side + flex-shrink: 0; +} diff --git a/packages/block-editor/src/components/link-control/test/__snapshots__/index.js.snap b/packages/block-editor/src/components/link-control/test/__snapshots__/index.js.snap new file mode 100644 index 0000000000000..df68c094eabc6 --- /dev/null +++ b/packages/block-editor/src/components/link-control/test/__snapshots__/index.js.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Basic rendering should display with required props 1`] = `"
"`; diff --git a/packages/block-editor/src/components/link-control/test/fixtures/index.js b/packages/block-editor/src/components/link-control/test/fixtures/index.js new file mode 100644 index 0000000000000..fc974749c982b --- /dev/null +++ b/packages/block-editor/src/components/link-control/test/fixtures/index.js @@ -0,0 +1,41 @@ +/** + * External dependencies + */ +import { uniqueId } from 'lodash'; + +export const fauxEntitySuggestions = [ + { + id: uniqueId(), + title: 'Hello Page', + type: 'Page', + info: '2 days ago', + url: `?p=${ uniqueId() }`, + }, + { + id: uniqueId(), + title: 'Hello Post', + type: 'Post', + info: '19 days ago', + url: `?p=${ uniqueId() }`, + }, + { + id: uniqueId(), + title: 'Hello Another One', + type: 'Page', + info: '19 days ago', + url: `?p=${ uniqueId() }`, + }, + { + id: uniqueId(), + title: 'This is another Post with a much longer title just to be really annoying and to try and break the UI', + type: 'Post', + info: '1 month ago', + url: `?p=${ uniqueId() }`, + }, +]; + +// export const fetchFauxEntitySuggestions = async () => fauxEntitySuggestions; + +export const fetchFauxEntitySuggestions = () => { + return Promise.resolve( fauxEntitySuggestions ); +}; diff --git a/packages/block-editor/src/components/link-control/test/index.js b/packages/block-editor/src/components/link-control/test/index.js new file mode 100644 index 0000000000000..d6dc506c7687b --- /dev/null +++ b/packages/block-editor/src/components/link-control/test/index.js @@ -0,0 +1,527 @@ +/** + * External dependencies + */ +import { render, unmountComponentAtNode } from 'react-dom'; +import { act, Simulate } from 'react-dom/test-utils'; +import { first, last, nth } from 'lodash'; +/** + * WordPress dependencies + */ +import { useState } from '@wordpress/element'; +import { UP, DOWN, ENTER } from '@wordpress/keycodes'; +/** + * Internal dependencies + */ +import LinkControl from '../index'; +import { fauxEntitySuggestions, fetchFauxEntitySuggestions } from './fixtures'; + +function eventLoopTick() { + return new Promise( ( resolve ) => setImmediate( resolve ) ); +} + +let container = null; + +beforeEach( () => { + // setup a DOM element as a render target + container = document.createElement( 'div' ); + document.body.appendChild( container ); +} ); + +afterEach( () => { + // cleanup on exiting + unmountComponentAtNode( container ); + container.remove(); + container = null; +} ); + +describe( 'Basic rendering', () => { + it( 'should display with required props', () => { + act( () => { + render( + , container + ); + } ); + + // Search Input UI + const searchInput = container.querySelector( 'input[aria-label="URL"]' ); + + // expect( searchInputLabel ).not.toBeNull(); + expect( searchInput ).not.toBeNull(); + + expect( container.innerHTML ).toMatchSnapshot(); + } ); +} ); + +describe( 'Searching for a link', () => { + it( 'should display loading UI when input is valid but search results have yet to be returned', async () => { + const searchTerm = 'Hello'; + + let resolver; + + const fauxRequest = () => new Promise( ( resolve ) => { + resolver = resolve; + } ); + + act( () => { + render( + , container + ); + } ); + + // Search Input UI + const searchInput = container.querySelector( 'input[aria-label="URL"]' ); + + // Simulate searching for a term + act( () => { + Simulate.change( searchInput, { target: { value: searchTerm } } ); + } ); + + // fetchFauxEntitySuggestions resolves on next "tick" of event loop + await eventLoopTick(); + + // TODO: select these by aria relationship to autocomplete rather than arbitary selector. + const searchResultElements = container.querySelectorAll( '[role="menu"] button[role="menuitem"]' ); + + let loadingUI = container.querySelector( '.components-spinner' ); + + expect( searchResultElements ).toHaveLength( 0 ); + + expect( loadingUI ).not.toBeNull(); + + act( () => { + resolver( fauxEntitySuggestions ); + } ); + + await eventLoopTick(); + + loadingUI = container.querySelector( '.components-spinner' ); + + expect( loadingUI ).toBeNull(); + } ); + + it( 'should display only search suggestions when current input value is not URL-like', async ( ) => { + const searchTerm = 'Hello world'; + const firstFauxSuggestion = first( fauxEntitySuggestions ); + + act( () => { + render( + , container + ); + } ); + + // Search Input UI + const searchInput = container.querySelector( 'input[aria-label="URL"]' ); + + // Simulate searching for a term + act( () => { + Simulate.change( searchInput, { target: { value: searchTerm } } ); + } ); + + // fetchFauxEntitySuggestions resolves on next "tick" of event loop + await eventLoopTick(); + + // TODO: select these by aria relationship to autocomplete rather than arbitary selector. + const searchResultElements = container.querySelectorAll( '[role="listbox"] [role="option"]' ); + const firstSearchResultItemHTML = first( searchResultElements ).innerHTML; + const lastSearchResultItemHTML = last( searchResultElements ).innerHTML; + + expect( searchResultElements ).toHaveLength( fauxEntitySuggestions.length ); + + // Sanity check that a search suggestion shows up corresponding to the data + expect( firstSearchResultItemHTML ).toEqual( expect.stringContaining( firstFauxSuggestion.title ) ); + expect( firstSearchResultItemHTML ).toEqual( expect.stringContaining( firstFauxSuggestion.type ) ); + + // The fallback URL suggestion should not be shown when input is not URL-like + expect( lastSearchResultItemHTML ).not.toEqual( expect.stringContaining( 'URL' ) ); + } ); + + it.each( [ + [ 'couldbeurlorentitysearchterm' ], + [ 'ThisCouldAlsoBeAValidURL' ], + ] )( 'should display a URL suggestion as a default fallback for the search term "%s" which could potentially be a valid url.', async ( searchTerm ) => { + act( () => { + render( + , container + ); + } ); + + // Search Input UI + const searchInput = container.querySelector( 'input[aria-label="URL"]' ); + + // Simulate searching for a term + act( () => { + Simulate.change( searchInput, { target: { value: searchTerm } } ); + } ); + + // fetchFauxEntitySuggestions resolves on next "tick" of event loop + await eventLoopTick(); + + // TODO: select these by aria relationship to autocomplete rather than arbitary selector. + const searchResultElements = container.querySelectorAll( '[role="listbox"] [role="option"]' ); + const lastSearchResultItemHTML = last( searchResultElements ).innerHTML; + const additionalDefaultFallbackURLSuggestionLength = 1; + + // We should see a search result for each of the expect search suggestions + // plus 1 additional one for the fallback URL suggestion + expect( searchResultElements ).toHaveLength( fauxEntitySuggestions.length + additionalDefaultFallbackURLSuggestionLength ); + + // The last item should be a URL search suggestion + expect( lastSearchResultItemHTML ).toEqual( expect.stringContaining( searchTerm ) ); + expect( lastSearchResultItemHTML ).toEqual( expect.stringContaining( 'URL' ) ); + expect( lastSearchResultItemHTML ).toEqual( expect.stringContaining( 'Press ENTER to add this link' ) ); + } ); + + it( 'should reset the input field and the search results when search term is cleared or reset', async ( ) => { + const searchTerm = 'Hello world'; + + act( () => { + render( + , container + ); + } ); + + let searchResultElements; + let searchInput; + + // Search Input UI + searchInput = container.querySelector( 'input[aria-label="URL"]' ); + + // Simulate searching for a term + act( () => { + Simulate.change( searchInput, { target: { value: searchTerm } } ); + } ); + + // fetchFauxEntitySuggestions resolves on next "tick" of event loop + await eventLoopTick(); + + // TODO: select these by aria relationship to autocomplete rather than arbitary selector. + searchResultElements = container.querySelectorAll( '[role="listbox"] [role="option"]' ); + + // Check we have definitely rendered some suggestions + expect( searchResultElements ).toHaveLength( fauxEntitySuggestions.length ); + + // Grab the reset button now it's available + const resetUI = container.querySelector( '[aria-label="Reset"]' ); + + act( () => { + Simulate.click( resetUI ); + } ); + + await eventLoopTick(); + + // TODO: select these by aria relationship to autocomplete rather than arbitary selector. + searchResultElements = container.querySelectorAll( '[role="listbox"] [role="option"]' ); + searchInput = container.querySelector( 'input[aria-label="URL"]' ); + + expect( searchInput.value ).toBe( '' ); + expect( searchResultElements ).toHaveLength( 0 ); + } ); +} ); + +describe( 'Manual link entry', () => { + it.each( [ + [ 'https://make.wordpress.org' ], // explicit https + [ 'http://make.wordpress.org' ], // explicit http + [ 'www.wordpress.org' ], // usage of "www" + ] )( 'should display a single suggestion result when the current input value is URL-like (eg: %s)', async ( searchTerm ) => { + act( () => { + render( + , container + ); + } ); + + // Search Input UI + const searchInput = container.querySelector( 'input[aria-label="URL"]' ); + + // Simulate searching for a term + act( () => { + Simulate.change( searchInput, { target: { value: searchTerm } } ); + } ); + + // fetchFauxEntitySuggestions resolves on next "tick" of event loop + await eventLoopTick(); + + // TODO: select these by aria relationship to autocomplete rather than arbitary selector. + const searchResultElements = container.querySelectorAll( '[role="listbox"] [role="option"]' ); + const firstSearchResultItemHTML = searchResultElements[ 0 ].innerHTML; + const expectedResultsLength = 1; + + expect( searchResultElements ).toHaveLength( expectedResultsLength ); + expect( firstSearchResultItemHTML ).toEqual( expect.stringContaining( searchTerm ) ); + expect( firstSearchResultItemHTML ).toEqual( expect.stringContaining( 'URL' ) ); + expect( firstSearchResultItemHTML ).toEqual( expect.stringContaining( 'Press ENTER to add this link' ) ); + } ); + + describe( 'Alternative link protocols and formats', () => { + it.each( [ + [ 'mailto:example123456@wordpress.org', 'mailto' ], + [ 'tel:example123456@wordpress.org', 'tel' ], + [ '#internal-anchor', 'internal' ], + ] )( 'should recognise "%s" as a %s link and handle as manual entry by displaying a single suggestion', async ( searchTerm, searchType ) => { + act( () => { + render( + , container + ); + } ); + + // Search Input UI + const searchInput = container.querySelector( 'input[aria-label="URL"]' ); + + // Simulate searching for a term + act( () => { + Simulate.change( searchInput, { target: { value: searchTerm } } ); + } ); + + // fetchFauxEntitySuggestions resolves on next "tick" of event loop + await eventLoopTick(); + + // TODO: select these by aria relationship to autocomplete rather than arbitary selector. + const searchResultElements = container.querySelectorAll( '[role="listbox"] [role="option"]' ); + const firstSearchResultItemHTML = searchResultElements[ 0 ].innerHTML; + const expectedResultsLength = 1; + + expect( searchResultElements ).toHaveLength( expectedResultsLength ); + expect( firstSearchResultItemHTML ).toEqual( expect.stringContaining( searchTerm ) ); + expect( firstSearchResultItemHTML ).toEqual( expect.stringContaining( searchType ) ); + expect( firstSearchResultItemHTML ).toEqual( expect.stringContaining( 'Press ENTER to add this link' ) ); + } ); + } ); +} ); + +describe( 'Selecting links', () => { + it( 'should display a selected link corresponding to the provided "currentLink" prop', () => { + const selectedLink = first( fauxEntitySuggestions ); + + const LinkControlConsumer = () => { + const [ link ] = useState( selectedLink ); + + return ( + + ); + }; + + act( () => { + render( + , container + ); + } ); + + // TODO: select by aria role or visible text + const currentLink = container.querySelector( '.block-editor-link-control__search-item.is-current' ); + const currentLinkHTML = currentLink.innerHTML; + const currentLinkAnchor = currentLink.querySelector( `[href="${ selectedLink.url }"]` ); + + expect( currentLinkHTML ).toEqual( expect.stringContaining( selectedLink.title ) ); + expect( currentLinkHTML ).toEqual( expect.stringContaining( selectedLink.type ) ); + expect( currentLinkHTML ).toEqual( expect.stringContaining( 'Change' ) ); + expect( currentLinkAnchor ).not.toBeNull(); + } ); + + it( 'should remove currently selected link and (re)display search UI when "Change" button is clicked', () => { + const selectedLink = first( fauxEntitySuggestions ); + + const LinkControlConsumer = () => { + const [ link, setLink ] = useState( selectedLink ); + + return ( + setLink( suggestion ) } + fetchSearchSuggestions={ fetchFauxEntitySuggestions } + /> + ); + }; + + act( () => { + render( + , container + ); + } ); + + // TODO: select by aria role or visible text + let currentLink = container.querySelector( '.block-editor-link-control__search-item.is-current' ); + + const currentLinkBtn = currentLink.querySelector( 'button' ); + + // Simulate searching for a term + act( () => { + Simulate.click( currentLinkBtn ); + } ); + + const searchInput = container.querySelector( 'input[aria-label="URL"]' ); + currentLink = container.querySelector( '.block-editor-link-control__search-item.is-current' ); + + // We should be back to showing the search input + expect( searchInput ).not.toBeNull(); + expect( currentLink ).toBeNull(); + } ); + + describe( 'Selection using mouse click', () => { + it.each( [ + [ 'entity', 'hello world', first( fauxEntitySuggestions ) ], // entity search + [ 'url', 'https://www.wordpress.org', { + id: '1', + title: 'https://www.wordpress.org', + url: 'https://www.wordpress.org', + type: 'URL', + } ], // url + ] )( 'should display a current selected link UI when a %s suggestion for the search "%s" is clicked', async ( type, searchTerm, selectedLink ) => { + const LinkControlConsumer = () => { + const [ link, setLink ] = useState( null ); + + return ( + setLink( suggestion ) } + fetchSearchSuggestions={ fetchFauxEntitySuggestions } + /> + ); + }; + + act( () => { + render( + , container + ); + } ); + + // Search Input UI + const searchInput = container.querySelector( 'input[aria-label="URL"]' ); + + // Simulate searching for a term + act( () => { + Simulate.change( searchInput, { target: { value: searchTerm } } ); + } ); + + // fetchFauxEntitySuggestions resolves on next "tick" of event loop + await eventLoopTick(); + + // TODO: select these by aria relationship to autocomplete rather than arbitary selector. + const searchResultElements = container.querySelectorAll( '[role="listbox"] [role="option"]' ); + + const firstSearchSuggestion = first( searchResultElements ); + + // Simulate selecting the first of the search suggestions + act( () => { + Simulate.click( firstSearchSuggestion ); + } ); + + const currentLink = container.querySelector( '.block-editor-link-control__search-item.is-current' ); + const currentLinkHTML = currentLink.innerHTML; + const currentLinkAnchor = currentLink.querySelector( `[href="${ selectedLink.url }"]` ); + + // Check that this suggestion is now shown as selected + expect( currentLinkHTML ).toEqual( expect.stringContaining( selectedLink.title ) ); + expect( currentLinkHTML ).toEqual( expect.stringContaining( 'Change' ) ); + expect( currentLinkAnchor ).not.toBeNull(); + } ); + } ); + + describe( 'Selection using keyboard', () => { + it.each( [ + [ 'entity', 'hello world', first( fauxEntitySuggestions ) ], // entity search + [ 'url', 'https://www.wordpress.org', { + id: '1', + title: 'https://www.wordpress.org', + url: 'https://www.wordpress.org', + type: 'URL', + } ], // url + ] )( 'should display a current selected link UI when an %s suggestion for the search "%s" is selected using the keyboard', async ( type, searchTerm, selectedLink ) => { + const LinkControlConsumer = () => { + const [ link, setLink ] = useState( null ); + + return ( + setLink( suggestion ) } + fetchSearchSuggestions={ fetchFauxEntitySuggestions } + /> + ); + }; + + act( () => { + render( + , container + ); + } ); + + // Search Input UI + const searchInput = container.querySelector( 'input[aria-label="URL"]' ); + + // Simulate searching for a term + act( () => { + Simulate.change( searchInput, { target: { value: searchTerm } } ); + } ); + + //fetchFauxEntitySuggestions resolves on next "tick" of event loop + await eventLoopTick(); + + // Step down into the search results, highlighting the first result item + act( () => { + Simulate.keyDown( searchInput, { keyCode: DOWN } ); + } ); + + // TODO: select these by aria relationship to autocomplete rather than arbitary selector. + const searchResultElements = container.querySelectorAll( '[role="listbox"] [role="option"]' ); + const firstSearchSuggestion = first( searchResultElements ); + const secondSearchSuggestion = nth( searchResultElements, 1 ); + + let selectedSearchResultElement = container.querySelector( '[role="option"][aria-selected="true"]' ); + + // We should have highlighted the first item using the keyboard + expect( selectedSearchResultElement ).toEqual( firstSearchSuggestion ); + + // Only entity searches contain more than 1 suggestion + if ( type === 'entity' ) { + // Check we can go down again using the down arrow + act( () => { + Simulate.keyDown( searchInput, { keyCode: DOWN } ); + } ); + + selectedSearchResultElement = container.querySelector( '[role="option"][aria-selected="true"]' ); + + // We should have highlighted the first item using the keyboard + expect( selectedSearchResultElement ).toEqual( secondSearchSuggestion ); + + // Check we can go back up via up arrow + act( () => { + Simulate.keyDown( searchInput, { keyCode: UP } ); + } ); + + selectedSearchResultElement = container.querySelector( '[role="option"][aria-selected="true"]' ); + + // We should be back to highlighting the first search result again + expect( selectedSearchResultElement ).toEqual( firstSearchSuggestion ); + } + + // Commit the selected item as the current link + act( () => { + Simulate.keyDown( searchInput, { keyCode: ENTER } ); + } ); + + // Check that the suggestion selected via is now shown as selected + const currentLink = container.querySelector( '.block-editor-link-control__search-item.is-current' ); + const currentLinkHTML = currentLink.innerHTML; + const currentLinkAnchor = currentLink.querySelector( `[href="${ selectedLink.url }"]` ); + + expect( currentLinkHTML ).toEqual( expect.stringContaining( selectedLink.title ) ); + expect( currentLinkHTML ).toEqual( expect.stringContaining( 'Change' ) ); + expect( currentLinkAnchor ).not.toBeNull(); + } ); + } ); +} ); diff --git a/packages/block-editor/src/components/link-control/text-highlight.js b/packages/block-editor/src/components/link-control/text-highlight.js new file mode 100644 index 0000000000000..dc7b35a3d6d2b --- /dev/null +++ b/packages/block-editor/src/components/link-control/text-highlight.js @@ -0,0 +1,29 @@ +/** + * External dependencies + */ +import { escapeRegExp } from 'lodash'; + +/** + * WordPress dependencies + */ +import { + Fragment, +} from '@wordpress/element'; + +const TextHighlight = ( { text = '', highlight = '' } ) => { + if ( ! highlight.trim() ) { + return text; + } + + const regex = new RegExp( `(${ escapeRegExp( highlight ) })`, 'gi' ); + const parts = text.split( regex ); + return ( + + { parts.filter( ( part ) => part ).map( ( part, i ) => ( + regex.test( part ) ? { part } : { part } + ) ) } + + ); +}; + +export default TextHighlight; diff --git a/packages/block-editor/src/components/media-placeholder/index.js b/packages/block-editor/src/components/media-placeholder/index.js index d142d6d6c7000..d4e98de6179b1 100644 --- a/packages/block-editor/src/components/media-placeholder/index.js +++ b/packages/block-editor/src/components/media-placeholder/index.js @@ -429,7 +429,7 @@ const applyWithSelect = withSelect( ( select ) => { const { getSettings } = select( 'core/block-editor' ); return { - mediaUpload: getSettings().__experimentalMediaUpload, + mediaUpload: getSettings().mediaUpload, }; } ); diff --git a/packages/block-editor/src/components/media-upload/check.js b/packages/block-editor/src/components/media-upload/check.js index 6f2903be5b1e1..c1fb3398020f4 100644 --- a/packages/block-editor/src/components/media-upload/check.js +++ b/packages/block-editor/src/components/media-upload/check.js @@ -14,6 +14,6 @@ export default withSelect( ( select ) => { const { getSettings } = select( 'core/block-editor' ); return { - hasUploadPermissions: !! getSettings().__experimentalMediaUpload, + hasUploadPermissions: !! getSettings().mediaUpload, }; } )( MediaUploadCheck ); diff --git a/packages/block-editor/src/components/media-upload/index.native.js b/packages/block-editor/src/components/media-upload/index.native.js index 77f125fb35f91..2e1c3a9fcd801 100644 --- a/packages/block-editor/src/components/media-upload/index.native.js +++ b/packages/block-editor/src/components/media-upload/index.native.js @@ -6,6 +6,8 @@ import { requestMediaPickFromMediaLibrary, requestMediaPickFromDeviceLibrary, requestMediaPickFromDeviceCamera, + getOtherMediaOptions, + requestOtherMediaPickFrom, } from 'react-native-gutenberg-bridge'; /** @@ -31,7 +33,27 @@ export class MediaUpload extends React.Component { this.onPickerPresent = this.onPickerPresent.bind( this ); this.onPickerChange = this.onPickerChange.bind( this ); this.onPickerSelect = this.onPickerSelect.bind( this ); + + this.state = { + otherMediaOptions: undefined, + }; + } + + componentDidMount() { + const { allowedTypes = [] } = this.props; + getOtherMediaOptions( allowedTypes, ( otherMediaOptions ) => { + const otherMediaOptionsWithIcons = otherMediaOptions.map( ( option ) => { + return { + icon: this.getChooseFromDeviceIcon(), + value: option.value, + label: option.label, + }; + } ); + + this.setState( { otherMediaOptions: otherMediaOptionsWithIcons } ); + } ); } + getTakeMediaLabel() { const { allowedTypes = [] } = this.props; @@ -98,11 +120,22 @@ export class MediaUpload extends React.Component { this.onPickerSelect( requestMediaPickFromDeviceCamera ); } else if ( value === MEDIA_UPLOAD_BOTTOM_SHEET_VALUE_WORD_PRESS_LIBRARY ) { this.onPickerSelect( requestMediaPickFromMediaLibrary ); + } else { + const { onSelect, multiple = false } = this.props; + requestOtherMediaPickFrom( value, multiple, ( media ) => { + if ( ( multiple && media ) || ( media && media.id ) ) { + onSelect( media ); + } + } ); } } render() { - const mediaOptions = this.getMediaOptionsItems(); + let mediaOptions = this.getMediaOptionsItems(); + + if ( this.state.otherMediaOptions ) { + mediaOptions = [ ...mediaOptions, ...this.state.otherMediaOptions ]; + } const getMediaOptions = () => ( ` component passing the required props plus: + +1. a `renderDefaultControl` function which renders an interface control. +2. an boolean state for `isResponsive` (see "Props" below). +3. a handler function for `onIsResponsiveChange` (see "Props" below). + + +By default the default control will be used to render the default (ie: "All") setting _as well as_ the per-viewport responsive settings. + +```jsx +import { registerBlockType } from '@wordpress/blocks'; +import { + InspectorControls, + ResponsiveBlockControl, +} from '@wordpress/block-editor'; + +import { useState } from '@wordpress/element'; + +import { + DimensionControl, +} from '@wordpress/components'; + +registerBlockType( 'my-plugin/my-block', { + // ... + + edit( { attributes, setAttributes } ) { + + const [ isResponsive, setIsResponsive ] = useState( false ); + + // Used for example purposes only + const sizeOptions = [ + { + label: 'Small', + value: 'small', + }, + { + label: 'Medium', + value: 'medium', + }, + { + label: 'Large', + value: 'large', + }, + ]; + + const { paddingSize } = attributes; + + + // Your custom control can be anything you'd like to use. + // You are not restricted to `DimensionControl`s, but this + // makes life easier if dealing with standard CSS values. + // see `packages/components/src/dimension-control/README.md` + const paddingControl = ( labelComponent, viewport ) => { + return ( + + ); + }; + + return ( + <> + + { + setIsResponsive( ! isResponsive ); + } } + /> + +
+ // your Block here +
+ + ); + } +} ); +``` + +## Props + +### `title` +* **Type:** `String` +* **Default:** `undefined` +* **Required:** `true` + +The title of the control group used in the `fieldset`'s `legend` element to label the _entire_ set of controls. + +### `property` +* **Type:** `String` +* **Default:** `undefined` +* **Required:** `true` + +Used to build accessible labels and ARIA roles for the control group. Should represent the layout property which the component controls (eg: `padding`, `margin`...etc). + +### `isResponsive` +* **Type:** `Boolean` +* **Default:** `false` ) +* **Required:** `false` + +Determines whether the component displays the default or responsive controls. Updates the state of the toggle control. See also `onIsResponsiveChange` below. + +### `onIsResponsiveChange` +* **Type:** `Function` +* **Default:** `undefined` +* **Required:** `true` + +A callback function invoked when the component's toggle value is changed between responsive and non-responsive mode. Should be used to update the value of the `isResponsive` prop to reflect the current state of the toggle control. + +### `renderDefaultControl` +* **Type:** `Function` +* **Default:** `undefined` +* **Required:** `true` +* **Args:** + - **labelComponent:** (`Function`) - a rendered `ResponsiveBlockControlLabel` component for your control. + - **viewport:** (`Object`) - an object representing viewport attributes for your control. + +A render function (prop) used to render the control for which you would like to display per viewport settings. + +For example, if you have a `SelectControl` which controls padding size, then pass this component as `renderDefaultControl` and it will be used to render both default and "responsive" controls for "padding". + +The component you return from this function will be used to render the control displayed for the (default) "All" state and (if the `renderResponsiveControls` is not provided) the individual responsive controls when in "responsive" mode. + +It is passed a pre-created, accessible `