svedit 0.12.0 → 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 +33 -21
- package/dist/Command.svelte.d.ts +38 -30
- package/dist/Command.svelte.d.ts.map +1 -1
- package/dist/Command.svelte.js +271 -326
- 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 +16 -13
- package/dist/Node.svelte.d.ts +3 -5
- package/dist/Node.svelte.d.ts.map +1 -1
- package/dist/NodeArrayProperty.svelte +39 -22
- 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 +12 -6
- 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 +81 -129
- package/dist/Session.svelte.d.ts.map +1 -1
- package/dist/Session.svelte.js +542 -648
- package/dist/Svedit.svelte +144 -138
- package/dist/Svedit.svelte.d.ts +30 -10
- package/dist/Svedit.svelte.d.ts.map +1 -1
- package/dist/TextProperty.svelte +32 -18
- package/dist/TextProperty.svelte.d.ts +3 -5
- package/dist/TextProperty.svelte.d.ts.map +1 -1
- package/dist/Transaction.svelte.d.ts +64 -127
- package/dist/Transaction.svelte.d.ts.map +1 -1
- package/dist/Transaction.svelte.js +839 -1001
- 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 +67 -155
- package/dist/doc_utils.d.ts.map +1 -1
- package/dist/doc_utils.js +619 -863
- package/dist/index.d.ts +20 -17
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -17
- package/dist/node_visibility.svelte.d.ts +110 -9
- package/dist/node_visibility.svelte.d.ts.map +1 -1
- package/dist/node_visibility.svelte.js +473 -544
- package/dist/paste_utils.d.ts +11 -44
- package/dist/paste_utils.d.ts.map +1 -1
- package/dist/paste_utils.js +111 -152
- package/dist/transforms.svelte.d.ts +9 -7
- package/dist/transforms.svelte.d.ts.map +1 -1
- package/dist/transforms.svelte.js +125 -150
- package/dist/types.d.ts +277 -309
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/utils.d.ts +40 -119
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +346 -473
- package/package.json +69 -70
- package/src/lib/{Command.svelte.js → Command.svelte.ts} +29 -17
- package/src/lib/CustomProperty.svelte +3 -5
- package/src/lib/{KeyMapper.svelte.js → KeyMapper.svelte.ts} +28 -11
- package/src/lib/Node.svelte +16 -13
- package/src/lib/NodeArrayProperty.svelte +39 -22
- package/src/lib/NodeCaret.svelte +1 -1
- package/src/lib/NodeGap.svelte +12 -6
- package/src/lib/NodeGapMarkers.svelte +19 -10
- package/src/lib/NodeSelectionMarkers.svelte +20 -6
- package/src/lib/{Session.svelte.js → Session.svelte.ts} +130 -127
- package/src/lib/Svedit.svelte +144 -138
- package/src/lib/TextProperty.svelte +32 -18
- package/src/lib/{Transaction.svelte.js → Transaction.svelte.ts} +139 -143
- package/src/lib/UnknownNode.svelte +4 -3
- package/src/lib/{doc_utils.js → doc_utils.ts} +247 -206
- package/src/lib/{index.js → index.ts} +47 -0
- package/src/lib/{node_visibility.svelte.js → node_visibility.svelte.ts} +68 -92
- package/src/lib/{paste_utils.js → paste_utils.ts} +47 -58
- package/src/lib/{transforms.svelte.js → transforms.svelte.ts} +39 -21
- package/src/lib/{types.d.ts → types.ts} +143 -66
- package/src/lib/{utils.js → utils.ts} +110 -129
package/package.json
CHANGED
|
@@ -1,71 +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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
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
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
|
|
@@ -114,7 +122,9 @@ export class SelectParentCommand extends Command {
|
|
|
114
122
|
* touched types disable the command. Annotations never affect mark toggling.
|
|
115
123
|
*/
|
|
116
124
|
export class ToggleMarkCommand extends Command {
|
|
117
|
-
|
|
125
|
+
node_type: string;
|
|
126
|
+
|
|
127
|
+
constructor(node_type: string, context: CommandContext) {
|
|
118
128
|
super(context);
|
|
119
129
|
this.node_type = node_type;
|
|
120
130
|
}
|
|
@@ -124,8 +134,7 @@ export class ToggleMarkCommand extends Command {
|
|
|
124
134
|
is_active() {
|
|
125
135
|
const selected_marks = this.context.session.selected_marks;
|
|
126
136
|
return (
|
|
127
|
-
selected_marks.length > 0 &&
|
|
128
|
-
selected_marks.every(({ node }) => node?.type === this.node_type)
|
|
137
|
+
selected_marks.length > 0 && selected_marks.every(({ node }) => node?.type === this.node_type)
|
|
129
138
|
);
|
|
130
139
|
}
|
|
131
140
|
|
|
@@ -167,7 +176,9 @@ export class ToggleMarkCommand extends Command {
|
|
|
167
176
|
* Marks and other annotation types never block the toggle.
|
|
168
177
|
*/
|
|
169
178
|
export class ToggleAnnotationCommand extends Command {
|
|
170
|
-
|
|
179
|
+
node_type: string;
|
|
180
|
+
|
|
181
|
+
constructor(node_type: string, context: CommandContext) {
|
|
171
182
|
super(context);
|
|
172
183
|
this.node_type = node_type;
|
|
173
184
|
}
|
|
@@ -234,6 +245,7 @@ export class AddNewLineCommand extends Command {
|
|
|
234
245
|
if (selection.anchor_offset !== selection.focus_offset) {
|
|
235
246
|
tr.delete_selection();
|
|
236
247
|
}
|
|
248
|
+
if (tr.selection.type !== 'text') return;
|
|
237
249
|
|
|
238
250
|
const collapsed_offset = tr.selection.anchor_offset;
|
|
239
251
|
const content = tr.get(tr.selection.path);
|
|
@@ -280,7 +292,7 @@ export class BreakTextNodeCommand extends Command {
|
|
|
280
292
|
*/
|
|
281
293
|
export class SelectAllCommand extends Command {
|
|
282
294
|
is_enabled() {
|
|
283
|
-
return this.context.editable && this.context.session.selection;
|
|
295
|
+
return Boolean(this.context.editable && this.context.session.selection);
|
|
284
296
|
}
|
|
285
297
|
|
|
286
298
|
execute() {
|
|
@@ -318,7 +330,7 @@ export class SelectAllCommand extends Command {
|
|
|
318
330
|
session.inspect(node_path.slice(0, -1))?.type === 'node_array';
|
|
319
331
|
|
|
320
332
|
if (is_inside_node_array) {
|
|
321
|
-
const node_index =
|
|
333
|
+
const node_index = node_path.at(-1) as number;
|
|
322
334
|
session.selection = {
|
|
323
335
|
type: 'node',
|
|
324
336
|
path: node_path.slice(0, -1),
|
|
@@ -356,7 +368,7 @@ export class SelectAllCommand extends Command {
|
|
|
356
368
|
session.inspect(parent_path.slice(0, -1))?.type === 'node_array';
|
|
357
369
|
|
|
358
370
|
if (is_parent_node_array) {
|
|
359
|
-
const parent_node_index =
|
|
371
|
+
const parent_node_index = parent_path.at(-1) as number;
|
|
360
372
|
session.selection = {
|
|
361
373
|
type: 'node',
|
|
362
374
|
path: parent_path.slice(0, -1),
|
|
@@ -376,7 +388,7 @@ export class SelectAllCommand extends Command {
|
|
|
376
388
|
const is_inside_node_array = session.inspect(node_path.slice(0, -1))?.type === 'node_array';
|
|
377
389
|
|
|
378
390
|
if (is_inside_node_array) {
|
|
379
|
-
const node_index =
|
|
391
|
+
const node_index = node_path.at(-1) as number;
|
|
380
392
|
session.selection = {
|
|
381
393
|
type: 'node',
|
|
382
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,19 +1,22 @@
|
|
|
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);
|
|
19
22
|
|
|
@@ -25,9 +28,9 @@
|
|
|
25
28
|
// `annotations` props.
|
|
26
29
|
let range_classes = $derived.by(() => {
|
|
27
30
|
if (child_index < 0 || !node_array_meta?.annotations_for) return '';
|
|
28
|
-
const classes = [];
|
|
31
|
+
const classes: string[] = [];
|
|
29
32
|
|
|
30
|
-
const add_classes = (range, prefix) => {
|
|
33
|
+
const add_classes = (range: NodeArrayAttachmentContext | null | undefined, prefix: string) => {
|
|
31
34
|
const range_type = range?.node?.type;
|
|
32
35
|
if (!range_type) return;
|
|
33
36
|
classes.push(`${prefix}-${range_type}`);
|
|
@@ -46,9 +49,9 @@
|
|
|
46
49
|
<svelte:element
|
|
47
50
|
this={tag}
|
|
48
51
|
id={node.id}
|
|
49
|
-
class="node-{node.type} {css_class}{range_classes
|
|
50
|
-
?
|
|
51
|
-
: ''}{
|
|
52
|
+
class="node-{node.type} {css_class}{range_classes ? ` ${range_classes}` : ''}{is_first
|
|
53
|
+
? ' first'
|
|
54
|
+
: ''}{is_last ? ' last' : ''}"
|
|
52
55
|
data-node-id={node.id}
|
|
53
56
|
data-path={path_str}
|
|
54
57
|
data-type="node"
|
|
@@ -1,20 +1,33 @@
|
|
|
1
|
-
<script>
|
|
1
|
+
<script lang="ts">
|
|
2
2
|
import { getContext, setContext } from 'svelte';
|
|
3
3
|
import UnknownNode from './UnknownNode.svelte';
|
|
4
4
|
import { serialize_path, calculate_fragment_ranges } from './utils.js';
|
|
5
5
|
import DefaultNodeGap from './NodeGap.svelte';
|
|
6
6
|
import DefaultNodeGapMarkers from './NodeGapMarkers.svelte';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
import type {
|
|
8
|
+
NodeArrayPropertyProps,
|
|
9
|
+
NodeArrayAttachmentContext,
|
|
10
|
+
NodeArrayFragment,
|
|
11
|
+
NodeId,
|
|
12
|
+
Attachment,
|
|
13
|
+
Mark,
|
|
14
|
+
NodeArrayRenderContext,
|
|
15
|
+
SveditRenderContext
|
|
16
|
+
} from './types.js';
|
|
17
|
+
|
|
18
|
+
const svedit = getContext<SveditRenderContext>('svedit');
|
|
11
19
|
let NodeGap = $derived(svedit.session.config.system_components?.node_gap ?? DefaultNodeGap);
|
|
12
20
|
let NodeGapMarkers = $derived(
|
|
13
21
|
svedit.session.config.system_components?.node_gap_markers ?? DefaultNodeGapMarkers
|
|
14
22
|
);
|
|
15
23
|
|
|
16
|
-
|
|
17
|
-
|
|
24
|
+
let {
|
|
25
|
+
path,
|
|
26
|
+
tag = 'div',
|
|
27
|
+
class: css_class,
|
|
28
|
+
style = '',
|
|
29
|
+
...rest
|
|
30
|
+
}: NodeArrayPropertyProps = $props();
|
|
18
31
|
|
|
19
32
|
// Pre-joined once per path change; reused by data-path and by every
|
|
20
33
|
// NodeGap's should_position_gap call to avoid N+1 joins per render.
|
|
@@ -27,9 +40,9 @@
|
|
|
27
40
|
|
|
28
41
|
let fragments = $derived(get_fragments(node_ids, marks));
|
|
29
42
|
|
|
30
|
-
function get_fragments(node_ids, marks) {
|
|
43
|
+
function get_fragments(node_ids: NodeId[], marks: Mark[]): NodeArrayFragment[] {
|
|
31
44
|
const ranges = calculate_fragment_ranges(node_ids.length, marks);
|
|
32
|
-
|
|
45
|
+
const fragments: NodeArrayFragment[] = [];
|
|
33
46
|
|
|
34
47
|
for (const range of ranges) {
|
|
35
48
|
const nodes_slice = node_ids.slice(range.start_offset, range.end_offset);
|
|
@@ -41,7 +54,7 @@
|
|
|
41
54
|
node,
|
|
42
55
|
nodes: nodes_slice,
|
|
43
56
|
start_index: range.start_offset,
|
|
44
|
-
mark_index: range.mark_index
|
|
57
|
+
mark_index: range.mark_index!
|
|
45
58
|
});
|
|
46
59
|
} else if (range.type === 'content') {
|
|
47
60
|
fragments.push({
|
|
@@ -54,7 +67,7 @@
|
|
|
54
67
|
return fragments;
|
|
55
68
|
}
|
|
56
69
|
|
|
57
|
-
function get_node_key(node_id, index) {
|
|
70
|
+
function get_node_key(node_id: NodeId, index: number): string {
|
|
58
71
|
let occurrence = 0;
|
|
59
72
|
for (let current_index = 0; current_index < index; current_index++) {
|
|
60
73
|
if (node_ids[current_index] === node_id) occurrence += 1;
|
|
@@ -62,10 +75,11 @@
|
|
|
62
75
|
return `${node_id}:${occurrence}`;
|
|
63
76
|
}
|
|
64
77
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
78
|
+
function get_range_context(
|
|
79
|
+
range: Attachment,
|
|
80
|
+
index: number,
|
|
81
|
+
node_index: number
|
|
82
|
+
): NodeArrayAttachmentContext {
|
|
69
83
|
const node = svedit.session.get(range.node_id);
|
|
70
84
|
const is_start = node_index === range.start_offset;
|
|
71
85
|
const is_end = node_index === range.end_offset - 1;
|
|
@@ -80,18 +94,21 @@
|
|
|
80
94
|
};
|
|
81
95
|
}
|
|
82
96
|
|
|
83
|
-
function get_covering_ranges(
|
|
97
|
+
function get_covering_ranges(
|
|
98
|
+
ranges: Attachment[],
|
|
99
|
+
node_index: number
|
|
100
|
+
): NodeArrayAttachmentContext[] {
|
|
84
101
|
return ranges
|
|
85
102
|
.map((range, index) => ({ range, index }))
|
|
86
103
|
.filter(({ range }) => range.start_offset <= node_index && node_index < range.end_offset)
|
|
87
104
|
.map(({ range, index }) => get_range_context(range, index, node_index));
|
|
88
105
|
}
|
|
89
106
|
|
|
90
|
-
function get_annotation_contexts(node_index) {
|
|
107
|
+
function get_annotation_contexts(node_index: number): NodeArrayAttachmentContext[] {
|
|
91
108
|
return get_covering_ranges(annotations, node_index);
|
|
92
109
|
}
|
|
93
110
|
|
|
94
|
-
function get_mark(node_index) {
|
|
111
|
+
function get_mark(node_index: number): NodeArrayAttachmentContext | null {
|
|
95
112
|
// The single mark wrapping this node, or null. Mark exclusivity
|
|
96
113
|
// guarantees at most one. Annotations never wrap and are exposed via
|
|
97
114
|
// `annotations` instead.
|
|
@@ -107,17 +124,17 @@
|
|
|
107
124
|
serialize_path(svedit.session.selection.path) === path_str
|
|
108
125
|
);
|
|
109
126
|
|
|
110
|
-
setContext('node_array_meta', {
|
|
127
|
+
setContext<NodeArrayRenderContext>('node_array_meta', {
|
|
111
128
|
get length() {
|
|
112
129
|
return node_ids.length;
|
|
113
130
|
},
|
|
114
131
|
// Lets Node self-serve the marks and annotations covering a child, so
|
|
115
132
|
// node wrappers can carry range classes without every node component
|
|
116
133
|
// having to thread the `mark`/`annotations` props through.
|
|
117
|
-
mark_for(node_index) {
|
|
134
|
+
mark_for(node_index: number) {
|
|
118
135
|
return get_mark(node_index);
|
|
119
136
|
},
|
|
120
|
-
annotations_for(node_index) {
|
|
137
|
+
annotations_for(node_index: number) {
|
|
121
138
|
return get_annotation_contexts(node_index);
|
|
122
139
|
}
|
|
123
140
|
});
|
|
@@ -145,7 +162,7 @@
|
|
|
145
162
|
<NodeGap array_path={path} offset={0} count={0} empty />
|
|
146
163
|
</div>
|
|
147
164
|
{/if}
|
|
148
|
-
{#snippet render_nodes(nodes_slice, start_index)}
|
|
165
|
+
{#snippet render_nodes(nodes_slice: NodeId[], start_index: number)}
|
|
149
166
|
{#each nodes_slice as id, slice_index (get_node_key(id, start_index + slice_index))}
|
|
150
167
|
{@const index = start_index + slice_index}
|
|
151
168
|
{@const node = svedit.session.get(id)}
|
package/src/lib/NodeCaret.svelte
CHANGED
package/src/lib/NodeGap.svelte
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
<script module>
|
|
2
|
-
function cycle_z_index(
|
|
3
|
-
const el =
|
|
1
|
+
<script module lang="ts">
|
|
2
|
+
function cycle_z_index(e: MouseEvent) {
|
|
3
|
+
const el = e.currentTarget as HTMLElement;
|
|
4
4
|
if (el.hasAttribute('data-sent-to-back')) {
|
|
5
5
|
document.querySelectorAll('.svedit-selectable[data-sent-to-back]').forEach((prev) => {
|
|
6
6
|
prev.removeAttribute('data-sent-to-back');
|
|
@@ -10,10 +10,11 @@
|
|
|
10
10
|
}
|
|
11
11
|
</script>
|
|
12
12
|
|
|
13
|
-
<script>
|
|
13
|
+
<script lang="ts">
|
|
14
14
|
import { serialize_path } from './utils.js';
|
|
15
15
|
import { getContext } from 'svelte';
|
|
16
16
|
import { should_position_gap } from './node_visibility.svelte.js';
|
|
17
|
+
import type { DocumentPath, SveditRenderContext } from './types.js';
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* ┌─────────────────────────────────────────────────────────────────┐
|
|
@@ -58,8 +59,13 @@
|
|
|
58
59
|
* sure your custom component also handles read-only mode by rendering a
|
|
59
60
|
* plain `.node-gap` placeholder without editable internals.
|
|
60
61
|
*/
|
|
61
|
-
const svedit = getContext('svedit');
|
|
62
|
-
let {
|
|
62
|
+
const svedit = getContext<SveditRenderContext>('svedit');
|
|
63
|
+
let {
|
|
64
|
+
array_path,
|
|
65
|
+
offset,
|
|
66
|
+
count,
|
|
67
|
+
empty = false
|
|
68
|
+
}: { array_path: DocumentPath; offset: number; count: number; empty?: boolean } = $props();
|
|
63
69
|
let is_editable = $derived(svedit.editable);
|
|
64
70
|
|
|
65
71
|
let is_first = $derived(offset === 0);
|