@payload-onpage/react 0.2.1 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/dist/OnPageClient.d.ts.map +1 -1
- package/dist/OnPageClient.js +9 -5
- package/dist/OnPageClient.js.map +1 -1
- package/dist/annotations.d.ts +1 -1
- package/dist/annotations.d.ts.map +1 -1
- package/dist/context.d.ts +19 -5
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +31 -7
- package/dist/context.js.map +1 -1
- package/dist/dom.d.ts +16 -12
- package/dist/dom.d.ts.map +1 -1
- package/dist/dom.js +16 -16
- package/dist/dom.js.map +1 -1
- package/dist/editor/FieldHost.d.ts.map +1 -1
- package/dist/editor/FieldHost.js +18 -11
- package/dist/editor/FieldHost.js.map +1 -1
- package/dist/editor/OnPageEditor.d.ts +15 -8
- package/dist/editor/OnPageEditor.d.ts.map +1 -1
- package/dist/editor/OnPageEditor.js +73 -103
- package/dist/editor/OnPageEditor.js.map +1 -1
- package/dist/editor/editTarget.d.ts +28 -0
- package/dist/editor/editTarget.d.ts.map +1 -0
- package/dist/editor/editTarget.js +20 -0
- package/dist/editor/editTarget.js.map +1 -0
- package/dist/editor/useClientDocs.d.ts +4 -0
- package/dist/editor/useClientDocs.d.ts.map +1 -0
- package/dist/editor/useClientDocs.js +16 -0
- package/dist/editor/useClientDocs.js.map +1 -0
- package/dist/editor/useEditorSession.d.ts +13 -0
- package/dist/editor/useEditorSession.d.ts.map +1 -0
- package/dist/editor/useEditorSession.js +30 -0
- package/dist/editor/useEditorSession.js.map +1 -0
- package/dist/editor/useFlash.d.ts +4 -0
- package/dist/editor/useFlash.d.ts.map +1 -0
- package/dist/editor/useFlash.js +16 -0
- package/dist/editor/useFlash.js.map +1 -0
- package/dist/editor/useGhosts.d.ts +5 -7
- package/dist/editor/useGhosts.d.ts.map +1 -1
- package/dist/editor/useGhosts.js.map +1 -1
- package/dist/editor/usePageEvents.d.ts.map +1 -1
- package/dist/editor/usePageEvents.js +16 -16
- package/dist/editor/usePageEvents.js.map +1 -1
- package/dist/editor-kit.d.ts +10 -6
- package/dist/editor-kit.d.ts.map +1 -1
- package/dist/editor-kit.js +8 -5
- package/dist/editor-kit.js.map +1 -1
- package/dist/editors/CheckboxEditor.d.ts +6 -1
- package/dist/editors/CheckboxEditor.d.ts.map +1 -1
- package/dist/editors/CheckboxEditor.js +6 -3
- package/dist/editors/CheckboxEditor.js.map +1 -1
- package/dist/editors/ColorEditor.d.ts +3 -1
- package/dist/editors/ColorEditor.d.ts.map +1 -1
- package/dist/editors/ColorEditor.js +9 -15
- package/dist/editors/ColorEditor.js.map +1 -1
- package/dist/editors/DateEditor.d.ts +3 -1
- package/dist/editors/DateEditor.d.ts.map +1 -1
- package/dist/editors/DateEditor.js +10 -12
- package/dist/editors/DateEditor.js.map +1 -1
- package/dist/editors/InlineText.d.ts +2 -2
- package/dist/editors/InlineText.d.ts.map +1 -1
- package/dist/editors/InlineText.js +2 -3
- package/dist/editors/InlineText.js.map +1 -1
- package/dist/editors/NumberEditor.d.ts +3 -1
- package/dist/editors/NumberEditor.d.ts.map +1 -1
- package/dist/editors/NumberEditor.js +9 -17
- package/dist/editors/NumberEditor.js.map +1 -1
- package/dist/editors/RelationshipEditor.d.ts +7 -1
- package/dist/editors/RelationshipEditor.d.ts.map +1 -1
- package/dist/editors/RelationshipEditor.js +10 -18
- package/dist/editors/RelationshipEditor.js.map +1 -1
- package/dist/editors/SelectEditor.d.ts +4 -1
- package/dist/editors/SelectEditor.d.ts.map +1 -1
- package/dist/editors/SelectEditor.js +8 -13
- package/dist/editors/SelectEditor.js.map +1 -1
- package/dist/editors/UploadEditor.d.ts +7 -1
- package/dist/editors/UploadEditor.d.ts.map +1 -1
- package/dist/editors/UploadEditor.js +5 -5
- package/dist/editors/UploadEditor.js.map +1 -1
- package/dist/editors/index.d.ts +0 -1
- package/dist/editors/index.d.ts.map +1 -1
- package/dist/editors/index.js +56 -16
- package/dist/editors/index.js.map +1 -1
- package/dist/editors/kit.d.ts +39 -0
- package/dist/editors/kit.d.ts.map +1 -0
- package/dist/editors/kit.js +44 -0
- package/dist/editors/kit.js.map +1 -0
- package/dist/editors/useDebouncedSearch.d.ts +10 -6
- package/dist/editors/useDebouncedSearch.d.ts.map +1 -1
- package/dist/editors/useDebouncedSearch.js +11 -7
- package/dist/editors/useDebouncedSearch.js.map +1 -1
- package/dist/geometry.d.ts +49 -0
- package/dist/geometry.d.ts.map +1 -0
- package/dist/geometry.js +48 -0
- package/dist/geometry.js.map +1 -0
- package/dist/hub.d.ts +4 -4
- package/dist/hub.d.ts.map +1 -1
- package/dist/hub.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/layout.d.ts.map +1 -1
- package/dist/layout.js +4 -2
- package/dist/layout.js.map +1 -1
- package/dist/locators.d.ts +0 -2
- package/dist/locators.d.ts.map +1 -1
- package/dist/locators.js +10 -16
- package/dist/locators.js.map +1 -1
- package/dist/messages.d.ts +15 -2
- package/dist/messages.d.ts.map +1 -1
- package/dist/messages.js +5 -0
- package/dist/messages.js.map +1 -1
- package/dist/registry.d.ts +21 -2
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +31 -6
- package/dist/registry.js.map +1 -1
- package/dist/rowOps.d.ts +20 -0
- package/dist/rowOps.d.ts.map +1 -0
- package/dist/rowOps.js +48 -0
- package/dist/rowOps.js.map +1 -0
- package/dist/sanitize.d.ts +2 -0
- package/dist/sanitize.d.ts.map +1 -1
- package/dist/sanitize.js +3 -0
- package/dist/sanitize.js.map +1 -1
- package/dist/settings.d.ts +10 -0
- package/dist/settings.d.ts.map +1 -0
- package/dist/settings.js +30 -0
- package/dist/settings.js.map +1 -0
- package/dist/styles.d.ts +1 -5
- package/dist/styles.d.ts.map +1 -1
- package/dist/styles.js +5 -4
- package/dist/styles.js.map +1 -1
- package/dist/targets.d.ts +1 -1
- package/dist/targets.d.ts.map +1 -1
- package/dist/types.d.ts +71 -46
- package/dist/types.d.ts.map +1 -1
- package/dist/ui/BlockPicker.d.ts.map +1 -1
- package/dist/ui/BlockPicker.js +6 -5
- package/dist/ui/BlockPicker.js.map +1 -1
- package/dist/ui/Dialog.d.ts +0 -5
- package/dist/ui/Dialog.d.ts.map +1 -1
- package/dist/ui/Dialog.js +10 -6
- package/dist/ui/Dialog.js.map +1 -1
- package/dist/ui/Dock.d.ts +1 -6
- package/dist/ui/Dock.d.ts.map +1 -1
- package/dist/ui/Dock.js +46 -28
- package/dist/ui/Dock.js.map +1 -1
- package/dist/ui/Overlay.d.ts.map +1 -1
- package/dist/ui/Overlay.js +39 -37
- package/dist/ui/Overlay.js.map +1 -1
- package/dist/ui/PreviewToolbar.d.ts +3 -3
- package/dist/ui/PreviewToolbar.d.ts.map +1 -1
- package/dist/ui/PreviewToolbar.js +4 -6
- package/dist/ui/PreviewToolbar.js.map +1 -1
- package/dist/ui/SettingsPanel.d.ts +0 -9
- package/dist/ui/SettingsPanel.d.ts.map +1 -1
- package/dist/ui/SettingsPanel.js +36 -41
- package/dist/ui/SettingsPanel.js.map +1 -1
- package/dist/ui/ToolbarButton.d.ts +12 -0
- package/dist/ui/ToolbarButton.d.ts.map +1 -0
- package/dist/ui/ToolbarButton.js +7 -0
- package/dist/ui/ToolbarButton.js.map +1 -0
- package/dist/useEvent.d.ts +1 -1
- package/dist/useEvent.d.ts.map +1 -1
- package/dist/useEvent.js +3 -1
- package/dist/useEvent.js.map +1 -1
- package/dist/useIsClient.d.ts +5 -0
- package/dist/useIsClient.d.ts.map +1 -0
- package/dist/useIsClient.js +8 -0
- package/dist/useIsClient.js.map +1 -0
- package/package.json +15 -4
- package/src/OnPageClient.tsx +9 -5
- package/src/annotations.ts +1 -1
- package/src/context.tsx +53 -10
- package/src/dom.ts +17 -18
- package/src/editor/FieldHost.tsx +26 -11
- package/src/editor/OnPageEditor.tsx +103 -160
- package/src/editor/editTarget.ts +41 -0
- package/src/editor/useClientDocs.ts +19 -0
- package/src/editor/useEditorSession.ts +36 -0
- package/src/editor/useFlash.ts +18 -0
- package/src/editor/useGhosts.ts +5 -7
- package/src/editor/usePageEvents.ts +13 -14
- package/src/editor-kit.ts +10 -6
- package/src/editors/CheckboxEditor.tsx +11 -12
- package/src/editors/ColorEditor.tsx +15 -35
- package/src/editors/DateEditor.tsx +18 -37
- package/src/editors/InlineText.tsx +4 -5
- package/src/editors/NumberEditor.tsx +15 -36
- package/src/editors/RelationshipEditor.tsx +18 -36
- package/src/editors/SelectEditor.tsx +14 -22
- package/src/editors/UploadEditor.tsx +6 -6
- package/src/editors/index.ts +56 -16
- package/src/editors/kit.tsx +97 -0
- package/src/editors/useDebouncedSearch.ts +18 -10
- package/src/geometry.ts +62 -0
- package/src/hub.ts +5 -5
- package/src/index.ts +5 -2
- package/src/layout.ts +5 -2
- package/src/locators.ts +11 -14
- package/src/messages.ts +26 -5
- package/src/registry.ts +42 -8
- package/src/rowOps.ts +64 -0
- package/src/sanitize.ts +6 -0
- package/src/settings.ts +49 -0
- package/src/styles.ts +5 -4
- package/src/targets.ts +1 -1
- package/src/types.ts +78 -45
- package/src/ui/BlockPicker.tsx +6 -7
- package/src/ui/Dialog.tsx +9 -5
- package/src/ui/Dock.tsx +67 -50
- package/src/ui/Overlay.tsx +75 -102
- package/src/ui/PreviewToolbar.tsx +7 -15
- package/src/ui/SettingsPanel.tsx +84 -81
- package/src/ui/ToolbarButton.tsx +36 -0
- package/src/useEvent.ts +4 -2
- package/src/useIsClient.ts +15 -0
package/src/ui/Overlay.tsx
CHANGED
|
@@ -6,35 +6,34 @@
|
|
|
6
6
|
* from the target elements whenever the layout changes.
|
|
7
7
|
*/
|
|
8
8
|
import {
|
|
9
|
-
cloneRow,
|
|
10
9
|
fieldAtPath,
|
|
11
10
|
getAt,
|
|
12
|
-
insertItem,
|
|
13
11
|
isEditablePath,
|
|
14
|
-
|
|
12
|
+
isListField,
|
|
15
13
|
newArrayRow,
|
|
16
14
|
refKey,
|
|
17
|
-
removeItem,
|
|
18
15
|
resolveRow,
|
|
19
|
-
|
|
20
|
-
type Doc,
|
|
16
|
+
rowPrefixes,
|
|
21
17
|
type RowInfo,
|
|
22
18
|
type Target,
|
|
19
|
+
isReady,
|
|
20
|
+
type RefKey,
|
|
23
21
|
} from '@payload-onpage/core'
|
|
24
22
|
import { useEffect, useState } from 'react'
|
|
25
23
|
import { useOnPage } from '../context.tsx'
|
|
26
24
|
import { isOurs } from '../dom.ts'
|
|
25
|
+
import { belowList, GEOMETRY, inViewport, outlineBox, toolbarPosition } from '../geometry.ts'
|
|
27
26
|
import { useLayoutTick } from '../layout.ts'
|
|
27
|
+
import { ToolbarButton } from './ToolbarButton.tsx'
|
|
28
28
|
import { targetsAt } from '../targets.ts'
|
|
29
|
-
import {
|
|
29
|
+
import { insertRow, rowOps } from '../rowOps.ts'
|
|
30
|
+
import { hasSettings } from '../settings.ts'
|
|
30
31
|
|
|
31
32
|
interface Hover {
|
|
32
|
-
field?: Target
|
|
33
|
+
field?: Target | undefined
|
|
33
34
|
rows: Target[] // innermost first
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
const VIEW_PAD = 8
|
|
37
|
-
|
|
38
37
|
function useHover(): Hover {
|
|
39
38
|
const { targets } = useOnPage()
|
|
40
39
|
const [hover, setHover] = useState<Hover>({ rows: [] })
|
|
@@ -70,14 +69,14 @@ function Outline({
|
|
|
70
69
|
if (r.width === 0 && r.height === 0) return null
|
|
71
70
|
return (
|
|
72
71
|
<>
|
|
73
|
-
<div
|
|
74
|
-
className={`po-outline${variant ? ` is-${variant}` : ''}`}
|
|
75
|
-
style={{ top: r.top - 2, left: r.left - 2, width: r.width + 4, height: r.height + 4 }}
|
|
76
|
-
/>
|
|
72
|
+
<div className={`po-outline${variant ? ` is-${variant}` : ''}`} style={outlineBox(r)} />
|
|
77
73
|
{label && (
|
|
78
74
|
<div
|
|
79
75
|
className={`po-chip${variant === 'item' ? ' is-item' : ''}`}
|
|
80
|
-
style={{
|
|
76
|
+
style={{
|
|
77
|
+
top: Math.max(r.top - GEOMETRY.outline, GEOMETRY.chipMinTop),
|
|
78
|
+
left: r.left - GEOMETRY.outline,
|
|
79
|
+
}}
|
|
81
80
|
>
|
|
82
81
|
{label}
|
|
83
82
|
</div>
|
|
@@ -87,7 +86,7 @@ function Outline({
|
|
|
87
86
|
}
|
|
88
87
|
|
|
89
88
|
interface ActiveRow {
|
|
90
|
-
key:
|
|
89
|
+
key: RefKey
|
|
91
90
|
info: RowInfo
|
|
92
91
|
el: Element
|
|
93
92
|
}
|
|
@@ -96,94 +95,67 @@ function RowToolbar({ row, variant }: { row: ActiveRow; variant: 'block' | 'item
|
|
|
96
95
|
const { session, setSelected, setPanel, selected, targets, t } = useOnPage()
|
|
97
96
|
const { key, info, el } = row
|
|
98
97
|
const r = el.getBoundingClientRect()
|
|
99
|
-
if (r
|
|
100
|
-
const
|
|
101
|
-
const right = Math.max(window.innerWidth - r.right + VIEW_PAD, VIEW_PAD)
|
|
102
|
-
const { listPath, index, length, rowFields, path } = info
|
|
98
|
+
if (!inViewport(r, GEOMETRY.toolbar)) return null
|
|
99
|
+
const { path } = info
|
|
103
100
|
const label = targets.find(key, path, ['item'])?.label || info.label
|
|
104
|
-
const
|
|
105
|
-
const max = info.listField.maxRows
|
|
106
|
-
const min = info.listField.minRows ?? 0
|
|
107
|
-
|
|
101
|
+
const ops = rowOps(session, key, info)
|
|
108
102
|
const move = (dir: -1 | 1) => {
|
|
109
|
-
|
|
110
|
-
setSelected(
|
|
103
|
+
const next = ops.move(dir)
|
|
104
|
+
if (next) setSelected(next)
|
|
111
105
|
}
|
|
112
106
|
const duplicate = () => {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
)
|
|
116
|
-
setSelected({ key, path: `${listPath}.${index + 1}` })
|
|
107
|
+
const next = ops.duplicate()
|
|
108
|
+
if (next) setSelected(next)
|
|
117
109
|
}
|
|
118
110
|
const remove = () => {
|
|
119
|
-
|
|
120
|
-
if (selected?.key === key && selected.path.startsWith(path)) setSelected(null)
|
|
111
|
+
if (ops.remove() && selected?.key === key && selected.path.startsWith(path)) setSelected(null)
|
|
121
112
|
}
|
|
122
113
|
return (
|
|
123
114
|
<div
|
|
124
115
|
className={`po-bar${variant === 'item' ? ' is-item' : ''}`}
|
|
125
|
-
style={
|
|
116
|
+
style={toolbarPosition(r)}
|
|
126
117
|
role="toolbar"
|
|
127
118
|
aria-label={t('row.toolbar', { label })}
|
|
128
119
|
data-testid={`onpage-${variant}-toolbar`}
|
|
129
120
|
>
|
|
130
121
|
<span className="po-btn po-bar-label">{label}</span>
|
|
131
|
-
<
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
disabled={
|
|
122
|
+
<ToolbarButton
|
|
123
|
+
label={t('row.moveUp')}
|
|
124
|
+
icon="↑"
|
|
125
|
+
disabled={!ops.canMove(-1)}
|
|
135
126
|
onClick={() => move(-1)}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
<button
|
|
143
|
-
type="button"
|
|
144
|
-
className="po-btn"
|
|
145
|
-
disabled={index >= length - 1}
|
|
127
|
+
testId={`onpage-${variant}-up`}
|
|
128
|
+
/>
|
|
129
|
+
<ToolbarButton
|
|
130
|
+
label={t('row.moveDown')}
|
|
131
|
+
icon="↓"
|
|
132
|
+
disabled={!ops.canMove(1)}
|
|
146
133
|
onClick={() => move(1)}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
<button
|
|
154
|
-
type="button"
|
|
155
|
-
className="po-btn"
|
|
156
|
-
disabled={max !== undefined && length >= max}
|
|
134
|
+
testId={`onpage-${variant}-down`}
|
|
135
|
+
/>
|
|
136
|
+
<ToolbarButton
|
|
137
|
+
label={t('row.duplicate')}
|
|
138
|
+
icon="⧉"
|
|
139
|
+
disabled={!ops.canDuplicate}
|
|
157
140
|
onClick={duplicate}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
{hasSettings(rowFields, path, key, targets) && (
|
|
165
|
-
<button
|
|
166
|
-
type="button"
|
|
167
|
-
className="po-btn"
|
|
141
|
+
testId={`onpage-${variant}-duplicate`}
|
|
142
|
+
/>
|
|
143
|
+
{hasSettings(info.rowFields, path, key, targets) && (
|
|
144
|
+
<ToolbarButton
|
|
145
|
+
label={t('row.settings')}
|
|
146
|
+
icon="⚙"
|
|
168
147
|
onClick={() => setPanel({ kind: 'settings', key, path, title: label })}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
data-testid={`onpage-${variant}-settings`}
|
|
172
|
-
>
|
|
173
|
-
⚙
|
|
174
|
-
</button>
|
|
148
|
+
testId={`onpage-${variant}-settings`}
|
|
149
|
+
/>
|
|
175
150
|
)}
|
|
176
|
-
<
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
151
|
+
<ToolbarButton
|
|
152
|
+
label={t('row.delete')}
|
|
153
|
+
icon="✕"
|
|
154
|
+
danger
|
|
155
|
+
disabled={!ops.canRemove}
|
|
180
156
|
onClick={remove}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
data-testid={`onpage-${variant}-delete`}
|
|
184
|
-
>
|
|
185
|
-
✕
|
|
186
|
-
</button>
|
|
157
|
+
testId={`onpage-${variant}-delete`}
|
|
158
|
+
/>
|
|
187
159
|
</div>
|
|
188
160
|
)
|
|
189
161
|
}
|
|
@@ -201,7 +173,7 @@ function InsertPoints({ row }: { row: ActiveRow }) {
|
|
|
201
173
|
onClick={() =>
|
|
202
174
|
setPanel({ kind: 'blocks', key: row.key, listPath: row.info.listPath, index })
|
|
203
175
|
}
|
|
204
|
-
aria-label={
|
|
176
|
+
aria-label={t(edge === 'above' ? 'row.addSectionAbove' : 'row.addSectionBelow')}
|
|
205
177
|
data-testid={`onpage-insert-${edge}`}
|
|
206
178
|
>
|
|
207
179
|
+ {t('row.addSection')}
|
|
@@ -220,10 +192,11 @@ function AddItem({ list }: { list: Target }) {
|
|
|
220
192
|
const { snap, session, setPanel, setSelected, t } = useOnPage()
|
|
221
193
|
const key = refKey(list.ref)
|
|
222
194
|
const state = snap.docs[key]
|
|
223
|
-
if (!state
|
|
195
|
+
if (!isReady(state) || !isEditablePath(state.schema, list.path)) return null
|
|
224
196
|
const field = fieldAtPath(state.schema.fields, state.doc, list.path)?.field
|
|
225
|
-
if (!field || (field
|
|
226
|
-
const
|
|
197
|
+
if (!field || !isListField(field)) return null
|
|
198
|
+
const rows = getAt(state.doc, list.path)
|
|
199
|
+
const length = Array.isArray(rows) ? rows.length : 0
|
|
227
200
|
const r = list.el.getBoundingClientRect()
|
|
228
201
|
if (field.type === 'blocks') {
|
|
229
202
|
// Sections get "+ Add section" above/below each one; this covers an empty list.
|
|
@@ -231,7 +204,10 @@ function AddItem({ list }: { list: Target }) {
|
|
|
231
204
|
return (
|
|
232
205
|
<div
|
|
233
206
|
className="po-add"
|
|
234
|
-
style={{
|
|
207
|
+
style={{
|
|
208
|
+
top: r.top + Math.min(r.height / 2, GEOMETRY.emptyListOffset),
|
|
209
|
+
left: r.left + r.width / 2,
|
|
210
|
+
}}
|
|
235
211
|
>
|
|
236
212
|
<button
|
|
237
213
|
type="button"
|
|
@@ -246,13 +222,10 @@ function AddItem({ list }: { list: Target }) {
|
|
|
246
222
|
}
|
|
247
223
|
if (field.maxRows !== undefined && length >= field.maxRows) return null
|
|
248
224
|
// Below the list, or pinned to the bottom of the viewport while the list runs past it.
|
|
249
|
-
if (r
|
|
250
|
-
const y =
|
|
225
|
+
if (!inViewport(r) || r.top > window.innerHeight - GEOMETRY.toolbar) return null
|
|
226
|
+
const y = belowList(r)
|
|
251
227
|
const item = list.label || field.labels?.singular || field.label
|
|
252
|
-
const add = () =>
|
|
253
|
-
session.apply(key, (d) => updateArray(d, list.path, (a) => [...a, newArrayRow(field)]))
|
|
254
|
-
setSelected({ key, path: `${list.path}.${length}` })
|
|
255
|
-
}
|
|
228
|
+
const add = () => setSelected(insertRow(session, key, list.path, length, newArrayRow(field)))
|
|
256
229
|
return (
|
|
257
230
|
<div className="po-add is-item" style={{ top: y, left: r.left + r.width / 2 }}>
|
|
258
231
|
<button type="button" className="po-btn" onClick={add} data-testid="onpage-add-item">
|
|
@@ -271,8 +244,7 @@ export function Overlay() {
|
|
|
271
244
|
if (!target) return null
|
|
272
245
|
const key = refKey(target.ref)
|
|
273
246
|
const state = snap.docs[key]
|
|
274
|
-
const info =
|
|
275
|
-
state?.schema && state.doc ? resolveRow(state.schema, state.doc, target.path) : null
|
|
247
|
+
const info = isReady(state) ? resolveRow(state.schema, state.doc, target.path) : null
|
|
276
248
|
return info ? { key, info, el: target.el } : null
|
|
277
249
|
}
|
|
278
250
|
|
|
@@ -284,13 +256,13 @@ export function Overlay() {
|
|
|
284
256
|
// Its section: the outermost enclosing row of a blocks field (may be itself).
|
|
285
257
|
let section: ActiveRow | null = null
|
|
286
258
|
if (active) {
|
|
287
|
-
const
|
|
288
|
-
for (let i = 1; i < segs.length && !section; i++) {
|
|
289
|
-
if (!/^\d+$/.test(segs[i])) continue
|
|
290
|
-
const path = segs.slice(0, i + 1).join('.')
|
|
259
|
+
for (const path of [...rowPrefixes(active.info.path), active.info.path]) {
|
|
291
260
|
const candidate =
|
|
292
261
|
path === active.info.path ? active : rowFor(targets.find(active.key, path, ['item']))
|
|
293
|
-
if (candidate?.info.isBlock)
|
|
262
|
+
if (candidate?.info.isBlock) {
|
|
263
|
+
section = candidate
|
|
264
|
+
break
|
|
265
|
+
}
|
|
294
266
|
}
|
|
295
267
|
}
|
|
296
268
|
const item = active && section?.info.path !== active.info.path ? active : null
|
|
@@ -302,7 +274,8 @@ export function Overlay() {
|
|
|
302
274
|
: undefined
|
|
303
275
|
const emptyLists = lists.filter((l) => {
|
|
304
276
|
const doc = snap.docs[refKey(l.ref)]?.doc
|
|
305
|
-
|
|
277
|
+
const rows = doc ? getAt(doc, l.path) : undefined
|
|
278
|
+
return doc && !(Array.isArray(rows) && rows.length)
|
|
306
279
|
})
|
|
307
280
|
const hoverField = !editing ? hover.field : undefined
|
|
308
281
|
|
|
@@ -5,21 +5,21 @@
|
|
|
5
5
|
* page in a framed viewport. Signed-in editors can switch back to editing;
|
|
6
6
|
* people on a shared link can leave to the published page.
|
|
7
7
|
*/
|
|
8
|
-
import { useEffect, useMemo, useState
|
|
8
|
+
import { useEffect, useMemo, useState } from 'react'
|
|
9
|
+
import { useEditorStyles, useIsClient } from '../useIsClient.ts'
|
|
9
10
|
import { createPortal } from 'react-dom'
|
|
10
11
|
import { UI } from '../dom.ts'
|
|
11
12
|
import { createTranslate, type Messages } from '../messages.ts'
|
|
12
|
-
import { injectStyles } from '../styles.ts'
|
|
13
13
|
import { PostButton } from './PostButton.tsx'
|
|
14
14
|
|
|
15
15
|
export interface PreviewToolbarProps {
|
|
16
16
|
/** Endpoint that leaves preview (POST), back to the published page. */
|
|
17
17
|
exitHref: string
|
|
18
18
|
/** Endpoint that switches to editing (POST); omit for people who can't edit (shared links). */
|
|
19
|
-
editHref?: string
|
|
19
|
+
editHref?: string | undefined
|
|
20
20
|
/** Opened from a shared preview link. */
|
|
21
|
-
shared?: boolean
|
|
22
|
-
messages?: Messages
|
|
21
|
+
shared?: boolean | undefined
|
|
22
|
+
messages?: Messages | undefined
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
const DEVICES = [
|
|
@@ -34,19 +34,11 @@ export const inPreviewFrame = (): boolean =>
|
|
|
34
34
|
typeof window !== 'undefined' &&
|
|
35
35
|
(window.self !== window.top || new URLSearchParams(window.location.search).has(FRAME_PARAM))
|
|
36
36
|
|
|
37
|
-
const noop = () => () => {}
|
|
38
|
-
const useMounted = () =>
|
|
39
|
-
useSyncExternalStore(
|
|
40
|
-
noop,
|
|
41
|
-
() => true,
|
|
42
|
-
() => false,
|
|
43
|
-
)
|
|
44
|
-
|
|
45
37
|
export function PreviewToolbar({ exitHref, editHref, shared, messages }: PreviewToolbarProps) {
|
|
46
38
|
const [device, setDevice] = useState<(typeof DEVICES)[number] | null>(null)
|
|
47
|
-
const mounted =
|
|
39
|
+
const mounted = useIsClient()
|
|
48
40
|
const t = useMemo(() => createTranslate(messages), [messages])
|
|
49
|
-
|
|
41
|
+
useEditorStyles()
|
|
50
42
|
useEffect(() => {
|
|
51
43
|
if (!device) return
|
|
52
44
|
const onKey = (e: KeyboardEvent) => e.key === 'Escape' && setDevice(null)
|
package/src/ui/SettingsPanel.tsx
CHANGED
|
@@ -6,40 +6,29 @@
|
|
|
6
6
|
* spacing…), checkboxes, numbers, dates, relationships, URLs and other plain
|
|
7
7
|
* text, images that aren't on the page. Changes apply immediately.
|
|
8
8
|
*/
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
fieldAtPath,
|
|
11
|
+
getAt,
|
|
12
|
+
relationTitle,
|
|
13
|
+
rowFieldsOf,
|
|
14
|
+
toText,
|
|
15
|
+
isListField,
|
|
16
|
+
type ChoiceField,
|
|
17
|
+
type NumberField,
|
|
18
|
+
type ScalarField,
|
|
19
|
+
type SchemaField,
|
|
20
|
+
isReady,
|
|
21
|
+
type RefKey,
|
|
22
|
+
} from '@payload-onpage/core'
|
|
10
23
|
import { useId, useRef, useState, type KeyboardEvent, type RefObject } from 'react'
|
|
11
24
|
import { useOnPage } from '../context.tsx'
|
|
12
25
|
import type { Translate } from '../messages.ts'
|
|
13
|
-
import
|
|
26
|
+
import { checkedOf } from '../editors/CheckboxEditor.tsx'
|
|
27
|
+
import { selectedValues } from '../editors/SelectEditor.tsx'
|
|
28
|
+
import { canEdit } from '../registry.ts'
|
|
29
|
+
import { settable } from '../settings.ts'
|
|
14
30
|
import { Dialog } from './Dialog.tsx'
|
|
15
31
|
|
|
16
|
-
const SKIP = new Set(['id', 'blockType', 'blockName'])
|
|
17
|
-
const TEXTUAL = new Set(['text', 'textarea', 'email', 'number', 'date', 'relationship', 'upload'])
|
|
18
|
-
const INPUT_TYPES = new Set(['text', 'textarea', 'email', 'number'])
|
|
19
|
-
|
|
20
|
-
function settable(field: SchemaField, path: string, key: string, targets: TargetIndex): boolean {
|
|
21
|
-
if (SKIP.has(field.name)) return false
|
|
22
|
-
const full = `${path}.${field.name}`
|
|
23
|
-
switch (field.type) {
|
|
24
|
-
case 'select':
|
|
25
|
-
case 'radio':
|
|
26
|
-
case 'checkbox':
|
|
27
|
-
return true
|
|
28
|
-
case 'group':
|
|
29
|
-
return (field.fields ?? []).some((f) => settable(f, full, key, targets))
|
|
30
|
-
default:
|
|
31
|
-
// Shown on the page? Edit it there.
|
|
32
|
-
return TEXTUAL.has(field.type) && !targets.find(key, full)
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export const hasSettings = (
|
|
37
|
-
fields: SchemaField[],
|
|
38
|
-
path: string,
|
|
39
|
-
key: string,
|
|
40
|
-
targets: TargetIndex,
|
|
41
|
-
): boolean => fields.some((f) => settable(f, path, key, targets))
|
|
42
|
-
|
|
43
32
|
const display = (v: unknown, t: Translate): string => {
|
|
44
33
|
if (v == null || v === '') return t('common.none')
|
|
45
34
|
if (Array.isArray(v))
|
|
@@ -53,9 +42,9 @@ function TextInput({
|
|
|
53
42
|
field,
|
|
54
43
|
id,
|
|
55
44
|
}: {
|
|
56
|
-
docKey:
|
|
45
|
+
docKey: RefKey
|
|
57
46
|
path: string
|
|
58
|
-
field:
|
|
47
|
+
field: ScalarField | NumberField
|
|
59
48
|
id: string
|
|
60
49
|
}) {
|
|
61
50
|
const { session, snap } = useOnPage()
|
|
@@ -92,14 +81,15 @@ function Segmented({
|
|
|
92
81
|
field,
|
|
93
82
|
labelId,
|
|
94
83
|
}: {
|
|
95
|
-
docKey:
|
|
84
|
+
docKey: RefKey
|
|
96
85
|
path: string
|
|
97
|
-
field:
|
|
86
|
+
field: ChoiceField
|
|
98
87
|
labelId: string
|
|
99
88
|
}) {
|
|
100
89
|
const { session, snap } = useOnPage()
|
|
101
|
-
const options = field.options
|
|
102
|
-
const current =
|
|
90
|
+
const options = field.options
|
|
91
|
+
const current =
|
|
92
|
+
selectedValues(getAt(snap.docs[docKey]?.doc, path) ?? field.defaultValue, field)[0] ?? ''
|
|
103
93
|
const index = Math.max(
|
|
104
94
|
0,
|
|
105
95
|
options.findIndex((o) => o.value === current),
|
|
@@ -148,54 +138,17 @@ function Control({
|
|
|
148
138
|
id,
|
|
149
139
|
labelId,
|
|
150
140
|
}: {
|
|
151
|
-
docKey:
|
|
141
|
+
docKey: RefKey
|
|
152
142
|
path: string
|
|
153
143
|
field: SchemaField
|
|
154
144
|
anchor: RefObject<HTMLDivElement | null>
|
|
155
145
|
id: string
|
|
156
146
|
labelId: string
|
|
157
147
|
}) {
|
|
158
|
-
const { session, snap, edit, t } = useOnPage()
|
|
148
|
+
const { session, snap, edit, editors, t } = useOnPage()
|
|
159
149
|
const value = getAt(snap.docs[docKey]?.doc, path)
|
|
160
150
|
const set = (v: unknown) => session.setValue(docKey, path, v)
|
|
161
|
-
|
|
162
|
-
const options = field.options ?? []
|
|
163
|
-
if (options.length <= 5)
|
|
164
|
-
return <Segmented docKey={docKey} path={path} field={field} labelId={labelId} />
|
|
165
|
-
return (
|
|
166
|
-
<select
|
|
167
|
-
id={id}
|
|
168
|
-
className="po-select"
|
|
169
|
-
value={toText(value ?? field.defaultValue)}
|
|
170
|
-
onChange={(e) => set(e.target.value || null)}
|
|
171
|
-
data-testid={`onpage-setting-${path}`}
|
|
172
|
-
>
|
|
173
|
-
{!field.required && <option value="">{t('common.none')}</option>}
|
|
174
|
-
{options.map((o) => (
|
|
175
|
-
<option key={o.value} value={o.value}>
|
|
176
|
-
{o.label}
|
|
177
|
-
</option>
|
|
178
|
-
))}
|
|
179
|
-
</select>
|
|
180
|
-
)
|
|
181
|
-
}
|
|
182
|
-
if (field.type === 'checkbox') {
|
|
183
|
-
return (
|
|
184
|
-
<label className="po-check">
|
|
185
|
-
<input
|
|
186
|
-
type="checkbox"
|
|
187
|
-
checked={Boolean(value ?? field.defaultValue)}
|
|
188
|
-
onChange={(e) => set(e.target.checked)}
|
|
189
|
-
data-testid={`onpage-setting-${path}`}
|
|
190
|
-
/>
|
|
191
|
-
{field.label}
|
|
192
|
-
</label>
|
|
193
|
-
)
|
|
194
|
-
}
|
|
195
|
-
if (INPUT_TYPES.has(field.type))
|
|
196
|
-
return <TextInput docKey={docKey} path={path} field={field} id={id} />
|
|
197
|
-
// Everything else opens its field editor, anchored to the panel row.
|
|
198
|
-
return (
|
|
151
|
+
const openEditor = (
|
|
199
152
|
<button
|
|
200
153
|
id={id}
|
|
201
154
|
type="button"
|
|
@@ -208,9 +161,59 @@ function Control({
|
|
|
208
161
|
: display(value, t)}
|
|
209
162
|
</button>
|
|
210
163
|
)
|
|
164
|
+
// A field with its own editor (e.g. `editor: 'color'`) opens it, whatever its type.
|
|
165
|
+
if (field.editor && canEdit(editors[field.editor], field)) return openEditor
|
|
166
|
+
switch (field.type) {
|
|
167
|
+
case 'select':
|
|
168
|
+
case 'radio': {
|
|
169
|
+
if (field.hasMany) return openEditor
|
|
170
|
+
if (field.options.length <= 5)
|
|
171
|
+
return <Segmented docKey={docKey} path={path} field={field} labelId={labelId} />
|
|
172
|
+
return (
|
|
173
|
+
<select
|
|
174
|
+
id={id}
|
|
175
|
+
className="po-select"
|
|
176
|
+
value={selectedValues(value ?? field.defaultValue, field)[0] ?? ''}
|
|
177
|
+
onChange={(e) => set(e.target.value || null)}
|
|
178
|
+
data-testid={`onpage-setting-${path}`}
|
|
179
|
+
>
|
|
180
|
+
{!field.required && <option value="">{t('common.none')}</option>}
|
|
181
|
+
{field.options.map((o) => (
|
|
182
|
+
<option key={o.value} value={o.value}>
|
|
183
|
+
{o.label}
|
|
184
|
+
</option>
|
|
185
|
+
))}
|
|
186
|
+
</select>
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
case 'checkbox':
|
|
190
|
+
return (
|
|
191
|
+
<label className="po-check">
|
|
192
|
+
<input
|
|
193
|
+
type="checkbox"
|
|
194
|
+
checked={checkedOf(value, field)}
|
|
195
|
+
onChange={(e) => set(e.target.checked)}
|
|
196
|
+
data-testid={`onpage-setting-${path}`}
|
|
197
|
+
/>
|
|
198
|
+
{field.label}
|
|
199
|
+
</label>
|
|
200
|
+
)
|
|
201
|
+
case 'text':
|
|
202
|
+
case 'textarea':
|
|
203
|
+
case 'email':
|
|
204
|
+
case 'number':
|
|
205
|
+
return 'hasMany' in field && field.hasMany ? (
|
|
206
|
+
openEditor
|
|
207
|
+
) : (
|
|
208
|
+
<TextInput docKey={docKey} path={path} field={field} id={id} />
|
|
209
|
+
)
|
|
210
|
+
default:
|
|
211
|
+
// Dates, relationships, images…: open the field editor, anchored to the panel row.
|
|
212
|
+
return openEditor
|
|
213
|
+
}
|
|
211
214
|
}
|
|
212
215
|
|
|
213
|
-
function Row({ docKey, path, field }: { docKey:
|
|
216
|
+
function Row({ docKey, path, field }: { docKey: RefKey; path: string; field: SchemaField }) {
|
|
214
217
|
const anchor = useRef<HTMLDivElement>(null)
|
|
215
218
|
const id = useId()
|
|
216
219
|
const labelId = `${id}-label`
|
|
@@ -234,7 +237,7 @@ function Row({ docKey, path, field }: { docKey: string; path: string; field: Sch
|
|
|
234
237
|
)
|
|
235
238
|
}
|
|
236
239
|
|
|
237
|
-
function Fields({ docKey, fields, path }: { docKey:
|
|
240
|
+
function Fields({ docKey, fields, path }: { docKey: RefKey; fields: SchemaField[]; path: string }) {
|
|
238
241
|
const { targets } = useOnPage()
|
|
239
242
|
return (
|
|
240
243
|
<>
|
|
@@ -246,7 +249,7 @@ function Fields({ docKey, fields, path }: { docKey: string; fields: SchemaField[
|
|
|
246
249
|
return (
|
|
247
250
|
<fieldset key={f.name} className="po-group">
|
|
248
251
|
<legend className="po-h4">{f.label}</legend>
|
|
249
|
-
<Fields docKey={docKey} fields={f.fields
|
|
252
|
+
<Fields docKey={docKey} fields={f.fields} path={full} />
|
|
250
253
|
</fieldset>
|
|
251
254
|
)
|
|
252
255
|
}
|
|
@@ -260,11 +263,11 @@ export function SettingsPanel() {
|
|
|
260
263
|
const { panel, setPanel, snap, t } = useOnPage()
|
|
261
264
|
if (panel?.kind !== 'settings') return null
|
|
262
265
|
const state = snap.docs[panel.key]
|
|
263
|
-
if (!state
|
|
264
|
-
const owner = fieldAtPath(state.schema.fields, state.doc, panel.path)
|
|
266
|
+
if (!isReady(state)) return null
|
|
267
|
+
const owner = fieldAtPath(state.schema.fields, state.doc, panel.path)?.field
|
|
265
268
|
// Row fields: the block's fields for a blocks row, the item fields for an array row.
|
|
266
269
|
const fields =
|
|
267
|
-
owner
|
|
270
|
+
owner && isListField(owner) ? (rowFieldsOf(owner, getAt(state.doc, panel.path)) ?? []) : []
|
|
268
271
|
return (
|
|
269
272
|
<Dialog
|
|
270
273
|
variant="panel"
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import type { ReactNode } from 'react'
|
|
4
|
+
|
|
5
|
+
/** An icon button in a toolbar: labelled for assistive tech, with the label (or a hint) as its tooltip. */
|
|
6
|
+
export function ToolbarButton({
|
|
7
|
+
label,
|
|
8
|
+
hint,
|
|
9
|
+
icon,
|
|
10
|
+
onClick,
|
|
11
|
+
disabled,
|
|
12
|
+
danger,
|
|
13
|
+
testId,
|
|
14
|
+
}: {
|
|
15
|
+
label: string
|
|
16
|
+
hint?: string
|
|
17
|
+
icon: ReactNode
|
|
18
|
+
onClick: () => void
|
|
19
|
+
disabled?: boolean
|
|
20
|
+
danger?: boolean
|
|
21
|
+
testId: string
|
|
22
|
+
}) {
|
|
23
|
+
return (
|
|
24
|
+
<button
|
|
25
|
+
type="button"
|
|
26
|
+
className={`po-btn${danger ? ' is-danger' : ''}`}
|
|
27
|
+
disabled={disabled}
|
|
28
|
+
onClick={onClick}
|
|
29
|
+
aria-label={label}
|
|
30
|
+
title={hint ?? label}
|
|
31
|
+
data-testid={testId}
|
|
32
|
+
>
|
|
33
|
+
{icon}
|
|
34
|
+
</button>
|
|
35
|
+
)
|
|
36
|
+
}
|
package/src/useEvent.ts
CHANGED
|
@@ -7,10 +7,12 @@ import { useCallback, useInsertionEffect, useRef } from 'react'
|
|
|
7
7
|
* pattern). Our own, because the React bundled with Next.js 15 doesn't export
|
|
8
8
|
* `useEffectEvent`. Call it from effects and event handlers, not during render.
|
|
9
9
|
*/
|
|
10
|
-
export function useEvent<
|
|
10
|
+
export function useEvent<F extends (...args: never[]) => unknown>(fn: F): F {
|
|
11
11
|
const latest = useRef(fn)
|
|
12
12
|
useInsertionEffect(() => {
|
|
13
13
|
latest.current = fn
|
|
14
14
|
})
|
|
15
|
-
|
|
15
|
+
const stable = useCallback((...args: Parameters<F>) => latest.current(...args), [])
|
|
16
|
+
// The wrapper has F's exact signature: it forwards its arguments and result unchanged.
|
|
17
|
+
return stable as F
|
|
16
18
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { useEffect, useSyncExternalStore } from 'react'
|
|
2
|
+
import { injectStyles } from './styles.ts'
|
|
3
|
+
|
|
4
|
+
const noopSubscribe = () => () => {}
|
|
5
|
+
|
|
6
|
+
/** False during SSR and hydration, true after: for UI that only exists in the browser. */
|
|
7
|
+
export const useIsClient = (): boolean =>
|
|
8
|
+
useSyncExternalStore(
|
|
9
|
+
noopSubscribe,
|
|
10
|
+
() => true,
|
|
11
|
+
() => false,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
/** Adds the editor's stylesheet to the page while mounted. */
|
|
15
|
+
export const useEditorStyles = (): void => useEffect(() => injectStyles(), [])
|