UNPKG

@wordpress/editor

Version:
358 lines (350 loc) 15 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, useResizeObserver } 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 } from '../../store/constants'; import { jsx as _jsx } from "react/jsx-runtime"; import { Fragment as _Fragment } from "react/jsx-runtime"; import { jsxs as _jsxs } from "react/jsx-runtime"; 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. */ const DESIGN_POST_TYPES = [PATTERN_POST_TYPE, TEMPLATE_POST_TYPE, NAVIGATION_POST_TYPE, TEMPLATE_PART_POST_TYPE]; /** * 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, styles, disableIframe = false, iframeProps, contentRef, className }) { const [resizeObserver, sizes] = useResizeObserver(); const isMobileViewport = useViewportMatch('small', '<'); const isTabletViewport = useViewportMatch('medium', '<'); const { renderingMode, postContentAttributes, editedPostTemplate = {}, wrapperBlockName, wrapperUniqueId, deviceType, isFocusedEntity, isDesignPostType, postType, isPreview } = useSelect(select => { const { getCurrentPostId, getCurrentPostType, getCurrentTemplateId, getEditorSettings, getRenderingMode, getDeviceType } = 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.__unstableIsPreviewMode }; }, []); const { isCleanNewPost } = useSelect(editorStore); const { hasRootPaddingAwareAlignments, themeHasDisabledLayoutStyles, themeSupportsLayout, isZoomOutMode } = useSelect(select => { const { getSettings, __unstableGetEditorMode } = select(blockEditorStore); const _settings = getSettings(); return { themeHasDisabledLayoutStyles: _settings.disableLayoutStyles, themeSupportsLayout: _settings.supportsLayout, hasRootPaddingAwareAlignments: _settings.__experimentalFeatures?.useRootPaddingAwareAlignments, isZoomOutMode: __unstableGetEditorMode() === 'zoom-out' }; }, []); 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 localRef = useRef(); const typewriterRef = useTypewriter(); contentRef = useMergeRefs([localRef, contentRef, renderingMode === 'post-only' ? typewriterRef : null, useFlashEditableBlocks({ isEnabled: renderingMode === 'template-locked' }), useSelectNearestEditableBlock({ isEnabled: renderingMode === 'template-locked' })]); const zoomOutProps = isZoomOutMode && !isTabletViewport ? { scale: 'default', frameSize: '48px' } : {}; const forceFullHeight = postType === NAVIGATION_POST_TYPE; 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 && // Dsiable resizing in zoomed-out mode. !isZoomOutMode; const shouldIframe = !disableIframe || ['Tablet', 'Mobile'].includes(deviceType); const iframeStyles = useMemo(() => { return [...(styles !== null && styles !== void 0 ? styles : []), { css: `.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 ? 'min-height:0!important;' : ''}}` }]; }, [styles, enableResizing]); return /*#__PURE__*/_jsx("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': shouldIframe }), children: /*#__PURE__*/_jsx(ResizableEditor, { enableResizing: enableResizing, height: sizes.height && !forceFullHeight ? sizes.height : '100%', children: /*#__PURE__*/_jsxs(BlockCanvas, { shouldIframe: shouldIframe, contentRef: contentRef, styles: iframeStyles, height: "100%", iframeProps: { ...iframeProps, ...zoomOutProps, style: { ...iframeProps?.style, ...deviceStyles } }, children: [themeSupportsLayout && !themeHasDisabledLayoutStyles && renderingMode === 'post-only' && !isDesignPostType && /*#__PURE__*/_jsxs(_Fragment, { children: [/*#__PURE__*/_jsx(LayoutStyle, { selector: ".editor-visual-editor__post-title-wrapper", layout: fallbackLayout }), /*#__PURE__*/_jsx(LayoutStyle, { selector: ".block-editor-block-list__layout.is-root-container", layout: postEditorLayout }), align && /*#__PURE__*/_jsx(LayoutStyle, { css: alignCSS }), postContentLayoutStyles && /*#__PURE__*/_jsx(LayoutStyle, { layout: postContentLayout, css: postContentLayoutStyles })] }), renderingMode === 'post-only' && !isDesignPostType && /*#__PURE__*/_jsx("div", { className: clsx('editor-visual-editor__post-title-wrapper', // The following class is only here for backward comapatibility // 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' }, children: /*#__PURE__*/_jsx(PostTitle, { ref: titleRef }) }), /*#__PURE__*/_jsxs(RecursionProvider, { blockName: wrapperBlockName, uniqueId: wrapperUniqueId, children: [/*#__PURE__*/_jsx(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. ), 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' && /*#__PURE__*/_jsx(EditTemplateBlocksNotification, { contentRef: localRef })] }), // Avoid resize listeners when not needed, // these will trigger unnecessary re-renders // when animating the iframe width. enableResizing && resizeObserver] }) }) }); } export default VisualEditor; //# sourceMappingURL=index.js.map