UNPKG

@wordpress/editor

Version:
454 lines (453 loc) 18.3 kB
// packages/editor/src/components/visual-editor/index.js import clsx from "clsx"; import { BlockList, store as blockEditorStore, __unstableUseTypewriter as useTypewriter, useSettings, RecursionProvider, privateApis as blockEditorPrivateApis } from "@wordpress/block-editor"; import { useEffect, useRef, useMemo, useState } from "@wordpress/element"; import { useSelect } from "@wordpress/data"; import { parse } from "@wordpress/blocks"; import { store as coreStore } from "@wordpress/core-data"; import { useMergeRefs, useResizeObserver, useViewportMatch } from "@wordpress/compose"; import PostTitle from "../post-title/index.mjs"; import { store as editorStore } from "../../store/index.mjs"; import { unlock } from "../../lock-unlock.mjs"; import EditTemplateBlocksNotification from "./edit-template-blocks-notification.mjs"; import ResizableEditor from "../resizable-editor/index.mjs"; import useSelectNearestEditableBlock from "./use-select-nearest-editable-block.mjs"; import { NAVIGATION_POST_TYPE, PATTERN_POST_TYPE, TEMPLATE_PART_POST_TYPE, TEMPLATE_POST_TYPE, DESIGN_POST_TYPES } from "../../store/constants.mjs"; import { useZoomOutModeExit } from "./use-zoom-out-mode-exit.mjs"; import { usePaddingAppender } from "./use-padding-appender.mjs"; import { useEditContentOnlySectionExit } from "./use-edit-content-only-section-exit.mjs"; import { SyncConnectionErrorModal } from "../sync-connection-error-modal/index.mjs"; import { Fragment, jsx, jsxs } from "react/jsx-runtime"; var { LayoutStyle, useLayoutClasses, useLayoutStyles, ExperimentalBlockCanvas: BlockCanvas, useFlashEditableBlocks } = unlock(blockEditorPrivateApis); 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; } var CANVAS_MIN_WIDTH = 300; var CANVAS_TARGET_ASPECT_RATIO = 9 / 16; function getCanvasHeight(width, containerSize) { const lerp = (a, b, amount) => a + (b - a) * amount; const lerpFactor = 1 - Math.max( 0, Math.min( 1, (width - CANVAS_MIN_WIDTH) / (containerSize.width - CANVAS_MIN_WIDTH) ) ); const aspectRatio = lerp( containerSize.width / containerSize.height, CANVAS_TARGET_ASPECT_RATIO, lerpFactor ); return Math.min(Math.round(width / aspectRatio), containerSize.height); } function VisualEditor({ // Ideally as we unify post and site editors, we won't need these props. autoFocus, iframeProps, contentRef, className }) { const isMobileViewport = useViewportMatch("small", "<"); const { renderingMode, postContentAttributes, editedPostTemplate = {}, wrapperBlockName, wrapperUniqueId, deviceType, isFocusedEntity, isDesignPostType, postType, isPreview, styles, hasCanvasWidth, canvasWidth } = useSelect((select) => { const { getCurrentPostId, getCurrentPostType, getCurrentTemplateId, getEditorSettings, getRenderingMode, getDeviceType, getCanvasWidth } = 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 _canvasWidth = getCanvasWidth(); const template = currentTemplateId ? getEditedEntityRecord( "postType", TEMPLATE_POST_TYPE, currentTemplateId ) : void 0; 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 : void 0, wrapperBlockName: _wrapperBlockName, wrapperUniqueId: getCurrentPostId(), deviceType: getDeviceType(), isFocusedEntity: !!editorSettings.onNavigateToPreviousEntityRecord, postType: postTypeSlug, isPreview: editorSettings.isPreviewMode, styles: editorSettings.styles, hasCanvasWidth: _canvasWidth !== void 0, canvasWidth: _canvasWidth }; }, []); 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 [globalLayoutSettings] = useSettings("layout"); const [containerSize, setContainerSize] = useState(); const containerRef = useResizeObserver((entries) => { const { width, height } = entries[0].contentRect; setContainerSize( (size) => size?.width === width && size?.height === height ? size : { width, height } ); }); const fallbackLayout = useMemo(() => { if (renderingMode !== "post-only" || isDesignPostType) { return { type: "default" }; } if (themeSupportsLayout) { return { ...globalLayoutSettings, type: "constrained" }; } return { type: "default" }; }, [ renderingMode, themeSupportsLayout, globalLayoutSettings, isDesignPostType ]); const newestPostContentAttributes = useMemo(() => { if (!editedPostTemplate?.content && !editedPostTemplate?.blocks && postContentAttributes) { return postContentAttributes; } if (editedPostTemplate?.blocks) { return getPostContentAttributes(editedPostTemplate?.blocks); } 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; } if (editedPostTemplate?.blocks) { return checkForPostContentAtRootLevel(editedPostTemplate?.blocks); } 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" ); 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 ]); const blockListLayout = postContentAttributes ? postContentLayout : fallbackLayout; const postEditorLayout = blockListLayout?.type === "default" && !hasPostContentAtRootLevel ? fallbackLayout : blockListLayout; const titleRef = useRef(); useEffect(() => { if (!autoFocus || !isCleanNewPost()) { return; } titleRef?.current?.focus(); }, [autoFocus, isCleanNewPost]); 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 isResizablePostType = [ NAVIGATION_POST_TYPE, TEMPLATE_PART_POST_TYPE, PATTERN_POST_TYPE ].includes(postType); const enableResizing = isResizablePostType && // Disable in previews / view mode. !isPreview && // Disable resizing in mobile viewport. !isMobileViewport && // Disable resizing in zoomed-out mode. !isZoomedOut || // When the canvas has an explicit width, always allow resizing. hasCanvasWidth; const isNavigationPreview = postType === NAVIGATION_POST_TYPE && isPreview; const [paddingAppenderRef, paddingStyle] = usePaddingAppender( !isPreview && renderingMode === "post-only" && !isDesignPostType ); const shouldConstrainCanvasHeight = enableResizing && canvasWidth && containerSize?.height > 0 && containerSize?.width > CANVAS_MIN_WIDTH; const canvasHeight = shouldConstrainCanvasHeight ? getCanvasHeight(canvasWidth, containerSize) : "100%"; const centerContentCSS = `display:flex;align-items:center;justify-content:center;`; const iframeBodyMinHeightCSS = hasCanvasWidth && !isResizablePostType ? "min-height:100vh;" : ""; 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;}.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%;${iframeBodyMinHeightCSS}}` : ""}${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, centerContentCSS, iframeBodyMinHeightCSS, isNavigationPreview, 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 /* @__PURE__ */ jsxs( "div", { ref: containerRef, className: clsx( "editor-visual-editor", // this class is here for backward compatibility reasons. "edit-post-visual-editor", className, { // Vertical padding frames a width-constrained canvas // (device preview or after a resize) as a centered preview. "has-vertical-padding": isFocusedEntity || hasCanvasWidth, // Horizontal padding leaves room for the resize handles // that appear on the left/right of a resizable canvas. "has-horizontal-padding": isFocusedEntity || enableResizing } ), children: [ /* @__PURE__ */ jsx(SyncConnectionErrorModal, {}), /* @__PURE__ */ jsx( ResizableEditor, { enableResizing, width: enableResizing && canvasWidth ? canvasWidth + "px" : "100%", height: canvasHeight, children: /* @__PURE__ */ jsxs( BlockCanvas, { shouldIframe: true, contentRef, styles: iframeStyles, height: "100%", iframeProps: { ...iframeProps, style: iframeProps?.style }, 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 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, style: { // This is using inline styles so it's applied // within the editor iframe. 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. { "has-global-padding": renderingMode === "post-only" && !isDesignPostType && hasRootPaddingAwareAlignments } ), layout: blockListLayout, dropZoneElement: ( // Pass in the html element of the iframe to // ensure the drop zone extends to the edges of // the iframe. 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 } ) ] } ) ] } ) } ) ] } ); } var visual_editor_default = VisualEditor; export { visual_editor_default as default, getCanvasHeight }; //# sourceMappingURL=index.mjs.map