@elytracms/studio 0.0.58 → 0.0.59
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/builder/blocks-model.js +12 -7
- package/dist/components/builder/blocks-model.js.map +1 -1
- package/dist/components/builder/canvas-dnd.d.ts +59 -0
- package/dist/components/builder/canvas-dnd.js +207 -0
- package/dist/components/builder/canvas-dnd.js.map +1 -0
- package/dist/components/builder/canvas-dom.d.ts +11 -0
- package/dist/components/builder/canvas-dom.js +17 -0
- package/dist/components/builder/canvas-dom.js.map +1 -0
- package/dist/components/builder/canvas-inline-editing.d.ts +91 -0
- package/dist/components/builder/canvas-inline-editing.js +242 -0
- package/dist/components/builder/canvas-inline-editing.js.map +1 -0
- package/dist/components/builder/canvas-render-context.d.ts +83 -0
- package/dist/components/builder/canvas-render-context.js +196 -0
- package/dist/components/builder/canvas-render-context.js.map +1 -0
- package/dist/components/builder/canvas.d.ts +4 -1
- package/dist/components/builder/canvas.js +32 -567
- package/dist/components/builder/canvas.js.map +1 -1
- package/dist/components/builder/composition-field-editor.js +14 -4
- package/dist/components/builder/composition-field-editor.js.map +1 -1
- package/dist/components/builder/insert-context.d.ts +5 -2
- package/dist/components/builder/insert-context.js +1 -3
- package/dist/components/builder/insert-context.js.map +1 -1
- package/dist/components/builder/node-factory.d.ts +12 -0
- package/dist/components/builder/node-factory.js +18 -1
- package/dist/components/builder/node-factory.js.map +1 -1
- package/dist/components/cms/asset-field.d.ts +22 -0
- package/dist/components/cms/asset-field.js +300 -0
- package/dist/components/cms/asset-field.js.map +1 -0
- package/dist/components/cms/field-control-lib.d.ts +43 -0
- package/dist/components/cms/field-control-lib.js +98 -0
- package/dist/components/cms/field-control-lib.js.map +1 -0
- package/dist/components/cms/field-control.d.ts +7 -35
- package/dist/components/cms/field-control.js +13 -556
- package/dist/components/cms/field-control.js.map +1 -1
- package/dist/components/cms/object-array-field.d.ts +29 -0
- package/dist/components/cms/object-array-field.js +205 -0
- package/dist/components/cms/object-array-field.js.map +1 -0
- package/dist/components/project-session.d.ts +19 -0
- package/dist/components/project-session.js +29 -2
- package/dist/components/project-session.js.map +1 -1
- package/dist/components/use-hydration-phase.d.ts +6 -4
- package/dist/components/use-hydration-phase.js.map +1 -1
- package/dist/lib/data/collection-content.d.ts +12 -7
- package/dist/lib/data/collection-content.js +6 -4
- package/dist/lib/data/collection-content.js.map +1 -1
- package/dist/lib/operations/session-hydration.d.ts +4 -1
- package/dist/lib/operations/session-hydration.js +4 -1
- package/dist/lib/operations/session-hydration.js.map +1 -1
- package/dist/lib/persistence/index.js +13 -1
- package/dist/lib/persistence/index.js.map +1 -1
- package/dist/lib/workspace/index.d.ts +1 -1
- package/dist/lib/workspace/index.js +1 -1
- package/dist/lib/workspace/index.js.map +1 -1
- package/dist/lib/workspace/workspace-source.d.ts +17 -22
- package/dist/lib/workspace/workspace-source.js +1 -49
- package/dist/lib/workspace/workspace-source.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { getNode, staticProp } from '@elytracms/core/project-graph';
|
|
3
|
+
import { PRESERVE_NODE_ATTR, collectPreservedPairs, richTextDocFromDom, } from '@elytracms/core/rich-text';
|
|
4
|
+
import { createInlineEditSession, createInlineRichTextSession, findInlineTextHost, resolveInlineEditableProp, resolveInlineRichTextProp, } from './inline-edit.js';
|
|
5
|
+
import { wrapperAt } from './canvas-dom.js';
|
|
6
|
+
/**
|
|
7
|
+
* The rendered `.rich-text` element inside a node's wrapper — the rich-text
|
|
8
|
+
* editing host (EC-159). Never crosses into a nested node's wrapper.
|
|
9
|
+
*/
|
|
10
|
+
function findRichTextHost(wrapper) {
|
|
11
|
+
const host = wrapper.querySelector('.rich-text');
|
|
12
|
+
return host && host.closest('[data-ec-node-id]') === wrapper ? host : null;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Wire the inline-editing dblclick listener onto the iframe document (native —
|
|
16
|
+
* React's synthetic events don't cross frames). Returns the detach function;
|
|
17
|
+
* detaching mid-session cancels it (never dispatch from cleanup).
|
|
18
|
+
*/
|
|
19
|
+
export function attachCanvasInlineEditingListeners(doc, deps) {
|
|
20
|
+
const { dispatch } = deps;
|
|
21
|
+
// ---- plain text (EC-158) ------------------------------------------------
|
|
22
|
+
const beginText = (wrapper, host, session) => {
|
|
23
|
+
let done = false;
|
|
24
|
+
const finish = (mode) => {
|
|
25
|
+
if (done)
|
|
26
|
+
return;
|
|
27
|
+
done = true;
|
|
28
|
+
deps.setEditing(null);
|
|
29
|
+
const text = host.textContent ?? '';
|
|
30
|
+
host.removeEventListener('keydown', onKeyDown);
|
|
31
|
+
host.removeEventListener('blur', onBlur);
|
|
32
|
+
host.removeAttribute('contenteditable');
|
|
33
|
+
host.removeAttribute('data-ec-edit');
|
|
34
|
+
wrapper.removeAttribute('data-ec-editing');
|
|
35
|
+
if (mode === 'commit') {
|
|
36
|
+
const outcome = session.commit(text);
|
|
37
|
+
if (outcome) {
|
|
38
|
+
dispatch({
|
|
39
|
+
type: 'setProp',
|
|
40
|
+
id: outcome.nodeId,
|
|
41
|
+
name: outcome.propName,
|
|
42
|
+
value: staticProp(outcome.value),
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
const outcome = session.cancel();
|
|
48
|
+
if (outcome)
|
|
49
|
+
host.textContent = outcome.value;
|
|
50
|
+
}
|
|
51
|
+
doc.getSelection()?.removeAllRanges();
|
|
52
|
+
};
|
|
53
|
+
const onKeyDown = (e) => {
|
|
54
|
+
if (e.key === 'Enter') {
|
|
55
|
+
e.preventDefault();
|
|
56
|
+
e.stopPropagation();
|
|
57
|
+
finish('commit');
|
|
58
|
+
}
|
|
59
|
+
else if (e.key === 'Escape') {
|
|
60
|
+
e.preventDefault();
|
|
61
|
+
e.stopPropagation();
|
|
62
|
+
finish('cancel');
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
const onBlur = () => finish('commit');
|
|
66
|
+
// `plaintext-only` keeps paste/typing plain; fall back where unsupported.
|
|
67
|
+
host.setAttribute('contenteditable', 'plaintext-only');
|
|
68
|
+
if (!host.isContentEditable)
|
|
69
|
+
host.setAttribute('contenteditable', 'true');
|
|
70
|
+
host.setAttribute('data-ec-edit', 'text');
|
|
71
|
+
wrapper.setAttribute('data-ec-editing', '');
|
|
72
|
+
host.addEventListener('keydown', onKeyDown);
|
|
73
|
+
host.addEventListener('blur', onBlur);
|
|
74
|
+
deps.setEditing({ nodeId: session.nodeId, host, finish });
|
|
75
|
+
host.focus({ preventScroll: true });
|
|
76
|
+
// Keep the gesture's native caret/word selection when it landed inside
|
|
77
|
+
// the host; otherwise select the whole value.
|
|
78
|
+
const sel = doc.getSelection();
|
|
79
|
+
if (!sel || sel.rangeCount === 0 || !host.contains(sel.anchorNode)) {
|
|
80
|
+
const range = doc.createRange();
|
|
81
|
+
range.selectNodeContents(host);
|
|
82
|
+
sel?.removeAllRanges();
|
|
83
|
+
sel?.addRange(range);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
// ---- rich text (EC-159) -------------------------------------------------
|
|
87
|
+
const beginRichText = (wrapper, nodeId, target) => {
|
|
88
|
+
const host = findRichTextHost(wrapper);
|
|
89
|
+
if (!host)
|
|
90
|
+
return;
|
|
91
|
+
// Pair the storage tree against the rendered DOM and stamp every
|
|
92
|
+
// non-invertible node (custom blocks, unknown nodes, images, embeds)
|
|
93
|
+
// with its exact JSON: they stay rendered *inside* the live editor as
|
|
94
|
+
// non-editable atoms and survive the session byte-exact. A pairing
|
|
95
|
+
// mismatch means the DOM diverged from the deterministic render —
|
|
96
|
+
// refuse to start rather than risk lossy parsing ("polished or absent").
|
|
97
|
+
const pairs = collectPreservedPairs(target.value.doc, host);
|
|
98
|
+
if (!pairs)
|
|
99
|
+
return;
|
|
100
|
+
const session = createInlineRichTextSession({
|
|
101
|
+
nodeId,
|
|
102
|
+
propName: target.propName,
|
|
103
|
+
original: target.value,
|
|
104
|
+
});
|
|
105
|
+
for (const pair of pairs) {
|
|
106
|
+
const el = pair.element;
|
|
107
|
+
el.setAttribute(PRESERVE_NODE_ATTR, JSON.stringify(pair.node));
|
|
108
|
+
el.setAttribute('contenteditable', 'false');
|
|
109
|
+
}
|
|
110
|
+
let done = false;
|
|
111
|
+
const finish = (mode) => {
|
|
112
|
+
if (done)
|
|
113
|
+
return;
|
|
114
|
+
done = true;
|
|
115
|
+
deps.setEditing(null);
|
|
116
|
+
deps.setRichEditHost(null);
|
|
117
|
+
// Parse the edited DOM, detach the session, then bump the node's
|
|
118
|
+
// remount epoch: React discards the editor-mutated DOM and renders
|
|
119
|
+
// fresh from the graph — committed content or, on cancel, the
|
|
120
|
+
// untouched original. Never patch a fiber tree against DOM the
|
|
121
|
+
// editing commands rewrote.
|
|
122
|
+
const parsed = mode === 'commit' ? richTextDocFromDom(host) : null;
|
|
123
|
+
host.removeEventListener('keydown', onKeyDown);
|
|
124
|
+
host.removeEventListener('blur', onBlur);
|
|
125
|
+
host.removeAttribute('contenteditable');
|
|
126
|
+
host.removeAttribute('data-ec-edit');
|
|
127
|
+
wrapper.removeAttribute('data-ec-editing');
|
|
128
|
+
deps.bumpRichEditEpoch(nodeId);
|
|
129
|
+
if (parsed) {
|
|
130
|
+
// An unchanged document commits nothing: the stored value is never
|
|
131
|
+
// rewritten, so untouched content round-trips byte-identical.
|
|
132
|
+
const outcome = session.commit(parsed.doc);
|
|
133
|
+
if (outcome) {
|
|
134
|
+
dispatch({
|
|
135
|
+
type: 'setProp',
|
|
136
|
+
id: outcome.nodeId,
|
|
137
|
+
name: outcome.propName,
|
|
138
|
+
value: staticProp(outcome.value),
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
session.cancel();
|
|
144
|
+
}
|
|
145
|
+
doc.getSelection()?.removeAllRanges();
|
|
146
|
+
};
|
|
147
|
+
const onKeyDown = (e) => {
|
|
148
|
+
if (e.key === 'Escape') {
|
|
149
|
+
e.preventDefault();
|
|
150
|
+
e.stopPropagation();
|
|
151
|
+
finish('cancel');
|
|
152
|
+
}
|
|
153
|
+
else if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
|
|
154
|
+
e.preventDefault();
|
|
155
|
+
e.stopPropagation();
|
|
156
|
+
finish('commit');
|
|
157
|
+
}
|
|
158
|
+
// Plain Enter falls through: native paragraph insertion (multiline).
|
|
159
|
+
};
|
|
160
|
+
const onBlur = (e) => {
|
|
161
|
+
// Interacting with the floating toolbar is part of the session.
|
|
162
|
+
const next = e.relatedTarget;
|
|
163
|
+
if (next?.closest?.('[data-ec-rt-toolbar]'))
|
|
164
|
+
return;
|
|
165
|
+
finish('commit');
|
|
166
|
+
};
|
|
167
|
+
host.setAttribute('contenteditable', 'true');
|
|
168
|
+
host.setAttribute('data-ec-edit', 'rich');
|
|
169
|
+
wrapper.setAttribute('data-ec-editing', '');
|
|
170
|
+
host.addEventListener('keydown', onKeyDown);
|
|
171
|
+
host.addEventListener('blur', onBlur);
|
|
172
|
+
deps.setEditing({ nodeId, host, finish });
|
|
173
|
+
deps.setRichEditHost(host);
|
|
174
|
+
host.focus({ preventScroll: true });
|
|
175
|
+
};
|
|
176
|
+
const onDblClick = (e) => {
|
|
177
|
+
const wrapper = wrapperAt(e.target);
|
|
178
|
+
if (!wrapper)
|
|
179
|
+
return;
|
|
180
|
+
const nodeId = wrapper.getAttribute('data-ec-node-id');
|
|
181
|
+
const editing = deps.getEditing();
|
|
182
|
+
if (editing) {
|
|
183
|
+
// Double-click inside the live editor is native word selection.
|
|
184
|
+
if (editing.nodeId === nodeId)
|
|
185
|
+
return;
|
|
186
|
+
editing.finish('commit');
|
|
187
|
+
}
|
|
188
|
+
const node = getNode(deps.currentRoot(), nodeId);
|
|
189
|
+
if (!node)
|
|
190
|
+
return;
|
|
191
|
+
const manifest = deps.getManifest(node.componentId);
|
|
192
|
+
const target = resolveInlineEditableProp(manifest, node);
|
|
193
|
+
if (target) {
|
|
194
|
+
const host = findInlineTextHost(wrapper, target.value);
|
|
195
|
+
if (!host || typeof host.focus !== 'function')
|
|
196
|
+
return;
|
|
197
|
+
dispatch({ type: 'select', id: nodeId });
|
|
198
|
+
beginText(wrapper, host, createInlineEditSession({ nodeId, propName: target.propName, original: target.value }));
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const richTarget = resolveInlineRichTextProp(manifest, node);
|
|
202
|
+
if (!richTarget)
|
|
203
|
+
return;
|
|
204
|
+
dispatch({ type: 'select', id: nodeId });
|
|
205
|
+
beginRichText(wrapper, nodeId, richTarget);
|
|
206
|
+
};
|
|
207
|
+
doc.addEventListener('dblclick', onDblClick, true);
|
|
208
|
+
return () => {
|
|
209
|
+
doc.removeEventListener('dblclick', onDblClick, true);
|
|
210
|
+
// Teardown mid-session (doc swap/unmount): cancel — never dispatch from cleanup.
|
|
211
|
+
deps.getEditing()?.finish('cancel');
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* The React face of the canvas inline editing (EC-158/EC-159): owns the
|
|
216
|
+
* session ref and the render state (`richEditHost`, `richEditEpochs`), wires
|
|
217
|
+
* the dblclick listener when the iframe document exists, and cancels a live
|
|
218
|
+
* session on teardown.
|
|
219
|
+
*/
|
|
220
|
+
export function useCanvasInlineEditing(doc, deps) {
|
|
221
|
+
const editingRef = useRef(null);
|
|
222
|
+
const [richEditHost, setRichEditHost] = useState(null);
|
|
223
|
+
const [richEditEpochs, setRichEditEpochs] = useState({});
|
|
224
|
+
const { presentRef, dispatch, studio } = deps;
|
|
225
|
+
useEffect(() => {
|
|
226
|
+
if (!doc)
|
|
227
|
+
return;
|
|
228
|
+
return attachCanvasInlineEditingListeners(doc, {
|
|
229
|
+
currentRoot: () => presentRef.current,
|
|
230
|
+
getManifest: (id) => studio.registry.getManifest(id),
|
|
231
|
+
dispatch,
|
|
232
|
+
getEditing: () => editingRef.current,
|
|
233
|
+
setEditing: (edit) => {
|
|
234
|
+
editingRef.current = edit;
|
|
235
|
+
},
|
|
236
|
+
setRichEditHost,
|
|
237
|
+
bumpRichEditEpoch: (nodeId) => setRichEditEpochs((prev) => ({ ...prev, [nodeId]: (prev[nodeId] ?? 0) + 1 })),
|
|
238
|
+
});
|
|
239
|
+
}, [doc, dispatch, studio, presentRef]);
|
|
240
|
+
return { editingRef, richEditHost, richEditEpochs };
|
|
241
|
+
}
|
|
242
|
+
//# sourceMappingURL=canvas-inline-editing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"canvas-inline-editing.js","sourceRoot":"","sources":["../../../src/components/builder/canvas-inline-editing.ts"],"names":[],"mappings":"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","sourcesContent":["import { useEffect, useRef, useState } from 'react'\nimport type { RefObject } from 'react'\nimport type { ComponentManifest } from '@elytracms/core/component-registry'\nimport { getNode, staticProp } from '@elytracms/core/project-graph'\nimport type { ComponentNode, JsonValue } from '@elytracms/core/project-graph'\nimport {\n PRESERVE_NODE_ATTR,\n collectPreservedPairs,\n richTextDocFromDom,\n} from '@elytracms/core/rich-text'\nimport type { RtDomElement } from '@elytracms/core/rich-text'\nimport type { StudioRegistry } from '@elytracms/studio/lib/registry'\nimport type { BuilderAction } from './builder-store'\nimport {\n createInlineEditSession,\n createInlineRichTextSession,\n findInlineTextHost,\n resolveInlineEditableProp,\n resolveInlineRichTextProp,\n} from './inline-edit'\nimport type { InlineEditSession, InlineRichTextProp } from './inline-edit'\nimport { wrapperAt } from './canvas-dom'\n\n/**\n * Inline editing on the canvas (EC-158 plain text, EC-159 rich text),\n * extracted from `BuilderCanvas` (EC-330): double-click on a node whose\n * manifest flags an inline-editable prop turns the *rendered element itself*\n * contenteditable — no overlay, no input swap, so typography and layout are\n * identical in edit and render states. A session finishes exactly once, and a\n * commit dispatches exactly one `setProp` action — one undo step. The graph\n * stays the single source of truth.\n *\n * Commit/cancel gestures: plain text commits on Enter or blur; rich text is\n * multiline, so plain Enter inserts a paragraph and the commit gesture is\n * Cmd/Ctrl+Enter or blur (Enter-to-commit would make multi-paragraph content\n * unauthorable in place). Esc always cancels, dispatching nothing.\n *\n * The listener wiring is framework-free\n * ({@link attachCanvasInlineEditingListeners}, the `clipboard-actions`\n * pattern) so the begin/commit/cancel state machine is unit-testable without\n * a DOM environment; {@link useCanvasInlineEditing} owns the React state the\n * canvas renders from (the live rich-text host, the remount epochs) and the\n * session ref other canvas listeners consult.\n */\n\n/** A live inline-editing session bound to its DOM host (EC-158/EC-159). */\nexport interface ActiveInlineEdit {\n nodeId: string\n host: HTMLElement\n /** Finish the session exactly once (later calls are no-ops). */\n finish: (mode: 'commit' | 'cancel') => void\n}\n\n/** The structural slice of the iframe document the editing wiring needs. */\nexport interface CanvasInlineEditingDocument {\n addEventListener(type: string, listener: (e: Event) => void, capture?: boolean): void\n removeEventListener(type: string, listener: (e: Event) => void, capture?: boolean): void\n getSelection(): Selection | null\n createRange(): Range\n}\n\nexport interface CanvasInlineEditingDeps {\n /** The current composition root (read live — listeners outlive renders). */\n currentRoot(): ComponentNode\n getManifest(componentId: string): ComponentManifest | undefined\n dispatch(action: BuilderAction): void\n /** The live session — imperative DOM state, mirrored for other listeners. */\n getEditing(): ActiveInlineEdit | null\n setEditing(edit: ActiveInlineEdit | null): void\n /** Mirror the rich-text host into render state (mounts the mark toolbar). */\n setRichEditHost(host: HTMLElement | null): void\n /**\n * A rich-text session mutates the rendered DOM behind React's back\n * (contenteditable + mark commands), so finishing one must REMOUNT the\n * node's subtree — bumping its epoch changes the decorate key, React\n * discards the edited DOM wholesale and re-renders from the graph (the\n * committed value, or the untouched original on cancel). Patching the old\n * fiber tree against editor-mutated DOM would leave stale artifacts.\n */\n bumpRichEditEpoch(nodeId: string): void\n}\n\n/**\n * The rendered `.rich-text` element inside a node's wrapper — the rich-text\n * editing host (EC-159). Never crosses into a nested node's wrapper.\n */\nfunction findRichTextHost(wrapper: HTMLElement): HTMLElement | null {\n const host = wrapper.querySelector('.rich-text') as HTMLElement | null\n return host && host.closest('[data-ec-node-id]') === wrapper ? host : null\n}\n\n/**\n * Wire the inline-editing dblclick listener onto the iframe document (native —\n * React's synthetic events don't cross frames). Returns the detach function;\n * detaching mid-session cancels it (never dispatch from cleanup).\n */\nexport function attachCanvasInlineEditingListeners(\n doc: CanvasInlineEditingDocument,\n deps: CanvasInlineEditingDeps,\n): () => void {\n const { dispatch } = deps\n\n // ---- plain text (EC-158) ------------------------------------------------\n const beginText = (wrapper: HTMLElement, host: HTMLElement, session: InlineEditSession) => {\n let done = false\n const finish = (mode: 'commit' | 'cancel') => {\n if (done) return\n done = true\n deps.setEditing(null)\n const text = host.textContent ?? ''\n host.removeEventListener('keydown', onKeyDown)\n host.removeEventListener('blur', onBlur)\n host.removeAttribute('contenteditable')\n host.removeAttribute('data-ec-edit')\n wrapper.removeAttribute('data-ec-editing')\n if (mode === 'commit') {\n const outcome = session.commit(text)\n if (outcome) {\n dispatch({\n type: 'setProp',\n id: outcome.nodeId,\n name: outcome.propName,\n value: staticProp(outcome.value),\n })\n }\n } else {\n const outcome = session.cancel()\n if (outcome) host.textContent = outcome.value\n }\n doc.getSelection()?.removeAllRanges()\n }\n const onKeyDown = (e: KeyboardEvent) => {\n if (e.key === 'Enter') {\n e.preventDefault()\n e.stopPropagation()\n finish('commit')\n } else if (e.key === 'Escape') {\n e.preventDefault()\n e.stopPropagation()\n finish('cancel')\n }\n }\n const onBlur = () => finish('commit')\n // `plaintext-only` keeps paste/typing plain; fall back where unsupported.\n host.setAttribute('contenteditable', 'plaintext-only')\n if (!host.isContentEditable) host.setAttribute('contenteditable', 'true')\n host.setAttribute('data-ec-edit', 'text')\n wrapper.setAttribute('data-ec-editing', '')\n host.addEventListener('keydown', onKeyDown)\n host.addEventListener('blur', onBlur)\n deps.setEditing({ nodeId: session.nodeId, host, finish })\n host.focus({ preventScroll: true })\n // Keep the gesture's native caret/word selection when it landed inside\n // the host; otherwise select the whole value.\n const sel = doc.getSelection()\n if (!sel || sel.rangeCount === 0 || !host.contains(sel.anchorNode)) {\n const range = doc.createRange()\n range.selectNodeContents(host)\n sel?.removeAllRanges()\n sel?.addRange(range)\n }\n }\n\n // ---- rich text (EC-159) -------------------------------------------------\n const beginRichText = (wrapper: HTMLElement, nodeId: string, target: InlineRichTextProp) => {\n const host = findRichTextHost(wrapper)\n if (!host) return\n // Pair the storage tree against the rendered DOM and stamp every\n // non-invertible node (custom blocks, unknown nodes, images, embeds)\n // with its exact JSON: they stay rendered *inside* the live editor as\n // non-editable atoms and survive the session byte-exact. A pairing\n // mismatch means the DOM diverged from the deterministic render —\n // refuse to start rather than risk lossy parsing (\"polished or absent\").\n const pairs = collectPreservedPairs(target.value.doc, host as unknown as RtDomElement)\n if (!pairs) return\n const session = createInlineRichTextSession({\n nodeId,\n propName: target.propName,\n original: target.value,\n })\n for (const pair of pairs) {\n const el = pair.element as unknown as HTMLElement\n el.setAttribute(PRESERVE_NODE_ATTR, JSON.stringify(pair.node))\n el.setAttribute('contenteditable', 'false')\n }\n let done = false\n const finish = (mode: 'commit' | 'cancel') => {\n if (done) return\n done = true\n deps.setEditing(null)\n deps.setRichEditHost(null)\n // Parse the edited DOM, detach the session, then bump the node's\n // remount epoch: React discards the editor-mutated DOM and renders\n // fresh from the graph — committed content or, on cancel, the\n // untouched original. Never patch a fiber tree against DOM the\n // editing commands rewrote.\n const parsed = mode === 'commit' ? richTextDocFromDom(host as unknown as RtDomElement) : null\n host.removeEventListener('keydown', onKeyDown)\n host.removeEventListener('blur', onBlur)\n host.removeAttribute('contenteditable')\n host.removeAttribute('data-ec-edit')\n wrapper.removeAttribute('data-ec-editing')\n deps.bumpRichEditEpoch(nodeId)\n if (parsed) {\n // An unchanged document commits nothing: the stored value is never\n // rewritten, so untouched content round-trips byte-identical.\n const outcome = session.commit(parsed.doc)\n if (outcome) {\n dispatch({\n type: 'setProp',\n id: outcome.nodeId,\n name: outcome.propName,\n value: staticProp(outcome.value as unknown as JsonValue),\n })\n }\n } else {\n session.cancel()\n }\n doc.getSelection()?.removeAllRanges()\n }\n const onKeyDown = (e: KeyboardEvent) => {\n if (e.key === 'Escape') {\n e.preventDefault()\n e.stopPropagation()\n finish('cancel')\n } else if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {\n e.preventDefault()\n e.stopPropagation()\n finish('commit')\n }\n // Plain Enter falls through: native paragraph insertion (multiline).\n }\n const onBlur = (e: FocusEvent) => {\n // Interacting with the floating toolbar is part of the session.\n const next = e.relatedTarget as HTMLElement | null\n if (next?.closest?.('[data-ec-rt-toolbar]')) return\n finish('commit')\n }\n host.setAttribute('contenteditable', 'true')\n host.setAttribute('data-ec-edit', 'rich')\n wrapper.setAttribute('data-ec-editing', '')\n host.addEventListener('keydown', onKeyDown)\n host.addEventListener('blur', onBlur)\n deps.setEditing({ nodeId, host, finish })\n deps.setRichEditHost(host)\n host.focus({ preventScroll: true })\n }\n\n const onDblClick = (e: Event) => {\n const wrapper = wrapperAt(e.target)\n if (!wrapper) return\n const nodeId = wrapper.getAttribute('data-ec-node-id')!\n const editing = deps.getEditing()\n if (editing) {\n // Double-click inside the live editor is native word selection.\n if (editing.nodeId === nodeId) return\n editing.finish('commit')\n }\n const node = getNode(deps.currentRoot(), nodeId)\n if (!node) return\n const manifest = deps.getManifest(node.componentId)\n const target = resolveInlineEditableProp(manifest, node)\n if (target) {\n const host = findInlineTextHost(wrapper, target.value) as HTMLElement | null\n if (!host || typeof host.focus !== 'function') return\n dispatch({ type: 'select', id: nodeId })\n beginText(\n wrapper,\n host,\n createInlineEditSession({ nodeId, propName: target.propName, original: target.value }),\n )\n return\n }\n const richTarget = resolveInlineRichTextProp(manifest, node)\n if (!richTarget) return\n dispatch({ type: 'select', id: nodeId })\n beginRichText(wrapper, nodeId, richTarget)\n }\n\n doc.addEventListener('dblclick', onDblClick, true)\n return () => {\n doc.removeEventListener('dblclick', onDblClick, true)\n // Teardown mid-session (doc swap/unmount): cancel — never dispatch from cleanup.\n deps.getEditing()?.finish('cancel')\n }\n}\n\n/** What `useCanvasInlineEditing` hands back to the canvas. */\nexport interface CanvasInlineEditing {\n /**\n * The active session — imperative DOM state inside the iframe, not render\n * state. Other canvas listeners (selection clicks, DnD arming, clipboard\n * blocking) consult it live through this ref.\n */\n editingRef: RefObject<ActiveInlineEdit | null>\n /** The live rich-text host, mirrored into render state (mark toolbar mount). */\n richEditHost: HTMLElement | null\n /** Per-node remount epochs — decorate keys change when a rich session ends. */\n richEditEpochs: Record<string, number>\n}\n\n/**\n * The React face of the canvas inline editing (EC-158/EC-159): owns the\n * session ref and the render state (`richEditHost`, `richEditEpochs`), wires\n * the dblclick listener when the iframe document exists, and cancels a live\n * session on teardown.\n */\nexport function useCanvasInlineEditing(\n doc: Document | null,\n deps: {\n /** The live composition root (`builder.present`, mirrored into a ref). */\n presentRef: RefObject<ComponentNode>\n dispatch: (action: BuilderAction) => void\n studio: StudioRegistry\n },\n): CanvasInlineEditing {\n const editingRef = useRef<ActiveInlineEdit | null>(null)\n const [richEditHost, setRichEditHost] = useState<HTMLElement | null>(null)\n const [richEditEpochs, setRichEditEpochs] = useState<Record<string, number>>({})\n const { presentRef, dispatch, studio } = deps\n\n useEffect(() => {\n if (!doc) return\n return attachCanvasInlineEditingListeners(doc, {\n currentRoot: () => presentRef.current,\n getManifest: (id) => studio.registry.getManifest(id),\n dispatch,\n getEditing: () => editingRef.current,\n setEditing: (edit) => {\n editingRef.current = edit\n },\n setRichEditHost,\n bumpRichEditEpoch: (nodeId) =>\n setRichEditEpochs((prev) => ({ ...prev, [nodeId]: (prev[nodeId] ?? 0) + 1 })),\n })\n }, [doc, dispatch, studio, presentRef])\n\n return { editingRef, richEditHost, richEditEpochs }\n}\n"]}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { RenderContext, ResolveAsset, ResolveBinding, ResolveLink, ResolveListing, ResolveRelation } from '@elytracms/core/runtime-renderer';
|
|
2
|
+
import type { BindingReference, ComponentNode } from '@elytracms/core/project-graph';
|
|
3
|
+
import type { StudioRegistry } from '@elytracms/studio/lib/registry';
|
|
4
|
+
import type { CanvasPerspective } from '@elytracms/studio/lib/data';
|
|
5
|
+
import type { ReuseCapability } from './reuse.js';
|
|
6
|
+
/**
|
|
7
|
+
* The canvas's `RenderContext` (EC-330, extracted from `BuilderCanvas`): the
|
|
8
|
+
* seven injected resolvers that make the iframe preview match delivery —
|
|
9
|
+
* bindings, assets (EC-195), relations (EC-254), listings (EC-255), rich-text
|
|
10
|
+
* links (EC-291) — plus the per-node decorator that stamps the editable
|
|
11
|
+
* wrappers (selection/hover state, locale-fallback chips EC-176, rich-text
|
|
12
|
+
* remount epochs, and the EC-300 unresolved-reusable placeholder).
|
|
13
|
+
*
|
|
14
|
+
* The context assembly is a pure factory ({@link createCanvasRenderContext})
|
|
15
|
+
* over pure decision helpers ({@link unresolvedReuseStatus},
|
|
16
|
+
* {@link firstBindingFallbackLocale}), so the decoration and resolver wiring
|
|
17
|
+
* are unit-testable without providers; {@link useCanvasRenderContext} binds
|
|
18
|
+
* them to the live stores.
|
|
19
|
+
*/
|
|
20
|
+
/** Collect every node id under a root (the page subtree). */
|
|
21
|
+
export declare function collectIds(node: ComponentNode, out?: Set<string>): Set<string>;
|
|
22
|
+
/**
|
|
23
|
+
* EC-300/AD-16: a transcluding embed whose target doesn't resolve in the active
|
|
24
|
+
* perspective renders a placeholder, never an empty section. `unpublished` = the
|
|
25
|
+
* target exists as a draft but isn't live (Published view of a draft-only reusable);
|
|
26
|
+
* `missing` = no resolvable target at all (deleted). `null` = resolves fine, or the
|
|
27
|
+
* node isn't an embed / has no target picked yet (its own empty state owns that).
|
|
28
|
+
*/
|
|
29
|
+
export declare function unresolvedReuseStatus(node: ComponentNode, capability: ReuseCapability | null | undefined, resolveAt: (ref: {
|
|
30
|
+
collection: string;
|
|
31
|
+
id: string;
|
|
32
|
+
}, perspective: CanvasPerspective) => unknown, perspective: CanvasPerspective): 'unpublished' | 'missing' | null;
|
|
33
|
+
/**
|
|
34
|
+
* Locale-fallback chips (EC-176): the locale the node's first fallback-served
|
|
35
|
+
* bound prop actually came from, or undefined when every bound value is a real
|
|
36
|
+
* translation (or nothing is bound). Pure over an injected per-binding decision
|
|
37
|
+
* (the hook wires `cmsBindingFallback` — the same decision function as the
|
|
38
|
+
* inspector's Content tab, so chip and label can never disagree).
|
|
39
|
+
*/
|
|
40
|
+
export declare function firstBindingFallbackLocale(node: ComponentNode, fallbackOf: (binding: BindingReference) => {
|
|
41
|
+
sourceLocale: string;
|
|
42
|
+
} | null): string | undefined;
|
|
43
|
+
/** Everything the pure context factory needs — resolvers plus decoration state. */
|
|
44
|
+
export interface CanvasRenderContextDeps {
|
|
45
|
+
registry: RenderContext['registry'];
|
|
46
|
+
implementations: RenderContext['implementations'];
|
|
47
|
+
resolveBinding: ResolveBinding;
|
|
48
|
+
resolveAsset: ResolveAsset;
|
|
49
|
+
resolveRelation: ResolveRelation;
|
|
50
|
+
resolveListing: ResolveListing;
|
|
51
|
+
resolveLink: ResolveLink;
|
|
52
|
+
/** Only page nodes are editable; layout chrome must not be decorated. */
|
|
53
|
+
pageNodeIds: ReadonlySet<string>;
|
|
54
|
+
selectedId: string | null;
|
|
55
|
+
hoveredId: string | null;
|
|
56
|
+
/** Per-node remount epochs from the inline rich-text editing (EC-159). */
|
|
57
|
+
richEditEpochs: Record<string, number>;
|
|
58
|
+
unresolvedReuse(node: ComponentNode): 'unpublished' | 'missing' | null;
|
|
59
|
+
localeFallbackOf(node: ComponentNode): string | undefined;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Assemble the canvas `RenderContext`. Pure (no hooks): the same deps produce
|
|
63
|
+
* the same context, so the decorator's wrapper markup is pinnable in tests.
|
|
64
|
+
*/
|
|
65
|
+
export declare function createCanvasRenderContext(deps: CanvasRenderContextDeps): RenderContext;
|
|
66
|
+
/**
|
|
67
|
+
* The live canvas render context: binds the pure factory to the studio's
|
|
68
|
+
* stores (data sources, asset library, active locale, reuse capability) for
|
|
69
|
+
* the given perspective. Returns the context plus the page-node id set the
|
|
70
|
+
* canvas also needs for spacing-handle gating.
|
|
71
|
+
*/
|
|
72
|
+
export declare function useCanvasRenderContext(args: {
|
|
73
|
+
/** The edited composition root (`builder.present`). */
|
|
74
|
+
present: ComponentNode;
|
|
75
|
+
selectedId: string | null;
|
|
76
|
+
hoveredId: string | null;
|
|
77
|
+
studio: StudioRegistry;
|
|
78
|
+
perspective: CanvasPerspective;
|
|
79
|
+
richEditEpochs: Record<string, number>;
|
|
80
|
+
}): {
|
|
81
|
+
ctx: RenderContext;
|
|
82
|
+
pageNodeIds: ReadonlySet<string>;
|
|
83
|
+
};
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useMemo } from 'react';
|
|
3
|
+
import { translate } from '@elytracms/studio/lib/i18n';
|
|
4
|
+
import { resolveCanvasLink, resolveCanvasListing, resolveCanvasRelation, useData, } from '@elytracms/studio/lib/data';
|
|
5
|
+
import { cmsBindingFallback, useStudioLocale } from '@elytracms/studio/lib/locale';
|
|
6
|
+
import { useOptionalAssets } from '../assets/assets-store.js';
|
|
7
|
+
import { useReuse } from './reuse-context.js';
|
|
8
|
+
import { reuseEmbedRef } from './reuse.js';
|
|
9
|
+
/**
|
|
10
|
+
* The canvas's `RenderContext` (EC-330, extracted from `BuilderCanvas`): the
|
|
11
|
+
* seven injected resolvers that make the iframe preview match delivery —
|
|
12
|
+
* bindings, assets (EC-195), relations (EC-254), listings (EC-255), rich-text
|
|
13
|
+
* links (EC-291) — plus the per-node decorator that stamps the editable
|
|
14
|
+
* wrappers (selection/hover state, locale-fallback chips EC-176, rich-text
|
|
15
|
+
* remount epochs, and the EC-300 unresolved-reusable placeholder).
|
|
16
|
+
*
|
|
17
|
+
* The context assembly is a pure factory ({@link createCanvasRenderContext})
|
|
18
|
+
* over pure decision helpers ({@link unresolvedReuseStatus},
|
|
19
|
+
* {@link firstBindingFallbackLocale}), so the decoration and resolver wiring
|
|
20
|
+
* are unit-testable without providers; {@link useCanvasRenderContext} binds
|
|
21
|
+
* them to the live stores.
|
|
22
|
+
*/
|
|
23
|
+
/** Collect every node id under a root (the page subtree). */
|
|
24
|
+
export function collectIds(node, out = new Set()) {
|
|
25
|
+
out.add(node.id);
|
|
26
|
+
for (const children of Object.values(node.slots ?? {})) {
|
|
27
|
+
for (const child of children)
|
|
28
|
+
collectIds(child, out);
|
|
29
|
+
}
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* EC-300/AD-16: a transcluding embed whose target doesn't resolve in the active
|
|
34
|
+
* perspective renders a placeholder, never an empty section. `unpublished` = the
|
|
35
|
+
* target exists as a draft but isn't live (Published view of a draft-only reusable);
|
|
36
|
+
* `missing` = no resolvable target at all (deleted). `null` = resolves fine, or the
|
|
37
|
+
* node isn't an embed / has no target picked yet (its own empty state owns that).
|
|
38
|
+
*/
|
|
39
|
+
export function unresolvedReuseStatus(node, capability, resolveAt, perspective) {
|
|
40
|
+
if (!capability)
|
|
41
|
+
return null;
|
|
42
|
+
const ref = reuseEmbedRef(node, capability);
|
|
43
|
+
if (!ref)
|
|
44
|
+
return null;
|
|
45
|
+
if (resolveAt(ref, perspective))
|
|
46
|
+
return null;
|
|
47
|
+
return resolveAt(ref, 'draft') ? 'unpublished' : 'missing';
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Locale-fallback chips (EC-176): the locale the node's first fallback-served
|
|
51
|
+
* bound prop actually came from, or undefined when every bound value is a real
|
|
52
|
+
* translation (or nothing is bound). Pure over an injected per-binding decision
|
|
53
|
+
* (the hook wires `cmsBindingFallback` — the same decision function as the
|
|
54
|
+
* inspector's Content tab, so chip and label can never disagree).
|
|
55
|
+
*/
|
|
56
|
+
export function firstBindingFallbackLocale(node, fallbackOf) {
|
|
57
|
+
for (const value of Object.values(node.props ?? {})) {
|
|
58
|
+
if (value.kind !== 'binding')
|
|
59
|
+
continue;
|
|
60
|
+
const fallback = fallbackOf(value.binding);
|
|
61
|
+
if (fallback)
|
|
62
|
+
return fallback.sourceLocale;
|
|
63
|
+
}
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Assemble the canvas `RenderContext`. Pure (no hooks): the same deps produce
|
|
68
|
+
* the same context, so the decorator's wrapper markup is pinnable in tests.
|
|
69
|
+
*/
|
|
70
|
+
export function createCanvasRenderContext(deps) {
|
|
71
|
+
return {
|
|
72
|
+
registry: deps.registry,
|
|
73
|
+
implementations: deps.implementations,
|
|
74
|
+
resolveBinding: deps.resolveBinding,
|
|
75
|
+
resolveAsset: deps.resolveAsset,
|
|
76
|
+
resolveRelation: deps.resolveRelation,
|
|
77
|
+
resolveListing: deps.resolveListing,
|
|
78
|
+
resolveLink: deps.resolveLink,
|
|
79
|
+
decorateNode: (node, rendered) => {
|
|
80
|
+
if (!deps.pageNodeIds.has(node.id))
|
|
81
|
+
return rendered;
|
|
82
|
+
// An unresolved hard reference shows the placeholder in place of the empty
|
|
83
|
+
// transclusion; the node stays selectable, so its inspector relation chip
|
|
84
|
+
// (EC-296) deep-links to the reusable to edit/publish it.
|
|
85
|
+
const unresolved = deps.unresolvedReuse(node);
|
|
86
|
+
return (_jsx("div", { "data-ec-node-id": node.id, className: "ec-editable", "data-selected": node.id === deps.selectedId || undefined, "data-hovered": node.id === deps.hoveredId || undefined, "data-ec-locale-fallback": deps.localeFallbackOf(node), children: unresolved ? _jsx(UnresolvedReusablePlaceholder, { status: unresolved }) : rendered }, `${node.id}#${deps.richEditEpochs[node.id] ?? 0}`));
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* The live canvas render context: binds the pure factory to the studio's
|
|
92
|
+
* stores (data sources, asset library, active locale, reuse capability) for
|
|
93
|
+
* the given perspective. Returns the context plus the page-node id set the
|
|
94
|
+
* canvas also needs for spacing-handle gating.
|
|
95
|
+
*/
|
|
96
|
+
export function useCanvasRenderContext(args) {
|
|
97
|
+
const { present, selectedId, hoveredId, studio, perspective, richEditEpochs } = args;
|
|
98
|
+
const data = useData();
|
|
99
|
+
// Only page nodes are editable; layout chrome (rendered around the page) is
|
|
100
|
+
// read-only context and must not be selectable (it isn't in the page tree).
|
|
101
|
+
const pageNodeIds = useMemo(() => collectIds(present), [present]);
|
|
102
|
+
// Bridge the renderer's untyped ItemContext (item: unknown) to the binding
|
|
103
|
+
// layer's RepeaterItemContext (item: JsonValue). The renderer feeds back values
|
|
104
|
+
// it resolved from JSON payloads, so the item is a JsonValue.
|
|
105
|
+
const resolveBinding = useCallback((ref, item) => data.resolveValue(ref, item ? { item: item.item, index: item.index } : undefined), [data]);
|
|
106
|
+
// EC-195: resolve an `asset`-typed Image prop (an asset id) to its url +
|
|
107
|
+
// intrinsic dimensions from the live asset library, so the canvas previews
|
|
108
|
+
// exactly what the host renders. A record without a resolved url (e.g. a
|
|
109
|
+
// never-served seed) → null → the implementation's visible empty state.
|
|
110
|
+
const assetsStore = useOptionalAssets();
|
|
111
|
+
const resolveAsset = useCallback((assetId) => {
|
|
112
|
+
const record = assetsStore?.assets.find((a) => a.id === assetId);
|
|
113
|
+
if (!record?.url)
|
|
114
|
+
return null;
|
|
115
|
+
return {
|
|
116
|
+
url: record.url,
|
|
117
|
+
width: record.width ?? null,
|
|
118
|
+
height: record.height ?? null,
|
|
119
|
+
alt: record.alt ?? null,
|
|
120
|
+
focalPoint: record.focalPoint ?? null,
|
|
121
|
+
};
|
|
122
|
+
}, [assetsStore]);
|
|
123
|
+
// Locale-fallback chips (EC-176): a node whose bound content currently
|
|
124
|
+
// previews via EC-018 locale fallback wears a corner chip naming the locale
|
|
125
|
+
// the value actually came from. Same decision function as the inspector's
|
|
126
|
+
// Content tab (`cmsBindingFallback`), so chip and label can never disagree.
|
|
127
|
+
const { locale: activeLocale } = useStudioLocale();
|
|
128
|
+
// EC-300: the reuse capability identifies a transcluding embed + its relation prop,
|
|
129
|
+
// so the canvas can detect a HARD reference whose target doesn't resolve and show a
|
|
130
|
+
// placeholder instead of an empty section. Null-safe (no provider / no capability).
|
|
131
|
+
const reuse = useReuse();
|
|
132
|
+
// EC-254: populate a block's `relation` prop (the editor's document pick — a
|
|
133
|
+
// ProductGrid's products, a Reusable's element) to delivery shape from the
|
|
134
|
+
// active content session, in the canvas's active locale, so the canvas previews
|
|
135
|
+
// picked documents exactly as the host renders them. The studio analog of the
|
|
136
|
+
// host's baked relations map. A missing/invisible target → null → the block's
|
|
137
|
+
// own empty state.
|
|
138
|
+
const resolveRelation = useCallback((ref) => resolveCanvasRelation(ref, activeLocale, perspective), [activeLocale, perspective]);
|
|
139
|
+
// EC-255: resolve a listing block's query (a ProductGrid keyed to a category) to
|
|
140
|
+
// its documents from the active content session, in the canvas's active locale, so
|
|
141
|
+
// the canvas previews a dynamic grid exactly as the host renders it. The studio
|
|
142
|
+
// analog of the host's baked listings map; an empty/invalid query → [] → the
|
|
143
|
+
// block's own empty state.
|
|
144
|
+
const resolveListing = useCallback((query) => resolveCanvasListing(query, activeLocale, perspective), [activeLocale, perspective]);
|
|
145
|
+
// EC-291: resolve a rich-text internal link (`{type:'reference'}`) to its target's
|
|
146
|
+
// URL path from the active content session, in the canvas's active locale — the
|
|
147
|
+
// studio analog of the host's baked links map. A missing/unpublished/unroutable
|
|
148
|
+
// target → null → the rich-text renderer shows the visible `unresolved-ref` issue.
|
|
149
|
+
const resolveLink = useCallback((ref) => resolveCanvasLink(ref, activeLocale, perspective), [activeLocale, perspective]);
|
|
150
|
+
const unresolvedReuse = useCallback((node) => unresolvedReuseStatus(node, reuse?.capability, (ref, p) => resolveCanvasRelation(ref, activeLocale, p), perspective), [reuse, activeLocale, perspective]);
|
|
151
|
+
const localeFallbackOf = useCallback((node) => firstBindingFallbackLocale(node, (binding) => cmsBindingFallback(binding, data.getSource(binding.sourceId), activeLocale)), [data, activeLocale]);
|
|
152
|
+
const ctx = useMemo(() => createCanvasRenderContext({
|
|
153
|
+
registry: studio.registry,
|
|
154
|
+
implementations: studio.implementations,
|
|
155
|
+
resolveBinding,
|
|
156
|
+
resolveAsset,
|
|
157
|
+
resolveRelation,
|
|
158
|
+
resolveListing,
|
|
159
|
+
resolveLink,
|
|
160
|
+
pageNodeIds,
|
|
161
|
+
selectedId,
|
|
162
|
+
hoveredId,
|
|
163
|
+
richEditEpochs,
|
|
164
|
+
unresolvedReuse,
|
|
165
|
+
localeFallbackOf,
|
|
166
|
+
}), [studio, resolveBinding, resolveAsset, resolveRelation, resolveListing, resolveLink, unresolvedReuse, pageNodeIds, selectedId, hoveredId, localeFallbackOf, richEditEpochs]);
|
|
167
|
+
return { ctx, pageNodeIds };
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* In-canvas placeholder for a transcluding embed whose target doesn't resolve
|
|
171
|
+
* (EC-300/AD-16) — shown in place of the empty section so a hard reference never
|
|
172
|
+
* silently vanishes. Rendered INTO the iframe (portal), which carries only the
|
|
173
|
+
* project's stylesheet, so it uses inline styles rather than studio UI classes.
|
|
174
|
+
* The node stays selectable; the inspector's `element` relation chip (EC-296)
|
|
175
|
+
* deep-links to the reusable to edit or publish it.
|
|
176
|
+
*/
|
|
177
|
+
function UnresolvedReusablePlaceholder({ status }) {
|
|
178
|
+
const title = status === 'unpublished'
|
|
179
|
+
? translate('builder.canvas.reusableUnpublishedTitle')
|
|
180
|
+
: translate('builder.canvas.reusableMissingTitle');
|
|
181
|
+
const hint = status === 'unpublished'
|
|
182
|
+
? translate('builder.canvas.reusableUnpublishedHint')
|
|
183
|
+
: translate('builder.canvas.reusableMissingHint');
|
|
184
|
+
return (_jsxs("div", { style: {
|
|
185
|
+
boxSizing: 'border-box',
|
|
186
|
+
border: '1px dashed rgba(120,120,120,0.5)',
|
|
187
|
+
borderRadius: 8,
|
|
188
|
+
padding: '24px 28px',
|
|
189
|
+
margin: '12px 16px',
|
|
190
|
+
background: 'rgba(127,127,127,0.06)',
|
|
191
|
+
color: 'rgba(90,90,90,0.95)',
|
|
192
|
+
font: '14px/1.5 system-ui, -apple-system, sans-serif',
|
|
193
|
+
textAlign: 'center',
|
|
194
|
+
}, children: [_jsxs("div", { style: { fontWeight: 600 }, children: ["\u26A0 ", title] }), _jsx("div", { style: { marginTop: 6, fontSize: 13, opacity: 0.85 }, children: hint })] }));
|
|
195
|
+
}
|
|
196
|
+
//# sourceMappingURL=canvas-render-context.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"canvas-render-context.js","sourceRoot":"","sources":["../../../src/components/builder/canvas-render-context.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useMemo } from 'react'\nimport type {\n RenderContext,\n ResolveAsset,\n ResolveBinding,\n ResolveLink,\n ResolveListing,\n ResolveRelation,\n} from '@elytracms/core/runtime-renderer'\nimport type { BindingReference, ComponentNode, JsonValue } from '@elytracms/core/project-graph'\nimport type { StudioRegistry } from '@elytracms/studio/lib/registry'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport {\n resolveCanvasLink,\n resolveCanvasListing,\n resolveCanvasRelation,\n useData,\n} from '@elytracms/studio/lib/data'\nimport type { CanvasPerspective } from '@elytracms/studio/lib/data'\nimport { cmsBindingFallback, useStudioLocale } from '@elytracms/studio/lib/locale'\nimport { useOptionalAssets } from '../assets/assets-store'\nimport { useReuse } from './reuse-context'\nimport { reuseEmbedRef } from './reuse'\nimport type { ReuseCapability } from './reuse'\n\n/**\n * The canvas's `RenderContext` (EC-330, extracted from `BuilderCanvas`): the\n * seven injected resolvers that make the iframe preview match delivery —\n * bindings, assets (EC-195), relations (EC-254), listings (EC-255), rich-text\n * links (EC-291) — plus the per-node decorator that stamps the editable\n * wrappers (selection/hover state, locale-fallback chips EC-176, rich-text\n * remount epochs, and the EC-300 unresolved-reusable placeholder).\n *\n * The context assembly is a pure factory ({@link createCanvasRenderContext})\n * over pure decision helpers ({@link unresolvedReuseStatus},\n * {@link firstBindingFallbackLocale}), so the decoration and resolver wiring\n * are unit-testable without providers; {@link useCanvasRenderContext} binds\n * them to the live stores.\n */\n\n/** Collect every node id under a root (the page subtree). */\nexport function collectIds(node: ComponentNode, out: Set<string> = new Set()): Set<string> {\n out.add(node.id)\n for (const children of Object.values(node.slots ?? {})) {\n for (const child of children) collectIds(child, out)\n }\n return out\n}\n\n/**\n * EC-300/AD-16: a transcluding embed whose target doesn't resolve in the active\n * perspective renders a placeholder, never an empty section. `unpublished` = the\n * target exists as a draft but isn't live (Published view of a draft-only reusable);\n * `missing` = no resolvable target at all (deleted). `null` = resolves fine, or the\n * node isn't an embed / has no target picked yet (its own empty state owns that).\n */\nexport function unresolvedReuseStatus(\n node: ComponentNode,\n capability: ReuseCapability | null | undefined,\n resolveAt: (ref: { collection: string; id: string }, perspective: CanvasPerspective) => unknown,\n perspective: CanvasPerspective,\n): 'unpublished' | 'missing' | null {\n if (!capability) return null\n const ref = reuseEmbedRef(node, capability)\n if (!ref) return null\n if (resolveAt(ref, perspective)) return null\n return resolveAt(ref, 'draft') ? 'unpublished' : 'missing'\n}\n\n/**\n * Locale-fallback chips (EC-176): the locale the node's first fallback-served\n * bound prop actually came from, or undefined when every bound value is a real\n * translation (or nothing is bound). Pure over an injected per-binding decision\n * (the hook wires `cmsBindingFallback` — the same decision function as the\n * inspector's Content tab, so chip and label can never disagree).\n */\nexport function firstBindingFallbackLocale(\n node: ComponentNode,\n fallbackOf: (binding: BindingReference) => { sourceLocale: string } | null,\n): string | undefined {\n for (const value of Object.values(node.props ?? {})) {\n if (value.kind !== 'binding') continue\n const fallback = fallbackOf(value.binding)\n if (fallback) return fallback.sourceLocale\n }\n return undefined\n}\n\n/** Everything the pure context factory needs — resolvers plus decoration state. */\nexport interface CanvasRenderContextDeps {\n registry: RenderContext['registry']\n implementations: RenderContext['implementations']\n resolveBinding: ResolveBinding\n resolveAsset: ResolveAsset\n resolveRelation: ResolveRelation\n resolveListing: ResolveListing\n resolveLink: ResolveLink\n /** Only page nodes are editable; layout chrome must not be decorated. */\n pageNodeIds: ReadonlySet<string>\n selectedId: string | null\n hoveredId: string | null\n /** Per-node remount epochs from the inline rich-text editing (EC-159). */\n richEditEpochs: Record<string, number>\n unresolvedReuse(node: ComponentNode): 'unpublished' | 'missing' | null\n localeFallbackOf(node: ComponentNode): string | undefined\n}\n\n/**\n * Assemble the canvas `RenderContext`. Pure (no hooks): the same deps produce\n * the same context, so the decorator's wrapper markup is pinnable in tests.\n */\nexport function createCanvasRenderContext(deps: CanvasRenderContextDeps): RenderContext {\n return {\n registry: deps.registry,\n implementations: deps.implementations,\n resolveBinding: deps.resolveBinding,\n resolveAsset: deps.resolveAsset,\n resolveRelation: deps.resolveRelation,\n resolveListing: deps.resolveListing,\n resolveLink: deps.resolveLink,\n decorateNode: (node, rendered) => {\n if (!deps.pageNodeIds.has(node.id)) return rendered\n // An unresolved hard reference shows the placeholder in place of the empty\n // transclusion; the node stays selectable, so its inspector relation chip\n // (EC-296) deep-links to the reusable to edit/publish it.\n const unresolved = deps.unresolvedReuse(node)\n return (\n <div\n // The epoch suffix forces a remount after a rich-text editing\n // session mutated this node's rendered DOM (see richEditEpochs).\n key={`${node.id}#${deps.richEditEpochs[node.id] ?? 0}`}\n data-ec-node-id={node.id}\n className=\"ec-editable\"\n data-selected={node.id === deps.selectedId || undefined}\n data-hovered={node.id === deps.hoveredId || undefined}\n data-ec-locale-fallback={deps.localeFallbackOf(node)}\n >\n {unresolved ? <UnresolvedReusablePlaceholder status={unresolved} /> : rendered}\n </div>\n )\n },\n }\n}\n\n/**\n * The live canvas render context: binds the pure factory to the studio's\n * stores (data sources, asset library, active locale, reuse capability) for\n * the given perspective. Returns the context plus the page-node id set the\n * canvas also needs for spacing-handle gating.\n */\nexport function useCanvasRenderContext(args: {\n /** The edited composition root (`builder.present`). */\n present: ComponentNode\n selectedId: string | null\n hoveredId: string | null\n studio: StudioRegistry\n perspective: CanvasPerspective\n richEditEpochs: Record<string, number>\n}): { ctx: RenderContext; pageNodeIds: ReadonlySet<string> } {\n const { present, selectedId, hoveredId, studio, perspective, richEditEpochs } = args\n const data = useData()\n\n // Only page nodes are editable; layout chrome (rendered around the page) is\n // read-only context and must not be selectable (it isn't in the page tree).\n const pageNodeIds = useMemo(() => collectIds(present), [present])\n\n // Bridge the renderer's untyped ItemContext (item: unknown) to the binding\n // layer's RepeaterItemContext (item: JsonValue). The renderer feeds back values\n // it resolved from JSON payloads, so the item is a JsonValue.\n const resolveBinding: ResolveBinding = useCallback(\n (ref, item) =>\n data.resolveValue(ref, item ? { item: item.item as JsonValue, index: item.index } : undefined),\n [data],\n )\n\n // EC-195: resolve an `asset`-typed Image prop (an asset id) to its url +\n // intrinsic dimensions from the live asset library, so the canvas previews\n // exactly what the host renders. A record without a resolved url (e.g. a\n // never-served seed) → null → the implementation's visible empty state.\n const assetsStore = useOptionalAssets()\n const resolveAsset: ResolveAsset = useCallback(\n (assetId) => {\n const record = assetsStore?.assets.find((a) => a.id === assetId)\n if (!record?.url) return null\n return {\n url: record.url,\n width: record.width ?? null,\n height: record.height ?? null,\n alt: record.alt ?? null,\n focalPoint: record.focalPoint ?? null,\n }\n },\n [assetsStore],\n )\n\n // Locale-fallback chips (EC-176): a node whose bound content currently\n // previews via EC-018 locale fallback wears a corner chip naming the locale\n // the value actually came from. Same decision function as the inspector's\n // Content tab (`cmsBindingFallback`), so chip and label can never disagree.\n const { locale: activeLocale } = useStudioLocale()\n // EC-300: the reuse capability identifies a transcluding embed + its relation prop,\n // so the canvas can detect a HARD reference whose target doesn't resolve and show a\n // placeholder instead of an empty section. Null-safe (no provider / no capability).\n const reuse = useReuse()\n\n // EC-254: populate a block's `relation` prop (the editor's document pick — a\n // ProductGrid's products, a Reusable's element) to delivery shape from the\n // active content session, in the canvas's active locale, so the canvas previews\n // picked documents exactly as the host renders them. The studio analog of the\n // host's baked relations map. A missing/invisible target → null → the block's\n // own empty state.\n const resolveRelation: ResolveRelation = useCallback(\n (ref) => resolveCanvasRelation(ref, activeLocale, perspective),\n [activeLocale, perspective],\n )\n\n // EC-255: resolve a listing block's query (a ProductGrid keyed to a category) to\n // its documents from the active content session, in the canvas's active locale, so\n // the canvas previews a dynamic grid exactly as the host renders it. The studio\n // analog of the host's baked listings map; an empty/invalid query → [] → the\n // block's own empty state.\n const resolveListing: ResolveListing = useCallback(\n (query) => resolveCanvasListing(query, activeLocale, perspective),\n [activeLocale, perspective],\n )\n\n // EC-291: resolve a rich-text internal link (`{type:'reference'}`) to its target's\n // URL path from the active content session, in the canvas's active locale — the\n // studio analog of the host's baked links map. A missing/unpublished/unroutable\n // target → null → the rich-text renderer shows the visible `unresolved-ref` issue.\n const resolveLink: ResolveLink = useCallback(\n (ref) => resolveCanvasLink(ref, activeLocale, perspective),\n [activeLocale, perspective],\n )\n\n const unresolvedReuse = useCallback(\n (node: ComponentNode): 'unpublished' | 'missing' | null =>\n unresolvedReuseStatus(\n node,\n reuse?.capability,\n (ref, p) => resolveCanvasRelation(ref, activeLocale, p),\n perspective,\n ),\n [reuse, activeLocale, perspective],\n )\n\n const localeFallbackOf = useCallback(\n (node: ComponentNode): string | undefined =>\n firstBindingFallbackLocale(node, (binding) =>\n cmsBindingFallback(binding, data.getSource(binding.sourceId), activeLocale),\n ),\n [data, activeLocale],\n )\n\n const ctx: RenderContext = useMemo(\n () =>\n createCanvasRenderContext({\n registry: studio.registry,\n implementations: studio.implementations,\n resolveBinding,\n resolveAsset,\n resolveRelation,\n resolveListing,\n resolveLink,\n pageNodeIds,\n selectedId,\n hoveredId,\n richEditEpochs,\n unresolvedReuse,\n localeFallbackOf,\n }),\n [studio, resolveBinding, resolveAsset, resolveRelation, resolveListing, resolveLink, unresolvedReuse, pageNodeIds, selectedId, hoveredId, localeFallbackOf, richEditEpochs],\n )\n\n return { ctx, pageNodeIds }\n}\n\n/**\n * In-canvas placeholder for a transcluding embed whose target doesn't resolve\n * (EC-300/AD-16) — shown in place of the empty section so a hard reference never\n * silently vanishes. Rendered INTO the iframe (portal), which carries only the\n * project's stylesheet, so it uses inline styles rather than studio UI classes.\n * The node stays selectable; the inspector's `element` relation chip (EC-296)\n * deep-links to the reusable to edit or publish it.\n */\nfunction UnresolvedReusablePlaceholder({ status }: { status: 'unpublished' | 'missing' }) {\n const title =\n status === 'unpublished'\n ? translate('builder.canvas.reusableUnpublishedTitle')\n : translate('builder.canvas.reusableMissingTitle')\n const hint =\n status === 'unpublished'\n ? translate('builder.canvas.reusableUnpublishedHint')\n : translate('builder.canvas.reusableMissingHint')\n return (\n <div\n style={{\n boxSizing: 'border-box',\n border: '1px dashed rgba(120,120,120,0.5)',\n borderRadius: 8,\n padding: '24px 28px',\n margin: '12px 16px',\n background: 'rgba(127,127,127,0.06)',\n color: 'rgba(90,90,90,0.95)',\n font: '14px/1.5 system-ui, -apple-system, sans-serif',\n textAlign: 'center',\n }}\n >\n <div style={{ fontWeight: 600 }}>⚠ {title}</div>\n <div style={{ marginTop: 6, fontSize: 13, opacity: 0.85 }}>{hint}</div>\n </div>\n )\n}\n"]}
|
|
@@ -4,7 +4,10 @@ import type { CanvasPerspective } from '@elytracms/studio/lib/data';
|
|
|
4
4
|
* into a same-origin iframe via a React portal — true style isolation + true
|
|
5
5
|
* responsive widths. Each node is wrapped with an editable marker so clicks
|
|
6
6
|
* select and hover/selection draw outlines. Native listeners bridge the iframe
|
|
7
|
-
* boundary (React's synthetic events don't cross frames)
|
|
7
|
+
* boundary (React's synthetic events don't cross frames); the two big gesture
|
|
8
|
+
* state machines live in their own tested modules (EC-330): drag & drop in
|
|
9
|
+
* `canvas-dnd`, inline text/rich-text editing in `canvas-inline-editing`, and
|
|
10
|
+
* the render-context assembly in `canvas-render-context`.
|
|
8
11
|
*/
|
|
9
12
|
export declare function BuilderCanvas({ perspective, }?: {
|
|
10
13
|
/** Resolve references against this perspective (EC-300/AD-16). The editable Draft
|