@jxsuite/studio 0.23.2 → 0.24.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 +2230 -714
- package/dist/studio.js.map +121 -104
- package/package.json +8 -7
- package/src/browse/{browse-modal.js → browse-modal.ts} +9 -10
- package/src/browse/{browse.js → browse.ts} +109 -84
- package/src/canvas/{canvas-diff.js → canvas-diff.ts} +42 -35
- package/src/canvas/{canvas-helpers.js → canvas-helpers.ts} +25 -19
- package/src/canvas/{canvas-live-render.js → canvas-live-render.ts} +53 -63
- package/src/canvas/{canvas-render.js → canvas-render.ts} +110 -121
- package/src/canvas/{canvas-utils.js → canvas-utils.ts} +48 -44
- package/src/canvas/{nested-site-style.js → nested-site-style.ts} +3 -3
- package/src/editor/{component-inline-edit.js → component-inline-edit.ts} +45 -33
- package/src/editor/{content-inline-edit.js → content-inline-edit.ts} +27 -45
- package/src/editor/{context-menu.js → context-menu.ts} +38 -36
- package/src/editor/{convert-targets.js → convert-targets.ts} +15 -12
- package/src/editor/{convert-to-component.js → convert-to-component.ts} +25 -26
- package/src/editor/{convert-to-repeater.js → convert-to-repeater.ts} +60 -40
- package/src/editor/{inline-edit.js → inline-edit.ts} +91 -55
- package/src/editor/{inline-format.js → inline-format.ts} +46 -51
- package/src/editor/{insertion-helper.js → insertion-helper.ts} +47 -63
- package/src/editor/{shortcuts.js → shortcuts.ts} +45 -31
- package/src/editor/{slash-menu.js → slash-menu.ts} +34 -35
- package/src/files/{components.js → components.ts} +12 -15
- package/src/files/{file-ops.js → file-ops.ts} +36 -29
- package/src/files/{files.js → files.ts} +102 -84
- package/src/github/{github-auth.js → github-auth.ts} +3 -3
- package/src/github/{github-publish.js → github-publish.ts} +20 -18
- package/src/markdown/{md-allowlist.js → md-allowlist.ts} +5 -2
- package/src/markdown/{md-convert.js → md-convert.ts} +146 -169
- package/src/new-project/{new-project-modal.js → new-project-modal.ts} +16 -18
- package/src/panels/{activity-bar.js → activity-bar.ts} +26 -34
- package/src/panels/{ai-panel.js → ai-panel.ts} +30 -30
- package/src/panels/{block-action-bar.js → block-action-bar.ts} +64 -73
- package/src/panels/{canvas-dnd.js → canvas-dnd.ts} +44 -49
- package/src/panels/{data-explorer.js → data-explorer.ts} +21 -15
- package/src/panels/dnd.ts +417 -0
- package/src/panels/{editors.js → editors.ts} +43 -50
- package/src/panels/{elements-panel.js → elements-panel.ts} +30 -27
- package/src/panels/{events-panel.js → events-panel.ts} +33 -25
- package/src/panels/{git-panel.js → git-panel.ts} +97 -83
- package/src/panels/{head-panel.js → head-panel.ts} +117 -139
- package/src/panels/{imports-panel.js → imports-panel.ts} +69 -89
- package/src/panels/{layers-panel.js → layers-panel.ts} +61 -57
- package/src/panels/{left-panel.js → left-panel.ts} +56 -63
- package/src/panels/{overlays.js → overlays.ts} +28 -36
- package/src/panels/{panel-events.js → panel-events.ts} +53 -51
- package/src/panels/{preview-render.js → preview-render.ts} +20 -15
- package/src/panels/{properties-panel.js → properties-panel.ts} +212 -246
- package/src/panels/{pseudo-preview.js → pseudo-preview.ts} +9 -8
- package/src/panels/{quick-search.js → quick-search.ts} +16 -17
- package/src/panels/{right-panel.js → right-panel.ts} +41 -59
- package/src/panels/{shared.js → shared.ts} +7 -5
- package/src/panels/{signals-panel.js → signals-panel.ts} +322 -362
- package/src/panels/{statusbar.js → statusbar.ts} +14 -14
- package/src/panels/{style-inputs.js → style-inputs.ts} +66 -48
- package/src/panels/{style-panel.js → style-panel.ts} +235 -290
- package/src/panels/{style-utils.js → style-utils.ts} +32 -31
- package/src/panels/{stylebook-layers-panel.js → stylebook-layers-panel.ts} +22 -14
- package/src/panels/{stylebook-panel.js → stylebook-panel.ts} +127 -140
- package/src/panels/{tab-strip.js → tab-strip.ts} +12 -14
- package/src/panels/{toolbar.js → toolbar.ts} +68 -70
- package/src/panels/{welcome-screen.js → welcome-screen.ts} +14 -16
- package/src/{platform.js → platform.ts} +5 -3
- package/src/platforms/{devserver.js → devserver.ts} +53 -44
- package/src/{recent-projects.js → recent-projects.ts} +17 -12
- package/src/{resize-edges.js → resize-edges.ts} +18 -13
- package/src/services/{cem-export.js → cem-export.ts} +14 -7
- package/src/services/{code-services.js → code-services.ts} +29 -20
- package/src/services/{monaco-setup.js → monaco-setup.ts} +2 -3
- package/src/settings/{content-types-editor.js → content-types-editor.ts} +88 -67
- package/src/settings/{css-vars-editor.js → css-vars-editor.ts} +78 -51
- package/src/settings/{defs-editor.js → defs-editor.ts} +70 -56
- package/src/settings/{general-settings.js → general-settings.ts} +29 -28
- package/src/settings/{head-editor.js → head-editor.ts} +37 -35
- package/src/settings/{schema-field-ui.js → schema-field-ui.ts} +86 -76
- package/src/settings/{settings-modal.js → settings-modal.ts} +14 -16
- package/src/{site-context.js → site-context.ts} +35 -32
- package/src/{state.js → state.ts} +95 -78
- package/src/{store.js → store.ts} +60 -63
- package/src/{studio.js → studio.ts} +117 -119
- package/src/tabs/tab.ts +196 -0
- package/src/tabs/{transact.js → transact.ts} +107 -60
- package/src/types.ts +185 -0
- package/src/ui/{button-group.js → button-group.ts} +21 -21
- package/src/ui/{color-selector.js → color-selector.ts} +44 -37
- package/src/ui/{expression-editor.js → expression-editor.ts} +65 -40
- package/src/ui/{field-row.js → field-row.ts} +19 -2
- package/src/ui/{icons.js → icons.ts} +2 -2
- package/src/ui/{layers.js → layers.ts} +33 -24
- package/src/ui/{media-picker.js → media-picker.ts} +13 -9
- package/src/ui/{panel-resize.js → panel-resize.ts} +9 -4
- package/src/ui/{spectrum.js → spectrum.ts} +4 -4
- package/src/ui/{unit-selector.js → unit-selector.ts} +16 -19
- package/src/ui/{value-selector.js → value-selector.ts} +28 -28
- package/src/ui/{widgets.js → widgets.ts} +62 -15
- package/src/utils/{canvas-media.js → canvas-media.ts} +29 -14
- package/src/utils/{edit-display.js → edit-display.ts} +23 -20
- package/src/utils/{google-fonts.js → google-fonts.ts} +14 -13
- package/src/utils/{inherited-style.js → inherited-style.ts} +15 -10
- package/src/utils/{studio-utils.js → studio-utils.ts} +35 -23
- package/src/{view.js → view.ts} +53 -55
- package/src/workspace/{workspace.js → workspace.ts} +44 -33
- package/src/panels/dnd.js +0 -426
- package/src/tabs/tab.js +0 -192
- /package/src/{reactivity.js → reactivity.ts} +0 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
1
2
|
/**
|
|
2
3
|
* Component inline edit — extracted from studio.js (Phase 4j). Manages plaintext-only editing on
|
|
3
4
|
* canvas elements in design mode, with slash menu delegation for block insertion.
|
|
@@ -11,28 +12,43 @@ import {
|
|
|
11
12
|
childIndex,
|
|
12
13
|
canvasPanels,
|
|
13
14
|
elToPath,
|
|
14
|
-
} from "../store
|
|
15
|
-
import { activeTab } from "../workspace/workspace
|
|
15
|
+
} from "../store";
|
|
16
|
+
import { activeTab } from "../workspace/workspace";
|
|
17
|
+
import type { JxPath } from "../state";
|
|
16
18
|
import {
|
|
17
19
|
transactDoc,
|
|
18
20
|
mutateRemoveNode,
|
|
19
21
|
mutateInsertNode,
|
|
20
22
|
mutateUpdateProperty,
|
|
21
|
-
} from "../tabs/transact
|
|
22
|
-
import { view } from "../view
|
|
23
|
-
import { isSlashMenuOpen, showSlashMenu, dismissSlashMenu } from "./slash-menu
|
|
24
|
-
import { renderBlockActionBar } from "../panels/block-action-bar
|
|
25
|
-
import { defaultDef } from "../panels/shared
|
|
23
|
+
} from "../tabs/transact";
|
|
24
|
+
import { view } from "../view";
|
|
25
|
+
import { isSlashMenuOpen, showSlashMenu, dismissSlashMenu } from "./slash-menu";
|
|
26
|
+
import { renderBlockActionBar } from "../panels/block-action-bar";
|
|
27
|
+
import { defaultDef } from "../panels/shared";
|
|
26
28
|
|
|
27
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* @type {{
|
|
31
|
+
* findCanvasElement: (
|
|
32
|
+
* path: import("../state").JxPath,
|
|
33
|
+
* canvasEl: HTMLElement,
|
|
34
|
+
* ) => HTMLElement | null;
|
|
35
|
+
* } | null}
|
|
36
|
+
*/
|
|
28
37
|
let _ctx = null;
|
|
29
38
|
|
|
30
39
|
/**
|
|
31
40
|
* Initialize the component inline edit module.
|
|
32
41
|
*
|
|
33
|
-
* @param {{
|
|
42
|
+
* @param {{
|
|
43
|
+
* findCanvasElement: (
|
|
44
|
+
* path: import("../state").JxPath,
|
|
45
|
+
* canvasEl: HTMLElement,
|
|
46
|
+
* ) => HTMLElement | null;
|
|
47
|
+
* }} ctx
|
|
34
48
|
*/
|
|
35
|
-
export function initComponentInlineEdit(ctx
|
|
49
|
+
export function initComponentInlineEdit(ctx: {
|
|
50
|
+
findCanvasElement: (path: JxPath, canvasEl: HTMLElement) => HTMLElement | null;
|
|
51
|
+
}) {
|
|
36
52
|
_ctx = ctx;
|
|
37
53
|
}
|
|
38
54
|
|
|
@@ -42,12 +58,12 @@ export function initComponentInlineEdit(ctx) {
|
|
|
42
58
|
* @param {HTMLElement} el
|
|
43
59
|
* @param {JxPath} path
|
|
44
60
|
*/
|
|
45
|
-
export function enterComponentInlineEdit(el, path) {
|
|
61
|
+
export function enterComponentInlineEdit(el: HTMLElement, path: JxPath) {
|
|
46
62
|
if (view.componentInlineEdit && view.componentInlineEdit.el === el) {
|
|
47
63
|
return;
|
|
48
64
|
}
|
|
49
65
|
|
|
50
|
-
const node = getNodeAtPath(activeTab.value
|
|
66
|
+
const node = getNodeAtPath(activeTab.value!.doc.document, path);
|
|
51
67
|
if (!node) return;
|
|
52
68
|
|
|
53
69
|
const tc = node.textContent;
|
|
@@ -59,9 +75,7 @@ export function enterComponentInlineEdit(el, path) {
|
|
|
59
75
|
if (voids.has(node.tagName || "")) return;
|
|
60
76
|
|
|
61
77
|
for (const p of canvasPanels) {
|
|
62
|
-
const boxes =
|
|
63
|
-
p.overlay.querySelectorAll(".overlay-box")
|
|
64
|
-
);
|
|
78
|
+
const boxes = p.overlay.querySelectorAll(".overlay-box") as NodeListOf<HTMLElement>;
|
|
65
79
|
for (const box of boxes) {
|
|
66
80
|
box.style.border = "none";
|
|
67
81
|
}
|
|
@@ -96,22 +110,20 @@ export function enterComponentInlineEdit(el, path) {
|
|
|
96
110
|
el.addEventListener("keydown", componentInlineKeydown);
|
|
97
111
|
el.addEventListener("input", componentInlineInput);
|
|
98
112
|
|
|
99
|
-
const outsideHandler = (
|
|
113
|
+
const outsideHandler = (evt: MouseEvent) => {
|
|
100
114
|
if (!view.componentInlineEdit) {
|
|
101
115
|
document.removeEventListener("mousedown", outsideHandler, true);
|
|
102
116
|
return;
|
|
103
117
|
}
|
|
104
|
-
if (view.componentInlineEdit.el.contains(
|
|
118
|
+
if (view.componentInlineEdit.el.contains(evt.target as Node)) return;
|
|
105
119
|
if (isSlashMenuOpen()) return;
|
|
106
|
-
if (view.blockActionBarEl && view.blockActionBarEl.contains(
|
|
107
|
-
return;
|
|
120
|
+
if (view.blockActionBarEl && view.blockActionBarEl.contains(evt.target as Node)) return;
|
|
108
121
|
document.removeEventListener("mousedown", outsideHandler, true);
|
|
109
122
|
|
|
110
|
-
|
|
111
|
-
let hitPath = null,
|
|
123
|
+
let hitPath: (string | number)[] | null = null,
|
|
112
124
|
hitMedia = null;
|
|
113
125
|
for (const p of canvasPanels) {
|
|
114
|
-
const els =
|
|
126
|
+
const els = p.canvas.querySelectorAll("*") as NodeListOf<HTMLElement>;
|
|
115
127
|
for (const el of els) el.style.pointerEvents = "auto";
|
|
116
128
|
p.overlayClk.style.display = "none";
|
|
117
129
|
const found = document.elementsFromPoint(evt.clientX, evt.clientY);
|
|
@@ -141,12 +153,12 @@ export function enterComponentInlineEdit(el, path) {
|
|
|
141
153
|
let hp = hitPath;
|
|
142
154
|
const media = hitMedia === "base" ? null : (hitMedia ?? null);
|
|
143
155
|
updateUi("pendingInlineEdit", { path: hp, mediaName: hitMedia });
|
|
144
|
-
activeTab.value
|
|
156
|
+
activeTab.value!.session.ui.activeMedia = media;
|
|
145
157
|
if (isEmpty && pPath) {
|
|
146
158
|
transactDoc(activeTab.value, (t) => {
|
|
147
159
|
mutateRemoveNode(t, editPath);
|
|
148
|
-
const removedIdx =
|
|
149
|
-
const hitIdx =
|
|
160
|
+
const removedIdx = childIndex(editPath) as number;
|
|
161
|
+
const hitIdx = childIndex(hp) as number;
|
|
150
162
|
const hitParent = parentElementPath(hp);
|
|
151
163
|
if (
|
|
152
164
|
hitParent &&
|
|
@@ -165,7 +177,7 @@ export function enterComponentInlineEdit(el, path) {
|
|
|
165
177
|
t.session.selection = hp;
|
|
166
178
|
});
|
|
167
179
|
} else {
|
|
168
|
-
activeTab.value
|
|
180
|
+
activeTab.value!.session.selection = hp;
|
|
169
181
|
}
|
|
170
182
|
} else {
|
|
171
183
|
if (isEmpty && pPath) {
|
|
@@ -187,7 +199,7 @@ export function enterComponentInlineEdit(el, path) {
|
|
|
187
199
|
}
|
|
188
200
|
|
|
189
201
|
/** @param {KeyboardEvent} e */
|
|
190
|
-
function componentInlineKeydown(e) {
|
|
202
|
+
function componentInlineKeydown(e: KeyboardEvent) {
|
|
191
203
|
if (isSlashMenuOpen()) {
|
|
192
204
|
if (["ArrowDown", "ArrowUp", "Enter", "Escape"].includes(e.key)) return;
|
|
193
205
|
}
|
|
@@ -206,7 +218,7 @@ function splitParagraph() {
|
|
|
206
218
|
if (!view.componentInlineEdit) return;
|
|
207
219
|
const { el, path, mediaName } = view.componentInlineEdit;
|
|
208
220
|
|
|
209
|
-
const sel =
|
|
221
|
+
const sel = el.ownerDocument.defaultView?.getSelection() as Selection | null;
|
|
210
222
|
const fullText = el.textContent || "";
|
|
211
223
|
let offset = fullText.length;
|
|
212
224
|
if (sel && sel.rangeCount) {
|
|
@@ -221,8 +233,8 @@ function splitParagraph() {
|
|
|
221
233
|
const textAfter = fullText.slice(offset);
|
|
222
234
|
|
|
223
235
|
const tag = "p";
|
|
224
|
-
const pPath =
|
|
225
|
-
const idx =
|
|
236
|
+
const pPath = parentElementPath(path) as JxPath;
|
|
237
|
+
const idx = childIndex(path) as number;
|
|
226
238
|
if (!pPath) return;
|
|
227
239
|
|
|
228
240
|
const newDef = { tagName: tag, textContent: textAfter };
|
|
@@ -268,7 +280,7 @@ function cancelComponentInlineEdit() {
|
|
|
268
280
|
}
|
|
269
281
|
|
|
270
282
|
/** @param {HTMLElement} el */
|
|
271
|
-
function cleanupComponentInlineEdit(el) {
|
|
283
|
+
function cleanupComponentInlineEdit(el: HTMLElement) {
|
|
272
284
|
el.removeEventListener("keydown", componentInlineKeydown);
|
|
273
285
|
el.removeEventListener("input", componentInlineInput);
|
|
274
286
|
dismissSlashMenu();
|
|
@@ -306,11 +318,11 @@ function componentInlineInput() {
|
|
|
306
318
|
}
|
|
307
319
|
|
|
308
320
|
/** @param {{ tag: string; label: string; description: string }} cmd */
|
|
309
|
-
function handleComponentSlashSelect(cmd) {
|
|
321
|
+
function handleComponentSlashSelect(cmd: { tag: string; label: string; description: string }) {
|
|
310
322
|
if (!view.componentInlineEdit) return;
|
|
311
323
|
const { el, path, mediaName } = view.componentInlineEdit;
|
|
312
324
|
const pPath = parentElementPath(path);
|
|
313
|
-
const idx =
|
|
325
|
+
const idx = childIndex(path) as number;
|
|
314
326
|
if (!pPath) return;
|
|
315
327
|
|
|
316
328
|
cleanupComponentInlineEdit(el);
|
|
@@ -1,27 +1,18 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
} from "../store.js";
|
|
17
|
-
import { activeTab } from "../workspace/workspace.js";
|
|
18
|
-
import { transactDoc, mutateInsertNode, mutateUpdateProperty } from "../tabs/transact.js";
|
|
19
|
-
import { view } from "../view.js";
|
|
20
|
-
import { startEditing, isEditableBlock } from "./inline-edit.js";
|
|
21
|
-
import { restoreTemplateExpressions } from "../utils/edit-display.js";
|
|
22
|
-
import { renderBlockActionBar } from "../panels/block-action-bar.js";
|
|
23
|
-
import { defaultDef } from "../panels/shared.js";
|
|
24
|
-
import { findCanvasElement, getActivePanel } from "../canvas/canvas-helpers.js";
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
import { renderOnly, getNodeAtPath, parentElementPath, childIndex, canvasPanels } from "../store";
|
|
3
|
+
import { activeTab } from "../workspace/workspace";
|
|
4
|
+
import { transactDoc, mutateInsertNode, mutateUpdateProperty } from "../tabs/transact";
|
|
5
|
+
import { view } from "../view";
|
|
6
|
+
import { startEditing, isEditableBlock } from "./inline-edit";
|
|
7
|
+
import { restoreTemplateExpressions } from "../utils/edit-display";
|
|
8
|
+
import { renderBlockActionBar } from "../panels/block-action-bar";
|
|
9
|
+
import { defaultDef } from "../panels/shared";
|
|
10
|
+
import { findCanvasElement, getActivePanel } from "../canvas/canvas-helpers";
|
|
11
|
+
|
|
12
|
+
import type { JxContentResult } from "./inline-edit";
|
|
13
|
+
import type { SlashCommand } from "./inline-edit";
|
|
14
|
+
import type { JxPath } from "../state";
|
|
15
|
+
import type { JxMutableNode } from "@jxsuite/schema/types";
|
|
25
16
|
|
|
26
17
|
/**
|
|
27
18
|
* Enter rich-text inline editing on a canvas element (edit/content mode).
|
|
@@ -29,7 +20,7 @@ import { findCanvasElement, getActivePanel } from "../canvas/canvas-helpers.js";
|
|
|
29
20
|
* @param {HTMLElement} el
|
|
30
21
|
* @param {JxPath} path
|
|
31
22
|
*/
|
|
32
|
-
export function enterInlineEdit(el, path) {
|
|
23
|
+
export function enterInlineEdit(el: HTMLElement, path: JxPath) {
|
|
33
24
|
// Restore raw template expressions before editing.
|
|
34
25
|
// prepareForEditMode renders ${expr} as ❪ expr ❫ for display;
|
|
35
26
|
// revert so the user edits the real syntax and commits it back intact.
|
|
@@ -43,11 +34,11 @@ export function enterInlineEdit(el, path) {
|
|
|
43
34
|
|
|
44
35
|
startEditing(el, path, {
|
|
45
36
|
onCommit(
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
37
|
+
commitPath: JxPath,
|
|
38
|
+
children: (JxMutableNode | string)[] | null,
|
|
39
|
+
textContent: string | null,
|
|
49
40
|
) {
|
|
50
|
-
const node = getNodeAtPath(activeTab.value
|
|
41
|
+
const node = getNodeAtPath(activeTab.value!.doc.document, commitPath);
|
|
51
42
|
if (children) {
|
|
52
43
|
if (node && JSON.stringify(node.children) === JSON.stringify(children)) return;
|
|
53
44
|
transactDoc(activeTab.value, (t) => {
|
|
@@ -63,18 +54,13 @@ export function enterInlineEdit(el, path) {
|
|
|
63
54
|
}
|
|
64
55
|
},
|
|
65
56
|
|
|
66
|
-
onSplit(
|
|
67
|
-
/** @type {JxPath} */ splitPath,
|
|
68
|
-
/** @type {JxContentResult} */ before,
|
|
69
|
-
/** @type {JxContentResult} */ after,
|
|
70
|
-
) {
|
|
57
|
+
onSplit(splitPath: JxPath, before: JxContentResult, after: JxContentResult) {
|
|
71
58
|
const tag = "p";
|
|
72
59
|
|
|
73
60
|
// Insert new element after with "after" content
|
|
74
|
-
const parentPath =
|
|
75
|
-
const idx =
|
|
76
|
-
|
|
77
|
-
const newNode = { tagName: tag };
|
|
61
|
+
const parentPath = parentElementPath(splitPath) as JxPath;
|
|
62
|
+
const idx = childIndex(splitPath) as number;
|
|
63
|
+
const newNode: JxMutableNode = { tagName: tag };
|
|
78
64
|
if (after.textContent != null) {
|
|
79
65
|
newNode.textContent = after.textContent;
|
|
80
66
|
} else if (after.children) {
|
|
@@ -116,11 +102,7 @@ export function enterInlineEdit(el, path) {
|
|
|
116
102
|
});
|
|
117
103
|
},
|
|
118
104
|
|
|
119
|
-
onInsert(
|
|
120
|
-
/** @type {JxPath} */ afterPath,
|
|
121
|
-
/** @type {SlashCommand} */ cmd,
|
|
122
|
-
/** @type {JxContentResult | undefined} */ commitData,
|
|
123
|
-
) {
|
|
105
|
+
onInsert(afterPath: JxPath, cmd: SlashCommand, commitData: JxContentResult | undefined) {
|
|
124
106
|
const isEmpty =
|
|
125
107
|
!commitData ||
|
|
126
108
|
(commitData.textContent != null && commitData.textContent.trim() === "") ||
|
|
@@ -160,8 +142,8 @@ export function enterInlineEdit(el, path) {
|
|
|
160
142
|
}
|
|
161
143
|
|
|
162
144
|
const elementDef = defaultDef(cmd.tag);
|
|
163
|
-
const parentPath =
|
|
164
|
-
const idx =
|
|
145
|
+
const parentPath = parentElementPath(afterPath) as JxPath;
|
|
146
|
+
const idx = childIndex(afterPath) as number;
|
|
165
147
|
const newPath = [...parentPath, "children", idx + 1];
|
|
166
148
|
|
|
167
149
|
// Apply pending commit from inline edit first (batched to avoid double render)
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
1
2
|
// ─── Clipboard & Context Menu ─────────────────────────────────────────────────
|
|
2
3
|
import { html } from "lit-html";
|
|
3
4
|
import { ref } from "lit-html/directives/ref.js";
|
|
4
5
|
import { htmlToJx } from "@jxsuite/parser/html-to-jx";
|
|
5
|
-
import { getNodeAtPath, parentElementPath, childIndex } from "../store
|
|
6
|
-
import { activeTab, workspace } from "../workspace/workspace
|
|
6
|
+
import { getNodeAtPath, parentElementPath, childIndex } from "../store";
|
|
7
|
+
import { activeTab, workspace } from "../workspace/workspace";
|
|
7
8
|
import {
|
|
8
9
|
transactDoc,
|
|
9
10
|
mutateInsertNode,
|
|
@@ -11,28 +12,25 @@ import {
|
|
|
11
12
|
mutateDuplicateNode,
|
|
12
13
|
mutateWrapNode,
|
|
13
14
|
mutateReplaceStyle,
|
|
14
|
-
} from "../tabs/transact
|
|
15
|
-
import { statusMessage } from "../panels/statusbar
|
|
16
|
-
import { convertToComponent } from "./convert-to-component
|
|
17
|
-
import { convertToRepeater } from "./convert-to-repeater
|
|
18
|
-
import { componentRegistry } from "../files/components
|
|
19
|
-
import { renderPopover } from "../ui/layers
|
|
20
|
-
import { startLayerTitleEdit } from "../panels/layers-panel
|
|
15
|
+
} from "../tabs/transact";
|
|
16
|
+
import { statusMessage } from "../panels/statusbar";
|
|
17
|
+
import { convertToComponent } from "./convert-to-component";
|
|
18
|
+
import { convertToRepeater } from "./convert-to-repeater";
|
|
19
|
+
import { componentRegistry } from "../files/components";
|
|
20
|
+
import { renderPopover } from "../ui/layers";
|
|
21
|
+
import { startLayerTitleEdit } from "../panels/layers-panel";
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
*
|
|
27
|
-
* @typedef {JxMutableNode} JxNode
|
|
28
|
-
*/
|
|
23
|
+
import type { JxPath } from "../state";
|
|
24
|
+
import type { JxMutableNode } from "@jxsuite/schema/types";
|
|
25
|
+
|
|
26
|
+
type JxNode = JxMutableNode;
|
|
29
27
|
|
|
30
28
|
// ─── Clipboard helpers ───────────────────────────────────────────────────────
|
|
31
29
|
|
|
32
30
|
const JX_MIME = "web application/jx+json";
|
|
33
31
|
|
|
34
32
|
/** @param {JxNode | string} node */
|
|
35
|
-
function nodeToHtml(node) {
|
|
33
|
+
function nodeToHtml(node: JxNode | string) {
|
|
36
34
|
if (typeof node === "string") return node;
|
|
37
35
|
const tag = node.tagName || "div";
|
|
38
36
|
let attrs = "";
|
|
@@ -61,7 +59,7 @@ function nodeToHtml(node) {
|
|
|
61
59
|
*
|
|
62
60
|
* @param {object} json
|
|
63
61
|
*/
|
|
64
|
-
async function writeToClipboard(json) {
|
|
62
|
+
async function writeToClipboard(json: Record<string, unknown>) {
|
|
65
63
|
workspace.clipboard = json;
|
|
66
64
|
try {
|
|
67
65
|
await navigator.clipboard.write([
|
|
@@ -98,9 +96,9 @@ async function readFromClipboard() {
|
|
|
98
96
|
const blob = await item.getType("text/html");
|
|
99
97
|
const htmlStr = await blob.text();
|
|
100
98
|
const nodes = htmlToJx(htmlStr);
|
|
101
|
-
const jxNodes =
|
|
102
|
-
|
|
103
|
-
);
|
|
99
|
+
const jxNodes = nodes.map((n) =>
|
|
100
|
+
typeof n === "string" ? { tagName: "p", textContent: n } : n,
|
|
101
|
+
) as JxNode[];
|
|
104
102
|
if (jxNodes.length > 0) return jxNodes;
|
|
105
103
|
}
|
|
106
104
|
if (item.types.includes("text/plain")) {
|
|
@@ -161,8 +159,8 @@ export async function pasteNode() {
|
|
|
161
159
|
if (!parent) return;
|
|
162
160
|
|
|
163
161
|
if (tab.session.selection && tab.session.selection.length >= 2) {
|
|
164
|
-
const pp =
|
|
165
|
-
const idx =
|
|
162
|
+
const pp = parentElementPath(tab.session.selection) as JxPath;
|
|
163
|
+
const idx = childIndex(tab.session.selection) as number;
|
|
166
164
|
transactDoc(tab, (t) => {
|
|
167
165
|
for (let i = 0; i < nodes.length; i++) {
|
|
168
166
|
mutateInsertNode(t, pp, idx + 1 + i, nodes[i]);
|
|
@@ -193,15 +191,14 @@ export function pasteStyles() {
|
|
|
193
191
|
const tab = activeTab.value;
|
|
194
192
|
if (!tab?.session.selection) return;
|
|
195
193
|
const style = JSON.parse(JSON.stringify(workspace.styleClipboard));
|
|
196
|
-
const sel =
|
|
194
|
+
const sel = tab.session.selection as JxPath;
|
|
197
195
|
transactDoc(tab, (t) => mutateReplaceStyle(t, sel, style));
|
|
198
196
|
statusMessage("Styles pasted");
|
|
199
197
|
}
|
|
200
198
|
|
|
201
199
|
// ─── Context menu ─────────────────────────────────────────────────────────────
|
|
202
200
|
|
|
203
|
-
|
|
204
|
-
let _ctxHandle = null;
|
|
201
|
+
let _ctxHandle: ReturnType<typeof renderPopover> | null = null;
|
|
205
202
|
|
|
206
203
|
/** Dismiss the context menu if open. */
|
|
207
204
|
export function dismissContextMenu() {
|
|
@@ -216,12 +213,17 @@ export function dismissContextMenu() {
|
|
|
216
213
|
* @param {JxPath} path
|
|
217
214
|
* @param {{ onEditComponent?: (path: string) => void; rerender?: () => void }} [opts]
|
|
218
215
|
*/
|
|
219
|
-
export function showContextMenu(
|
|
216
|
+
export function showContextMenu(
|
|
217
|
+
e: MouseEvent,
|
|
218
|
+
path: JxPath,
|
|
219
|
+
opts: { onEditComponent?: (path: string) => void; rerender?: () => void } = {},
|
|
220
|
+
) {
|
|
220
221
|
e.preventDefault();
|
|
221
222
|
dismissContextMenu();
|
|
222
223
|
|
|
223
224
|
const tab = activeTab.value;
|
|
224
|
-
|
|
225
|
+
if (!tab) return;
|
|
226
|
+
const node = getNodeAtPath(tab.doc.document, path);
|
|
225
227
|
if (!node) return;
|
|
226
228
|
|
|
227
229
|
// Select the node
|
|
@@ -261,8 +263,8 @@ export function showContextMenu(e, path, opts = {}) {
|
|
|
261
263
|
items.push({
|
|
262
264
|
label: "Insert before",
|
|
263
265
|
action: () => {
|
|
264
|
-
const pp =
|
|
265
|
-
const idx =
|
|
266
|
+
const pp = parentElementPath(path) as JxPath;
|
|
267
|
+
const idx = childIndex(path) as number;
|
|
266
268
|
transactDoc(activeTab.value, (t) =>
|
|
267
269
|
mutateInsertNode(t, pp, idx, { tagName: "p", children: [] }),
|
|
268
270
|
);
|
|
@@ -271,8 +273,8 @@ export function showContextMenu(e, path, opts = {}) {
|
|
|
271
273
|
items.push({
|
|
272
274
|
label: "Insert after",
|
|
273
275
|
action: () => {
|
|
274
|
-
const pp =
|
|
275
|
-
const idx =
|
|
276
|
+
const pp = parentElementPath(path) as JxPath;
|
|
277
|
+
const idx = childIndex(path) as number;
|
|
276
278
|
transactDoc(activeTab.value, (t) =>
|
|
277
279
|
mutateInsertNode(t, pp, idx + 1, { tagName: "p", children: [] }),
|
|
278
280
|
);
|
|
@@ -309,7 +311,7 @@ export function showContextMenu(e, path, opts = {}) {
|
|
|
309
311
|
);
|
|
310
312
|
items.push({
|
|
311
313
|
label: "Edit Component",
|
|
312
|
-
action: () => opts.onEditComponent?.(
|
|
314
|
+
action: () => opts.onEditComponent?.(comp?.path as string),
|
|
313
315
|
});
|
|
314
316
|
} else if (!isComponent) {
|
|
315
317
|
items.push({
|
|
@@ -346,8 +348,8 @@ export function showContextMenu(e, path, opts = {}) {
|
|
|
346
348
|
action: async () => {
|
|
347
349
|
const nodes = await readFromClipboard();
|
|
348
350
|
if (!nodes || nodes.length === 0) return;
|
|
349
|
-
const pp =
|
|
350
|
-
const idx =
|
|
351
|
+
const pp = parentElementPath(path) as JxPath;
|
|
352
|
+
const idx = childIndex(path) as number;
|
|
351
353
|
transactDoc(activeTab.value, (t) => {
|
|
352
354
|
for (let i = 0; i < nodes.length; i++) {
|
|
353
355
|
mutateInsertNode(t, pp, idx + 1 + i, nodes[i]);
|
|
@@ -368,7 +370,7 @@ export function showContextMenu(e, path, opts = {}) {
|
|
|
368
370
|
${ref((el) => {
|
|
369
371
|
if (!el) return;
|
|
370
372
|
requestAnimationFrame(() => {
|
|
371
|
-
const popover =
|
|
373
|
+
const popover = el as HTMLElement;
|
|
372
374
|
const menuRect = popover.getBoundingClientRect();
|
|
373
375
|
if (x + menuRect.width > window.innerWidth) x = window.innerWidth - menuRect.width - 4;
|
|
374
376
|
if (y + menuRect.height > window.innerHeight)
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import elementsMeta from "../../data/elements-meta.json";
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
export interface SlashCommand {
|
|
4
|
+
label: string;
|
|
5
|
+
tag: string;
|
|
6
|
+
description: string;
|
|
7
|
+
}
|
|
4
8
|
|
|
5
|
-
const TAG_LABELS =
|
|
9
|
+
const TAG_LABELS = {
|
|
6
10
|
p: "Paragraph",
|
|
7
11
|
h1: "Heading 1",
|
|
8
12
|
h2: "Heading 2",
|
|
@@ -22,9 +26,9 @@ const TAG_LABELS = /** @type {Record<string, string>} */ ({
|
|
|
22
26
|
search: "Search",
|
|
23
27
|
ul: "Bulleted List",
|
|
24
28
|
ol: "Numbered List",
|
|
25
|
-
}
|
|
29
|
+
} as Record<string, string>;
|
|
26
30
|
|
|
27
|
-
const groups =
|
|
31
|
+
const groups = (elementsMeta.$convertGroups || {}) as Record<string, string[]>;
|
|
28
32
|
|
|
29
33
|
/**
|
|
30
34
|
* Get the list of tags the current element can be converted to.
|
|
@@ -33,19 +37,18 @@ const groups = /** @type {Record<string, string[]>} */ (elementsMeta.$convertGro
|
|
|
33
37
|
* @param {boolean} isEmpty
|
|
34
38
|
* @returns {SlashCommand[]}
|
|
35
39
|
*/
|
|
36
|
-
export function getConvertTargets(currentTag, isEmpty) {
|
|
37
|
-
const def =
|
|
38
|
-
|
|
39
|
-
|
|
40
|
+
export function getConvertTargets(currentTag: string, isEmpty: boolean) {
|
|
41
|
+
const def = (elementsMeta.$defs as Record<string, Record<string, unknown>>)?.[currentTag] as
|
|
42
|
+
| Record<string, unknown>
|
|
43
|
+
| undefined;
|
|
40
44
|
if (!def?.$convertTo) return [];
|
|
41
45
|
|
|
42
46
|
const groupNames =
|
|
43
47
|
isEmpty && def.$convertToWhenEmpty
|
|
44
|
-
?
|
|
45
|
-
: [
|
|
48
|
+
? (def.$convertToWhenEmpty as string[])
|
|
49
|
+
: [def.$convertTo as string];
|
|
46
50
|
|
|
47
|
-
|
|
48
|
-
const tags = new Set();
|
|
51
|
+
const tags: Set<string> = new Set();
|
|
49
52
|
for (const name of groupNames) {
|
|
50
53
|
for (const tag of groups[name] || []) {
|
|
51
54
|
if (tag !== currentTag) tags.add(tag);
|
|
@@ -1,17 +1,16 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
1
2
|
// ─── Convert to Component ─────────────────────────────────────────────────────
|
|
2
3
|
import { html, render as litRender } from "lit-html";
|
|
3
4
|
import { ref } from "lit-html/directives/ref.js";
|
|
4
|
-
import { getNodeAtPath, parentElementPath, childIndex } from "../store
|
|
5
|
-
import { activeTab } from "../workspace/workspace
|
|
6
|
-
import { transact } from "../tabs/transact
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
import {
|
|
13
|
-
import { statusMessage } from "../panels/statusbar.js";
|
|
14
|
-
import { showDialog } from "../ui/layers.js";
|
|
5
|
+
import { getNodeAtPath, parentElementPath, childIndex } from "../store";
|
|
6
|
+
import { activeTab } from "../workspace/workspace";
|
|
7
|
+
import { transact } from "../tabs/transact";
|
|
8
|
+
import { computeRelativePath, loadComponentRegistry, componentRegistry } from "../files/components";
|
|
9
|
+
import { getPlatform } from "../platform";
|
|
10
|
+
import { statusMessage } from "../panels/statusbar";
|
|
11
|
+
import { showDialog } from "../ui/layers";
|
|
12
|
+
|
|
13
|
+
import type { JxMutableNode } from "@jxsuite/schema/types";
|
|
15
14
|
|
|
16
15
|
const VALID_NAME = /^[a-z][a-z0-9]*(-[a-z0-9]+)+$/;
|
|
17
16
|
|
|
@@ -40,7 +39,7 @@ export async function convertToComponent() {
|
|
|
40
39
|
transact(tab, (doc) => {
|
|
41
40
|
// Navigate to parent's children array and replace the node
|
|
42
41
|
const pp = parentElementPath(selectionPath) ?? [];
|
|
43
|
-
const idx =
|
|
42
|
+
const idx = childIndex(selectionPath) as number;
|
|
44
43
|
let parent = doc;
|
|
45
44
|
for (const seg of pp) parent = parent[seg];
|
|
46
45
|
if (!parent.children) parent.children = [];
|
|
@@ -63,8 +62,8 @@ export async function convertToComponent() {
|
|
|
63
62
|
await platform.writeFile(componentFile, JSON.stringify(componentDef, null, 2));
|
|
64
63
|
await loadComponentRegistry();
|
|
65
64
|
statusMessage(`Converted to <${name}>`);
|
|
66
|
-
} catch (
|
|
67
|
-
statusMessage(`Error saving component: ${
|
|
65
|
+
} catch (err) {
|
|
66
|
+
statusMessage(`Error saving component: ${(err as Error).message}`);
|
|
68
67
|
}
|
|
69
68
|
}
|
|
70
69
|
|
|
@@ -74,7 +73,7 @@ export async function convertToComponent() {
|
|
|
74
73
|
* @param {JxMutableNode} node
|
|
75
74
|
* @returns {string}
|
|
76
75
|
*/
|
|
77
|
-
function deriveDefaultName(node) {
|
|
76
|
+
function deriveDefaultName(node: JxMutableNode) {
|
|
78
77
|
if (node.$id && node.$id.includes("-")) return node.$id.toLowerCase();
|
|
79
78
|
const tag = (node.tagName ?? "div").toLowerCase();
|
|
80
79
|
return tag.includes("-") ? tag : "jx-" + tag;
|
|
@@ -86,7 +85,7 @@ function deriveDefaultName(node) {
|
|
|
86
85
|
* @param {JxMutableNode} node
|
|
87
86
|
* @returns {JxMutableNode}
|
|
88
87
|
*/
|
|
89
|
-
function extractComponentDef(node) {
|
|
88
|
+
function extractComponentDef(node: JxMutableNode) {
|
|
90
89
|
const clone = structuredClone(node);
|
|
91
90
|
delete clone.$id;
|
|
92
91
|
delete clone.$layout;
|
|
@@ -100,7 +99,7 @@ function extractComponentDef(node) {
|
|
|
100
99
|
* @param {string} val
|
|
101
100
|
* @returns {{ valid: boolean; error: string }}
|
|
102
101
|
*/
|
|
103
|
-
function validateName(val) {
|
|
102
|
+
function validateName(val: string) {
|
|
104
103
|
val = val.trim().toLowerCase();
|
|
105
104
|
if (!val.includes("-")) {
|
|
106
105
|
return { valid: false, error: "Name must contain a hyphen (e.g. my-component)" };
|
|
@@ -108,7 +107,7 @@ function validateName(val) {
|
|
|
108
107
|
if (!VALID_NAME.test(val)) {
|
|
109
108
|
return { valid: false, error: "Lowercase letters, digits, and hyphens only" };
|
|
110
109
|
}
|
|
111
|
-
const exists = componentRegistry.some((
|
|
110
|
+
const exists = componentRegistry.some((c: JxMutableNode) => c.tagName === val);
|
|
112
111
|
if (exists) {
|
|
113
112
|
return { valid: false, error: `Component <${val}> already exists` };
|
|
114
113
|
}
|
|
@@ -121,11 +120,11 @@ function validateName(val) {
|
|
|
121
120
|
* @param {string} defaultName
|
|
122
121
|
* @returns {Promise<string | null>}
|
|
123
122
|
*/
|
|
124
|
-
function promptComponentName(defaultName) {
|
|
123
|
+
function promptComponentName(defaultName: string) {
|
|
125
124
|
let value = defaultName;
|
|
126
125
|
let error = "";
|
|
127
126
|
|
|
128
|
-
return showDialog((done) => {
|
|
127
|
+
return showDialog<string | null>((done) => {
|
|
129
128
|
function confirm() {
|
|
130
129
|
const result = validateName(value);
|
|
131
130
|
if (!result.valid) {
|
|
@@ -136,21 +135,21 @@ function promptComponentName(defaultName) {
|
|
|
136
135
|
done(value.trim().toLowerCase());
|
|
137
136
|
}
|
|
138
137
|
|
|
139
|
-
function onInput(
|
|
140
|
-
value =
|
|
138
|
+
function onInput(e: Event) {
|
|
139
|
+
value = (e.target as HTMLInputElement).value || "";
|
|
141
140
|
const result = validateName(value);
|
|
142
141
|
error = result.valid ? "" : result.error;
|
|
143
142
|
rerender();
|
|
144
143
|
}
|
|
145
144
|
|
|
146
|
-
function onKeydown(
|
|
145
|
+
function onKeydown(e: KeyboardEvent) {
|
|
147
146
|
if (e.key === "Enter") confirm();
|
|
148
147
|
}
|
|
149
148
|
|
|
150
149
|
function rerender() {
|
|
151
150
|
const layer = document.getElementById("layer-dialog");
|
|
152
151
|
const slot = layer?.lastElementChild;
|
|
153
|
-
if (slot) litRender(buildTpl(),
|
|
152
|
+
if (slot) litRender(buildTpl(), slot as HTMLElement);
|
|
154
153
|
}
|
|
155
154
|
|
|
156
155
|
function buildTpl() {
|
|
@@ -176,8 +175,8 @@ function promptComponentName(defaultName) {
|
|
|
176
175
|
${ref((el) => {
|
|
177
176
|
if (el)
|
|
178
177
|
requestAnimationFrame(() => {
|
|
179
|
-
|
|
180
|
-
const input =
|
|
178
|
+
(el as HTMLElement).focus();
|
|
179
|
+
const input = (el as HTMLElement).shadowRoot?.querySelector("input");
|
|
181
180
|
if (input) input.select();
|
|
182
181
|
});
|
|
183
182
|
})}
|