svedit 0.11.1 → 0.13.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/README.md +260 -82
- package/dist/Command.svelte.d.ts +54 -29
- package/dist/Command.svelte.d.ts.map +1 -1
- package/dist/Command.svelte.js +286 -283
- package/dist/CustomProperty.svelte +3 -5
- package/dist/CustomProperty.svelte.d.ts +3 -6
- package/dist/CustomProperty.svelte.d.ts.map +1 -1
- package/dist/KeyMapper.svelte.d.ts +19 -6
- package/dist/KeyMapper.svelte.d.ts.map +1 -1
- package/dist/KeyMapper.svelte.js +95 -112
- package/dist/Node.svelte +40 -9
- package/dist/Node.svelte.d.ts +3 -5
- package/dist/Node.svelte.d.ts.map +1 -1
- package/dist/NodeArrayProperty.svelte +142 -18
- package/dist/NodeArrayProperty.svelte.d.ts +3 -5
- package/dist/NodeArrayProperty.svelte.d.ts.map +1 -1
- package/dist/NodeCaret.svelte +1 -1
- package/dist/NodeCaret.svelte.d.ts +6 -14
- package/dist/NodeCaret.svelte.d.ts.map +1 -1
- package/dist/NodeGap.svelte +60 -12
- package/dist/NodeGap.svelte.d.ts +7 -14
- package/dist/NodeGap.svelte.d.ts.map +1 -1
- package/dist/NodeGapMarkers.svelte +19 -10
- package/dist/NodeGapMarkers.svelte.d.ts +6 -11
- package/dist/NodeGapMarkers.svelte.d.ts.map +1 -1
- package/dist/NodeSelectionMarkers.svelte +20 -6
- package/dist/NodeSelectionMarkers.svelte.d.ts +2 -5
- package/dist/NodeSelectionMarkers.svelte.d.ts.map +1 -1
- package/dist/Session.svelte.d.ts +85 -116
- package/dist/Session.svelte.d.ts.map +1 -1
- package/dist/Session.svelte.js +543 -571
- package/dist/Svedit.svelte +243 -216
- package/dist/Svedit.svelte.d.ts +30 -10
- package/dist/Svedit.svelte.d.ts.map +1 -1
- package/dist/{AnnotatedTextProperty.svelte → TextProperty.svelte} +71 -56
- package/dist/TextProperty.svelte.d.ts +5 -0
- package/dist/TextProperty.svelte.d.ts.map +1 -0
- package/dist/Transaction.svelte.d.ts +101 -126
- package/dist/Transaction.svelte.d.ts.map +1 -1
- package/dist/Transaction.svelte.js +843 -827
- package/dist/UnknownNode.svelte +4 -3
- package/dist/UnknownNode.svelte.d.ts +5 -9
- package/dist/UnknownNode.svelte.d.ts.map +1 -1
- package/dist/doc_utils.d.ts +109 -122
- package/dist/doc_utils.d.ts.map +1 -1
- package/dist/doc_utils.js +726 -698
- package/dist/index.d.ts +20 -17
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -18
- package/dist/node_visibility.svelte.d.ts +132 -5
- package/dist/node_visibility.svelte.d.ts.map +1 -1
- package/dist/node_visibility.svelte.js +535 -642
- package/dist/paste_utils.d.ts +11 -43
- package/dist/paste_utils.d.ts.map +1 -1
- package/dist/paste_utils.js +111 -149
- package/dist/transforms.svelte.d.ts +9 -7
- package/dist/transforms.svelte.d.ts.map +1 -1
- package/dist/transforms.svelte.js +126 -152
- package/dist/types.d.ts +329 -291
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/utils.d.ts +79 -95
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +392 -305
- package/package.json +69 -67
- package/src/lib/{Command.svelte.js → Command.svelte.ts} +111 -41
- package/src/lib/CustomProperty.svelte +3 -5
- package/src/lib/{KeyMapper.svelte.js → KeyMapper.svelte.ts} +28 -11
- package/src/lib/Node.svelte +40 -9
- package/src/lib/NodeArrayProperty.svelte +142 -18
- package/src/lib/NodeCaret.svelte +1 -1
- package/src/lib/NodeGap.svelte +60 -12
- package/src/lib/NodeGapMarkers.svelte +19 -10
- package/src/lib/NodeSelectionMarkers.svelte +20 -6
- package/src/lib/Session.svelte.ts +656 -0
- package/src/lib/Svedit.svelte +243 -216
- package/src/lib/{AnnotatedTextProperty.svelte → TextProperty.svelte} +71 -56
- package/src/lib/Transaction.svelte.ts +1020 -0
- package/src/lib/UnknownNode.svelte +4 -3
- package/src/lib/doc_utils.ts +1120 -0
- package/src/lib/{index.js → index.ts} +48 -1
- package/src/lib/node_visibility.svelte.ts +658 -0
- package/src/lib/{paste_utils.js → paste_utils.ts} +51 -59
- package/src/lib/{transforms.svelte.js → transforms.svelte.ts} +44 -27
- package/src/lib/{types.d.ts → types.ts} +250 -103
- package/src/lib/utils.ts +660 -0
- package/dist/AnnotatedTextProperty.svelte.d.ts +0 -7
- package/dist/AnnotatedTextProperty.svelte.d.ts.map +0 -1
- package/src/lib/Session.svelte.js +0 -575
- package/src/lib/Transaction.svelte.js +0 -846
- package/src/lib/doc_utils.js +0 -807
- package/src/lib/node_visibility.svelte.js +0 -718
- package/src/lib/utils.js +0 -465
package/package.json
CHANGED
|
@@ -1,68 +1,70 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
2
|
+
"name": "svedit",
|
|
3
|
+
"version": "0.13.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"files": [
|
|
6
|
+
"dist",
|
|
7
|
+
"!dist/**/*.test.*",
|
|
8
|
+
"!dist/**/*.spec.*",
|
|
9
|
+
"src/lib",
|
|
10
|
+
"!src/lib/**/*.test.*",
|
|
11
|
+
"!src/lib/**/*.spec.*"
|
|
12
|
+
],
|
|
13
|
+
"sideEffects": [
|
|
14
|
+
"**/*.css"
|
|
15
|
+
],
|
|
16
|
+
"svelte": "./dist/index.js",
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"type": "module",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"svelte": "./dist/index.js"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"svelte": "^5.0.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@eslint/compat": "^2.0.3",
|
|
30
|
+
"@eslint/js": "^10.0.1",
|
|
31
|
+
"@sveltejs/adapter-auto": "^7.0.0",
|
|
32
|
+
"@sveltejs/kit": "^2.55.0",
|
|
33
|
+
"@sveltejs/package": "^2.5.7",
|
|
34
|
+
"@sveltejs/vite-plugin-svelte": "^7.0.0",
|
|
35
|
+
"@types/node": "^25.6.0",
|
|
36
|
+
"@typescript-eslint/eslint-plugin": "^8.57.0",
|
|
37
|
+
"@typescript-eslint/parser": "^8.57.0",
|
|
38
|
+
"@vitest/browser": "^4.1.0",
|
|
39
|
+
"@vitest/browser-preview": "^4.1.0",
|
|
40
|
+
"eslint": "^10.0.3",
|
|
41
|
+
"eslint-config-prettier": "^10.1.8",
|
|
42
|
+
"eslint-plugin-svelte": "^3.15.1",
|
|
43
|
+
"globals": "^17.4.0",
|
|
44
|
+
"nanoid": "^5.1.6",
|
|
45
|
+
"prettier": "^3.8.1",
|
|
46
|
+
"prettier-plugin-svelte": "^3.5.1",
|
|
47
|
+
"publint": "^0.3.18",
|
|
48
|
+
"svelte": "^5.53.12",
|
|
49
|
+
"svelte-check": "^4.4.5",
|
|
50
|
+
"typescript": "^5.9.3",
|
|
51
|
+
"vite": "^8.0.0",
|
|
52
|
+
"vitest": "^4.1.0",
|
|
53
|
+
"vitest-browser-svelte": "^2.1.0"
|
|
54
|
+
},
|
|
55
|
+
"allowScripts": {
|
|
56
|
+
"fsevents@2.3.3": true
|
|
57
|
+
},
|
|
58
|
+
"scripts": {
|
|
59
|
+
"dev": "vite dev",
|
|
60
|
+
"build": "vite build",
|
|
61
|
+
"preview": "vite preview",
|
|
62
|
+
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
|
63
|
+
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
|
64
|
+
"check:strict": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.strict.json",
|
|
65
|
+
"format": "prettier --write .",
|
|
66
|
+
"lint": "prettier --check . && eslint .",
|
|
67
|
+
"test:unit": "vitest",
|
|
68
|
+
"test": "pnpm test:unit --run"
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import { insert_default_node, break_text_node } from './transforms.svelte.js';
|
|
2
|
-
import {
|
|
2
|
+
import { can_switch_mark_type, get_selected_range_types } from './doc_utils.js';
|
|
3
3
|
import { is_selection_collapsed, get_char_length, char_slice } from './utils.js';
|
|
4
|
+
import type Session from './Session.svelte.js';
|
|
5
|
+
|
|
6
|
+
// Commands operate on sessions with arbitrary application schemas.
|
|
7
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
8
|
+
type CommandSession = Session<any>;
|
|
9
|
+
|
|
10
|
+
export type CommandContext = {
|
|
11
|
+
session: CommandSession;
|
|
12
|
+
editable: boolean;
|
|
13
|
+
};
|
|
4
14
|
|
|
5
15
|
/**
|
|
6
16
|
* Base class for commands that can be executed in response to user actions
|
|
@@ -25,6 +35,8 @@ import { is_selection_collapsed, get_char_length, char_slice } from './utils.js'
|
|
|
25
35
|
* ```
|
|
26
36
|
*/
|
|
27
37
|
export default class Command {
|
|
38
|
+
context: CommandContext;
|
|
39
|
+
|
|
28
40
|
/**
|
|
29
41
|
* Derived state that indicates if the command is disabled.
|
|
30
42
|
* Automatically computed from is_enabled().
|
|
@@ -34,19 +46,17 @@ export default class Command {
|
|
|
34
46
|
/**
|
|
35
47
|
* Creates a new Command instance.
|
|
36
48
|
*
|
|
37
|
-
* @param
|
|
49
|
+
* @param context - The context object providing access to application state
|
|
38
50
|
*/
|
|
39
|
-
constructor(context) {
|
|
51
|
+
constructor(context: CommandContext) {
|
|
40
52
|
this.context = context;
|
|
41
53
|
}
|
|
42
54
|
|
|
43
55
|
/**
|
|
44
56
|
* Determines if the command can currently be executed.
|
|
45
57
|
* Override this method to implement command-specific logic.
|
|
46
|
-
*
|
|
47
|
-
* @returns {boolean} true if the command can be executed, false otherwise
|
|
48
58
|
*/
|
|
49
|
-
is_enabled() {
|
|
59
|
+
is_enabled(): boolean {
|
|
50
60
|
return true;
|
|
51
61
|
}
|
|
52
62
|
|
|
@@ -54,10 +64,8 @@ export default class Command {
|
|
|
54
64
|
* Executes the command.
|
|
55
65
|
* Override this method to implement the command's behavior.
|
|
56
66
|
* Can be async for commands that need to perform asynchronous operations.
|
|
57
|
-
*
|
|
58
|
-
* @returns {void | Promise<void>}
|
|
59
67
|
*/
|
|
60
|
-
execute() {
|
|
68
|
+
execute(): void | Promise<void> {
|
|
61
69
|
throw new Error('Not implemented');
|
|
62
70
|
}
|
|
63
71
|
}
|
|
@@ -94,7 +102,7 @@ export class RedoCommand extends Command {
|
|
|
94
102
|
*/
|
|
95
103
|
export class SelectParentCommand extends Command {
|
|
96
104
|
is_enabled() {
|
|
97
|
-
return (
|
|
105
|
+
return Boolean(
|
|
98
106
|
this.context.editable &&
|
|
99
107
|
this.context.session.selection &&
|
|
100
108
|
this.context.session.selection.path.length > 3
|
|
@@ -107,11 +115,16 @@ export class SelectParentCommand extends Command {
|
|
|
107
115
|
}
|
|
108
116
|
|
|
109
117
|
/**
|
|
110
|
-
* Generic command that toggles
|
|
111
|
-
*
|
|
118
|
+
* Generic command that toggles a mark on the current text or node selection.
|
|
119
|
+
*
|
|
120
|
+
* Marks are mutually exclusive, so all touched marks compete: same-type marks
|
|
121
|
+
* are removed, a single touched property-less mark may switch type, and mixed
|
|
122
|
+
* touched types disable the command. Annotations never affect mark toggling.
|
|
112
123
|
*/
|
|
113
|
-
export class
|
|
114
|
-
|
|
124
|
+
export class ToggleMarkCommand extends Command {
|
|
125
|
+
node_type: string;
|
|
126
|
+
|
|
127
|
+
constructor(node_type: string, context: CommandContext) {
|
|
115
128
|
super(context);
|
|
116
129
|
this.node_type = node_type;
|
|
117
130
|
}
|
|
@@ -119,34 +132,90 @@ export class ToggleAnnotationCommand extends Command {
|
|
|
119
132
|
active = $derived(this.is_active());
|
|
120
133
|
|
|
121
134
|
is_active() {
|
|
122
|
-
|
|
135
|
+
const selected_marks = this.context.session.selected_marks;
|
|
136
|
+
return (
|
|
137
|
+
selected_marks.length > 0 && selected_marks.every(({ node }) => node?.type === this.node_type)
|
|
138
|
+
);
|
|
123
139
|
}
|
|
124
140
|
|
|
125
141
|
is_enabled() {
|
|
126
142
|
const { session, editable } = this.context;
|
|
127
|
-
const
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
143
|
+
const selection = session.selection;
|
|
144
|
+
const is_valid_selection =
|
|
145
|
+
selection?.type === 'text' ||
|
|
146
|
+
(selection?.type === 'node' && !is_selection_collapsed(selection));
|
|
147
|
+
const mark_type_is_allowed = session.available_mark_types.includes(this.node_type);
|
|
148
|
+
const selected_marks = session.selected_marks;
|
|
149
|
+
const selected_mark_types = get_selected_range_types(selected_marks);
|
|
150
|
+
|
|
151
|
+
if (!editable || !is_valid_selection || !mark_type_is_allowed) return false;
|
|
152
|
+
if (selected_mark_types.size > 1) return false;
|
|
153
|
+
|
|
154
|
+
if (selected_marks.length === 0) {
|
|
155
|
+
return Boolean(selection && !is_selection_collapsed(selection));
|
|
156
|
+
}
|
|
140
157
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
158
|
+
const first_selected_mark = selected_marks[0];
|
|
159
|
+
const selected_mark_type = first_selected_mark.node.type;
|
|
160
|
+
if (selected_mark_type === this.node_type) return true;
|
|
161
|
+
if (selected_marks.length !== 1) return false;
|
|
162
|
+
|
|
163
|
+
return can_switch_mark_type(session.schema, selected_mark_type, this.node_type);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
execute() {
|
|
167
|
+
this.context.session.apply(this.context.session.tr.toggle_mark(this.node_type));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Generic command that toggles an annotation on the current text or node selection.
|
|
173
|
+
*
|
|
174
|
+
* Annotations only compete with touched annotations of the same type: touched
|
|
175
|
+
* same-type annotations are removed, otherwise a new annotation is created.
|
|
176
|
+
* Marks and other annotation types never block the toggle.
|
|
177
|
+
*/
|
|
178
|
+
export class ToggleAnnotationCommand extends Command {
|
|
179
|
+
node_type: string;
|
|
180
|
+
|
|
181
|
+
constructor(node_type: string, context: CommandContext) {
|
|
182
|
+
super(context);
|
|
183
|
+
this.node_type = node_type;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
active = $derived(this.is_active());
|
|
187
|
+
|
|
188
|
+
// Mirrors the filter in tr.toggle_annotation, so active/enabled match what
|
|
189
|
+
// execute() does.
|
|
190
|
+
relevant_annotations() {
|
|
191
|
+
return this.context.session.selected_annotations.filter(
|
|
192
|
+
({ node }) => node?.type === this.node_type
|
|
145
193
|
);
|
|
146
194
|
}
|
|
147
195
|
|
|
196
|
+
is_active() {
|
|
197
|
+
return this.relevant_annotations().length > 0;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
is_enabled() {
|
|
201
|
+
const { session, editable } = this.context;
|
|
202
|
+
const selection = session.selection;
|
|
203
|
+
const is_valid_selection =
|
|
204
|
+
selection?.type === 'text' ||
|
|
205
|
+
(selection?.type === 'node' && !is_selection_collapsed(selection));
|
|
206
|
+
const annotation_type_is_allowed = session.available_annotation_types.includes(this.node_type);
|
|
207
|
+
|
|
208
|
+
if (!editable || !is_valid_selection || !annotation_type_is_allowed) return false;
|
|
209
|
+
|
|
210
|
+
// Removing touched same-type annotations also works from a collapsed
|
|
211
|
+
// caret inside the annotation; creating a new one requires an expanded
|
|
212
|
+
// selection.
|
|
213
|
+
if (this.relevant_annotations().length > 0) return true;
|
|
214
|
+
return Boolean(selection && !is_selection_collapsed(selection));
|
|
215
|
+
}
|
|
216
|
+
|
|
148
217
|
execute() {
|
|
149
|
-
this.context.session.apply(this.context.session.tr.
|
|
218
|
+
this.context.session.apply(this.context.session.tr.toggle_annotation(this.node_type));
|
|
150
219
|
}
|
|
151
220
|
}
|
|
152
221
|
|
|
@@ -176,10 +245,11 @@ export class AddNewLineCommand extends Command {
|
|
|
176
245
|
if (selection.anchor_offset !== selection.focus_offset) {
|
|
177
246
|
tr.delete_selection();
|
|
178
247
|
}
|
|
248
|
+
if (tr.selection.type !== 'text') return;
|
|
179
249
|
|
|
180
250
|
const collapsed_offset = tr.selection.anchor_offset;
|
|
181
251
|
const content = tr.get(tr.selection.path);
|
|
182
|
-
const text_before_caret = char_slice(content.
|
|
252
|
+
const text_before_caret = char_slice(content.content, 0, collapsed_offset);
|
|
183
253
|
const line_start_index = text_before_caret.lastIndexOf('\n') + 1;
|
|
184
254
|
const current_line_prefix = text_before_caret.slice(line_start_index);
|
|
185
255
|
const indentation_match = current_line_prefix.match(/^[\t ]*/);
|
|
@@ -222,7 +292,7 @@ export class BreakTextNodeCommand extends Command {
|
|
|
222
292
|
*/
|
|
223
293
|
export class SelectAllCommand extends Command {
|
|
224
294
|
is_enabled() {
|
|
225
|
-
return this.context.editable && this.context.session.selection;
|
|
295
|
+
return Boolean(this.context.editable && this.context.session.selection);
|
|
226
296
|
}
|
|
227
297
|
|
|
228
298
|
execute() {
|
|
@@ -235,7 +305,7 @@ export class SelectAllCommand extends Command {
|
|
|
235
305
|
|
|
236
306
|
if (selection.type === 'text') {
|
|
237
307
|
const text_content = session.get(selection.path);
|
|
238
|
-
const text_length = get_char_length(text_content.
|
|
308
|
+
const text_length = get_char_length(text_content.content);
|
|
239
309
|
|
|
240
310
|
// Check if all text is already selected
|
|
241
311
|
const is_all_text_selected =
|
|
@@ -260,7 +330,7 @@ export class SelectAllCommand extends Command {
|
|
|
260
330
|
session.inspect(node_path.slice(0, -1))?.type === 'node_array';
|
|
261
331
|
|
|
262
332
|
if (is_inside_node_array) {
|
|
263
|
-
const node_index =
|
|
333
|
+
const node_index = node_path.at(-1) as number;
|
|
264
334
|
session.selection = {
|
|
265
335
|
type: 'node',
|
|
266
336
|
path: node_path.slice(0, -1),
|
|
@@ -278,7 +348,7 @@ export class SelectAllCommand extends Command {
|
|
|
278
348
|
// Check if the entire node_array is already selected
|
|
279
349
|
const is_entire_node_array_selected =
|
|
280
350
|
Math.min(selection.anchor_offset, selection.focus_offset) === 0 &&
|
|
281
|
-
Math.max(selection.anchor_offset, selection.focus_offset) === node_array.length;
|
|
351
|
+
Math.max(selection.anchor_offset, selection.focus_offset) === node_array.nodes.length;
|
|
282
352
|
|
|
283
353
|
if (!is_entire_node_array_selected) {
|
|
284
354
|
// Select the entire node_array
|
|
@@ -286,7 +356,7 @@ export class SelectAllCommand extends Command {
|
|
|
286
356
|
type: 'node',
|
|
287
357
|
path: node_array_path,
|
|
288
358
|
anchor_offset: 0,
|
|
289
|
-
focus_offset: node_array.length
|
|
359
|
+
focus_offset: node_array.nodes.length
|
|
290
360
|
};
|
|
291
361
|
} else {
|
|
292
362
|
// Entire node_array is selected, try to move up to parent node_array
|
|
@@ -298,7 +368,7 @@ export class SelectAllCommand extends Command {
|
|
|
298
368
|
session.inspect(parent_path.slice(0, -1))?.type === 'node_array';
|
|
299
369
|
|
|
300
370
|
if (is_parent_node_array) {
|
|
301
|
-
const parent_node_index =
|
|
371
|
+
const parent_node_index = parent_path.at(-1) as number;
|
|
302
372
|
session.selection = {
|
|
303
373
|
type: 'node',
|
|
304
374
|
path: parent_path.slice(0, -1),
|
|
@@ -318,7 +388,7 @@ export class SelectAllCommand extends Command {
|
|
|
318
388
|
const is_inside_node_array = session.inspect(node_path.slice(0, -1))?.type === 'node_array';
|
|
319
389
|
|
|
320
390
|
if (is_inside_node_array) {
|
|
321
|
-
const node_index =
|
|
391
|
+
const node_index = node_path.at(-1) as number;
|
|
322
392
|
session.selection = {
|
|
323
393
|
type: 'node',
|
|
324
394
|
path: node_path.slice(0, -1),
|
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
<script>
|
|
1
|
+
<script lang="ts">
|
|
2
2
|
import { serialize_path } from './utils.js';
|
|
3
|
+
import type { CustomPropertyProps } from './types.js';
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
/** @type {CustomPropertyProps} */
|
|
7
|
-
let { path, tag = 'div', class: css_class, children, style, ...rest } = $props();
|
|
5
|
+
let { path, tag = 'div', class: css_class, children, style, ...rest }: CustomPropertyProps = $props();
|
|
8
6
|
let path_str = $derived(serialize_path(path));
|
|
9
7
|
|
|
10
8
|
</script>
|
|
@@ -1,20 +1,30 @@
|
|
|
1
1
|
import { is_virtual_keyboard_active } from './utils.js';
|
|
2
2
|
|
|
3
|
-
const MODIFIER_KEYS = ['meta', 'ctrl', 'alt', 'shift'];
|
|
4
|
-
const MODIFIER_EVENT_KEYS = {
|
|
3
|
+
const MODIFIER_KEYS: readonly string[] = ['meta', 'ctrl', 'alt', 'shift'];
|
|
4
|
+
const MODIFIER_EVENT_KEYS: Record<string, 'metaKey' | 'ctrlKey' | 'altKey' | 'shiftKey'> = {
|
|
5
5
|
meta: 'metaKey',
|
|
6
6
|
ctrl: 'ctrlKey',
|
|
7
7
|
alt: 'altKey',
|
|
8
8
|
shift: 'shiftKey'
|
|
9
9
|
};
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* A command as required by the key mapper: anything with is_enabled/execute.
|
|
13
|
+
*/
|
|
14
|
+
type KeymapCommand = { is_enabled: () => boolean; execute: () => unknown };
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A keymap maps key combo strings to arrays of commands.
|
|
18
|
+
*/
|
|
19
|
+
export type Keymap = Record<string, KeymapCommand[]>;
|
|
20
|
+
|
|
11
21
|
/**
|
|
12
22
|
* Validates and defines a keymap.
|
|
13
23
|
* Throws an error if any key combo is invalid.
|
|
14
24
|
* Valid formats: 'meta+e,ctrl+e' or 'meta+shift+a'
|
|
15
25
|
* Invalid: 'meta+e+a' (only one non-modifier key allowed)
|
|
16
26
|
*/
|
|
17
|
-
export function define_keymap(keymap) {
|
|
27
|
+
export function define_keymap<K extends Keymap>(keymap: K): K {
|
|
18
28
|
for (const [key_combo] of Object.entries(keymap)) {
|
|
19
29
|
const alternatives = key_combo.split(',');
|
|
20
30
|
|
|
@@ -36,7 +46,11 @@ export function define_keymap(keymap) {
|
|
|
36
46
|
* Matches a keyboard event against a key combo string.
|
|
37
47
|
* Example: 'meta+e,ctrl+e' matches either (metaKey && key==='e') OR (ctrlKey && key==='e')
|
|
38
48
|
*/
|
|
39
|
-
function matches_key_combo(
|
|
49
|
+
function matches_key_combo(
|
|
50
|
+
key_combo: string,
|
|
51
|
+
event: KeyboardEvent,
|
|
52
|
+
virtual_keyboard_active = false
|
|
53
|
+
): boolean {
|
|
40
54
|
const alternatives = key_combo.split(',');
|
|
41
55
|
|
|
42
56
|
return alternatives.some((alternative) => {
|
|
@@ -57,9 +71,9 @@ function matches_key_combo(key_combo, event, virtual_keyboard_active = false) {
|
|
|
57
71
|
const no_extra_modifiers = virtual_keyboard_active
|
|
58
72
|
? true
|
|
59
73
|
: MODIFIER_KEYS.every((mod) => {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
74
|
+
if (modifiers.includes(mod)) return true; // This modifier is expected
|
|
75
|
+
return !event[MODIFIER_EVENT_KEYS[mod]]; // This modifier should NOT be pressed
|
|
76
|
+
});
|
|
63
77
|
|
|
64
78
|
// Check if the key matches
|
|
65
79
|
const key_matches = event.key.toLowerCase() === non_modifier;
|
|
@@ -74,7 +88,7 @@ function matches_key_combo(key_combo, event, virtual_keyboard_active = false) {
|
|
|
74
88
|
* Supports both sync and async commands. For async commands, errors are logged
|
|
75
89
|
* but don't crash the application.
|
|
76
90
|
*/
|
|
77
|
-
function handle_key_map(key_map, event) {
|
|
91
|
+
function handle_key_map(key_map: Keymap, event: KeyboardEvent): boolean {
|
|
78
92
|
const virtual_keyboard_active = is_virtual_keyboard_active();
|
|
79
93
|
|
|
80
94
|
for (const [key_combo, commands] of Object.entries(key_map)) {
|
|
@@ -114,6 +128,9 @@ function handle_key_map(key_map, event) {
|
|
|
114
128
|
* mapper.pop_scope(); // When editor loses focus
|
|
115
129
|
*/
|
|
116
130
|
export class KeyMapper {
|
|
131
|
+
scope_stack: Keymap[];
|
|
132
|
+
skip_onkeydown: boolean;
|
|
133
|
+
|
|
117
134
|
constructor() {
|
|
118
135
|
this.scope_stack = [];
|
|
119
136
|
this.skip_onkeydown = false;
|
|
@@ -122,7 +139,7 @@ export class KeyMapper {
|
|
|
122
139
|
/**
|
|
123
140
|
* Push a new scope onto the stack (becomes highest priority)
|
|
124
141
|
*/
|
|
125
|
-
push_scope(keymap) {
|
|
142
|
+
push_scope(keymap: Keymap): void {
|
|
126
143
|
// console.log('pushed keymap', keymap);
|
|
127
144
|
this.scope_stack.push(keymap);
|
|
128
145
|
}
|
|
@@ -130,7 +147,7 @@ export class KeyMapper {
|
|
|
130
147
|
/**
|
|
131
148
|
* Pop the most recent scope from the stack
|
|
132
149
|
*/
|
|
133
|
-
pop_scope() {
|
|
150
|
+
pop_scope(): Keymap | undefined {
|
|
134
151
|
const keymap = this.scope_stack.pop();
|
|
135
152
|
// console.log('popped keymap', keymap);
|
|
136
153
|
return keymap;
|
|
@@ -139,7 +156,7 @@ export class KeyMapper {
|
|
|
139
156
|
/**
|
|
140
157
|
* Handle keyboard event by trying scopes from top to bottom
|
|
141
158
|
*/
|
|
142
|
-
handle_keydown(event) {
|
|
159
|
+
handle_keydown(event: KeyboardEvent): void {
|
|
143
160
|
// Key handling temporarily disabled (e.g. while character composition takes place)
|
|
144
161
|
if (this.skip_onkeydown) return;
|
|
145
162
|
// console.log('KeyMapper.handle_keydown', event);
|
package/src/lib/Node.svelte
CHANGED
|
@@ -1,32 +1,63 @@
|
|
|
1
|
-
<script>
|
|
1
|
+
<script lang="ts">
|
|
2
2
|
import { getContext } from 'svelte';
|
|
3
3
|
import { serialize_path } from './utils.js';
|
|
4
|
+
import type {
|
|
5
|
+
NodeArrayAttachmentContext,
|
|
6
|
+
NodeArrayRenderContext,
|
|
7
|
+
NodeProps,
|
|
8
|
+
SveditRenderContext
|
|
9
|
+
} from './types.js';
|
|
4
10
|
|
|
5
|
-
|
|
11
|
+
const svedit = getContext<SveditRenderContext>('svedit');
|
|
6
12
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
/** @type {NodeProps} */
|
|
10
|
-
let { path, children, tag = 'div', class: css_class, style = '', ...rest } = $props();
|
|
13
|
+
let { path, children, tag = 'div', class: css_class, style = '', ...rest }: NodeProps = $props();
|
|
11
14
|
|
|
12
15
|
let node = $derived(svedit.session.get(path));
|
|
13
16
|
let path_str = $derived(serialize_path(path));
|
|
14
17
|
|
|
15
|
-
const node_array_meta = getContext('node_array_meta');
|
|
16
|
-
let child_index = $derived(node_array_meta ?
|
|
18
|
+
const node_array_meta = getContext<NodeArrayRenderContext | undefined>('node_array_meta');
|
|
19
|
+
let child_index = $derived(node_array_meta ? (path.at(-1) as number) : -1);
|
|
17
20
|
let is_first = $derived(node_array_meta && child_index === 0);
|
|
18
21
|
let is_last = $derived(node_array_meta && child_index === node_array_meta.length - 1);
|
|
22
|
+
|
|
23
|
+
// Marks and annotations covering this node become classes automatically:
|
|
24
|
+
// `mark-<type>`/`anno-<type>`, plus `-start`/`-end` variants on the run's
|
|
25
|
+
// first/last covered node. This allows styling ranges (including
|
|
26
|
+
// annotations, which never render wrapper components) with pure CSS. For
|
|
27
|
+
// richer rendering, node components still receive the `mark` and
|
|
28
|
+
// `annotations` props.
|
|
29
|
+
let range_classes = $derived.by(() => {
|
|
30
|
+
if (child_index < 0 || !node_array_meta?.annotations_for) return '';
|
|
31
|
+
const classes: string[] = [];
|
|
32
|
+
|
|
33
|
+
const add_classes = (range: NodeArrayAttachmentContext | null | undefined, prefix: string) => {
|
|
34
|
+
const range_type = range?.node?.type;
|
|
35
|
+
if (!range_type) return;
|
|
36
|
+
classes.push(`${prefix}-${range_type}`);
|
|
37
|
+
if (range.is_start) classes.push(`${prefix}-${range_type}-start`);
|
|
38
|
+
if (range.is_end) classes.push(`${prefix}-${range_type}-end`);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
add_classes(node_array_meta.mark_for?.(child_index), 'mark');
|
|
42
|
+
for (const annotation of node_array_meta.annotations_for(child_index)) {
|
|
43
|
+
add_classes(annotation, 'anno');
|
|
44
|
+
}
|
|
45
|
+
return classes.join(' ');
|
|
46
|
+
});
|
|
19
47
|
</script>
|
|
20
48
|
|
|
21
49
|
<svelte:element
|
|
22
50
|
this={tag}
|
|
23
51
|
id={node.id}
|
|
24
|
-
class="node-{node.type} {css_class}{
|
|
52
|
+
class="node-{node.type} {css_class}{range_classes ? ` ${range_classes}` : ''}{is_first
|
|
53
|
+
? ' first'
|
|
54
|
+
: ''}{is_last ? ' last' : ''}"
|
|
25
55
|
data-node-id={node.id}
|
|
26
56
|
data-path={path_str}
|
|
27
57
|
data-type="node"
|
|
28
58
|
style="anchor-name: --{path_str};{style}"
|
|
29
59
|
{...rest}
|
|
60
|
+
{@attach svedit.visibility_registry.track_node(path_str)}
|
|
30
61
|
>
|
|
31
62
|
{@render children()}
|
|
32
63
|
</svelte:element>
|