@jxsuite/studio 0.8.0 → 0.10.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/dist/studio.js +156878 -156742
- package/dist/studio.js.map +156 -145
- package/package.json +2 -2
- package/src/canvas/canvas-helpers.js +121 -0
- package/src/canvas/canvas-live-render.js +286 -0
- package/src/canvas/canvas-render.js +429 -0
- package/src/canvas/canvas-utils.js +277 -0
- package/src/editor/component-inline-edit.js +317 -0
- package/src/editor/content-inline-edit.js +213 -0
- package/src/panels/block-action-bar.js +8 -4
- package/src/panels/canvas-dnd.js +154 -0
- package/src/panels/left-panel.js +1 -2
- package/src/panels/overlays.js +9 -23
- package/src/panels/panel-events.js +260 -0
- package/src/panels/preview-render.js +103 -0
- package/src/panels/pseudo-preview.js +57 -0
- package/src/panels/shared.js +1 -1
- package/src/platform.js +9 -6
- package/src/state.js +9 -1
- package/src/store.js +9 -0
- package/src/studio.js +122 -2349
- package/src/utils/edit-display.js +197 -0
- package/src/view.js +0 -2
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Component inline edit — extracted from studio.js (Phase 4j). Manages plaintext-only editing on
|
|
3
|
+
* canvas elements in design mode, with slash menu delegation for block insertion.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
getState,
|
|
8
|
+
update,
|
|
9
|
+
updateUi,
|
|
10
|
+
renderOnly,
|
|
11
|
+
getNodeAtPath,
|
|
12
|
+
selectNode,
|
|
13
|
+
removeNode,
|
|
14
|
+
insertNode,
|
|
15
|
+
updateProperty,
|
|
16
|
+
parentElementPath,
|
|
17
|
+
childIndex,
|
|
18
|
+
canvasPanels,
|
|
19
|
+
elToPath,
|
|
20
|
+
} from "../store.js";
|
|
21
|
+
import { view } from "../view.js";
|
|
22
|
+
import { isSlashMenuOpen, showSlashMenu, dismissSlashMenu } from "./slash-menu.js";
|
|
23
|
+
import { renderBlockActionBar } from "../panels/block-action-bar.js";
|
|
24
|
+
import { defaultDef } from "../panels/shared.js";
|
|
25
|
+
|
|
26
|
+
/** @type {any} */
|
|
27
|
+
let _ctx = null;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Initialize the component inline edit module.
|
|
31
|
+
*
|
|
32
|
+
* @param {{ findCanvasElement: Function }} ctx
|
|
33
|
+
*/
|
|
34
|
+
export function initComponentInlineEdit(ctx) {
|
|
35
|
+
_ctx = ctx;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Enter plaintext inline editing on a canvas element.
|
|
40
|
+
*
|
|
41
|
+
* @param {any} el
|
|
42
|
+
* @param {any} path
|
|
43
|
+
*/
|
|
44
|
+
export function enterComponentInlineEdit(el, path) {
|
|
45
|
+
if (view.componentInlineEdit && view.componentInlineEdit.el === el) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const S = getState();
|
|
50
|
+
const node = getNodeAtPath(S.document, path);
|
|
51
|
+
if (!node) return;
|
|
52
|
+
|
|
53
|
+
const tc = node.textContent;
|
|
54
|
+
if (node.$props && (node.tagName || "").includes("-")) return;
|
|
55
|
+
if (Array.isArray(node.children) && node.children.length > 0) return;
|
|
56
|
+
if (node.children && typeof node.children === "object") return;
|
|
57
|
+
if (tc && typeof tc === "object") return;
|
|
58
|
+
const voids = new Set(["img", "input", "br", "hr", "video", "audio", "source", "embed", "slot"]);
|
|
59
|
+
if (voids.has(node.tagName)) return;
|
|
60
|
+
|
|
61
|
+
for (const p of canvasPanels) {
|
|
62
|
+
const boxes = p.overlay.querySelectorAll(".overlay-box");
|
|
63
|
+
for (const box of boxes) {
|
|
64
|
+
box.style.border = "none";
|
|
65
|
+
}
|
|
66
|
+
p.overlayClk.style.pointerEvents = "none";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
el.contentEditable = "plaintext-only";
|
|
70
|
+
el.style.pointerEvents = "auto";
|
|
71
|
+
el.style.cursor = "text";
|
|
72
|
+
el.style.outline = "1px solid var(--accent, #4f8bc7)";
|
|
73
|
+
el.style.outlineOffset = "-1px";
|
|
74
|
+
el.style.minHeight = "1em";
|
|
75
|
+
|
|
76
|
+
const rawText = typeof tc === "string" ? tc : "";
|
|
77
|
+
el.textContent = rawText;
|
|
78
|
+
|
|
79
|
+
view.componentInlineEdit = {
|
|
80
|
+
el,
|
|
81
|
+
path,
|
|
82
|
+
originalText: rawText,
|
|
83
|
+
mediaName: canvasPanels.find((p) => p.canvas.contains(el))?.mediaName || null,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
el.focus();
|
|
87
|
+
const sel = window.getSelection();
|
|
88
|
+
const range = document.createRange();
|
|
89
|
+
range.selectNodeContents(el);
|
|
90
|
+
range.collapse(false);
|
|
91
|
+
sel?.removeAllRanges();
|
|
92
|
+
sel?.addRange(range);
|
|
93
|
+
|
|
94
|
+
el.addEventListener("keydown", componentInlineKeydown);
|
|
95
|
+
el.addEventListener("input", componentInlineInput);
|
|
96
|
+
|
|
97
|
+
const outsideHandler = (/** @type {any} */ evt) => {
|
|
98
|
+
if (!view.componentInlineEdit) {
|
|
99
|
+
document.removeEventListener("mousedown", outsideHandler, true);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (view.componentInlineEdit.el.contains(evt.target)) return;
|
|
103
|
+
if (isSlashMenuOpen()) return;
|
|
104
|
+
if (view.blockActionBarEl && view.blockActionBarEl.contains(evt.target)) return;
|
|
105
|
+
document.removeEventListener("mousedown", outsideHandler, true);
|
|
106
|
+
|
|
107
|
+
let hitPath = null,
|
|
108
|
+
hitMedia = null;
|
|
109
|
+
for (const p of canvasPanels) {
|
|
110
|
+
const els = p.canvas.querySelectorAll("*");
|
|
111
|
+
for (const el of els) el.style.pointerEvents = "auto";
|
|
112
|
+
p.overlayClk.style.display = "none";
|
|
113
|
+
const found = document.elementsFromPoint(evt.clientX, evt.clientY);
|
|
114
|
+
p.overlayClk.style.display = "";
|
|
115
|
+
for (const el of els) el.style.pointerEvents = "none";
|
|
116
|
+
for (const hit of found) {
|
|
117
|
+
if (p.canvas.contains(hit) && hit !== p.canvas) {
|
|
118
|
+
const path = elToPath.get(hit);
|
|
119
|
+
if (path) {
|
|
120
|
+
hitPath = path;
|
|
121
|
+
hitMedia = p.mediaName;
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (hitPath) break;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const { el: editEl, path: editPath, originalText } = view.componentInlineEdit;
|
|
130
|
+
const newText = (editEl.textContent ?? "").trim();
|
|
131
|
+
cleanupComponentInlineEdit(editEl);
|
|
132
|
+
|
|
133
|
+
const isEmpty = !newText;
|
|
134
|
+
const pPath = parentElementPath(editPath);
|
|
135
|
+
const S = getState();
|
|
136
|
+
|
|
137
|
+
if (hitPath) {
|
|
138
|
+
const media = hitMedia === "base" ? null : (hitMedia ?? null);
|
|
139
|
+
updateUi("pendingInlineEdit", { path: hitPath, mediaName: hitMedia });
|
|
140
|
+
const withMedia = { ...S, ui: { ...S.ui, activeMedia: media } };
|
|
141
|
+
if (isEmpty && pPath) {
|
|
142
|
+
let s = removeNode(withMedia, editPath);
|
|
143
|
+
const removedIdx = /** @type {number} */ (childIndex(editPath));
|
|
144
|
+
const hitIdx = /** @type {number} */ (childIndex(hitPath));
|
|
145
|
+
const hitParent = parentElementPath(hitPath);
|
|
146
|
+
if (hitParent && pPath && hitParent.join("/") === pPath.join("/") && hitIdx > removedIdx) {
|
|
147
|
+
hitPath = [...pPath, "children", hitIdx - 1];
|
|
148
|
+
updateUi("pendingInlineEdit", { path: hitPath, mediaName: hitMedia });
|
|
149
|
+
}
|
|
150
|
+
update(selectNode(s, hitPath));
|
|
151
|
+
} else if (newText !== originalText) {
|
|
152
|
+
update(
|
|
153
|
+
selectNode(
|
|
154
|
+
updateProperty(withMedia, editPath, "textContent", newText || undefined),
|
|
155
|
+
hitPath,
|
|
156
|
+
),
|
|
157
|
+
);
|
|
158
|
+
} else {
|
|
159
|
+
update(selectNode(withMedia, hitPath));
|
|
160
|
+
}
|
|
161
|
+
} else {
|
|
162
|
+
if (isEmpty && pPath) {
|
|
163
|
+
update(removeNode(S, editPath));
|
|
164
|
+
} else if (newText !== originalText) {
|
|
165
|
+
update(updateProperty(S, editPath, "textContent", newText || undefined));
|
|
166
|
+
} else {
|
|
167
|
+
renderOnly("canvas");
|
|
168
|
+
renderOnly("overlays");
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
document.addEventListener("mousedown", outsideHandler, true);
|
|
173
|
+
view.componentInlineEdit._outsideHandler = outsideHandler;
|
|
174
|
+
|
|
175
|
+
renderBlockActionBar();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** @param {any} e */
|
|
179
|
+
function componentInlineKeydown(e) {
|
|
180
|
+
if (isSlashMenuOpen()) {
|
|
181
|
+
if (["ArrowDown", "ArrowUp", "Enter", "Escape"].includes(e.key)) return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
185
|
+
e.preventDefault();
|
|
186
|
+
splitParagraph();
|
|
187
|
+
} else if (e.key === "Escape") {
|
|
188
|
+
e.preventDefault();
|
|
189
|
+
cancelComponentInlineEdit();
|
|
190
|
+
}
|
|
191
|
+
e.stopPropagation();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function splitParagraph() {
|
|
195
|
+
if (!view.componentInlineEdit) return;
|
|
196
|
+
const { el, path, mediaName } = view.componentInlineEdit;
|
|
197
|
+
|
|
198
|
+
const sel = /** @type {any} */ (el.ownerDocument.defaultView?.getSelection());
|
|
199
|
+
const fullText = el.textContent || "";
|
|
200
|
+
let offset = fullText.length;
|
|
201
|
+
if (sel.rangeCount) {
|
|
202
|
+
const range = sel.getRangeAt(0);
|
|
203
|
+
const preRange = document.createRange();
|
|
204
|
+
preRange.selectNodeContents(el);
|
|
205
|
+
preRange.setEnd(range.startContainer, range.startOffset);
|
|
206
|
+
offset = preRange.toString().length;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const textBefore = fullText.slice(0, offset);
|
|
210
|
+
const textAfter = fullText.slice(offset);
|
|
211
|
+
|
|
212
|
+
const tag = "p";
|
|
213
|
+
const pPath = /** @type {any} */ (parentElementPath(path));
|
|
214
|
+
const idx = /** @type {number} */ (childIndex(path));
|
|
215
|
+
if (!pPath) return;
|
|
216
|
+
|
|
217
|
+
const newDef = { tagName: tag, textContent: textAfter };
|
|
218
|
+
const newPath = [...pPath, "children", idx + 1];
|
|
219
|
+
|
|
220
|
+
cleanupComponentInlineEdit(el);
|
|
221
|
+
|
|
222
|
+
const S = getState();
|
|
223
|
+
let s = updateProperty(S, path, "textContent", textBefore || undefined);
|
|
224
|
+
s = insertNode(s, pPath, idx + 1, newDef);
|
|
225
|
+
s = selectNode(s, newPath);
|
|
226
|
+
|
|
227
|
+
updateUi("pendingInlineEdit", { path: newPath, mediaName });
|
|
228
|
+
update(s);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function _commitComponentInlineEdit() {
|
|
232
|
+
if (!view.componentInlineEdit) return;
|
|
233
|
+
const { el, path, originalText } = view.componentInlineEdit;
|
|
234
|
+
const newText = (el.textContent ?? "").trim();
|
|
235
|
+
|
|
236
|
+
cleanupComponentInlineEdit(el);
|
|
237
|
+
|
|
238
|
+
const S = getState();
|
|
239
|
+
const pPath = parentElementPath(path);
|
|
240
|
+
if (!newText && pPath) {
|
|
241
|
+
update(removeNode(S, path));
|
|
242
|
+
} else if (newText !== originalText) {
|
|
243
|
+
update(updateProperty(S, path, "textContent", newText || undefined));
|
|
244
|
+
} else {
|
|
245
|
+
renderOnly("canvas");
|
|
246
|
+
renderOnly("overlays");
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function cancelComponentInlineEdit() {
|
|
251
|
+
if (!view.componentInlineEdit) return;
|
|
252
|
+
const { el } = view.componentInlineEdit;
|
|
253
|
+
cleanupComponentInlineEdit(el);
|
|
254
|
+
renderOnly("canvas");
|
|
255
|
+
renderOnly("overlays");
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** @param {any} el */
|
|
259
|
+
function cleanupComponentInlineEdit(el) {
|
|
260
|
+
el.removeEventListener("keydown", componentInlineKeydown);
|
|
261
|
+
el.removeEventListener("input", componentInlineInput);
|
|
262
|
+
dismissSlashMenu();
|
|
263
|
+
el.removeAttribute("contenteditable");
|
|
264
|
+
el.style.cursor = "";
|
|
265
|
+
el.style.outline = "";
|
|
266
|
+
el.style.outlineOffset = "";
|
|
267
|
+
el.style.minHeight = "";
|
|
268
|
+
el.style.pointerEvents = "";
|
|
269
|
+
|
|
270
|
+
if (view.componentInlineEdit?._outsideHandler) {
|
|
271
|
+
document.removeEventListener("mousedown", view.componentInlineEdit._outsideHandler, true);
|
|
272
|
+
}
|
|
273
|
+
view.componentInlineEdit = null;
|
|
274
|
+
|
|
275
|
+
for (const p of canvasPanels) {
|
|
276
|
+
p.overlay.style.display = "";
|
|
277
|
+
p.overlayClk.style.pointerEvents = "";
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ─── Component-mode slash commands ──────────────────────────────────────────
|
|
282
|
+
|
|
283
|
+
function componentInlineInput() {
|
|
284
|
+
if (!view.componentInlineEdit) return;
|
|
285
|
+
const { el, originalText } = view.componentInlineEdit;
|
|
286
|
+
const text = el.textContent || "";
|
|
287
|
+
|
|
288
|
+
if (originalText === "" && text.startsWith("/")) {
|
|
289
|
+
const filter = text.slice(1).toLowerCase();
|
|
290
|
+
showSlashMenu(el, filter, { onSelect: handleComponentSlashSelect });
|
|
291
|
+
} else {
|
|
292
|
+
dismissSlashMenu();
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** @param {any} cmd */
|
|
297
|
+
function handleComponentSlashSelect(cmd) {
|
|
298
|
+
if (!view.componentInlineEdit) return;
|
|
299
|
+
const { el, path, mediaName } = view.componentInlineEdit;
|
|
300
|
+
const pPath = parentElementPath(path);
|
|
301
|
+
const idx = /** @type {number} */ (childIndex(path));
|
|
302
|
+
if (!pPath) return;
|
|
303
|
+
|
|
304
|
+
cleanupComponentInlineEdit(el);
|
|
305
|
+
|
|
306
|
+
const S = getState();
|
|
307
|
+
const newDef = defaultDef(cmd.tag);
|
|
308
|
+
const newPath = [...pPath, "children", idx];
|
|
309
|
+
|
|
310
|
+
let s = removeNode(S, path);
|
|
311
|
+
s = insertNode(s, pPath, idx, newDef);
|
|
312
|
+
s = selectNode(s, newPath);
|
|
313
|
+
|
|
314
|
+
const hasText = newDef.textContent != null;
|
|
315
|
+
if (hasText) updateUi("pendingInlineEdit", { path: newPath, mediaName });
|
|
316
|
+
update(s);
|
|
317
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content inline edit bridge — extracted from studio.js (Phase 4k). Rich-text editing entry point
|
|
3
|
+
* for edit/content mode. Bridges startEditing() with Jx document state mutations.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
getState,
|
|
8
|
+
update,
|
|
9
|
+
renderOnly,
|
|
10
|
+
selectNode,
|
|
11
|
+
insertNode,
|
|
12
|
+
updateProperty,
|
|
13
|
+
getNodeAtPath,
|
|
14
|
+
parentElementPath,
|
|
15
|
+
childIndex,
|
|
16
|
+
canvasPanels,
|
|
17
|
+
} from "../store.js";
|
|
18
|
+
import { view } from "../view.js";
|
|
19
|
+
import { startEditing, isEditableBlock } from "./inline-edit.js";
|
|
20
|
+
import { restoreTemplateExpressions } from "../utils/edit-display.js";
|
|
21
|
+
import { renderBlockActionBar } from "../panels/block-action-bar.js";
|
|
22
|
+
import { defaultDef } from "../panels/shared.js";
|
|
23
|
+
import { findCanvasElement, getActivePanel } from "../canvas/canvas-helpers.js";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Enter rich-text inline editing on a canvas element (edit/content mode).
|
|
27
|
+
*
|
|
28
|
+
* @param {any} el
|
|
29
|
+
* @param {any} path
|
|
30
|
+
*/
|
|
31
|
+
export function enterInlineEdit(el, path) {
|
|
32
|
+
// Restore raw template expressions before editing.
|
|
33
|
+
// prepareForEditMode renders ${expr} as ❪ expr ❫ for display;
|
|
34
|
+
// revert so the user edits the real syntax and commits it back intact.
|
|
35
|
+
restoreTemplateExpressions(el);
|
|
36
|
+
|
|
37
|
+
// Hide overlays while editing
|
|
38
|
+
for (const p of canvasPanels) {
|
|
39
|
+
p.overlay.style.display = "none";
|
|
40
|
+
p.overlayClk.style.pointerEvents = "none";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
startEditing(el, path, {
|
|
44
|
+
onCommit(
|
|
45
|
+
/** @type {any} */ commitPath,
|
|
46
|
+
/** @type {any} */ children,
|
|
47
|
+
/** @type {any} */ textContent,
|
|
48
|
+
) {
|
|
49
|
+
const S = getState();
|
|
50
|
+
const node = getNodeAtPath(S.document, commitPath);
|
|
51
|
+
if (children) {
|
|
52
|
+
if (node && JSON.stringify(node.children) === JSON.stringify(children)) return;
|
|
53
|
+
let s = updateProperty(S, commitPath, "textContent", undefined);
|
|
54
|
+
s = updateProperty(s, commitPath, "children", children);
|
|
55
|
+
update(s);
|
|
56
|
+
} else if (textContent != null) {
|
|
57
|
+
if (node && node.textContent === textContent && !node.children) return;
|
|
58
|
+
let s = updateProperty(S, commitPath, "children", undefined);
|
|
59
|
+
s = updateProperty(s, commitPath, "textContent", textContent);
|
|
60
|
+
update(s);
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
onSplit(/** @type {any} */ splitPath, /** @type {any} */ before, /** @type {any} */ after) {
|
|
65
|
+
const tag = "p";
|
|
66
|
+
let s = getState();
|
|
67
|
+
|
|
68
|
+
if (before.textContent != null) {
|
|
69
|
+
s = updateProperty(s, splitPath, "children", undefined);
|
|
70
|
+
s = updateProperty(s, splitPath, "textContent", before.textContent);
|
|
71
|
+
} else if (before.children) {
|
|
72
|
+
s = updateProperty(s, splitPath, "textContent", undefined);
|
|
73
|
+
s = updateProperty(s, splitPath, "children", before.children);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Insert new element after with "after" content
|
|
77
|
+
const parentPath = /** @type {any} */ (parentElementPath(splitPath));
|
|
78
|
+
const idx = /** @type {number} */ (childIndex(splitPath));
|
|
79
|
+
/** @type {any} */
|
|
80
|
+
const newNode = { tagName: tag };
|
|
81
|
+
if (after.textContent != null) {
|
|
82
|
+
newNode.textContent = after.textContent;
|
|
83
|
+
} else if (after.children) {
|
|
84
|
+
newNode.children = after.children;
|
|
85
|
+
} else {
|
|
86
|
+
newNode.textContent = "";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
s = insertNode(s, parentPath, idx + 1, newNode);
|
|
90
|
+
const newPath = [...parentPath, "children", idx + 1];
|
|
91
|
+
s = selectNode(s, newPath);
|
|
92
|
+
update(s);
|
|
93
|
+
|
|
94
|
+
// Re-enter editing on the new element after render
|
|
95
|
+
requestAnimationFrame(() => {
|
|
96
|
+
const activePanel = getActivePanel();
|
|
97
|
+
if (activePanel) {
|
|
98
|
+
const newEl = findCanvasElement(newPath, activePanel.canvas);
|
|
99
|
+
if (newEl && isEditableBlock(newEl)) {
|
|
100
|
+
enterInlineEdit(newEl, newPath);
|
|
101
|
+
// Place cursor at start of new element
|
|
102
|
+
const sel = window.getSelection();
|
|
103
|
+
const range = document.createRange();
|
|
104
|
+
range.selectNodeContents(newEl);
|
|
105
|
+
range.collapse(true);
|
|
106
|
+
sel?.removeAllRanges();
|
|
107
|
+
sel?.addRange(range);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
onInsert(/** @type {any} */ afterPath, /** @type {any} */ cmd, /** @type {any} */ commitData) {
|
|
114
|
+
const isEmpty =
|
|
115
|
+
!commitData ||
|
|
116
|
+
(commitData.textContent != null && commitData.textContent.trim() === "") ||
|
|
117
|
+
(commitData.children &&
|
|
118
|
+
(commitData.children.length === 0 ||
|
|
119
|
+
(commitData.children.length === 1 &&
|
|
120
|
+
typeof commitData.children[0] === "string" &&
|
|
121
|
+
commitData.children[0].trim() === "") ||
|
|
122
|
+
(commitData.children.length === 1 &&
|
|
123
|
+
typeof commitData.children[0] === "object" &&
|
|
124
|
+
commitData.children[0]?.tagName === "br")));
|
|
125
|
+
|
|
126
|
+
// If the element is empty, swap its tagName instead of inserting after
|
|
127
|
+
if (isEmpty) {
|
|
128
|
+
let s = getState();
|
|
129
|
+
s = updateProperty(s, afterPath, "tagName", cmd.tag);
|
|
130
|
+
s = updateProperty(s, afterPath, "children", undefined);
|
|
131
|
+
const def = defaultDef(cmd.tag);
|
|
132
|
+
if (def.textContent && def.textContent !== "Paragraph text") {
|
|
133
|
+
s = updateProperty(s, afterPath, "textContent", def.textContent);
|
|
134
|
+
} else {
|
|
135
|
+
s = updateProperty(s, afterPath, "textContent", undefined);
|
|
136
|
+
}
|
|
137
|
+
s = selectNode(s, afterPath);
|
|
138
|
+
update(s);
|
|
139
|
+
|
|
140
|
+
requestAnimationFrame(() => {
|
|
141
|
+
const activePanel = getActivePanel();
|
|
142
|
+
if (activePanel) {
|
|
143
|
+
const el = findCanvasElement(afterPath, activePanel.canvas);
|
|
144
|
+
if (el && isEditableBlock(el)) {
|
|
145
|
+
enterInlineEdit(el, afterPath);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const elementDef = defaultDef(cmd.tag);
|
|
153
|
+
const parentPath = /** @type {any} */ (parentElementPath(afterPath));
|
|
154
|
+
const idx = /** @type {number} */ (childIndex(afterPath));
|
|
155
|
+
|
|
156
|
+
// Apply pending commit from inline edit first (batched to avoid double render)
|
|
157
|
+
let s = getState();
|
|
158
|
+
if (commitData) {
|
|
159
|
+
if (commitData.children) {
|
|
160
|
+
s = updateProperty(s, afterPath, "textContent", undefined);
|
|
161
|
+
s = updateProperty(s, afterPath, "children", commitData.children);
|
|
162
|
+
} else if (commitData.textContent != null) {
|
|
163
|
+
s = updateProperty(s, afterPath, "children", undefined);
|
|
164
|
+
s = updateProperty(s, afterPath, "textContent", commitData.textContent);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
s = insertNode(s, parentPath, idx + 1, structuredClone(elementDef));
|
|
169
|
+
const newPath = [...parentPath, "children", idx + 1];
|
|
170
|
+
s = selectNode(s, newPath);
|
|
171
|
+
update(s);
|
|
172
|
+
|
|
173
|
+
// If the inserted element is editable, enter editing
|
|
174
|
+
requestAnimationFrame(() => {
|
|
175
|
+
const activePanel = getActivePanel();
|
|
176
|
+
if (activePanel) {
|
|
177
|
+
const newEl = findCanvasElement(newPath, activePanel.canvas);
|
|
178
|
+
if (newEl && isEditableBlock(newEl)) {
|
|
179
|
+
enterInlineEdit(newEl, newPath);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
},
|
|
184
|
+
|
|
185
|
+
onEnd() {
|
|
186
|
+
if (view.inlineEditCleanup) {
|
|
187
|
+
view.inlineEditCleanup();
|
|
188
|
+
view.inlineEditCleanup = null;
|
|
189
|
+
}
|
|
190
|
+
for (const p of canvasPanels) {
|
|
191
|
+
p.overlay.style.display = "";
|
|
192
|
+
p.overlayClk.style.pointerEvents = "";
|
|
193
|
+
}
|
|
194
|
+
renderOnly("overlays");
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Show the block action bar (with inline formatting buttons) on the viewport
|
|
199
|
+
requestAnimationFrame(() => renderBlockActionBar());
|
|
200
|
+
|
|
201
|
+
// Re-render action bar when selection changes inside contenteditable
|
|
202
|
+
const selectionHandler = () => renderBlockActionBar();
|
|
203
|
+
document.addEventListener("selectionchange", selectionHandler);
|
|
204
|
+
el.addEventListener("mouseup", selectionHandler);
|
|
205
|
+
el.addEventListener("keyup", selectionHandler);
|
|
206
|
+
|
|
207
|
+
const inlineEditCleanup = () => {
|
|
208
|
+
document.removeEventListener("selectionchange", selectionHandler);
|
|
209
|
+
el.removeEventListener("mouseup", selectionHandler);
|
|
210
|
+
el.removeEventListener("keyup", selectionHandler);
|
|
211
|
+
};
|
|
212
|
+
view.inlineEditCleanup = inlineEditCleanup;
|
|
213
|
+
}
|
|
@@ -23,6 +23,7 @@ import { isEditing, getActiveElement, getInlineActions } from "../editor/inline-
|
|
|
23
23
|
import { toggleInlineFormat, isTagActiveInSelection } from "../editor/inline-format.js";
|
|
24
24
|
import { componentRegistry } from "../files/components.js";
|
|
25
25
|
import { convertToComponent } from "../editor/convert-to-component.js";
|
|
26
|
+
import { findCanvasElement, getActivePanel } from "../canvas/canvas-helpers.js";
|
|
26
27
|
|
|
27
28
|
/** @type {any} */
|
|
28
29
|
let _ctx = null;
|
|
@@ -32,8 +33,6 @@ let _ctx = null;
|
|
|
32
33
|
*
|
|
33
34
|
* @param {{
|
|
34
35
|
* getCanvasMode: () => string;
|
|
35
|
-
* findCanvasElement: Function;
|
|
36
|
-
* getActivePanel: Function;
|
|
37
36
|
* navigateToComponent: Function;
|
|
38
37
|
* createFloatingContainer: Function;
|
|
39
38
|
* }} ctx
|
|
@@ -186,6 +185,11 @@ export function dismissLinkPopover() {
|
|
|
186
185
|
if (view.linkPopoverHost) litRender(nothing, view.linkPopoverHost);
|
|
187
186
|
}
|
|
188
187
|
|
|
188
|
+
/** Dismiss the block action bar. */
|
|
189
|
+
export function dismissBlockActionBar() {
|
|
190
|
+
if (view.blockActionBarEl) litRender(nothing, view.blockActionBarEl);
|
|
191
|
+
}
|
|
192
|
+
|
|
189
193
|
/** @param {any} anchorBtn */
|
|
190
194
|
function showLinkPopover(anchorBtn) {
|
|
191
195
|
litRender(nothing, view.linkPopoverHost);
|
|
@@ -312,12 +316,12 @@ export function renderBlockActionBar() {
|
|
|
312
316
|
return;
|
|
313
317
|
}
|
|
314
318
|
|
|
315
|
-
const activePanel =
|
|
319
|
+
const activePanel = getActivePanel();
|
|
316
320
|
if (!activePanel) {
|
|
317
321
|
litRender(nothing, view.blockActionBarEl);
|
|
318
322
|
return;
|
|
319
323
|
}
|
|
320
|
-
const el =
|
|
324
|
+
const el = findCanvasElement(S.selection, activePanel.canvas);
|
|
321
325
|
const node = el && getNodeAtPath(S.document, S.selection);
|
|
322
326
|
if (!el || !node) {
|
|
323
327
|
litRender(nothing, view.blockActionBarEl);
|