@jxsuite/studio 0.28.2 → 0.28.4
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 +86144 -83971
- package/dist/studio.js.map +229 -219
- package/package.json +5 -5
- package/src/browse/browse-modal.ts +9 -3
- package/src/browse/browse.ts +139 -82
- package/src/canvas/canvas-diff.ts +35 -18
- package/src/canvas/canvas-helpers.ts +55 -21
- package/src/canvas/canvas-live-render.ts +140 -89
- package/src/canvas/canvas-render.ts +127 -82
- package/src/canvas/canvas-utils.ts +68 -42
- package/src/canvas/nested-site-style.ts +15 -7
- package/src/editor/component-inline-edit.ts +90 -51
- package/src/editor/content-inline-edit.ts +75 -72
- package/src/editor/context-menu.ts +125 -71
- package/src/editor/convert-targets.ts +17 -13
- package/src/editor/convert-to-component.ts +51 -28
- package/src/editor/convert-to-repeater.ts +44 -19
- package/src/editor/inline-edit.ts +107 -52
- package/src/editor/inline-format.ts +128 -60
- package/src/editor/insertion-helper.ts +26 -14
- package/src/editor/shortcuts.ts +89 -43
- package/src/editor/slash-menu.ts +41 -26
- package/src/files/components.ts +9 -7
- package/src/files/file-ops.ts +53 -33
- package/src/files/files.ts +246 -143
- package/src/format/constraints.ts +25 -15
- package/src/format/format-host.ts +33 -10
- package/src/github/github-auth.ts +24 -14
- package/src/github/github-publish.ts +20 -14
- package/src/new-project/new-project-modal.ts +27 -18
- package/src/panels/activity-bar.ts +27 -26
- package/src/panels/ai-panel.ts +100 -44
- package/src/panels/block-action-bar.ts +84 -52
- package/src/panels/canvas-dnd.ts +47 -28
- package/src/panels/data-explorer.ts +24 -11
- package/src/panels/dnd.ts +148 -100
- package/src/panels/editors.ts +54 -37
- package/src/panels/elements-panel.ts +23 -13
- package/src/panels/events-panel.ts +48 -38
- package/src/panels/git-panel.ts +95 -60
- package/src/panels/head-panel.ts +138 -84
- package/src/panels/imports-panel.ts +66 -29
- package/src/panels/layers-panel.ts +80 -36
- package/src/panels/left-panel.ts +86 -53
- package/src/panels/overlays.ts +36 -19
- package/src/panels/panel-events.ts +48 -27
- package/src/panels/panel-scheduler.ts +41 -23
- package/src/panels/preview-render.ts +26 -26
- package/src/panels/properties-panel.ts +155 -118
- package/src/panels/pseudo-preview.ts +31 -15
- package/src/panels/quick-search.ts +28 -13
- package/src/panels/right-panel.ts +35 -24
- package/src/panels/shared.ts +46 -29
- package/src/panels/signals-panel.ts +303 -186
- package/src/panels/statusbar.ts +27 -12
- package/src/panels/style-inputs.ts +30 -24
- package/src/panels/style-panel.ts +130 -90
- package/src/panels/style-utils.ts +63 -23
- package/src/panels/stylebook-layers-panel.ts +27 -30
- package/src/panels/stylebook-panel.ts +140 -72
- package/src/panels/tab-strip.ts +21 -10
- package/src/panels/toolbar.ts +52 -40
- package/src/panels/welcome-screen.ts +1 -1
- package/src/platform.ts +2 -1
- package/src/platforms/devserver.ts +221 -105
- package/src/recent-projects.ts +15 -7
- package/src/resize-edges.ts +7 -5
- package/src/services/cem-export.ts +73 -57
- package/src/services/code-services.ts +52 -25
- package/src/services/monaco-setup.ts +5 -5
- package/src/settings/content-types-editor.ts +133 -62
- package/src/settings/css-vars-editor.ts +31 -14
- package/src/settings/defs-editor.ts +125 -63
- package/src/settings/general-settings.ts +11 -6
- package/src/settings/head-editor.ts +30 -15
- package/src/settings/schema-field-ui.ts +75 -35
- package/src/settings/settings-modal.ts +35 -17
- package/src/site-context.ts +80 -47
- package/src/state.ts +98 -65
- package/src/store.ts +42 -23
- package/src/studio.ts +165 -144
- package/src/tabs/tab.ts +51 -45
- package/src/tabs/transact.ts +230 -109
- package/src/types.ts +60 -51
- package/src/ui/button-group.ts +3 -2
- package/src/ui/color-selector.ts +33 -15
- package/src/ui/expression-editor.ts +139 -86
- package/src/ui/field-input.ts +35 -18
- package/src/ui/field-row.ts +1 -1
- package/src/ui/icons.ts +5 -6
- package/src/ui/layers.ts +33 -26
- package/src/ui/media-picker.ts +13 -7
- package/src/ui/panel-resize.ts +39 -20
- package/src/ui/spectrum.ts +4 -2
- package/src/ui/unit-selector.ts +20 -11
- package/src/ui/value-selector.ts +13 -7
- package/src/ui/widgets.ts +34 -18
- package/src/utils/canvas-media.ts +66 -33
- package/src/utils/edit-display.ts +43 -44
- package/src/utils/google-fonts.ts +10 -6
- package/src/utils/inherited-style.ts +22 -8
- package/src/utils/studio-utils.ts +88 -45
- package/src/view.ts +9 -5
- package/src/workspace/workspace.ts +49 -29
package/src/editor/shortcuts.ts
CHANGED
|
@@ -6,15 +6,15 @@
|
|
|
6
6
|
* shortcuts on the canvas / document.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import {
|
|
10
|
-
import { activeTab,
|
|
9
|
+
import { canvasWrap, childIndex, childList, getNodeAtPath, parentElementPath } from "../store";
|
|
10
|
+
import { activeTab, closeTab, workspace } from "../workspace/workspace";
|
|
11
11
|
import {
|
|
12
|
-
|
|
12
|
+
mutateDuplicateNode,
|
|
13
13
|
mutateInsertNode,
|
|
14
14
|
mutateRemoveNode,
|
|
15
|
-
mutateDuplicateNode,
|
|
16
|
-
undo as tabUndo,
|
|
17
15
|
redo as tabRedo,
|
|
16
|
+
undo as tabUndo,
|
|
17
|
+
transactDoc,
|
|
18
18
|
} from "../tabs/transact";
|
|
19
19
|
import { isEditing, stopEditing } from "./inline-edit";
|
|
20
20
|
import { copyNode, cutNode, pasteNode } from "./context-menu";
|
|
@@ -59,9 +59,13 @@ export function initShortcuts(
|
|
|
59
59
|
(e: WheelEvent) => {
|
|
60
60
|
const { canvasMode, panX, panY, setPan, applyTransform } = getContext();
|
|
61
61
|
// Edit (content) mode: let the scroll container handle scrolling natively
|
|
62
|
-
if (canvasMode === "edit")
|
|
62
|
+
if (canvasMode === "edit") {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
63
65
|
// Manage mode: browse table handles its own scrolling
|
|
64
|
-
if (canvasMode === "manage")
|
|
66
|
+
if (canvasMode === "manage") {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
65
69
|
e.preventDefault();
|
|
66
70
|
if (e.ctrlKey || e.metaKey) {
|
|
67
71
|
// Zoom towards cursor
|
|
@@ -70,7 +74,7 @@ export function initShortcuts(
|
|
|
70
74
|
const cursorY = e.clientY - rect.top;
|
|
71
75
|
const oldZoom = activeTab.value?.session.ui.zoom ?? 1;
|
|
72
76
|
const delta = -e.deltaY * 0.005;
|
|
73
|
-
const newZoom = Math.min(5
|
|
77
|
+
const newZoom = Math.min(5, Math.max(0.05, oldZoom * (1 + delta)));
|
|
74
78
|
const ratio = newZoom / oldZoom;
|
|
75
79
|
// Adjust pan so the point under cursor stays stationary
|
|
76
80
|
setPan(cursorX - (cursorX - panX) * ratio, cursorY - (cursorY - panY) * ratio);
|
|
@@ -90,8 +94,12 @@ export function initShortcuts(
|
|
|
90
94
|
// Middle-mouse drag panning
|
|
91
95
|
canvasWrap.addEventListener("pointerdown", (e: PointerEvent) => {
|
|
92
96
|
const ctx = getContext();
|
|
93
|
-
if (ctx.canvasMode === "edit")
|
|
94
|
-
|
|
97
|
+
if (ctx.canvasMode === "edit") {
|
|
98
|
+
return;
|
|
99
|
+
} // No panning in edit mode
|
|
100
|
+
if (e.button !== 1) {
|
|
101
|
+
return;
|
|
102
|
+
} // Middle button only
|
|
95
103
|
e.preventDefault();
|
|
96
104
|
canvasWrap.setPointerCapture(e.pointerId);
|
|
97
105
|
let lastX = e.clientX,
|
|
@@ -169,97 +177,125 @@ export function initShortcuts(
|
|
|
169
177
|
|
|
170
178
|
if (mod) {
|
|
171
179
|
switch (e.key) {
|
|
172
|
-
case "w":
|
|
180
|
+
case "w": {
|
|
173
181
|
e.preventDefault();
|
|
174
182
|
if (workspace.activeTabId && workspace.tabOrder.length > 1) {
|
|
175
|
-
const
|
|
176
|
-
if (
|
|
177
|
-
const name =
|
|
183
|
+
const tabToClose = workspace.tabs.get(workspace.activeTabId);
|
|
184
|
+
if (tabToClose?.doc.dirty) {
|
|
185
|
+
const name = tabToClose.documentPath?.split("/").pop() || "Untitled";
|
|
178
186
|
showConfirmDialog(
|
|
179
187
|
"Unsaved Changes",
|
|
180
188
|
`"${name}" has unsaved changes. Close without saving?`,
|
|
181
189
|
{ confirmLabel: "Close", destructive: true },
|
|
182
190
|
).then((confirmed) => {
|
|
183
|
-
if (confirmed && workspace.activeTabId)
|
|
191
|
+
if (confirmed && workspace.activeTabId) {
|
|
192
|
+
closeTab(workspace.activeTabId);
|
|
193
|
+
}
|
|
184
194
|
});
|
|
185
195
|
} else {
|
|
186
196
|
closeTab(workspace.activeTabId);
|
|
187
197
|
}
|
|
188
198
|
}
|
|
189
199
|
break;
|
|
190
|
-
|
|
200
|
+
}
|
|
201
|
+
case "o": {
|
|
191
202
|
e.preventDefault();
|
|
192
203
|
openProject();
|
|
193
204
|
break;
|
|
194
|
-
|
|
205
|
+
}
|
|
206
|
+
case "p": {
|
|
195
207
|
e.preventDefault();
|
|
196
208
|
openQuickSearch();
|
|
197
209
|
break;
|
|
198
|
-
|
|
210
|
+
}
|
|
211
|
+
case "s": {
|
|
199
212
|
e.preventDefault();
|
|
200
213
|
saveFile();
|
|
201
214
|
break;
|
|
202
|
-
|
|
215
|
+
}
|
|
216
|
+
case "z": {
|
|
203
217
|
e.preventDefault();
|
|
204
|
-
if (e.shiftKey)
|
|
205
|
-
|
|
218
|
+
if (e.shiftKey) {
|
|
219
|
+
tabRedo(activeTab.value!);
|
|
220
|
+
} else {
|
|
221
|
+
tabUndo(activeTab.value!);
|
|
222
|
+
}
|
|
206
223
|
break;
|
|
207
|
-
|
|
224
|
+
}
|
|
225
|
+
case "d": {
|
|
208
226
|
e.preventDefault();
|
|
209
227
|
if (tab?.session.selection) {
|
|
210
228
|
const sel = tab.session.selection;
|
|
211
229
|
transactDoc(tab, (t) => mutateDuplicateNode(t, sel));
|
|
212
230
|
}
|
|
213
231
|
break;
|
|
214
|
-
|
|
232
|
+
}
|
|
233
|
+
case "c": {
|
|
215
234
|
e.preventDefault();
|
|
216
235
|
copyNode();
|
|
217
236
|
break;
|
|
218
|
-
|
|
237
|
+
}
|
|
238
|
+
case "x": {
|
|
219
239
|
e.preventDefault();
|
|
220
240
|
cutNode();
|
|
221
241
|
break;
|
|
222
|
-
|
|
242
|
+
}
|
|
243
|
+
case "v": {
|
|
223
244
|
e.preventDefault();
|
|
224
245
|
pasteNode();
|
|
225
246
|
break;
|
|
226
|
-
|
|
227
|
-
|
|
247
|
+
}
|
|
248
|
+
case "0": {
|
|
249
|
+
if (canvasMode === "edit") {
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
228
252
|
e.preventDefault();
|
|
229
253
|
activeTab.value!.session.ui.zoom = 1;
|
|
230
254
|
setPan(16, 16);
|
|
231
255
|
applyTransform();
|
|
232
256
|
break;
|
|
257
|
+
}
|
|
233
258
|
case "=":
|
|
234
|
-
case "+":
|
|
235
|
-
if (canvasMode === "edit")
|
|
259
|
+
case "+": {
|
|
260
|
+
if (canvasMode === "edit") {
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
236
263
|
e.preventDefault();
|
|
237
|
-
activeTab.value!.session.ui.zoom = Math.min(5
|
|
264
|
+
activeTab.value!.session.ui.zoom = Math.min(5, (tab?.session.ui.zoom ?? 1) * 1.2);
|
|
238
265
|
applyTransform();
|
|
239
266
|
break;
|
|
240
|
-
|
|
241
|
-
|
|
267
|
+
}
|
|
268
|
+
case "-": {
|
|
269
|
+
if (canvasMode === "edit") {
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
242
272
|
e.preventDefault();
|
|
243
273
|
activeTab.value!.session.ui.zoom = Math.max(0.05, (tab?.session.ui.zoom ?? 1) / 1.2);
|
|
244
274
|
applyTransform();
|
|
245
275
|
break;
|
|
276
|
+
}
|
|
277
|
+
default: {
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
246
280
|
}
|
|
247
281
|
return;
|
|
248
282
|
}
|
|
249
283
|
|
|
250
284
|
switch (e.key) {
|
|
251
285
|
case "Delete":
|
|
252
|
-
case "Backspace":
|
|
286
|
+
case "Backspace": {
|
|
253
287
|
if (tab?.session.selection && tab.session.selection.length >= 2) {
|
|
254
288
|
e.preventDefault();
|
|
255
289
|
const sel = tab.session.selection;
|
|
256
290
|
transactDoc(tab, (t) => mutateRemoveNode(t, sel));
|
|
257
291
|
}
|
|
258
292
|
break;
|
|
259
|
-
|
|
293
|
+
}
|
|
294
|
+
case "Escape": {
|
|
260
295
|
activeTab.value!.session.selection = null;
|
|
261
296
|
break;
|
|
262
|
-
|
|
297
|
+
}
|
|
298
|
+
case "Enter": {
|
|
263
299
|
if (tab?.session.selection && tab.session.selection.length >= 2) {
|
|
264
300
|
e.preventDefault();
|
|
265
301
|
const pp = parentElementPath(tab.session.selection) as JxPath;
|
|
@@ -272,29 +308,37 @@ export function initShortcuts(
|
|
|
272
308
|
enterEditOnPath(newPath);
|
|
273
309
|
}
|
|
274
310
|
break;
|
|
275
|
-
|
|
311
|
+
}
|
|
312
|
+
case "ArrowUp": {
|
|
276
313
|
e.preventDefault();
|
|
277
314
|
navigateSelection(-1);
|
|
278
315
|
break;
|
|
279
|
-
|
|
316
|
+
}
|
|
317
|
+
case "ArrowDown": {
|
|
280
318
|
e.preventDefault();
|
|
281
319
|
navigateSelection(1);
|
|
282
320
|
break;
|
|
283
|
-
|
|
321
|
+
}
|
|
322
|
+
case "ArrowLeft": {
|
|
284
323
|
e.preventDefault();
|
|
285
324
|
if (tab?.session.selection && tab.session.selection.length >= 2) {
|
|
286
325
|
activeTab.value!.session.selection = parentElementPath(tab.session.selection);
|
|
287
326
|
}
|
|
288
327
|
break;
|
|
289
|
-
|
|
328
|
+
}
|
|
329
|
+
case "ArrowRight": {
|
|
290
330
|
e.preventDefault();
|
|
291
331
|
if (tab?.session.selection) {
|
|
292
332
|
const node = getNodeAtPath(tab.doc.document, tab.session.selection);
|
|
293
|
-
if ((node
|
|
333
|
+
if (childList(node).length > 0) {
|
|
294
334
|
activeTab.value!.session.selection = [...tab.session.selection, "children", 0];
|
|
295
335
|
}
|
|
296
336
|
}
|
|
297
337
|
break;
|
|
338
|
+
}
|
|
339
|
+
default: {
|
|
340
|
+
break;
|
|
341
|
+
}
|
|
298
342
|
}
|
|
299
343
|
});
|
|
300
344
|
|
|
@@ -311,13 +355,15 @@ export function initShortcuts(
|
|
|
311
355
|
}
|
|
312
356
|
|
|
313
357
|
/** @param {number} [direction] */
|
|
314
|
-
function navigateSelection(direction
|
|
358
|
+
function navigateSelection(direction = -1) {
|
|
315
359
|
const tab = activeTab.value;
|
|
316
360
|
if (!tab?.session.selection) {
|
|
317
361
|
activeTab.value!.session.selection = [];
|
|
318
362
|
return;
|
|
319
363
|
}
|
|
320
|
-
if (tab.session.selection.length < 2)
|
|
364
|
+
if (tab.session.selection.length < 2) {
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
321
367
|
|
|
322
368
|
const parent = getNodeAtPath(
|
|
323
369
|
tab.doc.document,
|
|
@@ -325,7 +371,7 @@ function navigateSelection(direction: number = -1) {
|
|
|
325
371
|
);
|
|
326
372
|
const idx = childIndex(tab.session.selection) as number;
|
|
327
373
|
const newIdx = idx + direction;
|
|
328
|
-
if (
|
|
374
|
+
if (newIdx >= 0 && newIdx < childList(parent).length) {
|
|
329
375
|
activeTab.value!.session.selection = [
|
|
330
376
|
...(parentElementPath(tab.session.selection) as JxPath),
|
|
331
377
|
"children",
|
package/src/editor/slash-menu.ts
CHANGED
|
@@ -21,21 +21,21 @@ interface SlashCommand {
|
|
|
21
21
|
// ─── Commands ─────────────────────────────────────────────────────────────────
|
|
22
22
|
|
|
23
23
|
const SLASH_COMMANDS = [
|
|
24
|
-
{
|
|
25
|
-
{
|
|
26
|
-
{
|
|
27
|
-
{
|
|
28
|
-
{
|
|
29
|
-
{
|
|
30
|
-
{
|
|
31
|
-
{
|
|
32
|
-
{
|
|
33
|
-
{
|
|
34
|
-
{
|
|
35
|
-
{
|
|
36
|
-
{
|
|
37
|
-
{
|
|
38
|
-
{
|
|
24
|
+
{ description: "Large heading", label: "Heading 1", tag: "h1" },
|
|
25
|
+
{ description: "Medium heading", label: "Heading 2", tag: "h2" },
|
|
26
|
+
{ description: "Small heading", label: "Heading 3", tag: "h3" },
|
|
27
|
+
{ description: "Plain text", label: "Paragraph", tag: "p" },
|
|
28
|
+
{ description: "Unordered list", label: "Bulleted List", tag: "ul" },
|
|
29
|
+
{ description: "Numbered list", label: "Numbered List", tag: "ol" },
|
|
30
|
+
{ description: "Quote block", label: "Blockquote", tag: "blockquote" },
|
|
31
|
+
{ description: "Insert image", label: "Image", tag: "img" },
|
|
32
|
+
{ description: "Divider line", label: "Horizontal Rule", tag: "hr" },
|
|
33
|
+
{ description: "Button element", label: "Button", tag: "button" },
|
|
34
|
+
{ description: "Anchor link", label: "Link", tag: "a" },
|
|
35
|
+
{ description: "Preformatted code", label: "Code Block", tag: "pre" },
|
|
36
|
+
{ description: "Insert table", label: "Table", tag: "table" },
|
|
37
|
+
{ description: "Container", label: "Div", tag: "div" },
|
|
38
|
+
{ description: "Section container", label: "Section", tag: "section" },
|
|
39
39
|
];
|
|
40
40
|
|
|
41
41
|
// ─── State ────────────────────────────────────────────────────────────────────
|
|
@@ -96,7 +96,7 @@ export function showSlashMenu(
|
|
|
96
96
|
)
|
|
97
97
|
: source;
|
|
98
98
|
|
|
99
|
-
if (
|
|
99
|
+
if (filteredItems.length === 0 && !cbs.showFilter) {
|
|
100
100
|
dismissSlashMenu();
|
|
101
101
|
return;
|
|
102
102
|
}
|
|
@@ -107,7 +107,7 @@ export function showSlashMenu(
|
|
|
107
107
|
|
|
108
108
|
if (!open) {
|
|
109
109
|
open = true;
|
|
110
|
-
document.addEventListener("keydown", onKeydown, true); //
|
|
110
|
+
document.addEventListener("keydown", onKeydown, true); // Capture phase
|
|
111
111
|
requestAnimationFrame(() => {
|
|
112
112
|
document.addEventListener("mousedown", onOutsideClick, true);
|
|
113
113
|
});
|
|
@@ -115,13 +115,17 @@ export function showSlashMenu(
|
|
|
115
115
|
|
|
116
116
|
if (cbs.showFilter) {
|
|
117
117
|
requestAnimationFrame(() => {
|
|
118
|
-
if (_filterEl)
|
|
118
|
+
if (_filterEl) {
|
|
119
|
+
_filterEl.focus();
|
|
120
|
+
}
|
|
119
121
|
});
|
|
120
122
|
}
|
|
121
123
|
}
|
|
122
124
|
|
|
123
125
|
export function dismissSlashMenu() {
|
|
124
|
-
if (!open)
|
|
126
|
+
if (!open) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
125
129
|
open = false;
|
|
126
130
|
callbacks = null;
|
|
127
131
|
_anchorEl = null;
|
|
@@ -167,7 +171,7 @@ function render(anchorEl: HTMLElement, showFilter: boolean) {
|
|
|
167
171
|
/>`
|
|
168
172
|
: nothing}
|
|
169
173
|
<sp-menu style="min-width:220px">
|
|
170
|
-
${filteredItems.length
|
|
174
|
+
${filteredItems.length > 0
|
|
171
175
|
? filteredItems.map(
|
|
172
176
|
(cmd, i) => html`
|
|
173
177
|
<sp-menu-item
|
|
@@ -220,27 +224,34 @@ function onFilterInput(e: Event) {
|
|
|
220
224
|
: source;
|
|
221
225
|
|
|
222
226
|
activeIdx = 0;
|
|
223
|
-
if (_anchorEl)
|
|
227
|
+
if (_anchorEl) {
|
|
228
|
+
render(_anchorEl, true);
|
|
229
|
+
}
|
|
224
230
|
|
|
225
231
|
// Re-focus input after re-render
|
|
226
232
|
requestAnimationFrame(() => {
|
|
227
233
|
if (_filterEl && _filterEl !== document.activeElement) {
|
|
228
234
|
_filterEl.focus();
|
|
229
|
-
_filterEl.selectionStart = _filterEl.
|
|
235
|
+
_filterEl.selectionStart = _filterEl.value.length;
|
|
236
|
+
_filterEl.selectionEnd = _filterEl.value.length;
|
|
230
237
|
}
|
|
231
238
|
});
|
|
232
239
|
}
|
|
233
240
|
|
|
234
241
|
/** @param {KeyboardEvent} e */
|
|
235
242
|
function onKeydown(e: KeyboardEvent) {
|
|
236
|
-
if (!open)
|
|
243
|
+
if (!open) {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
237
246
|
|
|
238
247
|
const items = getHost().querySelectorAll("sp-menu-item:not([disabled])") as NodeListOf<Element>;
|
|
239
248
|
|
|
240
249
|
if (e.key === "ArrowDown") {
|
|
241
250
|
e.preventDefault();
|
|
242
251
|
e.stopPropagation();
|
|
243
|
-
if (
|
|
252
|
+
if (items.length === 0) {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
244
255
|
items[activeIdx]?.removeAttribute("focused");
|
|
245
256
|
activeIdx = (activeIdx + 1) % items.length;
|
|
246
257
|
items[activeIdx]?.setAttribute("focused", "");
|
|
@@ -248,7 +259,9 @@ function onKeydown(e: KeyboardEvent) {
|
|
|
248
259
|
} else if (e.key === "ArrowUp") {
|
|
249
260
|
e.preventDefault();
|
|
250
261
|
e.stopPropagation();
|
|
251
|
-
if (
|
|
262
|
+
if (items.length === 0) {
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
252
265
|
items[activeIdx]?.removeAttribute("focused");
|
|
253
266
|
activeIdx = (activeIdx - 1 + items.length) % items.length;
|
|
254
267
|
items[activeIdx]?.setAttribute("focused", "");
|
|
@@ -257,7 +270,9 @@ function onKeydown(e: KeyboardEvent) {
|
|
|
257
270
|
e.preventDefault();
|
|
258
271
|
e.stopPropagation();
|
|
259
272
|
const cmd = filteredItems[activeIdx];
|
|
260
|
-
if (cmd)
|
|
273
|
+
if (cmd) {
|
|
274
|
+
select(cmd);
|
|
275
|
+
}
|
|
261
276
|
} else if (e.key === "Escape") {
|
|
262
277
|
e.preventDefault();
|
|
263
278
|
e.stopPropagation();
|
package/src/files/components.ts
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
import { getPlatform } from "../platform";
|
|
4
4
|
import { projectState } from "../store";
|
|
5
|
+
import type { ComponentMeta } from "../types";
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
path
|
|
9
|
-
props?: { name: string; type?: string; [k: string]: unknown }[];
|
|
7
|
+
/** A discovered component: project components carry a file path; package components may not. */
|
|
8
|
+
export interface ComponentEntry extends Omit<ComponentMeta, "path"> {
|
|
9
|
+
path?: string;
|
|
10
10
|
source?: string;
|
|
11
11
|
package?: string;
|
|
12
12
|
modulePath?: string;
|
|
@@ -30,10 +30,12 @@ export async function loadComponentRegistry() {
|
|
|
30
30
|
* @param {string} toCompPath
|
|
31
31
|
*/
|
|
32
32
|
export function computeRelativePath(fromDocPath: string | null, toCompPath: string) {
|
|
33
|
-
if (!fromDocPath)
|
|
33
|
+
if (!fromDocPath) {
|
|
34
|
+
return `./${toCompPath}`;
|
|
35
|
+
}
|
|
34
36
|
const from = fromDocPath.replaceAll("\\", "/");
|
|
35
37
|
const to = toCompPath.replaceAll("\\", "/");
|
|
36
|
-
const fromDir = from.
|
|
38
|
+
const fromDir = from.includes("/") ? from.slice(0, from.lastIndexOf("/")) : "";
|
|
37
39
|
const fromParts = fromDir.split("/").filter(Boolean);
|
|
38
40
|
const toParts = to.split("/").filter(Boolean);
|
|
39
41
|
let common = 0;
|
|
@@ -42,7 +44,7 @@ export function computeRelativePath(fromDocPath: string | null, toCompPath: stri
|
|
|
42
44
|
common < toParts.length &&
|
|
43
45
|
fromParts[common] === toParts[common]
|
|
44
46
|
) {
|
|
45
|
-
common
|
|
47
|
+
common += 1;
|
|
46
48
|
}
|
|
47
49
|
const ups = fromParts.length - common;
|
|
48
50
|
const remaining = toParts.slice(common);
|
package/src/files/file-ops.ts
CHANGED
|
@@ -8,21 +8,24 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { locateDocument } from "../services/code-services";
|
|
11
|
+
import { errorMessage } from "@jxsuite/schema/parse";
|
|
11
12
|
import { statusMessage } from "../panels/statusbar";
|
|
12
13
|
import { getPlatform } from "../platform";
|
|
13
14
|
import { activeTab, openTab } from "../workspace/workspace";
|
|
14
15
|
import { isEditing, stopEditing } from "../editor/inline-edit";
|
|
15
16
|
import {
|
|
16
|
-
|
|
17
|
-
getFormats,
|
|
18
|
-
formatForPath,
|
|
17
|
+
defaultContentFormat,
|
|
19
18
|
formatByName,
|
|
19
|
+
formatForPath,
|
|
20
20
|
formatParse,
|
|
21
21
|
formatSerialize,
|
|
22
|
-
|
|
22
|
+
getFormats,
|
|
23
|
+
loadFormats,
|
|
24
|
+
noFormatError,
|
|
23
25
|
splitFormatDocument,
|
|
24
|
-
type StudioFormat,
|
|
25
26
|
} from "../format/format-host";
|
|
27
|
+
import type { StudioFormat } from "../format/format-host";
|
|
28
|
+
import type { Tab } from "../tabs/tab.js";
|
|
26
29
|
|
|
27
30
|
/**
|
|
28
31
|
* Parse a format-class source string into document + frontmatter + mode per the format's
|
|
@@ -48,10 +51,7 @@ export async function parseSourceForPath(path: string, source: string) {
|
|
|
48
51
|
await loadFormats();
|
|
49
52
|
const format = formatForPath(path);
|
|
50
53
|
if (!format || !format.capabilities.parse) {
|
|
51
|
-
throw
|
|
52
|
-
`No format class imported for "${path}" — add one to project.json imports ` +
|
|
53
|
-
`(e.g. "Markdown": "@jxsuite/parser/Markdown.class.json")`,
|
|
54
|
-
);
|
|
54
|
+
throw noFormatError(path);
|
|
55
55
|
}
|
|
56
56
|
const result = await parseFormatSource(format, source);
|
|
57
57
|
return { ...result, format };
|
|
@@ -64,14 +64,14 @@ export async function openFile() {
|
|
|
64
64
|
const formats = getFormats();
|
|
65
65
|
const pickerTypes = [
|
|
66
66
|
{
|
|
67
|
-
description: "Jx Component",
|
|
68
67
|
accept: { "application/json": [".json"] },
|
|
68
|
+
description: "Jx Component",
|
|
69
69
|
},
|
|
70
70
|
...formats
|
|
71
71
|
.filter((f) => f.capabilities.parse)
|
|
72
72
|
.map((f) => ({
|
|
73
|
-
description: f.name,
|
|
74
73
|
accept: { [f.mediaType ?? "text/plain"]: f.extensions },
|
|
74
|
+
description: f.name,
|
|
75
75
|
})),
|
|
76
76
|
];
|
|
77
77
|
const acceptExts = [".json", ...formats.flatMap((f) => f.extensions)].join(",");
|
|
@@ -86,23 +86,27 @@ export async function openFile() {
|
|
|
86
86
|
if (format) {
|
|
87
87
|
const { document, frontmatter } = await parseFormatSource(format, text);
|
|
88
88
|
openTab({
|
|
89
|
-
|
|
89
|
+
document,
|
|
90
90
|
documentPath,
|
|
91
91
|
fileHandle: handle,
|
|
92
|
-
document,
|
|
93
92
|
frontmatter,
|
|
93
|
+
id: name,
|
|
94
94
|
sourceFormat: format.name,
|
|
95
95
|
});
|
|
96
|
-
} else {
|
|
96
|
+
} else if (name.endsWith(".json")) {
|
|
97
97
|
const document = JSON.parse(text);
|
|
98
|
-
openTab({
|
|
98
|
+
openTab({ document, documentPath, fileHandle: handle, id: name });
|
|
99
|
+
} else {
|
|
100
|
+
throw noFormatError(name);
|
|
99
101
|
}
|
|
100
102
|
statusMessage(`Opened ${name}`);
|
|
101
103
|
};
|
|
102
104
|
|
|
103
105
|
if ("showOpenFilePicker" in window) {
|
|
104
106
|
const [handle] = await (
|
|
105
|
-
window as unknown as {
|
|
107
|
+
window as unknown as {
|
|
108
|
+
showOpenFilePicker: (options?: unknown) => Promise<FileSystemFileHandle[]>;
|
|
109
|
+
}
|
|
106
110
|
).showOpenFilePicker({ types: pickerTypes });
|
|
107
111
|
const file = await handle.getFile();
|
|
108
112
|
await handleSource(handle.name, await file.text(), handle);
|
|
@@ -111,23 +115,31 @@ export async function openFile() {
|
|
|
111
115
|
const input = document.createElement("input");
|
|
112
116
|
input.type = "file";
|
|
113
117
|
input.accept = acceptExts;
|
|
114
|
-
input.
|
|
118
|
+
input.addEventListener("change", async () => {
|
|
115
119
|
const file = input.files?.[0];
|
|
116
|
-
if (!file)
|
|
120
|
+
if (!file) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
117
123
|
await handleSource(file.name, await file.text());
|
|
118
|
-
};
|
|
124
|
+
});
|
|
119
125
|
input.click();
|
|
120
126
|
}
|
|
121
|
-
} catch (
|
|
122
|
-
if ((
|
|
127
|
+
} catch (error) {
|
|
128
|
+
if (!(error instanceof Error && error.name === "AbortError")) {
|
|
129
|
+
statusMessage(`Error: ${errorMessage(error)}`);
|
|
130
|
+
}
|
|
123
131
|
}
|
|
124
132
|
}
|
|
125
133
|
|
|
126
134
|
/** Save the current document back to its source location. */
|
|
127
135
|
export async function saveFile() {
|
|
128
|
-
if (isEditing())
|
|
136
|
+
if (isEditing()) {
|
|
137
|
+
stopEditing();
|
|
138
|
+
}
|
|
129
139
|
const tab = activeTab.value;
|
|
130
|
-
if (!tab)
|
|
140
|
+
if (!tab) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
131
143
|
try {
|
|
132
144
|
const output = await serializeDocument(tab);
|
|
133
145
|
|
|
@@ -153,13 +165,15 @@ export async function saveFile() {
|
|
|
153
165
|
} else {
|
|
154
166
|
statusMessage("No save target — use Export");
|
|
155
167
|
}
|
|
156
|
-
} catch (
|
|
157
|
-
if ((
|
|
168
|
+
} catch (error) {
|
|
169
|
+
if (!(error instanceof Error && error.name === "AbortError")) {
|
|
170
|
+
statusMessage(`Save error: ${errorMessage(error)}`);
|
|
171
|
+
}
|
|
158
172
|
}
|
|
159
173
|
}
|
|
160
174
|
|
|
161
175
|
/** The output format for a tab: its source format, or the default content format. */
|
|
162
|
-
function tabFormat(tab:
|
|
176
|
+
function tabFormat(tab: Tab): StudioFormat | undefined {
|
|
163
177
|
return (
|
|
164
178
|
formatByName(tab.doc.sourceFormat) ??
|
|
165
179
|
(tab.doc.mode === "content" ? defaultContentFormat() : undefined)
|
|
@@ -169,7 +183,9 @@ function tabFormat(tab: import("../tabs/tab.js").Tab): StudioFormat | undefined
|
|
|
169
183
|
/** Export the current document to a new location (Save As / download). */
|
|
170
184
|
export async function exportFile() {
|
|
171
185
|
const tab = activeTab.value;
|
|
172
|
-
if (!tab)
|
|
186
|
+
if (!tab) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
173
189
|
try {
|
|
174
190
|
await loadFormats();
|
|
175
191
|
const format = tabFormat(tab);
|
|
@@ -182,10 +198,12 @@ export async function exportFile() {
|
|
|
182
198
|
if ("showSaveFilePicker" in window) {
|
|
183
199
|
const suggestedName = tab.documentPath ? tab.documentPath.split("/").pop() : fallbackName;
|
|
184
200
|
const handle = await (
|
|
185
|
-
window as unknown as {
|
|
201
|
+
window as unknown as {
|
|
202
|
+
showSaveFilePicker: (options?: unknown) => Promise<FileSystemFileHandle>;
|
|
203
|
+
}
|
|
186
204
|
).showSaveFilePicker({
|
|
187
205
|
suggestedName,
|
|
188
|
-
types: [{
|
|
206
|
+
types: [{ accept: { [mimeType]: [ext] }, description }],
|
|
189
207
|
});
|
|
190
208
|
const writable = await handle.createWritable();
|
|
191
209
|
await writable.write(output);
|
|
@@ -204,8 +222,10 @@ export async function exportFile() {
|
|
|
204
222
|
tab.doc.dirty = false;
|
|
205
223
|
statusMessage("Downloaded");
|
|
206
224
|
}
|
|
207
|
-
} catch (
|
|
208
|
-
if ((
|
|
225
|
+
} catch (error) {
|
|
226
|
+
if (!(error instanceof Error && error.name === "AbortError")) {
|
|
227
|
+
statusMessage(`Export error: ${errorMessage(error)}`);
|
|
228
|
+
}
|
|
209
229
|
}
|
|
210
230
|
}
|
|
211
231
|
|
|
@@ -216,7 +236,7 @@ export async function exportFile() {
|
|
|
216
236
|
* @param {import("../tabs/tab.js").Tab} tab
|
|
217
237
|
* @returns {Promise<string>}
|
|
218
238
|
*/
|
|
219
|
-
export async function serializeDocument(tab:
|
|
239
|
+
export async function serializeDocument(tab: Tab): Promise<string> {
|
|
220
240
|
await loadFormats();
|
|
221
241
|
const sourceFormat = formatByName(tab.doc.sourceFormat);
|
|
222
242
|
if (sourceFormat?.capabilities.serialize) {
|
|
@@ -232,8 +252,8 @@ export async function serializeDocument(tab: import("../tabs/tab.js").Tab): Prom
|
|
|
232
252
|
const hasFrontmatter = Object.keys(fm).length > 0;
|
|
233
253
|
const fullDoc = { ...fm, ...tab.doc.document };
|
|
234
254
|
return formatSerialize(format.name, fullDoc, {
|
|
235
|
-
mode: "roundtrip",
|
|
236
255
|
frontmatter: hasFrontmatter,
|
|
256
|
+
mode: "roundtrip",
|
|
237
257
|
});
|
|
238
258
|
}
|
|
239
259
|
}
|