svedit 0.11.0 → 0.12.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 +230 -64
- package/dist/Command.svelte.d.ts +21 -5
- package/dist/Command.svelte.d.ts.map +1 -1
- package/dist/Command.svelte.js +86 -30
- package/dist/KeyMapper.svelte.d.ts.map +1 -1
- package/dist/KeyMapper.svelte.js +18 -6
- package/dist/Node.svelte +29 -1
- package/dist/Node.svelte.d.ts.map +1 -1
- package/dist/NodeArrayProperty.svelte +119 -12
- package/dist/NodeArrayProperty.svelte.d.ts.map +1 -1
- package/dist/NodeGap.svelte +48 -6
- package/dist/NodeGap.svelte.d.ts.map +1 -1
- package/dist/Session.svelte.d.ts +31 -14
- package/dist/Session.svelte.d.ts.map +1 -1
- package/dist/Session.svelte.js +155 -77
- package/dist/Svedit.svelte +110 -89
- package/dist/Svedit.svelte.d.ts.map +1 -1
- package/dist/{AnnotatedTextProperty.svelte → TextProperty.svelte} +45 -44
- package/dist/TextProperty.svelte.d.ts +7 -0
- package/dist/TextProperty.svelte.d.ts.map +1 -0
- package/dist/Transaction.svelte.d.ts +78 -40
- package/dist/Transaction.svelte.d.ts.map +1 -1
- package/dist/Transaction.svelte.js +412 -234
- package/dist/doc_utils.d.ts +96 -21
- package/dist/doc_utils.d.ts.map +1 -1
- package/dist/doc_utils.js +440 -168
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/node_visibility.svelte.d.ts +30 -4
- package/dist/node_visibility.svelte.d.ts.map +1 -1
- package/dist/node_visibility.svelte.js +327 -363
- package/dist/paste_utils.d.ts +3 -2
- package/dist/paste_utils.d.ts.map +1 -1
- package/dist/paste_utils.js +16 -13
- package/dist/transforms.svelte.d.ts.map +1 -1
- package/dist/transforms.svelte.js +5 -6
- package/dist/types.d.ts +112 -42
- package/dist/utils.d.ts +89 -13
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +320 -85
- package/package.json +4 -1
- package/src/lib/Command.svelte.js +86 -30
- package/src/lib/KeyMapper.svelte.js +18 -6
- package/src/lib/Node.svelte +29 -1
- package/src/lib/NodeArrayProperty.svelte +119 -12
- package/src/lib/NodeGap.svelte +48 -6
- package/src/lib/Session.svelte.js +155 -77
- package/src/lib/Svedit.svelte +110 -89
- package/src/lib/{AnnotatedTextProperty.svelte → TextProperty.svelte} +45 -44
- package/src/lib/Transaction.svelte.js +412 -234
- package/src/lib/doc_utils.js +440 -168
- package/src/lib/index.js +1 -1
- package/src/lib/node_visibility.svelte.js +327 -363
- package/src/lib/paste_utils.js +16 -13
- package/src/lib/transforms.svelte.js +5 -6
- package/src/lib/types.d.ts +112 -42
- package/src/lib/utils.js +320 -85
- package/dist/AnnotatedTextProperty.svelte.d.ts +0 -7
- package/dist/AnnotatedTextProperty.svelte.d.ts.map +0 -1
package/dist/Command.svelte.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { insert_default_node, break_text_node } from './transforms.svelte.js';
|
|
2
|
-
import {
|
|
3
|
-
import { is_selection_collapsed,
|
|
2
|
+
import { can_switch_mark_type, get_selected_range_types } from './doc_utils.js';
|
|
3
|
+
import { is_selection_collapsed, get_char_length, char_slice } from './utils.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Base class for commands that can be executed in response to user actions
|
|
@@ -107,10 +107,13 @@ export class SelectParentCommand extends Command {
|
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
/**
|
|
110
|
-
* Generic command that toggles
|
|
111
|
-
*
|
|
110
|
+
* Generic command that toggles a mark on the current text or node selection.
|
|
111
|
+
*
|
|
112
|
+
* Marks are mutually exclusive, so all touched marks compete: same-type marks
|
|
113
|
+
* are removed, a single touched property-less mark may switch type, and mixed
|
|
114
|
+
* touched types disable the command. Annotations never affect mark toggling.
|
|
112
115
|
*/
|
|
113
|
-
export class
|
|
116
|
+
export class ToggleMarkCommand extends Command {
|
|
114
117
|
constructor(node_type, context) {
|
|
115
118
|
super(context);
|
|
116
119
|
this.node_type = node_type;
|
|
@@ -119,41 +122,95 @@ export class ToggleAnnotationCommand extends Command {
|
|
|
119
122
|
active = $derived(this.is_active());
|
|
120
123
|
|
|
121
124
|
is_active() {
|
|
122
|
-
|
|
125
|
+
const selected_marks = this.context.session.selected_marks;
|
|
126
|
+
return (
|
|
127
|
+
selected_marks.length > 0 &&
|
|
128
|
+
selected_marks.every(({ node }) => node?.type === this.node_type)
|
|
129
|
+
);
|
|
123
130
|
}
|
|
124
131
|
|
|
125
132
|
is_enabled() {
|
|
126
133
|
const { session, editable } = this.context;
|
|
127
|
-
const
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
134
|
+
const selection = session.selection;
|
|
135
|
+
const is_valid_selection =
|
|
136
|
+
selection?.type === 'text' ||
|
|
137
|
+
(selection?.type === 'node' && !is_selection_collapsed(selection));
|
|
138
|
+
const mark_type_is_allowed = session.available_mark_types.includes(this.node_type);
|
|
139
|
+
const selected_marks = session.selected_marks;
|
|
140
|
+
const selected_mark_types = get_selected_range_types(selected_marks);
|
|
141
|
+
|
|
142
|
+
if (!editable || !is_valid_selection || !mark_type_is_allowed) return false;
|
|
143
|
+
if (selected_mark_types.size > 1) return false;
|
|
144
|
+
|
|
145
|
+
if (selected_marks.length === 0) {
|
|
146
|
+
return Boolean(selection && !is_selection_collapsed(selection));
|
|
147
|
+
}
|
|
140
148
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
149
|
+
const first_selected_mark = selected_marks[0];
|
|
150
|
+
const selected_mark_type = first_selected_mark.node.type;
|
|
151
|
+
if (selected_mark_type === this.node_type) return true;
|
|
152
|
+
if (selected_marks.length !== 1) return false;
|
|
153
|
+
|
|
154
|
+
return can_switch_mark_type(session.schema, selected_mark_type, this.node_type);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
execute() {
|
|
158
|
+
this.context.session.apply(this.context.session.tr.toggle_mark(this.node_type));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Generic command that toggles an annotation on the current text or node selection.
|
|
164
|
+
*
|
|
165
|
+
* Annotations only compete with touched annotations of the same type: touched
|
|
166
|
+
* same-type annotations are removed, otherwise a new annotation is created.
|
|
167
|
+
* Marks and other annotation types never block the toggle.
|
|
168
|
+
*/
|
|
169
|
+
export class ToggleAnnotationCommand extends Command {
|
|
170
|
+
constructor(node_type, context) {
|
|
171
|
+
super(context);
|
|
172
|
+
this.node_type = node_type;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
active = $derived(this.is_active());
|
|
176
|
+
|
|
177
|
+
// Mirrors the filter in tr.toggle_annotation, so active/enabled match what
|
|
178
|
+
// execute() does.
|
|
179
|
+
relevant_annotations() {
|
|
180
|
+
return this.context.session.selected_annotations.filter(
|
|
181
|
+
({ node }) => node?.type === this.node_type
|
|
145
182
|
);
|
|
146
183
|
}
|
|
147
184
|
|
|
185
|
+
is_active() {
|
|
186
|
+
return this.relevant_annotations().length > 0;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
is_enabled() {
|
|
190
|
+
const { session, editable } = this.context;
|
|
191
|
+
const selection = session.selection;
|
|
192
|
+
const is_valid_selection =
|
|
193
|
+
selection?.type === 'text' ||
|
|
194
|
+
(selection?.type === 'node' && !is_selection_collapsed(selection));
|
|
195
|
+
const annotation_type_is_allowed = session.available_annotation_types.includes(this.node_type);
|
|
196
|
+
|
|
197
|
+
if (!editable || !is_valid_selection || !annotation_type_is_allowed) return false;
|
|
198
|
+
|
|
199
|
+
// Removing touched same-type annotations also works from a collapsed
|
|
200
|
+
// caret inside the annotation; creating a new one requires an expanded
|
|
201
|
+
// selection.
|
|
202
|
+
if (this.relevant_annotations().length > 0) return true;
|
|
203
|
+
return Boolean(selection && !is_selection_collapsed(selection));
|
|
204
|
+
}
|
|
205
|
+
|
|
148
206
|
execute() {
|
|
149
|
-
this.context.session.apply(this.context.session.tr.
|
|
207
|
+
this.context.session.apply(this.context.session.tr.toggle_annotation(this.node_type));
|
|
150
208
|
}
|
|
151
209
|
}
|
|
152
210
|
|
|
153
211
|
/**
|
|
154
212
|
* Command that adds a new line character at the current caret position.
|
|
155
213
|
* Only works in text selections where newlines are allowed.
|
|
156
|
-
* Disabled on mobile browsers where Shift+Enter has different behavior.
|
|
157
214
|
*/
|
|
158
215
|
export class AddNewLineCommand extends Command {
|
|
159
216
|
is_enabled() {
|
|
@@ -162,7 +219,6 @@ export class AddNewLineCommand extends Command {
|
|
|
162
219
|
|
|
163
220
|
return (
|
|
164
221
|
this.context.editable &&
|
|
165
|
-
!is_mobile_browser() &&
|
|
166
222
|
selection?.type === 'text' &&
|
|
167
223
|
session.inspect(selection.path).allow_newlines
|
|
168
224
|
);
|
|
@@ -181,7 +237,7 @@ export class AddNewLineCommand extends Command {
|
|
|
181
237
|
|
|
182
238
|
const collapsed_offset = tr.selection.anchor_offset;
|
|
183
239
|
const content = tr.get(tr.selection.path);
|
|
184
|
-
const text_before_caret = char_slice(content.
|
|
240
|
+
const text_before_caret = char_slice(content.content, 0, collapsed_offset);
|
|
185
241
|
const line_start_index = text_before_caret.lastIndexOf('\n') + 1;
|
|
186
242
|
const current_line_prefix = text_before_caret.slice(line_start_index);
|
|
187
243
|
const indentation_match = current_line_prefix.match(/^[\t ]*/);
|
|
@@ -237,7 +293,7 @@ export class SelectAllCommand extends Command {
|
|
|
237
293
|
|
|
238
294
|
if (selection.type === 'text') {
|
|
239
295
|
const text_content = session.get(selection.path);
|
|
240
|
-
const text_length = get_char_length(text_content.
|
|
296
|
+
const text_length = get_char_length(text_content.content);
|
|
241
297
|
|
|
242
298
|
// Check if all text is already selected
|
|
243
299
|
const is_all_text_selected =
|
|
@@ -280,7 +336,7 @@ export class SelectAllCommand extends Command {
|
|
|
280
336
|
// Check if the entire node_array is already selected
|
|
281
337
|
const is_entire_node_array_selected =
|
|
282
338
|
Math.min(selection.anchor_offset, selection.focus_offset) === 0 &&
|
|
283
|
-
Math.max(selection.anchor_offset, selection.focus_offset) === node_array.length;
|
|
339
|
+
Math.max(selection.anchor_offset, selection.focus_offset) === node_array.nodes.length;
|
|
284
340
|
|
|
285
341
|
if (!is_entire_node_array_selected) {
|
|
286
342
|
// Select the entire node_array
|
|
@@ -288,7 +344,7 @@ export class SelectAllCommand extends Command {
|
|
|
288
344
|
type: 'node',
|
|
289
345
|
path: node_array_path,
|
|
290
346
|
anchor_offset: 0,
|
|
291
|
-
focus_offset: node_array.length
|
|
347
|
+
focus_offset: node_array.nodes.length
|
|
292
348
|
};
|
|
293
349
|
} else {
|
|
294
350
|
// Entire node_array is selected, try to move up to parent node_array
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"KeyMapper.svelte.d.ts","sourceRoot":"","sources":["../src/lib/KeyMapper.svelte.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"KeyMapper.svelte.d.ts","sourceRoot":"","sources":["../src/lib/KeyMapper.svelte.js"],"names":[],"mappings":"AAUA;;;;;GAKG;AACH,gDAgBC;AAuED;;;;;;;;;;;GAWG;AACH;IAEE,mBAAqB;IACrB,wBAA2B;IAG5B;;OAEG;IACH,8BAGC;IAED;;OAEG;IACH,iBAIC;IAED;;OAEG;IACH,iCAUC;CACD"}
|
package/dist/KeyMapper.svelte.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { is_virtual_keyboard_active } from './utils.js';
|
|
2
|
+
|
|
1
3
|
const MODIFIER_KEYS = ['meta', 'ctrl', 'alt', 'shift'];
|
|
2
4
|
const MODIFIER_EVENT_KEYS = {
|
|
3
5
|
meta: 'metaKey',
|
|
@@ -34,7 +36,7 @@ export function define_keymap(keymap) {
|
|
|
34
36
|
* Matches a keyboard event against a key combo string.
|
|
35
37
|
* Example: 'meta+e,ctrl+e' matches either (metaKey && key==='e') OR (ctrlKey && key==='e')
|
|
36
38
|
*/
|
|
37
|
-
function matches_key_combo(key_combo, event) {
|
|
39
|
+
function matches_key_combo(key_combo, event, virtual_keyboard_active = false) {
|
|
38
40
|
const alternatives = key_combo.split(',');
|
|
39
41
|
|
|
40
42
|
return alternatives.some((alternative) => {
|
|
@@ -42,14 +44,22 @@ function matches_key_combo(key_combo, event) {
|
|
|
42
44
|
const modifiers = parts.filter((part) => MODIFIER_KEYS.includes(part));
|
|
43
45
|
const non_modifier = parts.find((part) => !MODIFIER_KEYS.includes(part));
|
|
44
46
|
|
|
47
|
+
if (virtual_keyboard_active && modifiers.length > 0) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
|
|
45
51
|
// Check if all specified modifiers are pressed
|
|
46
52
|
const modifiers_match = modifiers.every((mod) => event[MODIFIER_EVENT_KEYS[mod]]);
|
|
47
53
|
|
|
48
54
|
// Check if no unspecified modifiers are pressed
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
55
|
+
// NOTE: On virtual keyboards (iOS / Anroid) modifier keys are just ignored, because
|
|
56
|
+
// they must be assumed accidental (e.g. Shift activated as a result of auto-capitalization)
|
|
57
|
+
const no_extra_modifiers = virtual_keyboard_active
|
|
58
|
+
? true
|
|
59
|
+
: MODIFIER_KEYS.every((mod) => {
|
|
60
|
+
if (modifiers.includes(mod)) return true; // This modifier is expected
|
|
61
|
+
return !event[MODIFIER_EVENT_KEYS[mod]]; // This modifier should NOT be pressed
|
|
62
|
+
});
|
|
53
63
|
|
|
54
64
|
// Check if the key matches
|
|
55
65
|
const key_matches = event.key.toLowerCase() === non_modifier;
|
|
@@ -65,8 +75,10 @@ function matches_key_combo(key_combo, event) {
|
|
|
65
75
|
* but don't crash the application.
|
|
66
76
|
*/
|
|
67
77
|
function handle_key_map(key_map, event) {
|
|
78
|
+
const virtual_keyboard_active = is_virtual_keyboard_active();
|
|
79
|
+
|
|
68
80
|
for (const [key_combo, commands] of Object.entries(key_map)) {
|
|
69
|
-
if (matches_key_combo(key_combo, event)) {
|
|
81
|
+
if (matches_key_combo(key_combo, event, virtual_keyboard_active)) {
|
|
70
82
|
// Find the first enabled command and execute it
|
|
71
83
|
const enabled_command = commands.find((cmd) => cmd.is_enabled());
|
|
72
84
|
if (enabled_command) {
|
package/dist/Node.svelte
CHANGED
|
@@ -16,17 +16,45 @@
|
|
|
16
16
|
let child_index = $derived(node_array_meta ? parseInt(String(path.at(-1)), 10) : -1);
|
|
17
17
|
let is_first = $derived(node_array_meta && child_index === 0);
|
|
18
18
|
let is_last = $derived(node_array_meta && child_index === node_array_meta.length - 1);
|
|
19
|
+
|
|
20
|
+
// Marks and annotations covering this node become classes automatically:
|
|
21
|
+
// `mark-<type>`/`anno-<type>`, plus `-start`/`-end` variants on the run's
|
|
22
|
+
// first/last covered node. This allows styling ranges (including
|
|
23
|
+
// annotations, which never render wrapper components) with pure CSS. For
|
|
24
|
+
// richer rendering, node components still receive the `mark` and
|
|
25
|
+
// `annotations` props.
|
|
26
|
+
let range_classes = $derived.by(() => {
|
|
27
|
+
if (child_index < 0 || !node_array_meta?.annotations_for) return '';
|
|
28
|
+
const classes = [];
|
|
29
|
+
|
|
30
|
+
const add_classes = (range, prefix) => {
|
|
31
|
+
const range_type = range?.node?.type;
|
|
32
|
+
if (!range_type) return;
|
|
33
|
+
classes.push(`${prefix}-${range_type}`);
|
|
34
|
+
if (range.is_start) classes.push(`${prefix}-${range_type}-start`);
|
|
35
|
+
if (range.is_end) classes.push(`${prefix}-${range_type}-end`);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
add_classes(node_array_meta.mark_for?.(child_index), 'mark');
|
|
39
|
+
for (const annotation of node_array_meta.annotations_for(child_index)) {
|
|
40
|
+
add_classes(annotation, 'anno');
|
|
41
|
+
}
|
|
42
|
+
return classes.join(' ');
|
|
43
|
+
});
|
|
19
44
|
</script>
|
|
20
45
|
|
|
21
46
|
<svelte:element
|
|
22
47
|
this={tag}
|
|
23
48
|
id={node.id}
|
|
24
|
-
class="node-{node.type} {css_class}{
|
|
49
|
+
class="node-{node.type} {css_class}{range_classes
|
|
50
|
+
? ` ${range_classes}`
|
|
51
|
+
: ''}{is_first ? ' first' : ''}{is_last ? ' last' : ''}"
|
|
25
52
|
data-node-id={node.id}
|
|
26
53
|
data-path={path_str}
|
|
27
54
|
data-type="node"
|
|
28
55
|
style="anchor-name: --{path_str};{style}"
|
|
29
56
|
{...rest}
|
|
57
|
+
{@attach svedit.visibility_registry.track_node(path_str)}
|
|
30
58
|
>
|
|
31
59
|
{@render children()}
|
|
32
60
|
</svelte:element>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Node.svelte.d.ts","sourceRoot":"","sources":["../src/lib/Node.svelte.js"],"names":[],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"Node.svelte.d.ts","sourceRoot":"","sources":["../src/lib/Node.svelte.js"],"names":[],"mappings":";;;;;AA6DA,yFAAmD"}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
<script>
|
|
2
2
|
import { getContext, setContext } from 'svelte';
|
|
3
3
|
import UnknownNode from './UnknownNode.svelte';
|
|
4
|
-
import { serialize_path } from './utils.js';
|
|
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
7
|
|
|
8
|
-
/** @import { NodeArrayPropertyProps } from './types.d.ts'; */
|
|
8
|
+
/** @import { NodeArrayPropertyProps, NodeArrayAttachmentContext } from './types.d.ts'; */
|
|
9
9
|
|
|
10
10
|
const svedit = getContext('svedit');
|
|
11
11
|
let NodeGap = $derived(svedit.session.config.system_components?.node_gap ?? DefaultNodeGap);
|
|
@@ -20,10 +20,85 @@
|
|
|
20
20
|
// NodeGap's should_position_gap call to avoid N+1 joins per render.
|
|
21
21
|
let path_str = $derived(serialize_path(path));
|
|
22
22
|
|
|
23
|
-
let
|
|
24
|
-
let
|
|
23
|
+
let raw_value = $derived(svedit.session.get(path));
|
|
24
|
+
let node_ids = $derived(raw_value.nodes);
|
|
25
|
+
let marks = $derived(raw_value.marks);
|
|
26
|
+
let annotations = $derived(raw_value.annotations);
|
|
25
27
|
|
|
26
|
-
|
|
28
|
+
let fragments = $derived(get_fragments(node_ids, marks));
|
|
29
|
+
|
|
30
|
+
function get_fragments(node_ids, marks) {
|
|
31
|
+
const ranges = calculate_fragment_ranges(node_ids.length, marks);
|
|
32
|
+
let fragments = [];
|
|
33
|
+
|
|
34
|
+
for (const range of ranges) {
|
|
35
|
+
const nodes_slice = node_ids.slice(range.start_offset, range.end_offset);
|
|
36
|
+
|
|
37
|
+
if (range.type === 'mark') {
|
|
38
|
+
const node = svedit.session.get(range.node_id);
|
|
39
|
+
fragments.push({
|
|
40
|
+
type: 'mark',
|
|
41
|
+
node,
|
|
42
|
+
nodes: nodes_slice,
|
|
43
|
+
start_index: range.start_offset,
|
|
44
|
+
mark_index: range.mark_index
|
|
45
|
+
});
|
|
46
|
+
} else if (range.type === 'content') {
|
|
47
|
+
fragments.push({
|
|
48
|
+
type: 'nodes',
|
|
49
|
+
nodes: nodes_slice,
|
|
50
|
+
start_index: range.start_offset
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return fragments;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function get_node_key(node_id, index) {
|
|
58
|
+
let occurrence = 0;
|
|
59
|
+
for (let current_index = 0; current_index < index; current_index++) {
|
|
60
|
+
if (node_ids[current_index] === node_id) occurrence += 1;
|
|
61
|
+
}
|
|
62
|
+
return `${node_id}:${occurrence}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @returns {NodeArrayAttachmentContext}
|
|
67
|
+
*/
|
|
68
|
+
function get_range_context(range, index, node_index) {
|
|
69
|
+
const node = svedit.session.get(range.node_id);
|
|
70
|
+
const is_start = node_index === range.start_offset;
|
|
71
|
+
const is_end = node_index === range.end_offset - 1;
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
...range,
|
|
75
|
+
index,
|
|
76
|
+
node,
|
|
77
|
+
is_start,
|
|
78
|
+
is_middle: !is_start && !is_end,
|
|
79
|
+
is_end
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function get_covering_ranges(ranges, node_index) {
|
|
84
|
+
return ranges
|
|
85
|
+
.map((range, index) => ({ range, index }))
|
|
86
|
+
.filter(({ range }) => range.start_offset <= node_index && node_index < range.end_offset)
|
|
87
|
+
.map(({ range, index }) => get_range_context(range, index, node_index));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function get_annotation_contexts(node_index) {
|
|
91
|
+
return get_covering_ranges(annotations, node_index);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function get_mark(node_index) {
|
|
95
|
+
// The single mark wrapping this node, or null. Mark exclusivity
|
|
96
|
+
// guarantees at most one. Annotations never wrap and are exposed via
|
|
97
|
+
// `annotations` instead.
|
|
98
|
+
return get_covering_ranges(marks, node_index)[0] ?? null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Mirrors the TextProperty pattern: a `focused` class follows
|
|
27
102
|
// the model selection. Used by the CSS rule below to hide the empty-array
|
|
28
103
|
// NodeGap while the caret is in the placeholder, so its <br> isn't a
|
|
29
104
|
// second arrow-key stop (issue #260).
|
|
@@ -35,6 +110,15 @@
|
|
|
35
110
|
setContext('node_array_meta', {
|
|
36
111
|
get length() {
|
|
37
112
|
return node_ids.length;
|
|
113
|
+
},
|
|
114
|
+
// Lets Node self-serve the marks and annotations covering a child, so
|
|
115
|
+
// node wrappers can carry range classes without every node component
|
|
116
|
+
// having to thread the `mark`/`annotations` props through.
|
|
117
|
+
mark_for(node_index) {
|
|
118
|
+
return get_mark(node_index);
|
|
119
|
+
},
|
|
120
|
+
annotations_for(node_index) {
|
|
121
|
+
return get_annotation_contexts(node_index);
|
|
38
122
|
}
|
|
39
123
|
});
|
|
40
124
|
</script>
|
|
@@ -48,6 +132,7 @@
|
|
|
48
132
|
data-path={path_str}
|
|
49
133
|
style="anchor-name: --{path_str};{style ? ` ${style}` : ''}"
|
|
50
134
|
{...rest}
|
|
135
|
+
{@attach svedit.visibility_registry.track_array(path_str)}
|
|
51
136
|
>
|
|
52
137
|
{#if node_ids.length === 0 && svedit.editable}
|
|
53
138
|
<div
|
|
@@ -55,17 +140,39 @@
|
|
|
55
140
|
data-path={serialize_path([...path, 0])}
|
|
56
141
|
data-type="node"
|
|
57
142
|
style="anchor-name: --{serialize_path([...path, 0])};"
|
|
143
|
+
{@attach svedit.visibility_registry.track_node(serialize_path([...path, 0]))}
|
|
58
144
|
>
|
|
59
145
|
<NodeGap array_path={path} offset={0} count={0} empty />
|
|
60
146
|
</div>
|
|
61
147
|
{/if}
|
|
62
|
-
{#
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
<
|
|
148
|
+
{#snippet render_nodes(nodes_slice, start_index)}
|
|
149
|
+
{#each nodes_slice as id, slice_index (get_node_key(id, start_index + slice_index))}
|
|
150
|
+
{@const index = start_index + slice_index}
|
|
151
|
+
{@const node = svedit.session.get(id)}
|
|
152
|
+
{@const mark = get_mark(index)}
|
|
153
|
+
{@const annotations = get_annotation_contexts(index)}
|
|
154
|
+
<NodeGap array_path={path} offset={index} count={node_ids.length} />
|
|
155
|
+
{@const Component = svedit.session.config.node_components[node.type]}
|
|
156
|
+
{#if Component}
|
|
157
|
+
<Component path={[...path, index]} {mark} {annotations} />
|
|
158
|
+
{:else}
|
|
159
|
+
<UnknownNode path={[...path, index]} />
|
|
160
|
+
{/if}
|
|
161
|
+
{/each}
|
|
162
|
+
{/snippet}
|
|
163
|
+
|
|
164
|
+
{#each fragments as fragment, fragment_index (fragment_index)}
|
|
165
|
+
{#if fragment.type === 'nodes'}
|
|
166
|
+
{@render render_nodes(fragment.nodes, fragment.start_index)}
|
|
167
|
+
{:else if fragment.type === 'mark'}
|
|
168
|
+
{@const MarkComponent = svedit.session.config.node_components[fragment.node.type]}
|
|
169
|
+
{#if MarkComponent}
|
|
170
|
+
<MarkComponent path={[...path, 'marks', fragment.mark_index, 'node_id']}>
|
|
171
|
+
{@render render_nodes(fragment.nodes, fragment.start_index)}
|
|
172
|
+
</MarkComponent>
|
|
173
|
+
{:else}
|
|
174
|
+
{@render render_nodes(fragment.nodes, fragment.start_index)}
|
|
175
|
+
{/if}
|
|
69
176
|
{/if}
|
|
70
177
|
{/each}
|
|
71
178
|
{#if node_ids.length > 0}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NodeArrayProperty.svelte.d.ts","sourceRoot":"","sources":["../src/lib/NodeArrayProperty.svelte.js"],"names":[],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"NodeArrayProperty.svelte.d.ts","sourceRoot":"","sources":["../src/lib/NodeArrayProperty.svelte.js"],"names":[],"mappings":";;;;;AAuLA,mHAAgE"}
|
package/dist/NodeGap.svelte
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
<script>
|
|
14
14
|
import { serialize_path } from './utils.js';
|
|
15
15
|
import { getContext } from 'svelte';
|
|
16
|
+
import { should_position_gap } from './node_visibility.svelte.js';
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* ┌─────────────────────────────────────────────────────────────────┐
|
|
@@ -37,10 +38,13 @@
|
|
|
37
38
|
*
|
|
38
39
|
* Always present in the DOM when editable (stable structure for
|
|
39
40
|
* selection anchoring, scrollTo, etc.). Anchor positioning is
|
|
40
|
-
* activated lazily via the `.positioned` class,
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
41
|
+
* activated lazily via the `.positioned` class, derived reactively
|
|
42
|
+
* from the visibility registry's array_indices / edge_map. Svelte's
|
|
43
|
+
* reactive collections track reads at per-key granularity, so a
|
|
44
|
+
* registry write only
|
|
45
|
+
* re-evaluates the adjacent gaps. Applying the class declaratively
|
|
46
|
+
* (instead of imperative classList toggles) means it survives DOM
|
|
47
|
+
* recreation without a document change (e.g. dev-mode HMR).
|
|
44
48
|
*
|
|
45
49
|
* `position-visibility: anchors-visible` does NOT skip anchor
|
|
46
50
|
* resolution — the browser resolves all anchor() then hides the
|
|
@@ -62,6 +66,22 @@
|
|
|
62
66
|
let is_last = $derived(offset === count);
|
|
63
67
|
let type = $derived(is_first ? 'gap-before' : 'gap-after');
|
|
64
68
|
|
|
69
|
+
let path_str = $derived(serialize_path(array_path));
|
|
70
|
+
// Two-derived split is load-bearing: get_array_indices lazily
|
|
71
|
+
// creates the set, and Svelte doesn't track deps on state created
|
|
72
|
+
// inside the reading derived — so acquire here, read in `positioned`.
|
|
73
|
+
let near_indices = $derived(svedit.visibility_registry.get_array_indices(path_str));
|
|
74
|
+
let positioned = $derived(
|
|
75
|
+
is_editable &&
|
|
76
|
+
should_position_gap(
|
|
77
|
+
near_indices,
|
|
78
|
+
svedit.visibility_registry.edge_map.get(path_str),
|
|
79
|
+
offset,
|
|
80
|
+
is_last,
|
|
81
|
+
empty
|
|
82
|
+
)
|
|
83
|
+
);
|
|
84
|
+
|
|
65
85
|
let gap_style = $derived.by(() => {
|
|
66
86
|
if (!is_editable) return '';
|
|
67
87
|
const prev_idx = offset - 1;
|
|
@@ -87,8 +107,9 @@
|
|
|
87
107
|
class:gap-after={!is_first}
|
|
88
108
|
class:empty
|
|
89
109
|
class:last={is_last}
|
|
110
|
+
class:positioned
|
|
90
111
|
data-type={type}
|
|
91
|
-
data-gap-array-path={
|
|
112
|
+
data-gap-array-path={path_str}
|
|
92
113
|
data-gap-offset={offset}
|
|
93
114
|
style={gap_style}
|
|
94
115
|
>
|
|
@@ -111,9 +132,30 @@
|
|
|
111
132
|
}
|
|
112
133
|
|
|
113
134
|
/* ------------------------------------------------------------------ */
|
|
114
|
-
/* Un-positioned:
|
|
135
|
+
/* Un-positioned: no layout box at all */
|
|
115
136
|
/* ------------------------------------------------------------------ */
|
|
116
137
|
|
|
138
|
+
/*
|
|
139
|
+
* display: none is load-bearing for large documents. A zero-size
|
|
140
|
+
* absolutely-positioned selectable still belongs to the containing
|
|
141
|
+
* block's out-of-flow list, and the browser lays out EVERY such box
|
|
142
|
+
* on EVERY layout pass — measured ~430ms per pass at 2000 nodes
|
|
143
|
+
* (~3500 gaps) in Chrome vs ~23ms with the boxes removed. That cost
|
|
144
|
+
* hits every keystroke (the per-change reconcile reads a rect for
|
|
145
|
+
* every node, and selection rendering forces layout), every window
|
|
146
|
+
* resize frame, and every scroll-triggered layout.
|
|
147
|
+
*
|
|
148
|
+
* Off-screen gaps therefore contribute no layout box. DOM structure
|
|
149
|
+
* stays stable (the .node-gap wrapper and selectable elements remain),
|
|
150
|
+
* and DOM Ranges may still point into display:none elements, so
|
|
151
|
+
* programmatic node selections targeting off-screen gaps keep working.
|
|
152
|
+
* Empty-array gaps are excluded: they must stay clickable/visible even
|
|
153
|
+
* before reconcile positions them.
|
|
154
|
+
*/
|
|
155
|
+
:global(.node-gap:not(.positioned):not(.empty) .svedit-selectable) {
|
|
156
|
+
display: none;
|
|
157
|
+
}
|
|
158
|
+
|
|
117
159
|
:global(.node-gap:not(.positioned) .svedit-selectable) {
|
|
118
160
|
position: absolute;
|
|
119
161
|
pointer-events: none;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NodeGap.svelte.d.ts","sourceRoot":"","sources":["../src/lib/NodeGap.svelte.js"],"names":[],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"NodeGap.svelte.d.ts","sourceRoot":"","sources":["../src/lib/NodeGap.svelte.js"],"names":[],"mappings":";;;;;AA6HA;gBA1DgC,GAAG;YAAU,GAAG;WAAS,GAAG;YAAU,OAAO;WA0DvB;wBA1DpC;IAAE,UAAU,EAAE,GAAG,CAAC;IAAC,MAAM,EAAE,GAAG,CAAC;IAAC,KAAK,EAAE,GAAG,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE"}
|
package/dist/Session.svelte.d.ts
CHANGED
|
@@ -27,7 +27,24 @@ export default class Session {
|
|
|
27
27
|
can_undo: boolean;
|
|
28
28
|
can_redo: boolean;
|
|
29
29
|
selected_node: any;
|
|
30
|
+
available_mark_types: any;
|
|
30
31
|
available_annotation_types: any;
|
|
32
|
+
selected_marks: (Attachment & {
|
|
33
|
+
index: number;
|
|
34
|
+
node: any;
|
|
35
|
+
})[];
|
|
36
|
+
active_mark: Attachment & {
|
|
37
|
+
index: number;
|
|
38
|
+
node: any;
|
|
39
|
+
};
|
|
40
|
+
selected_annotations: (Attachment & {
|
|
41
|
+
index: number;
|
|
42
|
+
node: any;
|
|
43
|
+
})[];
|
|
44
|
+
active_annotation: Attachment & {
|
|
45
|
+
index: number;
|
|
46
|
+
node: any;
|
|
47
|
+
};
|
|
31
48
|
/**
|
|
32
49
|
* Sets the selection with validation
|
|
33
50
|
* @param {Selection | null} value - The new selection
|
|
@@ -76,6 +93,7 @@ export default class Session {
|
|
|
76
93
|
* @param {object} context - The svedit context with session, editable, canvas, etc.
|
|
77
94
|
*/
|
|
78
95
|
initialize_commands(context: object): void;
|
|
96
|
+
get_available_mark_types(): any;
|
|
79
97
|
get_available_annotation_types(): any;
|
|
80
98
|
get_selected_node(): any;
|
|
81
99
|
/**
|
|
@@ -114,8 +132,8 @@ export default class Session {
|
|
|
114
132
|
* session.get(['page_1', 'body', 3, 'list_items', 0]) // => { type: 'list_item', id: 'list_item_1', ... }
|
|
115
133
|
*
|
|
116
134
|
* @example
|
|
117
|
-
* // Get
|
|
118
|
-
* session.get(['page_1', 'cover', 'title']) // => {
|
|
135
|
+
* // Get a text property
|
|
136
|
+
* session.get(['page_1', 'cover', 'title']) // => {content: 'Hello world', marks: [], annotations: []}
|
|
119
137
|
*/
|
|
120
138
|
get(path: DocumentPath | string): any;
|
|
121
139
|
/**
|
|
@@ -150,7 +168,7 @@ export default class Session {
|
|
|
150
168
|
};
|
|
151
169
|
/**
|
|
152
170
|
* Determines the kind of a node ('block' for structured blocks, 'text' for pure
|
|
153
|
-
* text nodes or 'annotation' for annotation nodes.
|
|
171
|
+
* text nodes, 'mark' for mark nodes or 'annotation' for annotation nodes.
|
|
154
172
|
* @param {any} node
|
|
155
173
|
* @returns {NodeKind}
|
|
156
174
|
*/
|
|
@@ -169,19 +187,18 @@ export default class Session {
|
|
|
169
187
|
* @returns {Selection|null} The next node insert caret selection, or null if none is available
|
|
170
188
|
*/
|
|
171
189
|
get_next_node_insert_caret(selection?: Selection): Selection | null;
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
*
|
|
176
|
-
* @param {string} [annotation_type] Optional annotation type to filter by
|
|
177
|
-
* @returns {Annotation|null}
|
|
178
|
-
*/
|
|
179
|
-
active_annotation(annotation_type?: string): Annotation | null;
|
|
180
|
-
get_selected_annotated_text(): {
|
|
181
|
-
text: string;
|
|
190
|
+
get_selected_text(): {
|
|
191
|
+
content: string;
|
|
192
|
+
marks: any;
|
|
182
193
|
annotations: any;
|
|
183
194
|
nodes: {};
|
|
184
195
|
};
|
|
196
|
+
get_selected_annotated_nodes(): {
|
|
197
|
+
nodes: {};
|
|
198
|
+
main_nodes: any;
|
|
199
|
+
marks: any;
|
|
200
|
+
annotations: any;
|
|
201
|
+
};
|
|
185
202
|
get_selected_plain_text(): string;
|
|
186
203
|
get_selected_nodes(): any;
|
|
187
204
|
select_parent(): void;
|
|
@@ -224,11 +241,11 @@ export type SessionOptions = {
|
|
|
224
241
|
};
|
|
225
242
|
import type { DocumentSchema } from './types.d.ts';
|
|
226
243
|
import type { Document } from './types.d.ts';
|
|
244
|
+
import type { Attachment } from './types.d.ts';
|
|
227
245
|
import type { Selection } from './types.d.ts';
|
|
228
246
|
import Transaction from './Transaction.svelte.js';
|
|
229
247
|
import type { DocumentPath } from './types.d.ts';
|
|
230
248
|
import type { NodeKind } from './types.d.ts';
|
|
231
|
-
import type { Annotation } from './types.d.ts';
|
|
232
249
|
import type { DocumentNode } from './types.d.ts';
|
|
233
250
|
import type { NodeId } from './types.d.ts';
|
|
234
251
|
//# sourceMappingURL=Session.svelte.d.ts.map
|