@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
package/src/registry.ts
CHANGED
|
@@ -7,24 +7,37 @@
|
|
|
7
7
|
* No match: the field isn't edited in place (it may still be in the row's
|
|
8
8
|
* settings panel).
|
|
9
9
|
*/
|
|
10
|
-
import type { SchemaField, TargetKind } from '@payload-onpage/core'
|
|
11
|
-
import type { EditorRegistry } from './types.ts'
|
|
10
|
+
import type { FieldOf, SchemaField, SchemaFieldType, TargetKind } from '@payload-onpage/core'
|
|
11
|
+
import type { EditorRegistry, FieldEditorDef, FieldEditorProps } from './types.ts'
|
|
12
|
+
import type { ComponentType } from 'react'
|
|
12
13
|
|
|
13
14
|
const KIND_EDITOR: Partial<Record<TargetKind, string>> = { richtext: 'richText', media: 'upload' }
|
|
14
|
-
|
|
15
|
+
/** Field types a "text" marker or annotation keeps as their own editor (email stays email). */
|
|
16
|
+
const TEXT_TYPES: ReadonlySet<SchemaFieldType> = new Set(['text', 'textarea', 'email'])
|
|
17
|
+
|
|
18
|
+
/** Can this editor take this field? Its declared kinds, and never a list of values in place. */
|
|
19
|
+
export function canEdit(
|
|
20
|
+
def: FieldEditorDef | undefined,
|
|
21
|
+
field: SchemaField,
|
|
22
|
+
): def is FieldEditorDef {
|
|
23
|
+
if (!def) return false
|
|
24
|
+
if (def.accepts && !def.accepts.includes(field.type)) return false
|
|
25
|
+
return !(def.mode === 'inline' && 'hasMany' in field && field.hasMany)
|
|
26
|
+
}
|
|
15
27
|
|
|
16
28
|
export function resolveEditor(
|
|
17
29
|
editors: EditorRegistry,
|
|
18
30
|
field: SchemaField,
|
|
19
31
|
kind?: TargetKind,
|
|
20
32
|
): string | null {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
if ((kind === 'text' || kind === 'textarea') &&
|
|
33
|
+
const ok = (id: string | undefined): id is string => !!id && canEdit(editors[id], field)
|
|
34
|
+
if (ok(field.editor)) return field.editor
|
|
35
|
+
if ((kind === 'text' || kind === 'textarea') && TEXT_TYPES.has(field.type) && ok(field.type))
|
|
24
36
|
return field.type
|
|
25
37
|
const byKind = kind ? KIND_EDITOR[kind] : undefined
|
|
26
|
-
|
|
27
|
-
|
|
38
|
+
// A "media" marker means the image picker only for upload fields (an <img alt> can show a text field).
|
|
39
|
+
if (ok(byKind) && (byKind !== 'upload' || field.type === 'upload')) return byKind
|
|
40
|
+
return ok(field.type) ? field.type : null
|
|
28
41
|
}
|
|
29
42
|
|
|
30
43
|
/** Merge registries left to right; `null` removes a default. */
|
|
@@ -41,3 +54,24 @@ export function mergeEditors(
|
|
|
41
54
|
}
|
|
42
55
|
return out
|
|
43
56
|
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Define a field editor typed by the field kinds it accepts:
|
|
60
|
+
*
|
|
61
|
+
* defineEditor({
|
|
62
|
+
* mode: 'popover',
|
|
63
|
+
* accepts: ['number'],
|
|
64
|
+
* normalize: (raw) => (typeof raw === 'number' ? raw : null),
|
|
65
|
+
* component: ({ field, value }) => … // field: NumberField, value: number | null
|
|
66
|
+
* })
|
|
67
|
+
*/
|
|
68
|
+
export function defineEditor<T extends SchemaFieldType, V = unknown>(def: {
|
|
69
|
+
mode: FieldEditorDef['mode']
|
|
70
|
+
accepts: readonly T[]
|
|
71
|
+
normalize?: (raw: unknown, field: FieldOf<T>) => V
|
|
72
|
+
component: ComponentType<FieldEditorProps<FieldOf<T>, V>>
|
|
73
|
+
}): FieldEditorDef {
|
|
74
|
+
// Widened once, here: resolveEditor only hands it fields whose type is in `accepts`,
|
|
75
|
+
// and FieldHost runs `normalize` before rendering.
|
|
76
|
+
return def as unknown as FieldEditorDef
|
|
77
|
+
}
|
package/src/rowOps.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Row operations on array and blocks fields, with one set of rules (bounds,
|
|
3
|
+
* `minRows`, `maxRows`) for the toolbar, the keyboard shortcuts, the block
|
|
4
|
+
* picker and "+ Add". Each returns where the selection should go, or null when
|
|
5
|
+
* the operation isn't allowed (nothing changes).
|
|
6
|
+
*/
|
|
7
|
+
import {
|
|
8
|
+
cloneRow,
|
|
9
|
+
insertItem,
|
|
10
|
+
moveItem,
|
|
11
|
+
removeItem,
|
|
12
|
+
updateArray,
|
|
13
|
+
type EditorSession,
|
|
14
|
+
type RowInfo,
|
|
15
|
+
type RefKey,
|
|
16
|
+
} from '@payload-onpage/core'
|
|
17
|
+
import type { Selection } from './types.ts'
|
|
18
|
+
|
|
19
|
+
const rowAt = (key: RefKey, listPath: string, index: number): Selection => ({
|
|
20
|
+
key,
|
|
21
|
+
path: `${listPath}.${index}`,
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
/** Insert a row into the list at `listPath`; returns the new row's selection. */
|
|
25
|
+
export function insertRow(
|
|
26
|
+
session: EditorSession,
|
|
27
|
+
key: RefKey,
|
|
28
|
+
listPath: string,
|
|
29
|
+
index: number,
|
|
30
|
+
row: unknown,
|
|
31
|
+
): Selection {
|
|
32
|
+
session.apply(key, (d) => updateArray(d, listPath, (a) => insertItem(a, index, row)))
|
|
33
|
+
return rowAt(key, listPath, index)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function rowOps(session: EditorSession, key: RefKey, info: RowInfo) {
|
|
37
|
+
const { listPath, index, length, rowFields, listField } = info
|
|
38
|
+
const apply = (fn: (rows: unknown[]) => unknown[]) =>
|
|
39
|
+
session.apply(key, (d) => updateArray(d, listPath, fn))
|
|
40
|
+
const canMove = (dir: -1 | 1) => index + dir >= 0 && index + dir < length
|
|
41
|
+
const canDuplicate = listField.maxRows === undefined || length < listField.maxRows
|
|
42
|
+
const canRemove = length > (listField.minRows ?? 0)
|
|
43
|
+
return {
|
|
44
|
+
canMove,
|
|
45
|
+
canDuplicate,
|
|
46
|
+
canRemove,
|
|
47
|
+
move(dir: -1 | 1): Selection | null {
|
|
48
|
+
if (!canMove(dir)) return null
|
|
49
|
+
apply((a) => moveItem(a, index, index + dir))
|
|
50
|
+
return rowAt(key, listPath, index + dir)
|
|
51
|
+
},
|
|
52
|
+
duplicate(): Selection | null {
|
|
53
|
+
if (!canDuplicate) return null
|
|
54
|
+
apply((a) => insertItem(a, index + 1, cloneRow(a[index], rowFields)))
|
|
55
|
+
return rowAt(key, listPath, index + 1)
|
|
56
|
+
},
|
|
57
|
+
/** Removes the row; true when it did. */
|
|
58
|
+
remove(): boolean {
|
|
59
|
+
if (!canRemove) return false
|
|
60
|
+
apply((a) => removeItem(a, index))
|
|
61
|
+
return true
|
|
62
|
+
},
|
|
63
|
+
}
|
|
64
|
+
}
|
package/src/sanitize.ts
CHANGED
|
@@ -50,3 +50,9 @@ export function sanitizeHtml(html: string): string {
|
|
|
50
50
|
walk(tpl.content)
|
|
51
51
|
return tpl.innerHTML
|
|
52
52
|
}
|
|
53
|
+
|
|
54
|
+
/** Text made safe to put in HTML. */
|
|
55
|
+
export const escapeHtml = (s: string): string =>
|
|
56
|
+
s.replace(/[&<>"]/g, (c) => ESCAPES[c as keyof typeof ESCAPES])
|
|
57
|
+
|
|
58
|
+
const ESCAPES = { '&': '&', '<': '<', '>': '>', '"': '"' } as const
|
package/src/settings.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which fields of a row appear in its settings panel: values that aren't edited
|
|
3
|
+
* on the page itself (layout selects, toggles, numbers, links, images that
|
|
4
|
+
* aren't shown…). Fields shown on the page are edited there instead.
|
|
5
|
+
*/
|
|
6
|
+
import type { SchemaField, SchemaFieldType, RefKey } from '@payload-onpage/core'
|
|
7
|
+
import type { TargetIndex } from './types.ts'
|
|
8
|
+
|
|
9
|
+
/** Row bookkeeping, never a setting. */
|
|
10
|
+
const SKIP = new Set(['id', 'blockType', 'blockName'])
|
|
11
|
+
|
|
12
|
+
/** Types shown in the panel when the page doesn't show them (choices and toggles always are). */
|
|
13
|
+
const PANEL_FIELD_TYPES: ReadonlySet<SchemaFieldType> = new Set([
|
|
14
|
+
'text',
|
|
15
|
+
'textarea',
|
|
16
|
+
'email',
|
|
17
|
+
'number',
|
|
18
|
+
'date',
|
|
19
|
+
'relationship',
|
|
20
|
+
'upload',
|
|
21
|
+
])
|
|
22
|
+
|
|
23
|
+
export function settable(
|
|
24
|
+
field: SchemaField,
|
|
25
|
+
path: string,
|
|
26
|
+
key: RefKey,
|
|
27
|
+
targets: TargetIndex,
|
|
28
|
+
): boolean {
|
|
29
|
+
if (SKIP.has(field.name)) return false
|
|
30
|
+
const full = `${path}.${field.name}`
|
|
31
|
+
switch (field.type) {
|
|
32
|
+
case 'select':
|
|
33
|
+
case 'radio':
|
|
34
|
+
case 'checkbox':
|
|
35
|
+
return true
|
|
36
|
+
case 'group':
|
|
37
|
+
return field.fields.some((f) => settable(f, full, key, targets))
|
|
38
|
+
default:
|
|
39
|
+
// Shown on the page? Edit it there.
|
|
40
|
+
return PANEL_FIELD_TYPES.has(field.type) && !targets.find(key, full)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const hasSettings = (
|
|
45
|
+
fields: SchemaField[],
|
|
46
|
+
path: string,
|
|
47
|
+
key: RefKey,
|
|
48
|
+
targets: TargetIndex,
|
|
49
|
+
): boolean => fields.some((f) => settable(f, path, key, targets))
|
package/src/styles.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* The editor's own styles, injected once. Everything is prefixed `po-` and set
|
|
3
3
|
* explicitly, so the host site's CSS (and its absence) doesn't matter.
|
|
4
4
|
*/
|
|
5
|
+
import { DOM_ATTR } from './dom.ts'
|
|
5
6
|
export const PO_Z = 2147483000
|
|
6
7
|
|
|
7
8
|
export const PO_CSS = `
|
|
@@ -116,7 +117,7 @@ export function injectStyles() {
|
|
|
116
117
|
if (injected || typeof document === 'undefined') return
|
|
117
118
|
injected = true
|
|
118
119
|
const el = document.createElement('style')
|
|
119
|
-
el.setAttribute(
|
|
120
|
+
el.setAttribute(DOM_ATTR.styles, '')
|
|
120
121
|
el.textContent = PO_CSS
|
|
121
122
|
document.head.appendChild(el)
|
|
122
123
|
}
|
package/src/targets.ts
CHANGED
|
@@ -42,7 +42,7 @@ export function buildIndex(all: Target[]): TargetIndex {
|
|
|
42
42
|
export function targetsAt(
|
|
43
43
|
index: TargetIndex,
|
|
44
44
|
el: Element | null,
|
|
45
|
-
): { field?: Target; rows: Target[] } {
|
|
45
|
+
): { field?: Target | undefined; rows: Target[] } {
|
|
46
46
|
let field: Target | undefined
|
|
47
47
|
const rows: Target[] = []
|
|
48
48
|
for (let node = el; node; node = node.parentElement) {
|
package/src/types.ts
CHANGED
|
@@ -6,41 +6,47 @@ import type {
|
|
|
6
6
|
MediaLibrary,
|
|
7
7
|
Renderer,
|
|
8
8
|
SchemaField,
|
|
9
|
+
SchemaFieldType,
|
|
9
10
|
SessionSnapshot,
|
|
10
11
|
Target,
|
|
11
12
|
TargetKind,
|
|
13
|
+
RefKey,
|
|
12
14
|
} from '@payload-onpage/core'
|
|
13
|
-
import type {
|
|
15
|
+
import type { Translate } from './messages.ts'
|
|
14
16
|
|
|
15
17
|
export interface EditingState {
|
|
16
18
|
/** refKey of the document */
|
|
17
|
-
key:
|
|
19
|
+
key: RefKey
|
|
18
20
|
path: string
|
|
19
21
|
/** Editor id from the registry ("text", "richText", …). */
|
|
20
22
|
editor: string
|
|
21
|
-
point?: { x: number; y: number }
|
|
23
|
+
point?: { x: number; y: number } | undefined
|
|
22
24
|
/** The element that was clicked (several elements may show one field). */
|
|
23
|
-
el?: Element
|
|
25
|
+
el?: Element | undefined
|
|
24
26
|
}
|
|
25
27
|
|
|
28
|
+
/** Which panel is open. */
|
|
26
29
|
export type Panel =
|
|
27
|
-
| { kind: 'settings'; key:
|
|
28
|
-
| { kind: 'blocks'; key:
|
|
30
|
+
| { kind: 'settings'; key: RefKey; path: string; title: string }
|
|
31
|
+
| { kind: 'blocks'; key: RefKey; listPath: string; index: number }
|
|
29
32
|
| null
|
|
30
33
|
|
|
31
34
|
export interface Selection {
|
|
32
|
-
key:
|
|
35
|
+
key: RefKey
|
|
33
36
|
path: string
|
|
34
37
|
}
|
|
35
38
|
|
|
36
|
-
/**
|
|
37
|
-
|
|
38
|
-
|
|
39
|
+
/**
|
|
40
|
+
* Props a field editor receives. `F` is the field kind it edits (e.g.
|
|
41
|
+
* `NumberField`, with `min`/`max`) and `V` its value after `normalize`.
|
|
42
|
+
*/
|
|
43
|
+
export interface FieldEditorProps<F extends SchemaField = SchemaField, V = unknown> {
|
|
44
|
+
docKey: RefKey
|
|
39
45
|
path: string
|
|
40
|
-
field:
|
|
46
|
+
field: F
|
|
41
47
|
/** The element showing the field (re-resolved after each refresh). */
|
|
42
48
|
element: () => Element | null
|
|
43
|
-
value:
|
|
49
|
+
value: V
|
|
44
50
|
/** Commit a value (one undo step, autosaved). */
|
|
45
51
|
commit: (value: unknown) => void
|
|
46
52
|
/** Live preview while editing (no history entry, not saved). */
|
|
@@ -48,23 +54,35 @@ export interface FieldEditorProps {
|
|
|
48
54
|
/** Close the editor (after commit, or to cancel: transient previews are reverted). */
|
|
49
55
|
close: () => void
|
|
50
56
|
/** Leave a stand-in showing the new content until the page re-renders (refresh mode). */
|
|
51
|
-
ghost: (g:
|
|
57
|
+
ghost: (g: GhostInput) => void
|
|
52
58
|
/** True when the page renders this document from editor state (instant); false = server refresh. */
|
|
53
59
|
live: boolean
|
|
54
|
-
point?: { x: number; y: number }
|
|
60
|
+
point?: { x: number; y: number } | undefined
|
|
55
61
|
/** UI strings. */
|
|
56
62
|
t: Translate
|
|
57
63
|
}
|
|
58
64
|
|
|
65
|
+
/** What an editor leaves on the page until it re-renders. */
|
|
66
|
+
export interface GhostInput {
|
|
67
|
+
html: string
|
|
68
|
+
className?: string | undefined
|
|
69
|
+
style?: Partial<CSSStyleDeclaration> | undefined
|
|
70
|
+
}
|
|
71
|
+
|
|
59
72
|
/**
|
|
60
73
|
* A field editor (the FieldEditor port). `inline` editors replace the element
|
|
61
74
|
* in place (text, rich text); `popover` editors open beside it or as a dialog
|
|
62
75
|
* (select, date, relationship, image…). Register your own by id and refer to it
|
|
63
76
|
* from a field with `admin: { custom: { onpage: { editor: 'my-id' } } }`, or by
|
|
64
|
-
* field type ("json", "code"…) to replace or add a default.
|
|
77
|
+
* field type ("json", "code"…) to replace or add a default. Build one with
|
|
78
|
+
* `defineEditor()` so its props are typed by the field kinds it accepts.
|
|
65
79
|
*/
|
|
66
80
|
export interface FieldEditorDef {
|
|
67
81
|
mode: 'inline' | 'popover'
|
|
82
|
+
/** Field types it can edit. The registry never hands it any other kind. Default: any. */
|
|
83
|
+
accepts?: readonly SchemaFieldType[]
|
|
84
|
+
/** The raw stored value → the editor's value. Default: passed through. */
|
|
85
|
+
normalize?: (raw: unknown, field: SchemaField) => unknown
|
|
68
86
|
component: ComponentType<FieldEditorProps>
|
|
69
87
|
}
|
|
70
88
|
|
|
@@ -81,52 +99,67 @@ export interface TargetIndex {
|
|
|
81
99
|
all: Target[]
|
|
82
100
|
/** Targets whose element is exactly `el`. */
|
|
83
101
|
at: (el: Element) => Target[]
|
|
84
|
-
find: (key:
|
|
102
|
+
find: (key: RefKey, path: string, kinds?: TargetKind[]) => Target | undefined
|
|
85
103
|
}
|
|
86
104
|
|
|
87
|
-
/**
|
|
88
|
-
export interface
|
|
105
|
+
/** A short message shown in the dock. */
|
|
106
|
+
export interface Flash {
|
|
107
|
+
text: string
|
|
108
|
+
bad?: boolean | undefined
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The editor's stable API: the session, configuration and actions. It changes
|
|
113
|
+
* only when the editor is reconfigured, so components that only act (toolbars,
|
|
114
|
+
* custom UI, client-mode documents) don't re-render on every keystroke.
|
|
115
|
+
*/
|
|
116
|
+
export interface OnPageApi {
|
|
89
117
|
session: EditorSession
|
|
90
|
-
snap: SessionSnapshot
|
|
91
118
|
renderer: Renderer
|
|
92
|
-
media?: MediaLibrary
|
|
119
|
+
media?: MediaLibrary | undefined
|
|
93
120
|
editors: EditorRegistry
|
|
94
|
-
targets: TargetIndex
|
|
95
121
|
links: Links
|
|
96
122
|
t: Translate
|
|
97
|
-
panel: Panel
|
|
98
123
|
setPanel: (p: Panel) => void
|
|
99
|
-
editing: EditingState | null
|
|
100
124
|
setEditing: (e: EditingState | null) => void
|
|
101
|
-
selected: Selection | null
|
|
102
125
|
setSelected: (s: Selection | null) => void
|
|
103
|
-
/** Documents shown on the current page (the session may hold others from earlier pages). */
|
|
104
|
-
docKeys: string[]
|
|
105
126
|
/** Is this document rendered from session state (client mode)? */
|
|
106
|
-
isLive: (key:
|
|
127
|
+
isLive: (key: RefKey) => boolean
|
|
107
128
|
/** Client-mode documents register here (they don't need a server refresh). */
|
|
108
|
-
registerClientDoc: (key:
|
|
109
|
-
/** The page is re-rendering after a save (refresh mode). */
|
|
110
|
-
rendering: boolean
|
|
129
|
+
registerClientDoc: (key: RefKey) => () => void
|
|
111
130
|
/** Open the right editor for a field (registry resolution); false if none applies. */
|
|
112
131
|
edit: (
|
|
113
|
-
key:
|
|
132
|
+
key: RefKey,
|
|
114
133
|
path: string,
|
|
115
|
-
opts?: {
|
|
134
|
+
opts?: {
|
|
135
|
+
point?: { x: number; y: number } | undefined
|
|
136
|
+
kind?: TargetKind | undefined
|
|
137
|
+
anchor?: Element | undefined
|
|
138
|
+
},
|
|
116
139
|
) => boolean
|
|
117
140
|
/** Re-render documents after an operation outside autosave (publish, reload). */
|
|
118
141
|
rerender: (refs: DocRef[]) => Promise<void>
|
|
119
|
-
/** Show a short message in the dock. */
|
|
120
|
-
notify: (
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
142
|
+
/** Show a short message in the dock: translated text (`notify(t('notify.published'))`), or an error's text. */
|
|
143
|
+
notify: (text: string, bad?: boolean) => void
|
|
144
|
+
addGhost: (g: GhostInput & { key: RefKey; path: string }) => void
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** What changes as people edit: the documents, what's on the page and what's open. */
|
|
148
|
+
export interface OnPageState {
|
|
149
|
+
snap: SessionSnapshot
|
|
150
|
+
targets: TargetIndex
|
|
151
|
+
panel: Panel
|
|
152
|
+
editing: EditingState | null
|
|
153
|
+
selected: Selection | null
|
|
154
|
+
/** Documents shown on the current page (the session may hold others from earlier pages). */
|
|
155
|
+
docKeys: RefKey[]
|
|
156
|
+
/** The page is re-rendering after a save (refresh mode). */
|
|
157
|
+
rendering: boolean
|
|
158
|
+
/** The dock's current message. */
|
|
159
|
+
flash: Flash | null
|
|
132
160
|
}
|
|
161
|
+
|
|
162
|
+
/** Everything inside the editor (`useOnPage()`): its API and its state. */
|
|
163
|
+
export type OnPageContextValue = OnPageApi & OnPageState
|
|
164
|
+
|
|
165
|
+
export type PanelKind = NonNullable<Panel>['kind']
|
package/src/ui/BlockPicker.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { fieldAtPath,
|
|
3
|
+
import { fieldAtPath, isReady, newBlockRow } from '@payload-onpage/core'
|
|
4
|
+
import { insertRow } from '../rowOps.ts'
|
|
4
5
|
import { useOnPage } from '../context.tsx'
|
|
5
6
|
import { Dialog } from './Dialog.tsx'
|
|
6
7
|
|
|
@@ -9,19 +10,17 @@ export function BlockPicker() {
|
|
|
9
10
|
const { panel, setPanel, snap, session, setSelected, t } = useOnPage()
|
|
10
11
|
if (panel?.kind !== 'blocks') return null
|
|
11
12
|
const state = snap.docs[panel.key]
|
|
12
|
-
if (!state
|
|
13
|
+
if (!isReady(state)) return null
|
|
13
14
|
const { key, listPath, index } = panel
|
|
14
15
|
const field = fieldAtPath(state.schema.fields, state.doc, listPath)?.field
|
|
15
|
-
const blocks = field?.type === 'blocks' ?
|
|
16
|
+
const blocks = field?.type === 'blocks' ? field.blocks : []
|
|
16
17
|
|
|
17
18
|
const choose = (slug: string) => {
|
|
18
19
|
const block = blocks.find((b) => b.slug === slug)
|
|
19
20
|
if (!block) return
|
|
20
|
-
session
|
|
21
|
-
updateArray(d, listPath, (a) => insertItem(a, index, newBlockRow(block))),
|
|
22
|
-
)
|
|
21
|
+
const next = insertRow(session, key, listPath, index, newBlockRow(block))
|
|
23
22
|
setPanel(null)
|
|
24
|
-
setSelected(
|
|
23
|
+
setSelected(next)
|
|
25
24
|
}
|
|
26
25
|
|
|
27
26
|
return (
|
package/src/ui/Dialog.tsx
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* close, Tab stays inside (modal), Escape closes. `variant="popover"` is
|
|
6
6
|
* anchored next to an element and closes on an outside click.
|
|
7
7
|
*/
|
|
8
|
+
import { useEvent } from '../useEvent.ts'
|
|
8
9
|
import { useEffect, useId, useRef, useState, type CSSProperties, type ReactNode } from 'react'
|
|
9
10
|
import { focusableIn, popoverPosition } from '../dom.ts'
|
|
10
11
|
import { useLayoutTick } from '../layout.ts'
|
|
@@ -62,18 +63,21 @@ export function Dialog({
|
|
|
62
63
|
return () => ro.disconnect()
|
|
63
64
|
}, [variant])
|
|
64
65
|
|
|
66
|
+
// Stable, so the listeners below aren't re-bound on every render (callers pass inline
|
|
67
|
+
// closures); a re-bind would also re-open the deferral gap where an outside click is lost.
|
|
68
|
+
const close = useEvent(onClose)
|
|
65
69
|
useEffect(() => {
|
|
66
70
|
const onKey = (e: KeyboardEvent) => {
|
|
67
71
|
if (e.key === 'Escape') {
|
|
68
72
|
e.stopPropagation()
|
|
69
|
-
|
|
73
|
+
close()
|
|
70
74
|
return
|
|
71
75
|
}
|
|
72
76
|
if (e.key !== 'Tab' || variant !== 'modal' || !box.current) return
|
|
73
77
|
const items = focusableIn(box.current)
|
|
74
|
-
if (!items.length) return
|
|
75
78
|
const first = items[0]
|
|
76
|
-
const last = items
|
|
79
|
+
const last = items.at(-1)
|
|
80
|
+
if (!first || !last) return
|
|
77
81
|
if (e.shiftKey && document.activeElement === first) {
|
|
78
82
|
e.preventDefault()
|
|
79
83
|
last.focus()
|
|
@@ -83,7 +87,7 @@ export function Dialog({
|
|
|
83
87
|
}
|
|
84
88
|
}
|
|
85
89
|
const onDown = (e: PointerEvent) => {
|
|
86
|
-
if (variant === 'popover' && box.current && !box.current.contains(e.target as Node))
|
|
90
|
+
if (variant === 'popover' && box.current && !box.current.contains(e.target as Node)) close()
|
|
87
91
|
}
|
|
88
92
|
document.addEventListener('keydown', onKey, true)
|
|
89
93
|
// Deferred, so the click that opened a popover doesn't close it.
|
|
@@ -93,7 +97,7 @@ export function Dialog({
|
|
|
93
97
|
document.removeEventListener('keydown', onKey, true)
|
|
94
98
|
document.removeEventListener('pointerdown', onDown, true)
|
|
95
99
|
}
|
|
96
|
-
}, [
|
|
100
|
+
}, [close, variant])
|
|
97
101
|
|
|
98
102
|
const header = (
|
|
99
103
|
<header className="po-dialog-head">
|