diff --git a/gutenberg.php b/gutenberg.php
index 4dc2ea3e862c23..511310f495de86 100644
--- a/gutenberg.php
+++ b/gutenberg.php
@@ -159,3 +159,25 @@ 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 "";
+ echo "\n";
+ }
+}
+remove_action( 'print_tinymce_scripts', 'wp_print_tinymce_scripts' );
+add_action( 'print_tinymce_scripts', 'gutenberg_print_tinymce_scripts' );
diff --git a/lib/class-wp-rest-dependencies-controller.php b/lib/class-wp-rest-dependencies-controller.php
new file mode 100755
index 00000000000000..66a5cb41b78ec9
--- /dev/null
+++ b/lib/class-wp-rest-dependencies-controller.php
@@ -0,0 +1,288 @@
+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.', 'gutenberg' ),
+ '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' ),
+ )
+ );
+ }
+
+ /**
+ * Get list of dependencies.
+ *
+ * @param WP_REST_Request $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;
+ }
+
+ /**
+ * Get a single dependency.
+ *
+ * @param WP_REST_Request $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;
+ }
+
+ /**
+ * Prepare item for response.
+ *
+ * @param mixed $dependency Dependency.
+ * @param WP_REST_Request $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;
+ }
+
+ /**
+ * Permission check.
+ *
+ * @param WP_REST_Request $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' );
+ }
+
+ /**
+ * Permission check.
+ *
+ * @param WP_REST_Request $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' );
+ }
+
+ /**
+ * Prepare links.
+ *
+ * @param object $dependency 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;
+ }
+
+ /**
+ * Get collection params.
+ *
+ * @return array
+ */
+ public function get_collection_params() {
+ $query_params = parent::get_collection_params();
+ $query_params['context']['default'] = 'view';
+
+ $query_params['dependency'] = array(
+ 'description' => __( 'Dependency.', 'gutenberg' ),
+ 'type' => 'array',
+ );
+
+ return $query_params;
+ }
+
+ /**
+ * Check handle exists and is viewable.
+ *
+ * @param string $handle script / style 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;
+ }
+
+ /**
+ * Get core assets.
+ *
+ * @return array
+ */
+ public function get_core_assets() {
+ /* translators: %s: Method name. */
+ _doing_it_wrong( sprintf( __( "Method '%s' not implemented. Must be overridden in subclass.", 'gutenberg' ), __METHOD__ ), 'x.x' );
+
+ return array();
+ }
+
+ /**
+ * Block asset.
+ *
+ * @param string $field Field to pluck from list of objects.
+ *
+ * @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..c8590a935d0cc4
--- /dev/null
+++ b/lib/class-wp-rest-scripts-controller.php
@@ -0,0 +1,57 @@
+namespace = 'wp/v2';
+ $this->rest_base = 'scripts';
+ $this->editor_block_dependency = 'editor_script';
+ $this->block_dependency = 'script';
+ $this->object = wp_scripts();
+ }
+
+ /**
+ * Helper to get Script URL.
+ *
+ * @param string $src Script URL.
+ * @param string $ver Version URL.
+ * @param string $handle Handle name.
+ *
+ * @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 );
+ }
+
+ /**
+ * Get core assets.
+ *
+ * @return array
+ */
+ public function get_core_assets() {
+ $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
new file mode 100755
index 00000000000000..2f7f518f726b68
--- /dev/null
+++ b/lib/class-wp-rest-styles-controller.php
@@ -0,0 +1,47 @@
+namespace = 'wp/v2';
+ $this->rest_base = 'styles';
+ $this->editor_block_dependency = 'editor_style';
+ $this->block_dependency = 'style';
+ $this->object = wp_styles();
+ }
+
+ /**
+ * Helper to get Style URL.
+ *
+ * @param string $src Style URL.
+ * @param string $ver Version URL.
+ * @param string $handle Handle name.
+ *
+ * @return string
+ */
+ public function get_url( $src, $ver, $handle ) {
+ return $this->object->_css_href( $src, $ver, $handle );
+ }
+
+ /**
+ * Get core assets.
+ *
+ * @return array
+ */
+ public function get_core_assets() {
+ $handles = wp_list_pluck( $this->object->registered, 'handle' );
+ $handles = array_values( $handles );
+
+ return $handles;
+ }
+}
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 7207dbbed184c6..879a80bf0d615a 100644
--- a/lib/load.php
+++ b/lib/load.php
@@ -48,6 +48,18 @@ 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';
+ }
+ 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 eba3685e4cdd7a..27193a31aecc89 100644
--- a/lib/rest-api.php
+++ b/lib/rest-api.php
@@ -140,6 +140,24 @@ 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();
+
+ // TinyMCE translations
+ $controller = new WP_REST_TinyMCE_I18n_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.
*
diff --git a/packages/block-library/src/classic/edit.js b/packages/block-library/src/classic/edit.js
index 7ea8b295c8aefc..7db36295744fca 100644
--- a/packages/block-library/src/classic/edit.js
+++ b/packages/block-library/src/classic/edit.js
@@ -5,6 +5,12 @@ import { Component } from '@wordpress/element';
import { __, _x } from '@wordpress/i18n';
import { BACKSPACE, DELETE, F10 } from '@wordpress/keycodes';
+/**
+ * Internal dependencies
+ */
+import LazyLoad from './lazy';
+import apiFetch from '@wordpress/api-fetch';
+
const { wp } = window;
function isTmceEmpty( editor ) {
@@ -23,7 +29,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 +222,25 @@ export default class ClassicEdit extends Component {
/* eslint-enable jsx-a11y/no-static-element-interactions */
}
}
+
+const LazyClassicEdit = ( props ) => (
+ {
+ 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...
}
+ >
+
+
+);
+
+export default LazyClassicEdit;
diff --git a/packages/block-library/src/classic/lazy.js b/packages/block-library/src/classic/lazy.js
new file mode 100644
index 00000000000000..de80dd403da3c6
--- /dev/null
+++ b/packages/block-library/src/classic/lazy.js
@@ -0,0 +1,175 @@
+/**
+ * WordPress dependencies
+ */
+import { useState } from '@wordpress/element';
+import apiFetch from '@wordpress/api-fetch';
+
+/**
+ * External dependencies
+ */
+import { noop, sum } from 'lodash';
+
+/**
+ * @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 {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 {(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();
+
+ return async ( dependencies ) => {
+ const dependenciesToLoad = dependencies.filter(
+ ( handle ) => ! previouslyLoadedDependencies.has( handle )
+ );
+
+ if ( dependenciesToLoad.length === 0 ) {
+ return 0;
+ }
+
+ 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, 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.
+ *
+ * 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 );
+ } );
+ }
+
+ orderedDependenciesWithDependencies.forEach( ( { handle } ) =>
+ previouslyLoadedDependencies.add( handle )
+ );
+
+ return orderedDependenciesWithDependencies.length;
+ };
+};
+
+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;
+} );
+
+/**
+ * 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,
+ children,
+ placeholder,
+ onLoaded,
+ onError,
+} ) => {
+ const [ loaded, setLoaded ] = useState( false );
+
+ Promise.all( [ loadScripts( scripts ), loadStyles( styles ) ] )
+ .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 );
+ } )
+ .catch( onError );
+
+ if ( loaded ) {
+ return children;
+ }
+
+ return placeholder;
+};
+
+LazyLoad.defaultProps = {
+ scripts: [],
+ styles: [],
+ placeholder: null,
+ onLoaded: () => Promise.resolve(),
+ onError: noop,
+};
+
+export default LazyLoad;
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.
*/