@payload-onpage/react 0.2.0 → 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
|
@@ -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'
|
|
@@ -1,23 +1,22 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
+
import type { ScalarField } from '@payload-onpage/core'
|
|
3
4
|
import type { FieldEditorProps } from '../types.ts'
|
|
4
|
-
import {
|
|
5
|
+
import { EditorPopover } from './kit.tsx'
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
/** A checkbox value (unset falls back to the field's default). */
|
|
8
|
+
export const checkedOf = (raw: unknown, field: { defaultValue?: unknown }): boolean =>
|
|
9
|
+
Boolean(raw ?? field.defaultValue)
|
|
10
|
+
|
|
11
|
+
export function CheckboxEditor(props: FieldEditorProps<ScalarField, boolean>) {
|
|
12
|
+
const { field, value, commit, close } = props
|
|
7
13
|
return (
|
|
8
|
-
<
|
|
9
|
-
variant="popover"
|
|
10
|
-
anchor={element}
|
|
11
|
-
title={field.label}
|
|
12
|
-
onClose={close}
|
|
13
|
-
t={t}
|
|
14
|
-
testId="onpage-checkbox"
|
|
15
|
-
>
|
|
14
|
+
<EditorPopover props={props} testId="onpage-checkbox">
|
|
16
15
|
<label className="po-check">
|
|
17
16
|
<input
|
|
18
17
|
type="checkbox"
|
|
19
18
|
data-autofocus
|
|
20
|
-
checked={
|
|
19
|
+
checked={value}
|
|
21
20
|
onChange={(e) => {
|
|
22
21
|
commit(e.target.checked)
|
|
23
22
|
close()
|
|
@@ -26,6 +25,6 @@ export function CheckboxEditor({ field, value, element, commit, close, t }: Fiel
|
|
|
26
25
|
/>
|
|
27
26
|
{field.label}
|
|
28
27
|
</label>
|
|
29
|
-
</
|
|
28
|
+
</EditorPopover>
|
|
30
29
|
)
|
|
31
30
|
}
|
|
@@ -1,41 +1,23 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import type { ScalarField } from '@payload-onpage/core'
|
|
4
4
|
import type { FieldEditorProps } from '../types.ts'
|
|
5
|
-
import {
|
|
5
|
+
import { ApplyBar, EditorPopover, useDraft } from './kit.tsx'
|
|
6
6
|
|
|
7
7
|
const HEX = /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i
|
|
8
8
|
|
|
9
|
+
export const toColor = (raw: unknown): string | null =>
|
|
10
|
+
typeof raw === 'string' && raw ? raw : null
|
|
11
|
+
|
|
9
12
|
/** Hex color with a native picker. Use it from a text field: `admin.custom.onpage.editor: 'color'`. */
|
|
10
|
-
export function ColorEditor({
|
|
11
|
-
field,
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
close,
|
|
17
|
-
live,
|
|
18
|
-
t,
|
|
19
|
-
}: FieldEditorProps) {
|
|
20
|
-
const [draft, setDraft] = useState(typeof value === 'string' ? value : '')
|
|
21
|
-
const change = (v: string) => {
|
|
22
|
-
setDraft(v)
|
|
23
|
-
if (live && HEX.test(v)) preview(v)
|
|
24
|
-
}
|
|
25
|
-
const save = () => {
|
|
26
|
-
if (draft !== (value ?? '') && (draft === '' || HEX.test(draft))) commit(draft || null)
|
|
27
|
-
close()
|
|
28
|
-
}
|
|
13
|
+
export function ColorEditor(props: FieldEditorProps<ScalarField, string | null>) {
|
|
14
|
+
const { field, t } = props
|
|
15
|
+
const { draft, change, save, onKeyDown } = useDraft(props, {
|
|
16
|
+
format: (v) => v ?? '',
|
|
17
|
+
parse: (d) => (d === '' ? null : HEX.test(d) ? d : undefined),
|
|
18
|
+
})
|
|
29
19
|
return (
|
|
30
|
-
<
|
|
31
|
-
variant="popover"
|
|
32
|
-
anchor={element}
|
|
33
|
-
title={field.label}
|
|
34
|
-
onClose={close}
|
|
35
|
-
t={t}
|
|
36
|
-
width={260}
|
|
37
|
-
testId="onpage-color"
|
|
38
|
-
>
|
|
20
|
+
<EditorPopover props={props} testId="onpage-color" width={260}>
|
|
39
21
|
<div className="po-row po-inline-row">
|
|
40
22
|
<input
|
|
41
23
|
type="color"
|
|
@@ -52,13 +34,11 @@ export function ColorEditor({
|
|
|
52
34
|
placeholder="#1e40af"
|
|
53
35
|
aria-invalid={draft !== '' && !HEX.test(draft)}
|
|
54
36
|
onChange={(e) => change(e.target.value)}
|
|
55
|
-
onKeyDown={
|
|
37
|
+
onKeyDown={onKeyDown}
|
|
56
38
|
data-testid="onpage-color-input"
|
|
57
39
|
/>
|
|
58
40
|
</div>
|
|
59
|
-
<
|
|
60
|
-
|
|
61
|
-
</button>
|
|
62
|
-
</Dialog>
|
|
41
|
+
<ApplyBar onApply={save} t={t} />
|
|
42
|
+
</EditorPopover>
|
|
63
43
|
)
|
|
64
44
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import type { DateField } from '@payload-onpage/core'
|
|
4
4
|
import type { FieldEditorProps } from '../types.ts'
|
|
5
|
-
import {
|
|
5
|
+
import { ApplyBar, EditorPopover, useDraft } from './kit.tsx'
|
|
6
6
|
|
|
7
7
|
/** Payload stores dates as ISO strings; the input works in local time. */
|
|
8
|
-
const toLocalInput = (iso:
|
|
9
|
-
if (
|
|
8
|
+
const toLocalInput = (iso: string | null, withTime: boolean): string => {
|
|
9
|
+
if (!iso) return ''
|
|
10
10
|
const d = new Date(iso)
|
|
11
11
|
if (Number.isNaN(d.getTime())) return ''
|
|
12
12
|
const pad = (n: number) => String(n).padStart(2, '0')
|
|
@@ -14,24 +14,19 @@ const toLocalInput = (iso: unknown, withTime: boolean): string => {
|
|
|
14
14
|
return withTime ? `${date}T${pad(d.getHours())}:${pad(d.getMinutes())}` : date
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
export
|
|
17
|
+
export const toIsoDate = (raw: unknown): string | null =>
|
|
18
|
+
typeof raw === 'string' && raw ? raw : null
|
|
19
|
+
|
|
20
|
+
export function DateEditor(props: FieldEditorProps<DateField, string | null>) {
|
|
21
|
+
const { field, t } = props
|
|
18
22
|
const withTime = field.pickerAppearance === 'dayAndTime' || field.pickerAppearance === 'timeOnly'
|
|
19
|
-
const
|
|
20
|
-
|
|
23
|
+
const { draft, change, save, onKeyDown, clear } = useDraft(props, {
|
|
24
|
+
format: (v) => toLocalInput(v, withTime),
|
|
21
25
|
// Date-only values are stored at local noon, which survives any timezone shift (as Payload's picker does).
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
close()
|
|
25
|
-
}
|
|
26
|
+
parse: (d) => (d ? new Date(withTime ? d : `${d}T12:00:00`).toISOString() : null),
|
|
27
|
+
})
|
|
26
28
|
return (
|
|
27
|
-
<
|
|
28
|
-
variant="popover"
|
|
29
|
-
anchor={element}
|
|
30
|
-
title={field.label}
|
|
31
|
-
onClose={close}
|
|
32
|
-
t={t}
|
|
33
|
-
testId="onpage-date"
|
|
34
|
-
>
|
|
29
|
+
<EditorPopover props={props} testId="onpage-date">
|
|
35
30
|
<div className="po-row">
|
|
36
31
|
<input
|
|
37
32
|
className="po-input"
|
|
@@ -39,26 +34,12 @@ export function DateEditor({ field, value, element, commit, close, t }: FieldEdi
|
|
|
39
34
|
aria-label={field.label}
|
|
40
35
|
data-autofocus
|
|
41
36
|
value={draft}
|
|
42
|
-
onChange={(e) =>
|
|
43
|
-
onKeyDown={
|
|
37
|
+
onChange={(e) => change(e.target.value)}
|
|
38
|
+
onKeyDown={onKeyDown}
|
|
44
39
|
data-testid="onpage-date-input"
|
|
45
40
|
/>
|
|
46
41
|
</div>
|
|
47
|
-
<
|
|
48
|
-
|
|
49
|
-
type="button"
|
|
50
|
-
className="po-btn po-primary"
|
|
51
|
-
onClick={save}
|
|
52
|
-
data-testid="onpage-apply"
|
|
53
|
-
>
|
|
54
|
-
{t('common.apply')}
|
|
55
|
-
</button>
|
|
56
|
-
{!field.required && (
|
|
57
|
-
<button type="button" className="po-btn" onClick={() => setDraft('')}>
|
|
58
|
-
{t('common.clear')}
|
|
59
|
-
</button>
|
|
60
|
-
)}
|
|
61
|
-
</div>
|
|
62
|
-
</Dialog>
|
|
42
|
+
<ApplyBar onApply={save} onClear={field.required ? undefined : clear} t={t} />
|
|
43
|
+
</EditorPopover>
|
|
63
44
|
)
|
|
64
45
|
}
|
|
@@ -11,13 +11,12 @@
|
|
|
11
11
|
* cancels.
|
|
12
12
|
*/
|
|
13
13
|
import { useLayoutEffect, useRef } from 'react'
|
|
14
|
-
import {
|
|
14
|
+
import type { ScalarField } from '@payload-onpage/core'
|
|
15
15
|
import { copyTypography, HIDDEN, placeCaret, placeOver, typographyOf } from '../dom.ts'
|
|
16
16
|
import { useTrackElement } from '../layout.ts'
|
|
17
17
|
import type { FieldEditorProps } from '../types.ts'
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
s.replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' })[c]!)
|
|
19
|
+
import { escapeHtml } from '../sanitize.ts'
|
|
21
20
|
|
|
22
21
|
export function InlineText({
|
|
23
22
|
field,
|
|
@@ -30,10 +29,10 @@ export function InlineText({
|
|
|
30
29
|
live,
|
|
31
30
|
point,
|
|
32
31
|
t,
|
|
33
|
-
}: FieldEditorProps) {
|
|
32
|
+
}: FieldEditorProps<ScalarField, string>) {
|
|
34
33
|
const multiline = field.type === 'textarea'
|
|
35
34
|
const ref = useRef<HTMLDivElement>(null)
|
|
36
|
-
const initial = useRef(
|
|
35
|
+
const initial = useRef(value)
|
|
37
36
|
const done = useRef(false)
|
|
38
37
|
|
|
39
38
|
useLayoutEffect(() => {
|
|
@@ -1,41 +1,22 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import { useState } from 'react'
|
|
3
|
+
import type { NumberField } from '@payload-onpage/core'
|
|
5
4
|
import type { FieldEditorProps } from '../types.ts'
|
|
6
|
-
import {
|
|
5
|
+
import { ApplyBar, EditorPopover, useDraft } from './kit.tsx'
|
|
6
|
+
|
|
7
|
+
export const toNumber = (raw: unknown): number | null =>
|
|
8
|
+
typeof raw === 'number' && Number.isFinite(raw) ? raw : null
|
|
7
9
|
|
|
8
10
|
/** Number input, with a slider when the field has min and max. */
|
|
9
|
-
export function NumberEditor({
|
|
10
|
-
field,
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
close,
|
|
16
|
-
live,
|
|
17
|
-
t,
|
|
18
|
-
}: FieldEditorProps) {
|
|
19
|
-
const [draft, setDraft] = useState(toText(value))
|
|
11
|
+
export function NumberEditor(props: FieldEditorProps<NumberField, number | null>) {
|
|
12
|
+
const { field, t } = props
|
|
13
|
+
const { draft, change, save, onKeyDown } = useDraft(props, {
|
|
14
|
+
format: (v) => (v === null ? '' : String(v)),
|
|
15
|
+
parse: (d) => (d === '' ? null : Number.isNaN(Number(d)) ? undefined : Number(d)),
|
|
16
|
+
})
|
|
20
17
|
const slider = field.min !== undefined && field.max !== undefined
|
|
21
|
-
const save = () => {
|
|
22
|
-
const next = draft === '' ? null : Number(draft)
|
|
23
|
-
if (next !== value && (next === null || !Number.isNaN(next))) commit(next)
|
|
24
|
-
close()
|
|
25
|
-
}
|
|
26
|
-
const change = (v: string) => {
|
|
27
|
-
setDraft(v)
|
|
28
|
-
if (live && v !== '' && !Number.isNaN(Number(v))) preview(Number(v))
|
|
29
|
-
}
|
|
30
18
|
return (
|
|
31
|
-
<
|
|
32
|
-
variant="popover"
|
|
33
|
-
anchor={element}
|
|
34
|
-
title={field.label}
|
|
35
|
-
onClose={close}
|
|
36
|
-
t={t}
|
|
37
|
-
testId="onpage-number"
|
|
38
|
-
>
|
|
19
|
+
<EditorPopover props={props} testId="onpage-number">
|
|
39
20
|
<div className="po-row">
|
|
40
21
|
{slider && (
|
|
41
22
|
<input
|
|
@@ -57,13 +38,11 @@ export function NumberEditor({
|
|
|
57
38
|
max={field.max}
|
|
58
39
|
value={draft}
|
|
59
40
|
onChange={(e) => change(e.target.value)}
|
|
60
|
-
onKeyDown={
|
|
41
|
+
onKeyDown={onKeyDown}
|
|
61
42
|
data-testid="onpage-number-input"
|
|
62
43
|
/>
|
|
63
44
|
</div>
|
|
64
|
-
<
|
|
65
|
-
|
|
66
|
-
</button>
|
|
67
|
-
</Dialog>
|
|
45
|
+
<ApplyBar onApply={save} t={t} />
|
|
46
|
+
</EditorPopover>
|
|
68
47
|
)
|
|
69
48
|
}
|
|
@@ -5,43 +5,32 @@
|
|
|
5
5
|
* (Payload REST, so access control applies) and pick one, or several for
|
|
6
6
|
* hasMany. Polymorphic fields get a collection switch and store `{ relationTo, value }`.
|
|
7
7
|
*/
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
asList,
|
|
10
|
+
relationId,
|
|
11
|
+
relationTitle,
|
|
12
|
+
type RelationField,
|
|
13
|
+
type SearchHit,
|
|
14
|
+
} from '@payload-onpage/core'
|
|
9
15
|
import { useCallback, useState } from 'react'
|
|
10
16
|
import { useOnPage } from '../context.tsx'
|
|
11
17
|
import type { FieldEditorProps } from '../types.ts'
|
|
12
|
-
import {
|
|
18
|
+
import { EditorPopover } from './kit.tsx'
|
|
13
19
|
import { useDebouncedSearch } from './useDebouncedSearch.ts'
|
|
14
20
|
|
|
15
|
-
export function RelationshipEditor({
|
|
16
|
-
docKey,
|
|
17
|
-
field,
|
|
18
|
-
value,
|
|
19
|
-
element,
|
|
20
|
-
commit,
|
|
21
|
-
close,
|
|
22
|
-
t,
|
|
23
|
-
}: FieldEditorProps) {
|
|
21
|
+
export function RelationshipEditor(props: FieldEditorProps<RelationField>) {
|
|
22
|
+
const { docKey, field, value, commit, close, t } = props
|
|
24
23
|
const { session, snap } = useOnPage()
|
|
25
|
-
const targets = Array.isArray(field.relationTo)
|
|
26
|
-
? field.relationTo
|
|
27
|
-
: field.relationTo
|
|
28
|
-
? [field.relationTo]
|
|
29
|
-
: []
|
|
24
|
+
const targets = Array.isArray(field.relationTo) ? field.relationTo : [field.relationTo]
|
|
30
25
|
const poly = Array.isArray(field.relationTo)
|
|
31
26
|
const [collection, setCollection] = useState(targets[0] ?? '')
|
|
32
27
|
const relations = snap.docs[docKey]?.schema?.relations ?? {}
|
|
33
28
|
const titleField = relations[collection]?.titleField
|
|
34
29
|
const many = !!field.hasMany
|
|
35
|
-
const current
|
|
36
|
-
? Array.isArray(value)
|
|
37
|
-
? value
|
|
38
|
-
: []
|
|
39
|
-
: value == null
|
|
40
|
-
? []
|
|
41
|
-
: [value]
|
|
30
|
+
const current = asList(value, many)
|
|
42
31
|
const ds = session.dataSource
|
|
43
32
|
const search = useCallback(
|
|
44
|
-
async (query: string) =>
|
|
33
|
+
async (query: string, _page: number, signal: AbortSignal) =>
|
|
45
34
|
ds.search
|
|
46
35
|
? {
|
|
47
36
|
items: await ds.search(collection, {
|
|
@@ -49,13 +38,14 @@ export function RelationshipEditor({
|
|
|
49
38
|
titleField,
|
|
50
39
|
locale: session.locale,
|
|
51
40
|
limit: 12,
|
|
41
|
+
signal,
|
|
52
42
|
}),
|
|
53
43
|
hasMore: false,
|
|
54
44
|
}
|
|
55
45
|
: { items: [] as SearchHit[], hasMore: false },
|
|
56
46
|
[ds, collection, titleField, session.locale],
|
|
57
47
|
)
|
|
58
|
-
const results = useDebouncedSearch<SearchHit>(ds.search ? search : null
|
|
48
|
+
const results = useDebouncedSearch<SearchHit>(ds.search ? search : null)
|
|
59
49
|
|
|
60
50
|
const wrap = (hit: SearchHit): unknown =>
|
|
61
51
|
poly ? { relationTo: collection, value: hit.doc } : hit.doc
|
|
@@ -75,15 +65,7 @@ export function RelationshipEditor({
|
|
|
75
65
|
const remove = (i: number) => commit(many ? current.filter((_, j) => j !== i) : null)
|
|
76
66
|
|
|
77
67
|
return (
|
|
78
|
-
<
|
|
79
|
-
variant="popover"
|
|
80
|
-
anchor={element}
|
|
81
|
-
title={field.label}
|
|
82
|
-
onClose={close}
|
|
83
|
-
t={t}
|
|
84
|
-
width={320}
|
|
85
|
-
testId="onpage-relationship"
|
|
86
|
-
>
|
|
68
|
+
<EditorPopover props={props} testId="onpage-relationship" width={320}>
|
|
87
69
|
{current.map((v, i) => (
|
|
88
70
|
<div className="po-rel-item" key={`${String(relationId(v))}-${i}`}>
|
|
89
71
|
{relationTitle(v, titleField)}
|
|
@@ -91,7 +73,7 @@ export function RelationshipEditor({
|
|
|
91
73
|
<button
|
|
92
74
|
type="button"
|
|
93
75
|
onClick={() => remove(i)}
|
|
94
|
-
aria-label={
|
|
76
|
+
aria-label={t('common.removeItem', { item: relationTitle(v, titleField) })}
|
|
95
77
|
>
|
|
96
78
|
✕
|
|
97
79
|
</button>
|
|
@@ -148,6 +130,6 @@ export function RelationshipEditor({
|
|
|
148
130
|
</div>
|
|
149
131
|
</>
|
|
150
132
|
)}
|
|
151
|
-
</
|
|
133
|
+
</EditorPopover>
|
|
152
134
|
)
|
|
153
135
|
}
|