From 03dd90b98c8a72e2af3baa8fc436ff7d4f4c7449 Mon Sep 17 00:00:00 2001 From: Chris Villa Date: Sun, 24 Mar 2024 20:52:39 +0100 Subject: [PATCH] feat: add API to opt-out of iframes --- .../app/custom-ui/[...puckPath]/client.tsx | 1 + apps/docs/components/Preview/index.tsx | 2 +- .../docs/api-reference/components/puck.mdx | 28 ++++++++++ packages/core/components/LayerTree/index.tsx | 15 ++---- .../Puck/components/Canvas/index.tsx | 15 +++--- .../Puck/components/Canvas/styles.module.css | 2 +- .../Puck/components/Preview/index.tsx | 38 ++++++++------ .../Puck/components/Preview/styles.module.css | 2 +- packages/core/components/Puck/context.tsx | 3 ++ packages/core/components/Puck/index.tsx | 52 +++++++++++-------- packages/core/lib/get-frame.ts | 11 ++++ packages/core/lib/use-frame.ts | 12 +++++ packages/core/lib/use-placeholder-style.ts | 12 ++--- packages/core/types/IframeConfig.tsx | 3 ++ .../src/HeadingAnalyzer.tsx | 43 +++++---------- 15 files changed, 145 insertions(+), 94 deletions(-) create mode 100644 packages/core/lib/get-frame.ts create mode 100644 packages/core/lib/use-frame.ts create mode 100644 packages/core/types/IframeConfig.tsx diff --git a/apps/demo/app/custom-ui/[...puckPath]/client.tsx b/apps/demo/app/custom-ui/[...puckPath]/client.tsx index f8cf423e29..023e014fe8 100644 --- a/apps/demo/app/custom-ui/[...puckPath]/client.tsx +++ b/apps/demo/app/custom-ui/[...puckPath]/client.tsx @@ -291,6 +291,7 @@ export function Client({ path, isEdit }: { path: string; isEdit: boolean }) { config={config} data={data} + iframe={{ enabled: false }} headerPath={path} overrides={{ outline: ({ children }) => ( diff --git a/apps/docs/components/Preview/index.tsx b/apps/docs/components/Preview/index.tsx index 4738369754..e6c202eb84 100644 --- a/apps/docs/components/Preview/index.tsx +++ b/apps/docs/components/Preview/index.tsx @@ -61,7 +61,7 @@ export const PuckPreview = ({ style?: CSSProperties; }) => { return ( - + {children} diff --git a/apps/docs/pages/docs/api-reference/components/puck.mdx b/apps/docs/pages/docs/api-reference/components/puck.mdx index a9ebaee085..0c38512a02 100644 --- a/apps/docs/pages/docs/api-reference/components/puck.mdx +++ b/apps/docs/pages/docs/api-reference/components/puck.mdx @@ -32,6 +32,7 @@ export function Editor() { | [`children`](#children) | `children: ` | ReactNode | - | | [`headerPath`](#headerpath) | `headerPath: "/my-page"` | String | - | | [`headerTitle`](#headertitle) | `headerTitle: "My Page"` | String | - | +| [`iframe`](#iframe) | `iframe: {}` | [IframeConfig](#iframe-params) | - | | [`onChange()`](#onchangedata) | `onChange: (data) => {}` | Function | - | | [`onPublish()`](#onpublishdata) | `onPublish: async (data) => {}` | Function | - | | [`overrides`](#overrides) | `overrides: { header: () =>
}` | [Overrides](/docs/api-reference/overrides) | Experimental | @@ -138,6 +139,33 @@ export function Editor() { } ``` +### `iframe` + +Configure the iframe behaviour. + +```tsx {4} copy +export function Editor() { + return ( + + ); +} +``` + +#### iframe params + +| Param | Example | Type | Status | +| --------------------- | ---------------- | ------- | ------ | +| [`enabled`](#enabled) | `enabled: false` | boolean | - | + +##### `enabled` + +Render the Puck preview within iframe. Defaults to `true`. + +Disabling iframes will also disable [viewports](#viewports). + ### `onChange(data)` Callback that triggers when the user makes a change. diff --git a/packages/core/components/LayerTree/index.tsx b/packages/core/components/LayerTree/index.tsx index ea8981833d..8616a1db75 100644 --- a/packages/core/components/LayerTree/index.tsx +++ b/packages/core/components/LayerTree/index.tsx @@ -10,6 +10,7 @@ import { dropZoneContext } from "../DropZone/context"; import { findZonesForArea } from "../../lib/find-zones-for-area"; import { getZoneId } from "../../lib/get-zone-id"; import { isChildOfZone } from "../../lib/is-child-of-zone"; +import { useFrame } from "../../lib/use-frame"; const getClassName = getClassNameFactory("LayerTree", styles); const getClassNameLayer = getClassNameFactory("Layer", styles); @@ -33,6 +34,8 @@ export const LayerTree = ({ }) => { const zones = data.zones || {}; const ctx = useContext(dropZoneContext); + const frame = useFrame(); + return ( <> {label && ( @@ -95,18 +98,8 @@ export const LayerTree = ({ const id = zoneContent[i].props.id; - const iframe = document.querySelector("#preview-iframe") as - | HTMLIFrameElement - | undefined; - - if (!iframe?.contentDocument) { - throw new Error( - `Preview iframe could not be found when trying to scroll to item ${id}` - ); - } - scrollIntoView( - iframe.contentDocument.querySelector( + frame?.querySelector( `[data-rfd-drag-handle-draggable-id="draggable-${id}"]` ) as HTMLElement ); diff --git a/packages/core/components/Puck/components/Canvas/index.tsx b/packages/core/components/Puck/components/Canvas/index.tsx index 450eb8e2a8..bdb652002f 100644 --- a/packages/core/components/Puck/components/Canvas/index.tsx +++ b/packages/core/components/Puck/components/Canvas/index.tsx @@ -20,7 +20,7 @@ const getClassName = getClassNameFactory("PuckCanvas", styles); const ZOOM_ON_CHANGE = true; export const Canvas = () => { - const { status } = useAppContext(); + const { status, iframe } = useAppContext(); const { dispatch, state, overrides, setUi, zoomConfig, setZoomConfig } = useAppContext(); const { ui } = state; @@ -102,7 +102,9 @@ export const Canvas = () => { return (
dispatch({ type: "setUi", @@ -111,7 +113,7 @@ export const Canvas = () => { }) } > - {ui.viewports.controlsVisible && ( + {ui.viewports.controlsVisible && iframe.enabled && (
{ />
)} -
+
diff --git a/packages/core/components/Puck/components/Canvas/styles.module.css b/packages/core/components/Puck/components/Canvas/styles.module.css index 485faa348a..d123762bad 100644 --- a/packages/core/components/Puck/components/Canvas/styles.module.css +++ b/packages/core/components/Puck/components/Canvas/styles.module.css @@ -18,7 +18,7 @@ } } -.PuckCanvas-frame { +.PuckCanvas-inner { box-sizing: border-box; display: flex; height: 100%; diff --git a/packages/core/components/Puck/components/Preview/index.tsx b/packages/core/components/Puck/components/Preview/index.tsx index eb9a521a77..66e7060770 100644 --- a/packages/core/components/Puck/components/Preview/index.tsx +++ b/packages/core/components/Puck/components/Preview/index.tsx @@ -1,6 +1,6 @@ import { DropZone } from "../../../DropZone"; import { rootDroppableId } from "../../../../lib/root-droppable-id"; -import { useCallback, useRef, useState } from "react"; +import { useCallback, useRef } from "react"; import { useAppContext } from "../../context"; import AutoFrame from "@measured/auto-frame-component"; import styles from "./styles.module.css"; @@ -9,7 +9,7 @@ import { getClassNameFactory } from "../../../../lib"; const getClassName = getClassNameFactory("PuckPreview", styles); export const Preview = ({ id = "puck-preview" }: { id?: string }) => { - const { config, dispatch, state, setStatus } = useAppContext(); + const { config, dispatch, state, setStatus, iframe } = useAppContext(); const Page = useCallback( (pageProps) => @@ -32,19 +32,27 @@ export const Preview = ({ id = "puck-preview" }: { id?: string }) => { dispatch({ type: "setUi", ui: { ...state.ui, itemSelector: null } }); }} > - { - setStatus("READY"); - }} - > - - - - + {iframe.enabled ? ( + { + setStatus("READY"); + }} + > + + + + + ) : ( +
+ + + +
+ )}
); }; diff --git a/packages/core/components/Puck/components/Preview/styles.module.css b/packages/core/components/Puck/components/Preview/styles.module.css index a119cc8567..42ad09f5d8 100644 --- a/packages/core/components/Puck/components/Preview/styles.module.css +++ b/packages/core/components/Puck/components/Preview/styles.module.css @@ -2,7 +2,7 @@ height: 100%; } -.PuckPreview-iframe { +.PuckPreview-frame { border: none; height: 100%; width: 100%; diff --git a/packages/core/components/Puck/context.tsx b/packages/core/components/Puck/context.tsx index c82ccd9f8b..d3c0c0d605 100644 --- a/packages/core/components/Puck/context.tsx +++ b/packages/core/components/Puck/context.tsx @@ -13,6 +13,7 @@ import { Overrides } from "../../types/Overrides"; import { PuckHistory } from "../../lib/use-puck-history"; import { defaultViewports } from "../ViewportControls/default-viewports"; import { Viewports } from "../../types/Viewports"; +import { IframeConfig } from "../../types/IframeConfig"; export const defaultAppState: AppState = { data: { content: [], root: { props: { title: "" } } }, @@ -58,6 +59,7 @@ type AppContext< setZoomConfig: (zoomConfig: ZoomConfig) => void; status: Status; setStatus: (status: Status) => void; + iframe: IframeConfig; }; const defaultContext: AppContext = { @@ -78,6 +80,7 @@ const defaultContext: AppContext = { setZoomConfig: () => null, status: "LOADING", setStatus: () => null, + iframe: {}, }; export const appContext = createContext(defaultContext); diff --git a/packages/core/components/Puck/index.tsx b/packages/core/components/Puck/index.tsx index 8913171f92..114493de9b 100644 --- a/packages/core/components/Puck/index.tsx +++ b/packages/core/components/Puck/index.tsx @@ -46,6 +46,7 @@ import { Canvas } from "./components/Canvas"; import { defaultViewports } from "../ViewportControls/default-viewports"; import { Viewports } from "../../types/Viewports"; import { DragDropContext } from "../DragDropContext"; +import { IframeConfig } from "../../types/IframeConfig"; const getClassName = getClassNameFactory("Puck", styles); @@ -65,6 +66,9 @@ export function Puck< headerTitle, headerPath, viewports = defaultViewports, + iframe = { + enabled: true, + }, }: { children?: ReactNode; config: UserConfig; @@ -86,6 +90,7 @@ export function Puck< headerTitle?: string; headerPath?: string; viewports?: Viewports; + iframe?: IframeConfig; }) { const historyStore = useHistoryStore(); @@ -110,29 +115,31 @@ export function Puck< const closestViewport = viewportDifferences[0].key; - clientUiState = { - // Hide side bars on mobile - ...(window.matchMedia("(min-width: 638px)").matches - ? {} - : { - leftSideBarVisible: false, - rightSideBarVisible: false, - }), - viewports: { - ...initial.viewports, - - current: { - ...initial.viewports.current, - height: - initialUi?.viewports?.current?.height || - viewports[closestViewport].height || - "auto", - width: - initialUi?.viewports?.current?.width || - viewports[closestViewport].width, + if (iframe.enabled) { + clientUiState = { + // Hide side bars on mobile + ...(window.matchMedia("(min-width: 638px)").matches + ? {} + : { + leftSideBarVisible: false, + rightSideBarVisible: false, + }), + viewports: { + ...initial.viewports, + + current: { + ...initial.viewports.current, + height: + initialUi?.viewports?.current?.height || + viewports[closestViewport].height || + "auto", + width: + initialUi?.viewports?.current?.width || + viewports[closestViewport].width, + }, }, - }, - }; + }; + } } return { @@ -351,6 +358,7 @@ export function Puck< overrides: loadedOverrides, history, viewports, + iframe, }} > { + let frame = document.querySelector("#preview-frame") as + | HTMLElement + | undefined; + + if (frame?.tagName === "IFRAME") { + frame = (frame as HTMLIFrameElement)!.contentDocument!.body; + } + + return frame; +}; diff --git a/packages/core/lib/use-frame.ts b/packages/core/lib/use-frame.ts new file mode 100644 index 0000000000..e6c89c3938 --- /dev/null +++ b/packages/core/lib/use-frame.ts @@ -0,0 +1,12 @@ +import { useEffect, useState } from "react"; +import { getFrame } from "./get-frame"; + +export const useFrame = () => { + const [el, setEl] = useState(); + + useEffect(() => { + setEl(getFrame()); + }, []); + + return el; +}; diff --git a/packages/core/lib/use-placeholder-style.ts b/packages/core/lib/use-placeholder-style.ts index a00fbae901..e0b4ceea2e 100644 --- a/packages/core/lib/use-placeholder-style.ts +++ b/packages/core/lib/use-placeholder-style.ts @@ -1,10 +1,12 @@ import { CSSProperties, useState } from "react"; import { DragStart, DragUpdate } from "@measured/dnd"; +import { useFrame } from "./use-frame"; export const usePlaceholderStyle = () => { const queryAttr = "data-rfd-drag-handle-draggable-id"; const [placeholderStyle, setPlaceholderStyle] = useState(); + const frame = useFrame(); const onDragStartOrUpdate = ( draggedItem: DragStart & Partial @@ -17,19 +19,13 @@ export const usePlaceholderStyle = () => { const domQuery = `[${queryAttr}='${draggableId}']`; - const iframe = document.querySelector(`#preview-iframe`) as - | HTMLIFrameElement - | undefined; - - const draggedDOM = - document.querySelector(domQuery) || - iframe?.contentWindow?.document.querySelector(domQuery); + const draggedDOM = frame?.ownerDocument.querySelector(domQuery); if (!draggedDOM) { return; } - const targetListElement = iframe?.contentWindow?.document.querySelector( + const targetListElement = frame?.ownerDocument.querySelector( `[data-rfd-droppable-id='${droppableId}']` ); diff --git a/packages/core/types/IframeConfig.tsx b/packages/core/types/IframeConfig.tsx new file mode 100644 index 0000000000..3aa7295564 --- /dev/null +++ b/packages/core/types/IframeConfig.tsx @@ -0,0 +1,3 @@ +export type IframeConfig = { + enabled?: boolean; +}; diff --git a/packages/plugin-heading-analyzer/src/HeadingAnalyzer.tsx b/packages/plugin-heading-analyzer/src/HeadingAnalyzer.tsx index 8b926e3739..4714b24cbc 100644 --- a/packages/plugin-heading-analyzer/src/HeadingAnalyzer.tsx +++ b/packages/plugin-heading-analyzer/src/HeadingAnalyzer.tsx @@ -6,6 +6,7 @@ import { SidebarSection } from "@/core/components/SidebarSection"; import { OutlineList } from "@/core/components/OutlineList"; import { scrollIntoView } from "@/core/lib/scroll-into-view"; +import { useFrame } from "@/core/lib/use-frame"; import ReactFromJSON from "react-from-json"; @@ -13,18 +14,9 @@ const dataAttr = "data-puck-heading-analyzer-id"; const getOutline = ({ addDataAttr = false, -}: { addDataAttr?: boolean } = {}) => { - const iframe = document.querySelector("#preview-iframe") as - | HTMLIFrameElement - | undefined; - - if (!iframe?.contentDocument) { - throw new Error( - `Preview iframe could not be found when trying to analyze headings` - ); - } - - const headings = iframe.contentDocument.querySelectorAll("h1,h2,h3,h4,h5,h6"); + frame, +}: { addDataAttr?: boolean; frame?: Element } = {}) => { + const headings = frame?.querySelectorAll("h1,h2,h3,h4,h5,h6") || []; const _outline: { rank: number; text: string; analyzeId: string }[] = []; @@ -51,8 +43,8 @@ type Block = { analyzeId?: string; }; -function buildHierarchy(): Block[] { - const headings = getOutline({ addDataAttr: true }); +function buildHierarchy(frame: Element): Block[] { + const headings = getOutline({ addDataAttr: true, frame }); const root = { rank: 0, children: [], text: "" }; // Placeholder root node let path: Block[] = [root]; @@ -100,19 +92,22 @@ export const HeadingAnalyzer = () => { const [hierarchy, setHierarchy] = useState([]); const [firstRender, setFirstRender] = useState(true); + const frame = useFrame(); + // Re-render when content changes useEffect(() => { - // We need to delay to allow remainder of page to render first + if (!frame) return; + // We need to delay to allow remainder of page to render first if (firstRender) { setTimeout(() => { - setHierarchy(buildHierarchy()); + setHierarchy(buildHierarchy(frame)); setFirstRender(false); }, 100); } else { - setHierarchy(buildHierarchy()); + setHierarchy(buildHierarchy(frame)); } - }, [appState.data.content]); + }, [appState.data.content, frame]); return ( <> @@ -135,17 +130,7 @@ export const HeadingAnalyzer = () => { : (e) => { e.stopPropagation(); - const iframe = document.querySelector( - "#preview-iframe" - ) as HTMLIFrameElement; - - if (!iframe.contentDocument) { - throw new Error( - `plugin-heading-outline-analyzer: Preview iframe could not be found when trying to scroll to item` - ); - } - - const el = iframe.contentDocument.querySelector( + const el = frame?.querySelector( `[${dataAttr}="${props.analyzeId}"]` ) as HTMLElement;