diff --git a/.buildignore b/.buildignore
new file mode 100644
index 0000000..8a0fb58
--- /dev/null
+++ b/.buildignore
@@ -0,0 +1,54 @@
+# Development files
+.git
+.gitignore
+.gitattributes
+.vscode
+.idea
+*.swp
+*.swo
+*~
+
+# Documentation
+README.md
+AGENTS.md
+CLAUDE.md
+LICENSE.md
+docs
+
+# Development dependencies
+node_modules
+vendor
+composer.lock
+package-lock.json
+yarn.lock
+
+# Source files (compiled assets are in build/)
+src
+
+# Build artifacts to exclude
+*.zip
+*.tar.gz
+.DS_Store
+._*
+*.log
+*.tmp
+*.temp
+.env*
+
+# Build/dev config files
+build.sh
+.buildignore
+.eslintrc.js
+.prettierrc.js
+tsconfig.json
+types.d.ts
+webpack.config.js
+phpcs.xml
+phpunit.xml
+psalm.xml
+
+# Tests
+tests
+
+# WordPress.org assets (not needed in plugin)
+resources
diff --git a/.eslintrc.js b/.eslintrc.js
index 3fc610e..be6fcc5 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -1,14 +1,7 @@
-/**
- * External dependencies
- */
-const { escapeRegExp
-} = require( 'lodash' );
-
/**
* Internal dependencies
*/
-const { version
-} = require( './package' );
+const { version } = require( './package' );
/**
* Regular expression string matching a SemVer string with equal major/minor to
@@ -16,19 +9,23 @@ const { version
*
* @type {string}
*/
-const majorMinorRegExp = escapeRegExp( version.replace( /\.\d+$/, '' ) ) + '(\\.\\d+)?';
+const majorMinorRegExp = version.replace( /\.\d+$/, '' ).replace( /[.*+?^${}()|[\]\\]/g, '\\$&' ) + '(\\.\\d+)?';
module.exports = {
root: true,
- extends: [
- 'plugin:@wordpress/eslint-plugin/recommended',
- 'plugin:eslint-comments/recommended',
- ],
- plugins: [
- 'import',
- ],
+ extends: [ 'plugin:@wordpress/eslint-plugin/recommended', 'plugin:eslint-comments/recommended' ],
+ plugins: [ 'import' ],
globals: {
- wp: 'off',
+ wp: 'readonly',
+ wpBlocksEverywhere: 'readonly',
+ jQuery: 'readonly',
+ MutationObserver: 'readonly',
+ DOMParser: 'readonly',
+ AbortController: 'readonly',
+ FormData: 'readonly',
+ },
+ env: {
+ browser: true,
},
rules: {
'@wordpress/dependency-group': 'error',
@@ -39,147 +36,129 @@ module.exports = {
// we'll get a `SyntaxError` (Invalid regular expression: \ at end of pattern)
// here. That's why we use \\u002F in the regexes below.
{
- selector: 'ImportDeclaration[source.value=/^@wordpress\\u002F.+\\u002F/
- ]',
+ selector: 'ImportDeclaration[source.value=/^@wordpress\\u002F.+\\u002F/]',
message: 'Path access on WordPress dependencies is not allowed.',
},
{
- selector: 'ImportDeclaration[source.value=/^react-spring(?!\\u002Fweb\.cjs)/
- ]',
+ selector: 'ImportDeclaration[source.value=/^react-spring(?!\\u002Fweb.cjs)/]',
message: 'The react-spring dependency must specify CommonJS bundle: react-spring/web.cjs',
},
{
- selector: 'CallExpression[callee.name="deprecated"
- ] Property[key.name="version"
- ][value.value=/' + majorMinorRegExp + '/
- ]',
+ selector:
+ 'CallExpression[callee.name="deprecated"] Property[key.name="version"][value.value=/' +
+ majorMinorRegExp +
+ '/]',
message: 'Deprecated functions must be removed before releasing this version.',
},
{
- selector: 'CallExpression[callee.name=/^(__|_n|_nx|_x)$/
- ]:not([arguments.0.type=/^Literal|BinaryExpression$/
- ])',
+ selector:
+ 'CallExpression[callee.name=/^(__|_n|_nx|_x)$/]:not([arguments.0.type=/^Literal|BinaryExpression$/])',
message: 'Translate function arguments must be string literals.',
},
{
- selector: 'CallExpression[callee.name=/^(_n|_nx|_x)$/
- ]:not([arguments.1.type=/^Literal|BinaryExpression$/
- ])',
+ selector:
+ 'CallExpression[callee.name=/^(_n|_nx|_x)$/]:not([arguments.1.type=/^Literal|BinaryExpression$/])',
message: 'Translate function arguments must be string literals.',
},
{
- selector: 'CallExpression[callee.name=_nx
- ]:not([arguments.3.type=/^Literal|BinaryExpression$/
- ])',
+ selector: 'CallExpression[callee.name=_nx]:not([arguments.3.type=/^Literal|BinaryExpression$/])',
message: 'Translate function arguments must be string literals.',
},
{
- selector: 'CallExpression[callee.name=/^(__|_x|_n|_nx)$/
- ] Literal[value=/\\.{
- 3
- }/
- ]',
+ selector: 'CallExpression[callee.name=/^(__|_x|_n|_nx)$/] Literal[value=/\\.{3}/]',
message: 'Use ellipsis character (…) in place of three dots',
},
{
- selector: 'ImportDeclaration[source.value="redux"
- ] Identifier.imported[name="combineReducers"
- ]',
+ selector: 'ImportDeclaration[source.value="redux"] Identifier.imported[name="combineReducers"]',
message: 'Use `combineReducers` from `@wordpress/data`',
},
{
- selector: 'ImportDeclaration[source.value="lodash"
- ] Identifier.imported[name="memoize"
- ]',
- message: 'Use memize instead of Lodash’s memoize',
+ selector: 'ImportDeclaration[source.value="lodash"] Identifier.imported[name="memoize"]',
+ message: "Use memize instead of Lodash's memoize",
},
{
- selector: 'CallExpression[callee.object.name="page"
- ][callee.property.name="waitFor"
- ]',
+ selector: 'CallExpression[callee.object.name="page"][callee.property.name="waitFor"]',
message: 'Prefer page.waitForSelector instead.',
},
{
- selector: 'JSXAttribute[name.name="id"
- ][value.type="Literal"
- ]',
+ selector: 'JSXAttribute[name.name="id"][value.type="Literal"]',
message: 'Do not use string literals for IDs; use withInstanceId instead.',
},
{
// Discourage the usage of `Math.random()` as it's a code smell
// for UUID generation, for which we already have a higher-order
// component: `withInstanceId`.
- selector: 'CallExpression[callee.object.name="Math"
- ][callee.property.name="random"
- ]',
- message: 'Do not use Math.random() to generate unique IDs; use withInstanceId instead. (If you’re not generating unique IDs: ignore this message.)',
+ selector: 'CallExpression[callee.object.name="Math"][callee.property.name="random"]',
+ message:
+ 'Do not use Math.random() to generate unique IDs; use withInstanceId instead. (If you’re not generating unique IDs: ignore this message.)',
},
{
- selector: 'CallExpression[callee.name="withDispatch"
- ] > :function > BlockStatement > :not(VariableDeclaration,ReturnStatement)',
- message: 'withDispatch must return an object with consistent keys. Avoid performing logic in `mapDispatchToProps`.',
+ selector:
+ 'CallExpression[callee.name="withDispatch"] > :function > BlockStatement > :not(VariableDeclaration,ReturnStatement)',
+ message:
+ 'withDispatch must return an object with consistent keys. Avoid performing logic in `mapDispatchToProps`.',
},
{
- selector: 'LogicalExpression[operator="&&"
- ][left.property.name="length"
- ][right.type="JSXElement"
- ]',
+ selector: 'LogicalExpression[operator="&&"][left.property.name="length"][right.type="JSXElement"]',
message: 'Avoid truthy checks on length property rendering, as zero length is rendered verbatim.',
},
],
- 'react/forbid-elements': [ 'error',
+ 'react/forbid-elements': [
+ 'error',
{
- forbid: [
- [ 'circle', 'Circle'
- ],
- [ 'g', 'G'
- ],
- [ 'path', 'Path'
- ],
- [ 'polygon', 'Polygon'
- ],
- [ 'rect', 'Rect'
- ],
- [ 'svg', 'SVG'
- ],
- ].map( ( [ element, componentName
- ] ) => {
- return {
- element,
- message: `use cross-platform <${ componentName
- }> component instead.`,
+ forbid: [
+ [ 'circle', 'Circle' ],
+ [ 'g', 'G' ],
+ [ 'path', 'Path' ],
+ [ 'polygon', 'Polygon' ],
+ [ 'rect', 'Rect' ],
+ [ 'svg', 'SVG' ],
+ ].map( ( [ element, componentName ] ) => {
+ return {
+ element,
+ message: `use cross-platform <${ componentName }> component instead.`,
};
} ),
- }
+ },
],
},
+ ignorePatterns: [ 'types.d.ts', 'build/**', 'node_modules/**' ],
overrides: [
{
- files: [ 'packages /**/*.js'
- ],
+ files: [ 'src/**/*.tsx', 'src/**/*.ts' ],
+ parserOptions: {
+ ecmaVersion: 2020,
+ sourceType: 'module',
+ ecmaFeatures: {
+ jsx: true,
+ },
+ },
rules: {
- 'import/no-extraneous-dependencies': 'error',
+ 'import/no-extraneous-dependencies': 'off',
+ '@wordpress/no-unsafe-wp-apis': 'off',
+ 'no-undef': 'off',
+ 'no-unused-vars': 'off',
+ 'jsdoc/require-param-type': 'off',
+ 'jsdoc/no-undefined-types': 'off',
+ '@wordpress/i18n-translator-comments': 'off',
+ 'no-shadow': 'off',
+ 'no-duplicate-imports': 'off',
+ 'no-nested-ternary': 'off',
},
- excludedFiles: [
- '** /*.@(android|ios|native).js',
- '**/benchmark /**/*.js',
- '**/@(__mocks__|__tests__|test) /**/*.js',
- ],
},
{
- files: [
- 'packages/jest* /**/*.js',
- ],
- extends: [
- 'plugin:@wordpress/eslint-plugin/test-unit',
- ],
+ files: [ 'src/**/*.js' ],
+ rules: {
+ 'import/no-extraneous-dependencies': 'off',
+ '@wordpress/no-unsafe-wp-apis': 'off',
+ 'no-undef': 'off',
+ },
},
{
- files: [ 'packages/e2e-test* /**/*.js'
- ],
- extends: [
- 'plugin:@wordpress/eslint-plugin/test-e2e',
- ],
+ files: [ 'webpack.config.js' ],
+ rules: {
+ 'import/no-extraneous-dependencies': 'off',
+ },
},
],
};
diff --git a/README.md b/README.md
index 9b44768..5ea335e 100644
--- a/README.md
+++ b/README.md
@@ -2,9 +2,7 @@
-Switches the default WordPress editor for comments, bbPress, and BuddyPress to use Gutenberg. These can now use a richer set of editing tools, as well as having access to the full power of Gutenberg blocks.
-
-Admin moderation is also upgraded to use Gutenberg, and blocks are processed on the front end.
+Switches the default WordPress editor for comments, bbPress, and BuddyPress to use Gutenberg. Admin moderation is also upgraded to use Gutenberg, and blocks are processed on the front end.
For extra security the list of available blocks is determined by the allowed tags from WordPress.
@@ -16,12 +14,7 @@ The condition of the Gutenberg replacements are:
- comments - alright
- BuddyPress - needs a lot of work
-The plugin uses the [Isolated Block Editor](https://github.com/Automattic/isolated-block-editor/). This can also be found in:
-
-- [Plain Text Editor](https://github.com/Automattic/isolated-block-editor/blob/trunk/src/browser/README.md) - standalone JS file that can replace any `textarea` on any page with a full Gutenberg editor
-- [Gutenberg Chrome Extension](https://github.com/Automattic/gutenberg-everywhere-chrome/) - a Chrome extension that allows Gutenberg to be used on any page
-- [Gutenberg Desktop](https://github.com/Automattic/gutenberg-desktop/) - a desktop editor that supports the loading and saving of HTML and Markdown files
-- [P2](https://wordpress.com/p2/) - WordPress as a collaborative workspace (coming soon for self-hosted)
+The plugin mounts Gutenberg directly against the `@wordpress/block-editor` primitives shipped with WordPress core (no wrapping editor framework). Earlier releases used [Isolated Block Editor](https://github.com/Automattic/isolated-block-editor/); that dependency was removed in v2.2.0 and the embedded shell now renders directly against current Gutenberg.
Blocks Everywhere can be downloaded from WordPress.org:
@@ -73,12 +66,84 @@ Some settings are available through the settings object, which is filterable wit
`replaceParagraphCode` - Enable the custom paragraph that converts HTML and PHP code into a code block
`pastePlainText` - Convert all pasted content to plain text
`patchEmoji` - set to `true` to stop twemoji from affecting the editor
-`iso.allowEmbeds` - List of enabled embeds
-`iso.blocks.allowBlocks` - List of enabled blocks
-`iso.className` - String of classes to be assigned to the editor.
-`iso.__experimentalOnChange` - An optional callback that is triggered when the blocks are changed.
-`iso.__experimentalOnInput` - An optional callback that is triggered when text is input.
-`iso.__experimentalOnSelection` - An optional callback when a block is selected.
+`blocksEverywhere.allowEmbeds` - List of enabled embeds
+`blocksEverywhere.blocks.allowBlocks` - List of enabled blocks
+`blocksEverywhere.className` - String of classes to be assigned to the editor.
+`blocksEverywhere.__experimentalOnChange` - An optional callback that is triggered when the blocks are changed.
+`blocksEverywhere.__experimentalOnInput` - An optional callback that is triggered when text is input.
+`blocksEverywhere.__experimentalOnSelection` - An optional callback when a block is selected.
+`blocksEverywhere.toolbar` - Per-primitive opt-out for the editor toolbar (see below). Default matches the upstream wp-admin post editor.
+`blocksEverywhere.chrome` - Editor shell region configuration (see below). Lets hosts enable extra chrome slots or hide built-in chrome without replacing the editor shell.
+
+#### Toolbar configuration
+
+`blocksEverywhere.toolbar` accepts a map of per-primitive booleans. Defaults match the upstream wp-admin post editor — every primitive is enabled. Consumers opt OUT of individual primitives; they never need to opt IN. Any key left `unset` is treated as `true`.
+
+| Key | Default | Description |
+| ------------ | ------- | -------------------------------------------------------------------------------------------- |
+| `inserter` | `true` | Document-level `+` block inserter button. Effectively suppressed when a persistent detached sidebar is mounted (the sidebar already exposes the inserter). |
+| `undo` | `true` | Undo button. Delegates to the core editor history; no-op when no entity is being edited. |
+| `redo` | `true` | Redo button. Delegates to the core editor history; no-op when no entity is being edited. |
+| `listView` | `true` | List view toggle + dropdown panel of the editor's blocks. |
+| `blockTools` | `true` | Selected-block format toolbar (the contextual `¶ B I link` row shown when a block is selected). |
+
+Default (matches wp-admin):
+
+```php
+add_filter( 'blocks_everywhere_editor_settings', function ( $settings ) {
+ // No toolbar config needed — defaults to the full upstream toolbar shape.
+ return $settings;
+} );
+```
+
+Opt out of specific primitives:
+
+```php
+add_filter( 'blocks_everywhere_editor_settings', function ( $settings ) {
+ $settings['blocksEverywhere']['toolbar'] = array(
+ 'undo' => false,
+ 'redo' => false,
+ 'listView' => false,
+ // `inserter` and `blockTools` default to true.
+ );
+ return $settings;
+} );
+```
+
+#### Chrome configuration and slots
+
+`blocksEverywhere.chrome` controls editor shell regions. Existing editor chrome stays unchanged by default: the primary toolbar and footer render, while optional host-owned regions are disabled until enabled.
+
+| Key | Default | Description |
+| --- | ------- | ----------- |
+| `mode` | `inline` | Adds a layout class for `inline`, `full-height`, `modal`, or `compact` editor shells. |
+| `topBar` | `false` | Enables the `topBar` and `windowControls` slots above the primary toolbar. |
+| `toolbar` | `true` | Shows the primary toolbar row. Set `false` when a host fully replaces toolbar chrome. |
+| `secondaryToolbar` | `false` | Enables a host-owned row below the primary toolbar. |
+| `footer` | `true` | Shows the footer slot. |
+| `documentSidebar` | `false` | Enables a persistent host-owned panel before the canvas. |
+| `inserterSidebar` | `false` | Enables a persistent host-owned panel after the canvas. |
+
+Register fills with `window.blocksEverywhere.registerSlotFill( slot, renderFn )`. Supported slots are `heading`, `toolbar`, `actions`, `footer`, `topBar`, `windowControls`, `secondaryToolbar`, `documentSidebar`, and `inserterSidebar`.
+
+```php
+add_filter( 'blocks_everywhere_editor_settings', function ( $settings ) {
+ $settings['blocksEverywhere']['chrome'] = array(
+ 'mode' => 'modal',
+ 'topBar' => true,
+ 'secondaryToolbar' => true,
+ 'documentSidebar' => true,
+ );
+
+ $settings['blocksEverywhere']['toolbar'] = array(
+ 'undo' => false,
+ 'redo' => false,
+ 'listView' => false,
+ );
+
+ return $settings;
+} );
+```
### Theme compatibility
@@ -106,7 +171,7 @@ To enable Content Embed block in the editor, pass these settings to `blocks_ever
```
add_filter( 'blocks_everywhere_editor_settings', function( $settings ) {
- $settings['iso']['blocks']['allowBlocks'][] = 'blocks-everywhere/support-content';
+ $settings['blocksEverywhere']['blocks']['allowBlocks'][] = 'blocks-everywhere/support-content';
return $settings;
} );
```
@@ -183,3 +248,19 @@ You can sync this to the WordPress.org SVN repo with:
`yarn dist:svn`
You will need appropriate permissions.
+
+## Documentation
+
+For detailed technical documentation, see the `/docs/` directory:
+
+- **[overview.md](docs/overview.md)** - Plugin purpose, architecture overview, and feature matrix
+- **[architecture.md](docs/architecture.md)** - Complete class hierarchy, design patterns, and extensibility
+- **[components.md](docs/components.md)** - React/TypeScript component organization and patterns
+- **[portable-editor-adapters.md](docs/portable-editor-adapters.md)** - Generic host adapter migration guide for embedded editor shells
+- **[build-and-development.md](docs/build-and-development.md)** - Development workflow and build process
+- **[handlers/](docs/handlers/)** - Platform-specific integration guides
+ - [bbpress-handler.md](docs/handlers/bbpress-handler.md) - BBPress forum integration
+ - [comments-handler.md](docs/handlers/comments-handler.md) - WordPress comments integration
+ - [buddypress-handler.md](docs/handlers/buddypress-handler.md) - BuddyPress activity integration
+
+For developers working on this plugin, see **[CLAUDE.md](CLAUDE.md)** for architectural guidance, coding standards, and development patterns.
diff --git a/blocks-everywhere.php b/blocks-everywhere.php
index 8ad93d0..c2eac99 100644
--- a/blocks-everywhere.php
+++ b/blocks-everywhere.php
@@ -1,47 +1,41 @@
engine = new Engine();
- // Admin editors
- add_action( 'admin_enqueue_scripts', [ $this, 'admin_enqueue_scripts' ] );
+ // Register built-in contexts at priority 5 so third-party can modify at 10.
+ add_filter( 'blocks_everywhere_contexts', [ $this, 'register_builtin_contexts' ], 5 );
+
+ add_action( 'init', [ $this, 'boot' ] );
}
/**
- * Load whatever handler is configured
+ * Register the built-in contexts (bbPress, BuddyPress, Comments).
*
- * @return void
+ * Uses the same filter mechanism as external consumers.
+ *
+ * @param array $contexts Existing contexts.
+ * @return array
*/
- public function load_handlers() {
- $default_comments = defined( 'BLOCKS_EVERYWHERE_COMMENTS' ) ? BLOCKS_EVERYWHERE_COMMENTS : false;
- $default_bbpress = defined( 'BLOCKS_EVERYWHERE_BBPRESS' ) ? BLOCKS_EVERYWHERE_BBPRESS : false;
- $default_buddypress = defined( 'BLOCKS_EVERYWHERE_BUDDYPRESS' ) ? BLOCKS_EVERYWHERE_BUDDYPRESS : false;
-
- if ( apply_filters( 'blocks_everywhere_comments', $default_comments ) ) {
- $this->handlers['Comments'] = new Handler\Comments();
+ public function register_builtin_contexts( $contexts ) {
+ $bbpress = Contexts\bbpress_context( $this->engine );
+ if ( $bbpress ) {
+ $contexts['bbpress'] = $bbpress;
}
- if ( apply_filters( 'blocks_everywhere_bbpress', $default_bbpress ) ) {
- $this->handlers['bbPress'] = new Handler\bbPress();
+ $buddypress = Contexts\buddypress_context( $this->engine );
+ if ( $buddypress ) {
+ $contexts['buddypress'] = $buddypress;
}
- if ( apply_filters( 'blocks_everywhere_buddypress', $default_buddypress ) ) {
- $this->handlers['BuddyPress'] = new Handler\BuddyPress();
+ $comments = Contexts\comments_context( $this->engine );
+ if ( $comments ) {
+ $contexts['comments'] = $comments;
}
+
+ return $contexts;
}
/**
- * Get the instantiated handler class for the specified type, or null if it isn't configured or known.
+ * Boot the engine on init.
*
- * @param 'Comments'|'bbPress'|'BuddyPress' $which The handler type.
- * @return Handler\Handler|null object or null it not configured.
+ * @return void
*/
- public function get_handler( $which ) {
- if ( isset( $this->handlers[ $which ] ) ) {
- return $this->handlers[ $which ];
+ public function boot() {
+ $this->engine->boot();
+
+ // Wire bbPress admin hooks if context is active.
+ if ( $this->engine->get_context( 'bbpress' ) ) {
+ Contexts\bbpress_wire_admin( $this->engine );
}
+ }
- return null;
+ /**
+ * Get the engine instance.
+ *
+ * @return Engine
+ */
+ public function get_engine() {
+ return $this->engine;
}
/**
- * Perform additional admin tasks when on the comment page
+ * Backward compatibility — get a handler-like object for the specified type.
*
- * @param String $hook Page hook.
- * @return void
+ * Returns the engine itself since it handles all contexts now.
+ *
+ * @param string $which The handler type ('Comments', 'bbPress', 'BuddyPress').
+ * @return Engine|null
*/
- public function admin_enqueue_scripts( $hook ) {
- foreach ( $this->handlers as $handler ) {
- if ( $handler->can_show_admin_editor( $hook ) ) {
- add_action(
- 'admin_head',
- function() use ( $handler ) {
- add_filter( 'the_editor', [ $handler, 'the_editor' ] );
- add_filter( 'wp_editor_settings', [ $handler, 'wp_editor_settings' ], 10, 2 );
- }
- );
-
- // Stops a problem with the Gutenberg plugin accessing widgets that don't exist
- remove_action( 'admin_footer', 'gutenberg_block_editor_admin_footer' );
-
- // Load Gutenberg in in_admin_header so WP admin doesn't set the 'block-editor-page' body class
- add_action(
- 'in_admin_header',
- function() use ( $handler ) {
- $handler->load_editor( '.wp-editor-area' );
- }
- );
-
- break;
- }
+ public function get_handler( $which ) {
+ $map = [
+ 'Comments' => 'comments',
+ 'bbPress' => 'bbpress',
+ 'BuddyPress' => 'buddypress',
+ ];
+
+ $context_id = $map[ $which ] ?? strtolower( $which );
+
+ if ( $this->engine->get_context( $context_id ) ) {
+ return $this->engine;
}
+
+ return null;
}
}
diff --git a/build.sh b/build.sh
new file mode 120000
index 0000000..c93010f
--- /dev/null
+++ b/build.sh
@@ -0,0 +1 @@
+../../.github/build.sh
\ No newline at end of file
diff --git a/classes/class-editor.php b/classes/class-editor.php
index 996a8b6..f9f07ab 100644
--- a/classes/class-editor.php
+++ b/classes/class-editor.php
@@ -2,8 +2,12 @@
namespace Automattic\Blocks_Everywhere;
+use WP_Block_Editor_Context;
+use WP_Theme_JSON_Data;
+use WP_Theme_JSON_Data_Gutenberg;
+
/**
- * Provides functions to load Gutenberg assets
+ * Provides functions to load Gutenberg assets.
*/
class Editor {
/**
@@ -14,7 +18,17 @@ class Editor {
private $can_upload = false;
/**
- * Constructor
+ * Whether iframe-canvas inline CSS should be appended to editor_settings['styles']
+ * for the next get_block_editor_settings() call. Set inside get_editor_settings()
+ * so the injection is scoped to BE's own block editor invocation and does not
+ * affect unrelated block editor contexts (e.g. wp-admin post editor).
+ *
+ * @var boolean
+ */
+ private $inject_iframe_canvas_styles = false;
+
+ /**
+ * Constructor.
*/
public function __construct() {
add_action( 'template_redirect', [ $this, 'setup_media' ] );
@@ -24,46 +38,58 @@ public function __construct() {
}
/**
- * Provide theme.json
+ * Provide theme.json.
*
- * @param \WP_Theme_JSON_Data_Gutenberg $json JSON.
- * @return \WP_Theme_JSON_Data_Gutenberg
+ * @param WP_Theme_JSON_Data|WP_Theme_JSON_Data_Gutenberg $json JSON.
+ * @return WP_Theme_JSON_Data|WP_Theme_JSON_Data_Gutenberg
*/
public function wp_theme_json_data_theme( $json ) {
- $theme = new \WP_Theme_JSON_Data_Gutenberg(
- [
- 'version' => 2,
- 'settings' => [
- 'color' => [
- 'background' => false,
- 'custom' => false,
- 'customDuotone' => false,
- 'customGradient' => false,
- 'defaultGradients' => false,
- 'defaultPalette' => false,
- 'text' => false,
- ],
- 'typography' => [
- 'customFontSize' => false,
- 'dropCap' => false,
- 'fontStyle' => false,
- 'fontWeight' => false,
- 'letterSpacing' => false,
- 'lineHeight' => false,
- 'textDecoration' => false,
- 'textTransform' => false,
- 'fontSizes' => [],
- 'fontFamilies' => [],
- ],
+ // Merge our customization restrictions into the existing theme.json
+ // data instead of replacing it. Replacing would clobber font-family
+ // definitions that themes register via the same filter to make
+ // @font-face declarations land in the editor iframe via
+ // wp_print_font_faces() — see WP_Font_Face_Resolver::get_fonts_from_theme_json().
+ $data = [
+ 'version' => 2,
+ 'settings' => [
+ 'color' => [
+ 'background' => false,
+ 'custom' => false,
+ 'customDuotone' => false,
+ 'customGradient' => false,
+ 'defaultGradients' => false,
+ 'defaultPalette' => false,
+ 'text' => false,
],
- ]
- );
+ 'typography' => [
+ 'customFontSize' => false,
+ 'dropCap' => false,
+ 'fontStyle' => false,
+ 'fontWeight' => false,
+ 'letterSpacing' => false,
+ 'lineHeight' => false,
+ 'textDecoration' => false,
+ 'textTransform' => false,
+ 'fontSizes' => [],
+ ],
+ ],
+ ];
- return $theme;
+ if ( method_exists( $json, 'update_with' ) ) {
+ $json->update_with( $data );
+ return $json;
+ }
+
+ // Fallback for older WP / Gutenberg without update_with().
+ if ( class_exists( 'WP_Theme_JSON_Data_Gutenberg' ) ) {
+ return new WP_Theme_JSON_Data_Gutenberg( $data );
+ }
+
+ return new WP_Theme_JSON_Data( $data );
}
/**
- * Restrict TinyMCE to the basics
+ * Restrict TinyMCE to the basics.
*
* @param array $settings TinyMCE settings.
* @return array
@@ -76,7 +102,7 @@ public function tiny_mce_before_init( $settings ) {
}
/**
- * Load Gutenberg
+ * Load Gutenberg.
*
* Based on wp-admin/edit-form-blocks.php
*
@@ -89,10 +115,6 @@ public function load( $settings ) {
$this->can_upload = isset( $settings['editor']['hasUploadPermissions'] ) && $settings['editor']['hasUploadPermissions'];
$this->load_extra_blocks();
- // Restrict tinymce buttons
- add_filter( 'tiny_mce_before_init', [ $this, 'tiny_mce_before_init' ] );
-
- // Keep Jetpack out of things
add_filter(
'jetpack_blocks_variation',
function() {
@@ -100,31 +122,44 @@ function() {
}
);
- // Only call the editor assets if we are not dynamically loading.
if ( ! defined( '__EXPERIMENTAL_DYNAMIC_LOAD' ) ) {
- wp_tinymce_inline_scripts();
-
- wp_enqueue_editor();
+ // Enqueue block editor scripts that are required dependencies.
+ // These are normally only loaded in the admin block editor context.
+ wp_enqueue_script( 'lodash' );
+ wp_enqueue_script( 'wp-block-library' );
+ wp_enqueue_script( 'wp-format-library' );
+ wp_enqueue_script( 'wp-editor' );
+ wp_enqueue_script( 'wp-plugins' );
+ wp_enqueue_script( 'wp-media-utils' );
+ wp_enqueue_script( 'wp-viewport' );
+ wp_enqueue_script( 'wp-admin-ui' );
do_action( 'enqueue_block_editor_assets' );
-
- add_action( 'wp_print_footer_scripts', array( '_WP_Editors', 'print_default_editor_scripts' ), 45 );
+ // Also fire enqueue_block_assets so styles from plugins reach the
+ // Gutenberg 22.8+ iframe. enqueue_block_editor_assets only runs on
+ // the host page; enqueue_block_assets fires for both host and iframe.
+ do_action( 'enqueue_block_assets' );
}
- // Optionally skip loading the editor styles.
$should_inline_styles = apply_filters( 'blocks_everywhere_should_enqueue_styles', true );
if ( $should_inline_styles ) {
+ // Core block styles needed for correct frontend rendering inside the iframe
+ // (notably responsive embeds).
+ wp_enqueue_style( 'wp-block-library' );
+ wp_enqueue_style( 'wp-block-library-theme' );
+
wp_enqueue_style( 'wp-edit-post' );
wp_enqueue_style( 'wp-format-library' );
set_current_screen( 'front' );
- wp_styles()->done = array( 'wp-reset-editor-styles' );
+ wp_styles()->done = [ 'wp-reset-editor-styles' ];
}
$this->setup_rest_api();
- $categories = wp_json_encode( get_block_categories( $post ) );
+ $block_editor_context = new WP_Block_Editor_Context( [ 'post' => $post ] );
+ $categories = wp_json_encode( get_block_categories( $block_editor_context ) );
if ( $categories !== false ) {
wp_add_inline_script(
@@ -137,16 +172,19 @@ function() {
/**
* @psalm-suppress PossiblyFalseOperand
*/
+ $server_block_settings = apply_filters( 'blocks_everywhere_server_block_settings', get_block_editor_server_block_settings(), $block_editor_context );
+
wp_add_inline_script(
'wp-blocks',
- 'wp.blocks.unstable__bootstrapServerSideBlockDefinitions(' . wp_json_encode( get_block_editor_server_block_settings() ) . ');'
+ 'if ( typeof wp.blocks.unstable__bootstrapServerSideBlockDefinitions === "function" ) { wp.blocks.unstable__bootstrapServerSideBlockDefinitions(' . wp_json_encode( $server_block_settings ) . '); }',
+ 'after'
);
$this->setup_media();
}
/**
- * Load any third-party blocks
+ * Load any third-party blocks.
*
* @return void
*/
@@ -167,7 +205,6 @@ private function load_extra_blocks() {
*/
require_once ABSPATH . 'wp-admin/includes/post.php';
- // Fake a WP_Screen object so we can pretend we're in the block editor, and therefore other block libraries load
set_current_screen();
$current_screen = get_current_screen();
@@ -177,7 +214,15 @@ private function load_extra_blocks() {
}
/**
- * Override some features that probably don't make sense in an isolated editor
+ * Override some features that probably don't make sense in an isolated editor.
+ *
+ * Also injects BE-owned iframe canvas CSS into editor_settings['styles'][]
+ * during BE's own get_editor_settings() invocation. Gutenberg inlines these
+ * entries directly into the iframe srcdoc, which is the canonical iframe
+ * styling API and avoids the iframe-compat clone warning that fires when
+ * host-page /