svedit 0.5.0 → 0.6.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.
Files changed (62) hide show
  1. package/README.md +761 -120
  2. package/dist/AnnotatedTextProperty.svelte +122 -75
  3. package/dist/AnnotatedTextProperty.svelte.d.ts.map +1 -1
  4. package/dist/Command.svelte.d.ts +118 -0
  5. package/dist/Command.svelte.d.ts.map +1 -0
  6. package/dist/Command.svelte.js +314 -0
  7. package/dist/CustomProperty.svelte +31 -42
  8. package/dist/CustomProperty.svelte.d.ts.map +1 -1
  9. package/dist/KeyMapper.svelte.d.ts +36 -0
  10. package/dist/KeyMapper.svelte.d.ts.map +1 -0
  11. package/dist/KeyMapper.svelte.js +141 -0
  12. package/dist/Node.svelte +33 -35
  13. package/dist/Node.svelte.d.ts.map +1 -1
  14. package/dist/NodeArrayProperty.svelte +40 -37
  15. package/dist/NodeArrayProperty.svelte.d.ts.map +1 -1
  16. package/dist/{Document.svelte.d.ts → Session.svelte.d.ts} +76 -83
  17. package/dist/Session.svelte.d.ts.map +1 -0
  18. package/dist/Session.svelte.js +594 -0
  19. package/dist/Svedit.svelte +1352 -1512
  20. package/dist/Svedit.svelte.d.ts.map +1 -1
  21. package/dist/Transaction.svelte.d.ts +114 -40
  22. package/dist/Transaction.svelte.d.ts.map +1 -1
  23. package/dist/Transaction.svelte.js +845 -661
  24. package/dist/UnknownNode.svelte +6 -6
  25. package/dist/UnknownNode.svelte.d.ts.map +1 -1
  26. package/dist/doc_utils.d.ts +127 -0
  27. package/dist/doc_utils.d.ts.map +1 -0
  28. package/dist/doc_utils.js +482 -0
  29. package/dist/index.d.ts +7 -3
  30. package/dist/index.js +20 -4
  31. package/dist/{commands.svelte.d.ts → transforms.svelte.d.ts} +1 -2
  32. package/dist/transforms.svelte.d.ts.map +1 -0
  33. package/dist/transforms.svelte.js +155 -0
  34. package/dist/types.d.ts +256 -110
  35. package/dist/{util.d.ts → utils.d.ts} +22 -2
  36. package/dist/utils.d.ts.map +1 -0
  37. package/dist/utils.js +338 -0
  38. package/package.json +20 -18
  39. package/src/lib/AnnotatedTextProperty.svelte +122 -75
  40. package/src/lib/Command.svelte.js +314 -0
  41. package/src/lib/CustomProperty.svelte +31 -42
  42. package/src/lib/KeyMapper.svelte.js +141 -0
  43. package/src/lib/Node.svelte +33 -35
  44. package/src/lib/NodeArrayProperty.svelte +40 -37
  45. package/src/lib/Session.svelte.js +594 -0
  46. package/src/lib/Svedit.svelte +1352 -1512
  47. package/src/lib/Transaction.svelte.js +845 -661
  48. package/src/lib/UnknownNode.svelte +6 -6
  49. package/src/lib/doc_utils.js +482 -0
  50. package/src/lib/index.js +20 -4
  51. package/src/lib/transforms.svelte.js +155 -0
  52. package/src/lib/types.d.ts +256 -110
  53. package/src/lib/utils.js +338 -0
  54. package/dist/Document.svelte.d.ts.map +0 -1
  55. package/dist/Document.svelte.js +0 -833
  56. package/dist/commands.svelte.d.ts.map +0 -1
  57. package/dist/commands.svelte.js +0 -266
  58. package/dist/util.d.ts.map +0 -1
  59. package/dist/util.js +0 -269
  60. package/src/lib/Document.svelte.js +0 -833
  61. package/src/lib/commands.svelte.js +0 -266
  62. package/src/lib/util.js +0 -269
@@ -0,0 +1,314 @@
1
+ import { insert_default_node, break_text_node } from './transforms.svelte.js';
2
+ import { is_selection_collapsed, is_mobile_browser, get_char_length } from './utils.js';
3
+
4
+ /**
5
+ * Base class for commands that can be executed in response to user actions
6
+ * like keyboard shortcuts, menu items, or toolbar buttons.
7
+ *
8
+ * Commands are stateful and UI-aware, unlike transforms which are pure functions.
9
+ * They can have derived state (like is_active for toggle commands) or their own
10
+ * state (like form inputs for prompt-based commands).
11
+ *
12
+ * @example
13
+ * ```js
14
+ * class SaveCommand extends Command {
15
+ * is_enabled() {
16
+ * return this.context.editable;
17
+ * }
18
+ *
19
+ * async execute() {
20
+ * await update_document(this.context.session);
21
+ * this.context.editable = false;
22
+ * }
23
+ * }
24
+ * ```
25
+ */
26
+ export default class Command {
27
+ /**
28
+ * Derived state that indicates if the command is disabled.
29
+ * Automatically computed from is_enabled().
30
+ */
31
+ disabled = $derived(!this.is_enabled());
32
+
33
+ /**
34
+ * Creates a new Command instance.
35
+ *
36
+ * @param {any} context - The context object providing access to application state
37
+ */
38
+ constructor(context) {
39
+ this.context = context;
40
+ }
41
+
42
+ /**
43
+ * Determines if the command can currently be executed.
44
+ * Override this method to implement command-specific logic.
45
+ *
46
+ * @returns {boolean} true if the command can be executed, false otherwise
47
+ */
48
+ is_enabled() {
49
+ return true;
50
+ }
51
+
52
+ /**
53
+ * Executes the command.
54
+ * Override this method to implement the command's behavior.
55
+ * Can be async for commands that need to perform asynchronous operations.
56
+ *
57
+ * @returns {void | Promise<void>}
58
+ */
59
+ execute() {
60
+ throw new Error('Not implemented');
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Command that undoes the last change to the document.
66
+ */
67
+ export class UndoCommand extends Command {
68
+ is_enabled() {
69
+ return this.context.editable && this.context.session.can_undo;
70
+ }
71
+
72
+ execute() {
73
+ this.context.session.undo();
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Command that redoes the last undone change to the document.
79
+ */
80
+ export class RedoCommand extends Command {
81
+ is_enabled() {
82
+ return this.context.editable && this.context.session.can_redo;
83
+ }
84
+
85
+ execute() {
86
+ this.context.session.redo();
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Command that selects the parent of the current selection.
92
+ * Useful for navigating up the document hierarchy.
93
+ */
94
+ export class SelectParentCommand extends Command {
95
+ is_enabled() {
96
+ return this.context.editable && this.context.session.selection;
97
+ }
98
+
99
+ execute() {
100
+ this.context.session.select_parent();
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Generic command that toggles an annotation on the current text selection.
106
+ * Used for simple annotations like bold, italic, highlight, etc.
107
+ */
108
+ export class ToggleAnnotationCommand extends Command {
109
+ constructor(node_type, context) {
110
+ super(context);
111
+ this.node_type = node_type;
112
+ }
113
+
114
+ active = $derived(this.is_active());
115
+
116
+ is_active() {
117
+ return this.context.session.active_annotation(this.node_type);
118
+ }
119
+
120
+ is_enabled() {
121
+ const { session, editable } = this.context;
122
+ const has_annotation = session.active_annotation(this.node_type);
123
+ const no_annotation_and_cursor_not_collapsed =
124
+ !session.active_annotation() && !is_selection_collapsed(session.selection);
125
+
126
+ return (
127
+ editable &&
128
+ session.selection?.type === 'text' &&
129
+ (has_annotation || no_annotation_and_cursor_not_collapsed)
130
+ );
131
+ }
132
+
133
+ execute() {
134
+ this.context.session.apply(this.context.session.tr.annotate_text(this.node_type));
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Command that adds a new line character at the current cursor position.
140
+ * Only works in text selections where newlines are allowed.
141
+ * Disabled on mobile browsers where Shift+Enter has different behavior.
142
+ */
143
+ export class AddNewLineCommand extends Command {
144
+ is_enabled() {
145
+ const session = this.context.session;
146
+ const selection = session.selection;
147
+
148
+ return (
149
+ this.context.editable &&
150
+ !is_mobile_browser() &&
151
+ selection?.type === 'text' &&
152
+ session.inspect(selection.path).allow_newlines
153
+ );
154
+ }
155
+
156
+ execute() {
157
+ this.context.session.apply(this.context.session.tr.insert_text('\n'));
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Command that breaks a text node at the cursor position.
163
+ * Creates a new node and splits the content between the current and new node.
164
+ * Only works in text selections.
165
+ */
166
+ export class BreakTextNodeCommand extends Command {
167
+ is_enabled() {
168
+ return this.context.editable && this.context.session.selection?.type === 'text';
169
+ }
170
+
171
+ execute() {
172
+ const tr = this.context.session.tr;
173
+ if (break_text_node(tr)) {
174
+ this.context.session.apply(tr);
175
+ }
176
+ }
177
+ }
178
+
179
+ /**
180
+ * Command that selects all content in the current context.
181
+ * Progressively expands selection from text → node → parent node array.
182
+ */
183
+ export class SelectAllCommand extends Command {
184
+ is_enabled() {
185
+ return this.context.editable && this.context.session.selection;
186
+ }
187
+
188
+ execute() {
189
+ const session = this.context.session;
190
+ const selection = session.selection;
191
+
192
+ if (!selection) {
193
+ return;
194
+ }
195
+
196
+ if (selection.type === 'text') {
197
+ const text_content = session.get(selection.path);
198
+ const text_length = get_char_length(text_content.text);
199
+
200
+ // Check if all text is already selected
201
+ const is_all_text_selected =
202
+ Math.min(selection.anchor_offset, selection.focus_offset) === 0 &&
203
+ Math.max(selection.anchor_offset, selection.focus_offset) === text_length;
204
+
205
+ if (!is_all_text_selected) {
206
+ // Select all text in the current text node
207
+ session.selection = {
208
+ type: 'text',
209
+ path: selection.path,
210
+ anchor_offset: 0,
211
+ focus_offset: text_length
212
+ };
213
+ } else {
214
+ // All text is selected, move up to select the containing node
215
+ const node_path = selection.path.slice(0, -1); // Remove the property name (e.g., 'content')
216
+
217
+ // Check if we have enough path segments and if we're inside a node_array
218
+ if (node_path.length >= 2) {
219
+ const is_inside_node_array =
220
+ session.inspect(node_path.slice(0, -1))?.type === 'node_array';
221
+
222
+ if (is_inside_node_array) {
223
+ const node_index = parseInt(node_path.at(-1));
224
+ session.selection = {
225
+ type: 'node',
226
+ path: node_path.slice(0, -1),
227
+ anchor_offset: node_index,
228
+ focus_offset: node_index + 1
229
+ };
230
+ }
231
+ }
232
+ // Stop expanding - text is not in a selectable node_array
233
+ }
234
+ } else if (selection.type === 'node') {
235
+ const node_array_path = selection.path;
236
+ const node_array = session.get(node_array_path);
237
+
238
+ // Check if the entire node_array is already selected
239
+ const is_entire_node_array_selected =
240
+ Math.min(selection.anchor_offset, selection.focus_offset) === 0 &&
241
+ Math.max(selection.anchor_offset, selection.focus_offset) === node_array.length;
242
+
243
+ if (!is_entire_node_array_selected) {
244
+ // Select the entire node_array
245
+ session.selection = {
246
+ type: 'node',
247
+ path: node_array_path,
248
+ anchor_offset: 0,
249
+ focus_offset: node_array.length
250
+ };
251
+ } else {
252
+ // Entire node_array is selected, try to move up to parent node_array
253
+ const parent_path = node_array_path.slice(0, -1);
254
+
255
+ // Check if we have enough path segments and if parent is a valid node_array
256
+ if (parent_path.length >= 2) {
257
+ const is_parent_node_array =
258
+ session.inspect(parent_path.slice(0, -1))?.type === 'node_array';
259
+
260
+ if (is_parent_node_array) {
261
+ const parent_node_index = parseInt(parent_path.at(-1));
262
+ session.selection = {
263
+ type: 'node',
264
+ path: parent_path.slice(0, -1),
265
+ anchor_offset: parent_node_index,
266
+ focus_offset: parent_node_index + 1
267
+ };
268
+ }
269
+ }
270
+ // Stop expanding - we've reached the top level
271
+ }
272
+ } else if (selection.type === 'property') {
273
+ // For property selections, select the containing node
274
+ const node_path = selection.path.slice(0, -1);
275
+
276
+ // Check if we have enough path segments and if we're inside a node_array
277
+ if (node_path.length >= 2) {
278
+ const is_inside_node_array = session.inspect(node_path.slice(0, -1))?.type === 'node_array';
279
+
280
+ if (is_inside_node_array) {
281
+ const node_index = parseInt(node_path.at(-1));
282
+ session.selection = {
283
+ type: 'node',
284
+ path: node_path.slice(0, -1),
285
+ anchor_offset: node_index,
286
+ focus_offset: node_index + 1
287
+ };
288
+ }
289
+ }
290
+ // Stop expanding - property is not in a selectable node_array
291
+ }
292
+ }
293
+ }
294
+
295
+ /**
296
+ * Command that inserts a default node at the current cursor position.
297
+ * Only works when a collapsed node selection is active.
298
+ */
299
+ export class InsertDefaultNodeCommand extends Command {
300
+ is_enabled() {
301
+ const selection = this.context.session.selection;
302
+ return (
303
+ this.context.editable &&
304
+ selection?.type === 'node' &&
305
+ selection.anchor_offset === selection.focus_offset
306
+ );
307
+ }
308
+
309
+ execute() {
310
+ const tr = this.context.session.tr;
311
+ insert_default_node(tr);
312
+ this.context.session.apply(tr);
313
+ }
314
+ }
@@ -1,51 +1,40 @@
1
1
  <script>
2
- import { getContext } from 'svelte';
2
+ import { getContext } from 'svelte';
3
3
 
4
- /** @import { CustomPropertyProps } from './types.d.ts'; */
4
+ /** @import { CustomPropertyProps } from './types.d.ts'; */
5
+ const svedit = getContext('svedit');
5
6
 
6
- const svedit = getContext('svedit');
7
-
8
- /** @type {CustomPropertyProps} */
9
- let {
10
- path,
11
- class: css_class,
12
- children,
13
- } = $props();
7
+ /** @type {CustomPropertyProps} */
8
+ let { path, tag = 'div', class: css_class, children } = $props();
14
9
  </script>
15
10
 
16
- {#if svedit.editable}
17
- <!-- NOTE: It's super crucial that there is no extra whitespace, to not introduce additional cursor positions in contenteditable -->
18
- <div
19
- class="property {css_class}"
20
- data-type="property"
21
- data-path={path.join('.')}
22
- style="anchor-name: --{path.join('-')};"
23
- >
24
- <div class="cursor-trap">
25
- <div class="svedit-selectable"><br/></div>
26
- </div>
11
+ <svelte:element
12
+ this={tag}
13
+ class={css_class}
14
+ data-type="property"
15
+ data-path={path.join('.')}
16
+ style="anchor-name: --{path.join('-')};"
17
+ >
18
+ {#if svedit.editable}
19
+ <div class="cursor-trap">
20
+ <div class="svedit-selectable"><br /></div>
21
+ </div>
22
+ {/if}
27
23
 
28
- <div class="property-content" contenteditable="false">{@render children()}</div>
29
- </div>
30
- {:else}
31
- {@render children()}
32
- {/if}
24
+ {@render children()}
25
+ </svelte:element>
33
26
 
34
27
  <style>
35
- .property {
36
- position: relative;
37
- }
38
- .cursor-trap {
39
- position: absolute;
40
- top: 0;
41
- left: 0;
42
- right: 0;
43
- bottom: 0;
44
- z-index: 1;
45
- outline: none;
46
- }
47
- .property-content {
48
- width: 100%;
49
- height: 100%;
50
- }
28
+ [data-type='property'] {
29
+ position: relative;
30
+ }
31
+ .cursor-trap {
32
+ position: absolute;
33
+ top: 0;
34
+ left: 0;
35
+ right: 0;
36
+ bottom: 0;
37
+ z-index: 1;
38
+ outline: none;
39
+ }
51
40
  </style>
@@ -0,0 +1,141 @@
1
+ const MODIFIER_KEYS = ['meta', 'ctrl', 'alt', 'shift'];
2
+ const MODIFIER_EVENT_KEYS = {
3
+ meta: 'metaKey',
4
+ ctrl: 'ctrlKey',
5
+ alt: 'altKey',
6
+ shift: 'shiftKey'
7
+ };
8
+
9
+ /**
10
+ * Validates and defines a keymap.
11
+ * Throws an error if any key combo is invalid.
12
+ * Valid formats: 'meta+e,ctrl+e' or 'meta+shift+a'
13
+ * Invalid: 'meta+e+a' (only one non-modifier key allowed)
14
+ */
15
+ export function define_keymap(keymap) {
16
+ for (const [key_combo] of Object.entries(keymap)) {
17
+ const alternatives = key_combo.split(',');
18
+
19
+ for (const alternative of alternatives) {
20
+ const parts = alternative.trim().toLowerCase().split('+');
21
+ const non_modifiers = parts.filter((part) => !MODIFIER_KEYS.includes(part));
22
+
23
+ if (non_modifiers.length !== 1) {
24
+ throw new Error(
25
+ `Invalid key combo: "${alternative}". Must have exactly one non-modifier key. Found: ${non_modifiers.length}`
26
+ );
27
+ }
28
+ }
29
+ }
30
+ return keymap;
31
+ }
32
+
33
+ /**
34
+ * Matches a keyboard event against a key combo string.
35
+ * Example: 'meta+e,ctrl+e' matches either (metaKey && key==='e') OR (ctrlKey && key==='e')
36
+ */
37
+ function matches_key_combo(key_combo, event) {
38
+ const alternatives = key_combo.split(',');
39
+
40
+ return alternatives.some((alternative) => {
41
+ const parts = alternative.trim().toLowerCase().split('+');
42
+ const modifiers = parts.filter((part) => MODIFIER_KEYS.includes(part));
43
+ const non_modifier = parts.find((part) => !MODIFIER_KEYS.includes(part));
44
+
45
+ // Check if all specified modifiers are pressed
46
+ const modifiers_match = modifiers.every((mod) => event[MODIFIER_EVENT_KEYS[mod]]);
47
+
48
+ // Check if no unspecified modifiers are pressed
49
+ const no_extra_modifiers = MODIFIER_KEYS.every((mod) => {
50
+ if (modifiers.includes(mod)) return true; // This modifier is expected
51
+ return !event[MODIFIER_EVENT_KEYS[mod]]; // This modifier should NOT be pressed
52
+ });
53
+
54
+ // Check if the key matches
55
+ const key_matches = event.key.toLowerCase() === non_modifier;
56
+ return modifiers_match && no_extra_modifiers && key_matches;
57
+ });
58
+ }
59
+
60
+ /**
61
+ * Handles a key map by matching keyboard event against registered key combos
62
+ * and executing the first enabled command.
63
+ *
64
+ * Supports both sync and async commands. For async commands, errors are logged
65
+ * but don't crash the application.
66
+ */
67
+ function handle_key_map(key_map, event) {
68
+ for (const [key_combo, commands] of Object.entries(key_map)) {
69
+ if (matches_key_combo(key_combo, event)) {
70
+ // Find the first enabled command and execute it
71
+ const enabled_command = commands.find((cmd) => cmd.is_enabled());
72
+ if (enabled_command) {
73
+ event.preventDefault();
74
+
75
+ // Execute command (may be sync or async)
76
+ const result = enabled_command.execute();
77
+
78
+ // If it's a promise, handle errors (fire-and-forget)
79
+ if (result instanceof Promise) {
80
+ result.catch((err) => {
81
+ console.error('Command execution failed:', err);
82
+ });
83
+ }
84
+
85
+ return true;
86
+ }
87
+ }
88
+ }
89
+ return false;
90
+ }
91
+
92
+ /**
93
+ * KeyMapper manages keyboard shortcuts using a stack-based scope system.
94
+ *
95
+ * Scopes are tried from top to bottom (most specific to most general).
96
+ * This makes it completely general-purpose - no knowledge of specific contexts.
97
+ *
98
+ * Usage:
99
+ * const mapper = new KeyMapper();
100
+ * mapper.push_scope(app_keymap); // Base layer
101
+ * mapper.push_scope(editor_keymap); // When editor gains focus
102
+ * mapper.pop_scope(); // When editor loses focus
103
+ */
104
+ export class KeyMapper {
105
+ constructor() {
106
+ this.scope_stack = [];
107
+ this.skip_onkeydown = false;
108
+ }
109
+
110
+ /**
111
+ * Push a new scope onto the stack (becomes highest priority)
112
+ */
113
+ push_scope(keymap) {
114
+ // console.log('pushed keymap', keymap);
115
+ this.scope_stack.push(keymap);
116
+ }
117
+
118
+ /**
119
+ * Pop the most recent scope from the stack
120
+ */
121
+ pop_scope() {
122
+ const keymap = this.scope_stack.pop();
123
+ // console.log('popped keymap', keymap);
124
+ return keymap;
125
+ }
126
+
127
+ /**
128
+ * Handle keyboard event by trying scopes from top to bottom
129
+ */
130
+ handle_keydown(event) {
131
+ // Key handling temporarily disabled (e.g. while character composition takes place)
132
+ if (this.skip_onkeydown) return;
133
+ // console.log('KeyMapper.handle_keydown', event);
134
+ // Try from most specific (top of stack) to most general (bottom)
135
+ for (let i = this.scope_stack.length - 1; i >= 0; i--) {
136
+ if (handle_key_map(this.scope_stack[i], event)) {
137
+ return;
138
+ }
139
+ }
140
+ }
141
+ }
@@ -1,45 +1,43 @@
1
1
  <script>
2
- import { getContext } from 'svelte';
2
+ import { getContext } from 'svelte';
3
3
 
4
- /** @import { NodeProps } from './types.d.ts'; */
4
+ /** @import { NodeProps } from './types.d.ts'; */
5
5
 
6
- const svedit = getContext('svedit');
7
- let NodeCursorTrap = $derived(svedit.doc.config.system_components.NodeCursorTrap);
6
+ const svedit = getContext('svedit');
7
+ let NodeCursorTrap = $derived(svedit.session.config.system_components.NodeCursorTrap);
8
8
 
9
- /** @type {NodeProps} */
10
- let {
11
- path,
12
- children
13
- } = $props();
9
+ /** @type {NodeProps} */
10
+ let { path, children, tag = 'div', class: css_class } = $props();
14
11
 
15
- // NOTE: When the next to last path segment is a node_array property, the node is wrapped in a node_array
16
- let is_inside_node_array = $derived(path.length > 1 && svedit.doc.inspect(path.slice(0, -1))?.type === 'node_array');
17
- let node = $derived(svedit.doc.get(path));
18
- let is_first_node_array_child = $derived(is_inside_node_array && parseInt(String(path.at(-1)), 10) === 0);
12
+ // NOTE: When the next to last path segment is a node_array property, the node is wrapped in a node_array
13
+ let is_inside_node_array = $derived(
14
+ path.length > 1 && svedit.session.inspect(path.slice(0, -1))?.type === 'node_array'
15
+ );
16
+ let node = $derived(svedit.session.get(path));
17
+ let is_first_node_array_child = $derived(
18
+ is_inside_node_array && parseInt(String(path.at(-1)), 10) === 0
19
+ );
19
20
  </script>
20
21
 
21
- {#if svedit.editable}
22
- <div
23
- class="node"
24
- data-node-id={node.id}
25
- data-path={path.join('.')}
26
- data-type="node"
27
- style="anchor-name: --{path.join('-')};"
28
- >
29
- {#if is_first_node_array_child}
30
- <NodeCursorTrap {path} type="position-zero-cursor-trap" />
31
- {/if}
32
- {@render children()}
33
- {#if is_inside_node_array}
34
- <NodeCursorTrap {path} type="after-node-cursor-trap" />
35
- {/if}
36
- </div>
37
- {:else}
38
- {@render children()}
39
- {/if}
22
+ <svelte:element
23
+ this={tag}
24
+ class={css_class}
25
+ data-node-id={node.id}
26
+ data-path={path.join('.')}
27
+ data-type="node"
28
+ style="anchor-name: --{path.join('-')};"
29
+ >
30
+ {#if svedit.editable && is_first_node_array_child}
31
+ <NodeCursorTrap {path} type="position-zero-cursor-trap" />
32
+ {/if}
33
+ {@render children()}
34
+ {#if svedit.editable && is_inside_node_array}
35
+ <NodeCursorTrap {path} type="after-node-cursor-trap" />
36
+ {/if}
37
+ </svelte:element>
40
38
 
41
39
  <style>
42
- div {
43
- position: relative;
44
- }
40
+ div {
41
+ position: relative;
42
+ }
45
43
  </style>