@payload-onpage/react 0.2.1 → 0.3.1
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/README.md +6 -0
- 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 +1 -5
- package/dist/styles.d.ts.map +1 -1
- package/dist/styles.js +5 -4
- 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 +15 -4
- 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 +5 -4
- 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
|
@@ -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
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What clicking or pressing Enter on a field opens: a pure function of the
|
|
3
|
+
* document's state, the editor registry and what was clicked.
|
|
4
|
+
*/
|
|
5
|
+
import {
|
|
6
|
+
fieldAtPath,
|
|
7
|
+
isEditablePath,
|
|
8
|
+
isReady,
|
|
9
|
+
type DocumentState,
|
|
10
|
+
type RefKey,
|
|
11
|
+
type TargetKind,
|
|
12
|
+
} from '@payload-onpage/core'
|
|
13
|
+
import { resolveEditor } from '../registry.ts'
|
|
14
|
+
import type { EditingState, EditorRegistry } from '../types.ts'
|
|
15
|
+
|
|
16
|
+
export type EditTarget =
|
|
17
|
+
/** The document isn't loaded (yet, or it failed). */
|
|
18
|
+
| { kind: 'not-ready'; state: DocumentState | undefined }
|
|
19
|
+
/** Nothing here is edited in place. */
|
|
20
|
+
| { kind: 'none' }
|
|
21
|
+
| { kind: 'edit'; editing: EditingState }
|
|
22
|
+
|
|
23
|
+
export function resolveEditTarget(
|
|
24
|
+
state: DocumentState | undefined,
|
|
25
|
+
editors: EditorRegistry,
|
|
26
|
+
key: RefKey,
|
|
27
|
+
path: string,
|
|
28
|
+
opts: {
|
|
29
|
+
point?: { x: number; y: number } | undefined
|
|
30
|
+
kind?: TargetKind | undefined
|
|
31
|
+
el?: Element | undefined
|
|
32
|
+
},
|
|
33
|
+
): EditTarget {
|
|
34
|
+
if (!isReady(state)) return { kind: 'not-ready', state }
|
|
35
|
+
if (!isEditablePath(state.schema, path)) return { kind: 'none' }
|
|
36
|
+
const hit = fieldAtPath(state.schema.fields, state.doc, path)
|
|
37
|
+
if (!hit) return { kind: 'none' }
|
|
38
|
+
const editor = resolveEditor(editors, hit.field, opts.kind === 'field' ? undefined : opts.kind)
|
|
39
|
+
if (!editor) return { kind: 'none' }
|
|
40
|
+
return { kind: 'edit', editing: { key, path, editor, point: opts.point, el: opts.el } }
|
|
41
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import type { RefKey } from '@payload-onpage/core'
|
|
4
|
+
import { useCallback, useState } from 'react'
|
|
5
|
+
|
|
6
|
+
/** Documents rendered from editor state on this page (client mode), and how they register. */
|
|
7
|
+
export function useClientDocs(): [ReadonlySet<RefKey>, (key: RefKey) => () => void] {
|
|
8
|
+
const [docs, setDocs] = useState<ReadonlySet<RefKey>>(() => new Set())
|
|
9
|
+
const register = useCallback((key: RefKey) => {
|
|
10
|
+
setDocs((s) => new Set(s).add(key))
|
|
11
|
+
return () =>
|
|
12
|
+
setDocs((s) => {
|
|
13
|
+
const next = new Set(s)
|
|
14
|
+
next.delete(key)
|
|
15
|
+
return next
|
|
16
|
+
})
|
|
17
|
+
}, [])
|
|
18
|
+
return [docs, register]
|
|
19
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { EditorSession, type DataSource, type DocRef } from '@payload-onpage/core'
|
|
4
|
+
import { useEffect, useState } from 'react'
|
|
5
|
+
import { useEvent } from '../useEvent.ts'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* One editor session per data source and locale, disposed (after flushing its
|
|
9
|
+
* saves) when they change or on unmount. It lives in state, so StrictMode's
|
|
10
|
+
* mount/unmount/mount gets a fresh one each time.
|
|
11
|
+
*/
|
|
12
|
+
export function useEditorSession(opts: {
|
|
13
|
+
dataSource: DataSource
|
|
14
|
+
locale: string | undefined
|
|
15
|
+
autosaveDelay: number | undefined
|
|
16
|
+
onSaved: (ref: DocRef, seq: number) => void
|
|
17
|
+
}): EditorSession | null {
|
|
18
|
+
const { dataSource, locale, autosaveDelay } = opts
|
|
19
|
+
const onSaved = useEvent(opts.onSaved)
|
|
20
|
+
const [session, setSession] = useState<EditorSession | null>(null)
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
const s = new EditorSession({
|
|
23
|
+
dataSource,
|
|
24
|
+
locale,
|
|
25
|
+
autosaveDelay,
|
|
26
|
+
onSaved: (ref, seq) => onSaved(ref, seq),
|
|
27
|
+
})
|
|
28
|
+
// The session is an external resource with a lifecycle (see above).
|
|
29
|
+
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
30
|
+
setSession(s)
|
|
31
|
+
return () => {
|
|
32
|
+
void s.flush().finally(() => s.dispose())
|
|
33
|
+
}
|
|
34
|
+
}, [dataSource, locale, autosaveDelay, onSaved])
|
|
35
|
+
return session
|
|
36
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useState } from 'react'
|
|
4
|
+
import type { Flash } from '../types.ts'
|
|
5
|
+
|
|
6
|
+
const SHOWN_FOR = 4000
|
|
7
|
+
|
|
8
|
+
/** The dock's short message, cleared after a few seconds. */
|
|
9
|
+
export function useFlash(): [Flash | null, (text: string, bad?: boolean) => void] {
|
|
10
|
+
const [flash, setFlash] = useState<Flash | null>(null)
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
if (!flash) return
|
|
13
|
+
const id = setTimeout(() => setFlash(null), SHOWN_FOR)
|
|
14
|
+
return () => clearTimeout(id)
|
|
15
|
+
}, [flash])
|
|
16
|
+
const notify = useCallback((text: string, bad?: boolean) => setFlash({ text, bad }), [])
|
|
17
|
+
return [flash, notify]
|
|
18
|
+
}
|
package/src/editor/useGhosts.ts
CHANGED
|
@@ -6,16 +6,14 @@
|
|
|
6
6
|
* records the document's save sequence when it was made; the render after a
|
|
7
7
|
* later save retires it. Failed saves and a TTL retire ghosts too.
|
|
8
8
|
*/
|
|
9
|
-
import type { SessionSnapshot } from '@payload-onpage/core'
|
|
9
|
+
import type { SessionSnapshot, RefKey } from '@payload-onpage/core'
|
|
10
|
+
import type { GhostInput } from '../types.ts'
|
|
10
11
|
import { useCallback, useEffect, useState } from 'react'
|
|
11
12
|
|
|
12
|
-
export interface Ghost {
|
|
13
|
+
export interface Ghost extends GhostInput {
|
|
13
14
|
id: string
|
|
14
|
-
key:
|
|
15
|
+
key: RefKey
|
|
15
16
|
path: string
|
|
16
|
-
html: string
|
|
17
|
-
className?: string
|
|
18
|
-
style?: Partial<CSSStyleDeclaration>
|
|
19
17
|
/** Save sequence of the document when the ghost was made. */
|
|
20
18
|
afterSeq: number
|
|
21
19
|
until: number
|
|
@@ -35,7 +33,7 @@ export function useGhosts(snap: SessionSnapshot | null) {
|
|
|
35
33
|
}, [])
|
|
36
34
|
|
|
37
35
|
/** The page re-rendered after save `seq` of document `key`. */
|
|
38
|
-
const rendered = useCallback((key:
|
|
36
|
+
const rendered = useCallback((key: RefKey, seq: number) => {
|
|
39
37
|
setGhosts((gs) =>
|
|
40
38
|
gs.some((g) => g.key === key && g.afterSeq < seq)
|
|
41
39
|
? gs.filter((g) => !(g.key === key && g.afterSeq < seq))
|
|
@@ -8,13 +8,14 @@
|
|
|
8
8
|
* - a warning before leaving with unsaved changes
|
|
9
9
|
*/
|
|
10
10
|
import { useEvent } from '../useEvent.ts'
|
|
11
|
-
import {
|
|
11
|
+
import { refKey, resolveRow, isReady } from '@payload-onpage/core'
|
|
12
|
+
import { rowOps } from '../rowOps.ts'
|
|
12
13
|
import { useEffect } from 'react'
|
|
13
|
-
import { isOurs, isTyping } from '../dom.ts'
|
|
14
|
+
import { DOM_ATTR, isOurs, isTyping } from '../dom.ts'
|
|
14
15
|
import { isFieldTarget, isRowTarget, targetsAt } from '../targets.ts'
|
|
15
16
|
import type { OnPageContextValue } from '../types.ts'
|
|
16
17
|
|
|
17
|
-
const FOCUSABLE_FLAG =
|
|
18
|
+
const FOCUSABLE_FLAG = DOM_ATTR.focusable
|
|
18
19
|
|
|
19
20
|
export function usePageEvents(ctx: OnPageContextValue | null, root: () => ParentNode | null): void {
|
|
20
21
|
const onClick = useEvent((e: MouseEvent) => {
|
|
@@ -43,22 +44,20 @@ export function usePageEvents(ctx: OnPageContextValue | null, root: () => Parent
|
|
|
43
44
|
}
|
|
44
45
|
})
|
|
45
46
|
|
|
47
|
+
/** ⌥↑/⌥↓ move the selected row, ⌥⌫ deletes it. True when the key was ours (even if the row can't move). */
|
|
46
48
|
const moveSelected = (dir: -1 | 1 | 'delete') => {
|
|
47
49
|
if (!ctx?.selected) return false
|
|
48
50
|
const { key, path } = ctx.selected
|
|
49
51
|
const state = ctx.snap.docs[key]
|
|
50
|
-
const info = state
|
|
52
|
+
const info = isReady(state) ? resolveRow(state.schema, state.doc, path) : null
|
|
51
53
|
if (!info) return false
|
|
54
|
+
const ops = rowOps(ctx.session, key, info)
|
|
52
55
|
if (dir === 'delete') {
|
|
53
|
-
if (
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
56
|
+
if (ops.remove()) ctx.setSelected(null)
|
|
57
|
+
} else {
|
|
58
|
+
const next = ops.move(dir)
|
|
59
|
+
if (next) ctx.setSelected(next)
|
|
57
60
|
}
|
|
58
|
-
const to = info.index + dir
|
|
59
|
-
if (to < 0 || to >= info.length) return true
|
|
60
|
-
ctx.session.apply(key, (d) => updateArray(d, info.listPath, (a) => moveItem(a, info.index, to)))
|
|
61
|
-
ctx.setSelected({ key, path: `${info.listPath}.${to}` })
|
|
62
61
|
return true
|
|
63
62
|
}
|
|
64
63
|
|
|
@@ -120,7 +119,7 @@ export function usePageEvents(ctx: OnPageContextValue | null, root: () => Parent
|
|
|
120
119
|
})
|
|
121
120
|
|
|
122
121
|
useEffect(() => {
|
|
123
|
-
document.documentElement.setAttribute(
|
|
122
|
+
document.documentElement.setAttribute(DOM_ATTR.editing, '')
|
|
124
123
|
const click = (e: MouseEvent) => onClick(e)
|
|
125
124
|
const key = (e: KeyboardEvent) => onKey(e)
|
|
126
125
|
const focus = (e: FocusEvent) => onFocus(e)
|
|
@@ -130,7 +129,7 @@ export function usePageEvents(ctx: OnPageContextValue | null, root: () => Parent
|
|
|
130
129
|
document.addEventListener('focusin', focus)
|
|
131
130
|
window.addEventListener('beforeunload', unload)
|
|
132
131
|
return () => {
|
|
133
|
-
document.documentElement.removeAttribute(
|
|
132
|
+
document.documentElement.removeAttribute(DOM_ATTR.editing)
|
|
134
133
|
document.removeEventListener('click', click, true)
|
|
135
134
|
document.removeEventListener('keydown', key)
|
|
136
135
|
document.removeEventListener('focusin', focus)
|
package/src/editor-kit.ts
CHANGED
|
@@ -2,14 +2,17 @@
|
|
|
2
2
|
* @payload-onpage/react/editor-kit — building blocks for custom field editors
|
|
3
3
|
* (the default editors and @payload-onpage/lexical use exactly these).
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* const json = defineEditor({
|
|
6
|
+
* mode: 'popover',
|
|
7
|
+
* accepts: ['json'],
|
|
8
|
+
* component: (props) => <EditorPopover props={props} testId="my-json">…</EditorPopover>,
|
|
9
|
+
* })
|
|
8
10
|
*/
|
|
9
11
|
export { Dialog, type DialogProps } from './ui/Dialog.tsx'
|
|
10
12
|
export { useOnPage } from './context.tsx'
|
|
11
13
|
export { useDebouncedSearch } from './editors/useDebouncedSearch.ts'
|
|
12
|
-
export {
|
|
14
|
+
export { ApplyBar, EditorPopover, useDraft } from './editors/kit.tsx'
|
|
15
|
+
export { canEdit, defineEditor } from './registry.ts'
|
|
13
16
|
export {
|
|
14
17
|
HIDDEN,
|
|
15
18
|
UI,
|
|
@@ -22,6 +25,7 @@ export {
|
|
|
22
25
|
} from './dom.ts'
|
|
23
26
|
export { useTrackElement, useLayoutTick, notifyLayout } from './layout.ts'
|
|
24
27
|
export { useEvent } from './useEvent.ts'
|
|
25
|
-
export { sanitizeHtml } from './sanitize.ts'
|
|
26
|
-
export type { EditorRegistry, FieldEditorDef, FieldEditorProps } from './types.ts'
|
|
28
|
+
export { escapeHtml, sanitizeHtml } from './sanitize.ts'
|
|
29
|
+
export type { EditorRegistry, FieldEditorDef, FieldEditorProps, GhostInput } from './types.ts'
|
|
30
|
+
export type { FieldOf } from '@payload-onpage/core'
|
|
27
31
|
export type { Translate } from './messages.ts'
|