payload-better-editor 1.0.0
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/LICENSE +21 -0
- package/README.md +57 -0
- package/dist/admin/ErrorBoundary.d.ts +17 -0
- package/dist/admin/ErrorBoundary.js +62 -0
- package/dist/admin/ErrorBoundary.js.map +1 -0
- package/dist/admin/LiveEditorOverlay.d.ts +12 -0
- package/dist/admin/LiveEditorOverlay.js +160 -0
- package/dist/admin/LiveEditorOverlay.js.map +1 -0
- package/dist/admin/LiveEditorToggle.d.ts +7 -0
- package/dist/admin/LiveEditorToggle.js +84 -0
- package/dist/admin/LiveEditorToggle.js.map +1 -0
- package/dist/admin/PreviewFrame.d.ts +22 -0
- package/dist/admin/PreviewFrame.js +137 -0
- package/dist/admin/PreviewFrame.js.map +1 -0
- package/dist/admin/PreviewToolbar.d.ts +16 -0
- package/dist/admin/PreviewToolbar.js +90 -0
- package/dist/admin/PreviewToolbar.js.map +1 -0
- package/dist/admin/SettingsBanner.d.ts +3 -0
- package/dist/admin/SettingsBanner.js +105 -0
- package/dist/admin/SettingsBanner.js.map +1 -0
- package/dist/admin/ViewportToggle.d.ts +7 -0
- package/dist/admin/ViewportToggle.js +79 -0
- package/dist/admin/ViewportToggle.js.map +1 -0
- package/dist/admin/blocks/AddBlockDrawer.d.ts +9 -0
- package/dist/admin/blocks/AddBlockDrawer.js +16 -0
- package/dist/admin/blocks/AddBlockDrawer.js.map +1 -0
- package/dist/admin/blocks/BlockActionsToolbar.d.ts +15 -0
- package/dist/admin/blocks/BlockActionsToolbar.js +102 -0
- package/dist/admin/blocks/BlockActionsToolbar.js.map +1 -0
- package/dist/admin/blocks/BlockEmptyState.d.ts +6 -0
- package/dist/admin/blocks/BlockEmptyState.js +26 -0
- package/dist/admin/blocks/BlockEmptyState.js.map +1 -0
- package/dist/admin/blocks/BlockHeader.d.ts +7 -0
- package/dist/admin/blocks/BlockHeader.js +32 -0
- package/dist/admin/blocks/BlockHeader.js.map +1 -0
- package/dist/admin/blocks/schema.d.ts +19 -0
- package/dist/admin/blocks/schema.js +80 -0
- package/dist/admin/blocks/schema.js.map +1 -0
- package/dist/admin/blocks/useBlockActions.d.ts +24 -0
- package/dist/admin/blocks/useBlockActions.js +100 -0
- package/dist/admin/blocks/useBlockActions.js.map +1 -0
- package/dist/admin/icons.d.ts +24 -0
- package/dist/admin/icons.js +36 -0
- package/dist/admin/icons.js.map +1 -0
- package/dist/admin/sidebar/BlockSettingsTab.d.ts +10 -0
- package/dist/admin/sidebar/BlockSettingsTab.js +153 -0
- package/dist/admin/sidebar/BlockSettingsTab.js.map +1 -0
- package/dist/admin/sidebar/DocumentFieldsTab.d.ts +8 -0
- package/dist/admin/sidebar/DocumentFieldsTab.js +38 -0
- package/dist/admin/sidebar/DocumentFieldsTab.js.map +1 -0
- package/dist/admin/sidebar/DocumentMetaTab.d.ts +2 -0
- package/dist/admin/sidebar/DocumentMetaTab.js +11 -0
- package/dist/admin/sidebar/DocumentMetaTab.js.map +1 -0
- package/dist/admin/sidebar/DocumentSettingsTab.d.ts +2 -0
- package/dist/admin/sidebar/DocumentSettingsTab.js +48 -0
- package/dist/admin/sidebar/DocumentSettingsTab.js.map +1 -0
- package/dist/admin/sidebar/Sidebar.d.ts +10 -0
- package/dist/admin/sidebar/Sidebar.js +92 -0
- package/dist/admin/sidebar/Sidebar.js.map +1 -0
- package/dist/client.d.ts +34 -0
- package/dist/client.js +30 -0
- package/dist/client.js.map +1 -0
- package/dist/global.d.ts +4 -0
- package/dist/global.js +200 -0
- package/dist/global.js.map +1 -0
- package/dist/hooks/useAddBlockDrawer.d.ts +14 -0
- package/dist/hooks/useAddBlockDrawer.js +26 -0
- package/dist/hooks/useAddBlockDrawer.js.map +1 -0
- package/dist/hooks/useBlockActionMessages.d.ts +8 -0
- package/dist/hooks/useBlockActionMessages.js +107 -0
- package/dist/hooks/useBlockActionMessages.js.map +1 -0
- package/dist/hooks/useDocConfig.d.ts +6 -0
- package/dist/hooks/useDocConfig.js +18 -0
- package/dist/hooks/useDocConfig.js.map +1 -0
- package/dist/hooks/useFocusTrap.d.ts +2 -0
- package/dist/hooks/useFocusTrap.js +84 -0
- package/dist/hooks/useFocusTrap.js.map +1 -0
- package/dist/hooks/useFullscreenOverlay.d.ts +2 -0
- package/dist/hooks/useFullscreenOverlay.js +30 -0
- package/dist/hooks/useFullscreenOverlay.js.map +1 -0
- package/dist/hooks/useIframeResizeObserver.d.ts +2 -0
- package/dist/hooks/useIframeResizeObserver.js +20 -0
- package/dist/hooks/useIframeResizeObserver.js.map +1 -0
- package/dist/hooks/useLatestRef.d.ts +6 -0
- package/dist/hooks/useLatestRef.js +12 -0
- package/dist/hooks/useLatestRef.js.map +1 -0
- package/dist/hooks/useMainWrapperPortal.d.ts +1 -0
- package/dist/hooks/useMainWrapperPortal.js +64 -0
- package/dist/hooks/useMainWrapperPortal.js.map +1 -0
- package/dist/hooks/useOverlayKeyboard.d.ts +6 -0
- package/dist/hooks/useOverlayKeyboard.js +43 -0
- package/dist/hooks/useOverlayKeyboard.js.map +1 -0
- package/dist/hooks/usePreviewBinding.d.ts +28 -0
- package/dist/hooks/usePreviewBinding.js +108 -0
- package/dist/hooks/usePreviewBinding.js.map +1 -0
- package/dist/hooks/usePreviewHandleDrag.d.ts +11 -0
- package/dist/hooks/usePreviewHandleDrag.js +53 -0
- package/dist/hooks/usePreviewHandleDrag.js.map +1 -0
- package/dist/hooks/usePreviewSelectionSync.d.ts +15 -0
- package/dist/hooks/usePreviewSelectionSync.js +80 -0
- package/dist/hooks/usePreviewSelectionSync.js.map +1 -0
- package/dist/hooks/usePreviewSettingsSync.d.ts +17 -0
- package/dist/hooks/usePreviewSettingsSync.js +55 -0
- package/dist/hooks/usePreviewSettingsSync.js.map +1 -0
- package/dist/hooks/useSidebarResize.d.ts +8 -0
- package/dist/hooks/useSidebarResize.js +101 -0
- package/dist/hooks/useSidebarResize.js.map +1 -0
- package/dist/hooks/useViewportState.d.ts +10 -0
- package/dist/hooks/useViewportState.js +44 -0
- package/dist/hooks/useViewportState.js.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +104 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/constants.d.ts +22 -0
- package/dist/internal/constants.js +38 -0
- package/dist/internal/constants.js.map +1 -0
- package/dist/internal/dom.d.ts +4 -0
- package/dist/internal/dom.js +6 -0
- package/dist/internal/dom.js.map +1 -0
- package/dist/internal/iframe.d.ts +5 -0
- package/dist/internal/iframe.js +12 -0
- package/dist/internal/iframe.js.map +1 -0
- package/dist/internal/limits.d.ts +9 -0
- package/dist/internal/limits.js +11 -0
- package/dist/internal/limits.js.map +1 -0
- package/dist/internal/path.d.ts +5 -0
- package/dist/internal/path.js +12 -0
- package/dist/internal/path.js.map +1 -0
- package/dist/internal/postmessage.d.ts +3 -0
- package/dist/internal/postmessage.js +21 -0
- package/dist/internal/postmessage.js.map +1 -0
- package/dist/internal/storage-keys.d.ts +8 -0
- package/dist/internal/storage-keys.js +9 -0
- package/dist/internal/storage-keys.js.map +1 -0
- package/dist/internal/storage.d.ts +2 -0
- package/dist/internal/storage.js +20 -0
- package/dist/internal/storage.js.map +1 -0
- package/dist/preview/HoverToolbar.d.ts +8 -0
- package/dist/preview/HoverToolbar.js +48 -0
- package/dist/preview/HoverToolbar.js.map +1 -0
- package/dist/preview/HoverToolbarController.d.ts +31 -0
- package/dist/preview/HoverToolbarController.js +160 -0
- package/dist/preview/HoverToolbarController.js.map +1 -0
- package/dist/preview/hover-css.d.ts +11 -0
- package/dist/preview/hover-css.js +94 -0
- package/dist/preview/hover-css.js.map +1 -0
- package/dist/preview/installClickToFocus.d.ts +6 -0
- package/dist/preview/installClickToFocus.js +21 -0
- package/dist/preview/installClickToFocus.js.map +1 -0
- package/dist/preview/installHoverStyles.d.ts +2 -0
- package/dist/preview/installHoverStyles.js +15 -0
- package/dist/preview/installHoverStyles.js.map +1 -0
- package/dist/preview/protocol.d.ts +11 -0
- package/dist/preview/protocol.js +19 -0
- package/dist/preview/protocol.js.map +1 -0
- package/dist/preview/toolbar-position.d.ts +20 -0
- package/dist/preview/toolbar-position.js +22 -0
- package/dist/preview/toolbar-position.js.map +1 -0
- package/dist/providers/BetterEditorConfigProvider.d.ts +14 -0
- package/dist/providers/BetterEditorConfigProvider.js +26 -0
- package/dist/providers/BetterEditorConfigProvider.js.map +1 -0
- package/dist/providers/OverlayProviders.d.ts +8 -0
- package/dist/providers/OverlayProviders.js +22 -0
- package/dist/providers/OverlayProviders.js.map +1 -0
- package/dist/state/useBetterEditorSettings.d.ts +18 -0
- package/dist/state/useBetterEditorSettings.js +65 -0
- package/dist/state/useBetterEditorSettings.js.map +1 -0
- package/dist/state/useEditorHistory.d.ts +16 -0
- package/dist/state/useEditorHistory.js +157 -0
- package/dist/state/useEditorHistory.js.map +1 -0
- package/dist/styles/blocks-tab.css +163 -0
- package/dist/styles/overlay.css +133 -0
- package/dist/styles/preview.css +211 -0
- package/dist/styles/settings-banner.css +73 -0
- package/dist/styles/sidebar.css +88 -0
- package/dist/types.d.ts +41 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +6 -0
- package/dist/version.js.map +1 -0
- package/package.json +117 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/hooks/useBlockActionMessages.ts"],"sourcesContent":["'use client'\n\nimport { useEffect, useMemo, useState } from 'react'\nimport { useAllFormFields, useForm } from '@payloadcms/ui'\nimport { listenForParentInbound } from '../internal/postmessage'\nimport { splitFieldPath } from '../internal/path'\nimport type { FormState } from 'payload'\nimport { useEditorHistory } from '../state/useEditorHistory'\nimport { useLatestRef } from './useLatestRef'\n\nconst ID_SUFFIX = '.id'\n\nconst buildIdIndex = (fields: FormState): Map<string, string> => {\n const map = new Map<string, string>()\n for (const key of Object.keys(fields)) {\n if (!key.endsWith(ID_SUFFIX)) continue\n const value = fields[key]?.value\n if (typeof value === 'string' && value.length > 0) {\n map.set(value, key.slice(0, -ID_SUFFIX.length))\n }\n }\n return map\n}\n\nexport type UseBlockActionMessagesArgs = {\n selectedBlockPath: string | null\n setSelectedBlockPath: (path: string | null) => void\n}\n\nexport type UseBlockActionMessagesReturn = {\n addBelowRequestId: number\n}\n\nexport const useBlockActionMessages = ({\n setSelectedBlockPath,\n}: UseBlockActionMessagesArgs): UseBlockActionMessagesReturn => {\n const [addBelowRequestId, setAddBelowRequestId] = useState<number>(0)\n\n const [allFields] = useAllFormFields()\n const idIndex = useMemo(() => buildIdIndex(allFields as FormState), [allFields])\n const { dispatchFields } = useForm()\n const history = useEditorHistory()\n\n // Refs let the postMessage listener stay bound across renders without\n // missing the latest form state, dispatcher, or history commit.\n const allFieldsRef = useLatestRef(allFields)\n const idIndexRef = useLatestRef(idIndex)\n const dispatchFieldsRef = useLatestRef(dispatchFields)\n const historyRef = useLatestRef(history)\n const setSelectedBlockPathRef = useLatestRef(setSelectedBlockPath)\n\n // Bind the postMessage listener exactly once. All four state inputs flow\n // through stable refs (above), so a re-bind is never needed.\n useEffect(\n () =>\n listenForParentInbound((data) => {\n const fields = allFieldsRef.current as FormState\n const path = idIndexRef.current.get(data.id) ?? null\n if (!path) return\n\n const select = setSelectedBlockPathRef.current\n const dispatch = dispatchFieldsRef.current\n const { commit } = historyRef.current\n\n if (data.type === 'focus-block') {\n select(path)\n return\n }\n\n if (data.action === 'add') {\n // Monotonic counter — BlockSettingsTab compares the latest id against\n // its lastHandledRequestRef. Date.now() risked collisions on\n // double-clicks landing in the same millisecond.\n select(path)\n setAddBelowRequestId((id) => id + 1)\n return\n }\n\n const split = splitFieldPath(path)\n if (!split) return\n const { parent: parentPath, index: rowIndex } = split\n const rows = fields[parentPath]?.rows\n const rowCount = Array.isArray(rows) ? rows.length : 0\n if (rowIndex < 0 || rowIndex >= rowCount) return\n\n switch (data.action) {\n case 'move-up':\n if (rowIndex === 0) return\n commit(() =>\n dispatch({\n type: 'MOVE_ROW',\n path: parentPath,\n moveFromIndex: rowIndex,\n moveToIndex: rowIndex - 1,\n }),\n )\n select(`${parentPath}.${rowIndex - 1}`)\n break\n case 'move-down':\n if (rowIndex >= rowCount - 1) return\n commit(() =>\n dispatch({\n type: 'MOVE_ROW',\n path: parentPath,\n moveFromIndex: rowIndex,\n moveToIndex: rowIndex + 1,\n }),\n )\n select(`${parentPath}.${rowIndex + 1}`)\n break\n case 'duplicate':\n commit(() => dispatch({ type: 'DUPLICATE_ROW', path: parentPath, rowIndex }))\n select(`${parentPath}.${rowIndex + 1}`)\n break\n case 'delete':\n commit(() => dispatch({ type: 'REMOVE_ROW', path: parentPath, rowIndex }))\n select(null)\n break\n }\n }),\n // eslint-disable-next-line react-hooks/exhaustive-deps -- refs are stable\n [],\n )\n\n return { addBelowRequestId }\n}\n"],"names":["useEffect","useMemo","useState","useAllFormFields","useForm","listenForParentInbound","splitFieldPath","useEditorHistory","useLatestRef","ID_SUFFIX","buildIdIndex","fields","map","Map","key","Object","keys","endsWith","value","length","set","slice","useBlockActionMessages","setSelectedBlockPath","addBelowRequestId","setAddBelowRequestId","allFields","idIndex","dispatchFields","history","allFieldsRef","idIndexRef","dispatchFieldsRef","historyRef","setSelectedBlockPathRef","data","current","path","get","id","select","dispatch","commit","type","action","split","parent","parentPath","index","rowIndex","rows","rowCount","Array","isArray","moveFromIndex","moveToIndex"],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useMemo } from 'react';
|
|
3
|
+
import { useDocumentInfo } from '@payloadcms/ui';
|
|
4
|
+
export const useDocConfig = ()=>{
|
|
5
|
+
const { docConfig } = useDocumentInfo();
|
|
6
|
+
return useMemo(()=>{
|
|
7
|
+
const fields = docConfig && 'fields' in docConfig ? docConfig.fields : undefined;
|
|
8
|
+
const slug = docConfig && 'slug' in docConfig ? docConfig.slug : '';
|
|
9
|
+
return {
|
|
10
|
+
fields,
|
|
11
|
+
slug
|
|
12
|
+
};
|
|
13
|
+
}, [
|
|
14
|
+
docConfig
|
|
15
|
+
]);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
//# sourceMappingURL=useDocConfig.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/hooks/useDocConfig.ts"],"sourcesContent":["'use client'\n\nimport { useMemo } from 'react'\nimport type { ClientField } from 'payload'\nimport { useDocumentInfo } from '@payloadcms/ui'\n\nexport type UseDocConfigReturn = {\n fields: ClientField[] | undefined\n slug: string\n}\n\nexport const useDocConfig = (): UseDocConfigReturn => {\n const { docConfig } = useDocumentInfo()\n return useMemo(() => {\n const fields =\n docConfig && 'fields' in docConfig ? (docConfig.fields as ClientField[]) : undefined\n const slug = docConfig && 'slug' in docConfig ? docConfig.slug : ''\n return { fields, slug }\n }, [docConfig])\n}\n"],"names":["useMemo","useDocumentInfo","useDocConfig","docConfig","fields","undefined","slug"],"mappings":"AAAA;AAEA,SAASA,OAAO,QAAQ,QAAO;AAE/B,SAASC,eAAe,QAAQ,iBAAgB;AAOhD,OAAO,MAAMC,eAAe;IAC1B,MAAM,EAAEC,SAAS,EAAE,GAAGF;IACtB,OAAOD,QAAQ;QACb,MAAMI,SACJD,aAAa,YAAYA,YAAaA,UAAUC,MAAM,GAAqBC;QAC7E,MAAMC,OAAOH,aAAa,UAAUA,YAAYA,UAAUG,IAAI,GAAG;QACjE,OAAO;YAAEF;YAAQE;QAAK;IACxB,GAAG;QAACH;KAAU;AAChB,EAAC"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useEffect } from 'react';
|
|
3
|
+
const FOCUSABLE_SELECTOR = [
|
|
4
|
+
'a[href]',
|
|
5
|
+
'area[href]',
|
|
6
|
+
'button:not([disabled])',
|
|
7
|
+
'input:not([disabled]):not([type="hidden"])',
|
|
8
|
+
'select:not([disabled])',
|
|
9
|
+
'textarea:not([disabled])',
|
|
10
|
+
'[tabindex]:not([tabindex="-1"])',
|
|
11
|
+
'audio[controls]',
|
|
12
|
+
'video[controls]',
|
|
13
|
+
'[contenteditable]:not([contenteditable="false"])'
|
|
14
|
+
].join(',');
|
|
15
|
+
const isVisible = (el)=>{
|
|
16
|
+
if (el.hidden) return false;
|
|
17
|
+
const rects = el.getClientRects();
|
|
18
|
+
return rects.length > 0;
|
|
19
|
+
};
|
|
20
|
+
const getFocusable = (root)=>{
|
|
21
|
+
const nodes = root.querySelectorAll(FOCUSABLE_SELECTOR);
|
|
22
|
+
const out = [];
|
|
23
|
+
for (const n of nodes)if (isVisible(n)) out.push(n);
|
|
24
|
+
return out;
|
|
25
|
+
};
|
|
26
|
+
export const useFocusTrap = (ref, active = true)=>{
|
|
27
|
+
useEffect(()=>{
|
|
28
|
+
if (!active) return;
|
|
29
|
+
const root = ref.current;
|
|
30
|
+
if (!root) return;
|
|
31
|
+
const previouslyFocused = document.activeElement ?? null;
|
|
32
|
+
// Initial focus: prefer first interactive child; fall back to root itself
|
|
33
|
+
// so keyboard users land inside the dialog.
|
|
34
|
+
const focusables = getFocusable(root);
|
|
35
|
+
if (focusables.length > 0) {
|
|
36
|
+
focusables[0].focus();
|
|
37
|
+
} else {
|
|
38
|
+
if (!root.hasAttribute('tabindex')) root.setAttribute('tabindex', '-1');
|
|
39
|
+
root.focus();
|
|
40
|
+
}
|
|
41
|
+
const onKey = (e)=>{
|
|
42
|
+
if (e.key !== 'Tab') return;
|
|
43
|
+
const items = getFocusable(root);
|
|
44
|
+
if (items.length === 0) {
|
|
45
|
+
e.preventDefault();
|
|
46
|
+
root.focus();
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const first = items[0];
|
|
50
|
+
const last = items[items.length - 1];
|
|
51
|
+
const activeEl = document.activeElement;
|
|
52
|
+
// If focus has escaped the trap entirely, pull it back to a safe edge.
|
|
53
|
+
if (!activeEl || !root.contains(activeEl)) {
|
|
54
|
+
e.preventDefault();
|
|
55
|
+
(e.shiftKey ? last : first).focus();
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (e.shiftKey && activeEl === first) {
|
|
59
|
+
e.preventDefault();
|
|
60
|
+
last.focus();
|
|
61
|
+
} else if (!e.shiftKey && activeEl === last) {
|
|
62
|
+
e.preventDefault();
|
|
63
|
+
first.focus();
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
document.addEventListener('keydown', onKey, true);
|
|
67
|
+
return ()=>{
|
|
68
|
+
document.removeEventListener('keydown', onKey, true);
|
|
69
|
+
// Best-effort restore — the original element may have been removed
|
|
70
|
+
// from the DOM while the overlay was open.
|
|
71
|
+
if (previouslyFocused && previouslyFocused.isConnected) {
|
|
72
|
+
try {
|
|
73
|
+
previouslyFocused.focus();
|
|
74
|
+
} catch {
|
|
75
|
+
/* element no longer focusable */ }
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
}, [
|
|
79
|
+
ref,
|
|
80
|
+
active
|
|
81
|
+
]);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
//# sourceMappingURL=useFocusTrap.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/hooks/useFocusTrap.ts"],"sourcesContent":["'use client'\n\nimport { useEffect, type RefObject } from 'react'\n\nconst FOCUSABLE_SELECTOR = [\n 'a[href]',\n 'area[href]',\n 'button:not([disabled])',\n 'input:not([disabled]):not([type=\"hidden\"])',\n 'select:not([disabled])',\n 'textarea:not([disabled])',\n '[tabindex]:not([tabindex=\"-1\"])',\n 'audio[controls]',\n 'video[controls]',\n '[contenteditable]:not([contenteditable=\"false\"])',\n].join(',')\n\nconst isVisible = (el: HTMLElement): boolean => {\n if (el.hidden) return false\n const rects = el.getClientRects()\n return rects.length > 0\n}\n\nconst getFocusable = (root: HTMLElement): HTMLElement[] => {\n const nodes = root.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR)\n const out: HTMLElement[] = []\n for (const n of nodes) if (isVisible(n)) out.push(n)\n return out\n}\n\nexport const useFocusTrap = (\n ref: RefObject<HTMLElement | null>,\n active: boolean = true,\n): void => {\n useEffect(() => {\n if (!active) return\n const root = ref.current\n if (!root) return\n\n const previouslyFocused = (document.activeElement as HTMLElement | null) ?? null\n\n // Initial focus: prefer first interactive child; fall back to root itself\n // so keyboard users land inside the dialog.\n const focusables = getFocusable(root)\n if (focusables.length > 0) {\n focusables[0].focus()\n } else {\n if (!root.hasAttribute('tabindex')) root.setAttribute('tabindex', '-1')\n root.focus()\n }\n\n const onKey = (e: KeyboardEvent) => {\n if (e.key !== 'Tab') return\n const items = getFocusable(root)\n if (items.length === 0) {\n e.preventDefault()\n root.focus()\n return\n }\n const first = items[0]\n const last = items[items.length - 1]\n const activeEl = document.activeElement as HTMLElement | null\n // If focus has escaped the trap entirely, pull it back to a safe edge.\n if (!activeEl || !root.contains(activeEl)) {\n e.preventDefault()\n ;(e.shiftKey ? last : first).focus()\n return\n }\n if (e.shiftKey && activeEl === first) {\n e.preventDefault()\n last.focus()\n } else if (!e.shiftKey && activeEl === last) {\n e.preventDefault()\n first.focus()\n }\n }\n\n document.addEventListener('keydown', onKey, true)\n return () => {\n document.removeEventListener('keydown', onKey, true)\n // Best-effort restore — the original element may have been removed\n // from the DOM while the overlay was open.\n if (previouslyFocused && previouslyFocused.isConnected) {\n try {\n previouslyFocused.focus()\n } catch {\n /* element no longer focusable */\n }\n }\n }\n }, [ref, active])\n}\n"],"names":["useEffect","FOCUSABLE_SELECTOR","join","isVisible","el","hidden","rects","getClientRects","length","getFocusable","root","nodes","querySelectorAll","out","n","push","useFocusTrap","ref","active","current","previouslyFocused","document","activeElement","focusables","focus","hasAttribute","setAttribute","onKey","e","key","items","preventDefault","first","last","activeEl","contains","shiftKey","addEventListener","removeEventListener","isConnected"],"mappings":"AAAA;AAEA,SAASA,SAAS,QAAwB,QAAO;AAEjD,MAAMC,qBAAqB;IACzB;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;CACD,CAACC,IAAI,CAAC;AAEP,MAAMC,YAAY,CAACC;IACjB,IAAIA,GAAGC,MAAM,EAAE,OAAO;IACtB,MAAMC,QAAQF,GAAGG,cAAc;IAC/B,OAAOD,MAAME,MAAM,GAAG;AACxB;AAEA,MAAMC,eAAe,CAACC;IACpB,MAAMC,QAAQD,KAAKE,gBAAgB,CAAcX;IACjD,MAAMY,MAAqB,EAAE;IAC7B,KAAK,MAAMC,KAAKH,MAAO,IAAIR,UAAUW,IAAID,IAAIE,IAAI,CAACD;IAClD,OAAOD;AACT;AAEA,OAAO,MAAMG,eAAe,CAC1BC,KACAC,SAAkB,IAAI;IAEtBlB,UAAU;QACR,IAAI,CAACkB,QAAQ;QACb,MAAMR,OAAOO,IAAIE,OAAO;QACxB,IAAI,CAACT,MAAM;QAEX,MAAMU,oBAAoB,AAACC,SAASC,aAAa,IAA2B;QAE5E,0EAA0E;QAC1E,4CAA4C;QAC5C,MAAMC,aAAad,aAAaC;QAChC,IAAIa,WAAWf,MAAM,GAAG,GAAG;YACzBe,UAAU,CAAC,EAAE,CAACC,KAAK;QACrB,OAAO;YACL,IAAI,CAACd,KAAKe,YAAY,CAAC,aAAaf,KAAKgB,YAAY,CAAC,YAAY;YAClEhB,KAAKc,KAAK;QACZ;QAEA,MAAMG,QAAQ,CAACC;YACb,IAAIA,EAAEC,GAAG,KAAK,OAAO;YACrB,MAAMC,QAAQrB,aAAaC;YAC3B,IAAIoB,MAAMtB,MAAM,KAAK,GAAG;gBACtBoB,EAAEG,cAAc;gBAChBrB,KAAKc,KAAK;gBACV;YACF;YACA,MAAMQ,QAAQF,KAAK,CAAC,EAAE;YACtB,MAAMG,OAAOH,KAAK,CAACA,MAAMtB,MAAM,GAAG,EAAE;YACpC,MAAM0B,WAAWb,SAASC,aAAa;YACvC,uEAAuE;YACvE,IAAI,CAACY,YAAY,CAACxB,KAAKyB,QAAQ,CAACD,WAAW;gBACzCN,EAAEG,cAAc;gBACdH,CAAAA,EAAEQ,QAAQ,GAAGH,OAAOD,KAAI,EAAGR,KAAK;gBAClC;YACF;YACA,IAAII,EAAEQ,QAAQ,IAAIF,aAAaF,OAAO;gBACpCJ,EAAEG,cAAc;gBAChBE,KAAKT,KAAK;YACZ,OAAO,IAAI,CAACI,EAAEQ,QAAQ,IAAIF,aAAaD,MAAM;gBAC3CL,EAAEG,cAAc;gBAChBC,MAAMR,KAAK;YACb;QACF;QAEAH,SAASgB,gBAAgB,CAAC,WAAWV,OAAO;QAC5C,OAAO;YACLN,SAASiB,mBAAmB,CAAC,WAAWX,OAAO;YAC/C,mEAAmE;YACnE,2CAA2C;YAC3C,IAAIP,qBAAqBA,kBAAkBmB,WAAW,EAAE;gBACtD,IAAI;oBACFnB,kBAAkBI,KAAK;gBACzB,EAAE,OAAM;gBACN,+BAA+B,GACjC;YACF;QACF;IACF,GAAG;QAACP;QAAKC;KAAO;AAClB,EAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import React, { useEffect, useRef } from 'react';
|
|
3
|
+
export const useFullscreenOverlay = (isFullscreen, onExitFullscreen)=>{
|
|
4
|
+
const overlayRef = useRef(null);
|
|
5
|
+
// Mutable ref so the fullscreenchange listener can call the latest handler
|
|
6
|
+
// without re-binding when the consumer recreates its callback.
|
|
7
|
+
const onExitRef = useRef(onExitFullscreen);
|
|
8
|
+
onExitRef.current = onExitFullscreen;
|
|
9
|
+
useEffect(()=>{
|
|
10
|
+
const root = overlayRef.current;
|
|
11
|
+
if (!root) return;
|
|
12
|
+
if (isFullscreen && !document.fullscreenElement) {
|
|
13
|
+
// Some browsers reject requestFullscreen outside a user gesture.
|
|
14
|
+
root.requestFullscreen?.().catch(()=>{});
|
|
15
|
+
} else if (!isFullscreen && document.fullscreenElement === root) {
|
|
16
|
+
document.exitFullscreen?.().catch(()=>{});
|
|
17
|
+
}
|
|
18
|
+
if (!isFullscreen) return;
|
|
19
|
+
const onFsChange = ()=>{
|
|
20
|
+
if (!document.fullscreenElement) onExitRef.current();
|
|
21
|
+
};
|
|
22
|
+
document.addEventListener('fullscreenchange', onFsChange);
|
|
23
|
+
return ()=>document.removeEventListener('fullscreenchange', onFsChange);
|
|
24
|
+
}, [
|
|
25
|
+
isFullscreen
|
|
26
|
+
]);
|
|
27
|
+
return overlayRef;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
//# sourceMappingURL=useFullscreenOverlay.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/hooks/useFullscreenOverlay.ts"],"sourcesContent":["'use client'\n\nimport React, { useEffect, useRef } from 'react'\n\nexport const useFullscreenOverlay = (\n isFullscreen: boolean,\n onExitFullscreen: () => void,\n): React.RefObject<HTMLDivElement | null> => {\n const overlayRef = useRef<HTMLDivElement | null>(null)\n\n // Mutable ref so the fullscreenchange listener can call the latest handler\n // without re-binding when the consumer recreates its callback.\n const onExitRef = useRef(onExitFullscreen)\n onExitRef.current = onExitFullscreen\n\n useEffect(() => {\n const root = overlayRef.current\n if (!root) return\n\n if (isFullscreen && !document.fullscreenElement) {\n // Some browsers reject requestFullscreen outside a user gesture.\n root.requestFullscreen?.().catch(() => {})\n } else if (!isFullscreen && document.fullscreenElement === root) {\n document.exitFullscreen?.().catch(() => {})\n }\n\n if (!isFullscreen) return\n\n const onFsChange = () => {\n if (!document.fullscreenElement) onExitRef.current()\n }\n document.addEventListener('fullscreenchange', onFsChange)\n return () => document.removeEventListener('fullscreenchange', onFsChange)\n }, [isFullscreen])\n\n return overlayRef\n}\n"],"names":["React","useEffect","useRef","useFullscreenOverlay","isFullscreen","onExitFullscreen","overlayRef","onExitRef","current","root","document","fullscreenElement","requestFullscreen","catch","exitFullscreen","onFsChange","addEventListener","removeEventListener"],"mappings":"AAAA;AAEA,OAAOA,SAASC,SAAS,EAAEC,MAAM,QAAQ,QAAO;AAEhD,OAAO,MAAMC,uBAAuB,CAClCC,cACAC;IAEA,MAAMC,aAAaJ,OAA8B;IAEjD,2EAA2E;IAC3E,+DAA+D;IAC/D,MAAMK,YAAYL,OAAOG;IACzBE,UAAUC,OAAO,GAAGH;IAEpBJ,UAAU;QACR,MAAMQ,OAAOH,WAAWE,OAAO;QAC/B,IAAI,CAACC,MAAM;QAEX,IAAIL,gBAAgB,CAACM,SAASC,iBAAiB,EAAE;YAC/C,iEAAiE;YACjEF,KAAKG,iBAAiB,KAAKC,MAAM,KAAO;QAC1C,OAAO,IAAI,CAACT,gBAAgBM,SAASC,iBAAiB,KAAKF,MAAM;YAC/DC,SAASI,cAAc,KAAKD,MAAM,KAAO;QAC3C;QAEA,IAAI,CAACT,cAAc;QAEnB,MAAMW,aAAa;YACjB,IAAI,CAACL,SAASC,iBAAiB,EAAEJ,UAAUC,OAAO;QACpD;QACAE,SAASM,gBAAgB,CAAC,oBAAoBD;QAC9C,OAAO,IAAML,SAASO,mBAAmB,CAAC,oBAAoBF;IAChE,GAAG;QAACX;KAAa;IAEjB,OAAOE;AACT,EAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useEffect } from 'react';
|
|
3
|
+
export const useIframeResizeObserver = (iframeRef, onIframeWidthChange)=>{
|
|
4
|
+
useEffect(()=>{
|
|
5
|
+
const iframe = iframeRef.current;
|
|
6
|
+
if (!iframe || !onIframeWidthChange || typeof ResizeObserver === 'undefined') return;
|
|
7
|
+
onIframeWidthChange(iframe.clientWidth);
|
|
8
|
+
const ro = new ResizeObserver((entries)=>{
|
|
9
|
+
const w = entries[0]?.contentRect?.width;
|
|
10
|
+
if (typeof w === 'number') onIframeWidthChange(Math.round(w));
|
|
11
|
+
});
|
|
12
|
+
ro.observe(iframe);
|
|
13
|
+
return ()=>ro.disconnect();
|
|
14
|
+
}, [
|
|
15
|
+
iframeRef,
|
|
16
|
+
onIframeWidthChange
|
|
17
|
+
]);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
//# sourceMappingURL=useIframeResizeObserver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/hooks/useIframeResizeObserver.ts"],"sourcesContent":["'use client'\n\nimport { useEffect, type RefObject } from 'react'\n\nexport const useIframeResizeObserver = (\n iframeRef: RefObject<HTMLIFrameElement | null>,\n onIframeWidthChange?: (width: number) => void,\n): void => {\n useEffect(() => {\n const iframe = iframeRef.current\n if (!iframe || !onIframeWidthChange || typeof ResizeObserver === 'undefined') return\n onIframeWidthChange(iframe.clientWidth)\n const ro = new ResizeObserver((entries) => {\n const w = entries[0]?.contentRect?.width\n if (typeof w === 'number') onIframeWidthChange(Math.round(w))\n })\n ro.observe(iframe)\n return () => ro.disconnect()\n }, [iframeRef, onIframeWidthChange])\n}\n"],"names":["useEffect","useIframeResizeObserver","iframeRef","onIframeWidthChange","iframe","current","ResizeObserver","clientWidth","ro","entries","w","contentRect","width","Math","round","observe","disconnect"],"mappings":"AAAA;AAEA,SAASA,SAAS,QAAwB,QAAO;AAEjD,OAAO,MAAMC,0BAA0B,CACrCC,WACAC;IAEAH,UAAU;QACR,MAAMI,SAASF,UAAUG,OAAO;QAChC,IAAI,CAACD,UAAU,CAACD,uBAAuB,OAAOG,mBAAmB,aAAa;QAC9EH,oBAAoBC,OAAOG,WAAW;QACtC,MAAMC,KAAK,IAAIF,eAAe,CAACG;YAC7B,MAAMC,IAAID,OAAO,CAAC,EAAE,EAAEE,aAAaC;YACnC,IAAI,OAAOF,MAAM,UAAUP,oBAAoBU,KAAKC,KAAK,CAACJ;QAC5D;QACAF,GAAGO,OAAO,CAACX;QACX,OAAO,IAAMI,GAAGQ,UAAU;IAC5B,GAAG;QAACd;QAAWC;KAAoB;AACrC,EAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useRef } from 'react';
|
|
3
|
+
/**
|
|
4
|
+
* Mirror a value into a ref so long-lived listeners / effects can read the
|
|
5
|
+
* latest value without re-binding.
|
|
6
|
+
*/ export const useLatestRef = (value)=>{
|
|
7
|
+
const ref = useRef(value);
|
|
8
|
+
ref.current = value;
|
|
9
|
+
return ref;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
//# sourceMappingURL=useLatestRef.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/hooks/useLatestRef.ts"],"sourcesContent":["'use client'\n\nimport { useRef, type MutableRefObject } from 'react'\n\n/**\n * Mirror a value into a ref so long-lived listeners / effects can read the\n * latest value without re-binding.\n */\nexport const useLatestRef = <T>(value: T): MutableRefObject<T> => {\n const ref = useRef(value)\n ref.current = value\n return ref\n}\n"],"names":["useRef","useLatestRef","value","ref","current"],"mappings":"AAAA;AAEA,SAASA,MAAM,QAA+B,QAAO;AAErD;;;CAGC,GACD,OAAO,MAAMC,eAAe,CAAIC;IAC9B,MAAMC,MAAMH,OAAOE;IACnBC,IAAIC,OAAO,GAAGF;IACd,OAAOC;AACT,EAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const useMainWrapperPortal: (enabled: boolean, adminPortalSelector?: string) => HTMLElement | null;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
const DEFAULT_ADMIN_WRAPPER_SELECTOR = 'main[class*="collection-edit"] [class*="__main-wrapper"], main[class*="global-edit"] [class*="__main-wrapper"]';
|
|
4
|
+
// WeakSet so multiple plugin instances on the same origin each get their
|
|
5
|
+
// own warning instead of falling silent after the first one fires.
|
|
6
|
+
const warnedDocs = new WeakSet();
|
|
7
|
+
const resolveMountNode = (selector)=>{
|
|
8
|
+
if (typeof document === 'undefined') return null;
|
|
9
|
+
const target = document.querySelector(selector);
|
|
10
|
+
if (target) return target;
|
|
11
|
+
if (process.env.NODE_ENV !== 'production' && !warnedDocs.has(document)) {
|
|
12
|
+
warnedDocs.add(document);
|
|
13
|
+
console.warn(`[better-editor] No element matched "${selector}" — the Payload admin DOM may have changed. Falling back to <main> / <body>. You can override the selector via BetterEditorConfig.adminPortalSelector.`);
|
|
14
|
+
}
|
|
15
|
+
return document.querySelector('main') ?? document.body ?? null;
|
|
16
|
+
};
|
|
17
|
+
export const useMainWrapperPortal = (enabled, adminPortalSelector)=>{
|
|
18
|
+
const [mountNode, setMountNode] = useState(null);
|
|
19
|
+
useEffect(()=>{
|
|
20
|
+
if (!enabled || typeof document === 'undefined') return;
|
|
21
|
+
const main = resolveMountNode(adminPortalSelector ?? DEFAULT_ADMIN_WRAPPER_SELECTOR);
|
|
22
|
+
if (!main) return;
|
|
23
|
+
const html = document.documentElement;
|
|
24
|
+
const body = document.body;
|
|
25
|
+
const prev = {
|
|
26
|
+
position: main.style.position,
|
|
27
|
+
overflow: main.style.overflow,
|
|
28
|
+
height: main.style.height,
|
|
29
|
+
htmlOverflow: html.style.overflow,
|
|
30
|
+
bodyOverflow: body.style.overflow
|
|
31
|
+
};
|
|
32
|
+
if (!main.style.position) main.style.position = 'relative';
|
|
33
|
+
main.style.overflow = 'hidden';
|
|
34
|
+
// Lock outer page scroll so wheel events from iframe/sidebar don't
|
|
35
|
+
// also scroll the admin shell underneath.
|
|
36
|
+
html.style.overflow = 'hidden';
|
|
37
|
+
body.style.overflow = 'hidden';
|
|
38
|
+
// Without this clamp the wrapper grows with the underlying form
|
|
39
|
+
// (~thousands of pixels) and the iframe + sidebar never get their
|
|
40
|
+
// own scroll containers.
|
|
41
|
+
const updateHeight = ()=>{
|
|
42
|
+
const top = main.getBoundingClientRect().top;
|
|
43
|
+
main.style.height = `${Math.max(0, window.innerHeight - top)}px`;
|
|
44
|
+
};
|
|
45
|
+
updateHeight();
|
|
46
|
+
window.addEventListener('resize', updateHeight);
|
|
47
|
+
setMountNode(main);
|
|
48
|
+
return ()=>{
|
|
49
|
+
window.removeEventListener('resize', updateHeight);
|
|
50
|
+
main.style.position = prev.position;
|
|
51
|
+
main.style.overflow = prev.overflow;
|
|
52
|
+
main.style.height = prev.height;
|
|
53
|
+
html.style.overflow = prev.htmlOverflow;
|
|
54
|
+
body.style.overflow = prev.bodyOverflow;
|
|
55
|
+
setMountNode(null);
|
|
56
|
+
};
|
|
57
|
+
}, [
|
|
58
|
+
enabled,
|
|
59
|
+
adminPortalSelector
|
|
60
|
+
]);
|
|
61
|
+
return mountNode;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
//# sourceMappingURL=useMainWrapperPortal.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/hooks/useMainWrapperPortal.ts"],"sourcesContent":["'use client'\n\nimport { useEffect, useState } from 'react'\n\nconst DEFAULT_ADMIN_WRAPPER_SELECTOR =\n 'main[class*=\"collection-edit\"] [class*=\"__main-wrapper\"], main[class*=\"global-edit\"] [class*=\"__main-wrapper\"]'\n\n// WeakSet so multiple plugin instances on the same origin each get their\n// own warning instead of falling silent after the first one fires.\nconst warnedDocs = new WeakSet<Document>()\n\nconst resolveMountNode = (selector: string): HTMLElement | null => {\n if (typeof document === 'undefined') return null\n const target = document.querySelector<HTMLElement>(selector)\n if (target) return target\n\n if (process.env.NODE_ENV !== 'production' && !warnedDocs.has(document)) {\n warnedDocs.add(document)\n\n console.warn(\n `[better-editor] No element matched \"${selector}\" — the Payload admin DOM may have changed. Falling back to <main> / <body>. You can override the selector via BetterEditorConfig.adminPortalSelector.`,\n )\n }\n\n return document.querySelector<HTMLElement>('main') ?? document.body ?? null\n}\n\nexport const useMainWrapperPortal = (\n enabled: boolean,\n adminPortalSelector?: string,\n): HTMLElement | null => {\n const [mountNode, setMountNode] = useState<HTMLElement | null>(null)\n\n useEffect(() => {\n if (!enabled || typeof document === 'undefined') return\n\n const main = resolveMountNode(adminPortalSelector ?? DEFAULT_ADMIN_WRAPPER_SELECTOR)\n if (!main) return\n\n const html = document.documentElement\n const body = document.body\n\n const prev = {\n position: main.style.position,\n overflow: main.style.overflow,\n height: main.style.height,\n htmlOverflow: html.style.overflow,\n bodyOverflow: body.style.overflow,\n }\n\n if (!main.style.position) main.style.position = 'relative'\n main.style.overflow = 'hidden'\n\n // Lock outer page scroll so wheel events from iframe/sidebar don't\n // also scroll the admin shell underneath.\n html.style.overflow = 'hidden'\n body.style.overflow = 'hidden'\n\n // Without this clamp the wrapper grows with the underlying form\n // (~thousands of pixels) and the iframe + sidebar never get their\n // own scroll containers.\n const updateHeight = () => {\n const top = main.getBoundingClientRect().top\n main.style.height = `${Math.max(0, window.innerHeight - top)}px`\n }\n updateHeight()\n window.addEventListener('resize', updateHeight)\n\n setMountNode(main)\n\n return () => {\n window.removeEventListener('resize', updateHeight)\n main.style.position = prev.position\n main.style.overflow = prev.overflow\n main.style.height = prev.height\n html.style.overflow = prev.htmlOverflow\n body.style.overflow = prev.bodyOverflow\n setMountNode(null)\n }\n }, [enabled, adminPortalSelector])\n\n return mountNode\n}\n"],"names":["useEffect","useState","DEFAULT_ADMIN_WRAPPER_SELECTOR","warnedDocs","WeakSet","resolveMountNode","selector","document","target","querySelector","process","env","NODE_ENV","has","add","console","warn","body","useMainWrapperPortal","enabled","adminPortalSelector","mountNode","setMountNode","main","html","documentElement","prev","position","style","overflow","height","htmlOverflow","bodyOverflow","updateHeight","top","getBoundingClientRect","Math","max","window","innerHeight","addEventListener","removeEventListener"],"mappings":"AAAA;AAEA,SAASA,SAAS,EAAEC,QAAQ,QAAQ,QAAO;AAE3C,MAAMC,iCACJ;AAEF,yEAAyE;AACzE,mEAAmE;AACnE,MAAMC,aAAa,IAAIC;AAEvB,MAAMC,mBAAmB,CAACC;IACxB,IAAI,OAAOC,aAAa,aAAa,OAAO;IAC5C,MAAMC,SAASD,SAASE,aAAa,CAAcH;IACnD,IAAIE,QAAQ,OAAOA;IAEnB,IAAIE,QAAQC,GAAG,CAACC,QAAQ,KAAK,gBAAgB,CAACT,WAAWU,GAAG,CAACN,WAAW;QACtEJ,WAAWW,GAAG,CAACP;QAEfQ,QAAQC,IAAI,CACV,CAAC,oCAAoC,EAAEV,SAAS,sJAAsJ,CAAC;IAE3M;IAEA,OAAOC,SAASE,aAAa,CAAc,WAAWF,SAASU,IAAI,IAAI;AACzE;AAEA,OAAO,MAAMC,uBAAuB,CAClCC,SACAC;IAEA,MAAM,CAACC,WAAWC,aAAa,GAAGrB,SAA6B;IAE/DD,UAAU;QACR,IAAI,CAACmB,WAAW,OAAOZ,aAAa,aAAa;QAEjD,MAAMgB,OAAOlB,iBAAiBe,uBAAuBlB;QACrD,IAAI,CAACqB,MAAM;QAEX,MAAMC,OAAOjB,SAASkB,eAAe;QACrC,MAAMR,OAAOV,SAASU,IAAI;QAE1B,MAAMS,OAAO;YACXC,UAAUJ,KAAKK,KAAK,CAACD,QAAQ;YAC7BE,UAAUN,KAAKK,KAAK,CAACC,QAAQ;YAC7BC,QAAQP,KAAKK,KAAK,CAACE,MAAM;YACzBC,cAAcP,KAAKI,KAAK,CAACC,QAAQ;YACjCG,cAAcf,KAAKW,KAAK,CAACC,QAAQ;QACnC;QAEA,IAAI,CAACN,KAAKK,KAAK,CAACD,QAAQ,EAAEJ,KAAKK,KAAK,CAACD,QAAQ,GAAG;QAChDJ,KAAKK,KAAK,CAACC,QAAQ,GAAG;QAEtB,mEAAmE;QACnE,0CAA0C;QAC1CL,KAAKI,KAAK,CAACC,QAAQ,GAAG;QACtBZ,KAAKW,KAAK,CAACC,QAAQ,GAAG;QAEtB,gEAAgE;QAChE,kEAAkE;QAClE,yBAAyB;QACzB,MAAMI,eAAe;YACnB,MAAMC,MAAMX,KAAKY,qBAAqB,GAAGD,GAAG;YAC5CX,KAAKK,KAAK,CAACE,MAAM,GAAG,GAAGM,KAAKC,GAAG,CAAC,GAAGC,OAAOC,WAAW,GAAGL,KAAK,EAAE,CAAC;QAClE;QACAD;QACAK,OAAOE,gBAAgB,CAAC,UAAUP;QAElCX,aAAaC;QAEb,OAAO;YACLe,OAAOG,mBAAmB,CAAC,UAAUR;YACrCV,KAAKK,KAAK,CAACD,QAAQ,GAAGD,KAAKC,QAAQ;YACnCJ,KAAKK,KAAK,CAACC,QAAQ,GAAGH,KAAKG,QAAQ;YACnCN,KAAKK,KAAK,CAACE,MAAM,GAAGJ,KAAKI,MAAM;YAC/BN,KAAKI,KAAK,CAACC,QAAQ,GAAGH,KAAKK,YAAY;YACvCd,KAAKW,KAAK,CAACC,QAAQ,GAAGH,KAAKM,YAAY;YACvCV,aAAa;QACf;IACF,GAAG;QAACH;QAASC;KAAoB;IAEjC,OAAOC;AACT,EAAC"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { useEditorHistory } from '../state/useEditorHistory';
|
|
2
|
+
export type UseOverlayKeyboardArgs = {
|
|
3
|
+
onClose: () => void;
|
|
4
|
+
history: ReturnType<typeof useEditorHistory>;
|
|
5
|
+
};
|
|
6
|
+
export declare const useOverlayKeyboard: ({ onClose, history }: UseOverlayKeyboardArgs) => void;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useEffect } from 'react';
|
|
3
|
+
import { useLatestRef } from './useLatestRef';
|
|
4
|
+
const EDITABLE_TAGS = new Set([
|
|
5
|
+
'INPUT',
|
|
6
|
+
'TEXTAREA',
|
|
7
|
+
'SELECT'
|
|
8
|
+
]);
|
|
9
|
+
// Don't hijack Escape away from text inputs / native dropdowns / rich text
|
|
10
|
+
// editors — users expect it to clear/blur the field first.
|
|
11
|
+
const isEditableTarget = (el)=>!!el && (EDITABLE_TAGS.has(el.tagName) || el.isContentEditable === true);
|
|
12
|
+
export const useOverlayKeyboard = ({ onClose, history })=>{
|
|
13
|
+
const handlersRef = useLatestRef({
|
|
14
|
+
onClose,
|
|
15
|
+
history
|
|
16
|
+
});
|
|
17
|
+
useEffect(()=>{
|
|
18
|
+
const onKey = (e)=>{
|
|
19
|
+
if (e.key === 'Escape') {
|
|
20
|
+
if (isEditableTarget(document.activeElement)) return;
|
|
21
|
+
handlersRef.current.onClose();
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (!(e.metaKey || e.ctrlKey)) return;
|
|
25
|
+
const k = e.key.toLowerCase();
|
|
26
|
+
if (k === 'z') {
|
|
27
|
+
e.preventDefault();
|
|
28
|
+
const { history: h } = handlersRef.current;
|
|
29
|
+
if (e.shiftKey) h.redo();
|
|
30
|
+
else h.undo();
|
|
31
|
+
} else if (k === 'y') {
|
|
32
|
+
e.preventDefault();
|
|
33
|
+
handlersRef.current.history.redo();
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
window.addEventListener('keydown', onKey);
|
|
37
|
+
return ()=>window.removeEventListener('keydown', onKey);
|
|
38
|
+
}, [
|
|
39
|
+
handlersRef
|
|
40
|
+
]);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
//# sourceMappingURL=useOverlayKeyboard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/hooks/useOverlayKeyboard.ts"],"sourcesContent":["'use client'\n\nimport { useEffect } from 'react'\nimport { useEditorHistory } from '../state/useEditorHistory'\nimport { useLatestRef } from './useLatestRef'\n\nexport type UseOverlayKeyboardArgs = {\n onClose: () => void\n history: ReturnType<typeof useEditorHistory>\n}\n\nconst EDITABLE_TAGS = new Set(['INPUT', 'TEXTAREA', 'SELECT'])\n\n// Don't hijack Escape away from text inputs / native dropdowns / rich text\n// editors — users expect it to clear/blur the field first.\nconst isEditableTarget = (el: Element | null): boolean =>\n !!el && (EDITABLE_TAGS.has(el.tagName) || (el as HTMLElement).isContentEditable === true)\n\nexport const useOverlayKeyboard = ({ onClose, history }: UseOverlayKeyboardArgs): void => {\n const handlersRef = useLatestRef({ onClose, history })\n\n useEffect(() => {\n const onKey = (e: KeyboardEvent) => {\n if (e.key === 'Escape') {\n if (isEditableTarget(document.activeElement)) return\n handlersRef.current.onClose()\n return\n }\n if (!(e.metaKey || e.ctrlKey)) return\n const k = e.key.toLowerCase()\n if (k === 'z') {\n e.preventDefault()\n const { history: h } = handlersRef.current\n if (e.shiftKey) h.redo()\n else h.undo()\n } else if (k === 'y') {\n e.preventDefault()\n handlersRef.current.history.redo()\n }\n }\n window.addEventListener('keydown', onKey)\n return () => window.removeEventListener('keydown', onKey)\n }, [handlersRef])\n}\n"],"names":["useEffect","useLatestRef","EDITABLE_TAGS","Set","isEditableTarget","el","has","tagName","isContentEditable","useOverlayKeyboard","onClose","history","handlersRef","onKey","e","key","document","activeElement","current","metaKey","ctrlKey","k","toLowerCase","preventDefault","h","shiftKey","redo","undo","window","addEventListener","removeEventListener"],"mappings":"AAAA;AAEA,SAASA,SAAS,QAAQ,QAAO;AAEjC,SAASC,YAAY,QAAQ,iBAAgB;AAO7C,MAAMC,gBAAgB,IAAIC,IAAI;IAAC;IAAS;IAAY;CAAS;AAE7D,2EAA2E;AAC3E,2DAA2D;AAC3D,MAAMC,mBAAmB,CAACC,KACxB,CAAC,CAACA,MAAOH,CAAAA,cAAcI,GAAG,CAACD,GAAGE,OAAO,KAAK,AAACF,GAAmBG,iBAAiB,KAAK,IAAG;AAEzF,OAAO,MAAMC,qBAAqB,CAAC,EAAEC,OAAO,EAAEC,OAAO,EAA0B;IAC7E,MAAMC,cAAcX,aAAa;QAAES;QAASC;IAAQ;IAEpDX,UAAU;QACR,MAAMa,QAAQ,CAACC;YACb,IAAIA,EAAEC,GAAG,KAAK,UAAU;gBACtB,IAAIX,iBAAiBY,SAASC,aAAa,GAAG;gBAC9CL,YAAYM,OAAO,CAACR,OAAO;gBAC3B;YACF;YACA,IAAI,CAAEI,CAAAA,EAAEK,OAAO,IAAIL,EAAEM,OAAO,AAAD,GAAI;YAC/B,MAAMC,IAAIP,EAAEC,GAAG,CAACO,WAAW;YAC3B,IAAID,MAAM,KAAK;gBACbP,EAAES,cAAc;gBAChB,MAAM,EAAEZ,SAASa,CAAC,EAAE,GAAGZ,YAAYM,OAAO;gBAC1C,IAAIJ,EAAEW,QAAQ,EAAED,EAAEE,IAAI;qBACjBF,EAAEG,IAAI;YACb,OAAO,IAAIN,MAAM,KAAK;gBACpBP,EAAES,cAAc;gBAChBX,YAAYM,OAAO,CAACP,OAAO,CAACe,IAAI;YAClC;QACF;QACAE,OAAOC,gBAAgB,CAAC,WAAWhB;QACnC,OAAO,IAAMe,OAAOE,mBAAmB,CAAC,WAAWjB;IACrD,GAAG;QAACD;KAAY;AAClB,EAAC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type RefObject } from 'react';
|
|
2
|
+
import { HoverToolbarController } from '../preview/HoverToolbarController';
|
|
3
|
+
import type { BlockActionMessage } from '../preview/protocol';
|
|
4
|
+
export type PreviewBindingSettings = {
|
|
5
|
+
hoverColorTopLevel: string;
|
|
6
|
+
hoverColorNested: string;
|
|
7
|
+
hoverOutlineWidth: number;
|
|
8
|
+
showHoverToolbar: boolean;
|
|
9
|
+
hoverToolbarPosition: import('../internal/constants').HoverToolbarPosition;
|
|
10
|
+
};
|
|
11
|
+
export type UsePreviewBindingArgs = {
|
|
12
|
+
iframeRef: RefObject<HTMLIFrameElement | null>;
|
|
13
|
+
settings: PreviewBindingSettings;
|
|
14
|
+
interactModeRef: RefObject<boolean>;
|
|
15
|
+
onFocusBlock: (id: string) => void;
|
|
16
|
+
onBlockAction: (id: string, action: BlockActionMessage['action']) => void;
|
|
17
|
+
onLoadingChange: (loading: boolean) => void;
|
|
18
|
+
};
|
|
19
|
+
export type UsePreviewBindingReturn = {
|
|
20
|
+
controllerRef: RefObject<HoverToolbarController | null>;
|
|
21
|
+
isBoundRef: RefObject<boolean>;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Owns the iframe load → install styles + click handler + hover toolbar
|
|
25
|
+
* lifecycle. Idempotent: tears down previous bindings before installing
|
|
26
|
+
* new ones, and unbinds on unmount.
|
|
27
|
+
*/
|
|
28
|
+
export declare const usePreviewBinding: ({ iframeRef, settings, interactModeRef, onFocusBlock, onBlockAction, onLoadingChange, }: UsePreviewBindingArgs) => UsePreviewBindingReturn;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useCallback, useEffect, useRef } from 'react';
|
|
3
|
+
import { HoverToolbarController } from '../preview/HoverToolbarController';
|
|
4
|
+
import { installClickToFocus } from '../preview/installClickToFocus';
|
|
5
|
+
import { installHoverStyles } from '../preview/installHoverStyles';
|
|
6
|
+
import { BLOCK_ID_SELECTOR } from '../internal/dom';
|
|
7
|
+
import { getSameOriginDocument } from '../internal/iframe';
|
|
8
|
+
import { useLatestRef } from './useLatestRef';
|
|
9
|
+
/**
|
|
10
|
+
* Owns the iframe load → install styles + click handler + hover toolbar
|
|
11
|
+
* lifecycle. Idempotent: tears down previous bindings before installing
|
|
12
|
+
* new ones, and unbinds on unmount.
|
|
13
|
+
*/ export const usePreviewBinding = ({ iframeRef, settings, interactModeRef, onFocusBlock, onBlockAction, onLoadingChange })=>{
|
|
14
|
+
const teardownRef = useRef(null);
|
|
15
|
+
const controllerRef = useRef(null);
|
|
16
|
+
const isBoundRef = useRef(false);
|
|
17
|
+
// One-shot flags so dev-only console warnings don't repeat on every
|
|
18
|
+
// iframe re-load during a single editor session.
|
|
19
|
+
const warnedMissingBlocksRef = useRef(false);
|
|
20
|
+
const warnedCrossOriginRef = useRef(false);
|
|
21
|
+
const settingsRef = useLatestRef(settings);
|
|
22
|
+
const onFocusBlockRef = useLatestRef(onFocusBlock);
|
|
23
|
+
const onBlockActionRef = useLatestRef(onBlockAction);
|
|
24
|
+
const onLoadingChangeRef = useLatestRef(onLoadingChange);
|
|
25
|
+
const bindToDocument = useCallback((doc)=>{
|
|
26
|
+
teardownRef.current?.();
|
|
27
|
+
controllerRef.current?.destroy();
|
|
28
|
+
controllerRef.current = null;
|
|
29
|
+
const s = settingsRef.current;
|
|
30
|
+
const removeStyles = installHoverStyles(doc, {
|
|
31
|
+
topColor: s.hoverColorTopLevel,
|
|
32
|
+
nestedColor: s.hoverColorNested,
|
|
33
|
+
outlineWidth: s.hoverOutlineWidth
|
|
34
|
+
});
|
|
35
|
+
const removeClick = installClickToFocus(doc, (id)=>onFocusBlockRef.current(id), {
|
|
36
|
+
isEnabled: ()=>!interactModeRef.current
|
|
37
|
+
});
|
|
38
|
+
if (s.showHoverToolbar) {
|
|
39
|
+
controllerRef.current = new HoverToolbarController(doc, {
|
|
40
|
+
position: s.hoverToolbarPosition,
|
|
41
|
+
outlineWidth: s.hoverOutlineWidth,
|
|
42
|
+
onAction: (id, action)=>onBlockActionRef.current(id, action)
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
// Dev-only sanity check: zero [data-better-editor-id] elements means
|
|
46
|
+
// the consumer forgot to spread getBlockProps() on their block wrappers
|
|
47
|
+
// (or the page just has no blocks yet — both look identical from here).
|
|
48
|
+
// Warn at most once per editor session to avoid console spam.
|
|
49
|
+
if (process.env.NODE_ENV !== 'production' && !warnedMissingBlocksRef.current) {
|
|
50
|
+
const blockCount = doc.querySelectorAll(BLOCK_ID_SELECTOR).length;
|
|
51
|
+
if (blockCount === 0) {
|
|
52
|
+
warnedMissingBlocksRef.current = true;
|
|
53
|
+
console.warn("[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.");
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
isBoundRef.current = true;
|
|
57
|
+
teardownRef.current = ()=>{
|
|
58
|
+
removeStyles();
|
|
59
|
+
removeClick();
|
|
60
|
+
controllerRef.current?.destroy();
|
|
61
|
+
controllerRef.current = null;
|
|
62
|
+
isBoundRef.current = false;
|
|
63
|
+
};
|
|
64
|
+
}, // eslint-disable-next-line react-hooks/exhaustive-deps -- refs are stable
|
|
65
|
+
[]);
|
|
66
|
+
// Bind once on mount; teardown on unmount. All inputs flow through
|
|
67
|
+
// stable refs, so the load listener doesn't need to re-attach.
|
|
68
|
+
useEffect(()=>{
|
|
69
|
+
const iframe = iframeRef.current;
|
|
70
|
+
if (!iframe) return;
|
|
71
|
+
const onLoad = ()=>{
|
|
72
|
+
const doc = getSameOriginDocument(iframe);
|
|
73
|
+
if (!doc) {
|
|
74
|
+
onLoadingChangeRef.current(false);
|
|
75
|
+
if (process.env.NODE_ENV !== 'production' && !warnedCrossOriginRef.current) {
|
|
76
|
+
warnedCrossOriginRef.current = true;
|
|
77
|
+
console.warn('[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.');
|
|
78
|
+
}
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
// Fresh iframes report readyState='complete' on their initial
|
|
82
|
+
// `about:blank` document before `src` has navigated. Skip and wait
|
|
83
|
+
// for the real `load` event so we don't bind to an empty body.
|
|
84
|
+
const href = doc.location.href;
|
|
85
|
+
if (!href || href === 'about:blank') return;
|
|
86
|
+
onLoadingChangeRef.current(false);
|
|
87
|
+
bindToDocument(doc);
|
|
88
|
+
};
|
|
89
|
+
if (iframe.contentDocument && iframe.contentDocument.readyState === 'complete') {
|
|
90
|
+
onLoad();
|
|
91
|
+
}
|
|
92
|
+
iframe.addEventListener('load', onLoad);
|
|
93
|
+
return ()=>{
|
|
94
|
+
iframe.removeEventListener('load', onLoad);
|
|
95
|
+
teardownRef.current?.();
|
|
96
|
+
teardownRef.current = null;
|
|
97
|
+
controllerRef.current?.destroy();
|
|
98
|
+
controllerRef.current = null;
|
|
99
|
+
};
|
|
100
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- refs are stable
|
|
101
|
+
}, []);
|
|
102
|
+
return {
|
|
103
|
+
controllerRef,
|
|
104
|
+
isBoundRef
|
|
105
|
+
};
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
//# sourceMappingURL=usePreviewBinding.js.map
|
|
@@ -0,0 +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 const href = doc.location.href\n if (!href || href === 'about:blank') return\n onLoadingChangeRef.current(false)\n bindToDocument(doc)\n }\n\n if (iframe.contentDocument && iframe.contentDocument.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","href","location","contentDocument","readyState","addEventListener","removeEventListener"],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
export type UsePreviewHandleDragOptions = {
|
|
3
|
+
resizable: boolean;
|
|
4
|
+
viewportWidth?: number | null;
|
|
5
|
+
onResize?: (next: number) => void;
|
|
6
|
+
};
|
|
7
|
+
export type UsePreviewHandleDragReturn = {
|
|
8
|
+
isResizing: boolean;
|
|
9
|
+
onHandleMouseDown: (side: 'left' | 'right') => (e: React.MouseEvent) => void;
|
|
10
|
+
};
|
|
11
|
+
export declare const usePreviewHandleDrag: ({ resizable, viewportWidth, onResize, }: UsePreviewHandleDragOptions) => UsePreviewHandleDragReturn;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { clampViewport } from '../internal/limits';
|
|
4
|
+
export const usePreviewHandleDrag = ({ resizable, viewportWidth, onResize })=>{
|
|
5
|
+
const [isResizing, setIsResizing] = useState(false);
|
|
6
|
+
// Track in-flight drag so unmount mid-drag can release body styles + listeners.
|
|
7
|
+
const dragCleanupRef = useRef(null);
|
|
8
|
+
const isMountedRef = useRef(true);
|
|
9
|
+
useEffect(()=>{
|
|
10
|
+
isMountedRef.current = true;
|
|
11
|
+
return ()=>{
|
|
12
|
+
isMountedRef.current = false;
|
|
13
|
+
dragCleanupRef.current?.();
|
|
14
|
+
};
|
|
15
|
+
}, []);
|
|
16
|
+
const onHandleMouseDown = useCallback((side)=>(e)=>{
|
|
17
|
+
if (!resizable || !onResize || !viewportWidth) return;
|
|
18
|
+
e.preventDefault();
|
|
19
|
+
const startX = e.clientX;
|
|
20
|
+
const startWidth = viewportWidth;
|
|
21
|
+
// Iframe is centered; dragging either edge by N px symmetrically grows
|
|
22
|
+
// the width by 2N. Right handle: positive delta increases width.
|
|
23
|
+
const dir = side === 'right' ? 2 : -2;
|
|
24
|
+
setIsResizing(true);
|
|
25
|
+
const onMove = (ev)=>{
|
|
26
|
+
onResize(clampViewport(startWidth + (ev.clientX - startX) * dir));
|
|
27
|
+
};
|
|
28
|
+
const cleanup = ()=>{
|
|
29
|
+
window.removeEventListener('mousemove', onMove);
|
|
30
|
+
window.removeEventListener('mouseup', onUp);
|
|
31
|
+
document.body.style.cursor = '';
|
|
32
|
+
document.body.style.userSelect = '';
|
|
33
|
+
if (isMountedRef.current) setIsResizing(false);
|
|
34
|
+
dragCleanupRef.current = null;
|
|
35
|
+
};
|
|
36
|
+
const onUp = ()=>cleanup();
|
|
37
|
+
dragCleanupRef.current = cleanup;
|
|
38
|
+
document.body.style.cursor = 'ew-resize';
|
|
39
|
+
document.body.style.userSelect = 'none';
|
|
40
|
+
window.addEventListener('mousemove', onMove);
|
|
41
|
+
window.addEventListener('mouseup', onUp);
|
|
42
|
+
}, [
|
|
43
|
+
resizable,
|
|
44
|
+
onResize,
|
|
45
|
+
viewportWidth
|
|
46
|
+
]);
|
|
47
|
+
return {
|
|
48
|
+
isResizing,
|
|
49
|
+
onHandleMouseDown
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
//# sourceMappingURL=usePreviewHandleDrag.js.map
|