@wordpress/editor
Version:
Enhanced block editor for WordPress posts.
341 lines (327 loc) • 14.1 kB
JavaScript
/**
* WordPress dependencies
*/
import { useEffect, useLayoutEffect, useMemo } from '@wordpress/element';
import { useDispatch, useSelect } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { EntityProvider, useEntityBlockEditor, store as coreStore } from '@wordpress/core-data';
import { BlockEditorProvider, BlockContextProvider, privateApis as blockEditorPrivateApis } from '@wordpress/block-editor';
import { store as noticesStore } from '@wordpress/notices';
import { privateApis as editPatternsPrivateApis } from '@wordpress/patterns';
import { createBlock } from '@wordpress/blocks';
/**
* Internal dependencies
*/
import withRegistryProvider from './with-registry-provider';
import { store as editorStore } from '../../store';
import useBlockEditorSettings from './use-block-editor-settings';
import { unlock } from '../../lock-unlock';
import DisableNonPageContentBlocks from './disable-non-page-content-blocks';
import NavigationBlockEditingMode from './navigation-block-editing-mode';
import { useHideBlocksFromInserter } from './use-hide-blocks-from-inserter';
import useCommands from '../commands';
import BlockRemovalWarnings from '../block-removal-warnings';
import StartPageOptions from '../start-page-options';
import KeyboardShortcutHelpModal from '../keyboard-shortcut-help-modal';
import ContentOnlySettingsMenu from '../block-settings-menu/content-only-settings-menu';
import StartTemplateOptions from '../start-template-options';
import EditorKeyboardShortcuts from '../global-keyboard-shortcuts';
import PatternRenameModal from '../pattern-rename-modal';
import PatternDuplicateModal from '../pattern-duplicate-modal';
import TemplatePartMenuItems from '../template-part-menu-items';
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
const {
ExperimentalBlockEditorProvider
} = unlock(blockEditorPrivateApis);
const {
PatternsMenuItems
} = unlock(editPatternsPrivateApis);
const noop = () => {};
/**
* These are global entities that are only there to split blocks into logical units
* They don't provide a "context" for the current post/page being rendered.
* So we should not use their ids as post context. This is important to allow post blocks
* (post content, post title) to be used within them without issues.
*/
const NON_CONTEXTUAL_POST_TYPES = ['wp_block', 'wp_navigation', 'wp_template_part'];
/**
* Depending on the post, template and template mode,
* returns the appropriate blocks and change handlers for the block editor provider.
*
* @param {Array} post Block list.
* @param {boolean} template Whether the page content has focus (and the surrounding template is inert). If `true` return page content blocks. Default `false`.
* @param {string} mode Rendering mode.
*
* @example
* ```jsx
* const [ blocks, onInput, onChange ] = useBlockEditorProps( post, template, mode );
* ```
*
* @return {Array} Block editor props.
*/
function useBlockEditorProps(post, template, mode) {
const rootLevelPost = mode === 'template-locked' ? 'template' : 'post';
const [postBlocks, onInput, onChange] = useEntityBlockEditor('postType', post.type, {
id: post.id
});
const [templateBlocks, onInputTemplate, onChangeTemplate] = useEntityBlockEditor('postType', template?.type, {
id: template?.id
});
const maybeNavigationBlocks = useMemo(() => {
if (post.type === 'wp_navigation') {
return [createBlock('core/navigation', {
ref: post.id,
// As the parent editor is locked with `templateLock`, the template locking
// must be explicitly "unset" on the block itself to allow the user to modify
// the block's content.
templateLock: false
})];
}
}, [post.type, post.id]);
// It is important that we don't create a new instance of blocks on every change
// We should only create a new instance if the blocks them selves change, not a dependency of them.
const blocks = useMemo(() => {
if (maybeNavigationBlocks) {
return maybeNavigationBlocks;
}
if (rootLevelPost === 'template') {
return templateBlocks;
}
return postBlocks;
}, [maybeNavigationBlocks, rootLevelPost, templateBlocks, postBlocks]);
// Handle fallback to postBlocks outside of the above useMemo, to ensure
// that constructed block templates that call `createBlock` are not generated
// too frequently. This ensures that clientIds are stable.
const disableRootLevelChanges = !!template && mode === 'template-locked' || post.type === 'wp_navigation';
if (disableRootLevelChanges) {
return [blocks, noop, noop];
}
return [blocks, rootLevelPost === 'post' ? onInput : onInputTemplate, rootLevelPost === 'post' ? onChange : onChangeTemplate];
}
/**
* This component provides the editor context and manages the state of the block editor.
*
* @param {Object} props The component props.
* @param {Object} props.post The post object.
* @param {Object} props.settings The editor settings.
* @param {boolean} props.recovery Indicates if the editor is in recovery mode.
* @param {Array} props.initialEdits The initial edits for the editor.
* @param {Object} props.children The child components.
* @param {Object} [props.BlockEditorProviderComponent] The block editor provider component to use. Defaults to ExperimentalBlockEditorProvider.
* @param {Object} [props.__unstableTemplate] The template object.
*
* @example
* ```jsx
* <ExperimentalEditorProvider
* post={ post }
* settings={ settings }
* recovery={ recovery }
* initialEdits={ initialEdits }
* __unstableTemplate={ template }
* >
* { children }
* </ExperimentalEditorProvider>
*
* @return {Object} The rendered ExperimentalEditorProvider component.
*/
export const ExperimentalEditorProvider = withRegistryProvider(({
post,
settings,
recovery,
initialEdits,
children,
BlockEditorProviderComponent = ExperimentalBlockEditorProvider,
__unstableTemplate: template
}) => {
const hasTemplate = !!template;
const {
editorSettings,
selection,
isReady,
mode,
defaultMode,
postTypeEntities
} = useSelect(select => {
const {
getEditorSettings,
getEditorSelection,
getRenderingMode,
__unstableIsEditorReady,
getDefaultRenderingMode
} = unlock(select(editorStore));
const {
getEntitiesConfig
} = select(coreStore);
const _mode = getRenderingMode();
const _defaultMode = getDefaultRenderingMode(post.type);
/**
* To avoid content "flash", wait until rendering mode has been resolved.
* This is important for the initial render of the editor.
*
* - Wait for template to be resolved if the default mode is 'template-locked'.
* - Wait for default mode to be resolved otherwise.
*/
const hasResolvedDefaultMode = _defaultMode === 'template-locked' ? hasTemplate : _defaultMode !== undefined;
// Wait until the default mode is retrieved and start rendering canvas.
const isRenderingModeReady = _defaultMode !== undefined;
return {
editorSettings: getEditorSettings(),
isReady: __unstableIsEditorReady(),
mode: isRenderingModeReady ? _mode : undefined,
defaultMode: hasResolvedDefaultMode ? _defaultMode : undefined,
selection: getEditorSelection(),
postTypeEntities: post.type === 'wp_template' ? getEntitiesConfig('postType') : null
};
}, [post.type, hasTemplate]);
const shouldRenderTemplate = hasTemplate && mode !== 'post-only';
const rootLevelPost = shouldRenderTemplate ? template : post;
const defaultBlockContext = useMemo(() => {
const postContext = {};
// If it is a template, try to inherit the post type from the name.
if (post.type === 'wp_template') {
if (post.slug === 'page') {
postContext.postType = 'page';
} else if (post.slug === 'single') {
postContext.postType = 'post';
} else if (post.slug.split('-')[0] === 'single') {
// If the slug is single-{postType}, infer the post type from the name.
const postTypeNames = postTypeEntities?.map(entity => entity.name) || [];
const match = post.slug.match(`^single-(${postTypeNames.join('|')})(?:-.+)?$`);
if (match) {
postContext.postType = match[1];
}
}
} else if (!NON_CONTEXTUAL_POST_TYPES.includes(rootLevelPost.type) || shouldRenderTemplate) {
postContext.postId = post.id;
postContext.postType = post.type;
}
return {
...postContext,
templateSlug: rootLevelPost.type === 'wp_template' ? rootLevelPost.slug : undefined
};
}, [shouldRenderTemplate, post.id, post.type, post.slug, rootLevelPost.type, rootLevelPost.slug, postTypeEntities]);
const {
id,
type
} = rootLevelPost;
const blockEditorSettings = useBlockEditorSettings(editorSettings, type, id, mode);
const [blocks, onInput, onChange] = useBlockEditorProps(post, template, mode);
const {
updatePostLock,
setupEditor,
updateEditorSettings,
setCurrentTemplateId,
setEditedPost,
setRenderingMode
} = unlock(useDispatch(editorStore));
const {
createWarningNotice
} = useDispatch(noticesStore);
// Ideally this should be synced on each change and not just something you do once.
useLayoutEffect(() => {
// Assume that we don't need to initialize in the case of an error recovery.
if (recovery) {
return;
}
updatePostLock(settings.postLock);
setupEditor(post, initialEdits, settings.template);
if (settings.autosave) {
createWarningNotice(__('There is an autosave of this post that is more recent than the version below.'), {
id: 'autosave-exists',
actions: [{
label: __('View the autosave'),
url: settings.autosave.editLink
}]
});
}
// The dependencies of the hook are omitted deliberately
// We only want to run setupEditor (with initialEdits) only once per post.
// A better solution in the future would be to split this effect into multiple ones.
}, []);
// Synchronizes the active post with the state
useEffect(() => {
setEditedPost(post.type, post.id);
}, [post.type, post.id, setEditedPost]);
// Synchronize the editor settings as they change.
useEffect(() => {
updateEditorSettings(settings);
}, [settings, updateEditorSettings]);
// Synchronizes the active template with the state.
useEffect(() => {
setCurrentTemplateId(template?.id);
}, [template?.id, setCurrentTemplateId]);
// Sets the right rendering mode when loading the editor.
useEffect(() => {
if (defaultMode) {
setRenderingMode(defaultMode);
}
}, [defaultMode, setRenderingMode]);
useHideBlocksFromInserter(post.type, mode);
// Register the editor commands.
useCommands();
if (!isReady || !mode) {
return null;
}
return /*#__PURE__*/_jsx(EntityProvider, {
kind: "root",
type: "site",
children: /*#__PURE__*/_jsx(EntityProvider, {
kind: "postType",
type: post.type,
id: post.id,
children: /*#__PURE__*/_jsx(BlockContextProvider, {
value: defaultBlockContext,
children: /*#__PURE__*/_jsxs(BlockEditorProviderComponent, {
value: blocks,
onChange: onChange,
onInput: onInput,
selection: selection,
settings: blockEditorSettings,
useSubRegistry: false,
children: [children, !settings.isPreviewMode && /*#__PURE__*/_jsxs(_Fragment, {
children: [/*#__PURE__*/_jsx(PatternsMenuItems, {}), /*#__PURE__*/_jsx(TemplatePartMenuItems, {}), /*#__PURE__*/_jsx(ContentOnlySettingsMenu, {}), mode === 'template-locked' && /*#__PURE__*/_jsx(DisableNonPageContentBlocks, {}), type === 'wp_navigation' && /*#__PURE__*/_jsx(NavigationBlockEditingMode, {}), /*#__PURE__*/_jsx(EditorKeyboardShortcuts, {}), /*#__PURE__*/_jsx(KeyboardShortcutHelpModal, {}), /*#__PURE__*/_jsx(BlockRemovalWarnings, {}), /*#__PURE__*/_jsx(StartPageOptions, {}), /*#__PURE__*/_jsx(StartTemplateOptions, {}), /*#__PURE__*/_jsx(PatternRenameModal, {}), /*#__PURE__*/_jsx(PatternDuplicateModal, {})]
})]
})
})
})
});
});
/**
* This component establishes a new post editing context, and serves as the entry point for a new post editor (or post with template editor).
*
* It supports a large number of post types, including post, page, templates,
* custom post types, patterns, template parts.
*
* All modification and changes are performed to the `@wordpress/core-data` store.
*
* @param {Object} props The component props.
* @param {Object} [props.post] The post object to edit. This is required.
* @param {Object} [props.__unstableTemplate] The template object wrapper the edited post.
* This is optional and can only be used when the post type supports templates (like posts and pages).
* @param {Object} [props.settings] The settings object to use for the editor.
* This is optional and can be used to override the default settings.
* @param {React.ReactNode} [props.children] Children elements for which the BlockEditorProvider context should apply.
* This is optional.
*
* @example
* ```jsx
* <EditorProvider
* post={ post }
* settings={ settings }
* __unstableTemplate={ template }
* >
* { children }
* </EditorProvider>
* ```
*
* @return {React.ReactNode} The rendered EditorProvider component.
*/
export function EditorProvider(props) {
return /*#__PURE__*/_jsx(ExperimentalEditorProvider, {
...props,
BlockEditorProviderComponent: BlockEditorProvider,
children: props.children
});
}
export default EditorProvider;
//# sourceMappingURL=index.js.map