@payload-onpage/react 0.2.1 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (218) hide show
  1. package/README.md +6 -0
  2. package/dist/OnPageClient.d.ts.map +1 -1
  3. package/dist/OnPageClient.js +9 -5
  4. package/dist/OnPageClient.js.map +1 -1
  5. package/dist/annotations.d.ts +1 -1
  6. package/dist/annotations.d.ts.map +1 -1
  7. package/dist/context.d.ts +19 -5
  8. package/dist/context.d.ts.map +1 -1
  9. package/dist/context.js +31 -7
  10. package/dist/context.js.map +1 -1
  11. package/dist/dom.d.ts +16 -12
  12. package/dist/dom.d.ts.map +1 -1
  13. package/dist/dom.js +16 -16
  14. package/dist/dom.js.map +1 -1
  15. package/dist/editor/FieldHost.d.ts.map +1 -1
  16. package/dist/editor/FieldHost.js +18 -11
  17. package/dist/editor/FieldHost.js.map +1 -1
  18. package/dist/editor/OnPageEditor.d.ts +15 -8
  19. package/dist/editor/OnPageEditor.d.ts.map +1 -1
  20. package/dist/editor/OnPageEditor.js +73 -103
  21. package/dist/editor/OnPageEditor.js.map +1 -1
  22. package/dist/editor/editTarget.d.ts +28 -0
  23. package/dist/editor/editTarget.d.ts.map +1 -0
  24. package/dist/editor/editTarget.js +20 -0
  25. package/dist/editor/editTarget.js.map +1 -0
  26. package/dist/editor/useClientDocs.d.ts +4 -0
  27. package/dist/editor/useClientDocs.d.ts.map +1 -0
  28. package/dist/editor/useClientDocs.js +16 -0
  29. package/dist/editor/useClientDocs.js.map +1 -0
  30. package/dist/editor/useEditorSession.d.ts +13 -0
  31. package/dist/editor/useEditorSession.d.ts.map +1 -0
  32. package/dist/editor/useEditorSession.js +30 -0
  33. package/dist/editor/useEditorSession.js.map +1 -0
  34. package/dist/editor/useFlash.d.ts +4 -0
  35. package/dist/editor/useFlash.d.ts.map +1 -0
  36. package/dist/editor/useFlash.js +16 -0
  37. package/dist/editor/useFlash.js.map +1 -0
  38. package/dist/editor/useGhosts.d.ts +5 -7
  39. package/dist/editor/useGhosts.d.ts.map +1 -1
  40. package/dist/editor/useGhosts.js.map +1 -1
  41. package/dist/editor/usePageEvents.d.ts.map +1 -1
  42. package/dist/editor/usePageEvents.js +16 -16
  43. package/dist/editor/usePageEvents.js.map +1 -1
  44. package/dist/editor-kit.d.ts +10 -6
  45. package/dist/editor-kit.d.ts.map +1 -1
  46. package/dist/editor-kit.js +8 -5
  47. package/dist/editor-kit.js.map +1 -1
  48. package/dist/editors/CheckboxEditor.d.ts +6 -1
  49. package/dist/editors/CheckboxEditor.d.ts.map +1 -1
  50. package/dist/editors/CheckboxEditor.js +6 -3
  51. package/dist/editors/CheckboxEditor.js.map +1 -1
  52. package/dist/editors/ColorEditor.d.ts +3 -1
  53. package/dist/editors/ColorEditor.d.ts.map +1 -1
  54. package/dist/editors/ColorEditor.js +9 -15
  55. package/dist/editors/ColorEditor.js.map +1 -1
  56. package/dist/editors/DateEditor.d.ts +3 -1
  57. package/dist/editors/DateEditor.d.ts.map +1 -1
  58. package/dist/editors/DateEditor.js +10 -12
  59. package/dist/editors/DateEditor.js.map +1 -1
  60. package/dist/editors/InlineText.d.ts +2 -2
  61. package/dist/editors/InlineText.d.ts.map +1 -1
  62. package/dist/editors/InlineText.js +2 -3
  63. package/dist/editors/InlineText.js.map +1 -1
  64. package/dist/editors/NumberEditor.d.ts +3 -1
  65. package/dist/editors/NumberEditor.d.ts.map +1 -1
  66. package/dist/editors/NumberEditor.js +9 -17
  67. package/dist/editors/NumberEditor.js.map +1 -1
  68. package/dist/editors/RelationshipEditor.d.ts +7 -1
  69. package/dist/editors/RelationshipEditor.d.ts.map +1 -1
  70. package/dist/editors/RelationshipEditor.js +10 -18
  71. package/dist/editors/RelationshipEditor.js.map +1 -1
  72. package/dist/editors/SelectEditor.d.ts +4 -1
  73. package/dist/editors/SelectEditor.d.ts.map +1 -1
  74. package/dist/editors/SelectEditor.js +8 -13
  75. package/dist/editors/SelectEditor.js.map +1 -1
  76. package/dist/editors/UploadEditor.d.ts +7 -1
  77. package/dist/editors/UploadEditor.d.ts.map +1 -1
  78. package/dist/editors/UploadEditor.js +5 -5
  79. package/dist/editors/UploadEditor.js.map +1 -1
  80. package/dist/editors/index.d.ts +0 -1
  81. package/dist/editors/index.d.ts.map +1 -1
  82. package/dist/editors/index.js +56 -16
  83. package/dist/editors/index.js.map +1 -1
  84. package/dist/editors/kit.d.ts +39 -0
  85. package/dist/editors/kit.d.ts.map +1 -0
  86. package/dist/editors/kit.js +44 -0
  87. package/dist/editors/kit.js.map +1 -0
  88. package/dist/editors/useDebouncedSearch.d.ts +10 -6
  89. package/dist/editors/useDebouncedSearch.d.ts.map +1 -1
  90. package/dist/editors/useDebouncedSearch.js +11 -7
  91. package/dist/editors/useDebouncedSearch.js.map +1 -1
  92. package/dist/geometry.d.ts +49 -0
  93. package/dist/geometry.d.ts.map +1 -0
  94. package/dist/geometry.js +48 -0
  95. package/dist/geometry.js.map +1 -0
  96. package/dist/hub.d.ts +4 -4
  97. package/dist/hub.d.ts.map +1 -1
  98. package/dist/hub.js.map +1 -1
  99. package/dist/index.d.ts +3 -3
  100. package/dist/index.d.ts.map +1 -1
  101. package/dist/index.js +2 -2
  102. package/dist/index.js.map +1 -1
  103. package/dist/layout.d.ts.map +1 -1
  104. package/dist/layout.js +4 -2
  105. package/dist/layout.js.map +1 -1
  106. package/dist/locators.d.ts +0 -2
  107. package/dist/locators.d.ts.map +1 -1
  108. package/dist/locators.js +10 -16
  109. package/dist/locators.js.map +1 -1
  110. package/dist/messages.d.ts +15 -2
  111. package/dist/messages.d.ts.map +1 -1
  112. package/dist/messages.js +5 -0
  113. package/dist/messages.js.map +1 -1
  114. package/dist/registry.d.ts +21 -2
  115. package/dist/registry.d.ts.map +1 -1
  116. package/dist/registry.js +31 -6
  117. package/dist/registry.js.map +1 -1
  118. package/dist/rowOps.d.ts +20 -0
  119. package/dist/rowOps.d.ts.map +1 -0
  120. package/dist/rowOps.js +48 -0
  121. package/dist/rowOps.js.map +1 -0
  122. package/dist/sanitize.d.ts +2 -0
  123. package/dist/sanitize.d.ts.map +1 -1
  124. package/dist/sanitize.js +3 -0
  125. package/dist/sanitize.js.map +1 -1
  126. package/dist/settings.d.ts +10 -0
  127. package/dist/settings.d.ts.map +1 -0
  128. package/dist/settings.js +30 -0
  129. package/dist/settings.js.map +1 -0
  130. package/dist/styles.d.ts +1 -5
  131. package/dist/styles.d.ts.map +1 -1
  132. package/dist/styles.js +5 -4
  133. package/dist/styles.js.map +1 -1
  134. package/dist/targets.d.ts +1 -1
  135. package/dist/targets.d.ts.map +1 -1
  136. package/dist/types.d.ts +71 -46
  137. package/dist/types.d.ts.map +1 -1
  138. package/dist/ui/BlockPicker.d.ts.map +1 -1
  139. package/dist/ui/BlockPicker.js +6 -5
  140. package/dist/ui/BlockPicker.js.map +1 -1
  141. package/dist/ui/Dialog.d.ts +0 -5
  142. package/dist/ui/Dialog.d.ts.map +1 -1
  143. package/dist/ui/Dialog.js +10 -6
  144. package/dist/ui/Dialog.js.map +1 -1
  145. package/dist/ui/Dock.d.ts +1 -6
  146. package/dist/ui/Dock.d.ts.map +1 -1
  147. package/dist/ui/Dock.js +46 -28
  148. package/dist/ui/Dock.js.map +1 -1
  149. package/dist/ui/Overlay.d.ts.map +1 -1
  150. package/dist/ui/Overlay.js +39 -37
  151. package/dist/ui/Overlay.js.map +1 -1
  152. package/dist/ui/PreviewToolbar.d.ts +3 -3
  153. package/dist/ui/PreviewToolbar.d.ts.map +1 -1
  154. package/dist/ui/PreviewToolbar.js +4 -6
  155. package/dist/ui/PreviewToolbar.js.map +1 -1
  156. package/dist/ui/SettingsPanel.d.ts +0 -9
  157. package/dist/ui/SettingsPanel.d.ts.map +1 -1
  158. package/dist/ui/SettingsPanel.js +36 -41
  159. package/dist/ui/SettingsPanel.js.map +1 -1
  160. package/dist/ui/ToolbarButton.d.ts +12 -0
  161. package/dist/ui/ToolbarButton.d.ts.map +1 -0
  162. package/dist/ui/ToolbarButton.js +7 -0
  163. package/dist/ui/ToolbarButton.js.map +1 -0
  164. package/dist/useEvent.d.ts +1 -1
  165. package/dist/useEvent.d.ts.map +1 -1
  166. package/dist/useEvent.js +3 -1
  167. package/dist/useEvent.js.map +1 -1
  168. package/dist/useIsClient.d.ts +5 -0
  169. package/dist/useIsClient.d.ts.map +1 -0
  170. package/dist/useIsClient.js +8 -0
  171. package/dist/useIsClient.js.map +1 -0
  172. package/package.json +15 -4
  173. package/src/OnPageClient.tsx +9 -5
  174. package/src/annotations.ts +1 -1
  175. package/src/context.tsx +53 -10
  176. package/src/dom.ts +17 -18
  177. package/src/editor/FieldHost.tsx +26 -11
  178. package/src/editor/OnPageEditor.tsx +103 -160
  179. package/src/editor/editTarget.ts +41 -0
  180. package/src/editor/useClientDocs.ts +19 -0
  181. package/src/editor/useEditorSession.ts +36 -0
  182. package/src/editor/useFlash.ts +18 -0
  183. package/src/editor/useGhosts.ts +5 -7
  184. package/src/editor/usePageEvents.ts +13 -14
  185. package/src/editor-kit.ts +10 -6
  186. package/src/editors/CheckboxEditor.tsx +11 -12
  187. package/src/editors/ColorEditor.tsx +15 -35
  188. package/src/editors/DateEditor.tsx +18 -37
  189. package/src/editors/InlineText.tsx +4 -5
  190. package/src/editors/NumberEditor.tsx +15 -36
  191. package/src/editors/RelationshipEditor.tsx +18 -36
  192. package/src/editors/SelectEditor.tsx +14 -22
  193. package/src/editors/UploadEditor.tsx +6 -6
  194. package/src/editors/index.ts +56 -16
  195. package/src/editors/kit.tsx +97 -0
  196. package/src/editors/useDebouncedSearch.ts +18 -10
  197. package/src/geometry.ts +62 -0
  198. package/src/hub.ts +5 -5
  199. package/src/index.ts +5 -2
  200. package/src/layout.ts +5 -2
  201. package/src/locators.ts +11 -14
  202. package/src/messages.ts +26 -5
  203. package/src/registry.ts +42 -8
  204. package/src/rowOps.ts +64 -0
  205. package/src/sanitize.ts +6 -0
  206. package/src/settings.ts +49 -0
  207. package/src/styles.ts +5 -4
  208. package/src/targets.ts +1 -1
  209. package/src/types.ts +78 -45
  210. package/src/ui/BlockPicker.tsx +6 -7
  211. package/src/ui/Dialog.tsx +9 -5
  212. package/src/ui/Dock.tsx +67 -50
  213. package/src/ui/Overlay.tsx +75 -102
  214. package/src/ui/PreviewToolbar.tsx +7 -15
  215. package/src/ui/SettingsPanel.tsx +84 -81
  216. package/src/ui/ToolbarButton.tsx +36 -0
  217. package/src/useEvent.ts +4 -2
  218. package/src/useIsClient.ts +15 -0
@@ -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 { Dialog } from '../ui/Dialog.tsx'
5
+ import { EditorPopover } from './kit.tsx'
5
6
 
6
- export function CheckboxEditor({ field, value, element, commit, close, t }: FieldEditorProps) {
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
- <Dialog
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={Boolean(value ?? field.defaultValue)}
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
- </Dialog>
28
+ </EditorPopover>
30
29
  )
31
30
  }
@@ -1,41 +1,23 @@
1
1
  'use client'
2
2
 
3
- import { useState } from 'react'
3
+ import type { ScalarField } from '@payload-onpage/core'
4
4
  import type { FieldEditorProps } from '../types.ts'
5
- import { Dialog } from '../ui/Dialog.tsx'
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
- value,
13
- element,
14
- commit,
15
- preview,
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
- <Dialog
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={(e) => e.key === 'Enter' && save()}
37
+ onKeyDown={onKeyDown}
56
38
  data-testid="onpage-color-input"
57
39
  />
58
40
  </div>
59
- <button type="button" className="po-btn po-primary" onClick={save} data-testid="onpage-apply">
60
- {t('common.apply')}
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 { useState } from 'react'
3
+ import type { DateField } from '@payload-onpage/core'
4
4
  import type { FieldEditorProps } from '../types.ts'
5
- import { Dialog } from '../ui/Dialog.tsx'
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: unknown, withTime: boolean): string => {
9
- if (typeof iso !== 'string' || !iso) return ''
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 function DateEditor({ field, value, element, commit, close, t }: FieldEditorProps) {
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 [draft, setDraft] = useState(toLocalInput(value, withTime))
20
- const save = () => {
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
- const next = draft ? new Date(withTime ? draft : `${draft}T12:00:00`).toISOString() : null
23
- if (next !== value) commit(next)
24
- close()
25
- }
26
+ parse: (d) => (d ? new Date(withTime ? d : `${d}T12:00:00`).toISOString() : null),
27
+ })
26
28
  return (
27
- <Dialog
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) => setDraft(e.target.value)}
43
- onKeyDown={(e) => e.key === 'Enter' && save()}
37
+ onChange={(e) => change(e.target.value)}
38
+ onKeyDown={onKeyDown}
44
39
  data-testid="onpage-date-input"
45
40
  />
46
41
  </div>
47
- <div className="po-actions">
48
- <button
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 { toText } from '@payload-onpage/core'
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
- export const escapeHtml = (s: string): string =>
20
- s.replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' })[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(toText(value))
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 { toText } from '@payload-onpage/core'
4
- import { useState } from 'react'
3
+ import type { NumberField } from '@payload-onpage/core'
5
4
  import type { FieldEditorProps } from '../types.ts'
6
- import { Dialog } from '../ui/Dialog.tsx'
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
- value,
12
- element,
13
- commit,
14
- preview,
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
- <Dialog
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={(e) => e.key === 'Enter' && save()}
41
+ onKeyDown={onKeyDown}
61
42
  data-testid="onpage-number-input"
62
43
  />
63
44
  </div>
64
- <button type="button" className="po-btn po-primary" onClick={save} data-testid="onpage-apply">
65
- {t('common.apply')}
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 { relationId, relationTitle, type SearchHit } from '@payload-onpage/core'
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 { Dialog } from '../ui/Dialog.tsx'
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: unknown[] = many
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
- <Dialog
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={`${t('common.remove')} ${relationTitle(v, titleField)}`}
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
- </Dialog>
133
+ </EditorPopover>
152
134
  )
153
135
  }
@@ -1,20 +1,19 @@
1
1
  'use client'
2
2
 
3
- import { toText } from '@payload-onpage/core'
3
+ import { asList, type ChoiceField } from '@payload-onpage/core'
4
4
  import type { FieldEditorProps } from '../types.ts'
5
- import { Dialog } from '../ui/Dialog.tsx'
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({ field, value, element, commit, close, t }: FieldEditorProps) {
9
- const options = field.options ?? []
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
- <Dialog
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
- </Dialog>
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 as Error).message)
42
+ setUploadError(errorText(e))
43
43
  } finally {
44
44
  setUploading(false)
45
45
  }
@@ -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: { mode: 'inline', component: InlineText },
14
- textarea: { mode: 'inline', component: InlineText },
15
- email: { mode: 'inline', component: InlineText },
16
- select: { mode: 'popover', component: SelectEditor },
17
- radio: { mode: 'popover', component: SelectEditor },
18
- checkbox: { mode: 'popover', component: CheckboxEditor },
19
- number: { mode: 'popover', component: NumberEditor },
20
- date: { mode: 'popover', component: DateEditor },
21
- relationship: { mode: 'popover', component: RelationshipEditor },
22
- upload: { mode: 'popover', component: UploadEditor },
23
- color: { mode: 'popover', component: ColorEditor },
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 {