@simmalugnt-se/payload-visual-editing 0.1.0 → 0.1.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/CHANGELOG.md +11 -0
- package/README.md +2 -2
- package/dist/admin/VisualEditingAdminBridge.js +7 -3
- package/dist/frontend/VisualEditingPreview.js +16 -3
- package/dist/labels.d.ts +12 -2
- package/dist/labels.js +31 -0
- package/dist/protocol.d.ts +7 -3
- package/dist/protocol.js +13 -6
- package/dist/reveal-plan.d.ts +7 -2
- package/dist/reveal-plan.js +11 -2
- package/package.json +3 -2
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.1
|
|
4
|
+
|
|
5
|
+
- Array rows in the preview outline are labeled like Admin's row headers ("Item 02 › Title")
|
|
6
|
+
instead of "Block › Title".
|
|
7
|
+
|
|
8
|
+
## 0.1.0
|
|
9
|
+
|
|
10
|
+
- First release: click a block or field in Live Preview to open it in the edit form, with Admin's
|
|
11
|
+
block and field labels in the preview outline.
|
package/README.md
CHANGED
|
@@ -70,5 +70,5 @@ Requires Payload `>=3.85.2 <4` and React 19.
|
|
|
70
70
|
## Labels
|
|
71
71
|
|
|
72
72
|
The outline shows the same names as Admin: the editor's block name, otherwise the block's
|
|
73
|
-
`labels.singular`, then the field's `label`, e.g. "CTA › Button label".
|
|
74
|
-
|
|
73
|
+
`labels.singular`, then the field's `label`, e.g. "CTA › Button label". Array rows get their row
|
|
74
|
+
header, e.g. "Item 02 › Title". Admin sends the labels to the preview, so nothing needs configuring.
|
|
@@ -3,7 +3,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
3
3
|
import { useConfig, useDocumentForm, useDocumentInfo, useLivePreviewContext, useTranslation, } from "@payloadcms/ui";
|
|
4
4
|
import { useEffect, useRef } from "react";
|
|
5
5
|
import "../styles/admin.css";
|
|
6
|
-
import { collectBlockLabels } from "../labels.js";
|
|
6
|
+
import { collectBlockLabels, collectRowLabels } from "../labels.js";
|
|
7
7
|
import { fieldDomId, focusTargets, isReadyMessage, isSelectMessage, MESSAGE_TYPE, matchNodeIndex, pathFromDomId, pathFromRowDomId, resolveBlockTarget, rowDomId, } from "../protocol.js";
|
|
8
8
|
import { planReveal } from "../reveal-plan.js";
|
|
9
9
|
import { HIGHLIGHT_TINT } from "../theme.js";
|
|
@@ -53,11 +53,15 @@ export function VisualEditingAdminBridge() {
|
|
|
53
53
|
void select(event.data, run);
|
|
54
54
|
};
|
|
55
55
|
const sendLabels = (preview, origin) => {
|
|
56
|
-
const { config, getEntityConfig, collectionSlug, language } = latest.current;
|
|
56
|
+
const { form, config, getEntityConfig, collectionSlug, language } = latest.current;
|
|
57
|
+
const fields = form.getFields();
|
|
58
|
+
const rootFields = rootFieldsOf(getEntityConfig, collectionSlug);
|
|
59
|
+
const blocksBySlug = config.blocksMap;
|
|
57
60
|
const message = {
|
|
58
61
|
type: MESSAGE_TYPE,
|
|
59
62
|
action: "labels",
|
|
60
|
-
blocks: collectBlockLabels(
|
|
63
|
+
blocks: collectBlockLabels(rootFields, blocksBySlug, language),
|
|
64
|
+
rows: collectRowLabels(rootFields, fields, { blockTypeAt: (rowPath) => rowAt(fields, rowPath)?.blockType, blocksBySlug }, language),
|
|
61
65
|
};
|
|
62
66
|
preview.postMessage(message, origin);
|
|
63
67
|
};
|
|
@@ -19,7 +19,14 @@ export function VisualEditingPreview({ adminOrigin }) {
|
|
|
19
19
|
let hovered = null;
|
|
20
20
|
let current = null;
|
|
21
21
|
// Until Admin answers, labels fall back to the slugs.
|
|
22
|
-
let labels = {};
|
|
22
|
+
let labels = { blocks: {}, rows: {} };
|
|
23
|
+
// Array row numbers change as rows move, so ask again whenever the pointer reaches a row.
|
|
24
|
+
const describe = (hit) => {
|
|
25
|
+
if (hit && isArrayRow(hit.element)) {
|
|
26
|
+
const message = { type: MESSAGE_TYPE, action: "describe" };
|
|
27
|
+
admin.postMessage(message, targetOrigin);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
23
30
|
let frame = 0;
|
|
24
31
|
const redraw = () => {
|
|
25
32
|
cancelAnimationFrame(frame);
|
|
@@ -34,6 +41,7 @@ export function VisualEditingPreview({ adminOrigin }) {
|
|
|
34
41
|
return;
|
|
35
42
|
}
|
|
36
43
|
hovered = hit;
|
|
44
|
+
describe(hit);
|
|
37
45
|
redraw();
|
|
38
46
|
};
|
|
39
47
|
const onPointerLeave = () => {
|
|
@@ -72,7 +80,7 @@ export function VisualEditingPreview({ adminOrigin }) {
|
|
|
72
80
|
return;
|
|
73
81
|
}
|
|
74
82
|
if (isLabelsMessage(event.data)) {
|
|
75
|
-
labels = event.data.blocks;
|
|
83
|
+
labels = { blocks: event.data.blocks, rows: event.data.rows ?? labels.rows };
|
|
76
84
|
hovered = hovered && relabel(hovered, labels);
|
|
77
85
|
current = current && relabel(current, labels);
|
|
78
86
|
redraw();
|
|
@@ -164,13 +172,18 @@ function hitForBlock(blockId, field, labels) {
|
|
|
164
172
|
/** Same order as Admin's row header: the editor's block name, then the block's label, then its slug. */
|
|
165
173
|
function labelFor(block, field, labels) {
|
|
166
174
|
const type = block.getAttribute(ATTR_BLOCK_TYPE) ?? undefined;
|
|
167
|
-
const known = type ? labels[type] :
|
|
175
|
+
const known = type ? labels.blocks[type] : labels.rows[block.getAttribute(ATTR_BLOCK) ?? ""];
|
|
168
176
|
const blockLabel = block.getAttribute(ATTR_BLOCK_NAME) || known?.label || (type ? humanize(type) : "Block");
|
|
169
177
|
if (!field) {
|
|
170
178
|
return blockLabel;
|
|
171
179
|
}
|
|
172
180
|
return `${blockLabel} › ${known?.fields[field] ?? humanize(field)}`;
|
|
173
181
|
}
|
|
182
|
+
/** Rows marked without a block type are array rows. */
|
|
183
|
+
function isArrayRow(element) {
|
|
184
|
+
const row = element.closest(`[${ATTR_BLOCK}]`);
|
|
185
|
+
return !!row && !row.hasAttribute(ATTR_BLOCK_TYPE);
|
|
186
|
+
}
|
|
174
187
|
function relabel(hit, labels) {
|
|
175
188
|
const block = hit.element.closest(`[${ATTR_BLOCK}]`);
|
|
176
189
|
return block ? { ...hit, label: labelFor(block, hit.field, labels) } : hit;
|
package/dist/labels.d.ts
CHANGED
|
@@ -3,15 +3,25 @@
|
|
|
3
3
|
* the same names ("CTA › Title") instead of guessing from slugs ("C ta › Title").
|
|
4
4
|
* Pure: takes client field config, returns plain strings safe to post to the preview.
|
|
5
5
|
*/
|
|
6
|
-
import type
|
|
6
|
+
import { type BlockLike, type FieldLike, type Lookup } from "./reveal-plan.ts";
|
|
7
7
|
export type BlockLabel = {
|
|
8
8
|
/** `labels.singular` of the block config. */
|
|
9
9
|
label?: string;
|
|
10
10
|
/** Field path relative to the block (e.g. "title", "hero.headline") → its label. */
|
|
11
11
|
fields: Record<string, string>;
|
|
12
12
|
};
|
|
13
|
-
/** Block slug → labels. */
|
|
13
|
+
/** Block slug (or, for array rows, row id) → labels. */
|
|
14
14
|
export type BlockLabels = Record<string, BlockLabel>;
|
|
15
|
+
/** More than any page has; keeps the message within what the preview accepts. */
|
|
16
|
+
export declare const MAX_ROW_LABELS = 500;
|
|
15
17
|
export declare function collectBlockLabels(rootFields: FieldLike[], blocksBySlug: Record<string, BlockLike | undefined> | undefined, language: string): BlockLabels;
|
|
18
|
+
/**
|
|
19
|
+
* Labels for array rows, keyed by row id, as Admin's row headers show them: "Item 02".
|
|
20
|
+
* Array rows have no block slug to look labels up by, and their number changes when rows move,
|
|
21
|
+
* so these come from the current form state rather than the config alone.
|
|
22
|
+
*/
|
|
23
|
+
export declare function collectRowLabels(rootFields: FieldLike[], formFields: Record<string, {
|
|
24
|
+
value?: unknown;
|
|
25
|
+
} | undefined>, lookup: Lookup, language: string): BlockLabels;
|
|
16
26
|
/** Client config labels are a string or a `{ [language]: string }` map; anything else has no static text. */
|
|
17
27
|
export declare function labelText(label: unknown, language: string): string | undefined;
|
package/dist/labels.js
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
* the same names ("CTA › Title") instead of guessing from slugs ("C ta › Title").
|
|
4
4
|
* Pure: takes client field config, returns plain strings safe to post to the preview.
|
|
5
5
|
*/
|
|
6
|
+
import { fieldAtPath } from "./reveal-plan.js";
|
|
7
|
+
/** More than any page has; keeps the message within what the preview accepts. */
|
|
8
|
+
export const MAX_ROW_LABELS = 500;
|
|
6
9
|
export function collectBlockLabels(rootFields, blocksBySlug, language) {
|
|
7
10
|
const result = {};
|
|
8
11
|
const visitFields = (fields) => {
|
|
@@ -28,6 +31,34 @@ export function collectBlockLabels(rootFields, blocksBySlug, language) {
|
|
|
28
31
|
visitFields(rootFields);
|
|
29
32
|
return result;
|
|
30
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Labels for array rows, keyed by row id, as Admin's row headers show them: "Item 02".
|
|
36
|
+
* Array rows have no block slug to look labels up by, and their number changes when rows move,
|
|
37
|
+
* so these come from the current form state rather than the config alone.
|
|
38
|
+
*/
|
|
39
|
+
export function collectRowLabels(rootFields, formFields, lookup, language) {
|
|
40
|
+
const result = {};
|
|
41
|
+
for (const [path, formField] of Object.entries(formFields)) {
|
|
42
|
+
const id = formField?.value;
|
|
43
|
+
const match = /^(.+)\.(\d+)\.id$/.exec(path);
|
|
44
|
+
if (typeof id !== "string" || !id || !match) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const [, parentPath = "", index = "0"] = match;
|
|
48
|
+
const field = fieldAtPath(rootFields, parentPath, lookup);
|
|
49
|
+
if (field?.type !== "array") {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const singular = labelText(field.labels?.singular, language) ?? labelText(field.label, language) ?? field.name;
|
|
53
|
+
const fields = {};
|
|
54
|
+
collectFieldLabels(field.fields ?? [], "", language, fields);
|
|
55
|
+
result[id] = { label: `${singular} ${String(Number(index) + 1).padStart(2, "0")}`, fields };
|
|
56
|
+
if (Object.keys(result).length >= MAX_ROW_LABELS) {
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
31
62
|
/** Labels of the fields a block's preview can mark: named fields and paths into named groups and tabs. */
|
|
32
63
|
function collectFieldLabels(fields, prefix, language, into) {
|
|
33
64
|
for (const field of fields) {
|
package/dist/protocol.d.ts
CHANGED
|
@@ -43,17 +43,21 @@ export type FocusMessage = {
|
|
|
43
43
|
}>;
|
|
44
44
|
};
|
|
45
45
|
export declare function isFocusMessage(value: unknown): value is FocusMessage;
|
|
46
|
-
/**
|
|
46
|
+
/**
|
|
47
|
+
* Preview → Admin: send the labels. "ready" when the preview loads; "describe" when it points at
|
|
48
|
+
* an array row, whose number may have changed since.
|
|
49
|
+
*/
|
|
47
50
|
export type ReadyMessage = {
|
|
48
51
|
type: typeof MESSAGE_TYPE;
|
|
49
|
-
action: "ready";
|
|
52
|
+
action: "ready" | "describe";
|
|
50
53
|
};
|
|
51
54
|
export declare function isReadyMessage(value: unknown): value is ReadyMessage;
|
|
52
|
-
/** Admin → preview: labels for the overlay, keyed by
|
|
55
|
+
/** Admin → preview: labels for the overlay, blocks keyed by slug and array rows by row id. */
|
|
53
56
|
export type LabelsMessage = {
|
|
54
57
|
type: typeof MESSAGE_TYPE;
|
|
55
58
|
action: "labels";
|
|
56
59
|
blocks: BlockLabels;
|
|
60
|
+
rows?: BlockLabels;
|
|
57
61
|
};
|
|
58
62
|
export declare function isLabelsMessage(value: unknown): value is LabelsMessage;
|
|
59
63
|
/** Inverse of `fieldDomId`: "field-layout__0__headline" → "layout.0.headline". */
|
package/dist/protocol.js
CHANGED
|
@@ -77,10 +77,11 @@ export function isFocusMessage(value) {
|
|
|
77
77
|
(target.field === undefined || isFieldName(target.field))));
|
|
78
78
|
}
|
|
79
79
|
export function isReadyMessage(value) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
80
|
+
if (!value || typeof value !== "object") {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
const message = value;
|
|
84
|
+
return (message.type === MESSAGE_TYPE && (message.action === "ready" || message.action === "describe"));
|
|
84
85
|
}
|
|
85
86
|
const MAX_LABELED_BLOCKS = 500;
|
|
86
87
|
const MAX_LABELED_FIELDS = 500;
|
|
@@ -90,10 +91,16 @@ export function isLabelsMessage(value) {
|
|
|
90
91
|
return false;
|
|
91
92
|
}
|
|
92
93
|
const message = value;
|
|
93
|
-
|
|
94
|
+
return (message.type === MESSAGE_TYPE &&
|
|
95
|
+
message.action === "labels" &&
|
|
96
|
+
isLabelMap(message.blocks) &&
|
|
97
|
+
(message.rows === undefined || isLabelMap(message.rows)));
|
|
98
|
+
}
|
|
99
|
+
function isLabelMap(value) {
|
|
100
|
+
if (!isRecord(value)) {
|
|
94
101
|
return false;
|
|
95
102
|
}
|
|
96
|
-
const blocks = Object.values(
|
|
103
|
+
const blocks = Object.values(value);
|
|
97
104
|
return (blocks.length <= MAX_LABELED_BLOCKS &&
|
|
98
105
|
blocks.every((block) => {
|
|
99
106
|
if (!isRecord(block) || !isRecord(block.fields)) {
|
package/dist/reveal-plan.d.ts
CHANGED
|
@@ -7,6 +7,10 @@ export type FieldLike = {
|
|
|
7
7
|
type: string;
|
|
8
8
|
name?: string;
|
|
9
9
|
label?: unknown;
|
|
10
|
+
/** Array fields: Payload fills `singular` from the name when the config leaves it out. */
|
|
11
|
+
labels?: {
|
|
12
|
+
singular?: unknown;
|
|
13
|
+
};
|
|
10
14
|
fields?: FieldLike[];
|
|
11
15
|
tabs?: Array<{
|
|
12
16
|
name?: string;
|
|
@@ -35,11 +39,12 @@ export type RevealStep =
|
|
|
35
39
|
parentPath: string;
|
|
36
40
|
index: number;
|
|
37
41
|
};
|
|
38
|
-
type Lookup = {
|
|
42
|
+
export type Lookup = {
|
|
39
43
|
/** Block slug of the row at a form path, e.g. "layout.0" → "hero". */
|
|
40
44
|
blockTypeAt: (rowPath: string) => string | undefined;
|
|
41
45
|
/** Blocks declared once in config and referenced by slug. */
|
|
42
46
|
blocksBySlug?: Record<string, BlockLike | undefined>;
|
|
43
47
|
};
|
|
44
48
|
export declare function planReveal(rootFields: FieldLike[], path: string, lookup: Lookup): RevealStep[] | null;
|
|
45
|
-
|
|
49
|
+
/** The field config a form path ends at, e.g. "layout.2.items" → the `items` array field. */
|
|
50
|
+
export declare function fieldAtPath(rootFields: FieldLike[], path: string, lookup: Lookup): FieldLike | null;
|
package/dist/reveal-plan.js
CHANGED
|
@@ -4,11 +4,19 @@
|
|
|
4
4
|
* Pure: takes client field config, returns steps for the Admin bridge to perform.
|
|
5
5
|
*/
|
|
6
6
|
export function planReveal(rootFields, path, lookup) {
|
|
7
|
+
return walkPath(rootFields, path, lookup)?.steps ?? null;
|
|
8
|
+
}
|
|
9
|
+
/** The field config a form path ends at, e.g. "layout.2.items" → the `items` array field. */
|
|
10
|
+
export function fieldAtPath(rootFields, path, lookup) {
|
|
11
|
+
return walkPath(rootFields, path, lookup)?.field ?? null;
|
|
12
|
+
}
|
|
13
|
+
function walkPath(rootFields, path, lookup) {
|
|
7
14
|
const segments = path.split(".");
|
|
8
15
|
const steps = [];
|
|
9
16
|
let fields = rootFields;
|
|
10
17
|
let counter = { value: 0 };
|
|
11
18
|
let walked = [];
|
|
19
|
+
let last = null;
|
|
12
20
|
for (let i = 0; i < segments.length;) {
|
|
13
21
|
const segment = segments[i] ?? "";
|
|
14
22
|
const found = findIn(fields, segment, counter);
|
|
@@ -25,6 +33,7 @@ export function planReveal(rootFields, path, lookup) {
|
|
|
25
33
|
continue;
|
|
26
34
|
}
|
|
27
35
|
const field = found.field;
|
|
36
|
+
last = field;
|
|
28
37
|
const next = segments[i];
|
|
29
38
|
if ((field.type === "blocks" || field.type === "array") &&
|
|
30
39
|
next !== undefined &&
|
|
@@ -51,9 +60,9 @@ export function planReveal(rootFields, path, lookup) {
|
|
|
51
60
|
continue;
|
|
52
61
|
}
|
|
53
62
|
// A leaf; anything after it (e.g. rich text internals) is not a form field.
|
|
54
|
-
return i === segments.length ? steps : null;
|
|
63
|
+
return i === segments.length ? { steps, field } : null;
|
|
55
64
|
}
|
|
56
|
-
return steps;
|
|
65
|
+
return { steps, field: last };
|
|
57
66
|
}
|
|
58
67
|
function findIn(fields, name, counter) {
|
|
59
68
|
for (const field of fields) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@simmalugnt-se/payload-visual-editing",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Click a block in Payload Live Preview to open its fields in the edit form",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"payload",
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
"node": ">=22"
|
|
19
19
|
},
|
|
20
20
|
"files": [
|
|
21
|
-
"dist"
|
|
21
|
+
"dist",
|
|
22
|
+
"CHANGELOG.md"
|
|
22
23
|
],
|
|
23
24
|
"sideEffects": [
|
|
24
25
|
"*.css"
|