@wordpress/editor
Version:
Enhanced block editor for WordPress posts.
536 lines (497 loc) • 16.5 kB
JavaScript
/**
* External dependencies
*/
import clsx from 'clsx';
/**
* WordPress dependencies
*/
import {
BlockList,
store as blockEditorStore,
__unstableUseTypewriter as useTypewriter,
__unstableUseTypingObserver as useTypingObserver,
useSettings,
RecursionProvider,
privateApis as blockEditorPrivateApis,
__experimentalUseResizeCanvas as useResizeCanvas,
} from '@wordpress/block-editor';
import { useEffect, useRef, useMemo } from '@wordpress/element';
import { useSelect } from '@wordpress/data';
import { parse } from '@wordpress/blocks';
import { store as coreStore } from '@wordpress/core-data';
import { useMergeRefs, useViewportMatch } from '@wordpress/compose';
/**
* Internal dependencies
*/
import PostTitle from '../post-title';
import { store as editorStore } from '../../store';
import { unlock } from '../../lock-unlock';
import EditTemplateBlocksNotification from './edit-template-blocks-notification';
import ResizableEditor from '../resizable-editor';
import useSelectNearestEditableBlock from './use-select-nearest-editable-block';
import {
NAVIGATION_POST_TYPE,
PATTERN_POST_TYPE,
TEMPLATE_PART_POST_TYPE,
TEMPLATE_POST_TYPE,
DESIGN_POST_TYPES,
} from '../../store/constants';
import { useZoomOutModeExit } from './use-zoom-out-mode-exit';
import { usePaddingAppender } from './use-padding-appender';
import { useEditContentOnlySectionExit } from './use-edit-content-only-section-exit';
import { SyncConnectionErrorModal } from '../sync-connection-error-modal';
const {
LayoutStyle,
useLayoutClasses,
useLayoutStyles,
ExperimentalBlockCanvas: BlockCanvas,
useFlashEditableBlocks,
} = unlock( blockEditorPrivateApis );
/**
* These post types have a special editor where they don't allow you to fill the title
* and they don't apply the layout styles.
*/
/**
* Given an array of nested blocks, find the first Post Content
* block inside it, recursing through any nesting levels,
* and return its attributes.
*
* @param {Array} blocks A list of blocks.
*
* @return {Object | undefined} The Post Content block.
*/
function getPostContentAttributes( blocks ) {
for ( let i = 0; i < blocks.length; i++ ) {
if ( blocks[ i ].name === 'core/post-content' ) {
return blocks[ i ].attributes;
}
if ( blocks[ i ].innerBlocks.length ) {
const nestedPostContent = getPostContentAttributes(
blocks[ i ].innerBlocks
);
if ( nestedPostContent ) {
return nestedPostContent;
}
}
}
}
function checkForPostContentAtRootLevel( blocks ) {
for ( let i = 0; i < blocks.length; i++ ) {
if ( blocks[ i ].name === 'core/post-content' ) {
return true;
}
}
return false;
}
function VisualEditor( {
// Ideally as we unify post and site editors, we won't need these props.
autoFocus,
disableIframe = false,
iframeProps,
contentRef,
className,
} ) {
const isMobileViewport = useViewportMatch( 'small', '<' );
const {
renderingMode,
postContentAttributes,
editedPostTemplate = {},
wrapperBlockName,
wrapperUniqueId,
deviceType,
isFocusedEntity,
isDesignPostType,
postType,
isPreview,
styles,
canvasMinHeight,
} = useSelect( ( select ) => {
const {
getCurrentPostId,
getCurrentPostType,
getCurrentTemplateId,
getEditorSettings,
getRenderingMode,
getDeviceType,
getCanvasMinHeight,
} = unlock( select( editorStore ) );
const { getPostType, getEditedEntityRecord } = select( coreStore );
const postTypeSlug = getCurrentPostType();
const _renderingMode = getRenderingMode();
let _wrapperBlockName;
if ( postTypeSlug === PATTERN_POST_TYPE ) {
_wrapperBlockName = 'core/block';
} else if ( _renderingMode === 'post-only' ) {
_wrapperBlockName = 'core/post-content';
}
const editorSettings = getEditorSettings();
const supportsTemplateMode = editorSettings.supportsTemplateMode;
const postTypeObject = getPostType( postTypeSlug );
const currentTemplateId = getCurrentTemplateId();
const template = currentTemplateId
? getEditedEntityRecord(
'postType',
TEMPLATE_POST_TYPE,
currentTemplateId
)
: undefined;
return {
renderingMode: _renderingMode,
postContentAttributes: editorSettings.postContentAttributes,
isDesignPostType: DESIGN_POST_TYPES.includes( postTypeSlug ),
// Post template fetch returns a 404 on classic themes, which
// messes with e2e tests, so check it's a block theme first.
editedPostTemplate:
postTypeObject?.viewable && supportsTemplateMode
? template
: undefined,
wrapperBlockName: _wrapperBlockName,
wrapperUniqueId: getCurrentPostId(),
deviceType: getDeviceType(),
isFocusedEntity: !! editorSettings.onNavigateToPreviousEntityRecord,
postType: postTypeSlug,
isPreview: editorSettings.isPreviewMode,
styles: editorSettings.styles,
canvasMinHeight: getCanvasMinHeight(),
};
}, [] );
const { isCleanNewPost } = useSelect( editorStore );
const {
hasRootPaddingAwareAlignments,
themeHasDisabledLayoutStyles,
themeSupportsLayout,
isZoomedOut,
} = useSelect( ( select ) => {
const { getSettings, isZoomOut: _isZoomOut } = unlock(
select( blockEditorStore )
);
const _settings = getSettings();
return {
themeHasDisabledLayoutStyles: _settings.disableLayoutStyles,
themeSupportsLayout: _settings.supportsLayout,
hasRootPaddingAwareAlignments:
_settings.__experimentalFeatures?.useRootPaddingAwareAlignments,
isZoomedOut: _isZoomOut(),
};
}, [] );
const localRef = useRef();
const deviceStyles = useResizeCanvas( deviceType );
const [ globalLayoutSettings ] = useSettings( 'layout' );
// fallbackLayout is used if there is no Post Content,
// and for Post Title.
const fallbackLayout = useMemo( () => {
if ( renderingMode !== 'post-only' || isDesignPostType ) {
return { type: 'default' };
}
if ( themeSupportsLayout ) {
// We need to ensure support for wide and full alignments,
// so we add the constrained type.
return { ...globalLayoutSettings, type: 'constrained' };
}
// Set default layout for classic themes so all alignments are supported.
return { type: 'default' };
}, [
renderingMode,
themeSupportsLayout,
globalLayoutSettings,
isDesignPostType,
] );
const newestPostContentAttributes = useMemo( () => {
if (
! editedPostTemplate?.content &&
! editedPostTemplate?.blocks &&
postContentAttributes
) {
return postContentAttributes;
}
// When in template editing mode, we can access the blocks directly.
if ( editedPostTemplate?.blocks ) {
return getPostContentAttributes( editedPostTemplate?.blocks );
}
// If there are no blocks, we have to parse the content string.
// Best double-check it's a string otherwise the parse function gets unhappy.
const parseableContent =
typeof editedPostTemplate?.content === 'string'
? editedPostTemplate?.content
: '';
return getPostContentAttributes( parse( parseableContent ) ) || {};
}, [
editedPostTemplate?.content,
editedPostTemplate?.blocks,
postContentAttributes,
] );
const hasPostContentAtRootLevel = useMemo( () => {
if ( ! editedPostTemplate?.content && ! editedPostTemplate?.blocks ) {
return false;
}
// When in template editing mode, we can access the blocks directly.
if ( editedPostTemplate?.blocks ) {
return checkForPostContentAtRootLevel( editedPostTemplate?.blocks );
}
// If there are no blocks, we have to parse the content string.
// Best double-check it's a string otherwise the parse function gets unhappy.
const parseableContent =
typeof editedPostTemplate?.content === 'string'
? editedPostTemplate?.content
: '';
return (
checkForPostContentAtRootLevel( parse( parseableContent ) ) || false
);
}, [ editedPostTemplate?.content, editedPostTemplate?.blocks ] );
const { layout = {}, align = '' } = newestPostContentAttributes || {};
const postContentLayoutClasses = useLayoutClasses(
newestPostContentAttributes,
'core/post-content'
);
const blockListLayoutClass = clsx(
{
'is-layout-flow': ! themeSupportsLayout,
},
themeSupportsLayout && postContentLayoutClasses,
align && `align${ align }`
);
const postContentLayoutStyles = useLayoutStyles(
newestPostContentAttributes,
'core/post-content',
'.block-editor-block-list__layout.is-root-container'
);
// Update type for blocks using legacy layouts.
const postContentLayout = useMemo( () => {
return layout &&
( layout?.type === 'constrained' ||
layout?.inherit ||
layout?.contentSize ||
layout?.wideSize )
? { ...globalLayoutSettings, ...layout, type: 'constrained' }
: { ...globalLayoutSettings, ...layout, type: 'default' };
}, [
layout?.type,
layout?.inherit,
layout?.contentSize,
layout?.wideSize,
globalLayoutSettings,
] );
// If there is a Post Content block we use its layout for the block list;
// if not, this must be a classic theme, in which case we use the fallback layout.
const blockListLayout = postContentAttributes
? postContentLayout
: fallbackLayout;
const postEditorLayout =
blockListLayout?.type === 'default' && ! hasPostContentAtRootLevel
? fallbackLayout
: blockListLayout;
const observeTypingRef = useTypingObserver();
const titleRef = useRef();
useEffect( () => {
if ( ! autoFocus || ! isCleanNewPost() ) {
return;
}
titleRef?.current?.focus();
}, [ autoFocus, isCleanNewPost ] );
// Add some styles for alignwide/alignfull Post Content and its children.
const alignCSS = `.is-root-container.alignwide { max-width: var(--wp--style--global--wide-size); margin-left: auto; margin-right: auto;}
.is-root-container.alignwide:where(.is-layout-flow) > :not(.alignleft):not(.alignright) { max-width: var(--wp--style--global--wide-size);}
.is-root-container.alignfull { max-width: none; margin-left: auto; margin-right: auto;}
.is-root-container.alignfull:where(.is-layout-flow) > :not(.alignleft):not(.alignright) { max-width: none;}`;
const enableResizing =
[
NAVIGATION_POST_TYPE,
TEMPLATE_PART_POST_TYPE,
PATTERN_POST_TYPE,
].includes( postType ) &&
// Disable in previews / view mode.
! isPreview &&
// Disable resizing in mobile viewport.
! isMobileViewport &&
// Disable resizing in zoomed-out mode.
! isZoomedOut;
const isNavigationPreview = postType === NAVIGATION_POST_TYPE && isPreview;
// Calculate the minimum height including scroll offset to fit all notes.
const calculatedMinHeight = useMemo( () => {
if ( ! localRef.current ) {
return canvasMinHeight;
}
const { ownerDocument } = localRef.current;
const scrollTop =
ownerDocument.documentElement.scrollTop ||
ownerDocument.body.scrollTop;
return canvasMinHeight + scrollTop;
}, [ canvasMinHeight ] );
const [ paddingAppenderRef, paddingStyle ] = usePaddingAppender(
! isPreview && renderingMode === 'post-only' && ! isDesignPostType
);
const centerContentCSS = `display:flex;align-items:center;justify-content:center;`;
const iframeStyles = useMemo( () => {
return [
...( styles ?? [] ),
{
// Ensures margins of children are contained so that the body background paints behind them.
// Otherwise, the background of html (when zoomed out) would show there and appear broken. It's
// important mostly for post-only views yet conceivably an issue in templated views too.
css: `:where(.block-editor-iframe__body){display:flow-root;${
calculatedMinHeight
? `min-height:${ calculatedMinHeight }px;`
: ''
}}.is-root-container{display:flow-root;${
// Some themes will have `min-height: 100vh` for the root container,
// which isn't a requirement in auto resize mode.
enableResizing || isNavigationPreview
? 'min-height:0!important;'
: ''
}}
${ paddingStyle ? paddingStyle : '' }
${
enableResizing
? `.block-editor-iframe__html{background:var(--wp-editor-canvas-background);min-height:100vh;${ centerContentCSS }}.block-editor-iframe__body{width:100%;}`
: ''
}${
isNavigationPreview
? `.block-editor-iframe__body{${ centerContentCSS }padding:var(--wp--style--block-gap,2em);}`
: ''
}`,
// The CSS for enableResizing centers the body content vertically when resizing is enabled and applies a background
// color to the iframe HTML element to match the background color of the editor canvas.
// The CSS for isNavigationPreview centers the body content vertically and horizontally when the navigation is in preview mode.
},
];
}, [
styles,
enableResizing,
isNavigationPreview,
calculatedMinHeight,
paddingStyle,
] );
const typewriterRef = useTypewriter();
contentRef = useMergeRefs( [
localRef,
contentRef,
renderingMode === 'post-only' ? typewriterRef : null,
useFlashEditableBlocks( {
isEnabled: renderingMode === 'template-locked',
} ),
useSelectNearestEditableBlock( {
isEnabled: renderingMode === 'template-locked',
} ),
useZoomOutModeExit(),
paddingAppenderRef,
useEditContentOnlySectionExit(),
] );
return (
<div
className={ clsx(
'editor-visual-editor',
// this class is here for backward compatibility reasons.
'edit-post-visual-editor',
className,
{
'has-padding': isFocusedEntity || enableResizing,
'is-resizable': enableResizing,
'is-iframed': ! disableIframe,
}
) }
>
<SyncConnectionErrorModal />
<ResizableEditor enableResizing={ enableResizing } height="100%">
<BlockCanvas
shouldIframe={ ! disableIframe }
contentRef={ contentRef }
styles={ iframeStyles }
height="100%"
iframeProps={ {
...iframeProps,
style: {
...iframeProps?.style,
...deviceStyles,
},
} }
>
{ themeSupportsLayout &&
! themeHasDisabledLayoutStyles &&
renderingMode === 'post-only' &&
! isDesignPostType && (
<>
<LayoutStyle
selector=".editor-visual-editor__post-title-wrapper"
layout={ fallbackLayout }
/>
<LayoutStyle
selector=".block-editor-block-list__layout.is-root-container"
layout={ postEditorLayout }
/>
{ align && <LayoutStyle css={ alignCSS } /> }
{ postContentLayoutStyles && (
<LayoutStyle
layout={ postContentLayout }
css={ postContentLayoutStyles }
/>
) }
</>
) }
{ renderingMode === 'post-only' && ! isDesignPostType && (
<div
className={ clsx(
'editor-visual-editor__post-title-wrapper',
// The following class is only here for backward compatibility
// some themes might be using it to style the post title.
'edit-post-visual-editor__post-title-wrapper',
{
'has-global-padding':
hasRootPaddingAwareAlignments,
}
) }
contentEditable={ false }
ref={ observeTypingRef }
style={ {
// This is using inline styles
// so it's applied for both iframed and non iframed editors.
marginTop: '4rem',
} }
>
<PostTitle ref={ titleRef } />
</div>
) }
<RecursionProvider
blockName={ wrapperBlockName }
uniqueId={ wrapperUniqueId }
>
<BlockList
className={ clsx(
'is-' + deviceType.toLowerCase() + '-preview',
renderingMode !== 'post-only' ||
isDesignPostType
? 'wp-site-blocks'
: `${ blockListLayoutClass } wp-block-post-content`, // Ensure root level blocks receive default/flow blockGap styling rules.
{
'has-global-padding':
renderingMode === 'post-only' &&
! isDesignPostType &&
hasRootPaddingAwareAlignments,
}
) }
layout={ blockListLayout }
dropZoneElement={
// When iframed, pass in the html element of the iframe to
// ensure the drop zone extends to the edges of the iframe.
disableIframe
? localRef.current
: localRef.current?.parentNode
}
__unstableDisableDropZone={
// In template preview mode, disable drop zones at the root of the template.
renderingMode === 'template-locked'
? true
: false
}
/>
{ renderingMode === 'template-locked' && (
<EditTemplateBlocksNotification
contentRef={ localRef }
/>
) }
</RecursionProvider>
</BlockCanvas>
</ResizableEditor>
</div>
);
}
export default VisualEditor;