UNPKG

@wordpress/editor

Version:
536 lines (497 loc) 16.5 kB
/** * 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;