Automattic / isolated-block-editor

Repackages Gutenberg's editor playground as a full-featured multi-instance editor that does not require WordPress.
344 stars 50 forks source link

Isolated Block Editor

Repackages Gutenberg's editor playground as a full-featured multi-instance editor that does not require WordPress.

The key features are:

And a full list of features:

The Isolated Block Editor is provided in three forms:

This library only works with the specific versions of Gutenberg listed below.

Requires: Gutenberg 16.9

Examples:

Do you use this in your own project? Let us know!

Why would I use this?

Gutenberg already provides a playground which allows it to be used outside of WordPress. This is actually used as the basis for the Isolated Block Editor.

However, it provides a limited set of features, and extending it further into a usable editor can take some effort. For example, it doesn't include any undo history.

The Isolated Block Editor is a full editor that handles a lot of these complexities. It should not be considered a replacement for the Gutenberg playground, but more of an opinionated layer on top.

This should be considered experimental, and subject to changes.

Bundling and WordPress

The Isolated Block Editor aims to provide an editor that is as isolated from WordPress as possible. However, it can still be used on a WordPress site, and the decision comes down to the bundling:

Examples are provided for both situations (see Plain text editor for bundled and Blocks Everywhere for unbundled).

The key difference is in the Webpack config. If you don't want to bundle Gutenberg with your editor then you can use the DependencyExtractionWebpackPlugin plugin:

plugins: [
    new DependencyExtractionWebpackPlugin( { injectPolyfill: true } )
]

Alternatively you can use the @wordpress/scripts build system, which automatically runs DependencyExtractionWebpackPlugin:

wp-scripts start

You can use the provided iso-gutenberg.php file to help when using the IsolatedBlockEditor on a WordPress site. It will load Gutenberg and set up your configuration.

Standalone Module

You can use the provided isolated-block-editor.js, core.css, and isolated-block-editor.css files on any web page, regardless of whether it is on WordPress. It will provide two global functions which can turn a textarea into a block editor. Here's an example:

<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<script src="https://github.com/Automattic/isolated-block-editor/raw/trunk/isolated-block-editor.js"></script>
<link rel="stylesheet" href="https://github.com/Automattic/isolated-block-editor/blob/trunk/core.css" />
<link rel="stylesheet" href="https://github.com/Automattic/isolated-block-editor/blob/trunk/isolated-block-editor.css" />

<body>
    <textarea id="editor"></textarea>

    <script>
        wp.attachEditor( document.getElementById( 'editor' ) );
    </script>
</body>

Note that you must load React before loading the editor.

You can find an example in src/browser/index.html.

CSS

If you are using on a WordPress site then WordPress will load the core Gutenberg CSS as part of the iso-gutenberg.php script.

If you are not using on a WordPress site then you will need to load the core CSS yourself. You can either do this by including the following modules and importing directly:

@import '@wordpress/components/build-style/style.css';
@import '@wordpress/block-editor/build-style/style.css';
@import '@wordpress/block-library/build-style/style.css';
@import '@wordpress/block-library/build-style/editor.css';
@import '@wordpress/block-library/build-style/theme.css';
@import '@wordpress/format-library/build-style/style.css';

Alternatively you can directly import the bundled build-browser/core.css CSS:

import '@automattic/isolated-block-editor/build-browser/core.css';

Using

The module is currently only available on Github and can be added with:

npm install @automattic/isolated-block-editor@1.2.0 --save (where 1.2.0 is the version you want to use)

Future

The code here deals with two kinds of problems:

It is hoped that most of the workarounds can be migrated back to Gutenberg so that they are no longer needed. Sometimes these workarounds involve duplicating parts of Gutenberg, which is not ideal. It is possible that the Isolated Block Editor may no longer be needed as a seperate entity.

Development

If multiple editors are on-screen then the IsolatedBlockEditor will ensure that the wp global refers to the currently focussed instance. This should make it more compatible with plugins and third-party code that uses the wp global.

Usage

Include the IsolatedBlockEditor module and then create an instance:


import IsolatedBlockEditor from '@automattic/isolated-block-editor';

render(
    <IsolatedBlockEditor
        settings={ settings }
        onSaveContent={ ( html ) => saveContent( html ) }
        onLoad={ ( parse ) => loadInitialContent( parse ) }
        onError={ () => document.location.reload() }
    >
    </IsolatedBlockEditor>,
    document.querySelector( '#editor' )
);

The IsolatedBlockEditor also exports the following support components:


import IsolatedBlockEditor, { EditorLoaded, DocumentSection, ToolbarSlot } from 'isolated-block-editor';

render(
    <IsolatedBlockEditor
        settings={ settings }
        onSaveContent={ ( html ) => saveContent( html ) }
        onLoad={ ( parse ) => loadInitialContent( parse ) }
        onError={ () => document.location.reload() }
    >
        <EditorLoaded onLoaded={ () => {} } onLoading={ () => {} } />
        <DocumentSection>Extra Information</DocumentSection>

        <ToolbarSlot>
            <button>Beep!</button>
        </ToolbarSlot>
    </IsolatedBlockEditor>,
    document.querySelector( '#editor' )
);

The following function is also provided:

Props

settings

A settings object that contains all settings for the IsolatedBlockEditor, as well as for Gutenberg. Any settings not provided will use defaults.

The block allow and disallow list works as follows:

onSaveContent

Save callback that saves the content as an HTML string. Will be called for each change in the editor content.

onSaveBlocks

Save callback that is supplied with a list of blocks and a list of ignored content. This gives more control than onSaveContent, and is used if you want to filter the saved blocks. For example, if you are using a template then it will appear in the ignoredContent, and you can then ignore the onSaveBlocks call if it matches the blocks.

onLoad

Load the initial content into the editor. This is a callback that runs after the editor has been created, and is supplied with a parse function that is specific to this editor instance. This should be used so that the appropriate blocks are available.

__experimentalUndoManager

An alternative history undo/redo manager to be used by the editor. The passed in object must contain an undo and a redo methods, as well as a undoStack and a redoStack array properties containing the corresponding history. If not provided, the default history management will be used. This property is experimental and can change or be removed at any time.

__experimentalOnInput

An optional callback that will be passed down to the Gutenberg editor if provided.This property is experimental and can change or be removed at any time.

__experimentalOnChange

An optional callback that will be passed down to the Gutenberg editor if provided.This property is experimental and can change or be removed at any time.

__experimentalValue

An optional value (blocks) for the editor to show. If provided, it will be used as the internal value/blocks to display.This property is experimental and can change or be removed at any time.

__experimentalOnSelection

An optional callback that will be called when the selection changes in the editor. The only parameter passed to the callback will be the new selection value.

onError

Callback if an error occurs.

className

Additional class name attached to the editor.

renderMoreMenu

Render additional components in the more menu.

Note: this needs improving or replacing with something more flexible

children

Add any components to customise the editor. These components will have access to the editor's Redux store.

Media uploader

If you want to make use of a media uploader and media library then you must set this up similar using the editor.MediaUpload filter. For example, if you want to use the Gutenberg media library then this would be:

import { MediaUpload } from '@wordpress/media-utils';

addFilter( 'editor.MediaUpload', 'your-namespace/media-upload', () => MediaUpload );

You will also need to pass in the media uploader as part of the editor settings:

import { mediaUpload } from '@wordpress/editor';

const settings = { your settings };

settings.editor.mediaUpload = mediaUpload;

In versions earlier than 2.21.0 this was automatically done, but this meant that you were unable to modify or disable it.

Custom settings sidebar

By default the editor will use the Gutenberg settings sidebar. This provides the block and document inspector tabs, along with associated content.

If you wish to customise this sidebar then you can use the iso.sidebar.customComponent setting and pass a function that returns a React component.

You will need to manage the display of the sidebar yourself, including whether it should appear or not. It may help to look at the existing sidebar code for reference.

For example:

sidebar: {
    customComponent: () => <div>My custom sidebar</div>
},

Extending

Custom behaviour can be added through child components. These components will have access to the isolated/editor store, as well as to the editor instance versions of core/block-editor.

Gutenberg requirements

Gutenberg uses iframes to display various parts of the editor, and it uses the global window.__editorAssets to store styles and scripts that will be added to this iframe. You may need to also use this.

The default is:

window.__editorAssets = { styles: '', scripts: '' }

The values should be full <link> and <script> tags.

Releasing

To make a release, ensure you are on the trunk branch. Do not update the version number in package.json - the release process will do this for you. Then run:

GITHUB_TOKEN=<TOKEN> yarn dist

Common Problems

If Storybook complains about different ES6 problems then it can sometimes be solved with npx yarn-deduplicate --scopes @babel