UNPKG

@wordpress/editor

Version:
520 lines (483 loc) 16.3 kB
/** * WordPress dependencies */ import { useCallback, 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 { ATTACHMENT_POST_TYPE } from '../../store/constants'; 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 { useRevisionBlocks } from './use-revision-blocks'; import useCommands from '../commands'; import useUploadSaveLock from './use-upload-save-lock'; import BlockRemovalWarnings from '../block-removal-warnings'; import StartPageOptions from '../start-page-options'; import KeyboardShortcutHelpModal from '../keyboard-shortcut-help-modal'; 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'; 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 revisionBlocks = useRevisionBlocks(); 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 ] ); // In revisions mode, use the revision blocks and disable editing. if ( revisionBlocks !== null ) { return [ revisionBlocks, noop, noop ]; } // 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, isInRevisionsMode, currentRevisionId, } = useSelect( ( select ) => { const { getEditorSettings, getRenderingMode, __unstableIsEditorReady, getDefaultRenderingMode, isRevisionsMode: _isRevisionsMode, getCurrentRevisionId: _getCurrentRevisionId, } = unlock( select( editorStore ) ); const { getEntitiesConfig, getEntityRecordEdits } = 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; // Read selection directly from entity edits using the post prop, // bypassing getCurrentPostId() which lags behind in useEffect. const entityEdits = getEntityRecordEdits( 'postType', post.type, post.id ); return { editorSettings: getEditorSettings(), isReady: __unstableIsEditorReady(), mode: isRenderingModeReady ? _mode : undefined, defaultMode: hasResolvedDefaultMode ? _defaultMode : undefined, selection: entityEdits?.selection, postTypeEntities: post.type === 'wp_template' ? getEntitiesConfig( 'postType' ) : null, isInRevisionsMode: _isRevisionsMode(), currentRevisionId: _getCurrentRevisionId(), }; }, [ post.type, post.id, 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 { editEntityRecord } = useDispatch( coreStore ); const onChangeSelection = useCallback( ( newSelection ) => { editEntityRecord( 'postType', post.type, post.id, { selection: newSelection }, { undoIgnore: true } ); }, [ editEntityRecord, post.type, post.id ] ); const { createWarningNotice, removeNotice } = 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 ); if ( typeof window !== 'undefined' && window.__experimentalTemplateActivate ) { // Clear any notices dependent on the post context. removeNotice( 'template-activate-notice' ); } return () => setEditedPost( null, null ); }, [ post.type, post.id, setEditedPost, removeNotice ] ); // 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(); // Lock post saving when media uploads are in progress (experimental feature). useUploadSaveLock(); if ( ! isReady || ! mode ) { return null; } const isAttachment = post.type === ATTACHMENT_POST_TYPE && window?.__experimentalMediaEditor; // Early return for attachments - no block editor needed if ( isAttachment ) { return ( <EntityProvider kind="root" type="site"> <EntityProvider kind="postType" type={ post.type } id={ post.id } > { children } { ! settings.isPreviewMode && ( <> <EditorKeyboardShortcuts /> <KeyboardShortcutHelpModal /> </> ) } </EntityProvider> </EntityProvider> ); } return ( <EntityProvider kind="root" type="site"> <EntityProvider kind="postType" type={ post.type } id={ post.id } revisionId={ currentRevisionId ?? undefined } > <BlockContextProvider value={ defaultBlockContext }> <BlockEditorProviderComponent value={ blocks } onChange={ onChange } onInput={ onInput } selection={ isInRevisionsMode ? undefined : selection } onChangeSelection={ isInRevisionsMode ? noop : onChangeSelection } settings={ blockEditorSettings } useSubRegistry={ false } > { children } { ! settings.isPreviewMode && ( <> <PatternsMenuItems /> <TemplatePartMenuItems /> { mode === 'template-locked' && ( <DisableNonPageContentBlocks /> ) } { type === 'wp_navigation' && ( <NavigationBlockEditingMode /> ) } <EditorKeyboardShortcuts /> <KeyboardShortcutHelpModal /> <BlockRemovalWarnings /> <StartPageOptions /> <StartTemplateOptions /> <PatternRenameModal /> <PatternDuplicateModal /> </> ) } </BlockEditorProviderComponent> </BlockContextProvider> </EntityProvider> </EntityProvider> ); } ); /** * 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 ( <ExperimentalEditorProvider { ...props } BlockEditorProviderComponent={ BlockEditorProvider } > { props.children } </ExperimentalEditorProvider> ); } export default EditorProvider;