@jxsuite/studio 0.19.0 → 0.20.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.css +98 -98
- package/dist/studio.js +12577 -12183
- package/dist/studio.js.map +146 -144
- package/dist/workers/editor.worker.js +79 -79
- package/dist/workers/json.worker.js +109 -109
- package/dist/workers/ts.worker.js +82 -82
- package/package.json +17 -6
- package/src/browse/browse.js +25 -19
- package/src/canvas/canvas-diff.js +24 -16
- package/src/canvas/canvas-helpers.js +24 -19
- package/src/canvas/canvas-live-render.js +94 -39
- package/src/canvas/canvas-render.js +119 -73
- package/src/canvas/canvas-utils.js +38 -15
- package/src/canvas/nested-site-style.js +50 -0
- package/src/editor/component-inline-edit.js +14 -13
- package/src/editor/content-inline-edit.js +22 -10
- package/src/editor/context-menu.js +1 -1
- package/src/editor/convert-to-component.js +12 -10
- package/src/editor/inline-edit.js +66 -28
- package/src/editor/insertion-helper.js +16 -12
- package/src/editor/shortcuts.js +4 -2
- package/src/editor/slash-menu.js +6 -4
- package/src/files/components.js +1 -1
- package/src/files/file-ops.js +37 -16
- package/src/files/files.js +78 -52
- package/src/github/github-auth.js +122 -0
- package/src/github/github-publish.js +115 -0
- package/src/markdown/md-convert.js +172 -107
- package/src/new-project/new-project-modal.js +204 -0
- package/src/panels/activity-bar.js +22 -20
- package/src/panels/ai-panel.js +399 -0
- package/src/panels/block-action-bar.js +5 -5
- package/src/panels/canvas-dnd.js +8 -2
- package/src/panels/data-explorer.js +19 -13
- package/src/panels/dnd.js +144 -92
- package/src/panels/editors.js +55 -35
- package/src/panels/elements-panel.js +33 -19
- package/src/panels/events-panel.js +15 -10
- package/src/panels/git-panel.js +469 -98
- package/src/panels/head-panel.js +121 -81
- package/src/panels/imports-panel.js +88 -64
- package/src/panels/layers-panel.js +64 -43
- package/src/panels/left-panel.js +85 -47
- package/src/panels/overlays.js +1 -0
- package/src/panels/panel-events.js +45 -30
- package/src/panels/preview-render.js +23 -18
- package/src/panels/properties-panel.js +365 -249
- package/src/panels/pseudo-preview.js +2 -2
- package/src/panels/quick-search.js +7 -5
- package/src/panels/right-panel.js +26 -2
- package/src/panels/shared.js +4 -4
- package/src/panels/signals-panel.js +295 -166
- package/src/panels/statusbar.js +4 -4
- package/src/panels/style-inputs.js +67 -35
- package/src/panels/style-panel.js +233 -132
- package/src/panels/style-utils.js +34 -16
- package/src/panels/stylebook-layers-panel.js +12 -10
- package/src/panels/stylebook-panel.js +134 -66
- package/src/panels/toolbar.js +333 -109
- package/src/panels/welcome-screen.js +121 -0
- package/src/platform.js +2 -4
- package/src/platforms/devserver.js +113 -7
- package/src/resize-edges.js +98 -0
- package/src/services/cem-export.js +12 -7
- package/src/services/code-services.js +30 -12
- package/src/settings/content-types-editor.js +7 -7
- package/src/settings/css-vars-editor.js +30 -24
- package/src/settings/defs-editor.js +12 -7
- package/src/settings/general-settings.js +85 -3
- package/src/settings/head-editor.js +30 -24
- package/src/settings/schema-field-ui.js +58 -39
- package/src/site-context.js +41 -31
- package/src/state.js +70 -29
- package/src/store.js +21 -26
- package/src/studio.js +103 -81
- package/src/tabs/tab.js +67 -43
- package/src/tabs/transact.js +23 -13
- package/src/ui/button-group.js +24 -24
- package/src/ui/color-selector.js +28 -23
- package/src/ui/field-row.js +3 -3
- package/src/ui/icons.js +2 -2
- package/src/ui/media-picker.js +7 -7
- package/src/ui/spectrum.js +5 -1
- package/src/ui/unit-selector.js +18 -16
- package/src/ui/value-selector.js +21 -15
- package/src/ui/widgets.js +22 -19
- package/src/utils/canvas-media.js +5 -4
- package/src/utils/edit-display.js +31 -20
- package/src/utils/google-fonts.js +11 -11
- package/src/utils/inherited-style.js +9 -8
- package/src/utils/studio-utils.js +7 -7
- package/src/view.js +58 -1
- package/src/workspace/workspace.js +10 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/studio",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"description": "Jx Studio — visual builder for Jx documents",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"provenance": true
|
|
23
23
|
},
|
|
24
24
|
"scripts": {
|
|
25
|
-
"build": "bun build ./src/studio.js --outdir dist --target browser --sourcemap=linked && bun build
|
|
25
|
+
"build": "bun build ./src/studio.js --outdir dist --target browser --sourcemap=linked && bun build ./node_modules/monaco-editor/esm/vs/editor/editor.worker.js --outdir dist/workers --target browser --entry-naming editor.worker.js && bun build ./node_modules/monaco-editor/esm/vs/language/json/json.worker.js --outdir dist/workers --target browser --entry-naming json.worker.js && bun build ./node_modules/monaco-editor/esm/vs/language/typescript/ts.worker.js --outdir dist/workers --target browser --entry-naming ts.worker.js",
|
|
26
26
|
"gen:webdata": "bun run scripts/gen-webdata.js",
|
|
27
27
|
"test": "bun test",
|
|
28
28
|
"upgrade": "bunx npm-check-updates -u && bun install"
|
|
@@ -30,11 +30,15 @@
|
|
|
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/
|
|
33
|
+
"@jxsuite/parser": "^0.20.0",
|
|
34
|
+
"@jxsuite/runtime": "^0.20.0",
|
|
35
|
+
"@jxsuite/schema": "^0.20.0",
|
|
34
36
|
"@spectrum-web-components/accordion": "^1.12.1",
|
|
35
37
|
"@spectrum-web-components/action-bar": "1.12.1",
|
|
36
38
|
"@spectrum-web-components/action-button": "^1.12.1",
|
|
37
39
|
"@spectrum-web-components/action-group": "^1.12.1",
|
|
40
|
+
"@spectrum-web-components/base": "^1.12.1",
|
|
41
|
+
"@spectrum-web-components/button": "^1.12.1",
|
|
38
42
|
"@spectrum-web-components/checkbox": "^1.12.1",
|
|
39
43
|
"@spectrum-web-components/color-area": "^1.12.1",
|
|
40
44
|
"@spectrum-web-components/color-handle": "^1.12.1",
|
|
@@ -42,7 +46,10 @@
|
|
|
42
46
|
"@spectrum-web-components/combobox": "^1.12.1",
|
|
43
47
|
"@spectrum-web-components/dialog": "^1.12.1",
|
|
44
48
|
"@spectrum-web-components/divider": "^1.12.1",
|
|
49
|
+
"@spectrum-web-components/field-label": "^1.12.1",
|
|
50
|
+
"@spectrum-web-components/help-text": "^1.12.1",
|
|
45
51
|
"@spectrum-web-components/icon": "^1.12.1",
|
|
52
|
+
"@spectrum-web-components/icons-ui": "^1.12.1",
|
|
46
53
|
"@spectrum-web-components/icons-workflow": "^1.12.1",
|
|
47
54
|
"@spectrum-web-components/menu": "^1.12.1",
|
|
48
55
|
"@spectrum-web-components/number-field": "^1.12.1",
|
|
@@ -60,9 +67,12 @@
|
|
|
60
67
|
"@spectrum-web-components/theme": "^1.12.1",
|
|
61
68
|
"@spectrum-web-components/toast": "^1.12.1",
|
|
62
69
|
"@spectrum-web-components/tooltip": "^1.12.1",
|
|
63
|
-
"@
|
|
70
|
+
"@spectrum-web-components/underlay": "^1.12.1",
|
|
71
|
+
"@vue/reactivity": "3.5.35",
|
|
72
|
+
"lit": "^3.3.3",
|
|
64
73
|
"lit-html": "^3.3.3",
|
|
65
74
|
"monaco-editor": "^0.55.1",
|
|
75
|
+
"quikchat": "^1.2.7",
|
|
66
76
|
"remark-directive": "^4.0.0",
|
|
67
77
|
"remark-frontmatter": "^5.0.0",
|
|
68
78
|
"remark-gfm": "^4.0.1",
|
|
@@ -73,9 +83,10 @@
|
|
|
73
83
|
},
|
|
74
84
|
"devDependencies": {
|
|
75
85
|
"@happy-dom/global-registrator": "^20.9.0",
|
|
76
|
-
"@
|
|
86
|
+
"@types/mdast": "^4.0.4",
|
|
87
|
+
"@webref/css": "^8.5.7",
|
|
77
88
|
"@webref/elements": "^2.7.1",
|
|
78
|
-
"@webref/idl": "^3.
|
|
89
|
+
"@webref/idl": "^3.79.0"
|
|
79
90
|
},
|
|
80
91
|
"//vue-reactivity": "Exact pin required — must match @jxsuite/runtime to avoid cross-boundary proxy bugs"
|
|
81
92
|
}
|
package/src/browse/browse.js
CHANGED
|
@@ -15,6 +15,8 @@ import { yamlDefault } from "../settings/schema-field-ui.js";
|
|
|
15
15
|
import { invalidateMediaCache } from "../ui/media-picker.js";
|
|
16
16
|
import { statusMessage } from "../panels/statusbar.js";
|
|
17
17
|
import { componentRegistry } from "../files/components.js";
|
|
18
|
+
|
|
19
|
+
/** @typedef {import("../files/components.js").ComponentEntry} ComponentEntry */
|
|
18
20
|
import { showDialog, renderPopover } from "../ui/layers.js";
|
|
19
21
|
import { renderComponentPreview } from "../panels/stylebook-panel.js";
|
|
20
22
|
import { renderNode, buildScope, setSkipServerFunctions } from "@jxsuite/runtime";
|
|
@@ -150,7 +152,7 @@ function contentTypeFor(filePath) {
|
|
|
150
152
|
const config = projectState?.projectConfig;
|
|
151
153
|
if (!config?.contentTypes) return null;
|
|
152
154
|
for (const [name, def] of Object.entries(config.contentTypes)) {
|
|
153
|
-
const d = /** @type {
|
|
155
|
+
const d = /** @type {ContentTypeDef} */ (def);
|
|
154
156
|
if (!d.source) continue;
|
|
155
157
|
const prefix = d.source.replace(/^\.\//, "").split("/**")[0].split("/*")[0];
|
|
156
158
|
if (filePath.startsWith(prefix + "/") || filePath === prefix) {
|
|
@@ -215,8 +217,8 @@ function buildFrontmatterYaml(contentTypeName) {
|
|
|
215
217
|
|
|
216
218
|
let yaml = "";
|
|
217
219
|
for (const [field, def] of Object.entries(col.schema.properties)) {
|
|
218
|
-
const d = /** @type {
|
|
219
|
-
yaml += `${field}: ${yamlDefault(d.type, d.format)}\n`;
|
|
220
|
+
const d = /** @type {{ type?: string; format?: string }} */ (def);
|
|
221
|
+
yaml += `${field}: ${yamlDefault(d.type || "", d.format || "")}\n`;
|
|
220
222
|
}
|
|
221
223
|
return yaml || "title: Untitled\n";
|
|
222
224
|
}
|
|
@@ -230,7 +232,7 @@ function getContentTypeTypes() {
|
|
|
230
232
|
const config = projectState?.projectConfig;
|
|
231
233
|
if (!config?.contentTypes) return [];
|
|
232
234
|
return Object.entries(config.contentTypes).map(([name, def]) => {
|
|
233
|
-
const d = /** @type {
|
|
235
|
+
const d = /** @type {ContentTypeDef} */ (def);
|
|
234
236
|
const dir = d.source ? d.source.replace(/^\.\//, "").split("/")[0] : name;
|
|
235
237
|
return {
|
|
236
238
|
key: `contentType:${name}`,
|
|
@@ -415,8 +417,8 @@ async function browseRenameFile(file, container, ctx) {
|
|
|
415
417
|
invalidateBrowseCache();
|
|
416
418
|
renderBrowse(container, ctx);
|
|
417
419
|
statusMessage(`Renamed to ${newName}`);
|
|
418
|
-
} catch (/** @type {
|
|
419
|
-
statusMessage(`Error: ${e.message}`);
|
|
420
|
+
} catch (/** @type {unknown} */ e) {
|
|
421
|
+
statusMessage(`Error: ${/** @type {Error} */ (e).message}`);
|
|
420
422
|
}
|
|
421
423
|
}
|
|
422
424
|
|
|
@@ -439,8 +441,8 @@ async function browseDuplicateFile(file, container, ctx) {
|
|
|
439
441
|
invalidateBrowseCache();
|
|
440
442
|
renderBrowse(container, ctx);
|
|
441
443
|
statusMessage(`Duplicated as ${copyName}`);
|
|
442
|
-
} catch (/** @type {
|
|
443
|
-
statusMessage(`Error: ${e.message}`);
|
|
444
|
+
} catch (/** @type {unknown} */ e) {
|
|
445
|
+
statusMessage(`Error: ${/** @type {Error} */ (e).message}`);
|
|
444
446
|
}
|
|
445
447
|
}
|
|
446
448
|
|
|
@@ -458,8 +460,8 @@ async function browseDeleteFile(file, container, ctx) {
|
|
|
458
460
|
invalidateBrowseCache();
|
|
459
461
|
renderBrowse(container, ctx);
|
|
460
462
|
statusMessage(`Deleted ${file.name}`);
|
|
461
|
-
} catch (/** @type {
|
|
462
|
-
statusMessage(`Error: ${e.message}`);
|
|
463
|
+
} catch (/** @type {unknown} */ e) {
|
|
464
|
+
statusMessage(`Error: ${/** @type {Error} */ (e).message}`);
|
|
463
465
|
}
|
|
464
466
|
}
|
|
465
467
|
|
|
@@ -495,7 +497,7 @@ function showRenameDialog(currentName) {
|
|
|
495
497
|
style="width:100%"
|
|
496
498
|
value=${value}
|
|
497
499
|
@input=${(/** @type {Event} */ e) => {
|
|
498
|
-
value = /** @type {
|
|
500
|
+
value = /** @type {HTMLInputElement} */ (e.target).value || "";
|
|
499
501
|
}}
|
|
500
502
|
@keydown=${(/** @type {KeyboardEvent} */ e) => {
|
|
501
503
|
if (e.key === "Enter") confirm();
|
|
@@ -506,7 +508,7 @@ function showRenameDialog(currentName) {
|
|
|
506
508
|
|
|
507
509
|
requestAnimationFrame(() => {
|
|
508
510
|
const layer = document.getElementById("layer-dialog");
|
|
509
|
-
const tf = /** @type {
|
|
511
|
+
const tf = /** @type {HTMLElement | null} */ (layer?.querySelector("sp-textfield"));
|
|
510
512
|
if (tf) {
|
|
511
513
|
tf.focus();
|
|
512
514
|
const input = tf.shadowRoot?.querySelector("input");
|
|
@@ -587,7 +589,9 @@ async function loadPreview(el, file) {
|
|
|
587
589
|
let preview = _previewCache.get(file.path);
|
|
588
590
|
if (!preview) {
|
|
589
591
|
try {
|
|
590
|
-
const comp = componentRegistry.find(
|
|
592
|
+
const comp = componentRegistry.find(
|
|
593
|
+
(/** @type {ComponentEntry} */ c) => c.path === file.path,
|
|
594
|
+
);
|
|
591
595
|
if (comp) {
|
|
592
596
|
preview = /** @type {HTMLElement | undefined} */ (await renderComponentPreview(comp));
|
|
593
597
|
} else {
|
|
@@ -686,8 +690,8 @@ export async function renderBrowse(container, ctx) {
|
|
|
686
690
|
size="s"
|
|
687
691
|
placeholder="Filter files..."
|
|
688
692
|
.value=${searchQuery}
|
|
689
|
-
@input=${(/** @type {
|
|
690
|
-
searchQuery = e.target.value;
|
|
693
|
+
@input=${(/** @type {Event} */ e) => {
|
|
694
|
+
searchQuery = /** @type {HTMLInputElement} */ (e.target).value;
|
|
691
695
|
renderBrowse(container, ctx);
|
|
692
696
|
}}
|
|
693
697
|
@submit=${(/** @type {Event} */ e) => e.preventDefault()}
|
|
@@ -698,7 +702,8 @@ export async function renderBrowse(container, ctx) {
|
|
|
698
702
|
</sp-action-button>
|
|
699
703
|
<sp-popover slot="click-content" tip>
|
|
700
704
|
<sp-menu
|
|
701
|
-
@change=${(/** @type {
|
|
705
|
+
@change=${(/** @type {Event} */ e) =>
|
|
706
|
+
handleNewEntity(/** @type {HTMLSelectElement} */ (e.target).value, container, ctx)}
|
|
702
707
|
>
|
|
703
708
|
${ENTITY_TYPES.map((t) => html`<sp-menu-item value=${t.key}>${t.label}</sp-menu-item>`)}
|
|
704
709
|
${contentTypeTypes.length
|
|
@@ -726,9 +731,10 @@ export async function renderBrowse(container, ctx) {
|
|
|
726
731
|
accept=${UPLOAD_ACCEPT}
|
|
727
732
|
class="browse-upload-input"
|
|
728
733
|
style="display:none"
|
|
729
|
-
@change=${(/** @type {
|
|
730
|
-
|
|
731
|
-
|
|
734
|
+
@change=${(/** @type {Event} */ e) => {
|
|
735
|
+
const input = /** @type {HTMLInputElement} */ (e.target);
|
|
736
|
+
if (input.files?.length) handleUpload(input.files, container, ctx);
|
|
737
|
+
input.value = "";
|
|
732
738
|
}}
|
|
733
739
|
/>
|
|
734
740
|
<div class="browse-view-switcher">
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
* @typedef {
|
|
9
|
+
* @typedef {JxMutableNode} JxNode
|
|
10
10
|
*
|
|
11
11
|
* @typedef {"added" | "removed" | "modified"} DiffStatus
|
|
12
12
|
*
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
/**
|
|
17
17
|
* Deep equality check for two values. Ignores function and ref properties.
|
|
18
18
|
*
|
|
19
|
-
* @param {
|
|
20
|
-
* @param {
|
|
19
|
+
* @param {unknown} a
|
|
20
|
+
* @param {unknown} b
|
|
21
21
|
* @returns {boolean}
|
|
22
22
|
*/
|
|
23
23
|
function valuesEqual(a, b) {
|
|
@@ -26,38 +26,46 @@ function valuesEqual(a, b) {
|
|
|
26
26
|
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
27
27
|
|
|
28
28
|
if (Array.isArray(a)) {
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
const bArr = /** @type {unknown[]} */ (b);
|
|
30
|
+
if (a.length !== bArr.length) return false;
|
|
31
|
+
return a.every((/** @type {unknown} */ v, /** @type {number} */ i) => valuesEqual(v, bArr[i]));
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
const
|
|
34
|
+
const aObj = /** @type {Record<string, unknown>} */ (a);
|
|
35
|
+
const bObj = /** @type {Record<string, unknown>} */ (b);
|
|
36
|
+
|
|
37
|
+
const keysA = Object.keys(aObj).filter(
|
|
34
38
|
(/** @type {string} */ k) => !k.startsWith("on") && k !== "$ref",
|
|
35
39
|
);
|
|
36
|
-
const keysB = Object.keys(
|
|
40
|
+
const keysB = Object.keys(bObj).filter(
|
|
37
41
|
(/** @type {string} */ k) => !k.startsWith("on") && k !== "$ref",
|
|
38
42
|
);
|
|
39
43
|
|
|
40
44
|
if (keysA.length !== keysB.length) return false;
|
|
41
45
|
if (!keysA.every((/** @type {string} */ k) => keysB.includes(k))) return false;
|
|
42
46
|
|
|
43
|
-
return keysA.every((/** @type {string} */ k) => valuesEqual(
|
|
47
|
+
return keysA.every((/** @type {string} */ k) => valuesEqual(aObj[k], bObj[k]));
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
/**
|
|
47
51
|
* Filter element children from a node's children array.
|
|
48
52
|
*
|
|
49
|
-
* @param {
|
|
50
|
-
* @returns {
|
|
53
|
+
* @param {JxMutableNode | undefined} node
|
|
54
|
+
* @returns {JxMutableNode[]}
|
|
51
55
|
*/
|
|
52
56
|
function elementChildren(node) {
|
|
53
57
|
if (!node?.children || !Array.isArray(node.children)) return [];
|
|
54
|
-
return
|
|
58
|
+
return /** @type {JxMutableNode[]} */ (
|
|
59
|
+
node.children.filter(
|
|
60
|
+
(/** @type {JxMutableNode | string} */ c) => c != null && typeof c === "object",
|
|
61
|
+
)
|
|
62
|
+
);
|
|
55
63
|
}
|
|
56
64
|
|
|
57
65
|
/**
|
|
58
66
|
* Mark an entire subtree with a diff status.
|
|
59
67
|
*
|
|
60
|
-
* @param {
|
|
68
|
+
* @param {JxMutableNode} node
|
|
61
69
|
* @param {DiffStatus} status
|
|
62
70
|
* @param {string} path
|
|
63
71
|
* @param {Map<string, DiffStatus>} diffMap
|
|
@@ -76,8 +84,8 @@ function markRecursive(node, status, path, diffMap, allPaths) {
|
|
|
76
84
|
/**
|
|
77
85
|
* Compute structural diff between original and current documents.
|
|
78
86
|
*
|
|
79
|
-
* @param {
|
|
80
|
-
* @param {
|
|
87
|
+
* @param {JxMutableNode | undefined} originalDoc - Original document (from git)
|
|
88
|
+
* @param {JxMutableNode | undefined} currentDoc - Current document (in memory/on disk)
|
|
81
89
|
* @returns {DiffResult}
|
|
82
90
|
*/
|
|
83
91
|
export function computeDocumentDiff(originalDoc, currentDoc) {
|
|
@@ -89,8 +97,8 @@ export function computeDocumentDiff(originalDoc, currentDoc) {
|
|
|
89
97
|
/**
|
|
90
98
|
* Walk both trees in parallel and mark differences.
|
|
91
99
|
*
|
|
92
|
-
* @param {
|
|
93
|
-
* @param {
|
|
100
|
+
* @param {JxMutableNode | undefined} origNode
|
|
101
|
+
* @param {JxMutableNode | undefined} currNode
|
|
94
102
|
* @param {string} path
|
|
95
103
|
*/
|
|
96
104
|
const walk = (origNode, currNode, path = "") => {
|
|
@@ -7,7 +7,7 @@ import { canvasPanels, elToPath, pathsEqual, getNodeAtPath, parentElementPath }
|
|
|
7
7
|
import { activeTab } from "../workspace/workspace.js";
|
|
8
8
|
import { isInlineInContext } from "../editor/inline-edit.js";
|
|
9
9
|
|
|
10
|
-
/** @type {
|
|
10
|
+
/** @type {{ getCanvasMode: () => string; getZoom: () => number } | null} */
|
|
11
11
|
let _ctx = null;
|
|
12
12
|
|
|
13
13
|
/**
|
|
@@ -21,9 +21,10 @@ export function initCanvasHelpers(ctx) {
|
|
|
21
21
|
|
|
22
22
|
/** Effective zoom scale — always 1 in edit (content) mode, actual zoom otherwise. */
|
|
23
23
|
export function effectiveZoom() {
|
|
24
|
-
|
|
24
|
+
const ctx = /** @type {{ getCanvasMode: () => string; getZoom: () => number }} */ (_ctx);
|
|
25
|
+
return ctx.getCanvasMode() === "edit"
|
|
25
26
|
? 1
|
|
26
|
-
: (
|
|
27
|
+
: (ctx.getZoom?.() ?? activeTab.value?.session.ui.zoom ?? 1);
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
/** Return the active canvas panel based on the current activeMedia setting. */
|
|
@@ -42,10 +43,11 @@ export function getActivePanel() {
|
|
|
42
43
|
* Walk up the tree from a path, bubbling past inline elements until we find the nearest non-inline
|
|
43
44
|
* ancestor. Returns the original path if already non-inline.
|
|
44
45
|
*
|
|
45
|
-
* @param {
|
|
46
|
-
* @param {
|
|
46
|
+
* @param {JxMutableNode | undefined} doc
|
|
47
|
+
* @param {JxPath} path
|
|
47
48
|
*/
|
|
48
49
|
export function bubbleInlinePath(doc, path) {
|
|
50
|
+
if (!doc) return path;
|
|
49
51
|
let currentPath = path;
|
|
50
52
|
while (currentPath.length >= 2) {
|
|
51
53
|
const node = getNodeAtPath(doc, currentPath);
|
|
@@ -55,7 +57,7 @@ export function bubbleInlinePath(doc, path) {
|
|
|
55
57
|
const childTag = (node.tagName ?? "div").toLowerCase();
|
|
56
58
|
const parentTag = (parentNode.tagName ?? "div").toLowerCase();
|
|
57
59
|
if (!isInlineInContext(childTag, parentTag)) break;
|
|
58
|
-
currentPath = pPath;
|
|
60
|
+
currentPath = /** @type {JxPath} */ (pPath);
|
|
59
61
|
}
|
|
60
62
|
return currentPath;
|
|
61
63
|
}
|
|
@@ -63,11 +65,13 @@ export function bubbleInlinePath(doc, path) {
|
|
|
63
65
|
/**
|
|
64
66
|
* Find a canvas DOM element by its document path.
|
|
65
67
|
*
|
|
66
|
-
* @param {
|
|
67
|
-
* @param {
|
|
68
|
+
* @param {JxPath} path
|
|
69
|
+
* @param {HTMLElement} canvasEl
|
|
70
|
+
* @returns {HTMLElement | null}
|
|
68
71
|
*/
|
|
69
72
|
export function findCanvasElement(path, canvasEl) {
|
|
70
|
-
|
|
73
|
+
/** @type {HTMLElement | null | undefined} */
|
|
74
|
+
let el = /** @type {HTMLElement | null} */ (canvasEl.firstElementChild);
|
|
71
75
|
if (!el) return null;
|
|
72
76
|
if (path.length === 0) return el;
|
|
73
77
|
|
|
@@ -75,11 +79,11 @@ export function findCanvasElement(path, canvasEl) {
|
|
|
75
79
|
if (path[i] !== "children" && path[i] !== "cases") return null;
|
|
76
80
|
const idx = path[i + 1];
|
|
77
81
|
if (idx === undefined) {
|
|
78
|
-
el = el.children[0];
|
|
82
|
+
el = /** @type {HTMLElement | undefined} */ (el.children[0]);
|
|
79
83
|
} else if (idx === "map") {
|
|
80
|
-
el = el.children[0]?.children[0];
|
|
84
|
+
el = /** @type {HTMLElement | undefined} */ (el.children[0]?.children[0]);
|
|
81
85
|
} else {
|
|
82
|
-
el = el.children[idx];
|
|
86
|
+
el = /** @type {HTMLElement | undefined} */ (el.children[/** @type {number} */ (idx)]);
|
|
83
87
|
}
|
|
84
88
|
if (!el) break;
|
|
85
89
|
}
|
|
@@ -91,7 +95,7 @@ export function findCanvasElement(path, canvasEl) {
|
|
|
91
95
|
|
|
92
96
|
for (const candidate of canvasEl.querySelectorAll("*")) {
|
|
93
97
|
const p = elToPath.get(candidate);
|
|
94
|
-
if (p && pathsEqual(p, path)) return candidate;
|
|
98
|
+
if (p && pathsEqual(p, path)) return /** @type {HTMLElement} */ (candidate);
|
|
95
99
|
}
|
|
96
100
|
return null;
|
|
97
101
|
}
|
|
@@ -99,18 +103,19 @@ export function findCanvasElement(path, canvasEl) {
|
|
|
99
103
|
/**
|
|
100
104
|
* Build an overlay box descriptor (no DOM creation).
|
|
101
105
|
*
|
|
102
|
-
* @param {
|
|
103
|
-
* @param {
|
|
104
|
-
* @param {
|
|
106
|
+
* @param {Element} el
|
|
107
|
+
* @param {string} type
|
|
108
|
+
* @param {import("./canvas-render.js").CanvasPanel} panel
|
|
105
109
|
*/
|
|
106
110
|
export function overlayBoxDescriptor(el, type, panel) {
|
|
107
|
-
const
|
|
111
|
+
const viewport = /** @type {HTMLElement} */ (panel.viewport);
|
|
112
|
+
const vpRect = viewport.getBoundingClientRect();
|
|
108
113
|
const elRect = el.getBoundingClientRect();
|
|
109
114
|
const scale = effectiveZoom();
|
|
110
115
|
return {
|
|
111
116
|
cls: `overlay-box overlay-${type}`,
|
|
112
|
-
top: `${(elRect.top - vpRect.top +
|
|
113
|
-
left: `${(elRect.left - vpRect.left +
|
|
117
|
+
top: `${(elRect.top - vpRect.top + viewport.scrollTop) / scale}px`,
|
|
118
|
+
left: `${(elRect.left - vpRect.left + viewport.scrollLeft) / scale}px`,
|
|
114
119
|
width: `${elRect.width / scale}px`,
|
|
115
120
|
height: `${elRect.height / scale}px`,
|
|
116
121
|
};
|