@payload-onpage/react 0.2.1 → 0.3.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/dist/OnPageClient.d.ts.map +1 -1
- package/dist/OnPageClient.js +9 -5
- package/dist/OnPageClient.js.map +1 -1
- package/dist/annotations.d.ts +1 -1
- package/dist/annotations.d.ts.map +1 -1
- package/dist/context.d.ts +19 -5
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +31 -7
- package/dist/context.js.map +1 -1
- package/dist/dom.d.ts +16 -12
- package/dist/dom.d.ts.map +1 -1
- package/dist/dom.js +16 -16
- package/dist/dom.js.map +1 -1
- package/dist/editor/FieldHost.d.ts.map +1 -1
- package/dist/editor/FieldHost.js +18 -11
- package/dist/editor/FieldHost.js.map +1 -1
- package/dist/editor/OnPageEditor.d.ts +15 -8
- package/dist/editor/OnPageEditor.d.ts.map +1 -1
- package/dist/editor/OnPageEditor.js +73 -103
- package/dist/editor/OnPageEditor.js.map +1 -1
- package/dist/editor/editTarget.d.ts +28 -0
- package/dist/editor/editTarget.d.ts.map +1 -0
- package/dist/editor/editTarget.js +20 -0
- package/dist/editor/editTarget.js.map +1 -0
- package/dist/editor/useClientDocs.d.ts +4 -0
- package/dist/editor/useClientDocs.d.ts.map +1 -0
- package/dist/editor/useClientDocs.js +16 -0
- package/dist/editor/useClientDocs.js.map +1 -0
- package/dist/editor/useEditorSession.d.ts +13 -0
- package/dist/editor/useEditorSession.d.ts.map +1 -0
- package/dist/editor/useEditorSession.js +30 -0
- package/dist/editor/useEditorSession.js.map +1 -0
- package/dist/editor/useFlash.d.ts +4 -0
- package/dist/editor/useFlash.d.ts.map +1 -0
- package/dist/editor/useFlash.js +16 -0
- package/dist/editor/useFlash.js.map +1 -0
- package/dist/editor/useGhosts.d.ts +5 -7
- package/dist/editor/useGhosts.d.ts.map +1 -1
- package/dist/editor/useGhosts.js.map +1 -1
- package/dist/editor/usePageEvents.d.ts.map +1 -1
- package/dist/editor/usePageEvents.js +16 -16
- package/dist/editor/usePageEvents.js.map +1 -1
- package/dist/editor-kit.d.ts +10 -6
- package/dist/editor-kit.d.ts.map +1 -1
- package/dist/editor-kit.js +8 -5
- package/dist/editor-kit.js.map +1 -1
- package/dist/editors/CheckboxEditor.d.ts +6 -1
- package/dist/editors/CheckboxEditor.d.ts.map +1 -1
- package/dist/editors/CheckboxEditor.js +6 -3
- package/dist/editors/CheckboxEditor.js.map +1 -1
- package/dist/editors/ColorEditor.d.ts +3 -1
- package/dist/editors/ColorEditor.d.ts.map +1 -1
- package/dist/editors/ColorEditor.js +9 -15
- package/dist/editors/ColorEditor.js.map +1 -1
- package/dist/editors/DateEditor.d.ts +3 -1
- package/dist/editors/DateEditor.d.ts.map +1 -1
- package/dist/editors/DateEditor.js +10 -12
- package/dist/editors/DateEditor.js.map +1 -1
- package/dist/editors/InlineText.d.ts +2 -2
- package/dist/editors/InlineText.d.ts.map +1 -1
- package/dist/editors/InlineText.js +2 -3
- package/dist/editors/InlineText.js.map +1 -1
- package/dist/editors/NumberEditor.d.ts +3 -1
- package/dist/editors/NumberEditor.d.ts.map +1 -1
- package/dist/editors/NumberEditor.js +9 -17
- package/dist/editors/NumberEditor.js.map +1 -1
- package/dist/editors/RelationshipEditor.d.ts +7 -1
- package/dist/editors/RelationshipEditor.d.ts.map +1 -1
- package/dist/editors/RelationshipEditor.js +10 -18
- package/dist/editors/RelationshipEditor.js.map +1 -1
- package/dist/editors/SelectEditor.d.ts +4 -1
- package/dist/editors/SelectEditor.d.ts.map +1 -1
- package/dist/editors/SelectEditor.js +8 -13
- package/dist/editors/SelectEditor.js.map +1 -1
- package/dist/editors/UploadEditor.d.ts +7 -1
- package/dist/editors/UploadEditor.d.ts.map +1 -1
- package/dist/editors/UploadEditor.js +5 -5
- package/dist/editors/UploadEditor.js.map +1 -1
- package/dist/editors/index.d.ts +0 -1
- package/dist/editors/index.d.ts.map +1 -1
- package/dist/editors/index.js +56 -16
- package/dist/editors/index.js.map +1 -1
- package/dist/editors/kit.d.ts +39 -0
- package/dist/editors/kit.d.ts.map +1 -0
- package/dist/editors/kit.js +44 -0
- package/dist/editors/kit.js.map +1 -0
- package/dist/editors/useDebouncedSearch.d.ts +10 -6
- package/dist/editors/useDebouncedSearch.d.ts.map +1 -1
- package/dist/editors/useDebouncedSearch.js +11 -7
- package/dist/editors/useDebouncedSearch.js.map +1 -1
- package/dist/geometry.d.ts +49 -0
- package/dist/geometry.d.ts.map +1 -0
- package/dist/geometry.js +48 -0
- package/dist/geometry.js.map +1 -0
- package/dist/hub.d.ts +4 -4
- package/dist/hub.d.ts.map +1 -1
- package/dist/hub.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/layout.d.ts.map +1 -1
- package/dist/layout.js +4 -2
- package/dist/layout.js.map +1 -1
- package/dist/locators.d.ts +0 -2
- package/dist/locators.d.ts.map +1 -1
- package/dist/locators.js +10 -16
- package/dist/locators.js.map +1 -1
- package/dist/messages.d.ts +15 -2
- package/dist/messages.d.ts.map +1 -1
- package/dist/messages.js +5 -0
- package/dist/messages.js.map +1 -1
- package/dist/registry.d.ts +21 -2
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +31 -6
- package/dist/registry.js.map +1 -1
- package/dist/rowOps.d.ts +20 -0
- package/dist/rowOps.d.ts.map +1 -0
- package/dist/rowOps.js +48 -0
- package/dist/rowOps.js.map +1 -0
- package/dist/sanitize.d.ts +2 -0
- package/dist/sanitize.d.ts.map +1 -1
- package/dist/sanitize.js +3 -0
- package/dist/sanitize.js.map +1 -1
- package/dist/settings.d.ts +10 -0
- package/dist/settings.d.ts.map +1 -0
- package/dist/settings.js +30 -0
- package/dist/settings.js.map +1 -0
- package/dist/styles.d.ts +0 -4
- package/dist/styles.d.ts.map +1 -1
- package/dist/styles.js +2 -1
- package/dist/styles.js.map +1 -1
- package/dist/targets.d.ts +1 -1
- package/dist/targets.d.ts.map +1 -1
- package/dist/types.d.ts +71 -46
- package/dist/types.d.ts.map +1 -1
- package/dist/ui/BlockPicker.d.ts.map +1 -1
- package/dist/ui/BlockPicker.js +6 -5
- package/dist/ui/BlockPicker.js.map +1 -1
- package/dist/ui/Dialog.d.ts +0 -5
- package/dist/ui/Dialog.d.ts.map +1 -1
- package/dist/ui/Dialog.js +10 -6
- package/dist/ui/Dialog.js.map +1 -1
- package/dist/ui/Dock.d.ts +1 -6
- package/dist/ui/Dock.d.ts.map +1 -1
- package/dist/ui/Dock.js +46 -28
- package/dist/ui/Dock.js.map +1 -1
- package/dist/ui/Overlay.d.ts.map +1 -1
- package/dist/ui/Overlay.js +39 -37
- package/dist/ui/Overlay.js.map +1 -1
- package/dist/ui/PreviewToolbar.d.ts +3 -3
- package/dist/ui/PreviewToolbar.d.ts.map +1 -1
- package/dist/ui/PreviewToolbar.js +4 -6
- package/dist/ui/PreviewToolbar.js.map +1 -1
- package/dist/ui/SettingsPanel.d.ts +0 -9
- package/dist/ui/SettingsPanel.d.ts.map +1 -1
- package/dist/ui/SettingsPanel.js +36 -41
- package/dist/ui/SettingsPanel.js.map +1 -1
- package/dist/ui/ToolbarButton.d.ts +12 -0
- package/dist/ui/ToolbarButton.d.ts.map +1 -0
- package/dist/ui/ToolbarButton.js +7 -0
- package/dist/ui/ToolbarButton.js.map +1 -0
- package/dist/useEvent.d.ts +1 -1
- package/dist/useEvent.d.ts.map +1 -1
- package/dist/useEvent.js +3 -1
- package/dist/useEvent.js.map +1 -1
- package/dist/useIsClient.d.ts +5 -0
- package/dist/useIsClient.d.ts.map +1 -0
- package/dist/useIsClient.js +8 -0
- package/dist/useIsClient.js.map +1 -0
- package/package.json +2 -2
- package/src/OnPageClient.tsx +9 -5
- package/src/annotations.ts +1 -1
- package/src/context.tsx +53 -10
- package/src/dom.ts +17 -18
- package/src/editor/FieldHost.tsx +26 -11
- package/src/editor/OnPageEditor.tsx +103 -160
- package/src/editor/editTarget.ts +41 -0
- package/src/editor/useClientDocs.ts +19 -0
- package/src/editor/useEditorSession.ts +36 -0
- package/src/editor/useFlash.ts +18 -0
- package/src/editor/useGhosts.ts +5 -7
- package/src/editor/usePageEvents.ts +13 -14
- package/src/editor-kit.ts +10 -6
- package/src/editors/CheckboxEditor.tsx +11 -12
- package/src/editors/ColorEditor.tsx +15 -35
- package/src/editors/DateEditor.tsx +18 -37
- package/src/editors/InlineText.tsx +4 -5
- package/src/editors/NumberEditor.tsx +15 -36
- package/src/editors/RelationshipEditor.tsx +18 -36
- package/src/editors/SelectEditor.tsx +14 -22
- package/src/editors/UploadEditor.tsx +6 -6
- package/src/editors/index.ts +56 -16
- package/src/editors/kit.tsx +97 -0
- package/src/editors/useDebouncedSearch.ts +18 -10
- package/src/geometry.ts +62 -0
- package/src/hub.ts +5 -5
- package/src/index.ts +5 -2
- package/src/layout.ts +5 -2
- package/src/locators.ts +11 -14
- package/src/messages.ts +26 -5
- package/src/registry.ts +42 -8
- package/src/rowOps.ts +64 -0
- package/src/sanitize.ts +6 -0
- package/src/settings.ts +49 -0
- package/src/styles.ts +2 -1
- package/src/targets.ts +1 -1
- package/src/types.ts +78 -45
- package/src/ui/BlockPicker.tsx +6 -7
- package/src/ui/Dialog.tsx +9 -5
- package/src/ui/Dock.tsx +67 -50
- package/src/ui/Overlay.tsx +75 -102
- package/src/ui/PreviewToolbar.tsx +7 -15
- package/src/ui/SettingsPanel.tsx +84 -81
- package/src/ui/ToolbarButton.tsx +36 -0
- package/src/useEvent.ts +4 -2
- package/src/useIsClient.ts +15 -0
package/src/OnPageClient.tsx
CHANGED
|
@@ -12,10 +12,11 @@
|
|
|
12
12
|
* invisible markers (turn off with `autoMap={false}`).
|
|
13
13
|
*/
|
|
14
14
|
import { useEvent } from './useEvent.ts'
|
|
15
|
+
import { toRef } from './annotations.ts'
|
|
15
16
|
import { encodeDocument, refKey, type DocRef } from '@payload-onpage/core'
|
|
16
17
|
import { useEffect, useMemo, type ReactNode } from 'react'
|
|
17
18
|
import { editable, editableOff, type Editable } from './annotations.ts'
|
|
18
|
-
import { useOnPageOptional } from './context.tsx'
|
|
19
|
+
import { useOnPageOptional, useSessionSnapshot } from './context.tsx'
|
|
19
20
|
|
|
20
21
|
export type OnPageClientProps<T> = {
|
|
21
22
|
doc: T
|
|
@@ -33,9 +34,9 @@ export function OnPageClient<T extends { id?: string | number } | null | undefin
|
|
|
33
34
|
const ref = useMemo<DocRef | null>(
|
|
34
35
|
() =>
|
|
35
36
|
globalSlug
|
|
36
|
-
? {
|
|
37
|
-
: collection
|
|
38
|
-
? {
|
|
37
|
+
? toRef({ global: globalSlug })
|
|
38
|
+
: collection
|
|
39
|
+
? toRef({ collection, doc: { id } })
|
|
39
40
|
: null,
|
|
40
41
|
[globalSlug, collection, id],
|
|
41
42
|
)
|
|
@@ -51,7 +52,8 @@ export function OnPageClient<T extends { id?: string | number } | null | undefin
|
|
|
51
52
|
})
|
|
52
53
|
useEffect(() => (enabled && ref ? join(ref) : undefined), [enabled, ref, join])
|
|
53
54
|
|
|
54
|
-
const
|
|
55
|
+
const snap = useSessionSnapshot(ctx?.session)
|
|
56
|
+
const state = key ? snap.docs[key] : undefined
|
|
55
57
|
const stateDoc = state?.doc
|
|
56
58
|
const fields = state?.schema?.fields
|
|
57
59
|
const autoMap = props.autoMap !== false
|
|
@@ -63,5 +65,7 @@ export function OnPageClient<T extends { id?: string | number } | null | undefin
|
|
|
63
65
|
() => (enabled && ref ? editable(ref, { enabled: true }) : editableOff),
|
|
64
66
|
[enabled, ref],
|
|
65
67
|
)
|
|
68
|
+
// The session's copy started as this `doc` (same document, edited, held as plain JSON),
|
|
69
|
+
// so it is handed back as the page's own type.
|
|
66
70
|
return <>{render(current as T, e)}</>
|
|
67
71
|
}
|
package/src/annotations.ts
CHANGED
|
@@ -51,7 +51,7 @@ export const editableOff: Editable = {
|
|
|
51
51
|
|
|
52
52
|
export type EditableTarget =
|
|
53
53
|
| DocRef
|
|
54
|
-
| { collection: string; doc: { id?: string | number } | null | undefined }
|
|
54
|
+
| { collection: string; doc: { id?: string | number | undefined } | null | undefined }
|
|
55
55
|
| { global: string }
|
|
56
56
|
|
|
57
57
|
export function toRef(t: EditableTarget): DocRef | null {
|
package/src/context.tsx
CHANGED
|
@@ -1,23 +1,66 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { createContext, useContext, useSyncExternalStore } from 'react'
|
|
3
|
+
import { createContext, useContext, useSyncExternalStore, type ReactNode } from 'react'
|
|
4
|
+
import type { EditorSession, SessionSnapshot } from '@payload-onpage/core'
|
|
4
5
|
import { hub } from './hub.ts'
|
|
5
|
-
import type { OnPageContextValue } from './types.ts'
|
|
6
|
+
import type { OnPageApi, OnPageContextValue, OnPageState } from './types.ts'
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
const ApiContext = createContext<OnPageApi | null>(null)
|
|
9
|
+
const StateContext = createContext<OnPageState | null>(null)
|
|
8
10
|
|
|
9
|
-
/**
|
|
11
|
+
/** Provides the editor to everything it renders (two contexts: the stable API and the changing state). */
|
|
12
|
+
export function OnPageProvider({
|
|
13
|
+
api,
|
|
14
|
+
state,
|
|
15
|
+
children,
|
|
16
|
+
}: {
|
|
17
|
+
api: OnPageApi
|
|
18
|
+
state: OnPageState
|
|
19
|
+
children: ReactNode
|
|
20
|
+
}) {
|
|
21
|
+
return (
|
|
22
|
+
<ApiContext.Provider value={api}>
|
|
23
|
+
<StateContext.Provider value={state}>{children}</StateContext.Provider>
|
|
24
|
+
</ApiContext.Provider>
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The editor, for code rendered inside it (field editors, panels): its API and state. */
|
|
10
29
|
export function useOnPage(): OnPageContextValue {
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
30
|
+
const api = useContext(ApiContext)
|
|
31
|
+
const state = useContext(StateContext)
|
|
32
|
+
if (!api || !state) throw new Error('useOnPage must be used inside <OnPageEditor>')
|
|
33
|
+
return { ...api, ...state }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Only the editor's API (session, actions): re-renders when the editor is reconfigured, not on every edit. */
|
|
37
|
+
export function useOnPageApi(): OnPageApi {
|
|
38
|
+
const api = useContext(ApiContext)
|
|
39
|
+
if (!api) throw new Error('useOnPageApi must be used inside <OnPageEditor>')
|
|
40
|
+
return api
|
|
14
41
|
}
|
|
15
42
|
|
|
16
43
|
const getNull = () => null
|
|
17
44
|
|
|
18
|
-
/**
|
|
19
|
-
|
|
20
|
-
|
|
45
|
+
/**
|
|
46
|
+
* The editor's API from inside it or anywhere on the page; null when no editor
|
|
47
|
+
* is mounted (public pages). Read documents with `useSessionSnapshot(api?.session)`.
|
|
48
|
+
*/
|
|
49
|
+
export function useOnPageOptional(): OnPageApi | null {
|
|
50
|
+
const inside = useContext(ApiContext)
|
|
21
51
|
const outside = useSyncExternalStore(hub.subscribe, hub.get, getNull)
|
|
22
52
|
return inside ?? outside
|
|
23
53
|
}
|
|
54
|
+
|
|
55
|
+
const EMPTY: SessionSnapshot = { docs: {}, canUndo: false, canRedo: false, busy: false }
|
|
56
|
+
const noopSubscribe = () => () => {}
|
|
57
|
+
const getEmpty = () => EMPTY
|
|
58
|
+
|
|
59
|
+
/** A session's documents, re-rendering when they change (an empty snapshot without a session). */
|
|
60
|
+
export function useSessionSnapshot(session: EditorSession | null | undefined): SessionSnapshot {
|
|
61
|
+
return useSyncExternalStore(
|
|
62
|
+
session?.subscribe ?? noopSubscribe,
|
|
63
|
+
session?.getSnapshot ?? getEmpty,
|
|
64
|
+
getEmpty,
|
|
65
|
+
)
|
|
66
|
+
}
|
package/src/dom.ts
CHANGED
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
/** DOM helpers shared by the editor UI (browser-only, no React). */
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
export const
|
|
5
|
-
/**
|
|
6
|
-
|
|
3
|
+
/** Every DOM attribute the editor sets (annotations, which sites write, are `ATTR`). */
|
|
4
|
+
export const DOM_ATTR = {
|
|
5
|
+
/** Hides a page element while an editor or ghost stands in for it. */
|
|
6
|
+
hidden: 'data-onpage-hidden',
|
|
7
|
+
/** Marks the editor's own UI, which locators and click handling ignore. */
|
|
8
|
+
ui: 'data-onpage-ui',
|
|
9
|
+
/** A page target the editor made keyboard-focusable. */
|
|
10
|
+
focusable: 'data-onpage-focusable',
|
|
11
|
+
/** On <html> while editing. */
|
|
12
|
+
editing: 'data-onpage-editing',
|
|
13
|
+
/** The editor's injected stylesheet. */
|
|
14
|
+
styles: 'data-onpage-styles',
|
|
15
|
+
} as const
|
|
16
|
+
|
|
17
|
+
export const HIDDEN = DOM_ATTR.hidden
|
|
18
|
+
export const UI = DOM_ATTR.ui
|
|
7
19
|
|
|
8
20
|
/** Computed styles copied onto an inline editor so it looks like the element it replaces. */
|
|
9
21
|
export const TYPOGRAPHY = [
|
|
@@ -94,20 +106,7 @@ export function placeCaret(el: HTMLElement, point?: { x: number; y: number }): v
|
|
|
94
106
|
sel.addRange(range)
|
|
95
107
|
}
|
|
96
108
|
|
|
97
|
-
|
|
98
|
-
export function popoverPosition(
|
|
99
|
-
el: Element | null,
|
|
100
|
-
size = { width: 300, height: 320 },
|
|
101
|
-
): { top: number; left: number } {
|
|
102
|
-
const vw = window.innerWidth
|
|
103
|
-
const vh = window.innerHeight
|
|
104
|
-
if (!el) return { top: 80, left: Math.max(8, vw / 2 - size.width / 2) }
|
|
105
|
-
const r = el.getBoundingClientRect()
|
|
106
|
-
const below = r.bottom + 8
|
|
107
|
-
const top = below + size.height < vh ? below : Math.max(8, r.top - 8 - size.height)
|
|
108
|
-
const left = Math.min(Math.max(8, r.left), vw - size.width - 8)
|
|
109
|
-
return { top, left }
|
|
110
|
-
}
|
|
109
|
+
export { popoverPosition } from './geometry.ts'
|
|
111
110
|
|
|
112
111
|
const FOCUSABLE =
|
|
113
112
|
'a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])'
|
package/src/editor/FieldHost.tsx
CHANGED
|
@@ -1,19 +1,34 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { fieldAtPath, getAt } from '@payload-onpage/core'
|
|
3
|
+
import { fieldAtPath, getAt, isReady, type ReadyDocumentState } from '@payload-onpage/core'
|
|
4
4
|
import { Suspense, useCallback } from 'react'
|
|
5
5
|
import { useOnPage } from '../context.tsx'
|
|
6
|
-
import
|
|
6
|
+
import { canEdit } from '../registry.ts'
|
|
7
|
+
import type { EditingState, FieldEditorDef, FieldEditorProps } from '../types.ts'
|
|
7
8
|
|
|
8
9
|
/** Renders the open field editor, if it belongs to this layer. */
|
|
9
10
|
export function FieldHost({ mode }: { mode: 'inline' | 'popover' }) {
|
|
10
|
-
const { editing, editors,
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
|
|
11
|
+
const { editing, editors, snap } = useOnPage()
|
|
12
|
+
if (!editing) return null
|
|
13
|
+
const def = editors[editing.editor]
|
|
14
|
+
const state = snap.docs[editing.key]
|
|
15
|
+
if (!def || def.mode !== mode || !isReady(state)) return null
|
|
16
|
+
return <OpenEditor editing={editing} def={def} state={state} />
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** The editor for one field of a loaded document. */
|
|
20
|
+
function OpenEditor({
|
|
21
|
+
editing,
|
|
22
|
+
def,
|
|
23
|
+
state,
|
|
24
|
+
}: {
|
|
25
|
+
editing: EditingState
|
|
26
|
+
def: FieldEditorDef
|
|
27
|
+
state: ReadyDocumentState
|
|
28
|
+
}) {
|
|
29
|
+
const { session, targets, setEditing, addGhost, isLive, t } = useOnPage()
|
|
30
|
+
const { key, path, el: clicked } = editing
|
|
15
31
|
const live = isLive(key)
|
|
16
|
-
const clicked = editing?.el
|
|
17
32
|
|
|
18
33
|
const element = useCallback(
|
|
19
34
|
() => (clicked?.isConnected ? clicked : (targets.find(key, path)?.el ?? null)),
|
|
@@ -39,10 +54,10 @@ export function FieldHost({ mode }: { mode: 'inline' | 'popover' }) {
|
|
|
39
54
|
[addGhost, key, path],
|
|
40
55
|
)
|
|
41
56
|
|
|
42
|
-
if (!editing || !def || def.mode !== mode || !state?.schema || !state.doc) return null
|
|
43
57
|
const field = fieldAtPath(state.schema.fields, state.doc, path)?.field
|
|
44
|
-
if (!field) return null
|
|
58
|
+
if (!field || !canEdit(def, field)) return null
|
|
45
59
|
const Editor = def.component
|
|
60
|
+
const raw = getAt(state.doc, path)
|
|
46
61
|
// Editors may be lazy (the rich-text editor is): render nothing until they've loaded.
|
|
47
62
|
return (
|
|
48
63
|
<Suspense fallback={null}>
|
|
@@ -52,7 +67,7 @@ export function FieldHost({ mode }: { mode: 'inline' | 'popover' }) {
|
|
|
52
67
|
path={path}
|
|
53
68
|
field={field}
|
|
54
69
|
element={element}
|
|
55
|
-
value={
|
|
70
|
+
value={def.normalize ? def.normalize(raw, field) : raw}
|
|
56
71
|
commit={commit}
|
|
57
72
|
preview={preview}
|
|
58
73
|
close={close}
|
|
@@ -7,38 +7,33 @@
|
|
|
7
7
|
* through the DataSource; the Renderer shows them (server refresh by default,
|
|
8
8
|
* instant for documents rendered with <OnPageClient>).
|
|
9
9
|
*/
|
|
10
|
-
import { useEvent } from '../useEvent.ts'
|
|
11
10
|
import {
|
|
12
|
-
EditorSession,
|
|
13
|
-
fieldAtPath,
|
|
14
|
-
isEditablePath,
|
|
15
11
|
refKey,
|
|
16
12
|
type DataSource,
|
|
17
13
|
type DocRef,
|
|
18
14
|
type Locator,
|
|
19
15
|
type MediaLibrary,
|
|
16
|
+
type RefKey,
|
|
20
17
|
type Renderer,
|
|
21
|
-
type SessionSnapshot,
|
|
22
18
|
} from '@payload-onpage/core'
|
|
23
|
-
import { useCallback, useEffect, useMemo,
|
|
19
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
24
20
|
import { createPortal } from 'react-dom'
|
|
25
|
-
import {
|
|
21
|
+
import { OnPageProvider, useSessionSnapshot } from '../context.tsx'
|
|
26
22
|
import { UI } from '../dom.ts'
|
|
27
23
|
import { defaultEditors } from '../editors/index.ts'
|
|
28
24
|
import { hub } from '../hub.ts'
|
|
29
25
|
import { notifyLayout } from '../layout.ts'
|
|
30
26
|
import { annotations, composite, stega } from '../locators.ts'
|
|
31
27
|
import { createTranslate, type Messages } from '../messages.ts'
|
|
32
|
-
import { mergeEditors
|
|
28
|
+
import { mergeEditors } from '../registry.ts'
|
|
33
29
|
import { clientState } from '../renderers.ts'
|
|
34
|
-
import { injectStyles } from '../styles.ts'
|
|
35
30
|
import { useTargets } from '../targets.ts'
|
|
36
31
|
import type {
|
|
37
32
|
EditingState,
|
|
38
|
-
EditorRegistry,
|
|
39
33
|
FieldEditorDef,
|
|
40
34
|
Links,
|
|
41
|
-
|
|
35
|
+
OnPageApi,
|
|
36
|
+
OnPageState,
|
|
42
37
|
Panel,
|
|
43
38
|
Selection,
|
|
44
39
|
} from '../types.ts'
|
|
@@ -46,43 +41,46 @@ import { BlockPicker } from '../ui/BlockPicker.tsx'
|
|
|
46
41
|
import { Dock } from '../ui/Dock.tsx'
|
|
47
42
|
import { Overlay } from '../ui/Overlay.tsx'
|
|
48
43
|
import { SettingsPanel } from '../ui/SettingsPanel.tsx'
|
|
44
|
+
import { useEditorStyles, useIsClient } from '../useIsClient.ts'
|
|
45
|
+
import { useEvent } from '../useEvent.ts'
|
|
46
|
+
import { resolveEditTarget } from './editTarget.ts'
|
|
49
47
|
import { FieldHost } from './FieldHost.tsx'
|
|
50
48
|
import { Ghosts } from './Ghosts.tsx'
|
|
51
|
-
import {
|
|
49
|
+
import { useClientDocs } from './useClientDocs.ts'
|
|
50
|
+
import { useEditorSession } from './useEditorSession.ts'
|
|
51
|
+
import { useFlash } from './useFlash.ts'
|
|
52
52
|
import { useGhosts } from './useGhosts.ts'
|
|
53
|
+
import { usePageEvents } from './usePageEvents.ts'
|
|
53
54
|
|
|
54
55
|
export interface OnPageEditorProps {
|
|
55
56
|
dataSource: DataSource
|
|
56
57
|
/** Image search/upload (e.g. `payloadUploads()`). Without it, upload fields can't be changed. */
|
|
57
|
-
media?: MediaLibrary
|
|
58
|
+
media?: MediaLibrary | undefined
|
|
58
59
|
/** How elements map to fields. Default: composite(annotations(), stega()). */
|
|
59
|
-
locator?: Locator
|
|
60
|
+
locator?: Locator | undefined
|
|
60
61
|
/** How saved changes reach the page. Default: clientState() (no server refresh). */
|
|
61
|
-
renderer?: Renderer
|
|
62
|
+
renderer?: Renderer | undefined
|
|
62
63
|
/** Extra or replacement field editors by id; `null` removes a default. */
|
|
63
|
-
editors?: Record<string, FieldEditorDef | null>
|
|
64
|
+
editors?: Record<string, FieldEditorDef | null> | undefined
|
|
64
65
|
links: Links
|
|
65
66
|
/** Content locale to edit. */
|
|
66
|
-
locale?: string
|
|
67
|
+
locale?: string | undefined
|
|
67
68
|
/** Autosave debounce, ms. Default: 250 with a refreshing renderer; else the collection's autosave interval, else 800. */
|
|
68
|
-
autosaveDelay?: number
|
|
69
|
+
autosaveDelay?: number | undefined
|
|
69
70
|
/** Where to look for editable content. Default: document.body. */
|
|
70
|
-
root?: () => ParentNode | null
|
|
71
|
+
root?: (() => ParentNode | null) | undefined
|
|
71
72
|
/** Override any UI string (see `defaultMessages`). */
|
|
72
|
-
messages?: Messages
|
|
73
|
+
messages?: Messages | undefined
|
|
73
74
|
}
|
|
74
75
|
|
|
75
|
-
|
|
76
|
-
const
|
|
76
|
+
/** How long a re-rendered page gets to land (and the locator to rescan) before ghosts are retired. */
|
|
77
|
+
const SETTLE_MS = 250
|
|
77
78
|
|
|
78
79
|
export function OnPageEditor(props: OnPageEditorProps) {
|
|
79
|
-
const { dataSource, locale,
|
|
80
|
+
const { dataSource, locale, links, media } = props
|
|
80
81
|
const renderer = useMemo(() => props.renderer ?? clientState(), [props.renderer])
|
|
81
82
|
const locator = useMemo(() => props.locator ?? composite(annotations(), stega()), [props.locator])
|
|
82
|
-
const editors
|
|
83
|
-
() => mergeEditors(defaultEditors, props.editors),
|
|
84
|
-
[props.editors],
|
|
85
|
-
)
|
|
83
|
+
const editors = useMemo(() => mergeEditors(defaultEditors, props.editors), [props.editors])
|
|
86
84
|
const t = useMemo(() => createTranslate(props.messages), [props.messages])
|
|
87
85
|
const rootProp = props.root
|
|
88
86
|
const root = useCallback(
|
|
@@ -94,161 +92,112 @@ export function OnPageEditor(props: OnPageEditorProps) {
|
|
|
94
92
|
const [editing, setEditing] = useState<EditingState | null>(null)
|
|
95
93
|
const [selected, setSelected] = useState<Selection | null>(null)
|
|
96
94
|
const [renders, setRenders] = useState(0)
|
|
97
|
-
const [flash,
|
|
98
|
-
const [clientDocs,
|
|
99
|
-
const [session, setSession] = useState<EditorSession | null>(null)
|
|
100
|
-
const snap = useSyncExternalStore(
|
|
101
|
-
session?.subscribe ?? noopSubscribe,
|
|
102
|
-
session?.getSnapshot ?? (() => EMPTY_SNAP),
|
|
103
|
-
() => EMPTY_SNAP,
|
|
104
|
-
)
|
|
105
|
-
const targets = useTargets(locator, root)
|
|
106
|
-
const { ghosts, add: addGhostEntry, rendered: ghostsRendered } = useGhosts(session ? snap : null)
|
|
107
|
-
|
|
95
|
+
const [flash, notify] = useFlash()
|
|
96
|
+
const [clientDocs, registerClientDoc] = useClientDocs()
|
|
108
97
|
const isLive = useCallback(
|
|
109
|
-
(key:
|
|
98
|
+
(key: RefKey) => renderer.live || clientDocs.has(key),
|
|
110
99
|
[renderer, clientDocs],
|
|
111
100
|
)
|
|
112
101
|
|
|
113
|
-
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
} finally {
|
|
124
|
-
setRenders((n) => n - 1)
|
|
125
|
-
// Give the new DOM a moment to land (and the locator to rescan) before uncovering it.
|
|
126
|
-
setTimeout(() => {
|
|
127
|
-
for (const r of stale) ghostsRendered(refKey(r), seq ?? Number.MAX_SAFE_INTEGER)
|
|
128
|
-
}, 250)
|
|
129
|
-
}
|
|
130
|
-
},
|
|
131
|
-
[isLive, renderer, ghostsRendered],
|
|
132
|
-
)
|
|
133
|
-
|
|
134
|
-
const handleSaved = useEvent((ref: DocRef, seq: number) => void rerender([ref], seq))
|
|
102
|
+
// Refresh mode saves quickly (the page only updates after a save); client mode uses the collection's interval.
|
|
103
|
+
const session = useEditorSession({
|
|
104
|
+
dataSource,
|
|
105
|
+
locale,
|
|
106
|
+
autosaveDelay: props.autosaveDelay ?? (renderer.live ? undefined : 250),
|
|
107
|
+
onSaved: (ref, seq) => void rerender([ref], seq),
|
|
108
|
+
})
|
|
109
|
+
const snap = useSessionSnapshot(session)
|
|
110
|
+
const targets = useTargets(locator, root)
|
|
111
|
+
const { ghosts, add: addGhostEntry, rendered: ghostsRendered } = useGhosts(session ? snap : null)
|
|
135
112
|
|
|
136
|
-
//
|
|
113
|
+
// Pending "retire ghosts" timers, cleared on unmount.
|
|
114
|
+
const ghostTimers = useRef(new Set<ReturnType<typeof setTimeout>>())
|
|
137
115
|
useEffect(() => {
|
|
138
|
-
|
|
139
|
-
const delay = autosaveDelay ?? (renderer.live ? undefined : 250)
|
|
140
|
-
const s = new EditorSession({
|
|
141
|
-
dataSource,
|
|
142
|
-
locale,
|
|
143
|
-
autosaveDelay: delay,
|
|
144
|
-
onSaved: (ref, seq) => handleSaved(ref, seq),
|
|
145
|
-
})
|
|
146
|
-
// The session is an external resource with a lifecycle; it lives in state so StrictMode's
|
|
147
|
-
// mount/unmount/mount gets a fresh one each time.
|
|
148
|
-
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
149
|
-
setSession(s)
|
|
116
|
+
const timers = ghostTimers.current
|
|
150
117
|
return () => {
|
|
151
|
-
|
|
118
|
+
for (const id of timers) clearTimeout(id)
|
|
119
|
+
timers.clear()
|
|
152
120
|
}
|
|
153
|
-
}, [
|
|
121
|
+
}, [])
|
|
154
122
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
123
|
+
/** Re-render documents that aren't client-rendered, then retire their ghosts. */
|
|
124
|
+
const rerender = useEvent(async (refs: DocRef[], seq?: number) => {
|
|
125
|
+
const stale = refs.filter((r) => !isLive(refKey(r)))
|
|
126
|
+
if (!stale.length) return
|
|
127
|
+
setRenders((n) => n + 1)
|
|
128
|
+
try {
|
|
129
|
+
await renderer.afterSave({ refs: stale })
|
|
130
|
+
} catch (e) {
|
|
131
|
+
console.error('[payload-onpage] render after save failed', e)
|
|
132
|
+
} finally {
|
|
133
|
+
setRenders((n) => n - 1)
|
|
134
|
+
const id = setTimeout(() => {
|
|
135
|
+
ghostTimers.current.delete(id)
|
|
136
|
+
for (const r of stale) ghostsRendered(refKey(r), seq ?? Number.MAX_SAFE_INTEGER)
|
|
137
|
+
}, SETTLE_MS)
|
|
138
|
+
ghostTimers.current.add(id)
|
|
139
|
+
}
|
|
140
|
+
})
|
|
160
141
|
|
|
161
142
|
// Documents on this page: everything the locator found, plus client-mode documents.
|
|
162
143
|
const docKeys = useMemo(() => {
|
|
163
|
-
const keys = new Set<
|
|
144
|
+
const keys = new Set<RefKey>(clientDocs)
|
|
164
145
|
for (const target of targets.all) keys.add(refKey(target.ref))
|
|
165
146
|
return [...keys]
|
|
166
147
|
}, [targets, clientDocs])
|
|
167
148
|
|
|
168
149
|
useEffect(() => {
|
|
169
150
|
if (!session) return
|
|
170
|
-
const seen = new Set<
|
|
151
|
+
const seen = new Set<RefKey>()
|
|
171
152
|
for (const target of targets.all) {
|
|
172
|
-
const
|
|
173
|
-
if (seen.has(
|
|
174
|
-
seen.add(
|
|
153
|
+
const key = refKey(target.ref)
|
|
154
|
+
if (seen.has(key)) continue
|
|
155
|
+
seen.add(key)
|
|
175
156
|
void session.open(target.ref)
|
|
176
157
|
}
|
|
177
158
|
}, [targets, session])
|
|
178
159
|
|
|
179
160
|
useEffect(() => notifyLayout(), [snap, targets])
|
|
180
161
|
|
|
181
|
-
const
|
|
182
|
-
(
|
|
183
|
-
[t],
|
|
184
|
-
)
|
|
185
|
-
|
|
186
|
-
const registerClientDoc = useCallback((key: string) => {
|
|
187
|
-
setClientDocs((s) => new Set(s).add(key))
|
|
188
|
-
return () =>
|
|
189
|
-
setClientDocs((s) => {
|
|
190
|
-
const next = new Set(s)
|
|
191
|
-
next.delete(key)
|
|
192
|
-
return next
|
|
193
|
-
})
|
|
194
|
-
}, [])
|
|
195
|
-
|
|
196
|
-
const addGhost = useCallback<OnPageContextValue['addGhost']>(
|
|
197
|
-
(g) => addGhostEntry(g, session?.get(g.key)?.save.seq ?? 0),
|
|
198
|
-
[addGhostEntry, session],
|
|
162
|
+
const addGhost = useEvent<OnPageApi['addGhost']>((g) =>
|
|
163
|
+
addGhostEntry(g, session?.get(g.key)?.save.seq ?? 0),
|
|
199
164
|
)
|
|
200
165
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
166
|
+
// Stable: reads the latest state when called, so the API doesn't change on every rescan.
|
|
167
|
+
const edit = useEvent<OnPageApi['edit']>((key, path, opts = {}) => {
|
|
168
|
+
const target = resolveEditTarget(session?.get(key), editors, key, path, {
|
|
169
|
+
point: opts.point,
|
|
170
|
+
kind: opts.kind,
|
|
171
|
+
el: opts.anchor ?? targets.find(key, path)?.el,
|
|
172
|
+
})
|
|
173
|
+
switch (target.kind) {
|
|
174
|
+
case 'not-ready':
|
|
175
|
+
if (target.state?.status === 'error')
|
|
176
|
+
notify(t('notify.cantEdit', { message: target.state.error ?? '' }), true)
|
|
177
|
+
else notify(t('notify.loading'))
|
|
208
178
|
return true
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
opts.kind === 'field' ? undefined : opts.kind,
|
|
217
|
-
)
|
|
218
|
-
if (!editor) return false
|
|
219
|
-
setEditing({
|
|
220
|
-
key,
|
|
221
|
-
path,
|
|
222
|
-
editor,
|
|
223
|
-
point: opts.point,
|
|
224
|
-
el: opts.anchor ?? targets.find(key, path)?.el,
|
|
225
|
-
})
|
|
226
|
-
return true
|
|
227
|
-
},
|
|
228
|
-
[session, editors, targets, notify],
|
|
229
|
-
)
|
|
179
|
+
case 'none':
|
|
180
|
+
return false
|
|
181
|
+
case 'edit':
|
|
182
|
+
setEditing(target.editing)
|
|
183
|
+
return true
|
|
184
|
+
}
|
|
185
|
+
})
|
|
230
186
|
|
|
231
|
-
const
|
|
187
|
+
const api = useMemo<OnPageApi | null>(
|
|
232
188
|
() =>
|
|
233
189
|
session && {
|
|
234
190
|
session,
|
|
235
|
-
snap,
|
|
236
191
|
renderer,
|
|
237
192
|
media,
|
|
238
193
|
editors,
|
|
239
|
-
targets,
|
|
240
194
|
links,
|
|
241
195
|
t,
|
|
242
|
-
panel,
|
|
243
196
|
setPanel,
|
|
244
|
-
editing,
|
|
245
197
|
setEditing,
|
|
246
|
-
selected,
|
|
247
198
|
setSelected,
|
|
248
|
-
docKeys,
|
|
249
199
|
isLive,
|
|
250
200
|
registerClientDoc,
|
|
251
|
-
rendering: renders > 0,
|
|
252
201
|
edit,
|
|
253
202
|
rerender: (refs) => rerender(refs),
|
|
254
203
|
notify,
|
|
@@ -256,43 +205,37 @@ export function OnPageEditor(props: OnPageEditorProps) {
|
|
|
256
205
|
},
|
|
257
206
|
[
|
|
258
207
|
session,
|
|
259
|
-
snap,
|
|
260
208
|
renderer,
|
|
261
209
|
media,
|
|
262
210
|
editors,
|
|
263
|
-
targets,
|
|
264
211
|
links,
|
|
265
212
|
t,
|
|
266
|
-
panel,
|
|
267
|
-
editing,
|
|
268
|
-
selected,
|
|
269
|
-
docKeys,
|
|
270
213
|
isLive,
|
|
271
214
|
registerClientDoc,
|
|
272
|
-
renders,
|
|
273
215
|
edit,
|
|
274
216
|
rerender,
|
|
275
217
|
notify,
|
|
276
218
|
addGhost,
|
|
277
219
|
],
|
|
278
220
|
)
|
|
221
|
+
const state = useMemo<OnPageState>(
|
|
222
|
+
() => ({ snap, targets, panel, editing, selected, docKeys, rendering: renders > 0, flash }),
|
|
223
|
+
[snap, targets, panel, editing, selected, docKeys, renders, flash],
|
|
224
|
+
)
|
|
279
225
|
|
|
226
|
+
// Components outside the editor's tree (client-mode documents) reach it through the hub.
|
|
280
227
|
useEffect(() => {
|
|
281
|
-
hub.set(
|
|
282
|
-
}, [
|
|
228
|
+
hub.set(api)
|
|
229
|
+
}, [api])
|
|
283
230
|
useEffect(() => () => hub.set(null), [])
|
|
284
|
-
|
|
285
|
-
usePageEvents(
|
|
231
|
+
useEditorStyles()
|
|
232
|
+
usePageEvents(api && { ...api, ...state }, root)
|
|
286
233
|
|
|
287
|
-
const mounted =
|
|
288
|
-
|
|
289
|
-
() => true,
|
|
290
|
-
() => false,
|
|
291
|
-
)
|
|
292
|
-
if (!mounted || !ctx) return null
|
|
234
|
+
const mounted = useIsClient()
|
|
235
|
+
if (!mounted || !api) return null
|
|
293
236
|
|
|
294
237
|
return (
|
|
295
|
-
<
|
|
238
|
+
<OnPageProvider api={api} state={state}>
|
|
296
239
|
{createPortal(
|
|
297
240
|
<div className="po-doc-layer" {...{ [UI]: '' }}>
|
|
298
241
|
<Ghosts ghosts={ghosts} />
|
|
@@ -306,10 +249,10 @@ export function OnPageEditor(props: OnPageEditorProps) {
|
|
|
306
249
|
<SettingsPanel />
|
|
307
250
|
<BlockPicker />
|
|
308
251
|
<FieldHost mode="popover" />
|
|
309
|
-
<Dock
|
|
252
|
+
<Dock />
|
|
310
253
|
</div>,
|
|
311
254
|
document.body,
|
|
312
255
|
)}
|
|
313
|
-
</
|
|
256
|
+
</OnPageProvider>
|
|
314
257
|
)
|
|
315
258
|
}
|