@jxsuite/studio 0.7.0 → 0.9.0
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 +125373 -124927
- package/dist/studio.js.map +87 -73
- package/package.json +2 -2
- package/src/canvas/canvas-utils.js +313 -0
- package/src/editor/component-inline-edit.js +316 -0
- package/src/editor/content-inline-edit.js +220 -0
- package/src/editor/insertion-helper.js +301 -0
- package/src/editor/slash-menu.js +111 -37
- package/src/panels/block-action-bar.js +436 -0
- package/src/panels/canvas-dnd.js +165 -0
- package/src/panels/dnd.js +332 -0
- package/src/panels/editors.js +196 -0
- package/src/panels/left-panel.js +3 -2
- package/src/panels/panel-events.js +263 -0
- package/src/panels/preview-render.js +103 -0
- package/src/panels/properties-panel.js +1340 -0
- package/src/panels/pseudo-preview.js +64 -0
- package/src/panels/right-panel.js +2 -1
- package/src/panels/shared.js +74 -0
- package/src/panels/stylebook-panel.js +1052 -0
- package/src/studio.js +121 -4874
- package/src/utils/edit-display.js +197 -0
|
@@ -0,0 +1,220 @@
|
|
|
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
|
+
parentElementPath,
|
|
14
|
+
childIndex,
|
|
15
|
+
canvasPanels,
|
|
16
|
+
} from "../store.js";
|
|
17
|
+
import { view } from "../view.js";
|
|
18
|
+
import { startEditing, isEditableBlock } from "./inline-edit.js";
|
|
19
|
+
import { restoreTemplateExpressions } from "../utils/edit-display.js";
|
|
20
|
+
import { renderBlockActionBar } from "../panels/block-action-bar.js";
|
|
21
|
+
import { defaultDef } from "../panels/shared.js";
|
|
22
|
+
|
|
23
|
+
/** @type {any} */
|
|
24
|
+
let _ctx = null;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Initialize the content inline edit module.
|
|
28
|
+
*
|
|
29
|
+
* @param {{ findCanvasElement: Function; getActivePanel: Function }} ctx
|
|
30
|
+
*/
|
|
31
|
+
export function initContentInlineEdit(ctx) {
|
|
32
|
+
_ctx = ctx;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Enter rich-text inline editing on a canvas element (edit/content mode).
|
|
37
|
+
*
|
|
38
|
+
* @param {any} el
|
|
39
|
+
* @param {any} path
|
|
40
|
+
*/
|
|
41
|
+
export function enterInlineEdit(el, path) {
|
|
42
|
+
// Restore raw template expressions before editing.
|
|
43
|
+
// prepareForEditMode renders ${expr} as ❪ expr ❫ for display;
|
|
44
|
+
// revert so the user edits the real syntax and commits it back intact.
|
|
45
|
+
restoreTemplateExpressions(el);
|
|
46
|
+
|
|
47
|
+
// Hide overlays while editing
|
|
48
|
+
for (const p of canvasPanels) {
|
|
49
|
+
p.overlay.style.display = "none";
|
|
50
|
+
p.overlayClk.style.pointerEvents = "none";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
startEditing(el, path, {
|
|
54
|
+
onCommit(
|
|
55
|
+
/** @type {any} */ commitPath,
|
|
56
|
+
/** @type {any} */ children,
|
|
57
|
+
/** @type {any} */ textContent,
|
|
58
|
+
) {
|
|
59
|
+
const S = getState();
|
|
60
|
+
if (children) {
|
|
61
|
+
let s = updateProperty(S, commitPath, "textContent", undefined);
|
|
62
|
+
s = updateProperty(s, commitPath, "children", children);
|
|
63
|
+
update(s);
|
|
64
|
+
} else if (textContent != null) {
|
|
65
|
+
let s = updateProperty(S, commitPath, "children", undefined);
|
|
66
|
+
s = updateProperty(s, commitPath, "textContent", textContent);
|
|
67
|
+
update(s);
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
onSplit(/** @type {any} */ splitPath, /** @type {any} */ before, /** @type {any} */ after) {
|
|
72
|
+
const tag = "p";
|
|
73
|
+
let s = getState();
|
|
74
|
+
|
|
75
|
+
if (before.textContent != null) {
|
|
76
|
+
s = updateProperty(s, splitPath, "children", undefined);
|
|
77
|
+
s = updateProperty(s, splitPath, "textContent", before.textContent);
|
|
78
|
+
} else if (before.children) {
|
|
79
|
+
s = updateProperty(s, splitPath, "textContent", undefined);
|
|
80
|
+
s = updateProperty(s, splitPath, "children", before.children);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Insert new element after with "after" content
|
|
84
|
+
const parentPath = /** @type {any} */ (parentElementPath(splitPath));
|
|
85
|
+
const idx = /** @type {number} */ (childIndex(splitPath));
|
|
86
|
+
/** @type {any} */
|
|
87
|
+
const newNode = { tagName: tag };
|
|
88
|
+
if (after.textContent != null) {
|
|
89
|
+
newNode.textContent = after.textContent;
|
|
90
|
+
} else if (after.children) {
|
|
91
|
+
newNode.children = after.children;
|
|
92
|
+
} else {
|
|
93
|
+
newNode.textContent = "";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
s = insertNode(s, parentPath, idx + 1, newNode);
|
|
97
|
+
const newPath = [...parentPath, "children", idx + 1];
|
|
98
|
+
s = selectNode(s, newPath);
|
|
99
|
+
update(s);
|
|
100
|
+
|
|
101
|
+
// Re-enter editing on the new element after render
|
|
102
|
+
requestAnimationFrame(() => {
|
|
103
|
+
const activePanel = _ctx.getActivePanel();
|
|
104
|
+
if (activePanel) {
|
|
105
|
+
const newEl = _ctx.findCanvasElement(newPath, activePanel.canvas);
|
|
106
|
+
if (newEl && isEditableBlock(newEl)) {
|
|
107
|
+
enterInlineEdit(newEl, newPath);
|
|
108
|
+
// Place cursor at start of new element
|
|
109
|
+
const sel = window.getSelection();
|
|
110
|
+
const range = document.createRange();
|
|
111
|
+
range.selectNodeContents(newEl);
|
|
112
|
+
range.collapse(true);
|
|
113
|
+
sel?.removeAllRanges();
|
|
114
|
+
sel?.addRange(range);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
onInsert(/** @type {any} */ afterPath, /** @type {any} */ cmd, /** @type {any} */ commitData) {
|
|
121
|
+
const isEmpty =
|
|
122
|
+
!commitData ||
|
|
123
|
+
(commitData.textContent != null && commitData.textContent.trim() === "") ||
|
|
124
|
+
(commitData.children &&
|
|
125
|
+
(commitData.children.length === 0 ||
|
|
126
|
+
(commitData.children.length === 1 &&
|
|
127
|
+
typeof commitData.children[0] === "string" &&
|
|
128
|
+
commitData.children[0].trim() === "") ||
|
|
129
|
+
(commitData.children.length === 1 &&
|
|
130
|
+
typeof commitData.children[0] === "object" &&
|
|
131
|
+
commitData.children[0]?.tagName === "br")));
|
|
132
|
+
|
|
133
|
+
// If the element is empty, swap its tagName instead of inserting after
|
|
134
|
+
if (isEmpty) {
|
|
135
|
+
let s = getState();
|
|
136
|
+
s = updateProperty(s, afterPath, "tagName", cmd.tag);
|
|
137
|
+
s = updateProperty(s, afterPath, "children", undefined);
|
|
138
|
+
const def = defaultDef(cmd.tag);
|
|
139
|
+
if (def.textContent && def.textContent !== "Paragraph text") {
|
|
140
|
+
s = updateProperty(s, afterPath, "textContent", def.textContent);
|
|
141
|
+
} else {
|
|
142
|
+
s = updateProperty(s, afterPath, "textContent", undefined);
|
|
143
|
+
}
|
|
144
|
+
s = selectNode(s, afterPath);
|
|
145
|
+
update(s);
|
|
146
|
+
|
|
147
|
+
requestAnimationFrame(() => {
|
|
148
|
+
const activePanel = _ctx.getActivePanel();
|
|
149
|
+
if (activePanel) {
|
|
150
|
+
const el = _ctx.findCanvasElement(afterPath, activePanel.canvas);
|
|
151
|
+
if (el && isEditableBlock(el)) {
|
|
152
|
+
enterInlineEdit(el, afterPath);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const elementDef = defaultDef(cmd.tag);
|
|
160
|
+
const parentPath = /** @type {any} */ (parentElementPath(afterPath));
|
|
161
|
+
const idx = /** @type {number} */ (childIndex(afterPath));
|
|
162
|
+
|
|
163
|
+
// Apply pending commit from inline edit first (batched to avoid double render)
|
|
164
|
+
let s = getState();
|
|
165
|
+
if (commitData) {
|
|
166
|
+
if (commitData.children) {
|
|
167
|
+
s = updateProperty(s, afterPath, "textContent", undefined);
|
|
168
|
+
s = updateProperty(s, afterPath, "children", commitData.children);
|
|
169
|
+
} else if (commitData.textContent != null) {
|
|
170
|
+
s = updateProperty(s, afterPath, "children", undefined);
|
|
171
|
+
s = updateProperty(s, afterPath, "textContent", commitData.textContent);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
s = insertNode(s, parentPath, idx + 1, structuredClone(elementDef));
|
|
176
|
+
const newPath = [...parentPath, "children", idx + 1];
|
|
177
|
+
s = selectNode(s, newPath);
|
|
178
|
+
update(s);
|
|
179
|
+
|
|
180
|
+
// If the inserted element is editable, enter editing
|
|
181
|
+
requestAnimationFrame(() => {
|
|
182
|
+
const activePanel = _ctx.getActivePanel();
|
|
183
|
+
if (activePanel) {
|
|
184
|
+
const newEl = _ctx.findCanvasElement(newPath, activePanel.canvas);
|
|
185
|
+
if (newEl && isEditableBlock(newEl)) {
|
|
186
|
+
enterInlineEdit(newEl, newPath);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
},
|
|
191
|
+
|
|
192
|
+
onEnd() {
|
|
193
|
+
if (view.inlineEditCleanup) {
|
|
194
|
+
view.inlineEditCleanup();
|
|
195
|
+
view.inlineEditCleanup = null;
|
|
196
|
+
}
|
|
197
|
+
for (const p of canvasPanels) {
|
|
198
|
+
p.overlay.style.display = "";
|
|
199
|
+
p.overlayClk.style.pointerEvents = "";
|
|
200
|
+
}
|
|
201
|
+
renderOnly("overlays");
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// Show the block action bar (with inline formatting buttons) on the viewport
|
|
206
|
+
requestAnimationFrame(() => renderBlockActionBar());
|
|
207
|
+
|
|
208
|
+
// Re-render action bar when selection changes inside contenteditable
|
|
209
|
+
const selectionHandler = () => renderBlockActionBar();
|
|
210
|
+
document.addEventListener("selectionchange", selectionHandler);
|
|
211
|
+
el.addEventListener("mouseup", selectionHandler);
|
|
212
|
+
el.addEventListener("keyup", selectionHandler);
|
|
213
|
+
|
|
214
|
+
const inlineEditCleanup = () => {
|
|
215
|
+
document.removeEventListener("selectionchange", selectionHandler);
|
|
216
|
+
el.removeEventListener("mouseup", selectionHandler);
|
|
217
|
+
el.removeEventListener("keyup", selectionHandler);
|
|
218
|
+
};
|
|
219
|
+
view.inlineEditCleanup = inlineEditCleanup;
|
|
220
|
+
}
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Insertion-helper.js — Single floating "+" button for element insertion on the canvas.
|
|
3
|
+
*
|
|
4
|
+
* Uses CSS Anchor Positioning to attach to sibling boundaries and empty containers. Uses Native
|
|
5
|
+
* Observables (Chrome 135+) for declarative event handling.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { showSlashMenu } from "./slash-menu.js";
|
|
9
|
+
|
|
10
|
+
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @typedef {Object} ObservableSubscription
|
|
14
|
+
* @property {(obj: { next: Function }) => void} subscribe - Subscribes to the observable stream.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @typedef {Object} ObservableElement
|
|
19
|
+
* @property {(event: string, options?: Object) => ObservableSubscription} on - Creates an
|
|
20
|
+
* observable for the given event.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @typedef {Object} CanvasPanel
|
|
25
|
+
* @property {HTMLElement} canvas - The canvas content element.
|
|
26
|
+
* @property {HTMLElement & ObservableElement} overlayClk - The overlay click-capture layer.
|
|
27
|
+
* @property {HTMLElement} overlay - The overlay rendering layer.
|
|
28
|
+
* @property {HTMLElement} viewport - The viewport container (containing block for anchor
|
|
29
|
+
* positioning).
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @typedef {Object} InsertionHelperContext
|
|
34
|
+
* @property {() => any} getState - Returns the current editor state.
|
|
35
|
+
* @property {(state: any) => void} update - Commits a new state.
|
|
36
|
+
* @property {() => string} getCanvasMode - Returns the active canvas mode.
|
|
37
|
+
* @property {(fn: Function) => any} withPanelPointerEvents - Executes fn with pointer-events
|
|
38
|
+
* temporarily enabled on the canvas.
|
|
39
|
+
* @property {() => number} effectiveZoom - Returns the current zoom scale factor.
|
|
40
|
+
* @property {(tag: string) => Object} defaultDef - Creates a default element definition for a tag.
|
|
41
|
+
* @property {(s: any, path: any[], idx: number, def: Object) => any} insertNode - Inserts a node
|
|
42
|
+
* into the document tree.
|
|
43
|
+
* @property {(s: any, path: any[]) => any} selectNode - Sets the selection to the given path.
|
|
44
|
+
* @property {(path: any[]) => any[] | null} parentElementPath - Returns the parent element path, or
|
|
45
|
+
* null for root.
|
|
46
|
+
* @property {(path: any[]) => string | number} childIndex - Returns the child index within the
|
|
47
|
+
* parent.
|
|
48
|
+
* @property {(doc: any, path: any[]) => any} getNodeAtPath - Retrieves the node at a document path.
|
|
49
|
+
* @property {WeakMap<any, any[]>} elToPath - Maps rendered DOM elements to their document paths.
|
|
50
|
+
* @property {CanvasPanel} panel - The active canvas panel.
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
// ─── State ───────────────────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
/** @type {InsertionHelperContext | null} */
|
|
56
|
+
let _ctx = null;
|
|
57
|
+
|
|
58
|
+
/** @type {HTMLElement | null} */
|
|
59
|
+
let _helper = null;
|
|
60
|
+
|
|
61
|
+
/** @type {HTMLElement | null} */
|
|
62
|
+
let _currentAnchor = null;
|
|
63
|
+
|
|
64
|
+
/** @type {{ edge: string; path: any[]; parentPath: any[]; idx: number } | null} */
|
|
65
|
+
let _insertionPoint = null;
|
|
66
|
+
|
|
67
|
+
/** @type {AbortController | null} */
|
|
68
|
+
let _abort = null;
|
|
69
|
+
|
|
70
|
+
/** @type {ReturnType<typeof setTimeout> | null} */
|
|
71
|
+
let _hideTimer = null;
|
|
72
|
+
|
|
73
|
+
// Edge detection threshold in pixels
|
|
74
|
+
const EDGE_THRESHOLD = 14;
|
|
75
|
+
|
|
76
|
+
// Delay before hiding to allow cursor to reach the button
|
|
77
|
+
const HIDE_DELAY = 300;
|
|
78
|
+
|
|
79
|
+
// ─── Public API ──────────────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Mount the insertion helper system.
|
|
83
|
+
*
|
|
84
|
+
* @param {InsertionHelperContext} ctx
|
|
85
|
+
*/
|
|
86
|
+
export function mount(ctx) {
|
|
87
|
+
_ctx = ctx;
|
|
88
|
+
const { panel } = ctx;
|
|
89
|
+
|
|
90
|
+
_helper = document.createElement("button");
|
|
91
|
+
_helper.className = "insertion-helper";
|
|
92
|
+
_helper.textContent = "+";
|
|
93
|
+
_helper.addEventListener("click", onHelperClick);
|
|
94
|
+
_helper.addEventListener("mouseenter", () => {
|
|
95
|
+
cancelHide();
|
|
96
|
+
});
|
|
97
|
+
_helper.addEventListener("mouseleave", () => {
|
|
98
|
+
scheduleHide();
|
|
99
|
+
});
|
|
100
|
+
panel.viewport.appendChild(_helper);
|
|
101
|
+
|
|
102
|
+
_abort = new AbortController();
|
|
103
|
+
|
|
104
|
+
// Listen on viewport — overlayClk gets pointer-events:none during editing/selection
|
|
105
|
+
const viewport = /** @type {HTMLElement & ObservableElement} */ (panel.viewport);
|
|
106
|
+
if (typeof viewport.on === "function") {
|
|
107
|
+
viewport.on("mousemove", { signal: _abort.signal }).subscribe({ next: onMouseMove });
|
|
108
|
+
viewport.on("mouseleave", { signal: _abort.signal }).subscribe({ next: hide });
|
|
109
|
+
} else {
|
|
110
|
+
panel.viewport.addEventListener("mousemove", onMouseMove, { signal: _abort.signal });
|
|
111
|
+
panel.viewport.addEventListener("mouseleave", hide, { signal: _abort.signal });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function unmount() {
|
|
116
|
+
_abort?.abort();
|
|
117
|
+
_abort = null;
|
|
118
|
+
cancelHide();
|
|
119
|
+
if (_helper?.parentElement) _helper.remove();
|
|
120
|
+
clearAnchor();
|
|
121
|
+
_helper = null;
|
|
122
|
+
_ctx = null;
|
|
123
|
+
_insertionPoint = null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ─── Detection ───────────────────────────────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
/** @param {MouseEvent} e */
|
|
129
|
+
function onMouseMove(e) {
|
|
130
|
+
if (!_ctx || !_helper) return;
|
|
131
|
+
|
|
132
|
+
const { getCanvasMode } = _ctx;
|
|
133
|
+
if (getCanvasMode() !== "design") {
|
|
134
|
+
hide();
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const { panel, withPanelPointerEvents, elToPath } = _ctx;
|
|
139
|
+
const el = withPanelPointerEvents(() => document.elementFromPoint(e.clientX, e.clientY));
|
|
140
|
+
|
|
141
|
+
if (!el || !panel.canvas.contains(el)) {
|
|
142
|
+
hide();
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const path = elToPath.get(el);
|
|
147
|
+
if (!path) {
|
|
148
|
+
hide();
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Empty container: show centered "+"
|
|
153
|
+
if (el.classList.contains("empty-container-placeholder")) {
|
|
154
|
+
showAt(el, "center", path, path, 0);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Root element — can't insert siblings above/below root
|
|
159
|
+
if (path.length === 0) {
|
|
160
|
+
hide();
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Determine layout direction of parent container
|
|
165
|
+
const parent = el.parentElement;
|
|
166
|
+
if (!parent) {
|
|
167
|
+
hide();
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const parentStyle = getComputedStyle(parent);
|
|
172
|
+
const display = parentStyle.display;
|
|
173
|
+
const isFlex = display === "flex" || display === "inline-flex";
|
|
174
|
+
const isGrid = display === "grid" || display === "inline-grid";
|
|
175
|
+
const isRow =
|
|
176
|
+
(isFlex && parentStyle.flexDirection.startsWith("row")) ||
|
|
177
|
+
(isGrid && parentStyle.gridAutoFlow?.startsWith("column"));
|
|
178
|
+
|
|
179
|
+
// Calculate relative position within element
|
|
180
|
+
const rect = el.getBoundingClientRect();
|
|
181
|
+
const parentPath = _ctx.parentElementPath(path);
|
|
182
|
+
if (!parentPath) {
|
|
183
|
+
hide();
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const childIdx = /** @type {number} */ (_ctx.childIndex(path));
|
|
187
|
+
|
|
188
|
+
if (isRow) {
|
|
189
|
+
const relX = e.clientX - rect.left;
|
|
190
|
+
if (relX < EDGE_THRESHOLD) {
|
|
191
|
+
showAt(el, "left", path, parentPath, childIdx);
|
|
192
|
+
} else if (rect.width - relX < EDGE_THRESHOLD) {
|
|
193
|
+
showAt(el, "right", path, parentPath, childIdx + 1);
|
|
194
|
+
} else {
|
|
195
|
+
hide();
|
|
196
|
+
}
|
|
197
|
+
} else {
|
|
198
|
+
const relY = e.clientY - rect.top;
|
|
199
|
+
if (relY < EDGE_THRESHOLD) {
|
|
200
|
+
showAt(el, "top", path, parentPath, childIdx);
|
|
201
|
+
} else if (rect.height - relY < EDGE_THRESHOLD) {
|
|
202
|
+
showAt(el, "bottom", path, parentPath, childIdx + 1);
|
|
203
|
+
} else {
|
|
204
|
+
hide();
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ─── Show / Hide ─────────────────────────────────────────────────────────────
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* @param {HTMLElement} el
|
|
213
|
+
* @param {string} edge
|
|
214
|
+
* @param {any[]} path
|
|
215
|
+
* @param {any[]} parentPath
|
|
216
|
+
* @param {number} idx
|
|
217
|
+
*/
|
|
218
|
+
function showAt(el, edge, path, parentPath, idx) {
|
|
219
|
+
if (!_helper) return;
|
|
220
|
+
|
|
221
|
+
// Set CSS anchor on target element
|
|
222
|
+
if (_currentAnchor !== el) {
|
|
223
|
+
clearAnchor();
|
|
224
|
+
el.style.anchorName = "--jx-insert";
|
|
225
|
+
_currentAnchor = el;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
_helper.dataset.edge = edge;
|
|
229
|
+
_helper.classList.add("visible");
|
|
230
|
+
_insertionPoint = { edge, path, parentPath, idx };
|
|
231
|
+
cancelHide();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function scheduleHide() {
|
|
235
|
+
cancelHide();
|
|
236
|
+
_hideTimer = setTimeout(hideNow, HIDE_DELAY);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function cancelHide() {
|
|
240
|
+
if (_hideTimer !== null) {
|
|
241
|
+
clearTimeout(_hideTimer);
|
|
242
|
+
_hideTimer = null;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function hide() {
|
|
247
|
+
scheduleHide();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function hideNow() {
|
|
251
|
+
_hideTimer = null;
|
|
252
|
+
if (!_helper) return;
|
|
253
|
+
_helper.classList.remove("visible");
|
|
254
|
+
clearAnchor();
|
|
255
|
+
_insertionPoint = null;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function clearAnchor() {
|
|
259
|
+
if (_currentAnchor) {
|
|
260
|
+
_currentAnchor.style.anchorName = "";
|
|
261
|
+
_currentAnchor = null;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ─── Insertion ───────────────────────────────────────────────────────────────
|
|
266
|
+
|
|
267
|
+
function onHelperClick(/** @type {MouseEvent} */ e) {
|
|
268
|
+
e.stopPropagation();
|
|
269
|
+
e.preventDefault();
|
|
270
|
+
|
|
271
|
+
if (!_ctx || !_helper || !_insertionPoint) return;
|
|
272
|
+
|
|
273
|
+
const captured = _insertionPoint;
|
|
274
|
+
showSlashMenu(_helper, "", {
|
|
275
|
+
showFilter: true,
|
|
276
|
+
onSelect: (cmd) => onSlashSelect(cmd, captured),
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* @param {any} cmd
|
|
282
|
+
* @param {{ edge: string; path: any[]; parentPath: any[]; idx: number }} point
|
|
283
|
+
*/
|
|
284
|
+
function onSlashSelect(cmd, point) {
|
|
285
|
+
if (!_ctx) return;
|
|
286
|
+
|
|
287
|
+
const { getState, update, defaultDef, insertNode, selectNode } = _ctx;
|
|
288
|
+
const S = getState();
|
|
289
|
+
const { parentPath, idx, edge } = point;
|
|
290
|
+
|
|
291
|
+
const newDef = defaultDef(cmd.tag);
|
|
292
|
+
const insertPath = edge === "center" ? point.path : parentPath;
|
|
293
|
+
const insertIdx = edge === "center" ? 0 : idx;
|
|
294
|
+
|
|
295
|
+
let s = insertNode(S, insertPath, insertIdx, newDef);
|
|
296
|
+
const newPath = [...insertPath, "children", insertIdx];
|
|
297
|
+
s = selectNode(s, newPath);
|
|
298
|
+
update(s);
|
|
299
|
+
|
|
300
|
+
hide();
|
|
301
|
+
}
|