From f334112e2cf973567c7171d3c46b828e42670add Mon Sep 17 00:00:00 2001 From: Sara Marcondes Date: Fri, 17 Apr 2020 09:24:27 -0700 Subject: [PATCH 01/15] Add LazyLoad component This component allows us to wrap existing components and delay them until required 3rd party scripts and styles are asynchronously loaded. It also provides a hook for post-load setup that resolves before the children are rendered, guaranteeing that the children aren't active until the world around them is set up correctly. --- packages/block-library/src/classic/lazy.js | 89 ++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 packages/block-library/src/classic/lazy.js diff --git a/packages/block-library/src/classic/lazy.js b/packages/block-library/src/classic/lazy.js new file mode 100644 index 00000000000000..b0021448fb0f71 --- /dev/null +++ b/packages/block-library/src/classic/lazy.js @@ -0,0 +1,89 @@ +/** + * WordPress dependencies + */ +import { Component } from '@wordpress/element'; + +/** + * External dependencies + */ +import { noop } from 'lodash'; + +export default class LazyLoad extends Component { + static loadedScripts = new Set(); + static loadedStyles = new Set(); + static defaultProps = { + scripts: [], + styles: [], + placeholder: null, + onLoaded: () => Promise.resolve(), + onError: noop, + }; + + state = { + loaded: false, + }; + + loadScripts = async () => { + const scriptsToLoad = this.props.scripts.filter( + ( script ) => ! this.loadedScripts.has( script ) + ); + + if ( scriptsToLoad.length === 0 ) { + return Promise.resolve(); + } + + await new Promise( ( resolve, reject ) => { + const scriptElement = document.createElement( 'script' ); + // can't use load-scripts.php, so this would need to be replaced + scriptElement.src = `/wp-admin/load-scripts.php?load=${ scriptsToLoad.join( + ',' + ) }`; + scriptElement.onload = resolve; + scriptElement.onerror = reject; + document.head.appendChild( scriptElement ); + } ); + + scriptsToLoad.forEach( ( script ) => this.loadedScripts.add( script ) ); + }; + + loadStyles = async () => { + const stylesToLoad = this.props.styles.filter( + ( style ) => ! this.loadedStyles.has( style ) + ); + + if ( stylesToLoad.length === 0 ) { + return Promise.resolve(); + } + + await new Promise( ( resolve, reject ) => { + const linkElement = document.createElement( 'link' ); + linkElement.rel = 'stylesheet'; + // can't use load-styles.php, so this would need to be replaced + linkElement.href = `/wp-admin/load-styles.php?load=${ stylesToLoad.join( + ',' + ) }`; + linkElement.onload = resolve; + linkElement.onerror = reject; + document.head.appendChild( linkElement ); + } ); + + stylesToLoad.forEach( ( style ) => this.loadedStyles.add( style ) ); + }; + + componentDidMount() { + Promise.all( [ this.loadScripts(), this.loadStyles() ] ) + .then( this.props.onLoaded ) + .then( () => { + this.setState( { loaded: true } ); + } ) + .catch( this.props.onError ); + } + + render() { + if ( this.state.loaded ) { + return this.props.children; + } + + return this.props.placeholder; + } +} From a2e071e4f80fe90f1b190a5f0dd00a785aed32f8 Mon Sep 17 00:00:00 2001 From: Sara Marcondes Date: Fri, 17 Apr 2020 09:27:05 -0700 Subject: [PATCH 02/15] WIP: Lazy load tineMCE for the classic block --- packages/block-library/src/classic/edit.js | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/packages/block-library/src/classic/edit.js b/packages/block-library/src/classic/edit.js index 7ea8b295c8aefc..873f26885b0326 100644 --- a/packages/block-library/src/classic/edit.js +++ b/packages/block-library/src/classic/edit.js @@ -5,6 +5,11 @@ import { Component } from '@wordpress/element'; import { __, _x } from '@wordpress/i18n'; import { BACKSPACE, DELETE, F10 } from '@wordpress/keycodes'; +/** + * Internal dependencies + */ +import LazyLoad from './lazy'; + const { wp } = window; function isTmceEmpty( editor ) { @@ -23,7 +28,7 @@ function isTmceEmpty( editor ) { return /^\n?$/.test( body.innerText || body.textContent ); } -export default class ClassicEdit extends Component { +class ClassicEdit extends Component { constructor( props ) { super( props ); this.initialize = this.initialize.bind( this ); @@ -216,3 +221,16 @@ export default class ClassicEdit extends Component { /* eslint-enable jsx-a11y/no-static-element-interactions */ } } + +const LazyClassicEdit = ( props ) => ( + window.wpMceTranslation() } + placeholder={
Loading...
} + > + +
+); + +export default LazyClassicEdit; From bac6771e8fbc281c913f7f4753bbe79ea5a569df Mon Sep 17 00:00:00 2001 From: Sara Marcondes Date: Wed, 22 Apr 2020 09:53:03 -0700 Subject: [PATCH 03/15] Only print TinyMCE scripts outside of the block editor screen --- gutenberg.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/gutenberg.php b/gutenberg.php index 4dc2ea3e862c23..765f0c7d5988ff 100644 --- a/gutenberg.php +++ b/gutenberg.php @@ -159,3 +159,21 @@ function gutenberg_rest_nonce() { exit( wp_create_nonce( 'wp_rest' ) ); } add_action( 'wp_ajax_gutenberg_rest_nonce', 'gutenberg_rest_nonce' ); + +/** + * Prints TinyMCE scripts when we're outside of the block editor. + * Otherwise, use the default TinyMCE script printing behavior. + * + * @since 7.9.1 + */ +function gutenberg_print_tinymce_scripts() { + $current_screen = get_current_screen(); + if ( ! $current_screen->is_block_editor() ) { + // maybe also need to check a setting/feature flag? Put this behind phase 2? + wp_print_tinymce_scripts(); + } else { + echo ""; + } +} +remove_action( 'print_tinymce_scripts', 'wp_print_tinymce_scripts' ); +add_action( 'print_tinymce_scripts', 'gutenberg_print_tinymce_scripts' ); From 99c7af25faa6d1e8c458eb71e1c697625572b9a4 Mon Sep 17 00:00:00 2001 From: Sara Marcondes Date: Wed, 22 Apr 2020 10:11:12 -0700 Subject: [PATCH 04/15] Inline a window function for TinyMCE i18n init --- gutenberg.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/gutenberg.php b/gutenberg.php index 765f0c7d5988ff..511310f495de86 100644 --- a/gutenberg.php +++ b/gutenberg.php @@ -173,6 +173,10 @@ function gutenberg_print_tinymce_scripts() { wp_print_tinymce_scripts(); } else { echo ""; + echo "\n"; } } remove_action( 'print_tinymce_scripts', 'wp_print_tinymce_scripts' ); From 26a3b7c25e61119c6c48bf81bbb9557743e4845c Mon Sep 17 00:00:00 2001 From: Jonny Harris Date: Sun, 29 Mar 2020 19:42:02 +0100 Subject: [PATCH 05/15] Add script and style REST APIs --- lib/class-wp-rest-dependencies-controller.php | 506 ++++++++++++++++++ lib/class-wp-rest-scripts-controller.php | 106 ++++ lib/class-wp-rest-styles-controller.php | 80 +++ lib/load.php | 9 + lib/rest-api.php | 14 + 5 files changed, 715 insertions(+) create mode 100755 lib/class-wp-rest-dependencies-controller.php create mode 100755 lib/class-wp-rest-scripts-controller.php create mode 100755 lib/class-wp-rest-styles-controller.php diff --git a/lib/class-wp-rest-dependencies-controller.php b/lib/class-wp-rest-dependencies-controller.php new file mode 100755 index 00000000000000..47674283cd797c --- /dev/null +++ b/lib/class-wp-rest-dependencies-controller.php @@ -0,0 +1,506 @@ +namespace, + '/' . $this->rest_base, + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + 'schema' => array( $this, 'get_item_schema' ), + ) + ); + + $get_item_args = array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ); + + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/(?P[\w-]+)', + array( + 'args' => array( + 'handle' => array( + 'description' => __( 'Unique identifier for the object.' ), + 'type' => 'string', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => $get_item_args, + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + } + + /** + * @param WP_REST_Request $request + * + * @return array|WP_Error|WP_REST_Response + */ + public function get_items( $request ) { + $data = []; + $handle = $request['dependency']; + $filter = []; + if ( $handle ) { + $this->object->all_deps( $handle ); + $filter = $this->object->to_do; + } + + if ( $handle ) { + foreach ( $filter as $dependency_handle ) { + foreach ( $this->object->registered as $dependency ) { + if ( $dependency_handle === $dependency->handle ) { + $item = $this->prepare_item_for_response( $dependency, $request ); + $data[] = $this->prepare_response_for_collection( $item ); + } + } + } + } else { + foreach ( $this->object->registered as $dependency ) { + $item = $this->prepare_item_for_response( $dependency, $request ); + $data[] = $this->prepare_response_for_collection( $item ); + } + } + + return $data; + } + + /** + * @param WP_REST_Request $request + * + * @return array|mixed|WP_Error|WP_REST_Response + */ + public function get_item( $request ) { + if ( ! isset( $this->object->registered[ $request['handle'] ] ) ) { + return []; + } + $dependency = $this->object->registered[ $request['handle'] ]; + $data = $this->prepare_item_for_response( $dependency, $request ); + + return $data; + } + + /** + * @param mixed $dependency + * @param WP_REST_Request $request + * + * @return mixed|WP_Error|WP_REST_Response + */ + public function prepare_item_for_response( $dependency, $request ) { + $dependency->url = $this->get_url( $dependency->src, $dependency->ver, $dependency->handle ); + $response = rest_ensure_response( (array) $dependency ); + $dependencies = $this->prepare_links( $dependency ); + $response->add_links( $dependencies ); + + return $response; + } + + /** + * @param WP_REST_Request $request + * + * @return bool|true|WP_Error + */ + public function get_items_permissions_check( $request ) { + if ( $this->check_handle( $request['dependency'] ) ) { + return true; + } + + return current_user_can( 'manage_options' ); + } + + /** + * @param WP_REST_Request $request + * + * @return bool|true|WP_Error + */ + public function get_item_permissions_check( $request ) { + if ( $this->check_handle( $request['handle'] ) ) { + return true; + } + + return current_user_can( 'manage_options' ); + } + + /** + * @param $dependency + * + * @return array + */ + protected function prepare_links( $dependency ) { + $base = sprintf( '%s/%s', $this->namespace, $this->rest_base ); + // Entity meta. + $links = array( + 'self' => array( + 'href' => rest_url( trailingslashit( $base ) . $dependency->handle ), + ), + 'collection' => array( + 'href' => rest_url( $base ), + ), + 'deps' => array( + 'href' => rest_url( trailingslashit( $base ) . '?dependency=' . $dependency->handle ), + ), + ); + + return $links; + } + + /** + * @return array + */ + public function get_collection_params() { + $query_params = parent::get_collection_params(); + $query_params['context']['default'] = 'view'; + + $query_params['dependency'] = array( + 'description' => __( '' ), + 'type' => 'string', + ); + + return $query_params; + } + + /** + * @param $handle + * + * @return bool + */ + protected function check_handle( $handle ) { + + if ( ! $handle ) { + return false; + } + + // All core assets should be public. + if ( in_array( $handle, $this->get_core_assets(), true ) ) { + return true; + } + + // All block public assets should also be public. + if ( in_array( $handle, $this->block_asset( $this->block_dependency ), true ) ) { + return true; + } + + // All block edit assets should check if user is logged in and has the ability to using the editor. + if ( in_array( $handle, $this->block_asset( $this->editor_block_dependency ), true ) ) { + return current_user_can( 'edit_posts' ); + } + + return false; + } + + /** + * @return array + */ + public function get_core_assets() { + /* translators: %s: Method name. */ + _doing_it_wrong( sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ), 'x.x' ); + + return array(); + } + + /** + * @param $field + * + * @return array + */ + protected function block_asset( $field ) { + if ( ! $field ) { + return array(); + } + + $block_registry = WP_Block_Type_Registry::get_instance(); + $blocks = $block_registry->get_all_registered(); + $handles = wp_list_pluck( $blocks, $field ); + $handles = array_values( $handles ); + $handles = array_filter( $handles ); + + return $handles; + } + +} +< ? php + +/** + * Class WP_REST_Dependencies_Controller + */ +class WP_REST_Dependencies_Controller extends WP_REST_Controller { + + /** + * @var + */ + protected $object; + + + /** + * @var + */ + protected $editor_block_dependency = ''; + + + /** + * @var + */ + protected $block_dependency = ''; + + /** + * + */ + public function register_routes() { + register_rest_route( + $this->namespace, + '/' . $this->rest_base, + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + 'schema' => array( $this, 'get_item_schema' ), + ) + ); + + $get_item_args = array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ); + + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/(?P[\w-]+)', + array( + 'args' => array( + 'handle' => array( + 'description' => __( 'Unique identifier for the object.' ), + 'type' => 'string', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => $get_item_args, + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + } + + /** + * @param WP_REST_Request $request + * + * @return array|WP_Error|WP_REST_Response + */ + public function get_items( $request ) { + $data = []; + $handle = $request['dependency']; + $filter = []; + if ( $handle ) { + $this->object->all_deps( $handle ); + $filter = $this->object->to_do; + } + + if ( $handle ) { + foreach ( $filter as $dependency_handle ) { + foreach ( $this->object->registered as $dependency ) { + if ( $dependency_handle === $dependency->handle ) { + $item = $this->prepare_item_for_response( $dependency, $request ); + $data[] = $this->prepare_response_for_collection( $item ); + } + } + } + } else { + foreach ( $this->object->registered as $dependency ) { + $item = $this->prepare_item_for_response( $dependency, $request ); + $data[] = $this->prepare_response_for_collection( $item ); + } + } + + return $data; + } + + /** + * @param WP_REST_Request $request + * + * @return array|mixed|WP_Error|WP_REST_Response + */ + public function get_item( $request ) { + if ( ! isset( $this->object->registered[ $request['handle'] ] ) ) { + return []; + } + $dependency = $this->object->registered[ $request['handle'] ]; + $data = $this->prepare_item_for_response( $dependency, $request ); + + return $data; + } + + /** + * @param mixed $dependency + * @param WP_REST_Request $request + * + * @return mixed|WP_Error|WP_REST_Response + */ + public function prepare_item_for_response( $dependency, $request ) { + $dependency->url = $this->get_url( $dependency->src, $dependency->ver, $dependency->handle ); + $response = rest_ensure_response( (array) $dependency ); + $dependencies = $this->prepare_links( $dependency ); + $response->add_links( $dependencies ); + + return $response; + } + + /** + * @param WP_REST_Request $request + * + * @return bool|true|WP_Error + */ + public function get_items_permissions_check( $request ) { + if ( $this->check_handle( $request['dependency'] ) ) { + return true; + } + + return current_user_can( 'manage_options' ); + } + + /** + * @param WP_REST_Request $request + * + * @return bool|true|WP_Error + */ + public function get_item_permissions_check( $request ) { + if ( $this->check_handle( $request['handle'] ) ) { + return true; + } + + return current_user_can( 'manage_options' ); + } + + /** + * @param $dependency + * + * @return array + */ + protected function prepare_links( $dependency ) { + $base = sprintf( '%s/%s', $this->namespace, $this->rest_base ); + // Entity meta. + $links = array( + 'self' => array( + 'href' => rest_url( trailingslashit( $base ) . $dependency->handle ), + ), + 'collection' => array( + 'href' => rest_url( $base ), + ), + 'deps' => array( + 'href' => rest_url( trailingslashit( $base ) . '?dependency=' . $dependency->handle ), + ), + ); + + return $links; + } + + /** + * @return array + */ + public function get_collection_params() { + $query_params = parent::get_collection_params(); + $query_params['context']['default'] = 'view'; + + $query_params['dependency'] = array( + 'description' => __( '' ), + 'type' => 'string', + ); + + return $query_params; + } + + /** + * @param $handle + * + * @return bool + */ + protected function check_handle( $handle ) { + + if ( ! $handle ) { + return false; + } + + // All core assets should be public. + if ( in_array( $handle, $this->get_core_assets(), true ) ) { + return true; + } + + // All block public assets should also be public. + if ( in_array( $handle, $this->block_asset( $this->block_dependency ), true ) ) { + return true; + } + + // All block edit assets should check if user is logged in and has the ability to using the editor. + if ( in_array( $handle, $this->block_asset( $this->editor_block_dependency ), true ) ) { + return current_user_can( 'edit_posts' ); + } + + return false; + } + + /** + * @return array + */ + public function get_core_assets() { + /* translators: %s: Method name. */ + _doing_it_wrong( sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ), 'x.x' ); + + return array(); + } + + /** + * @param $field + * + * @return array + */ + protected function block_asset( $field ) { + if ( ! $field ) { + return array(); + } + + $block_registry = WP_Block_Type_Registry::get_instance(); + $blocks = $block_registry->get_all_registered(); + $handles = wp_list_pluck( $blocks, $field ); + $handles = array_values( $handles ); + $handles = array_filter( $handles ); + + return $handles; + } + +} diff --git a/lib/class-wp-rest-scripts-controller.php b/lib/class-wp-rest-scripts-controller.php new file mode 100755 index 00000000000000..87fc480cb97c9c --- /dev/null +++ b/lib/class-wp-rest-scripts-controller.php @@ -0,0 +1,106 @@ +namespace = 'wp/v2'; + $this->rest_base = 'scripts'; + $this->editor_block_dependency = 'editor_script'; + $this->block_dependency = 'script'; + $this->object = wp_scripts(); + } + + /** + * @param $src + * @param $ver + * @param $handle + * + * @return string + */ + public function get_url( $src, $ver, $handle ) { + if ( ! is_bool( $src ) && ! preg_match( '|^(https?:)?//|', $src ) && ! ( $this->object->content_url && 0 === strpos( $src, $this->object->content_url ) ) ) { + $src = $this->object->base_url . $src; + } + if ( ! empty( $ver ) ) { + $src = add_query_arg( 'ver', $ver, $src ); + } + + /** This filter is documented in wp-includes/class.wp-scripts.php */ + $src = esc_url( apply_filters( 'script_loader_src', $src, $handle ) ); + + return esc_url( $src ); + } + + /** + * @return array + */ + public function get_core_assets() { + $wp_scripts = new WP_Scripts(); + wp_default_scripts( $wp_scripts ); + wp_default_packages_vendor( $wp_scripts ); + wp_default_packages_scripts( $wp_scripts ); + $handles = wp_list_pluck( $wp_scripts->registered, 'handle' ); + $handles = array_values( $handles ); + + return $handles; + } + +} +< ? php + +/** + * Class WP_REST_Scripts_Controller + */ +class WP_REST_Scripts_Controller extends WP_REST_Dependencies_Controller { + /** + * WP_REST_Scripts_Controller constructor. + */ + public function __construct() { + $this->namespace = 'wp/v2'; + $this->rest_base = 'scripts'; + $this->editor_block_dependency = 'editor_script'; + $this->block_dependency = 'script'; + $this->object = wp_scripts(); + } + + /** + * @param $src + * @param $ver + * @param $handle + * + * @return string + */ + public function get_url( $src, $ver, $handle ) { + if ( ! is_bool( $src ) && ! preg_match( '|^(https?:)?//|', $src ) && ! ( $this->object->content_url && 0 === strpos( $src, $this->object->content_url ) ) ) { + $src = $this->object->base_url . $src; + } + if ( ! empty( $ver ) ) { + $src = add_query_arg( 'ver', $ver, $src ); + } + + /** This filter is documented in wp-includes/class.wp-scripts.php */ + $src = esc_url( apply_filters( 'script_loader_src', $src, $handle ) ); + + return esc_url( $src ); + } + + /** + * @return array + */ + public function get_core_assets() { + $wp_scripts = new WP_Scripts(); + wp_default_scripts( $wp_scripts ); + wp_default_packages_vendor( $wp_scripts ); + wp_default_packages_scripts( $wp_scripts ); + $handles = wp_list_pluck( $wp_scripts->registered, 'handle' ); + $handles = array_values( $handles ); + + return $handles; + } + +} diff --git a/lib/class-wp-rest-styles-controller.php b/lib/class-wp-rest-styles-controller.php new file mode 100755 index 00000000000000..4ccb091d5497db --- /dev/null +++ b/lib/class-wp-rest-styles-controller.php @@ -0,0 +1,80 @@ +namespace = 'wp/v2'; + $this->rest_base = 'styles'; + $this->editor_block_dependency = 'editor_style'; + $this->block_dependency = 'style'; + $this->object = wp_styles(); + } + + /** + * @param $src + * @param $ver + * @param $handle + * + * @return string + */ + public function get_url( $src, $ver, $handle ) { + return $this->object->_css_href( $src, $ver, $handle ); + } + + /** + * @return array + */ + public function get_core_assets() { + $wp_styles = new WP_Styles(); + wp_default_styles( $wp_styles ); + $handles = wp_list_pluck( $wp_styles->registered, 'handle' ); + $handles = array_values( $handles ); + + return $handles; + } +} +< ? php + +/** + * Class WP_REST_Styles_Controller + */ +class WP_REST_Styles_Controller extends WP_REST_Dependencies_Controller { + /** + * WP_REST_Styles_Controller constructor. + */ + public function __construct() { + $this->namespace = 'wp/v2'; + $this->rest_base = 'styles'; + $this->editor_block_dependency = 'editor_style'; + $this->block_dependency = 'style'; + $this->object = wp_styles(); + } + + /** + * @param $src + * @param $ver + * @param $handle + * + * @return string + */ + public function get_url( $src, $ver, $handle ) { + return $this->object->_css_href( $src, $ver, $handle ); + } + + /** + * @return array + */ + public function get_core_assets() { + $wp_styles = new WP_Styles(); + wp_default_styles( $wp_styles ); + $handles = wp_list_pluck( $wp_styles->registered, 'handle' ); + $handles = array_values( $handles ); + + return $handles; + } +} diff --git a/lib/load.php b/lib/load.php index 7207dbbed184c6..1e05c147d1a5f2 100644 --- a/lib/load.php +++ b/lib/load.php @@ -48,6 +48,15 @@ function gutenberg_is_experiment_enabled( $name ) { if ( ! class_exists( 'WP_REST_Menu_Locations_Controller' ) ) { require_once dirname( __FILE__ ) . '/class-wp-rest-menu-locations-controller.php'; } + if ( ! class_exists( 'WP_REST_Dependencies_Controller' ) ) { + require_once dirname( __FILE__ ) . '/class-wp-rest-dependencies-controller.php'; + } + if ( ! class_exists( 'WP_REST_Scripts_Controller' ) ) { + require_once dirname( __FILE__ ) . '/class-wp-rest-scripts-controller.php'; + } + if ( ! class_exists( 'WP_REST_Styles_Controller' ) ) { + require_once dirname( __FILE__ ) . '/class-wp-rest-styles-controller.php'; + } /** * End: Include for phase 2 */ diff --git a/lib/rest-api.php b/lib/rest-api.php index eba3685e4cdd7a..5f9d255fb6bd94 100644 --- a/lib/rest-api.php +++ b/lib/rest-api.php @@ -140,6 +140,20 @@ function wp_api_nav_menus_taxonomy_args( $args, $taxonomy ) { } add_filter( 'register_taxonomy_args', 'wp_api_nav_menus_taxonomy_args', 10, 2 ); +/** + * Registers the scripts and styles area REST API routes. + */ +function gutenberg_register_script_style() { + // Scripts. + $controller = new WP_REST_Scripts_Controller(); + $controller->register_routes(); + + // Styles. + $controller = new WP_REST_Styles_Controller(); + $controller->register_routes(); +} +add_action( 'rest_api_init', 'gutenberg_register_script_style' ); + /** * Shim for get_sample_permalink() to add support for auto-draft status. * From 288519c1abc6b656b1cd88eb136ff1873082413c Mon Sep 17 00:00:00 2001 From: Jonny Harris Date: Sun, 5 Apr 2020 18:30:38 +0100 Subject: [PATCH 06/15] Fix PHP --- lib/class-wp-rest-dependencies-controller.php | 259 +----------------- 1 file changed, 3 insertions(+), 256 deletions(-) diff --git a/lib/class-wp-rest-dependencies-controller.php b/lib/class-wp-rest-dependencies-controller.php index 47674283cd797c..d8efb1253072bc 100755 --- a/lib/class-wp-rest-dependencies-controller.php +++ b/lib/class-wp-rest-dependencies-controller.php @@ -50,7 +50,7 @@ public function register_routes() { array( 'args' => array( 'handle' => array( - 'description' => __( 'Unique identifier for the object.' ), + 'description' => __( 'Unique identifier for the object.', 'gutenberg' ), 'type' => 'string', ), ), @@ -185,7 +185,7 @@ public function get_collection_params() { $query_params['context']['default'] = 'view'; $query_params['dependency'] = array( - 'description' => __( '' ), + 'description' => __( '', 'gutenberg' ), 'type' => 'string', ); @@ -226,260 +226,7 @@ protected function check_handle( $handle ) { */ public function get_core_assets() { /* translators: %s: Method name. */ - _doing_it_wrong( sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ), 'x.x' ); - - return array(); - } - - /** - * @param $field - * - * @return array - */ - protected function block_asset( $field ) { - if ( ! $field ) { - return array(); - } - - $block_registry = WP_Block_Type_Registry::get_instance(); - $blocks = $block_registry->get_all_registered(); - $handles = wp_list_pluck( $blocks, $field ); - $handles = array_values( $handles ); - $handles = array_filter( $handles ); - - return $handles; - } - -} -< ? php - -/** - * Class WP_REST_Dependencies_Controller - */ -class WP_REST_Dependencies_Controller extends WP_REST_Controller { - - /** - * @var - */ - protected $object; - - - /** - * @var - */ - protected $editor_block_dependency = ''; - - - /** - * @var - */ - protected $block_dependency = ''; - - /** - * - */ - public function register_routes() { - register_rest_route( - $this->namespace, - '/' . $this->rest_base, - array( - array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( $this, 'get_items' ), - 'permission_callback' => array( $this, 'get_items_permissions_check' ), - 'args' => $this->get_collection_params(), - ), - 'schema' => array( $this, 'get_item_schema' ), - ) - ); - - $get_item_args = array( - 'context' => $this->get_context_param( array( 'default' => 'view' ) ), - ); - - register_rest_route( - $this->namespace, - '/' . $this->rest_base . '/(?P[\w-]+)', - array( - 'args' => array( - 'handle' => array( - 'description' => __( 'Unique identifier for the object.' ), - 'type' => 'string', - ), - ), - array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( $this, 'get_item' ), - 'permission_callback' => array( $this, 'get_item_permissions_check' ), - 'args' => $get_item_args, - ), - 'schema' => array( $this, 'get_public_item_schema' ), - ) - ); - } - - /** - * @param WP_REST_Request $request - * - * @return array|WP_Error|WP_REST_Response - */ - public function get_items( $request ) { - $data = []; - $handle = $request['dependency']; - $filter = []; - if ( $handle ) { - $this->object->all_deps( $handle ); - $filter = $this->object->to_do; - } - - if ( $handle ) { - foreach ( $filter as $dependency_handle ) { - foreach ( $this->object->registered as $dependency ) { - if ( $dependency_handle === $dependency->handle ) { - $item = $this->prepare_item_for_response( $dependency, $request ); - $data[] = $this->prepare_response_for_collection( $item ); - } - } - } - } else { - foreach ( $this->object->registered as $dependency ) { - $item = $this->prepare_item_for_response( $dependency, $request ); - $data[] = $this->prepare_response_for_collection( $item ); - } - } - - return $data; - } - - /** - * @param WP_REST_Request $request - * - * @return array|mixed|WP_Error|WP_REST_Response - */ - public function get_item( $request ) { - if ( ! isset( $this->object->registered[ $request['handle'] ] ) ) { - return []; - } - $dependency = $this->object->registered[ $request['handle'] ]; - $data = $this->prepare_item_for_response( $dependency, $request ); - - return $data; - } - - /** - * @param mixed $dependency - * @param WP_REST_Request $request - * - * @return mixed|WP_Error|WP_REST_Response - */ - public function prepare_item_for_response( $dependency, $request ) { - $dependency->url = $this->get_url( $dependency->src, $dependency->ver, $dependency->handle ); - $response = rest_ensure_response( (array) $dependency ); - $dependencies = $this->prepare_links( $dependency ); - $response->add_links( $dependencies ); - - return $response; - } - - /** - * @param WP_REST_Request $request - * - * @return bool|true|WP_Error - */ - public function get_items_permissions_check( $request ) { - if ( $this->check_handle( $request['dependency'] ) ) { - return true; - } - - return current_user_can( 'manage_options' ); - } - - /** - * @param WP_REST_Request $request - * - * @return bool|true|WP_Error - */ - public function get_item_permissions_check( $request ) { - if ( $this->check_handle( $request['handle'] ) ) { - return true; - } - - return current_user_can( 'manage_options' ); - } - - /** - * @param $dependency - * - * @return array - */ - protected function prepare_links( $dependency ) { - $base = sprintf( '%s/%s', $this->namespace, $this->rest_base ); - // Entity meta. - $links = array( - 'self' => array( - 'href' => rest_url( trailingslashit( $base ) . $dependency->handle ), - ), - 'collection' => array( - 'href' => rest_url( $base ), - ), - 'deps' => array( - 'href' => rest_url( trailingslashit( $base ) . '?dependency=' . $dependency->handle ), - ), - ); - - return $links; - } - - /** - * @return array - */ - public function get_collection_params() { - $query_params = parent::get_collection_params(); - $query_params['context']['default'] = 'view'; - - $query_params['dependency'] = array( - 'description' => __( '' ), - 'type' => 'string', - ); - - return $query_params; - } - - /** - * @param $handle - * - * @return bool - */ - protected function check_handle( $handle ) { - - if ( ! $handle ) { - return false; - } - - // All core assets should be public. - if ( in_array( $handle, $this->get_core_assets(), true ) ) { - return true; - } - - // All block public assets should also be public. - if ( in_array( $handle, $this->block_asset( $this->block_dependency ), true ) ) { - return true; - } - - // All block edit assets should check if user is logged in and has the ability to using the editor. - if ( in_array( $handle, $this->block_asset( $this->editor_block_dependency ), true ) ) { - return current_user_can( 'edit_posts' ); - } - - return false; - } - - /** - * @return array - */ - public function get_core_assets() { - /* translators: %s: Method name. */ - _doing_it_wrong( sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ), 'x.x' ); + _doing_it_wrong( sprintf( __( "Method '%s' not implemented. Must be overridden in subclass.", 'gutenberg' ), __METHOD__ ), 'x.x' ); return array(); } From d57e03f841b4a007cbd6d54f5461da4bc43fc947 Mon Sep 17 00:00:00 2001 From: Jonny Harris Date: Sun, 5 Apr 2020 18:54:20 +0100 Subject: [PATCH 07/15] Fix lints --- lib/class-wp-rest-dependencies-controller.php | 63 ++++++++++++++---- lib/class-wp-rest-scripts-controller.php | 65 +++---------------- lib/class-wp-rest-styles-controller.php | 51 +++------------ 3 files changed, 69 insertions(+), 110 deletions(-) diff --git a/lib/class-wp-rest-dependencies-controller.php b/lib/class-wp-rest-dependencies-controller.php index d8efb1253072bc..7e6b9a0aecd6ff 100755 --- a/lib/class-wp-rest-dependencies-controller.php +++ b/lib/class-wp-rest-dependencies-controller.php @@ -1,29 +1,44 @@ __( '', 'gutenberg' ), + 'description' => __( 'Dependency.', 'gutenberg' ), 'type' => 'string', ); @@ -193,7 +222,9 @@ public function get_collection_params() { } /** - * @param $handle + * Check handle exists and is viewable. + * + * @param string $handle script / style handle. * * @return bool */ @@ -222,6 +253,8 @@ protected function check_handle( $handle ) { } /** + * Get core assets. + * * @return array */ public function get_core_assets() { @@ -232,7 +265,9 @@ public function get_core_assets() { } /** - * @param $field + * Block asset. + * + * @param string $field Field to pluck from list of objects. * * @return array */ diff --git a/lib/class-wp-rest-scripts-controller.php b/lib/class-wp-rest-scripts-controller.php index 87fc480cb97c9c..335821c9c0f05c 100755 --- a/lib/class-wp-rest-scripts-controller.php +++ b/lib/class-wp-rest-scripts-controller.php @@ -1,57 +1,9 @@ namespace = 'wp/v2'; - $this->rest_base = 'scripts'; - $this->editor_block_dependency = 'editor_script'; - $this->block_dependency = 'script'; - $this->object = wp_scripts(); - } - - /** - * @param $src - * @param $ver - * @param $handle - * - * @return string - */ - public function get_url( $src, $ver, $handle ) { - if ( ! is_bool( $src ) && ! preg_match( '|^(https?:)?//|', $src ) && ! ( $this->object->content_url && 0 === strpos( $src, $this->object->content_url ) ) ) { - $src = $this->object->base_url . $src; - } - if ( ! empty( $ver ) ) { - $src = add_query_arg( 'ver', $ver, $src ); - } - - /** This filter is documented in wp-includes/class.wp-scripts.php */ - $src = esc_url( apply_filters( 'script_loader_src', $src, $handle ) ); - - return esc_url( $src ); - } - - /** - * @return array - */ - public function get_core_assets() { - $wp_scripts = new WP_Scripts(); - wp_default_scripts( $wp_scripts ); - wp_default_packages_vendor( $wp_scripts ); - wp_default_packages_scripts( $wp_scripts ); - $handles = wp_list_pluck( $wp_scripts->registered, 'handle' ); - $handles = array_values( $handles ); - - return $handles; - } - -} -< ? php /** * Class WP_REST_Scripts_Controller @@ -69,9 +21,11 @@ public function __construct() { } /** - * @param $src - * @param $ver - * @param $handle + * Helper to get Script URL. + * + * @param string $src Script URL. + * @param string $ver Version URL. + * @param string $handle Handle name. * * @return string */ @@ -90,6 +44,8 @@ public function get_url( $src, $ver, $handle ) { } /** + * Get core assets. + * * @return array */ public function get_core_assets() { @@ -102,5 +58,4 @@ public function get_core_assets() { return $handles; } - } diff --git a/lib/class-wp-rest-styles-controller.php b/lib/class-wp-rest-styles-controller.php index 4ccb091d5497db..ab2b2ea88d3b73 100755 --- a/lib/class-wp-rest-styles-controller.php +++ b/lib/class-wp-rest-styles-controller.php @@ -1,44 +1,9 @@ namespace = 'wp/v2'; - $this->rest_base = 'styles'; - $this->editor_block_dependency = 'editor_style'; - $this->block_dependency = 'style'; - $this->object = wp_styles(); - } - - /** - * @param $src - * @param $ver - * @param $handle - * - * @return string - */ - public function get_url( $src, $ver, $handle ) { - return $this->object->_css_href( $src, $ver, $handle ); - } - - /** - * @return array - */ - public function get_core_assets() { - $wp_styles = new WP_Styles(); - wp_default_styles( $wp_styles ); - $handles = wp_list_pluck( $wp_styles->registered, 'handle' ); - $handles = array_values( $handles ); - - return $handles; - } -} -< ? php /** * Class WP_REST_Styles_Controller @@ -56,9 +21,11 @@ public function __construct() { } /** - * @param $src - * @param $ver - * @param $handle + * Helper to get Style URL. + * + * @param string $src Style URL. + * @param string $ver Version URL. + * @param string $handle Handle name. * * @return string */ @@ -67,6 +34,8 @@ public function get_url( $src, $ver, $handle ) { } /** + * Get core assets. + * * @return array */ public function get_core_assets() { From 594cf9619b9b17a21478ea1f45a65dcf04f2161a Mon Sep 17 00:00:00 2001 From: Jonny Harris Date: Mon, 6 Apr 2020 09:59:25 +0100 Subject: [PATCH 08/15] Update lib/class-wp-rest-dependencies-controller.php Co-Authored-By: Steven Dufresne --- lib/class-wp-rest-dependencies-controller.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/class-wp-rest-dependencies-controller.php b/lib/class-wp-rest-dependencies-controller.php index 7e6b9a0aecd6ff..4977c4082d3bee 100755 --- a/lib/class-wp-rest-dependencies-controller.php +++ b/lib/class-wp-rest-dependencies-controller.php @@ -116,7 +116,7 @@ public function get_items( $request ) { } /** - * Get a single depencies. + * Get a single dependency. * * @param WP_REST_Request $request Request. * From a33ed9e7909329f394eedd68a5a75d86959cd09a Mon Sep 17 00:00:00 2001 From: Sara Marcondes Date: Fri, 24 Apr 2020 17:23:50 -0700 Subject: [PATCH 09/15] Allow passing multiple dependency handles --- lib/class-wp-rest-dependencies-controller.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/class-wp-rest-dependencies-controller.php b/lib/class-wp-rest-dependencies-controller.php index 4977c4082d3bee..66a5cb41b78ec9 100755 --- a/lib/class-wp-rest-dependencies-controller.php +++ b/lib/class-wp-rest-dependencies-controller.php @@ -215,7 +215,7 @@ public function get_collection_params() { $query_params['dependency'] = array( 'description' => __( 'Dependency.', 'gutenberg' ), - 'type' => 'string', + 'type' => 'array', ); return $query_params; From f70690320646fc9b45566260ce8deb1307840a2a Mon Sep 17 00:00:00 2001 From: Sara Marcondes Date: Fri, 24 Apr 2020 17:24:20 -0700 Subject: [PATCH 10/15] Use the actual object's handles rather than duplicating its initialization We were duplicating WP_Scripts and WP_Styles initialization and therefore misrepresnting what was handles were actually accessible through this API. By using the object, which for both these classes is accessed through a getter for the static global for each WP_Dependencies subclass, we can return the handles that are actually available. This makes it so that any script added by other plugins will also appear, for example. This doesn't change the accessibility of those handles, they were always already accessible. Something to consider is whether we should implement a blacklist, if there are scripts/styles that must never be requested in this way, as in, if they were then we're doing something really wrong. I'm not sure if any scripts like that exist today. --- lib/class-wp-rest-scripts-controller.php | 6 +----- lib/class-wp-rest-styles-controller.php | 4 +--- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/lib/class-wp-rest-scripts-controller.php b/lib/class-wp-rest-scripts-controller.php index 335821c9c0f05c..c8590a935d0cc4 100755 --- a/lib/class-wp-rest-scripts-controller.php +++ b/lib/class-wp-rest-scripts-controller.php @@ -49,11 +49,7 @@ public function get_url( $src, $ver, $handle ) { * @return array */ public function get_core_assets() { - $wp_scripts = new WP_Scripts(); - wp_default_scripts( $wp_scripts ); - wp_default_packages_vendor( $wp_scripts ); - wp_default_packages_scripts( $wp_scripts ); - $handles = wp_list_pluck( $wp_scripts->registered, 'handle' ); + $handles = wp_list_pluck( $this->object->registered, 'handle' ); $handles = array_values( $handles ); return $handles; diff --git a/lib/class-wp-rest-styles-controller.php b/lib/class-wp-rest-styles-controller.php index ab2b2ea88d3b73..2f7f518f726b68 100755 --- a/lib/class-wp-rest-styles-controller.php +++ b/lib/class-wp-rest-styles-controller.php @@ -39,9 +39,7 @@ public function get_url( $src, $ver, $handle ) { * @return array */ public function get_core_assets() { - $wp_styles = new WP_Styles(); - wp_default_styles( $wp_styles ); - $handles = wp_list_pluck( $wp_styles->registered, 'handle' ); + $handles = wp_list_pluck( $this->object->registered, 'handle' ); $handles = array_values( $handles ); return $handles; From 0d55d9ab499bad23d776ada32977e7cd6836247a Mon Sep 17 00:00:00 2001 From: Sara Marcondes Date: Fri, 24 Apr 2020 17:30:29 -0700 Subject: [PATCH 11/15] Use dependencies REST API to fetch style and script URIs Rather than using `load-scripts.php` which only loads a small subset of dependencies into a locally managed WP_Scripts/WP_Styles, we can use the two new REST endpoints that use the globals available throughout the rest of WP. Each endpoint returns the full list of the URIs for the requested dependencies as well as their respective dependencies. When multiple dependencies are passed as in this format: /wp/v2/scripts/?dependency=dep_1&dependency=dep_2 The endpoint will return: [ { handle: 'dep_1-dependency', src: '...', }, { handle: 'dep_1-actual', src: '...' }, { handle: 'dep_2-dependency', src: '...', }, { handle: 'dep_2-actual', src: '...', } ] That is, it will return descriptions for the list of dependencies you pass to the endpoint, as well as those dependencies dependencies, in the correct order in which they would need to be loaded. That means we can consume the response and just create a script or link element for each response to be able to load the dependencies we've requested. We'll also store the actually loaded dependency handles rather than the ones requested through the props. The thought here being that one component might have a dependency which itself depends on the dependency of another component. Using the example response above, if component Foo depends on `dep_1-dependency` and Bar depends on `dep_1-actual`, if Bar is rendered first, then Foo does not need to have its dependencies asynchronously loaded as they were already loaded as part of loading Bar's dependencies. At this point, this code doesn't actually fully work for the classic block. Something is still broken and I'm still trying to work out what exactly it is. However, the scripts and styles do indeed load and the initialization is successful. I _think_ there might just be a problem where something isn't being correctly awaited, but I've run out of steam today to figure out just what's going on. --- packages/block-library/src/classic/edit.js | 8 +- packages/block-library/src/classic/lazy.js | 121 +++++++++++++-------- 2 files changed, 81 insertions(+), 48 deletions(-) diff --git a/packages/block-library/src/classic/edit.js b/packages/block-library/src/classic/edit.js index 873f26885b0326..e54cb244d82f51 100644 --- a/packages/block-library/src/classic/edit.js +++ b/packages/block-library/src/classic/edit.js @@ -225,8 +225,12 @@ class ClassicEdit extends Component { const LazyClassicEdit = ( props ) => ( window.wpMceTranslation() } + onLoaded={ () => + new Promise( ( resolve ) => { + window.wpMceTranslation(); + resolve(); + } ) + } placeholder={
Loading...
} > diff --git a/packages/block-library/src/classic/lazy.js b/packages/block-library/src/classic/lazy.js index b0021448fb0f71..66365c565a937c 100644 --- a/packages/block-library/src/classic/lazy.js +++ b/packages/block-library/src/classic/lazy.js @@ -2,73 +2,91 @@ * WordPress dependencies */ import { Component } from '@wordpress/element'; +import apiFetch from '@wordpress/api-fetch'; /** * External dependencies */ import { noop } from 'lodash'; +const getPath = ( fileType, dependencies ) => + `/wp/v2/${ fileType }/?${ dependencies.map( + ( dependency ) => `dependency=${ dependency }` + ) }`; + export default class LazyLoad extends Component { - static loadedScripts = new Set(); - static loadedStyles = new Set(); - static defaultProps = { - scripts: [], - styles: [], - placeholder: null, - onLoaded: () => Promise.resolve(), - onError: noop, - }; - - state = { - loaded: false, - }; - - loadScripts = async () => { + constructor( props ) { + super( props ); + + this.state = { + loaded: false, + }; + + this.loadScripts = this.loadScripts.bind( this ); + this.loadStyles = this.loadStyles.bind( this ); + } + + async loadScripts() { const scriptsToLoad = this.props.scripts.filter( - ( script ) => ! this.loadedScripts.has( script ) + ( script ) => ! LazyLoad.loadedScripts.has( script ) ); if ( scriptsToLoad.length === 0 ) { return Promise.resolve(); } - await new Promise( ( resolve, reject ) => { - const scriptElement = document.createElement( 'script' ); - // can't use load-scripts.php, so this would need to be replaced - scriptElement.src = `/wp-admin/load-scripts.php?load=${ scriptsToLoad.join( - ',' - ) }`; - scriptElement.onload = resolve; - scriptElement.onerror = reject; - document.head.appendChild( scriptElement ); - } ); - - scriptsToLoad.forEach( ( script ) => this.loadedScripts.add( script ) ); - }; - - loadStyles = async () => { + const path = getPath( 'scripts', scriptsToLoad ); + + const scriptsWithDeps = await apiFetch( { path } ); + + await Promise.all( + scriptsWithDeps.map( ( { src: scriptSrc } ) => { + return new Promise( ( resolve, reject ) => { + const scriptElement = document.createElement( 'script' ); + scriptElement.src = scriptSrc; + scriptElement.defer = true; + scriptElement.onload = resolve; + scriptElement.onerror = reject; + document.body.appendChild( scriptElement ); + } ); + } ) + ); + + scriptsWithDeps.forEach( ( { handle } ) => + LazyLoad.loadedScripts.add( handle ) + ); + } + + async loadStyles() { const stylesToLoad = this.props.styles.filter( - ( style ) => ! this.loadedStyles.has( style ) + ( style ) => ! LazyLoad.loadedStyles.has( style ) ); if ( stylesToLoad.length === 0 ) { return Promise.resolve(); } - await new Promise( ( resolve, reject ) => { - const linkElement = document.createElement( 'link' ); - linkElement.rel = 'stylesheet'; - // can't use load-styles.php, so this would need to be replaced - linkElement.href = `/wp-admin/load-styles.php?load=${ stylesToLoad.join( - ',' - ) }`; - linkElement.onload = resolve; - linkElement.onerror = reject; - document.head.appendChild( linkElement ); - } ); - - stylesToLoad.forEach( ( style ) => this.loadedStyles.add( style ) ); - }; + const path = getPath( 'styles', stylesToLoad ); + + const stylesWithDeps = await apiFetch( { path } ); + + await Promise.all( + stylesWithDeps.map( ( { src: styleHref } ) => { + return new Promise( ( resolve, reject ) => { + const linkElement = document.createElement( 'link' ); + linkElement.rel = 'stylesheet'; + linkElement.href = styleHref; + linkElement.onload = resolve; + linkElement.onerror = reject; + document.head.appendChild( linkElement ); + } ); + } ) + ); + + stylesWithDeps.forEach( ( { handle } ) => + LazyLoad.loadedStyles.add( handle ) + ); + } componentDidMount() { Promise.all( [ this.loadScripts(), this.loadStyles() ] ) @@ -87,3 +105,14 @@ export default class LazyLoad extends Component { return this.props.placeholder; } } + +LazyLoad.defaultProps = { + scripts: [], + styles: [], + placeholder: null, + onLoaded: () => Promise.resolve(), + onError: noop, +}; + +LazyLoad.loadedScripts = new Set(); +LazyLoad.loadedStyles = new Set(); From 7d370f15f0f60e39c284e60bf1bef2d94cb63f86 Mon Sep 17 00:00:00 2001 From: Sara Marcondes Date: Mon, 27 Apr 2020 09:01:53 -0700 Subject: [PATCH 12/15] Guarantee script execution order See inline comment for explanation for why this is necessary. Also refactors the dependency loading code into a single function. All of the logic is shared except for the actual DOM element creation. While the component was pretty simple to understand on one read through when there was no need for blocking, it didn't make sense to repeat that complexity and the comment for it. This also lets the component hide the complexity of loadScripts and loadStyles from the exported function, as well as the already-loaded- dependency tracking inside those closures. --- packages/block-library/src/classic/lazy.js | 171 +++++++++++---------- 1 file changed, 86 insertions(+), 85 deletions(-) diff --git a/packages/block-library/src/classic/lazy.js b/packages/block-library/src/classic/lazy.js index 66365c565a937c..fa6cadd04b4534 100644 --- a/packages/block-library/src/classic/lazy.js +++ b/packages/block-library/src/classic/lazy.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { Component } from '@wordpress/element'; +import { useState } from '@wordpress/element'; import apiFetch from '@wordpress/api-fetch'; /** @@ -9,102 +9,104 @@ import apiFetch from '@wordpress/api-fetch'; */ import { noop } from 'lodash'; -const getPath = ( fileType, dependencies ) => - `/wp/v2/${ fileType }/?${ dependencies.map( +const getPath = ( dependencyType, dependencies ) => + `/wp/v2/${ dependencyType }/?${ dependencies.map( ( dependency ) => `dependency=${ dependency }` ) }`; -export default class LazyLoad extends Component { - constructor( props ) { - super( props ); - - this.state = { - loaded: false, - }; - - this.loadScripts = this.loadScripts.bind( this ); - this.loadStyles = this.loadStyles.bind( this ); - } +/** + * + * @param {'scripts' | 'styles'} dependencyType The type of dependency + * @param {Function} createElement A function accepting a URI for a dependency and returning an element to append to head + * + * @return {Function} A file loader function acepting a list of dependencies and returning a Promise which resolves when all the files have been loaded + */ +const createDependencyLoader = ( dependencyType, createElement ) => { + const previouslyLoadedDependencies = new Set(); - async loadScripts() { - const scriptsToLoad = this.props.scripts.filter( - ( script ) => ! LazyLoad.loadedScripts.has( script ) + return async ( dependencies ) => { + const dependenciesToLoad = dependencies.filter( + ( handle ) => ! previouslyLoadedDependencies.has( handle ) ); - if ( scriptsToLoad.length === 0 ) { - return Promise.resolve(); + if ( dependenciesToLoad.length === 0 ) { + return; } - const path = getPath( 'scripts', scriptsToLoad ); - - const scriptsWithDeps = await apiFetch( { path } ); - - await Promise.all( - scriptsWithDeps.map( ( { src: scriptSrc } ) => { - return new Promise( ( resolve, reject ) => { - const scriptElement = document.createElement( 'script' ); - scriptElement.src = scriptSrc; - scriptElement.defer = true; - scriptElement.onload = resolve; - scriptElement.onerror = reject; - document.body.appendChild( scriptElement ); - } ); - } ) - ); - - scriptsWithDeps.forEach( ( { handle } ) => - LazyLoad.loadedScripts.add( handle ) - ); - } - - async loadStyles() { - const stylesToLoad = this.props.styles.filter( - ( style ) => ! LazyLoad.loadedStyles.has( style ) - ); - - if ( stylesToLoad.length === 0 ) { - return Promise.resolve(); + const path = getPath( dependencyType, dependenciesToLoad ); + + const orderedDependenciesWithDependencies = await apiFetch( { path } ); + + /** + * Programatically added `script` tags get parsed and executed as though they + * had `async` on them. That means that if we have script A and B, where B + * depends on A and we add both to the DOM at the same time, there's no + * guaranteed order of execution, B could execute before A and break. + * + * Therefore, we need to block on each script's loading until the next one down + * the list. + * + * The only problem with this is if you have two final dependencies you're trying + * to load and they have completely separate dependency trees, we will _not_ be + * loading those trees in parallel. This isn't currently an issue, so fixing it + * right now would be a premature optimization. It would probably require rethinking + * how the API returns things (i.e., we should probably return things in an + * actual tree format and represent independent trees as duely separate trees if + * we want to be able to load things in parallel). For now, this will have to do. + */ + for ( const dependency of orderedDependenciesWithDependencies ) { + const { src: dependencyUri } = dependency; + + await new Promise( ( resolve, reject ) => { + const element = createElement( dependencyUri ); + element.onload = resolve; + element.onerror = reject; + document.head.appendChild( element ); + } ); } - const path = getPath( 'styles', stylesToLoad ); - - const stylesWithDeps = await apiFetch( { path } ); - - await Promise.all( - stylesWithDeps.map( ( { src: styleHref } ) => { - return new Promise( ( resolve, reject ) => { - const linkElement = document.createElement( 'link' ); - linkElement.rel = 'stylesheet'; - linkElement.href = styleHref; - linkElement.onload = resolve; - linkElement.onerror = reject; - document.head.appendChild( linkElement ); - } ); - } ) + orderedDependenciesWithDependencies.forEach( ( { handle } ) => + previouslyLoadedDependencies.add( handle ) ); + }; +}; - stylesWithDeps.forEach( ( { handle } ) => - LazyLoad.loadedStyles.add( handle ) - ); - } - - componentDidMount() { - Promise.all( [ this.loadScripts(), this.loadStyles() ] ) - .then( this.props.onLoaded ) - .then( () => { - this.setState( { loaded: true } ); - } ) - .catch( this.props.onError ); +const loadScripts = createDependencyLoader( 'scripts', ( dependencyUri ) => { + const scriptElement = document.createElement( 'script' ); + scriptElement.type = 'application/javascript'; + scriptElement.src = dependencyUri; + return scriptElement; +} ); + +const loadStyles = createDependencyLoader( 'styles', ( dependencyUri ) => { + const linkElement = document.createElement( 'link' ); + linkElement.rel = 'stylesheet'; + linkElement.href = dependencyUri; + return linkElement; +} ); + +const LazyLoad = ( { + scripts, + styles, + children, + placeholder, + onLoaded, + onError, +} ) => { + const [ loaded, setLoaded ] = useState( false ); + Promise.all( [ loadScripts( scripts ), loadStyles( styles ) ] ) + .then( () => onLoaded() ) + .then( () => { + setLoaded( true ); + } ) + .catch( onError ); + + if ( loaded ) { + return children; } - render() { - if ( this.state.loaded ) { - return this.props.children; - } - - return this.props.placeholder; - } -} + return placeholder; +}; LazyLoad.defaultProps = { scripts: [], @@ -114,5 +116,4 @@ LazyLoad.defaultProps = { onError: noop, }; -LazyLoad.loadedScripts = new Set(); -LazyLoad.loadedStyles = new Set(); +export default LazyLoad; From 3980857b3cf9903ac574bd55b7e474f8eba451e8 Mon Sep 17 00:00:00 2001 From: Sara Marcondes Date: Mon, 27 Apr 2020 18:03:46 -0700 Subject: [PATCH 13/15] Fetch TinyMCE translations from REST API Uses the "private" _WP_Editors class to retrieve the TinyMCE translations JSON blob and requisite metadata for initializing the editor on the frontend. If we decide to follow this approach, we'll want to refactor the methods used from _WP_Editors into free functions, or else move them into a separate class that is only used for getting the TinyMCE translations. One marked improvement we could achieve is to avoid json_encoding the translations only to have the JSON.stringified on the client. We should just send them down as an object rather than an encoded string. I didn't do that for this commit because it's going to require a wee bit of refactoring in wp_mce_translation to get the raw translation object back rather than a string. That refactoring should just go along with the refactor that pulls stuff out of _WP_Editors. --- lib/class-wp-rest-tinymce-i18n-controller.php | 76 +++++++++++++++++++ lib/load.php | 3 + lib/rest-api.php | 4 + packages/block-library/src/classic/edit.js | 18 +++-- 4 files changed, 95 insertions(+), 6 deletions(-) create mode 100755 lib/class-wp-rest-tinymce-i18n-controller.php diff --git a/lib/class-wp-rest-tinymce-i18n-controller.php b/lib/class-wp-rest-tinymce-i18n-controller.php new file mode 100755 index 00000000000000..7577085f418ce9 --- /dev/null +++ b/lib/class-wp-rest-tinymce-i18n-controller.php @@ -0,0 +1,76 @@ + WP_REST_Server::READABLE, + 'callback' => [ $this, 'get_translations' ], + 'permission_callback' => [ $this, 'check_permissions' ], + 'args' => [], + ], + 'schema' => array( $this, 'get_item_schema' ), + ] + ); + } + + /** + * Get the translations for the current user's locale. + * + * _WP_Editors is not included by default so we need to require it + * if the class isn't available. We should probably refactor all the + * TinyMCE translation files out of _WP_Editors. This shouldn't be + * too hard considering they're already all static functions. + * + * @param WP_REST_Request $request Request. + * + * @return array + */ + public function get_translations( $request ) { + if ( ! class_exists( '_WP_Editors', false ) ) { + require ABSPATH . WPINC . '/class-wp-editor.php'; + } + + $mce_locale = _WP_Editors::get_mce_locale(); + $json_only = true; + $baseurl = _WP_Editors::get_baseurl(); + + return [ + "translations" => _WP_Editors::wp_mce_translation( $mce_locale, $json_only ), + "locale" => $mce_locale, + "locale_script_handle" => "$baseurl/langs/$mce_locale.js", + ]; + } + + /** + * This endpoint is only mean to be used from the editor so it can be safely + * restricted only to users that can edit posts. + * + * @param WP_REST_Request $request Request. + * + * @return bool|true|WP_Error + */ + public function check_permissions( $request ) { + return current_user_can( 'edit_posts' ); + } +} diff --git a/lib/load.php b/lib/load.php index 1e05c147d1a5f2..879a80bf0d615a 100644 --- a/lib/load.php +++ b/lib/load.php @@ -57,6 +57,9 @@ function gutenberg_is_experiment_enabled( $name ) { if ( ! class_exists( 'WP_REST_Styles_Controller' ) ) { require_once dirname( __FILE__ ) . '/class-wp-rest-styles-controller.php'; } + if ( ! class_exists( 'WP_REST_TinyMCE_I81n_Controller' ) ) { + require_once dirname( __FILE__ ) . '/class-wp-rest-tinymce-i18n-controller.php'; + } /** * End: Include for phase 2 */ diff --git a/lib/rest-api.php b/lib/rest-api.php index 5f9d255fb6bd94..27193a31aecc89 100644 --- a/lib/rest-api.php +++ b/lib/rest-api.php @@ -151,6 +151,10 @@ function gutenberg_register_script_style() { // Styles. $controller = new WP_REST_Styles_Controller(); $controller->register_routes(); + + // TinyMCE translations + $controller = new WP_REST_TinyMCE_I18n_Controller(); + $controller->register_routes(); } add_action( 'rest_api_init', 'gutenberg_register_script_style' ); diff --git a/packages/block-library/src/classic/edit.js b/packages/block-library/src/classic/edit.js index e54cb244d82f51..7db36295744fca 100644 --- a/packages/block-library/src/classic/edit.js +++ b/packages/block-library/src/classic/edit.js @@ -9,6 +9,7 @@ import { BACKSPACE, DELETE, F10 } from '@wordpress/keycodes'; * Internal dependencies */ import LazyLoad from './lazy'; +import apiFetch from '@wordpress/api-fetch'; const { wp } = window; @@ -225,12 +226,17 @@ class ClassicEdit extends Component { const LazyClassicEdit = ( props ) => ( - new Promise( ( resolve ) => { - window.wpMceTranslation(); - resolve(); - } ) - } + onLoaded={ async () => { + const { + translations, + locale, + locale_script_handle: localeScriptHandle, + } = await apiFetch( { path: '/wp/v2/tinymce-i18n' } ); + + const { tinymce } = window; + tinymce.addI18n( locale, JSON.stringify( translations ) ); + tinymce.ScriptLoader.markDone( localeScriptHandle ); + } } placeholder={
Loading...
} > From 6f0575d5e19e6c53cdda7ab20a35078b3c688e0e Mon Sep 17 00:00:00 2001 From: Sara Marcondes Date: Tue, 28 Apr 2020 15:47:07 -0700 Subject: [PATCH 14/15] Prevent onLoaded for previously loaded deps --- packages/block-library/src/classic/lazy.js | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/packages/block-library/src/classic/lazy.js b/packages/block-library/src/classic/lazy.js index fa6cadd04b4534..1bb63061bf693f 100644 --- a/packages/block-library/src/classic/lazy.js +++ b/packages/block-library/src/classic/lazy.js @@ -7,7 +7,7 @@ import apiFetch from '@wordpress/api-fetch'; /** * External dependencies */ -import { noop } from 'lodash'; +import { noop, sum } from 'lodash'; const getPath = ( dependencyType, dependencies ) => `/wp/v2/${ dependencyType }/?${ dependencies.map( @@ -30,7 +30,7 @@ const createDependencyLoader = ( dependencyType, createElement ) => { ); if ( dependenciesToLoad.length === 0 ) { - return; + return 0; } const path = getPath( dependencyType, dependenciesToLoad ); @@ -41,7 +41,8 @@ const createDependencyLoader = ( dependencyType, createElement ) => { * Programatically added `script` tags get parsed and executed as though they * had `async` on them. That means that if we have script A and B, where B * depends on A and we add both to the DOM at the same time, there's no - * guaranteed order of execution, B could execute before A and break. + * guaranteed order of execution, B could execute before A and break, even if + * A's script element was added before B. * * Therefore, we need to block on each script's loading until the next one down * the list. @@ -68,6 +69,8 @@ const createDependencyLoader = ( dependencyType, createElement ) => { orderedDependenciesWithDependencies.forEach( ( { handle } ) => previouslyLoadedDependencies.add( handle ) ); + + return orderedDependenciesWithDependencies.length; }; }; @@ -94,8 +97,18 @@ const LazyLoad = ( { onError, } ) => { const [ loaded, setLoaded ] = useState( false ); + Promise.all( [ loadScripts( scripts ), loadStyles( styles ) ] ) - .then( () => onLoaded() ) + .then( ( loadedCounts ) => { + /** + * skip `onLoaded` if no dependencies were loaded, for example, + * if they were previously loaded + */ + if ( sum( loadedCounts ) > 0 ) { + return onLoaded(); + } + return Promise.resolve(); + } ) .then( () => { setLoaded( true ); } ) From 0f90624e509e173177ae9b75b56fbb835a8e836c Mon Sep 17 00:00:00 2001 From: Sara Marcondes Date: Tue, 28 Apr 2020 16:51:19 -0700 Subject: [PATCH 15/15] Add document to LazyLoad component Document types, props, functions, and use cases for the LazyLoad component. Also adds WPNode to @wordpress/elements to allow for the correct type annotation for a ReactNode prop (like children). --- packages/block-library/src/classic/lazy.js | 55 +++++++++++++++++++--- packages/element/src/react.js | 6 +++ 2 files changed, 55 insertions(+), 6 deletions(-) diff --git a/packages/block-library/src/classic/lazy.js b/packages/block-library/src/classic/lazy.js index 1bb63061bf693f..de80dd403da3c6 100644 --- a/packages/block-library/src/classic/lazy.js +++ b/packages/block-library/src/classic/lazy.js @@ -9,17 +9,30 @@ import apiFetch from '@wordpress/api-fetch'; */ import { noop, sum } from 'lodash'; -const getPath = ( dependencyType, dependencies ) => - `/wp/v2/${ dependencyType }/?${ dependencies.map( - ( dependency ) => `dependency=${ dependency }` +/** + * @typedef {'scripts' | 'styles'} WPLazyDependencyType + */ + +/** + * Gets the path corresponding to the dependency type and list of dependency + * handles to load. + * + * @see {WP_REST_Styles_Controller} + * + * @param {WPLazyDependencyType} dependencyType The type of dependency for which to build the path. + * @param {string[]} dependencyHandles List of dependency handles for which to build the path. + */ +const getPath = ( dependencyType, dependencyHandles ) => + `/wp/v2/${ dependencyType }/?${ dependencyHandles.map( + ( handle ) => `dependency=${ handle }` ) }`; /** * - * @param {'scripts' | 'styles'} dependencyType The type of dependency - * @param {Function} createElement A function accepting a URI for a dependency and returning an element to append to head + * @param {WPLazyDependencyType} dependencyType The type of dependency + * @param {(dependencyUri: string) => (HTMLScriptElement | HTMLLinkElement)} createElement A function accepting a URI for a dependency and returning an element to append to head * - * @return {Function} A file loader function acepting a list of dependencies and returning a Promise which resolves when all the files have been loaded + * @return {(dependencies: string[]) => Promise} A file loader function acepting a list of dependencies and returning a Promise which resolves when all the files have been loaded */ const createDependencyLoader = ( dependencyType, createElement ) => { const previouslyLoadedDependencies = new Set(); @@ -88,6 +101,36 @@ const loadStyles = createDependencyLoader( 'styles', ( dependencyUri ) => { return linkElement; } ); +/** + * A component which blocks the rendering of its children until the dependencies + * declared in scripts and styles have been loaded and the optional onLoaded + * function has successfully resolved. Allows asynchronously loading library + * dependencies for blocks (for example TinyMCE for the Classic Block). + * + * @example + * Given a library called `map-library` which loads an object onto window.mapLibrary, we can initialize this dependency using LazyLoad: + * + * const EmbeddedMapBlockEdit = () => { + * window.mapLibrary.drawMap(); + * }; + * + * const LazyEmbeddedMapBlockEdit = ( props ) => ( + * window.mapLibrary.init() } + * > + * + * + * ); + * + * @param {Object} props + * @param {string[]} props.scripts List of script handles to load for children + * @param {string[]} props.styles List of style handles to load for children + * @param {import('@wordpress/element').WPNode} props.children Children to render after loading dependencies + * @param {import('@wordpress/element').WPNode} props.placeholder Placeholder to render while loading dependencies + * @param {() => Promise} props.onLoaded + * @param {(e: Error) => void} props.onError Function to handle errors while loading dependencies + */ const LazyLoad = ( { scripts, styles, diff --git a/packages/element/src/react.js b/packages/element/src/react.js index 78752a1b5a179a..03674f04d4815c 100644 --- a/packages/element/src/react.js +++ b/packages/element/src/react.js @@ -46,6 +46,12 @@ import { isString } from 'lodash'; * @typedef {import('react').SyntheticEvent} WPSyntheticEvent */ +/** + * Object containing a React node. + * + * @typedef {import('react').ReactNode} WPNode + */ + /** * Object that provides utilities for dealing with React children. */