@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/styles.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* The editor's own styles, injected once. Everything is prefixed `po-` and set
|
|
3
3
|
* explicitly, so the host site's CSS (and its absence) doesn't matter.
|
|
4
4
|
*/
|
|
5
|
+
import { DOM_ATTR } from './dom.ts'
|
|
5
6
|
export const PO_Z = 2147483000
|
|
6
7
|
|
|
7
8
|
export const PO_CSS = `
|
|
@@ -51,14 +52,14 @@ export const PO_CSS = `
|
|
|
51
52
|
.po-blocks{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:10px}
|
|
52
53
|
.po-blocks button{all:unset;padding:14px;border:1px solid #e2e8f0;border-radius:10px;cursor:pointer;font:600 13px system-ui,sans-serif}
|
|
53
54
|
.po-blocks button:hover,.po-blocks button:focus-visible{border-color:#4f7cff;background:#f5f8ff}
|
|
54
|
-
.po-dock{position:fixed;left:50%;bottom:16px;transform:translateX(-50%);display:flex;align-items:center;gap:4px;padding:6px;background:#0f172a;color:#fff;border-radius:12px;box-shadow:0 12px 40px #0f172a55;pointer-events:auto;max-width:calc(100vw - 24px);flex-wrap:wrap}
|
|
55
|
+
.po-dock{position:fixed;left:50%;width:max-content;bottom:16px;transform:translateX(-50%);display:flex;align-items:center;gap:4px;padding:6px;background:#0f172a;color:#fff;border-radius:12px;box-shadow:0 12px 40px #0f172a55;pointer-events:auto;max-width:calc(100vw - 24px);flex-wrap:wrap}
|
|
55
56
|
.po-dock .po-status{padding:0 10px;font-size:12px;opacity:.85;white-space:nowrap}
|
|
56
57
|
.po-dock .po-status.is-bad{color:#fca5a5;opacity:1}
|
|
57
58
|
.po-dock .po-sep{width:1px;height:20px;background:#ffffff26;margin:0 4px}
|
|
58
59
|
.po-dock .po-primary{background:#3b63e0}
|
|
59
60
|
.po-dock .po-primary:hover{background:#2f5bd9}
|
|
60
61
|
.po-chip-locale{padding:2px 7px;border-radius:999px;background:#ffffff1f;font-size:11px;font-weight:700;text-transform:uppercase}
|
|
61
|
-
.po-notice{position:fixed;left:50%;bottom:72px;transform:translateX(-50%);max-width:min(560px,calc(100vw - 24px));padding:8px 12px;border-radius:10px;background:#fffbeb;color:#78350f;border:1px solid #fcd34d;font-size:12px;pointer-events:auto}
|
|
62
|
+
.po-notice{position:fixed;left:50%;width:max-content;bottom:72px;transform:translateX(-50%);max-width:min(560px,calc(100vw - 24px));padding:8px 12px;border-radius:10px;background:#fffbeb;color:#78350f;border:1px solid #fcd34d;font-size:12px;pointer-events:auto}
|
|
62
63
|
.po-doc-layer{position:absolute;top:0;left:0;width:0;height:0;overflow:visible;z-index:${PO_Z};pointer-events:none;font:13px/1.35 system-ui,-apple-system,"Segoe UI",sans-serif}
|
|
63
64
|
.po-inline{position:absolute;pointer-events:auto;outline:2px solid #4f7cff;outline-offset:2px;border-radius:2px;background:transparent;white-space:pre-wrap;word-break:break-word;caret-color:currentColor;cursor:text}
|
|
64
65
|
.po-inline:focus{outline-color:#4f7cff}
|
|
@@ -81,7 +82,7 @@ export const PO_CSS = `
|
|
|
81
82
|
.po-opaque{display:inline-flex;align-items:center;gap:6px;margin:6px 0;padding:6px 10px;border:1px dashed #94a3b8;border-radius:8px;background:#f8fafc;color:#475569;font:12px system-ui,sans-serif;user-select:none}
|
|
82
83
|
html[data-onpage-editing] [data-onpage-field]{cursor:pointer}
|
|
83
84
|
[data-onpage-hidden="true"]{visibility:hidden!important}
|
|
84
|
-
.po-preview{position:fixed;left:50%;bottom:16px;transform:translateX(-50%);display:flex;align-items:center;gap:6px;padding:6px;background:#0f172a;color:#fff;border-radius:12px;box-shadow:0 12px 40px #0f172a55;pointer-events:auto;max-width:calc(100vw - 24px);flex-wrap:wrap}
|
|
85
|
+
.po-preview{position:fixed;left:50%;width:max-content;bottom:16px;transform:translateX(-50%);display:flex;align-items:center;gap:6px;padding:6px;background:#0f172a;color:#fff;border-radius:12px;box-shadow:0 12px 40px #0f172a55;pointer-events:auto;max-width:calc(100vw - 24px);flex-wrap:wrap}
|
|
85
86
|
.po-preview .po-status{padding:0 8px;font-size:12px;opacity:.85}
|
|
86
87
|
.po-rel-item{display:flex;align-items:center;gap:6px;padding:4px 6px;border-radius:6px;background:#f1f5f9;font-size:12px;margin:0 0 4px}
|
|
87
88
|
.po-rel-item button{all:unset;cursor:pointer;margin-left:auto;padding:0 4px;color:#64748b}
|
|
@@ -116,7 +117,7 @@ export function injectStyles() {
|
|
|
116
117
|
if (injected || typeof document === 'undefined') return
|
|
117
118
|
injected = true
|
|
118
119
|
const el = document.createElement('style')
|
|
119
|
-
el.setAttribute(
|
|
120
|
+
el.setAttribute(DOM_ATTR.styles, '')
|
|
120
121
|
el.textContent = PO_CSS
|
|
121
122
|
document.head.appendChild(el)
|
|
122
123
|
}
|
package/src/targets.ts
CHANGED
|
@@ -42,7 +42,7 @@ export function buildIndex(all: Target[]): TargetIndex {
|
|
|
42
42
|
export function targetsAt(
|
|
43
43
|
index: TargetIndex,
|
|
44
44
|
el: Element | null,
|
|
45
|
-
): { field?: Target; rows: Target[] } {
|
|
45
|
+
): { field?: Target | undefined; rows: Target[] } {
|
|
46
46
|
let field: Target | undefined
|
|
47
47
|
const rows: Target[] = []
|
|
48
48
|
for (let node = el; node; node = node.parentElement) {
|
package/src/types.ts
CHANGED
|
@@ -6,41 +6,47 @@ import type {
|
|
|
6
6
|
MediaLibrary,
|
|
7
7
|
Renderer,
|
|
8
8
|
SchemaField,
|
|
9
|
+
SchemaFieldType,
|
|
9
10
|
SessionSnapshot,
|
|
10
11
|
Target,
|
|
11
12
|
TargetKind,
|
|
13
|
+
RefKey,
|
|
12
14
|
} from '@payload-onpage/core'
|
|
13
|
-
import type {
|
|
15
|
+
import type { Translate } from './messages.ts'
|
|
14
16
|
|
|
15
17
|
export interface EditingState {
|
|
16
18
|
/** refKey of the document */
|
|
17
|
-
key:
|
|
19
|
+
key: RefKey
|
|
18
20
|
path: string
|
|
19
21
|
/** Editor id from the registry ("text", "richText", …). */
|
|
20
22
|
editor: string
|
|
21
|
-
point?: { x: number; y: number }
|
|
23
|
+
point?: { x: number; y: number } | undefined
|
|
22
24
|
/** The element that was clicked (several elements may show one field). */
|
|
23
|
-
el?: Element
|
|
25
|
+
el?: Element | undefined
|
|
24
26
|
}
|
|
25
27
|
|
|
28
|
+
/** Which panel is open. */
|
|
26
29
|
export type Panel =
|
|
27
|
-
| { kind: 'settings'; key:
|
|
28
|
-
| { kind: 'blocks'; key:
|
|
30
|
+
| { kind: 'settings'; key: RefKey; path: string; title: string }
|
|
31
|
+
| { kind: 'blocks'; key: RefKey; listPath: string; index: number }
|
|
29
32
|
| null
|
|
30
33
|
|
|
31
34
|
export interface Selection {
|
|
32
|
-
key:
|
|
35
|
+
key: RefKey
|
|
33
36
|
path: string
|
|
34
37
|
}
|
|
35
38
|
|
|
36
|
-
/**
|
|
37
|
-
|
|
38
|
-
|
|
39
|
+
/**
|
|
40
|
+
* Props a field editor receives. `F` is the field kind it edits (e.g.
|
|
41
|
+
* `NumberField`, with `min`/`max`) and `V` its value after `normalize`.
|
|
42
|
+
*/
|
|
43
|
+
export interface FieldEditorProps<F extends SchemaField = SchemaField, V = unknown> {
|
|
44
|
+
docKey: RefKey
|
|
39
45
|
path: string
|
|
40
|
-
field:
|
|
46
|
+
field: F
|
|
41
47
|
/** The element showing the field (re-resolved after each refresh). */
|
|
42
48
|
element: () => Element | null
|
|
43
|
-
value:
|
|
49
|
+
value: V
|
|
44
50
|
/** Commit a value (one undo step, autosaved). */
|
|
45
51
|
commit: (value: unknown) => void
|
|
46
52
|
/** Live preview while editing (no history entry, not saved). */
|
|
@@ -48,23 +54,35 @@ export interface FieldEditorProps {
|
|
|
48
54
|
/** Close the editor (after commit, or to cancel: transient previews are reverted). */
|
|
49
55
|
close: () => void
|
|
50
56
|
/** Leave a stand-in showing the new content until the page re-renders (refresh mode). */
|
|
51
|
-
ghost: (g:
|
|
57
|
+
ghost: (g: GhostInput) => void
|
|
52
58
|
/** True when the page renders this document from editor state (instant); false = server refresh. */
|
|
53
59
|
live: boolean
|
|
54
|
-
point?: { x: number; y: number }
|
|
60
|
+
point?: { x: number; y: number } | undefined
|
|
55
61
|
/** UI strings. */
|
|
56
62
|
t: Translate
|
|
57
63
|
}
|
|
58
64
|
|
|
65
|
+
/** What an editor leaves on the page until it re-renders. */
|
|
66
|
+
export interface GhostInput {
|
|
67
|
+
html: string
|
|
68
|
+
className?: string | undefined
|
|
69
|
+
style?: Partial<CSSStyleDeclaration> | undefined
|
|
70
|
+
}
|
|
71
|
+
|
|
59
72
|
/**
|
|
60
73
|
* A field editor (the FieldEditor port). `inline` editors replace the element
|
|
61
74
|
* in place (text, rich text); `popover` editors open beside it or as a dialog
|
|
62
75
|
* (select, date, relationship, image…). Register your own by id and refer to it
|
|
63
76
|
* from a field with `admin: { custom: { onpage: { editor: 'my-id' } } }`, or by
|
|
64
|
-
* field type ("json", "code"…) to replace or add a default.
|
|
77
|
+
* field type ("json", "code"…) to replace or add a default. Build one with
|
|
78
|
+
* `defineEditor()` so its props are typed by the field kinds it accepts.
|
|
65
79
|
*/
|
|
66
80
|
export interface FieldEditorDef {
|
|
67
81
|
mode: 'inline' | 'popover'
|
|
82
|
+
/** Field types it can edit. The registry never hands it any other kind. Default: any. */
|
|
83
|
+
accepts?: readonly SchemaFieldType[]
|
|
84
|
+
/** The raw stored value → the editor's value. Default: passed through. */
|
|
85
|
+
normalize?: (raw: unknown, field: SchemaField) => unknown
|
|
68
86
|
component: ComponentType<FieldEditorProps>
|
|
69
87
|
}
|
|
70
88
|
|
|
@@ -81,52 +99,67 @@ export interface TargetIndex {
|
|
|
81
99
|
all: Target[]
|
|
82
100
|
/** Targets whose element is exactly `el`. */
|
|
83
101
|
at: (el: Element) => Target[]
|
|
84
|
-
find: (key:
|
|
102
|
+
find: (key: RefKey, path: string, kinds?: TargetKind[]) => Target | undefined
|
|
85
103
|
}
|
|
86
104
|
|
|
87
|
-
/**
|
|
88
|
-
export interface
|
|
105
|
+
/** A short message shown in the dock. */
|
|
106
|
+
export interface Flash {
|
|
107
|
+
text: string
|
|
108
|
+
bad?: boolean | undefined
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The editor's stable API: the session, configuration and actions. It changes
|
|
113
|
+
* only when the editor is reconfigured, so components that only act (toolbars,
|
|
114
|
+
* custom UI, client-mode documents) don't re-render on every keystroke.
|
|
115
|
+
*/
|
|
116
|
+
export interface OnPageApi {
|
|
89
117
|
session: EditorSession
|
|
90
|
-
snap: SessionSnapshot
|
|
91
118
|
renderer: Renderer
|
|
92
|
-
media?: MediaLibrary
|
|
119
|
+
media?: MediaLibrary | undefined
|
|
93
120
|
editors: EditorRegistry
|
|
94
|
-
targets: TargetIndex
|
|
95
121
|
links: Links
|
|
96
122
|
t: Translate
|
|
97
|
-
panel: Panel
|
|
98
123
|
setPanel: (p: Panel) => void
|
|
99
|
-
editing: EditingState | null
|
|
100
124
|
setEditing: (e: EditingState | null) => void
|
|
101
|
-
selected: Selection | null
|
|
102
125
|
setSelected: (s: Selection | null) => void
|
|
103
|
-
/** Documents shown on the current page (the session may hold others from earlier pages). */
|
|
104
|
-
docKeys: string[]
|
|
105
126
|
/** Is this document rendered from session state (client mode)? */
|
|
106
|
-
isLive: (key:
|
|
127
|
+
isLive: (key: RefKey) => boolean
|
|
107
128
|
/** Client-mode documents register here (they don't need a server refresh). */
|
|
108
|
-
registerClientDoc: (key:
|
|
109
|
-
/** The page is re-rendering after a save (refresh mode). */
|
|
110
|
-
rendering: boolean
|
|
129
|
+
registerClientDoc: (key: RefKey) => () => void
|
|
111
130
|
/** Open the right editor for a field (registry resolution); false if none applies. */
|
|
112
131
|
edit: (
|
|
113
|
-
key:
|
|
132
|
+
key: RefKey,
|
|
114
133
|
path: string,
|
|
115
|
-
opts?: {
|
|
134
|
+
opts?: {
|
|
135
|
+
point?: { x: number; y: number } | undefined
|
|
136
|
+
kind?: TargetKind | undefined
|
|
137
|
+
anchor?: Element | undefined
|
|
138
|
+
},
|
|
116
139
|
) => boolean
|
|
117
140
|
/** Re-render documents after an operation outside autosave (publish, reload). */
|
|
118
141
|
rerender: (refs: DocRef[]) => Promise<void>
|
|
119
|
-
/** Show a short message in the dock. */
|
|
120
|
-
notify: (
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
142
|
+
/** Show a short message in the dock: translated text (`notify(t('notify.published'))`), or an error's text. */
|
|
143
|
+
notify: (text: string, bad?: boolean) => void
|
|
144
|
+
addGhost: (g: GhostInput & { key: RefKey; path: string }) => void
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** What changes as people edit: the documents, what's on the page and what's open. */
|
|
148
|
+
export interface OnPageState {
|
|
149
|
+
snap: SessionSnapshot
|
|
150
|
+
targets: TargetIndex
|
|
151
|
+
panel: Panel
|
|
152
|
+
editing: EditingState | null
|
|
153
|
+
selected: Selection | null
|
|
154
|
+
/** Documents shown on the current page (the session may hold others from earlier pages). */
|
|
155
|
+
docKeys: RefKey[]
|
|
156
|
+
/** The page is re-rendering after a save (refresh mode). */
|
|
157
|
+
rendering: boolean
|
|
158
|
+
/** The dock's current message. */
|
|
159
|
+
flash: Flash | null
|
|
132
160
|
}
|
|
161
|
+
|
|
162
|
+
/** Everything inside the editor (`useOnPage()`): its API and its state. */
|
|
163
|
+
export type OnPageContextValue = OnPageApi & OnPageState
|
|
164
|
+
|
|
165
|
+
export type PanelKind = NonNullable<Panel>['kind']
|
package/src/ui/BlockPicker.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { fieldAtPath,
|
|
3
|
+
import { fieldAtPath, isReady, newBlockRow } from '@payload-onpage/core'
|
|
4
|
+
import { insertRow } from '../rowOps.ts'
|
|
4
5
|
import { useOnPage } from '../context.tsx'
|
|
5
6
|
import { Dialog } from './Dialog.tsx'
|
|
6
7
|
|
|
@@ -9,19 +10,17 @@ export function BlockPicker() {
|
|
|
9
10
|
const { panel, setPanel, snap, session, setSelected, t } = useOnPage()
|
|
10
11
|
if (panel?.kind !== 'blocks') return null
|
|
11
12
|
const state = snap.docs[panel.key]
|
|
12
|
-
if (!state
|
|
13
|
+
if (!isReady(state)) return null
|
|
13
14
|
const { key, listPath, index } = panel
|
|
14
15
|
const field = fieldAtPath(state.schema.fields, state.doc, listPath)?.field
|
|
15
|
-
const blocks = field?.type === 'blocks' ?
|
|
16
|
+
const blocks = field?.type === 'blocks' ? field.blocks : []
|
|
16
17
|
|
|
17
18
|
const choose = (slug: string) => {
|
|
18
19
|
const block = blocks.find((b) => b.slug === slug)
|
|
19
20
|
if (!block) return
|
|
20
|
-
session
|
|
21
|
-
updateArray(d, listPath, (a) => insertItem(a, index, newBlockRow(block))),
|
|
22
|
-
)
|
|
21
|
+
const next = insertRow(session, key, listPath, index, newBlockRow(block))
|
|
23
22
|
setPanel(null)
|
|
24
|
-
setSelected(
|
|
23
|
+
setSelected(next)
|
|
25
24
|
}
|
|
26
25
|
|
|
27
26
|
return (
|
package/src/ui/Dialog.tsx
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* close, Tab stays inside (modal), Escape closes. `variant="popover"` is
|
|
6
6
|
* anchored next to an element and closes on an outside click.
|
|
7
7
|
*/
|
|
8
|
+
import { useEvent } from '../useEvent.ts'
|
|
8
9
|
import { useEffect, useId, useRef, useState, type CSSProperties, type ReactNode } from 'react'
|
|
9
10
|
import { focusableIn, popoverPosition } from '../dom.ts'
|
|
10
11
|
import { useLayoutTick } from '../layout.ts'
|
|
@@ -62,18 +63,21 @@ export function Dialog({
|
|
|
62
63
|
return () => ro.disconnect()
|
|
63
64
|
}, [variant])
|
|
64
65
|
|
|
66
|
+
// Stable, so the listeners below aren't re-bound on every render (callers pass inline
|
|
67
|
+
// closures); a re-bind would also re-open the deferral gap where an outside click is lost.
|
|
68
|
+
const close = useEvent(onClose)
|
|
65
69
|
useEffect(() => {
|
|
66
70
|
const onKey = (e: KeyboardEvent) => {
|
|
67
71
|
if (e.key === 'Escape') {
|
|
68
72
|
e.stopPropagation()
|
|
69
|
-
|
|
73
|
+
close()
|
|
70
74
|
return
|
|
71
75
|
}
|
|
72
76
|
if (e.key !== 'Tab' || variant !== 'modal' || !box.current) return
|
|
73
77
|
const items = focusableIn(box.current)
|
|
74
|
-
if (!items.length) return
|
|
75
78
|
const first = items[0]
|
|
76
|
-
const last = items
|
|
79
|
+
const last = items.at(-1)
|
|
80
|
+
if (!first || !last) return
|
|
77
81
|
if (e.shiftKey && document.activeElement === first) {
|
|
78
82
|
e.preventDefault()
|
|
79
83
|
last.focus()
|
|
@@ -83,7 +87,7 @@ export function Dialog({
|
|
|
83
87
|
}
|
|
84
88
|
}
|
|
85
89
|
const onDown = (e: PointerEvent) => {
|
|
86
|
-
if (variant === 'popover' && box.current && !box.current.contains(e.target as Node))
|
|
90
|
+
if (variant === 'popover' && box.current && !box.current.contains(e.target as Node)) close()
|
|
87
91
|
}
|
|
88
92
|
document.addEventListener('keydown', onKey, true)
|
|
89
93
|
// Deferred, so the click that opened a popover doesn't close it.
|
|
@@ -93,7 +97,7 @@ export function Dialog({
|
|
|
93
97
|
document.removeEventListener('keydown', onKey, true)
|
|
94
98
|
document.removeEventListener('pointerdown', onDown, true)
|
|
95
99
|
}
|
|
96
|
-
}, [
|
|
100
|
+
}, [close, variant])
|
|
97
101
|
|
|
98
102
|
const header = (
|
|
99
103
|
<header className="po-dialog-head">
|
package/src/ui/Dock.tsx
CHANGED
|
@@ -5,14 +5,49 @@
|
|
|
5
5
|
* recovery: reload after a conflict, retry after an error), undo/redo, locale,
|
|
6
6
|
* share link, preview, discard, admin, exit and publish.
|
|
7
7
|
*/
|
|
8
|
+
import { errorText } from '@payload-onpage/core'
|
|
8
9
|
import type { DocumentState } from '@payload-onpage/core'
|
|
9
10
|
import { useEffect, useState } from 'react'
|
|
10
11
|
import { useOnPage } from '../context.tsx'
|
|
11
|
-
import { relativeTime } from '../messages.ts'
|
|
12
|
+
import { relativeTime, type Translate } from '../messages.ts'
|
|
12
13
|
import { PostButton } from './PostButton.tsx'
|
|
14
|
+
import { ToolbarButton } from './ToolbarButton.tsx'
|
|
15
|
+
import type { Flash } from '../types.ts'
|
|
13
16
|
|
|
14
17
|
const title = (d: DocumentState) => d.schema?.labels.singular ?? d.ref.slug
|
|
15
18
|
|
|
19
|
+
interface DockState {
|
|
20
|
+
docs: DocumentState[]
|
|
21
|
+
ready: DocumentState[]
|
|
22
|
+
failed: DocumentState | undefined
|
|
23
|
+
conflict: DocumentState | undefined
|
|
24
|
+
error: DocumentState | undefined
|
|
25
|
+
saving: boolean
|
|
26
|
+
rendering: boolean
|
|
27
|
+
lastSaved: number
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** What the dock says, most urgent first. */
|
|
31
|
+
function dockStatus(s: DockState, t: Translate): Flash {
|
|
32
|
+
if (s.failed)
|
|
33
|
+
return {
|
|
34
|
+
text: t('dock.unavailable', { doc: title(s.failed), error: s.failed.error ?? '' }),
|
|
35
|
+
bad: true,
|
|
36
|
+
}
|
|
37
|
+
if (!s.docs.length) return { text: t('dock.nothing') }
|
|
38
|
+
if (!s.ready.length) return { text: t('dock.loading') }
|
|
39
|
+
if (s.conflict) return { text: t('dock.conflict', { doc: title(s.conflict) }), bad: true }
|
|
40
|
+
if (s.error)
|
|
41
|
+
return {
|
|
42
|
+
text: t('dock.saveFailed', { doc: title(s.error), message: s.error.save.message ?? '' }),
|
|
43
|
+
bad: true,
|
|
44
|
+
}
|
|
45
|
+
if (s.saving) return { text: t('dock.saving') }
|
|
46
|
+
if (s.rendering) return { text: t('dock.updating') }
|
|
47
|
+
if (s.lastSaved) return { text: t('dock.saved', { ago: relativeTime(s.lastSaved) }) }
|
|
48
|
+
return { text: t('dock.editing') }
|
|
49
|
+
}
|
|
50
|
+
|
|
16
51
|
function useTicker(ms: number) {
|
|
17
52
|
const [, setN] = useState(0)
|
|
18
53
|
useEffect(() => {
|
|
@@ -21,8 +56,8 @@ function useTicker(ms: number) {
|
|
|
21
56
|
}, [ms])
|
|
22
57
|
}
|
|
23
58
|
|
|
24
|
-
export function Dock(
|
|
25
|
-
const { session, snap, links, rendering, notify, rerender, docKeys, t } = useOnPage()
|
|
59
|
+
export function Dock() {
|
|
60
|
+
const { session, snap, links, rendering, notify, rerender, docKeys, flash, t } = useOnPage()
|
|
26
61
|
const [confirmDiscard, setConfirmDiscard] = useState(false)
|
|
27
62
|
useTicker(5000)
|
|
28
63
|
useEffect(() => {
|
|
@@ -41,23 +76,11 @@ export function Dock({ flash }: { flash: { text: string; bad?: boolean } | null
|
|
|
41
76
|
const primary = docs[0]
|
|
42
77
|
const busy = snap.busy
|
|
43
78
|
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
? { text: t('dock.loading') }
|
|
50
|
-
: conflict
|
|
51
|
-
? { text: t('dock.conflict', { doc: title(conflict) }), bad: true }
|
|
52
|
-
: error
|
|
53
|
-
? { text: `${title(error)}: ${error.save.message ?? ''}`, bad: true }
|
|
54
|
-
: saving
|
|
55
|
-
? { text: t('dock.saving') }
|
|
56
|
-
: rendering
|
|
57
|
-
? { text: t('dock.updating') }
|
|
58
|
-
: lastSaved
|
|
59
|
-
? { text: t('dock.saved', { ago: relativeTime(lastSaved) }) }
|
|
60
|
-
: { text: t('dock.editing') }
|
|
79
|
+
const retryable = failed ?? error
|
|
80
|
+
const status = dockStatus(
|
|
81
|
+
{ docs, ready, failed, conflict, error, saving, rendering, lastSaved },
|
|
82
|
+
t,
|
|
83
|
+
)
|
|
61
84
|
|
|
62
85
|
const schema = ready[0]?.schema
|
|
63
86
|
const nonDefaultLocale =
|
|
@@ -67,10 +90,10 @@ export function Dock({ flash }: { flash: { text: string; bad?: boolean } | null
|
|
|
67
90
|
try {
|
|
68
91
|
const res = await session.publish({ keys: docKeys })
|
|
69
92
|
if (res.published.length) await rerender(res.published)
|
|
70
|
-
if (res.ok) notify(res.published.length ? 'notify.published' : 'notify.nothingToPublish')
|
|
71
|
-
else notify('notify.publishFailed', { message: res.message ?? '' }, true)
|
|
93
|
+
if (res.ok) notify(t(res.published.length ? 'notify.published' : 'notify.nothingToPublish'))
|
|
94
|
+
else notify(t('notify.publishFailed', { message: res.message ?? '' }), true)
|
|
72
95
|
} catch (e) {
|
|
73
|
-
notify('notify.publishFailed', { message: (e
|
|
96
|
+
notify(t('notify.publishFailed', { message: errorText(e) }), true)
|
|
74
97
|
}
|
|
75
98
|
}
|
|
76
99
|
const discard = async () => {
|
|
@@ -79,9 +102,9 @@ export function Dock({ flash }: { flash: { text: string; bad?: boolean } | null
|
|
|
79
102
|
setConfirmDiscard(false)
|
|
80
103
|
try {
|
|
81
104
|
await session.discard({ keys: docKeys })
|
|
82
|
-
notify('notify.discarded')
|
|
105
|
+
notify(t('notify.discarded'))
|
|
83
106
|
} catch (e) {
|
|
84
|
-
notify((e
|
|
107
|
+
notify(errorText(e), true)
|
|
85
108
|
}
|
|
86
109
|
}
|
|
87
110
|
const reload = async (d: DocumentState) => {
|
|
@@ -89,7 +112,7 @@ export function Dock({ flash }: { flash: { text: string; bad?: boolean } | null
|
|
|
89
112
|
await session.reload(d.key)
|
|
90
113
|
await rerender([d.ref])
|
|
91
114
|
} catch (e) {
|
|
92
|
-
notify((e
|
|
115
|
+
notify(errorText(e), true)
|
|
93
116
|
}
|
|
94
117
|
}
|
|
95
118
|
const createLink = session.dataSource.createPreviewLink?.bind(session.dataSource)
|
|
@@ -98,9 +121,9 @@ export function Dock({ flash }: { flash: { text: string; bad?: boolean } | null
|
|
|
98
121
|
try {
|
|
99
122
|
const { url, expiresAt } = await createLink(primary.ref, { locale: session.locale })
|
|
100
123
|
await navigator.clipboard.writeText(url)
|
|
101
|
-
notify('notify.linkCopied', { date: new Date(expiresAt).toLocaleString() })
|
|
124
|
+
notify(t('notify.linkCopied', { date: new Date(expiresAt).toLocaleString() }))
|
|
102
125
|
} catch (e) {
|
|
103
|
-
notify('notify.linkFailed', { message: (e
|
|
126
|
+
notify(t('notify.linkFailed', { message: errorText(e) }), true)
|
|
104
127
|
}
|
|
105
128
|
}
|
|
106
129
|
|
|
@@ -136,11 +159,11 @@ export function Dock({ flash }: { flash: { text: string; bad?: boolean } | null
|
|
|
136
159
|
{t('dock.reload')}
|
|
137
160
|
</button>
|
|
138
161
|
)}
|
|
139
|
-
{
|
|
162
|
+
{retryable && (
|
|
140
163
|
<button
|
|
141
164
|
type="button"
|
|
142
165
|
className="po-btn"
|
|
143
|
-
onClick={() => void session.retry(
|
|
166
|
+
onClick={() => void session.retry(retryable.key)}
|
|
144
167
|
data-testid="onpage-retry"
|
|
145
168
|
>
|
|
146
169
|
{t('dock.retry')}
|
|
@@ -157,28 +180,22 @@ export function Dock({ flash }: { flash: { text: string; bad?: boolean } | null
|
|
|
157
180
|
)}
|
|
158
181
|
{session.locale && <span className="po-chip-locale">{session.locale}</span>}
|
|
159
182
|
<span className="po-sep" />
|
|
160
|
-
<
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
183
|
+
<ToolbarButton
|
|
184
|
+
label={t('dock.undo')}
|
|
185
|
+
hint={`${t('dock.undo')} (⌘Z)`}
|
|
186
|
+
icon="↶"
|
|
164
187
|
disabled={!snap.canUndo}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
type="button"
|
|
173
|
-
className="po-btn"
|
|
174
|
-
onClick={() => session.redo()}
|
|
188
|
+
onClick={() => session.undo()}
|
|
189
|
+
testId="onpage-undo"
|
|
190
|
+
/>
|
|
191
|
+
<ToolbarButton
|
|
192
|
+
label={t('dock.redo')}
|
|
193
|
+
hint={`${t('dock.redo')} (⇧⌘Z)`}
|
|
194
|
+
icon="↷"
|
|
175
195
|
disabled={!snap.canRedo}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
>
|
|
180
|
-
↷
|
|
181
|
-
</button>
|
|
196
|
+
onClick={() => session.redo()}
|
|
197
|
+
testId="onpage-redo"
|
|
198
|
+
/>
|
|
182
199
|
<span className="po-sep" />
|
|
183
200
|
{links.mode && (
|
|
184
201
|
<PostButton
|