@jxsuite/studio 0.6.2 → 0.8.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 +162163 -151534
- package/dist/studio.js.map +99 -27
- package/package.json +2 -2
- package/src/editor/insertion-helper.js +301 -0
- package/src/editor/slash-menu.js +111 -37
- package/src/markdown/md-convert.js +18 -16
- package/src/panels/activity-bar.js +22 -0
- package/src/panels/block-action-bar.js +436 -0
- package/src/panels/dnd.js +332 -0
- package/src/panels/editors.js +196 -0
- package/src/panels/elements-panel.js +148 -0
- package/src/panels/git-panel.js +280 -0
- package/src/panels/layers-panel.js +270 -0
- package/src/panels/left-panel.js +142 -0
- package/src/panels/properties-panel.js +1340 -0
- package/src/panels/right-panel.js +5 -3
- package/src/panels/shared.js +74 -0
- package/src/panels/style-inputs.js +176 -0
- package/src/panels/style-panel.js +651 -0
- package/src/panels/style-utils.js +193 -0
- package/src/panels/stylebook-layers-panel.js +103 -0
- package/src/panels/stylebook-panel.js +1052 -0
- package/src/platforms/devserver.js +113 -0
- package/src/state.js +7 -0
- package/src/studio.js +92 -4820
- package/src/ui/spectrum.js +4 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/studio",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Jx Studio — visual builder for Jx documents",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@atlaskit/pragmatic-drag-and-drop": "^1.8.1",
|
|
32
32
|
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.1.0",
|
|
33
|
-
"@jxsuite/runtime": "^0.
|
|
33
|
+
"@jxsuite/runtime": "^0.7.0",
|
|
34
34
|
"@spectrum-web-components/accordion": "^1.12.0",
|
|
35
35
|
"@spectrum-web-components/action-bar": "1.12.0",
|
|
36
36
|
"@spectrum-web-components/action-button": "^1.12.0",
|
|
@@ -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
|
+
}
|
package/src/editor/slash-menu.js
CHANGED
|
@@ -34,12 +34,16 @@ const SLASH_COMMANDS = [
|
|
|
34
34
|
const host = document.createElement("div");
|
|
35
35
|
host.style.display = "contents";
|
|
36
36
|
|
|
37
|
-
/** @type {{ onSelect: (cmd: any) => void } | null} */
|
|
37
|
+
/** @type {{ onSelect: (cmd: any) => void; showFilter?: boolean } | null} */
|
|
38
38
|
let callbacks = null;
|
|
39
39
|
let activeIdx = 0;
|
|
40
40
|
/** @type {any[]} */
|
|
41
41
|
let filteredItems = [];
|
|
42
42
|
let open = false;
|
|
43
|
+
/** @type {HTMLElement | null} */
|
|
44
|
+
let _anchorEl = null;
|
|
45
|
+
/** @type {DOMRect | null} */
|
|
46
|
+
let _anchorRect = null;
|
|
43
47
|
|
|
44
48
|
// ─── Public API ───────────────────────────────────────────────────────────────
|
|
45
49
|
|
|
@@ -53,7 +57,7 @@ export function isSlashMenuOpen() {
|
|
|
53
57
|
*
|
|
54
58
|
* @param {HTMLElement} anchorEl — the element being edited (for positioning)
|
|
55
59
|
* @param {string} filter — current typed filter text (after the "/")
|
|
56
|
-
* @param {{ onSelect: (cmd: any) => void }} cbs
|
|
60
|
+
* @param {{ onSelect: (cmd: any) => void; showFilter?: boolean }} cbs
|
|
57
61
|
*/
|
|
58
62
|
export function showSlashMenu(anchorEl, filter, cbs) {
|
|
59
63
|
// Lazily attach host to sp-theme
|
|
@@ -62,6 +66,8 @@ export function showSlashMenu(anchorEl, filter, cbs) {
|
|
|
62
66
|
}
|
|
63
67
|
|
|
64
68
|
callbacks = cbs;
|
|
69
|
+
_anchorEl = anchorEl;
|
|
70
|
+
_anchorRect = anchorEl.getBoundingClientRect();
|
|
65
71
|
|
|
66
72
|
filteredItems = filter
|
|
67
73
|
? SLASH_COMMANDS.filter(
|
|
@@ -69,49 +75,28 @@ export function showSlashMenu(anchorEl, filter, cbs) {
|
|
|
69
75
|
)
|
|
70
76
|
: SLASH_COMMANDS;
|
|
71
77
|
|
|
72
|
-
if (!filteredItems.length) {
|
|
78
|
+
if (!filteredItems.length && !cbs.showFilter) {
|
|
73
79
|
dismissSlashMenu();
|
|
74
80
|
return;
|
|
75
81
|
}
|
|
76
82
|
|
|
77
83
|
activeIdx = 0;
|
|
78
84
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
litRender(
|
|
82
|
-
html`
|
|
83
|
-
<sp-popover
|
|
84
|
-
open
|
|
85
|
-
style="position:fixed;left:${rect.left}px;top:${rect.bottom +
|
|
86
|
-
4}px;z-index:9999;max-height:280px;overflow-y:auto"
|
|
87
|
-
>
|
|
88
|
-
<sp-menu style="min-width:220px">
|
|
89
|
-
${filteredItems.map(
|
|
90
|
-
(cmd, i) => html`
|
|
91
|
-
<sp-menu-item
|
|
92
|
-
?focused=${i === 0}
|
|
93
|
-
@click=${(/** @type {Event} */ e) => {
|
|
94
|
-
e.preventDefault();
|
|
95
|
-
e.stopPropagation();
|
|
96
|
-
select(cmd);
|
|
97
|
-
}}
|
|
98
|
-
>
|
|
99
|
-
${cmd.label}
|
|
100
|
-
${cmd.description
|
|
101
|
-
? html`<span slot="description">${cmd.description}</span>`
|
|
102
|
-
: nothing}
|
|
103
|
-
</sp-menu-item>
|
|
104
|
-
`,
|
|
105
|
-
)}
|
|
106
|
-
</sp-menu>
|
|
107
|
-
</sp-popover>
|
|
108
|
-
`,
|
|
109
|
-
host,
|
|
110
|
-
);
|
|
85
|
+
render(anchorEl, cbs.showFilter || false);
|
|
111
86
|
|
|
112
87
|
if (!open) {
|
|
113
88
|
open = true;
|
|
114
89
|
document.addEventListener("keydown", onKeydown, true); // capture phase
|
|
90
|
+
requestAnimationFrame(() => {
|
|
91
|
+
document.addEventListener("mousedown", onOutsideClick, true);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (cbs.showFilter) {
|
|
96
|
+
requestAnimationFrame(() => {
|
|
97
|
+
const input = /** @type {HTMLInputElement | null} */ (host.querySelector(".slash-filter"));
|
|
98
|
+
if (input) input.focus();
|
|
99
|
+
});
|
|
115
100
|
}
|
|
116
101
|
}
|
|
117
102
|
|
|
@@ -119,13 +104,75 @@ export function dismissSlashMenu() {
|
|
|
119
104
|
if (!open) return;
|
|
120
105
|
open = false;
|
|
121
106
|
callbacks = null;
|
|
107
|
+
_anchorEl = null;
|
|
108
|
+
_anchorRect = null;
|
|
122
109
|
filteredItems = [];
|
|
123
110
|
document.removeEventListener("keydown", onKeydown, true);
|
|
111
|
+
document.removeEventListener("mousedown", onOutsideClick, true);
|
|
124
112
|
litRender(nothing, host);
|
|
125
113
|
}
|
|
126
114
|
|
|
127
115
|
// ─── Internal ─────────────────────────────────────────────────────────────────
|
|
128
116
|
|
|
117
|
+
/**
|
|
118
|
+
* @param {HTMLElement} anchorEl
|
|
119
|
+
* @param {boolean} showFilter
|
|
120
|
+
*/
|
|
121
|
+
function render(anchorEl, showFilter) {
|
|
122
|
+
const rect = _anchorRect || anchorEl.getBoundingClientRect();
|
|
123
|
+
|
|
124
|
+
litRender(
|
|
125
|
+
html`
|
|
126
|
+
<sp-popover
|
|
127
|
+
open
|
|
128
|
+
style="position:fixed;left:${rect.left}px;top:${rect.bottom +
|
|
129
|
+
4}px;z-index:9999;max-height:320px;overflow-y:auto"
|
|
130
|
+
>
|
|
131
|
+
${showFilter
|
|
132
|
+
? html`<input
|
|
133
|
+
class="slash-filter"
|
|
134
|
+
type="text"
|
|
135
|
+
placeholder="Filter…"
|
|
136
|
+
autocomplete="off"
|
|
137
|
+
style="display:block;width:100%;box-sizing:border-box;padding:6px 10px;border:none;border-bottom:1px solid var(--border, #444);outline:none;font-size:13px;background:transparent;color:inherit"
|
|
138
|
+
@input=${onFilterInput}
|
|
139
|
+
/>`
|
|
140
|
+
: nothing}
|
|
141
|
+
<sp-menu style="min-width:220px">
|
|
142
|
+
${filteredItems.length
|
|
143
|
+
? filteredItems.map(
|
|
144
|
+
(cmd, i) => html`
|
|
145
|
+
<sp-menu-item
|
|
146
|
+
?focused=${i === 0}
|
|
147
|
+
@click=${(/** @type {Event} */ e) => {
|
|
148
|
+
e.preventDefault();
|
|
149
|
+
e.stopPropagation();
|
|
150
|
+
select(cmd);
|
|
151
|
+
}}
|
|
152
|
+
>
|
|
153
|
+
${cmd.label}
|
|
154
|
+
${cmd.description
|
|
155
|
+
? html`<span slot="description">${cmd.description}</span>`
|
|
156
|
+
: nothing}
|
|
157
|
+
</sp-menu-item>
|
|
158
|
+
`,
|
|
159
|
+
)
|
|
160
|
+
: html`<sp-menu-item disabled>No matches</sp-menu-item>`}
|
|
161
|
+
</sp-menu>
|
|
162
|
+
</sp-popover>
|
|
163
|
+
`,
|
|
164
|
+
host,
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** @param {MouseEvent} e */
|
|
169
|
+
function onOutsideClick(e) {
|
|
170
|
+
const popover = host.querySelector("sp-popover");
|
|
171
|
+
if (popover && !popover.contains(/** @type {Node} */ (e.target))) {
|
|
172
|
+
dismissSlashMenu();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
129
176
|
/** @param {any} cmd */
|
|
130
177
|
function select(cmd) {
|
|
131
178
|
const cbs = callbacks;
|
|
@@ -133,16 +180,42 @@ function select(cmd) {
|
|
|
133
180
|
cbs?.onSelect(cmd);
|
|
134
181
|
}
|
|
135
182
|
|
|
183
|
+
/** @param {Event} e */
|
|
184
|
+
function onFilterInput(e) {
|
|
185
|
+
const input = /** @type {HTMLInputElement} */ (e.target);
|
|
186
|
+
const filter = input.value.toLowerCase();
|
|
187
|
+
|
|
188
|
+
filteredItems = filter
|
|
189
|
+
? SLASH_COMMANDS.filter(
|
|
190
|
+
(c) => c.label.toLowerCase().includes(filter) || c.tag.toLowerCase().includes(filter),
|
|
191
|
+
)
|
|
192
|
+
: SLASH_COMMANDS;
|
|
193
|
+
|
|
194
|
+
activeIdx = 0;
|
|
195
|
+
if (_anchorEl) render(_anchorEl, true);
|
|
196
|
+
|
|
197
|
+
// Re-focus input after re-render
|
|
198
|
+
requestAnimationFrame(() => {
|
|
199
|
+
const el = /** @type {HTMLInputElement | null} */ (host.querySelector(".slash-filter"));
|
|
200
|
+
if (el && el !== document.activeElement) {
|
|
201
|
+
el.focus();
|
|
202
|
+
el.selectionStart = el.selectionEnd = el.value.length;
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
136
207
|
/** @param {KeyboardEvent} e */
|
|
137
208
|
function onKeydown(e) {
|
|
138
209
|
if (!open) return;
|
|
139
210
|
|
|
140
|
-
const items = /** @type {NodeListOf<Element>} */ (
|
|
141
|
-
|
|
211
|
+
const items = /** @type {NodeListOf<Element>} */ (
|
|
212
|
+
host.querySelectorAll("sp-menu-item:not([disabled])")
|
|
213
|
+
);
|
|
142
214
|
|
|
143
215
|
if (e.key === "ArrowDown") {
|
|
144
216
|
e.preventDefault();
|
|
145
217
|
e.stopPropagation();
|
|
218
|
+
if (!items.length) return;
|
|
146
219
|
items[activeIdx]?.removeAttribute("focused");
|
|
147
220
|
activeIdx = (activeIdx + 1) % items.length;
|
|
148
221
|
items[activeIdx]?.setAttribute("focused", "");
|
|
@@ -150,6 +223,7 @@ function onKeydown(e) {
|
|
|
150
223
|
} else if (e.key === "ArrowUp") {
|
|
151
224
|
e.preventDefault();
|
|
152
225
|
e.stopPropagation();
|
|
226
|
+
if (!items.length) return;
|
|
153
227
|
items[activeIdx]?.removeAttribute("focused");
|
|
154
228
|
activeIdx = (activeIdx - 1 + items.length) % items.length;
|
|
155
229
|
items[activeIdx]?.setAttribute("focused", "");
|
|
@@ -14,6 +14,7 @@ import { unified } from "unified";
|
|
|
14
14
|
import remarkStringify from "remark-stringify";
|
|
15
15
|
import remarkDirective from "remark-directive";
|
|
16
16
|
import { MD_ALL } from "./md-allowlist.js";
|
|
17
|
+
import { htmlToJx } from "@jxsuite/parser/transpile";
|
|
17
18
|
|
|
18
19
|
// ─── mdast → Jx ──────────────────────────────────────────────────────────
|
|
19
20
|
|
|
@@ -40,7 +41,6 @@ const MDAST_TAG_MAP = {
|
|
|
40
41
|
table: () => "table",
|
|
41
42
|
tableRow: () => "tr",
|
|
42
43
|
tableCell: (/** @type {any} */ n) => (n.isHeader ? "th" : "td"),
|
|
43
|
-
html: () => "div",
|
|
44
44
|
break: () => "br",
|
|
45
45
|
};
|
|
46
46
|
|
|
@@ -55,7 +55,7 @@ export function mdToJx(mdast) {
|
|
|
55
55
|
return {
|
|
56
56
|
children: (mdast.children ?? [])
|
|
57
57
|
.filter((/** @type {any} */ n) => n.type !== "yaml" && n.type !== "toml")
|
|
58
|
-
.
|
|
58
|
+
.flatMap(convertMdastNode)
|
|
59
59
|
.filter(Boolean),
|
|
60
60
|
};
|
|
61
61
|
}
|
|
@@ -78,6 +78,12 @@ function convertMdastNode(node) {
|
|
|
78
78
|
return convertDirective(node);
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
if (node.type === "html") {
|
|
82
|
+
if (!node.value) return null;
|
|
83
|
+
const nodes = htmlToJx(node.value);
|
|
84
|
+
return nodes.length === 1 ? nodes[0] : { tagName: "div", children: nodes };
|
|
85
|
+
}
|
|
86
|
+
|
|
81
87
|
const tagFn = MDAST_TAG_MAP[node.type];
|
|
82
88
|
if (!tagFn) return null;
|
|
83
89
|
|
|
@@ -92,7 +98,7 @@ function convertMdastNode(node) {
|
|
|
92
98
|
if (node.children?.length === 1 && node.children[0].type === "text") {
|
|
93
99
|
el.textContent = node.children[0].value;
|
|
94
100
|
} else if (node.children?.length > 0) {
|
|
95
|
-
el.children = node.children.
|
|
101
|
+
el.children = node.children.flatMap(convertMdastNode).filter(Boolean);
|
|
96
102
|
}
|
|
97
103
|
break;
|
|
98
104
|
}
|
|
@@ -107,7 +113,7 @@ function convertMdastNode(node) {
|
|
|
107
113
|
if (node.children?.length === 1 && node.children[0].type === "text") {
|
|
108
114
|
el.textContent = node.children[0].value;
|
|
109
115
|
} else if (node.children?.length > 0) {
|
|
110
|
-
el.children = node.children.
|
|
116
|
+
el.children = node.children.flatMap(convertMdastNode).filter(Boolean);
|
|
111
117
|
}
|
|
112
118
|
break;
|
|
113
119
|
}
|
|
@@ -122,7 +128,7 @@ function convertMdastNode(node) {
|
|
|
122
128
|
if (node.children?.length === 1 && node.children[0].type === "text") {
|
|
123
129
|
el.textContent = node.children[0].value;
|
|
124
130
|
} else if (node.children?.length > 0) {
|
|
125
|
-
el.children = node.children.
|
|
131
|
+
el.children = node.children.flatMap(convertMdastNode).filter(Boolean);
|
|
126
132
|
}
|
|
127
133
|
break;
|
|
128
134
|
|
|
@@ -134,13 +140,13 @@ function convertMdastNode(node) {
|
|
|
134
140
|
case "blockquote":
|
|
135
141
|
case "listItem":
|
|
136
142
|
if (node.children?.length > 0) {
|
|
137
|
-
el.children = node.children.
|
|
143
|
+
el.children = node.children.flatMap(convertMdastNode).filter(Boolean);
|
|
138
144
|
}
|
|
139
145
|
break;
|
|
140
146
|
|
|
141
147
|
case "list":
|
|
142
148
|
if (node.children?.length > 0) {
|
|
143
|
-
el.children = node.children.
|
|
149
|
+
el.children = node.children.flatMap(convertMdastNode).filter(Boolean);
|
|
144
150
|
}
|
|
145
151
|
if (node.start != null && node.start !== 1) {
|
|
146
152
|
el.attributes = { start: String(node.start) };
|
|
@@ -165,7 +171,7 @@ function convertMdastNode(node) {
|
|
|
165
171
|
|
|
166
172
|
case "table": {
|
|
167
173
|
// Mdast tables have rows directly; split into thead/tbody
|
|
168
|
-
const rows = (node.children ?? []).
|
|
174
|
+
const rows = (node.children ?? []).flatMap(convertMdastNode).filter(Boolean);
|
|
169
175
|
const thead = rows.length > 0 ? { tagName: "thead", children: [rows[0]] } : null;
|
|
170
176
|
const tbody = rows.length > 1 ? { tagName: "tbody", children: rows.slice(1) } : null;
|
|
171
177
|
el.children = [thead, tbody].filter(Boolean);
|
|
@@ -174,7 +180,7 @@ function convertMdastNode(node) {
|
|
|
174
180
|
|
|
175
181
|
case "tableRow":
|
|
176
182
|
if (node.children?.length > 0) {
|
|
177
|
-
el.children = node.children.
|
|
183
|
+
el.children = node.children.flatMap(convertMdastNode).filter(Boolean);
|
|
178
184
|
}
|
|
179
185
|
break;
|
|
180
186
|
|
|
@@ -182,13 +188,9 @@ function convertMdastNode(node) {
|
|
|
182
188
|
if (node.children?.length === 1 && node.children[0].type === "text") {
|
|
183
189
|
el.textContent = node.children[0].value;
|
|
184
190
|
} else if (node.children?.length > 0) {
|
|
185
|
-
el.children = node.children.
|
|
191
|
+
el.children = node.children.flatMap(convertMdastNode).filter(Boolean);
|
|
186
192
|
}
|
|
187
193
|
break;
|
|
188
|
-
|
|
189
|
-
case "html":
|
|
190
|
-
el.innerHTML = node.value;
|
|
191
|
-
break;
|
|
192
194
|
}
|
|
193
195
|
|
|
194
196
|
return el;
|
|
@@ -209,10 +211,10 @@ function convertDirective(node) {
|
|
|
209
211
|
if (node.children?.length === 1 && node.children[0].type === "text") {
|
|
210
212
|
el.textContent = node.children[0].value;
|
|
211
213
|
} else if (node.children?.length > 0) {
|
|
212
|
-
el.children = node.children.
|
|
214
|
+
el.children = node.children.flatMap(convertMdastNode).filter(Boolean);
|
|
213
215
|
}
|
|
214
216
|
} else if (node.type === "containerDirective" && node.children?.length > 0) {
|
|
215
|
-
el.children = node.children.
|
|
217
|
+
el.children = node.children.flatMap(convertMdastNode).filter(Boolean);
|
|
216
218
|
}
|
|
217
219
|
return el;
|
|
218
220
|
}
|