@jxsuite/studio 0.11.0 → 0.14.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 +6517 -5870
- package/dist/studio.js.map +57 -51
- package/package.json +5 -3
- package/src/canvas/canvas-diff.js +184 -0
- package/src/canvas/canvas-helpers.js +10 -14
- package/src/canvas/canvas-live-render.js +28 -2
- package/src/canvas/canvas-render.js +170 -20
- package/src/canvas/canvas-utils.js +22 -25
- package/src/editor/component-inline-edit.js +55 -44
- package/src/editor/content-inline-edit.js +47 -50
- package/src/editor/context-menu.js +78 -53
- package/src/editor/convert-to-component.js +11 -14
- package/src/editor/insertion-helper.js +8 -11
- package/src/editor/shortcuts.js +88 -64
- package/src/files/components.js +15 -4
- package/src/files/file-ops.js +57 -108
- package/src/files/files.js +81 -28
- package/src/panels/activity-bar.js +31 -7
- package/src/panels/block-action-bar.js +104 -80
- package/src/panels/canvas-dnd.js +4 -10
- package/src/panels/dnd.js +77 -35
- package/src/panels/editors.js +17 -26
- package/src/panels/elements-panel.js +17 -11
- package/src/panels/events-panel.js +44 -39
- package/src/panels/git-panel.js +47 -4
- package/src/panels/layers-panel.js +25 -21
- package/src/panels/left-panel.js +109 -44
- package/src/panels/overlays.js +91 -41
- package/src/panels/panel-events.js +28 -37
- package/src/panels/preview-render.js +7 -3
- package/src/panels/properties-panel.js +179 -104
- package/src/panels/pseudo-preview.js +9 -8
- package/src/panels/right-panel.js +85 -37
- package/src/panels/shared.js +0 -22
- package/src/panels/signals-panel.js +125 -54
- package/src/panels/statusbar.js +26 -19
- package/src/panels/style-inputs.js +5 -4
- package/src/panels/style-panel.js +128 -105
- package/src/panels/style-utils.js +8 -6
- package/src/panels/stylebook-layers-panel.js +5 -5
- package/src/panels/stylebook-panel.js +27 -31
- package/src/panels/tab-strip.js +124 -0
- package/src/panels/toolbar.js +40 -10
- package/src/reactivity.js +28 -0
- package/src/settings/content-types-editor.js +56 -7
- package/src/settings/defs-editor.js +56 -7
- package/src/settings/schema-field-ui.js +60 -25
- package/src/state.js +2 -459
- package/src/store.js +61 -219
- package/src/studio.js +163 -300
- package/src/tabs/tab.js +168 -0
- package/src/tabs/transact.js +406 -0
- package/src/ui/color-selector.js +4 -5
- package/src/view.js +3 -0
- package/src/workspace/workspace.js +90 -0
|
@@ -1,60 +1,67 @@
|
|
|
1
1
|
// ─── Clipboard & Context Menu ─────────────────────────────────────────────────
|
|
2
2
|
import { html, render as litRender } from "lit-html";
|
|
3
|
+
import { getNodeAtPath, parentElementPath, childIndex } from "../store.js";
|
|
4
|
+
import { activeTab } from "../workspace/workspace.js";
|
|
3
5
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
getNodeAtPath,
|
|
11
|
-
parentElementPath,
|
|
12
|
-
childIndex,
|
|
13
|
-
} from "../store.js";
|
|
6
|
+
transactDoc,
|
|
7
|
+
mutateInsertNode,
|
|
8
|
+
mutateRemoveNode,
|
|
9
|
+
mutateDuplicateNode,
|
|
10
|
+
mutateWrapNode,
|
|
11
|
+
} from "../tabs/transact.js";
|
|
14
12
|
import { statusMessage } from "../panels/statusbar.js";
|
|
15
13
|
import { convertToComponent } from "./convert-to-component.js";
|
|
16
14
|
import { componentRegistry } from "../files/components.js";
|
|
17
15
|
|
|
18
|
-
/**
|
|
16
|
+
/**
|
|
17
|
+
* @typedef {import("../state.js").StudioState} StudioState
|
|
18
|
+
*
|
|
19
|
+
* @typedef {import("../state.js").JxPath} JxPath
|
|
20
|
+
*
|
|
21
|
+
* @typedef {import("../state.js").JxNode} JxNode
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/** @type {JxNode | null} */
|
|
19
25
|
let clipboard = null;
|
|
20
26
|
|
|
21
27
|
// ─── Clipboard ────────────────────────────────────────────────────────────────
|
|
22
28
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if (!
|
|
26
|
-
const node = getNodeAtPath(
|
|
29
|
+
export function copyNode() {
|
|
30
|
+
const tab = activeTab.value;
|
|
31
|
+
if (!tab?.session.selection) return;
|
|
32
|
+
const node = getNodeAtPath(tab.doc.document, tab.session.selection);
|
|
27
33
|
if (!node) return;
|
|
28
34
|
clipboard = structuredClone(node);
|
|
29
35
|
statusMessage("Copied");
|
|
30
36
|
}
|
|
31
37
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
if (!
|
|
35
|
-
const
|
|
38
|
+
export function cutNode() {
|
|
39
|
+
const tab = activeTab.value;
|
|
40
|
+
if (!tab?.session.selection || tab.session.selection.length < 2) return;
|
|
41
|
+
const sel = tab.session.selection;
|
|
42
|
+
const node = getNodeAtPath(tab.doc.document, sel);
|
|
36
43
|
if (!node) return;
|
|
37
44
|
clipboard = structuredClone(node);
|
|
38
|
-
|
|
45
|
+
transactDoc(tab, (t) => mutateRemoveNode(t, sel));
|
|
39
46
|
statusMessage("Cut");
|
|
40
47
|
}
|
|
41
48
|
|
|
42
|
-
|
|
43
|
-
export function pasteNode(S) {
|
|
49
|
+
export function pasteNode() {
|
|
44
50
|
if (!clipboard) return;
|
|
45
|
-
const
|
|
46
|
-
|
|
51
|
+
const tab = activeTab.value;
|
|
52
|
+
if (!tab) return;
|
|
53
|
+
const clip = clipboard;
|
|
54
|
+
const pPath = tab.session.selection || [];
|
|
55
|
+
const parent = getNodeAtPath(tab.doc.document, pPath);
|
|
47
56
|
if (!parent) return;
|
|
48
57
|
|
|
49
|
-
if (
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
update(insertNode(S, pp, idx + 1, structuredClone(clipboard)));
|
|
58
|
+
if (tab.session.selection && tab.session.selection.length >= 2) {
|
|
59
|
+
const pp = /** @type {JxPath} */ (parentElementPath(tab.session.selection));
|
|
60
|
+
const idx = /** @type {number} */ (childIndex(tab.session.selection));
|
|
61
|
+
transactDoc(tab, (t) => mutateInsertNode(t, pp, idx + 1, structuredClone(clip)));
|
|
54
62
|
} else {
|
|
55
|
-
// Paste as last child of root/selected
|
|
56
63
|
const idx = parent.children ? parent.children.length : 0;
|
|
57
|
-
|
|
64
|
+
transactDoc(tab, (t) => mutateInsertNode(t, pPath, idx, structuredClone(clip)));
|
|
58
65
|
}
|
|
59
66
|
statusMessage("Pasted");
|
|
60
67
|
}
|
|
@@ -77,85 +84,103 @@ export function dismissContextMenu() {
|
|
|
77
84
|
}
|
|
78
85
|
|
|
79
86
|
/**
|
|
80
|
-
* @param {
|
|
81
|
-
* @param {
|
|
82
|
-
* @param {any} S
|
|
87
|
+
* @param {MouseEvent} e
|
|
88
|
+
* @param {JxPath} path
|
|
83
89
|
* @param {{ onEditComponent?: (path: string) => void }} [opts]
|
|
84
90
|
*/
|
|
85
|
-
export function showContextMenu(e, path,
|
|
91
|
+
export function showContextMenu(e, path, opts = {}) {
|
|
86
92
|
e.preventDefault();
|
|
87
93
|
ctxMenu.removeAttribute("open");
|
|
88
94
|
|
|
89
|
-
const
|
|
95
|
+
const tab = activeTab.value;
|
|
96
|
+
const node = getNodeAtPath(tab?.doc.document, path);
|
|
90
97
|
if (!node) return;
|
|
91
98
|
|
|
92
99
|
// Select the node
|
|
93
|
-
|
|
100
|
+
tab.session.selection = path;
|
|
94
101
|
|
|
95
102
|
/** @type {{ label: string; action?: () => void; danger?: boolean }[]} */
|
|
96
103
|
const items = [];
|
|
97
104
|
|
|
98
|
-
items.push({ label: "Copy", action: () => copyNode(
|
|
105
|
+
items.push({ label: "Copy", action: () => copyNode() });
|
|
99
106
|
if (path.length >= 2) {
|
|
100
|
-
items.push({ label: "Cut", action: () => cutNode(
|
|
101
|
-
items.push({
|
|
107
|
+
items.push({ label: "Cut", action: () => cutNode() });
|
|
108
|
+
items.push({
|
|
109
|
+
label: "Duplicate",
|
|
110
|
+
action: () => transactDoc(activeTab.value, (t) => mutateDuplicateNode(t, path)),
|
|
111
|
+
});
|
|
102
112
|
items.push({ label: "—" }); // separator
|
|
103
113
|
items.push({
|
|
104
114
|
label: "Insert before",
|
|
105
115
|
action: () => {
|
|
106
|
-
const pp = /** @type {
|
|
116
|
+
const pp = /** @type {JxPath} */ (parentElementPath(path));
|
|
107
117
|
const idx = /** @type {number} */ (childIndex(path));
|
|
108
|
-
|
|
118
|
+
transactDoc(activeTab.value, (t) =>
|
|
119
|
+
mutateInsertNode(t, pp, idx, { tagName: "p", children: [] }),
|
|
120
|
+
);
|
|
109
121
|
},
|
|
110
122
|
});
|
|
111
123
|
items.push({
|
|
112
124
|
label: "Insert after",
|
|
113
125
|
action: () => {
|
|
114
|
-
const pp = /** @type {
|
|
126
|
+
const pp = /** @type {JxPath} */ (parentElementPath(path));
|
|
115
127
|
const idx = /** @type {number} */ (childIndex(path));
|
|
116
|
-
|
|
128
|
+
transactDoc(activeTab.value, (t) =>
|
|
129
|
+
mutateInsertNode(t, pp, idx + 1, { tagName: "p", children: [] }),
|
|
130
|
+
);
|
|
117
131
|
},
|
|
118
132
|
});
|
|
119
133
|
items.push({
|
|
120
134
|
label: "Wrap in Div",
|
|
121
|
-
action: () =>
|
|
135
|
+
action: () => transactDoc(activeTab.value, (t) => mutateWrapNode(t, path)),
|
|
122
136
|
});
|
|
123
137
|
if (node.tagName) {
|
|
124
138
|
const isComponent =
|
|
125
139
|
node.tagName.includes("-") &&
|
|
126
|
-
componentRegistry.some(
|
|
140
|
+
componentRegistry.some(
|
|
141
|
+
(/** @type {{ tagName: string }} */ c) => c.tagName === node.tagName,
|
|
142
|
+
);
|
|
127
143
|
if (isComponent && opts.onEditComponent) {
|
|
128
|
-
const comp = componentRegistry.find(
|
|
144
|
+
const comp = componentRegistry.find(
|
|
145
|
+
(/** @type {{ tagName: string; path: string }} */ c) => c.tagName === node.tagName,
|
|
146
|
+
);
|
|
129
147
|
items.push({
|
|
130
148
|
label: "Edit Component",
|
|
131
|
-
action: () => opts.onEditComponent?.(comp
|
|
149
|
+
action: () => opts.onEditComponent?.(/** @type {string} */ (comp?.path)),
|
|
132
150
|
});
|
|
133
151
|
} else if (!isComponent) {
|
|
134
152
|
items.push({
|
|
135
153
|
label: "Convert to Component",
|
|
136
|
-
action: () => convertToComponent(
|
|
154
|
+
action: () => convertToComponent(),
|
|
137
155
|
});
|
|
138
156
|
}
|
|
139
157
|
}
|
|
140
158
|
items.push({ label: "—" }); // separator
|
|
141
|
-
items.push({
|
|
159
|
+
items.push({
|
|
160
|
+
label: "Delete",
|
|
161
|
+
action: () => transactDoc(activeTab.value, (t) => mutateRemoveNode(t, path)),
|
|
162
|
+
danger: true,
|
|
163
|
+
});
|
|
142
164
|
}
|
|
143
165
|
if (clipboard) {
|
|
166
|
+
const clip = clipboard;
|
|
144
167
|
items.push({ label: "—" });
|
|
145
168
|
items.push({
|
|
146
169
|
label: "Paste inside",
|
|
147
170
|
action: () => {
|
|
148
171
|
const idx = node.children ? node.children.length : 0;
|
|
149
|
-
|
|
172
|
+
transactDoc(activeTab.value, (t) => mutateInsertNode(t, path, idx, structuredClone(clip)));
|
|
150
173
|
},
|
|
151
174
|
});
|
|
152
175
|
if (path.length >= 2) {
|
|
153
176
|
items.push({
|
|
154
177
|
label: "Paste after",
|
|
155
178
|
action: () => {
|
|
156
|
-
const pp = /** @type {
|
|
179
|
+
const pp = /** @type {JxPath} */ (parentElementPath(path));
|
|
157
180
|
const idx = /** @type {number} */ (childIndex(path));
|
|
158
|
-
|
|
181
|
+
transactDoc(activeTab.value, (t) =>
|
|
182
|
+
mutateInsertNode(t, pp, idx + 1, structuredClone(clip)),
|
|
183
|
+
);
|
|
159
184
|
},
|
|
160
185
|
});
|
|
161
186
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// ─── Convert to Component ─────────────────────────────────────────────────────
|
|
2
2
|
import { html, render as litRender } from "lit-html";
|
|
3
|
-
import {
|
|
3
|
+
import { getNodeAtPath, parentElementPath, childIndex } from "../store.js";
|
|
4
|
+
import { activeTab } from "../workspace/workspace.js";
|
|
5
|
+
import { transact } from "../tabs/transact.js";
|
|
4
6
|
import {
|
|
5
7
|
computeRelativePath,
|
|
6
8
|
loadComponentRegistry,
|
|
@@ -11,15 +13,12 @@ import { statusMessage } from "../panels/statusbar.js";
|
|
|
11
13
|
|
|
12
14
|
const VALID_NAME = /^[a-z][a-z0-9]*(-[a-z0-9]+)+$/;
|
|
13
15
|
|
|
14
|
-
/**
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
*/
|
|
19
|
-
export async function convertToComponent(S) {
|
|
20
|
-
if (!S.selection || S.selection.length < 2) return;
|
|
16
|
+
/** Convert the currently selected element into a reusable component. */
|
|
17
|
+
export async function convertToComponent() {
|
|
18
|
+
const tab = activeTab.value;
|
|
19
|
+
if (!tab?.session.selection || tab.session.selection.length < 2) return;
|
|
21
20
|
|
|
22
|
-
const node = getNodeAtPath(
|
|
21
|
+
const node = getNodeAtPath(tab.doc.document, tab.session.selection);
|
|
23
22
|
if (!node || !node.tagName) return;
|
|
24
23
|
|
|
25
24
|
const defaultName = deriveDefaultName(node);
|
|
@@ -32,11 +31,11 @@ export async function convertToComponent(S) {
|
|
|
32
31
|
|
|
33
32
|
// Compute paths
|
|
34
33
|
const componentFile = "components/" + name + ".json";
|
|
35
|
-
const refPath = computeRelativePath(
|
|
34
|
+
const refPath = computeRelativePath(tab.documentPath, componentFile);
|
|
36
35
|
|
|
37
36
|
// Single atomic mutation: replace node + add $elements ref
|
|
38
|
-
const selectionPath =
|
|
39
|
-
|
|
37
|
+
const selectionPath = tab.session.selection;
|
|
38
|
+
transact(tab, (doc) => {
|
|
40
39
|
// Navigate to parent's children array and replace the node
|
|
41
40
|
const pp = parentElementPath(selectionPath) ?? [];
|
|
42
41
|
const idx = childIndex(selectionPath);
|
|
@@ -54,8 +53,6 @@ export async function convertToComponent(S) {
|
|
|
54
53
|
}
|
|
55
54
|
});
|
|
56
55
|
|
|
57
|
-
update(newState);
|
|
58
|
-
|
|
59
56
|
// Write component file and refresh registry
|
|
60
57
|
try {
|
|
61
58
|
const platform = getPlatform();
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { showSlashMenu } from "./slash-menu.js";
|
|
9
|
+
import { activeTab } from "../workspace/workspace.js";
|
|
10
|
+
import { transactDoc, mutateInsertNode } from "../tabs/transact.js";
|
|
9
11
|
|
|
10
12
|
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
11
13
|
|
|
@@ -31,16 +33,11 @@ import { showSlashMenu } from "./slash-menu.js";
|
|
|
31
33
|
|
|
32
34
|
/**
|
|
33
35
|
* @typedef {Object} InsertionHelperContext
|
|
34
|
-
* @property {() => any} getState - Returns the current editor state.
|
|
35
|
-
* @property {(state: any) => void} update - Commits a new state.
|
|
36
36
|
* @property {() => string} getCanvasMode - Returns the active canvas mode.
|
|
37
37
|
* @property {(fn: Function) => any} withPanelPointerEvents - Executes fn with pointer-events
|
|
38
38
|
* temporarily enabled on the canvas.
|
|
39
39
|
* @property {() => number} effectiveZoom - Returns the current zoom scale factor.
|
|
40
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
41
|
* @property {(path: any[]) => any[] | null} parentElementPath - Returns the parent element path, or
|
|
45
42
|
* null for root.
|
|
46
43
|
* @property {(path: any[]) => string | number} childIndex - Returns the child index within the
|
|
@@ -284,18 +281,18 @@ function onHelperClick(/** @type {MouseEvent} */ e) {
|
|
|
284
281
|
function onSlashSelect(cmd, point) {
|
|
285
282
|
if (!_ctx) return;
|
|
286
283
|
|
|
287
|
-
const {
|
|
288
|
-
const S = getState();
|
|
284
|
+
const { defaultDef } = _ctx;
|
|
289
285
|
const { parentPath, idx, edge } = point;
|
|
290
286
|
|
|
291
287
|
const newDef = defaultDef(cmd.tag);
|
|
292
288
|
const insertPath = edge === "center" ? point.path : parentPath;
|
|
293
289
|
const insertIdx = edge === "center" ? 0 : idx;
|
|
294
|
-
|
|
295
|
-
let s = insertNode(S, insertPath, insertIdx, newDef);
|
|
296
290
|
const newPath = [...insertPath, "children", insertIdx];
|
|
297
|
-
|
|
298
|
-
|
|
291
|
+
|
|
292
|
+
transactDoc(activeTab.value, (t) => {
|
|
293
|
+
mutateInsertNode(t, insertPath, insertIdx, newDef);
|
|
294
|
+
t.session.selection = newPath;
|
|
295
|
+
});
|
|
299
296
|
|
|
300
297
|
hide();
|
|
301
298
|
}
|
package/src/editor/shortcuts.js
CHANGED
|
@@ -5,46 +5,43 @@
|
|
|
5
5
|
* shortcuts on the canvas / document.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { getNodeAtPath, parentElementPath, childIndex, canvasWrap } from "../store.js";
|
|
9
|
+
import { activeTab, workspace, closeTab } from "../workspace/workspace.js";
|
|
8
10
|
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
parentElementPath,
|
|
17
|
-
childIndex,
|
|
18
|
-
canvasWrap,
|
|
19
|
-
update,
|
|
20
|
-
} from "../store.js";
|
|
11
|
+
transactDoc,
|
|
12
|
+
mutateInsertNode,
|
|
13
|
+
mutateRemoveNode,
|
|
14
|
+
mutateDuplicateNode,
|
|
15
|
+
undo as tabUndo,
|
|
16
|
+
redo as tabRedo,
|
|
17
|
+
} from "../tabs/transact.js";
|
|
21
18
|
import { isEditing } from "./inline-edit.js";
|
|
22
19
|
import { copyNode, cutNode, pasteNode } from "./context-menu.js";
|
|
23
20
|
|
|
21
|
+
/** @typedef {import("../state.js").JxPath} JxPath */
|
|
22
|
+
|
|
24
23
|
/**
|
|
25
24
|
* Initialise all keyboard (and wheel/pointer) shortcuts.
|
|
26
25
|
*
|
|
27
26
|
* @param {() => {
|
|
28
|
-
* S: any;
|
|
29
|
-
* setS: (s: any) => void;
|
|
30
27
|
* canvasMode: string;
|
|
31
28
|
* panX: number;
|
|
32
29
|
* panY: number;
|
|
33
30
|
* setPan: (x: number, y: number) => void;
|
|
34
31
|
* applyTransform: () => void;
|
|
35
32
|
* positionZoomIndicator: () => void;
|
|
36
|
-
* componentInlineEdit:
|
|
33
|
+
* componentInlineEdit: object | null;
|
|
37
34
|
* saveFile: () => void;
|
|
38
35
|
* openProject: () => void;
|
|
39
|
-
* enterEditOnPath: (path:
|
|
36
|
+
* enterEditOnPath: (path: JxPath) => void;
|
|
40
37
|
* }} getContext
|
|
41
38
|
*/
|
|
42
39
|
export function initShortcuts(getContext) {
|
|
43
40
|
// Wheel handler: Ctrl+Scroll = zoom (cursor-centered), plain scroll = pan
|
|
44
41
|
canvasWrap.addEventListener(
|
|
45
42
|
"wheel",
|
|
46
|
-
(/** @type {
|
|
47
|
-
const {
|
|
43
|
+
(/** @type {WheelEvent} */ e) => {
|
|
44
|
+
const { canvasMode, panX, panY, setPan, applyTransform } = getContext();
|
|
48
45
|
// Edit (content) mode: let the scroll container handle scrolling natively
|
|
49
46
|
if (canvasMode === "edit") return;
|
|
50
47
|
e.preventDefault();
|
|
@@ -53,13 +50,13 @@ export function initShortcuts(getContext) {
|
|
|
53
50
|
const rect = canvasWrap.getBoundingClientRect();
|
|
54
51
|
const cursorX = e.clientX - rect.left;
|
|
55
52
|
const cursorY = e.clientY - rect.top;
|
|
56
|
-
const oldZoom =
|
|
53
|
+
const oldZoom = activeTab.value?.session.ui.zoom ?? 1;
|
|
57
54
|
const delta = -e.deltaY * 0.005;
|
|
58
55
|
const newZoom = Math.min(5.0, Math.max(0.05, oldZoom * (1 + delta)));
|
|
59
56
|
const ratio = newZoom / oldZoom;
|
|
60
57
|
// Adjust pan so the point under cursor stays stationary
|
|
61
58
|
setPan(cursorX - (cursorX - panX) * ratio, cursorY - (cursorY - panY) * ratio);
|
|
62
|
-
|
|
59
|
+
activeTab.value.session.ui.zoom = newZoom;
|
|
63
60
|
} else if (e.shiftKey) {
|
|
64
61
|
// Shift+scroll = horizontal pan
|
|
65
62
|
setPan(panX - e.deltaY, panY);
|
|
@@ -73,7 +70,7 @@ export function initShortcuts(getContext) {
|
|
|
73
70
|
);
|
|
74
71
|
|
|
75
72
|
// Middle-mouse drag panning
|
|
76
|
-
canvasWrap.addEventListener("pointerdown", (/** @type {
|
|
73
|
+
canvasWrap.addEventListener("pointerdown", (/** @type {PointerEvent} */ e) => {
|
|
77
74
|
const ctx = getContext();
|
|
78
75
|
if (ctx.canvasMode === "edit") return; // no panning in edit mode
|
|
79
76
|
if (e.button !== 1) return; // middle button only
|
|
@@ -81,7 +78,7 @@ export function initShortcuts(getContext) {
|
|
|
81
78
|
canvasWrap.setPointerCapture(e.pointerId);
|
|
82
79
|
let lastX = e.clientX,
|
|
83
80
|
lastY = e.clientY;
|
|
84
|
-
const onMove = (/** @type {
|
|
81
|
+
const onMove = (/** @type {PointerEvent} */ ev) => {
|
|
85
82
|
const { panX, panY, setPan, applyTransform } = getContext();
|
|
86
83
|
setPan(panX + (ev.clientX - lastX), panY + (ev.clientY - lastY));
|
|
87
84
|
lastX = ev.clientX;
|
|
@@ -102,8 +99,6 @@ export function initShortcuts(getContext) {
|
|
|
102
99
|
|
|
103
100
|
document.addEventListener("keydown", (e) => {
|
|
104
101
|
const {
|
|
105
|
-
S,
|
|
106
|
-
setS,
|
|
107
102
|
canvasMode,
|
|
108
103
|
setPan,
|
|
109
104
|
applyTransform,
|
|
@@ -112,6 +107,7 @@ export function initShortcuts(getContext) {
|
|
|
112
107
|
openProject,
|
|
113
108
|
enterEditOnPath,
|
|
114
109
|
} = getContext();
|
|
110
|
+
const tab = activeTab.value;
|
|
115
111
|
const mod = e.ctrlKey || e.metaKey;
|
|
116
112
|
|
|
117
113
|
// Don't intercept when typing in inputs or contenteditable
|
|
@@ -125,14 +121,19 @@ export function initShortcuts(getContext) {
|
|
|
125
121
|
e.preventDefault();
|
|
126
122
|
saveFile();
|
|
127
123
|
}
|
|
124
|
+
if (mod && e.key === "w") {
|
|
125
|
+
e.preventDefault();
|
|
126
|
+
}
|
|
128
127
|
return;
|
|
129
128
|
}
|
|
130
129
|
if (isEditing()) {
|
|
131
|
-
// Let inline editor handle its own keyboard events; only intercept Save
|
|
132
130
|
if (mod && e.key === "s") {
|
|
133
131
|
e.preventDefault();
|
|
134
132
|
saveFile();
|
|
135
133
|
}
|
|
134
|
+
if (mod && e.key === "w") {
|
|
135
|
+
e.preventDefault();
|
|
136
|
+
}
|
|
136
137
|
return;
|
|
137
138
|
}
|
|
138
139
|
if (componentInlineEdit) {
|
|
@@ -140,11 +141,25 @@ export function initShortcuts(getContext) {
|
|
|
140
141
|
e.preventDefault();
|
|
141
142
|
saveFile();
|
|
142
143
|
}
|
|
144
|
+
if (mod && e.key === "w") {
|
|
145
|
+
e.preventDefault();
|
|
146
|
+
}
|
|
143
147
|
return;
|
|
144
148
|
}
|
|
145
149
|
|
|
146
150
|
if (mod) {
|
|
147
151
|
switch (e.key) {
|
|
152
|
+
case "w":
|
|
153
|
+
e.preventDefault();
|
|
154
|
+
if (workspace.activeTabId && workspace.tabOrder.length > 1) {
|
|
155
|
+
const tab = workspace.tabs.get(workspace.activeTabId);
|
|
156
|
+
if (tab?.doc.dirty) {
|
|
157
|
+
const name = tab.documentPath?.split("/").pop() || "Untitled";
|
|
158
|
+
if (!window.confirm(`"${name}" has unsaved changes. Close without saving?`)) break;
|
|
159
|
+
}
|
|
160
|
+
closeTab(workspace.activeTabId);
|
|
161
|
+
}
|
|
162
|
+
break;
|
|
148
163
|
case "o":
|
|
149
164
|
e.preventDefault();
|
|
150
165
|
openProject();
|
|
@@ -155,28 +170,32 @@ export function initShortcuts(getContext) {
|
|
|
155
170
|
break;
|
|
156
171
|
case "z":
|
|
157
172
|
e.preventDefault();
|
|
158
|
-
|
|
173
|
+
if (e.shiftKey) tabRedo(activeTab.value);
|
|
174
|
+
else tabUndo(activeTab.value);
|
|
159
175
|
break;
|
|
160
176
|
case "d":
|
|
161
177
|
e.preventDefault();
|
|
162
|
-
if (
|
|
178
|
+
if (tab?.session.selection) {
|
|
179
|
+
const sel = tab.session.selection;
|
|
180
|
+
transactDoc(tab, (t) => mutateDuplicateNode(t, sel));
|
|
181
|
+
}
|
|
163
182
|
break;
|
|
164
183
|
case "c":
|
|
165
184
|
e.preventDefault();
|
|
166
|
-
copyNode(
|
|
185
|
+
copyNode();
|
|
167
186
|
break;
|
|
168
187
|
case "x":
|
|
169
188
|
e.preventDefault();
|
|
170
|
-
cutNode(
|
|
189
|
+
cutNode();
|
|
171
190
|
break;
|
|
172
191
|
case "v":
|
|
173
192
|
e.preventDefault();
|
|
174
|
-
pasteNode(
|
|
193
|
+
pasteNode();
|
|
175
194
|
break;
|
|
176
195
|
case "0":
|
|
177
196
|
if (canvasMode === "edit") break;
|
|
178
197
|
e.preventDefault();
|
|
179
|
-
|
|
198
|
+
activeTab.value.session.ui.zoom = 1;
|
|
180
199
|
setPan(16, 16);
|
|
181
200
|
applyTransform();
|
|
182
201
|
break;
|
|
@@ -184,13 +203,13 @@ export function initShortcuts(getContext) {
|
|
|
184
203
|
case "+":
|
|
185
204
|
if (canvasMode === "edit") break;
|
|
186
205
|
e.preventDefault();
|
|
187
|
-
|
|
206
|
+
activeTab.value.session.ui.zoom = Math.min(5.0, (tab?.session.ui.zoom ?? 1) * 1.2);
|
|
188
207
|
applyTransform();
|
|
189
208
|
break;
|
|
190
209
|
case "-":
|
|
191
210
|
if (canvasMode === "edit") break;
|
|
192
211
|
e.preventDefault();
|
|
193
|
-
|
|
212
|
+
activeTab.value.session.ui.zoom = Math.max(0.05, (tab?.session.ui.zoom ?? 1) / 1.2);
|
|
194
213
|
applyTransform();
|
|
195
214
|
break;
|
|
196
215
|
}
|
|
@@ -200,46 +219,48 @@ export function initShortcuts(getContext) {
|
|
|
200
219
|
switch (e.key) {
|
|
201
220
|
case "Delete":
|
|
202
221
|
case "Backspace":
|
|
203
|
-
if (
|
|
222
|
+
if (tab?.session.selection && tab.session.selection.length >= 2) {
|
|
204
223
|
e.preventDefault();
|
|
205
|
-
|
|
224
|
+
const sel = tab.session.selection;
|
|
225
|
+
transactDoc(tab, (t) => mutateRemoveNode(t, sel));
|
|
206
226
|
}
|
|
207
227
|
break;
|
|
208
228
|
case "Escape":
|
|
209
|
-
|
|
229
|
+
activeTab.value.session.selection = null;
|
|
210
230
|
break;
|
|
211
231
|
case "Enter":
|
|
212
|
-
if (
|
|
232
|
+
if (tab?.session.selection && tab.session.selection.length >= 2) {
|
|
213
233
|
e.preventDefault();
|
|
214
|
-
const pp = /** @type {
|
|
215
|
-
const idx = /** @type {number} */ (childIndex(
|
|
216
|
-
let s = insertNode(S, pp, idx + 1, { tagName: "p", textContent: "" });
|
|
234
|
+
const pp = /** @type {JxPath} */ (parentElementPath(tab.session.selection));
|
|
235
|
+
const idx = /** @type {number} */ (childIndex(tab.session.selection));
|
|
217
236
|
const newPath = [...pp, "children", idx + 1];
|
|
218
|
-
|
|
219
|
-
|
|
237
|
+
transactDoc(tab, (t) => {
|
|
238
|
+
mutateInsertNode(t, pp, idx + 1, { tagName: "p", textContent: "" });
|
|
239
|
+
t.session.selection = newPath;
|
|
240
|
+
});
|
|
220
241
|
enterEditOnPath(newPath);
|
|
221
242
|
}
|
|
222
243
|
break;
|
|
223
244
|
case "ArrowUp":
|
|
224
245
|
e.preventDefault();
|
|
225
|
-
navigateSelection(
|
|
246
|
+
navigateSelection(-1);
|
|
226
247
|
break;
|
|
227
248
|
case "ArrowDown":
|
|
228
249
|
e.preventDefault();
|
|
229
|
-
navigateSelection(
|
|
250
|
+
navigateSelection(1);
|
|
230
251
|
break;
|
|
231
252
|
case "ArrowLeft":
|
|
232
253
|
e.preventDefault();
|
|
233
|
-
if (
|
|
234
|
-
|
|
254
|
+
if (tab?.session.selection && tab.session.selection.length >= 2) {
|
|
255
|
+
activeTab.value.session.selection = parentElementPath(tab.session.selection);
|
|
235
256
|
}
|
|
236
257
|
break;
|
|
237
258
|
case "ArrowRight":
|
|
238
259
|
e.preventDefault();
|
|
239
|
-
if (
|
|
240
|
-
const node = getNodeAtPath(
|
|
260
|
+
if (tab?.session.selection) {
|
|
261
|
+
const node = getNodeAtPath(tab.doc.document, tab.session.selection);
|
|
241
262
|
if (node?.children?.length > 0) {
|
|
242
|
-
|
|
263
|
+
activeTab.value.session.selection = [...tab.session.selection, "children", 0];
|
|
243
264
|
}
|
|
244
265
|
}
|
|
245
266
|
break;
|
|
@@ -249,8 +270,8 @@ export function initShortcuts(getContext) {
|
|
|
249
270
|
// Block ctrl+scroll (browser zoom) on all non-canvas areas
|
|
250
271
|
document.addEventListener(
|
|
251
272
|
"wheel",
|
|
252
|
-
(/** @type {
|
|
253
|
-
if ((e.ctrlKey || e.metaKey) && !canvasWrap.contains(e.target)) {
|
|
273
|
+
(/** @type {WheelEvent} */ e) => {
|
|
274
|
+
if ((e.ctrlKey || e.metaKey) && !canvasWrap.contains(/** @type {Node} */ (e.target))) {
|
|
254
275
|
e.preventDefault();
|
|
255
276
|
}
|
|
256
277
|
},
|
|
@@ -258,23 +279,26 @@ export function initShortcuts(getContext) {
|
|
|
258
279
|
);
|
|
259
280
|
}
|
|
260
281
|
|
|
261
|
-
/**
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
if (!S.selection) {
|
|
267
|
-
update(selectNode(S, []));
|
|
282
|
+
/** @param {number} [direction] */
|
|
283
|
+
function navigateSelection(direction = -1) {
|
|
284
|
+
const tab = activeTab.value;
|
|
285
|
+
if (!tab?.session.selection) {
|
|
286
|
+
activeTab.value.session.selection = [];
|
|
268
287
|
return;
|
|
269
288
|
}
|
|
270
|
-
if (
|
|
289
|
+
if (tab.session.selection.length < 2) return;
|
|
271
290
|
|
|
272
|
-
const parent = getNodeAtPath(
|
|
273
|
-
|
|
291
|
+
const parent = getNodeAtPath(
|
|
292
|
+
tab.doc.document,
|
|
293
|
+
/** @type {JxPath} */ (parentElementPath(tab.session.selection)),
|
|
294
|
+
);
|
|
295
|
+
const idx = /** @type {number} */ (childIndex(tab.session.selection));
|
|
274
296
|
const newIdx = idx + direction;
|
|
275
297
|
if (parent?.children && newIdx >= 0 && newIdx < parent.children.length) {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
298
|
+
activeTab.value.session.selection = [
|
|
299
|
+
.../** @type {JxPath} */ (parentElementPath(tab.session.selection)),
|
|
300
|
+
"children",
|
|
301
|
+
newIdx,
|
|
302
|
+
];
|
|
279
303
|
}
|
|
280
304
|
}
|