@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
|
@@ -1,20 +1,19 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { asList, type ChoiceField } from '@payload-onpage/core'
|
|
4
4
|
import type { FieldEditorProps } from '../types.ts'
|
|
5
|
-
import {
|
|
5
|
+
import { EditorPopover } from './kit.tsx'
|
|
6
|
+
|
|
7
|
+
/** A select/radio value as the selected option values. */
|
|
8
|
+
export const selectedValues = (raw: unknown, field: ChoiceField): string[] =>
|
|
9
|
+
asList(raw, field.hasMany, (v) =>
|
|
10
|
+
typeof v === 'string' || typeof v === 'number' ? String(v) : undefined,
|
|
11
|
+
)
|
|
6
12
|
|
|
7
13
|
/** Select / radio: pick one (closes) or toggle several (hasMany). */
|
|
8
|
-
export function SelectEditor(
|
|
9
|
-
const
|
|
14
|
+
export function SelectEditor(props: FieldEditorProps<ChoiceField, string[]>) {
|
|
15
|
+
const { field, value: current, commit, close, t } = props
|
|
10
16
|
const many = !!field.hasMany
|
|
11
|
-
const current: string[] = many
|
|
12
|
-
? Array.isArray(value)
|
|
13
|
-
? (value as string[])
|
|
14
|
-
: []
|
|
15
|
-
: value == null
|
|
16
|
-
? []
|
|
17
|
-
: [toText(value)]
|
|
18
17
|
const pick = (v: string | null) => {
|
|
19
18
|
if (!many) {
|
|
20
19
|
commit(v)
|
|
@@ -25,17 +24,10 @@ export function SelectEditor({ field, value, element, commit, close, t }: FieldE
|
|
|
25
24
|
if (v === null) set.clear()
|
|
26
25
|
else if (set.has(v)) set.delete(v)
|
|
27
26
|
else set.add(v)
|
|
28
|
-
commit(options.map((o) => o.value).filter((o) => set.has(o)))
|
|
27
|
+
commit(field.options.map((o) => o.value).filter((o) => set.has(o)))
|
|
29
28
|
}
|
|
30
29
|
return (
|
|
31
|
-
<
|
|
32
|
-
variant="popover"
|
|
33
|
-
anchor={element}
|
|
34
|
-
title={field.label}
|
|
35
|
-
onClose={close}
|
|
36
|
-
t={t}
|
|
37
|
-
testId="onpage-select"
|
|
38
|
-
>
|
|
30
|
+
<EditorPopover props={props} testId="onpage-select">
|
|
39
31
|
<div
|
|
40
32
|
className="po-pop-list"
|
|
41
33
|
role="listbox"
|
|
@@ -52,7 +44,7 @@ export function SelectEditor({ field, value, element, commit, close, t }: FieldE
|
|
|
52
44
|
{t('common.none')}
|
|
53
45
|
</button>
|
|
54
46
|
)}
|
|
55
|
-
{options.map((o) => {
|
|
47
|
+
{field.options.map((o) => {
|
|
56
48
|
const selected = current.includes(o.value)
|
|
57
49
|
return (
|
|
58
50
|
<button
|
|
@@ -69,6 +61,6 @@ export function SelectEditor({ field, value, element, commit, close, t }: FieldE
|
|
|
69
61
|
)
|
|
70
62
|
})}
|
|
71
63
|
</div>
|
|
72
|
-
</
|
|
64
|
+
</EditorPopover>
|
|
73
65
|
)
|
|
74
66
|
}
|
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
* new file, through the MediaLibrary port. The chosen document goes into state
|
|
6
6
|
* populated, so a client-rendered page shows it at once; saving sends its id.
|
|
7
7
|
*/
|
|
8
|
-
import { relationId, type MediaItem } from '@payload-onpage/core'
|
|
8
|
+
import { errorText, relationId, type MediaItem, type RelationField } from '@payload-onpage/core'
|
|
9
9
|
import { useCallback, useRef, useState } from 'react'
|
|
10
10
|
import { useOnPage } from '../context.tsx'
|
|
11
11
|
import type { FieldEditorProps } from '../types.ts'
|
|
12
12
|
import { Dialog } from '../ui/Dialog.tsx'
|
|
13
13
|
import { useDebouncedSearch } from './useDebouncedSearch.ts'
|
|
14
14
|
|
|
15
|
-
export function UploadEditor({ field, value, commit, close, t }: FieldEditorProps) {
|
|
15
|
+
export function UploadEditor({ field, value, commit, close, t }: FieldEditorProps<RelationField>) {
|
|
16
16
|
const { media } = useOnPage()
|
|
17
17
|
const relationTo =
|
|
18
18
|
(Array.isArray(field.relationTo) ? field.relationTo[0] : field.relationTo) ?? 'media'
|
|
@@ -20,13 +20,13 @@ export function UploadEditor({ field, value, commit, close, t }: FieldEditorProp
|
|
|
20
20
|
const [uploadError, setUploadError] = useState<string | null>(null)
|
|
21
21
|
const fileRef = useRef<HTMLInputElement>(null)
|
|
22
22
|
const search = useCallback(
|
|
23
|
-
(query: string, page: number) =>
|
|
23
|
+
(query: string, page: number, signal: AbortSignal) =>
|
|
24
24
|
media
|
|
25
|
-
? media.search({ query, page, relationTo })
|
|
25
|
+
? media.search({ query, page, relationTo, signal })
|
|
26
26
|
: Promise.resolve({ items: [] as MediaItem[], hasMore: false }),
|
|
27
27
|
[media, relationTo],
|
|
28
28
|
)
|
|
29
|
-
const results = useDebouncedSearch<MediaItem>(media ? search : null
|
|
29
|
+
const results = useDebouncedSearch<MediaItem>(media ? search : null)
|
|
30
30
|
const currentId = relationId(value)
|
|
31
31
|
|
|
32
32
|
const choose = (m: MediaItem) => {
|
|
@@ -39,7 +39,7 @@ export function UploadEditor({ field, value, commit, close, t }: FieldEditorProp
|
|
|
39
39
|
try {
|
|
40
40
|
choose(await media.upload(file, { relationTo }))
|
|
41
41
|
} catch (e) {
|
|
42
|
-
setUploadError((e
|
|
42
|
+
setUploadError(errorText(e))
|
|
43
43
|
} finally {
|
|
44
44
|
setUploading(false)
|
|
45
45
|
}
|
package/src/editors/index.ts
CHANGED
|
@@ -1,26 +1,66 @@
|
|
|
1
1
|
/** The default field editors. Spread, override or remove (`null`) entries via the `editors` option. */
|
|
2
|
+
import { toText } from '@payload-onpage/core'
|
|
3
|
+
import { defineEditor } from '../registry.ts'
|
|
2
4
|
import type { EditorRegistry } from '../types.ts'
|
|
3
|
-
import { CheckboxEditor } from './CheckboxEditor.tsx'
|
|
4
|
-
import { ColorEditor } from './ColorEditor.tsx'
|
|
5
|
-
import { DateEditor } from './DateEditor.tsx'
|
|
5
|
+
import { checkedOf, CheckboxEditor } from './CheckboxEditor.tsx'
|
|
6
|
+
import { ColorEditor, toColor } from './ColorEditor.tsx'
|
|
7
|
+
import { DateEditor, toIsoDate } from './DateEditor.tsx'
|
|
6
8
|
import { InlineText } from './InlineText.tsx'
|
|
7
|
-
import { NumberEditor } from './NumberEditor.tsx'
|
|
9
|
+
import { NumberEditor, toNumber } from './NumberEditor.tsx'
|
|
8
10
|
import { RelationshipEditor } from './RelationshipEditor.tsx'
|
|
9
|
-
import { SelectEditor } from './SelectEditor.tsx'
|
|
11
|
+
import { SelectEditor, selectedValues } from './SelectEditor.tsx'
|
|
10
12
|
import { UploadEditor } from './UploadEditor.tsx'
|
|
11
13
|
|
|
14
|
+
const text = defineEditor({
|
|
15
|
+
mode: 'inline',
|
|
16
|
+
accepts: ['text', 'textarea', 'email'],
|
|
17
|
+
normalize: (raw) => toText(raw),
|
|
18
|
+
component: InlineText,
|
|
19
|
+
})
|
|
20
|
+
const select = defineEditor({
|
|
21
|
+
mode: 'popover',
|
|
22
|
+
accepts: ['select', 'radio'],
|
|
23
|
+
normalize: selectedValues,
|
|
24
|
+
component: SelectEditor,
|
|
25
|
+
})
|
|
26
|
+
|
|
12
27
|
export const defaultEditors: EditorRegistry = {
|
|
13
|
-
text
|
|
14
|
-
textarea:
|
|
15
|
-
email:
|
|
16
|
-
select
|
|
17
|
-
radio:
|
|
18
|
-
checkbox: {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
28
|
+
text,
|
|
29
|
+
textarea: text,
|
|
30
|
+
email: text,
|
|
31
|
+
select,
|
|
32
|
+
radio: select,
|
|
33
|
+
checkbox: defineEditor({
|
|
34
|
+
mode: 'popover',
|
|
35
|
+
accepts: ['checkbox'],
|
|
36
|
+
normalize: checkedOf,
|
|
37
|
+
component: CheckboxEditor,
|
|
38
|
+
}),
|
|
39
|
+
number: defineEditor({
|
|
40
|
+
mode: 'popover',
|
|
41
|
+
accepts: ['number'],
|
|
42
|
+
normalize: toNumber,
|
|
43
|
+
component: NumberEditor,
|
|
44
|
+
}),
|
|
45
|
+
date: defineEditor({
|
|
46
|
+
mode: 'popover',
|
|
47
|
+
accepts: ['date'],
|
|
48
|
+
normalize: toIsoDate,
|
|
49
|
+
component: DateEditor,
|
|
50
|
+
}),
|
|
51
|
+
relationship: defineEditor({
|
|
52
|
+
mode: 'popover',
|
|
53
|
+
accepts: ['relationship'],
|
|
54
|
+
component: RelationshipEditor,
|
|
55
|
+
}),
|
|
56
|
+
upload: defineEditor({ mode: 'popover', accepts: ['upload'], component: UploadEditor }),
|
|
57
|
+
// A text field holding a hex color: `admin.custom.onpage.editor: 'color'`.
|
|
58
|
+
color: defineEditor({
|
|
59
|
+
mode: 'popover',
|
|
60
|
+
accepts: ['text'],
|
|
61
|
+
normalize: toColor,
|
|
62
|
+
component: ColorEditor,
|
|
63
|
+
}),
|
|
24
64
|
}
|
|
25
65
|
|
|
26
66
|
export {
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The parts every popover editor shares: the anchored dialog, the Apply/Clear
|
|
5
|
+
* bar, and a draft that previews live and commits once. Exported from
|
|
6
|
+
* `@payload-onpage/react/editor-kit` for custom editors.
|
|
7
|
+
*/
|
|
8
|
+
import { useState, type ReactNode } from 'react'
|
|
9
|
+
import type { Translate } from '../messages.ts'
|
|
10
|
+
import type { FieldEditorProps } from '../types.ts'
|
|
11
|
+
import { Dialog } from '../ui/Dialog.tsx'
|
|
12
|
+
|
|
13
|
+
/** The popover a field editor opens beside its element, titled with the field's label. */
|
|
14
|
+
export function EditorPopover({
|
|
15
|
+
props,
|
|
16
|
+
testId,
|
|
17
|
+
width,
|
|
18
|
+
children,
|
|
19
|
+
}: {
|
|
20
|
+
props: Pick<FieldEditorProps, 'element' | 'field' | 'close' | 't'>
|
|
21
|
+
testId: string
|
|
22
|
+
width?: number
|
|
23
|
+
children: ReactNode
|
|
24
|
+
}) {
|
|
25
|
+
return (
|
|
26
|
+
<Dialog
|
|
27
|
+
variant="popover"
|
|
28
|
+
anchor={props.element}
|
|
29
|
+
title={props.field.label}
|
|
30
|
+
onClose={props.close}
|
|
31
|
+
t={props.t}
|
|
32
|
+
testId={testId}
|
|
33
|
+
{...(width ? { width } : {})}
|
|
34
|
+
>
|
|
35
|
+
{children}
|
|
36
|
+
</Dialog>
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Apply, plus Clear when the field may be empty. */
|
|
41
|
+
export function ApplyBar({
|
|
42
|
+
onApply,
|
|
43
|
+
onClear,
|
|
44
|
+
t,
|
|
45
|
+
}: {
|
|
46
|
+
onApply: () => void
|
|
47
|
+
onClear?: (() => void) | undefined
|
|
48
|
+
t: Translate
|
|
49
|
+
}) {
|
|
50
|
+
return (
|
|
51
|
+
<div className="po-actions">
|
|
52
|
+
<button
|
|
53
|
+
type="button"
|
|
54
|
+
className="po-btn po-primary"
|
|
55
|
+
onClick={onApply}
|
|
56
|
+
data-testid="onpage-apply"
|
|
57
|
+
>
|
|
58
|
+
{t('common.apply')}
|
|
59
|
+
</button>
|
|
60
|
+
{onClear && (
|
|
61
|
+
<button type="button" className="po-btn" onClick={onClear} data-testid="onpage-clear">
|
|
62
|
+
{t('common.clear')}
|
|
63
|
+
</button>
|
|
64
|
+
)}
|
|
65
|
+
</div>
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* A text draft for a value: `parse` turns the draft into a value (undefined =
|
|
71
|
+
* not valid yet). Valid drafts preview live when the page renders from editor
|
|
72
|
+
* state; `save` commits only a changed, valid value, then closes.
|
|
73
|
+
*/
|
|
74
|
+
export function useDraft<V>(
|
|
75
|
+
props: Pick<FieldEditorProps<never, V>, 'value' | 'commit' | 'preview' | 'close' | 'live'>,
|
|
76
|
+
{
|
|
77
|
+
format,
|
|
78
|
+
parse,
|
|
79
|
+
}: { format: (value: V) => string; parse: (draft: string) => V | null | undefined },
|
|
80
|
+
) {
|
|
81
|
+
const [draft, setDraft] = useState(() => format(props.value))
|
|
82
|
+
const change = (next: string) => {
|
|
83
|
+
setDraft(next)
|
|
84
|
+
const value = parse(next)
|
|
85
|
+
if (props.live && value !== undefined && value !== null) props.preview(value)
|
|
86
|
+
}
|
|
87
|
+
const save = () => {
|
|
88
|
+
const value = parse(draft)
|
|
89
|
+
if (value !== undefined && value !== props.value) props.commit(value)
|
|
90
|
+
props.close()
|
|
91
|
+
}
|
|
92
|
+
/** Enter applies. */
|
|
93
|
+
const onKeyDown = (e: { key: string }) => {
|
|
94
|
+
if (e.key === 'Enter') save()
|
|
95
|
+
}
|
|
96
|
+
return { draft, change, save, onKeyDown, clear: () => change('') }
|
|
97
|
+
}
|
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
+
import { errorText } from '@payload-onpage/core'
|
|
3
4
|
import { useEffect, useState } from 'react'
|
|
4
5
|
|
|
6
|
+
/** A search the hook runs: it should stop when `signal` aborts. */
|
|
7
|
+
export type SearchFn<T> = (
|
|
8
|
+
query: string,
|
|
9
|
+
page: number,
|
|
10
|
+
signal: AbortSignal,
|
|
11
|
+
) => Promise<{ items: T[]; hasMore: boolean }>
|
|
12
|
+
|
|
5
13
|
/**
|
|
6
|
-
* Run `search(query, page)` 250ms after the inputs settle; results
|
|
7
|
-
* across pages and reset when the query changes.
|
|
14
|
+
* Run `search(query, page, signal)` 250ms after the inputs settle; results
|
|
15
|
+
* accumulate across pages and reset when the query changes. A superseded search
|
|
16
|
+
* is aborted, and its response ignored. Re-runs when `search` changes, so keep
|
|
17
|
+
* it stable (`useCallback`) with its inputs as dependencies.
|
|
8
18
|
*/
|
|
9
|
-
export function useDebouncedSearch<T>(
|
|
10
|
-
search: ((query: string, page: number) => Promise<{ items: T[]; hasMore: boolean }>) | null,
|
|
11
|
-
deps: unknown[],
|
|
12
|
-
) {
|
|
19
|
+
export function useDebouncedSearch<T>(search: SearchFn<T> | null) {
|
|
13
20
|
const [query, setQuery] = useState('')
|
|
14
21
|
const [page, setPage] = useState(1)
|
|
15
22
|
const [items, setItems] = useState<T[]>([])
|
|
@@ -20,24 +27,25 @@ export function useDebouncedSearch<T>(
|
|
|
20
27
|
useEffect(() => {
|
|
21
28
|
if (!search) return
|
|
22
29
|
let live = true
|
|
30
|
+
const aborter = new AbortController()
|
|
23
31
|
const timer = setTimeout(() => {
|
|
24
32
|
setBusy(true)
|
|
25
|
-
search(query, page)
|
|
33
|
+
search(query, page, aborter.signal)
|
|
26
34
|
.then((res) => {
|
|
27
35
|
if (!live) return
|
|
28
36
|
setItems((prev) => (page === 1 ? res.items : [...prev, ...res.items]))
|
|
29
37
|
setHasMore(res.hasMore)
|
|
30
38
|
setError(null)
|
|
31
39
|
})
|
|
32
|
-
.catch((e: unknown) => live &&
|
|
40
|
+
.catch((e: unknown) => live && !aborter.signal.aborted && setError(errorText(e)))
|
|
33
41
|
.finally(() => live && setBusy(false))
|
|
34
42
|
}, 250)
|
|
35
43
|
return () => {
|
|
36
44
|
live = false
|
|
37
45
|
clearTimeout(timer)
|
|
46
|
+
aborter.abort()
|
|
38
47
|
}
|
|
39
|
-
|
|
40
|
-
}, [search, query, page, ...deps])
|
|
48
|
+
}, [search, query, page])
|
|
41
49
|
|
|
42
50
|
return {
|
|
43
51
|
query,
|
package/src/geometry.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where the editor's floating UI goes: named distances (px) and the positioning
|
|
3
|
+
* rules shared by the overlay, toolbars and popovers.
|
|
4
|
+
*/
|
|
5
|
+
type Rect = Pick<DOMRect, 'top' | 'bottom' | 'left' | 'right' | 'width' | 'height'>
|
|
6
|
+
|
|
7
|
+
export const GEOMETRY = {
|
|
8
|
+
/** Gap kept between floating UI and the viewport edge or its element. */
|
|
9
|
+
margin: 8,
|
|
10
|
+
/** How far an outline sits outside its element. */
|
|
11
|
+
outline: 2,
|
|
12
|
+
/** Height of a row toolbar. */
|
|
13
|
+
toolbar: 40,
|
|
14
|
+
/** Toolbars never ride higher than this (the site header usually lives there). */
|
|
15
|
+
toolbarMinTop: 60,
|
|
16
|
+
/** Field label chips never go above this. */
|
|
17
|
+
chipMinTop: 22,
|
|
18
|
+
/** Gap between a list and its "+ Add" button. */
|
|
19
|
+
addGap: 14,
|
|
20
|
+
/** Room left at the bottom for the dock. */
|
|
21
|
+
dock: 84,
|
|
22
|
+
/** "+ Add first section" sits at most this far down an empty list. */
|
|
23
|
+
emptyListOffset: 160,
|
|
24
|
+
} as const
|
|
25
|
+
|
|
26
|
+
const { margin, outline, toolbar, toolbarMinTop, addGap, dock } = GEOMETRY
|
|
27
|
+
|
|
28
|
+
/** Is any of the element on screen, `inset` px inside the top and bottom? */
|
|
29
|
+
export const inViewport = (r: Rect, inset = 0): boolean =>
|
|
30
|
+
r.bottom >= inset && r.top <= window.innerHeight - inset
|
|
31
|
+
|
|
32
|
+
/** An outline box around an element. */
|
|
33
|
+
export const outlineBox = (r: Rect) => ({
|
|
34
|
+
top: r.top - outline,
|
|
35
|
+
left: r.left - outline,
|
|
36
|
+
width: r.width + 2 * outline,
|
|
37
|
+
height: r.height + 2 * outline,
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
/** A row toolbar: top-right inside the row, kept below the header and above the row's end. */
|
|
41
|
+
export const toolbarPosition = (r: Rect) => ({
|
|
42
|
+
top: Math.min(Math.max(r.top + margin, toolbarMinTop), r.bottom - toolbar),
|
|
43
|
+
right: Math.max(window.innerWidth - r.right + margin, margin),
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
/** "+ Add" below a list, or pinned above the dock while the list runs past the viewport. */
|
|
47
|
+
export const belowList = (r: Rect): number => Math.min(r.bottom + addGap, window.innerHeight - dock)
|
|
48
|
+
|
|
49
|
+
/** A fixed popover below (or, without room, above) an element, kept inside the viewport. */
|
|
50
|
+
export function popoverPosition(
|
|
51
|
+
el: Element | null,
|
|
52
|
+
size = { width: 300, height: 320 },
|
|
53
|
+
): { top: number; left: number } {
|
|
54
|
+
const vw = window.innerWidth
|
|
55
|
+
const vh = window.innerHeight
|
|
56
|
+
if (!el) return { top: 80, left: Math.max(margin, vw / 2 - size.width / 2) }
|
|
57
|
+
const r = el.getBoundingClientRect()
|
|
58
|
+
const below = r.bottom + margin
|
|
59
|
+
const top = below + size.height < vh ? below : Math.max(margin, r.top - margin - size.height)
|
|
60
|
+
const left = Math.min(Math.max(margin, r.left), vw - size.width - margin)
|
|
61
|
+
return { top, left }
|
|
62
|
+
}
|
package/src/hub.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The editor hub: a module-level store holding the mounted editor, so
|
|
2
|
+
* The editor hub: a module-level store holding the mounted editor's API, so
|
|
3
3
|
* components anywhere on the page (client-mode documents, custom UI) can reach
|
|
4
4
|
* it without being rendered inside it. That keeps `<OnPage />` a sibling in the
|
|
5
5
|
* layout, not a wrapper. Kept on `globalThis` so two copies of the package
|
|
6
6
|
* (a bundling mistake) still share one editor.
|
|
7
7
|
*/
|
|
8
|
-
import type {
|
|
8
|
+
import type { OnPageApi } from './types.ts'
|
|
9
9
|
|
|
10
10
|
interface Hub {
|
|
11
|
-
current:
|
|
11
|
+
current: OnPageApi | null
|
|
12
12
|
listeners: Set<() => void>
|
|
13
13
|
}
|
|
14
14
|
|
|
@@ -19,8 +19,8 @@ const store: Hub = ((globalThis as Record<symbol, Hub | undefined>)[KEY] ??= {
|
|
|
19
19
|
})
|
|
20
20
|
|
|
21
21
|
export const hub = {
|
|
22
|
-
get: ():
|
|
23
|
-
set: (value:
|
|
22
|
+
get: (): OnPageApi | null => store.current,
|
|
23
|
+
set: (value: OnPageApi | null): void => {
|
|
24
24
|
if (value === store.current) return
|
|
25
25
|
store.current = value
|
|
26
26
|
for (const fn of store.listeners) fn()
|
package/src/index.ts
CHANGED
|
@@ -14,13 +14,16 @@ export {
|
|
|
14
14
|
FRAME_PARAM,
|
|
15
15
|
type PreviewToolbarProps,
|
|
16
16
|
} from './ui/PreviewToolbar.tsx'
|
|
17
|
-
export { useOnPage, useOnPageOptional } from './context.tsx'
|
|
17
|
+
export { useOnPage, useOnPageApi, useOnPageOptional, useSessionSnapshot } from './context.tsx'
|
|
18
18
|
export type {
|
|
19
19
|
EditorRegistry,
|
|
20
20
|
FieldEditorDef,
|
|
21
21
|
FieldEditorProps,
|
|
22
22
|
Links,
|
|
23
|
+
OnPageApi,
|
|
23
24
|
OnPageContextValue,
|
|
25
|
+
OnPageState,
|
|
26
|
+
PanelKind,
|
|
24
27
|
TargetIndex,
|
|
25
28
|
} from './types.ts'
|
|
26
29
|
export {
|
|
@@ -35,7 +38,7 @@ export {
|
|
|
35
38
|
UploadEditor,
|
|
36
39
|
} from './editors/index.ts'
|
|
37
40
|
export { resolveEditor, mergeEditors } from './registry.ts'
|
|
38
|
-
export { annotations, stega, composite
|
|
41
|
+
export { annotations, stega, composite } from './locators.ts'
|
|
39
42
|
export {
|
|
40
43
|
editable,
|
|
41
44
|
editableOff,
|
package/src/layout.ts
CHANGED
|
@@ -71,8 +71,11 @@ export function useTrackElement(getElement: () => Element | null, fn: (el: Eleme
|
|
|
71
71
|
run()
|
|
72
72
|
const off = onLayoutChange(run)
|
|
73
73
|
const el = getElement()
|
|
74
|
-
|
|
75
|
-
if (el)
|
|
74
|
+
let ro: ResizeObserver | undefined
|
|
75
|
+
if (el) {
|
|
76
|
+
ro = new ResizeObserver(() => run())
|
|
77
|
+
ro.observe(el)
|
|
78
|
+
}
|
|
76
79
|
return () => {
|
|
77
80
|
off()
|
|
78
81
|
ro?.disconnect()
|
package/src/locators.ts
CHANGED
|
@@ -8,7 +8,10 @@
|
|
|
8
8
|
* composite() merge several; earlier locators win for the same element+path
|
|
9
9
|
*/
|
|
10
10
|
import {
|
|
11
|
+
hasStegaMarker,
|
|
12
|
+
isTargetKind,
|
|
11
13
|
parseRefKey,
|
|
14
|
+
rowPrefixes,
|
|
12
15
|
targetKey,
|
|
13
16
|
stegaDecode,
|
|
14
17
|
type DocRef,
|
|
@@ -29,12 +32,14 @@ export function annotations(): Locator {
|
|
|
29
32
|
if (!ref) return
|
|
30
33
|
const label = el.getAttribute(ATTR.label) ?? undefined
|
|
31
34
|
const field = el.getAttribute(ATTR.field)
|
|
35
|
+
const kind = el.getAttribute(ATTR.kind)
|
|
32
36
|
if (field)
|
|
33
37
|
out.push({
|
|
34
38
|
el,
|
|
35
39
|
ref,
|
|
36
40
|
path: field,
|
|
37
|
-
|
|
41
|
+
// Page markup is input: an unknown kind is treated as a plain field.
|
|
42
|
+
kind: isTargetKind(kind) ? kind : 'field',
|
|
38
43
|
label,
|
|
39
44
|
source: 'annotation',
|
|
40
45
|
})
|
|
@@ -53,7 +58,7 @@ const SKIP_TAGS = new Set(['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEMPLATE', 'TEXTAREA'
|
|
|
53
58
|
/** Lowest common ancestor of a set of elements. */
|
|
54
59
|
function commonAncestor(els: Element[]): Element | null {
|
|
55
60
|
if (!els.length) return null
|
|
56
|
-
let anc: Element | null = els[0]
|
|
61
|
+
let anc: Element | null = els[0] ?? null
|
|
57
62
|
for (const el of els.slice(1)) {
|
|
58
63
|
while (anc && !anc.contains(el)) anc = anc.parentElement
|
|
59
64
|
if (!anc) return null
|
|
@@ -61,15 +66,6 @@ function commonAncestor(els: Element[]): Element | null {
|
|
|
61
66
|
return anc
|
|
62
67
|
}
|
|
63
68
|
|
|
64
|
-
/** "layout.2.items.0.title" → ["layout.2", "layout.2.items.0"] (row paths, outermost first). */
|
|
65
|
-
export function rowPrefixes(path: string): string[] {
|
|
66
|
-
const segs = path.split('.')
|
|
67
|
-
const out: string[] = []
|
|
68
|
-
for (let i = 1; i < segs.length; i++)
|
|
69
|
-
if (/^\d+$/.test(segs[i]) && i < segs.length - 1) out.push(segs.slice(0, i + 1).join('.'))
|
|
70
|
-
return out
|
|
71
|
-
}
|
|
72
|
-
|
|
73
69
|
export function stega(opts: { inferRows?: boolean } = {}): Locator {
|
|
74
70
|
const inferRows = opts.inferRows ?? true
|
|
75
71
|
return {
|
|
@@ -92,7 +88,7 @@ export function stega(opts: { inferRows?: boolean } = {}): Locator {
|
|
|
92
88
|
const walker = doc.createTreeWalker(root, 4 /* NodeFilter.SHOW_TEXT */)
|
|
93
89
|
for (let n = walker.nextNode(); n; n = walker.nextNode()) {
|
|
94
90
|
const text = n.nodeValue
|
|
95
|
-
if (!text || !text
|
|
91
|
+
if (!text || !hasStegaMarker(text)) continue
|
|
96
92
|
const parent = n.parentElement
|
|
97
93
|
if (!parent || SKIP_TAGS.has(parent.tagName) || parent.closest(`[${UI}]`)) continue
|
|
98
94
|
for (const m of stegaDecode(text)) {
|
|
@@ -121,8 +117,9 @@ export function stega(opts: { inferRows?: boolean } = {}): Locator {
|
|
|
121
117
|
rich.set(k, [...(rich.get(k) ?? []), t])
|
|
122
118
|
}
|
|
123
119
|
for (const group of rich.values()) {
|
|
120
|
+
const [first] = group
|
|
124
121
|
const el = commonAncestor(group.map((t) => t.el))
|
|
125
|
-
if (el) out.push({ ...
|
|
122
|
+
if (first && el) out.push({ ...first, el })
|
|
126
123
|
}
|
|
127
124
|
|
|
128
125
|
if (!inferRows) return out
|
|
@@ -170,7 +167,7 @@ export function stega(opts: { inferRows?: boolean } = {}): Locator {
|
|
|
170
167
|
byList.set(k, entry)
|
|
171
168
|
}
|
|
172
169
|
for (const { ref, path, els } of byList.values()) {
|
|
173
|
-
const anc = els.length > 1 ? commonAncestor(els) : els[0]
|
|
170
|
+
const anc = els.length > 1 ? commonAncestor(els) : (els[0]?.parentElement ?? null)
|
|
174
171
|
if (anc && anc !== root) rows.push({ el: anc, ref, path, kind: 'list', source: 'stega' })
|
|
175
172
|
}
|
|
176
173
|
return [...out, ...rows]
|
package/src/messages.ts
CHANGED
|
@@ -7,6 +7,7 @@ export const defaultMessages = {
|
|
|
7
7
|
'dock.label': 'Page editor',
|
|
8
8
|
'dock.loading': 'Loading editor…',
|
|
9
9
|
'dock.unavailable': '{doc}: {error}',
|
|
10
|
+
'dock.saveFailed': '{doc}: {message}',
|
|
10
11
|
'dock.nothing': 'Nothing editable on this page',
|
|
11
12
|
'dock.conflict': '{doc}: changed by someone else',
|
|
12
13
|
'dock.saving': 'Saving draft…',
|
|
@@ -45,6 +46,8 @@ export const defaultMessages = {
|
|
|
45
46
|
'row.settings': 'Settings',
|
|
46
47
|
'row.delete': 'Delete (undo with ⌘Z)',
|
|
47
48
|
'row.addSection': 'Add section',
|
|
49
|
+
'row.addSectionAbove': 'Add section above',
|
|
50
|
+
'row.addSectionBelow': 'Add section below',
|
|
48
51
|
'row.addFirstSection': 'Add a section',
|
|
49
52
|
'row.addItem': 'Add {item}',
|
|
50
53
|
'row.toolbar': '{label} actions',
|
|
@@ -57,6 +60,7 @@ export const defaultMessages = {
|
|
|
57
60
|
'common.apply': 'Apply',
|
|
58
61
|
'common.clear': 'Clear',
|
|
59
62
|
'common.remove': 'Remove',
|
|
63
|
+
'common.removeItem': 'Remove {item}',
|
|
60
64
|
'common.none': '—',
|
|
61
65
|
'common.selected': '{count} selected',
|
|
62
66
|
'search.placeholder': 'Search…',
|
|
@@ -82,6 +86,7 @@ export const defaultMessages = {
|
|
|
82
86
|
'rt.italic': 'Italic',
|
|
83
87
|
'rt.underline': 'Underline',
|
|
84
88
|
'rt.link': 'Link',
|
|
89
|
+
'rt.linkApply': 'Apply',
|
|
85
90
|
'rt.h2': 'Heading 2',
|
|
86
91
|
'rt.h3': 'Heading 3',
|
|
87
92
|
'rt.ul': 'Bulleted list',
|
|
@@ -94,15 +99,31 @@ export const defaultMessages = {
|
|
|
94
99
|
|
|
95
100
|
export type MessageKey = keyof typeof defaultMessages
|
|
96
101
|
export type Messages = Partial<Record<MessageKey, string>>
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
102
|
+
|
|
103
|
+
/** The {placeholders} of a message: `Placeholders<'Saved {when}'>` is `'when'`. */
|
|
104
|
+
export type Placeholders<S extends string> = S extends `${string}{${infer Name}}${infer Rest}`
|
|
105
|
+
? Name | Placeholders<Rest>
|
|
106
|
+
: never
|
|
107
|
+
|
|
108
|
+
/** The variables a message needs, by key. */
|
|
109
|
+
export type MessageVars<K extends MessageKey> = Record<
|
|
110
|
+
Placeholders<(typeof defaultMessages)[K]>,
|
|
111
|
+
string | number
|
|
112
|
+
>
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Translate a message key. A message with placeholders requires exactly those
|
|
116
|
+
* variables (`t('dock.saved', { ago })`); one without takes none. Unknown keys
|
|
117
|
+
* don't compile: show raw text without `t`.
|
|
118
|
+
*/
|
|
119
|
+
export type Translate = <K extends MessageKey>(
|
|
120
|
+
key: K,
|
|
121
|
+
...vars: [Placeholders<(typeof defaultMessages)[K]>] extends [never] ? [] : [vars: MessageVars<K>]
|
|
101
122
|
) => string
|
|
102
123
|
|
|
103
124
|
export function createTranslate(overrides: Messages = {}): Translate {
|
|
104
125
|
const table: Record<string, string> = { ...defaultMessages, ...overrides }
|
|
105
|
-
return (key, vars) => {
|
|
126
|
+
return (key: string, vars?: Record<string, string | number>) => {
|
|
106
127
|
const template = table[key] ?? key
|
|
107
128
|
return vars
|
|
108
129
|
? template.replace(/\{(\w+)\}/g, (m, name: string) => (name in vars ? String(vars[name]) : m))
|