payload-better-editor 1.0.3 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/hooks/usePreviewBinding.js +5 -3
- package/dist/hooks/usePreviewBinding.js.map +1 -1
- package/dist/internal/iframe.d.ts +4 -0
- package/dist/internal/iframe.js +7 -1
- package/dist/internal/iframe.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +1 -1
|
@@ -81,12 +81,14 @@ import { useLatestRef } from './useLatestRef';
|
|
|
81
81
|
// Fresh iframes report readyState='complete' on their initial
|
|
82
82
|
// `about:blank` document before `src` has navigated. Skip and wait
|
|
83
83
|
// for the real `load` event so we don't bind to an empty body.
|
|
84
|
-
|
|
85
|
-
|
|
84
|
+
// `doc.URL` is a non-nullable string on Document — safer than
|
|
85
|
+
// `doc.location.href`, which Firefox can briefly expose as null.
|
|
86
|
+
if (!doc.URL || doc.URL === 'about:blank') return;
|
|
86
87
|
onLoadingChangeRef.current(false);
|
|
87
88
|
bindToDocument(doc);
|
|
88
89
|
};
|
|
89
|
-
|
|
90
|
+
const initialDoc = getSameOriginDocument(iframe);
|
|
91
|
+
if (initialDoc && initialDoc.readyState === 'complete') {
|
|
90
92
|
onLoad();
|
|
91
93
|
}
|
|
92
94
|
iframe.addEventListener('load', onLoad);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/hooks/usePreviewBinding.ts"],"sourcesContent":["'use client'\n\nimport { useCallback, useEffect, useRef, type RefObject } from 'react'\nimport { HoverToolbarController } from '../preview/HoverToolbarController'\nimport { installClickToFocus } from '../preview/installClickToFocus'\nimport { installHoverStyles } from '../preview/installHoverStyles'\nimport type { BlockActionMessage } from '../preview/protocol'\nimport { BLOCK_ID_SELECTOR } from '../internal/dom'\nimport { getSameOriginDocument } from '../internal/iframe'\nimport { useLatestRef } from './useLatestRef'\n\nexport type PreviewBindingSettings = {\n hoverColorTopLevel: string\n hoverColorNested: string\n hoverOutlineWidth: number\n showHoverToolbar: boolean\n hoverToolbarPosition: import('../internal/constants').HoverToolbarPosition\n}\n\nexport type UsePreviewBindingArgs = {\n iframeRef: RefObject<HTMLIFrameElement | null>\n settings: PreviewBindingSettings\n interactModeRef: RefObject<boolean>\n onFocusBlock: (id: string) => void\n onBlockAction: (id: string, action: BlockActionMessage['action']) => void\n onLoadingChange: (loading: boolean) => void\n}\n\nexport type UsePreviewBindingReturn = {\n controllerRef: RefObject<HoverToolbarController | null>\n isBoundRef: RefObject<boolean>\n}\n\n/**\n * Owns the iframe load → install styles + click handler + hover toolbar\n * lifecycle. Idempotent: tears down previous bindings before installing\n * new ones, and unbinds on unmount.\n */\nexport const usePreviewBinding = ({\n iframeRef,\n settings,\n interactModeRef,\n onFocusBlock,\n onBlockAction,\n onLoadingChange,\n}: UsePreviewBindingArgs): UsePreviewBindingReturn => {\n const teardownRef = useRef<(() => void) | null>(null)\n const controllerRef = useRef<HoverToolbarController | null>(null)\n const isBoundRef = useRef(false)\n // One-shot flags so dev-only console warnings don't repeat on every\n // iframe re-load during a single editor session.\n const warnedMissingBlocksRef = useRef(false)\n const warnedCrossOriginRef = useRef(false)\n const settingsRef = useLatestRef(settings)\n const onFocusBlockRef = useLatestRef(onFocusBlock)\n const onBlockActionRef = useLatestRef(onBlockAction)\n const onLoadingChangeRef = useLatestRef(onLoadingChange)\n\n const bindToDocument = useCallback(\n (doc: Document) => {\n teardownRef.current?.()\n controllerRef.current?.destroy()\n controllerRef.current = null\n\n const s = settingsRef.current\n\n const removeStyles = installHoverStyles(doc, {\n topColor: s.hoverColorTopLevel,\n nestedColor: s.hoverColorNested,\n outlineWidth: s.hoverOutlineWidth,\n })\n const removeClick = installClickToFocus(doc, (id) => onFocusBlockRef.current(id), {\n isEnabled: () => !interactModeRef.current,\n })\n\n if (s.showHoverToolbar) {\n controllerRef.current = new HoverToolbarController(doc, {\n position: s.hoverToolbarPosition,\n outlineWidth: s.hoverOutlineWidth,\n onAction: (id, action) => onBlockActionRef.current(id, action),\n })\n }\n\n // Dev-only sanity check: zero [data-better-editor-id] elements means\n // the consumer forgot to spread getBlockProps() on their block wrappers\n // (or the page just has no blocks yet — both look identical from here).\n // Warn at most once per editor session to avoid console spam.\n if (process.env.NODE_ENV !== 'production' && !warnedMissingBlocksRef.current) {\n const blockCount = doc.querySelectorAll(BLOCK_ID_SELECTOR).length\n if (blockCount === 0) {\n warnedMissingBlocksRef.current = true\n console.warn(\n \"[better-editor] no [data-better-editor-id] elements found in the preview iframe — if your page has blocks, wrap them with `getBlockProps(block)` from 'payload-better-editor/client' so click-to-edit works.\",\n )\n }\n }\n\n isBoundRef.current = true\n teardownRef.current = () => {\n removeStyles()\n removeClick()\n controllerRef.current?.destroy()\n controllerRef.current = null\n isBoundRef.current = false\n }\n },\n // eslint-disable-next-line react-hooks/exhaustive-deps -- refs are stable\n [],\n )\n\n // Bind once on mount; teardown on unmount. All inputs flow through\n // stable refs, so the load listener doesn't need to re-attach.\n useEffect(() => {\n const iframe = iframeRef.current\n if (!iframe) return\n\n const onLoad = () => {\n const doc = getSameOriginDocument(iframe)\n if (!doc) {\n onLoadingChangeRef.current(false)\n if (process.env.NODE_ENV !== 'production' && !warnedCrossOriginRef.current) {\n warnedCrossOriginRef.current = true\n console.warn(\n '[better-editor] preview iframe is cross-origin — click-to-edit, hover styles, and the in-iframe toolbar are disabled. Serve your preview URL from the same origin as the Payload admin.',\n )\n }\n return\n }\n // Fresh iframes report readyState='complete' on their initial\n // `about:blank` document before `src` has navigated. Skip and wait\n // for the real `load` event so we don't bind to an empty body.\n
|
|
1
|
+
{"version":3,"sources":["../../src/hooks/usePreviewBinding.ts"],"sourcesContent":["'use client'\n\nimport { useCallback, useEffect, useRef, type RefObject } from 'react'\nimport { HoverToolbarController } from '../preview/HoverToolbarController'\nimport { installClickToFocus } from '../preview/installClickToFocus'\nimport { installHoverStyles } from '../preview/installHoverStyles'\nimport type { BlockActionMessage } from '../preview/protocol'\nimport { BLOCK_ID_SELECTOR } from '../internal/dom'\nimport { getSameOriginDocument } from '../internal/iframe'\nimport { useLatestRef } from './useLatestRef'\n\nexport type PreviewBindingSettings = {\n hoverColorTopLevel: string\n hoverColorNested: string\n hoverOutlineWidth: number\n showHoverToolbar: boolean\n hoverToolbarPosition: import('../internal/constants').HoverToolbarPosition\n}\n\nexport type UsePreviewBindingArgs = {\n iframeRef: RefObject<HTMLIFrameElement | null>\n settings: PreviewBindingSettings\n interactModeRef: RefObject<boolean>\n onFocusBlock: (id: string) => void\n onBlockAction: (id: string, action: BlockActionMessage['action']) => void\n onLoadingChange: (loading: boolean) => void\n}\n\nexport type UsePreviewBindingReturn = {\n controllerRef: RefObject<HoverToolbarController | null>\n isBoundRef: RefObject<boolean>\n}\n\n/**\n * Owns the iframe load → install styles + click handler + hover toolbar\n * lifecycle. Idempotent: tears down previous bindings before installing\n * new ones, and unbinds on unmount.\n */\nexport const usePreviewBinding = ({\n iframeRef,\n settings,\n interactModeRef,\n onFocusBlock,\n onBlockAction,\n onLoadingChange,\n}: UsePreviewBindingArgs): UsePreviewBindingReturn => {\n const teardownRef = useRef<(() => void) | null>(null)\n const controllerRef = useRef<HoverToolbarController | null>(null)\n const isBoundRef = useRef(false)\n // One-shot flags so dev-only console warnings don't repeat on every\n // iframe re-load during a single editor session.\n const warnedMissingBlocksRef = useRef(false)\n const warnedCrossOriginRef = useRef(false)\n const settingsRef = useLatestRef(settings)\n const onFocusBlockRef = useLatestRef(onFocusBlock)\n const onBlockActionRef = useLatestRef(onBlockAction)\n const onLoadingChangeRef = useLatestRef(onLoadingChange)\n\n const bindToDocument = useCallback(\n (doc: Document) => {\n teardownRef.current?.()\n controllerRef.current?.destroy()\n controllerRef.current = null\n\n const s = settingsRef.current\n\n const removeStyles = installHoverStyles(doc, {\n topColor: s.hoverColorTopLevel,\n nestedColor: s.hoverColorNested,\n outlineWidth: s.hoverOutlineWidth,\n })\n const removeClick = installClickToFocus(doc, (id) => onFocusBlockRef.current(id), {\n isEnabled: () => !interactModeRef.current,\n })\n\n if (s.showHoverToolbar) {\n controllerRef.current = new HoverToolbarController(doc, {\n position: s.hoverToolbarPosition,\n outlineWidth: s.hoverOutlineWidth,\n onAction: (id, action) => onBlockActionRef.current(id, action),\n })\n }\n\n // Dev-only sanity check: zero [data-better-editor-id] elements means\n // the consumer forgot to spread getBlockProps() on their block wrappers\n // (or the page just has no blocks yet — both look identical from here).\n // Warn at most once per editor session to avoid console spam.\n if (process.env.NODE_ENV !== 'production' && !warnedMissingBlocksRef.current) {\n const blockCount = doc.querySelectorAll(BLOCK_ID_SELECTOR).length\n if (blockCount === 0) {\n warnedMissingBlocksRef.current = true\n console.warn(\n \"[better-editor] no [data-better-editor-id] elements found in the preview iframe — if your page has blocks, wrap them with `getBlockProps(block)` from 'payload-better-editor/client' so click-to-edit works.\",\n )\n }\n }\n\n isBoundRef.current = true\n teardownRef.current = () => {\n removeStyles()\n removeClick()\n controllerRef.current?.destroy()\n controllerRef.current = null\n isBoundRef.current = false\n }\n },\n // eslint-disable-next-line react-hooks/exhaustive-deps -- refs are stable\n [],\n )\n\n // Bind once on mount; teardown on unmount. All inputs flow through\n // stable refs, so the load listener doesn't need to re-attach.\n useEffect(() => {\n const iframe = iframeRef.current\n if (!iframe) return\n\n const onLoad = () => {\n const doc = getSameOriginDocument(iframe)\n if (!doc) {\n onLoadingChangeRef.current(false)\n if (process.env.NODE_ENV !== 'production' && !warnedCrossOriginRef.current) {\n warnedCrossOriginRef.current = true\n console.warn(\n '[better-editor] preview iframe is cross-origin — click-to-edit, hover styles, and the in-iframe toolbar are disabled. Serve your preview URL from the same origin as the Payload admin.',\n )\n }\n return\n }\n // Fresh iframes report readyState='complete' on their initial\n // `about:blank` document before `src` has navigated. Skip and wait\n // for the real `load` event so we don't bind to an empty body.\n // `doc.URL` is a non-nullable string on Document — safer than\n // `doc.location.href`, which Firefox can briefly expose as null.\n if (!doc.URL || doc.URL === 'about:blank') return\n onLoadingChangeRef.current(false)\n bindToDocument(doc)\n }\n\n const initialDoc = getSameOriginDocument(iframe)\n if (initialDoc && initialDoc.readyState === 'complete') {\n onLoad()\n }\n iframe.addEventListener('load', onLoad)\n\n return () => {\n iframe.removeEventListener('load', onLoad)\n teardownRef.current?.()\n teardownRef.current = null\n controllerRef.current?.destroy()\n controllerRef.current = null\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps -- refs are stable\n }, [])\n\n return { controllerRef, isBoundRef }\n}\n"],"names":["useCallback","useEffect","useRef","HoverToolbarController","installClickToFocus","installHoverStyles","BLOCK_ID_SELECTOR","getSameOriginDocument","useLatestRef","usePreviewBinding","iframeRef","settings","interactModeRef","onFocusBlock","onBlockAction","onLoadingChange","teardownRef","controllerRef","isBoundRef","warnedMissingBlocksRef","warnedCrossOriginRef","settingsRef","onFocusBlockRef","onBlockActionRef","onLoadingChangeRef","bindToDocument","doc","current","destroy","s","removeStyles","topColor","hoverColorTopLevel","nestedColor","hoverColorNested","outlineWidth","hoverOutlineWidth","removeClick","id","isEnabled","showHoverToolbar","position","hoverToolbarPosition","onAction","action","process","env","NODE_ENV","blockCount","querySelectorAll","length","console","warn","iframe","onLoad","URL","initialDoc","readyState","addEventListener","removeEventListener"],"mappings":"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"}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Reads `iframe.contentDocument` defensively. Throws cross-origin —
|
|
3
3
|
* returning `null` lets callers bail out without try/catch boilerplate.
|
|
4
|
+
*
|
|
5
|
+
* Firefox can hand back a Document whose `location` is `null` during the
|
|
6
|
+
* transient pre-navigation phase of a freshly mounted iframe. Treat that
|
|
7
|
+
* as "not ready" so callers don't trip a TypeError on `doc.location.href`.
|
|
4
8
|
*/
|
|
5
9
|
export declare const getSameOriginDocument: (iframe: HTMLIFrameElement) => Document | null;
|
package/dist/internal/iframe.js
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Reads `iframe.contentDocument` defensively. Throws cross-origin —
|
|
3
3
|
* returning `null` lets callers bail out without try/catch boilerplate.
|
|
4
|
+
*
|
|
5
|
+
* Firefox can hand back a Document whose `location` is `null` during the
|
|
6
|
+
* transient pre-navigation phase of a freshly mounted iframe. Treat that
|
|
7
|
+
* as "not ready" so callers don't trip a TypeError on `doc.location.href`.
|
|
4
8
|
*/ export const getSameOriginDocument = (iframe)=>{
|
|
5
9
|
try {
|
|
6
|
-
|
|
10
|
+
const doc = iframe.contentDocument;
|
|
11
|
+
if (!doc || !doc.location) return null;
|
|
12
|
+
return doc;
|
|
7
13
|
} catch {
|
|
8
14
|
return null;
|
|
9
15
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/internal/iframe.ts"],"sourcesContent":["/**\n * Reads `iframe.contentDocument` defensively. Throws cross-origin —\n * returning `null` lets callers bail out without try/catch boilerplate.\n */\nexport const getSameOriginDocument = (iframe: HTMLIFrameElement): Document | null => {\n try {\n
|
|
1
|
+
{"version":3,"sources":["../../src/internal/iframe.ts"],"sourcesContent":["/**\n * Reads `iframe.contentDocument` defensively. Throws cross-origin —\n * returning `null` lets callers bail out without try/catch boilerplate.\n *\n * Firefox can hand back a Document whose `location` is `null` during the\n * transient pre-navigation phase of a freshly mounted iframe. Treat that\n * as \"not ready\" so callers don't trip a TypeError on `doc.location.href`.\n */\nexport const getSameOriginDocument = (iframe: HTMLIFrameElement): Document | null => {\n try {\n const doc = iframe.contentDocument\n if (!doc || !doc.location) return null\n return doc\n } catch {\n return null\n }\n}\n"],"names":["getSameOriginDocument","iframe","doc","contentDocument","location"],"mappings":"AAAA;;;;;;;CAOC,GACD,OAAO,MAAMA,wBAAwB,CAACC;IACpC,IAAI;QACF,MAAMC,MAAMD,OAAOE,eAAe;QAClC,IAAI,CAACD,OAAO,CAACA,IAAIE,QAAQ,EAAE,OAAO;QAClC,OAAOF;IACT,EAAE,OAAM;QACN,OAAO;IACT;AACF,EAAC"}
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "1.0.
|
|
1
|
+
export declare const VERSION = "1.0.4";
|
package/dist/version.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Single source of truth — re-exported from index.ts as `VERSION` and used
|
|
2
2
|
// by client components (e.g. the settings footer) without dragging the
|
|
3
3
|
// whole server entry into the client bundle.
|
|
4
|
-
export const VERSION = '1.0.
|
|
4
|
+
export const VERSION = '1.0.4';
|
|
5
5
|
|
|
6
6
|
//# sourceMappingURL=version.js.map
|
package/dist/version.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/version.ts"],"sourcesContent":["// Single source of truth — re-exported from index.ts as `VERSION` and used\n// by client components (e.g. the settings footer) without dragging the\n// whole server entry into the client bundle.\nexport const VERSION = '1.0.
|
|
1
|
+
{"version":3,"sources":["../src/version.ts"],"sourcesContent":["// Single source of truth — re-exported from index.ts as `VERSION` and used\n// by client components (e.g. the settings footer) without dragging the\n// whole server entry into the client bundle.\nexport const VERSION = '1.0.4'\n"],"names":["VERSION"],"mappings":"AAAA,2EAA2E;AAC3E,uEAAuE;AACvE,6CAA6C;AAC7C,OAAO,MAAMA,UAAU,QAAO"}
|
package/package.json
CHANGED