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
|
@@ -1,29 +1,121 @@
|
|
|
1
|
-
<script>
|
|
1
|
+
<script lang="ts">
|
|
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
|
+
import type {
|
|
8
|
+
NodeArrayPropertyProps,
|
|
9
|
+
NodeArrayAttachmentContext,
|
|
10
|
+
NodeArrayFragment,
|
|
11
|
+
NodeId,
|
|
12
|
+
Attachment,
|
|
13
|
+
Mark,
|
|
14
|
+
NodeArrayRenderContext,
|
|
15
|
+
SveditRenderContext
|
|
16
|
+
} from './types.js';
|
|
7
17
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const svedit = getContext('svedit');
|
|
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.
|
|
21
34
|
let path_str = $derived(serialize_path(path));
|
|
22
35
|
|
|
23
|
-
let
|
|
24
|
-
let
|
|
36
|
+
let raw_value = $derived(svedit.session.get(path));
|
|
37
|
+
let node_ids = $derived(raw_value.nodes);
|
|
38
|
+
let marks = $derived(raw_value.marks);
|
|
39
|
+
let annotations = $derived(raw_value.annotations);
|
|
40
|
+
|
|
41
|
+
let fragments = $derived(get_fragments(node_ids, marks));
|
|
42
|
+
|
|
43
|
+
function get_fragments(node_ids: NodeId[], marks: Mark[]): NodeArrayFragment[] {
|
|
44
|
+
const ranges = calculate_fragment_ranges(node_ids.length, marks);
|
|
45
|
+
const fragments: NodeArrayFragment[] = [];
|
|
46
|
+
|
|
47
|
+
for (const range of ranges) {
|
|
48
|
+
const nodes_slice = node_ids.slice(range.start_offset, range.end_offset);
|
|
49
|
+
|
|
50
|
+
if (range.type === 'mark') {
|
|
51
|
+
const node = svedit.session.get(range.node_id);
|
|
52
|
+
fragments.push({
|
|
53
|
+
type: 'mark',
|
|
54
|
+
node,
|
|
55
|
+
nodes: nodes_slice,
|
|
56
|
+
start_index: range.start_offset,
|
|
57
|
+
mark_index: range.mark_index!
|
|
58
|
+
});
|
|
59
|
+
} else if (range.type === 'content') {
|
|
60
|
+
fragments.push({
|
|
61
|
+
type: 'nodes',
|
|
62
|
+
nodes: nodes_slice,
|
|
63
|
+
start_index: range.start_offset
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return fragments;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function get_node_key(node_id: NodeId, index: number): string {
|
|
71
|
+
let occurrence = 0;
|
|
72
|
+
for (let current_index = 0; current_index < index; current_index++) {
|
|
73
|
+
if (node_ids[current_index] === node_id) occurrence += 1;
|
|
74
|
+
}
|
|
75
|
+
return `${node_id}:${occurrence}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function get_range_context(
|
|
79
|
+
range: Attachment,
|
|
80
|
+
index: number,
|
|
81
|
+
node_index: number
|
|
82
|
+
): NodeArrayAttachmentContext {
|
|
83
|
+
const node = svedit.session.get(range.node_id);
|
|
84
|
+
const is_start = node_index === range.start_offset;
|
|
85
|
+
const is_end = node_index === range.end_offset - 1;
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
...range,
|
|
89
|
+
index,
|
|
90
|
+
node,
|
|
91
|
+
is_start,
|
|
92
|
+
is_middle: !is_start && !is_end,
|
|
93
|
+
is_end
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function get_covering_ranges(
|
|
98
|
+
ranges: Attachment[],
|
|
99
|
+
node_index: number
|
|
100
|
+
): NodeArrayAttachmentContext[] {
|
|
101
|
+
return ranges
|
|
102
|
+
.map((range, index) => ({ range, index }))
|
|
103
|
+
.filter(({ range }) => range.start_offset <= node_index && node_index < range.end_offset)
|
|
104
|
+
.map(({ range, index }) => get_range_context(range, index, node_index));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function get_annotation_contexts(node_index: number): NodeArrayAttachmentContext[] {
|
|
108
|
+
return get_covering_ranges(annotations, node_index);
|
|
109
|
+
}
|
|
25
110
|
|
|
26
|
-
|
|
111
|
+
function get_mark(node_index: number): NodeArrayAttachmentContext | null {
|
|
112
|
+
// The single mark wrapping this node, or null. Mark exclusivity
|
|
113
|
+
// guarantees at most one. Annotations never wrap and are exposed via
|
|
114
|
+
// `annotations` instead.
|
|
115
|
+
return get_covering_ranges(marks, node_index)[0] ?? null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Mirrors the TextProperty pattern: a `focused` class follows
|
|
27
119
|
// the model selection. Used by the CSS rule below to hide the empty-array
|
|
28
120
|
// NodeGap while the caret is in the placeholder, so its <br> isn't a
|
|
29
121
|
// second arrow-key stop (issue #260).
|
|
@@ -32,9 +124,18 @@
|
|
|
32
124
|
serialize_path(svedit.session.selection.path) === path_str
|
|
33
125
|
);
|
|
34
126
|
|
|
35
|
-
setContext('node_array_meta', {
|
|
127
|
+
setContext<NodeArrayRenderContext>('node_array_meta', {
|
|
36
128
|
get length() {
|
|
37
129
|
return node_ids.length;
|
|
130
|
+
},
|
|
131
|
+
// Lets Node self-serve the marks and annotations covering a child, so
|
|
132
|
+
// node wrappers can carry range classes without every node component
|
|
133
|
+
// having to thread the `mark`/`annotations` props through.
|
|
134
|
+
mark_for(node_index: number) {
|
|
135
|
+
return get_mark(node_index);
|
|
136
|
+
},
|
|
137
|
+
annotations_for(node_index: number) {
|
|
138
|
+
return get_annotation_contexts(node_index);
|
|
38
139
|
}
|
|
39
140
|
});
|
|
40
141
|
</script>
|
|
@@ -48,6 +149,7 @@
|
|
|
48
149
|
data-path={path_str}
|
|
49
150
|
style="anchor-name: --{path_str};{style ? ` ${style}` : ''}"
|
|
50
151
|
{...rest}
|
|
152
|
+
{@attach svedit.visibility_registry.track_array(path_str)}
|
|
51
153
|
>
|
|
52
154
|
{#if node_ids.length === 0 && svedit.editable}
|
|
53
155
|
<div
|
|
@@ -55,17 +157,39 @@
|
|
|
55
157
|
data-path={serialize_path([...path, 0])}
|
|
56
158
|
data-type="node"
|
|
57
159
|
style="anchor-name: --{serialize_path([...path, 0])};"
|
|
160
|
+
{@attach svedit.visibility_registry.track_node(serialize_path([...path, 0]))}
|
|
58
161
|
>
|
|
59
162
|
<NodeGap array_path={path} offset={0} count={0} empty />
|
|
60
163
|
</div>
|
|
61
164
|
{/if}
|
|
62
|
-
{#
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
<
|
|
165
|
+
{#snippet render_nodes(nodes_slice: NodeId[], start_index: number)}
|
|
166
|
+
{#each nodes_slice as id, slice_index (get_node_key(id, start_index + slice_index))}
|
|
167
|
+
{@const index = start_index + slice_index}
|
|
168
|
+
{@const node = svedit.session.get(id)}
|
|
169
|
+
{@const mark = get_mark(index)}
|
|
170
|
+
{@const annotations = get_annotation_contexts(index)}
|
|
171
|
+
<NodeGap array_path={path} offset={index} count={node_ids.length} />
|
|
172
|
+
{@const Component = svedit.session.config.node_components[node.type]}
|
|
173
|
+
{#if Component}
|
|
174
|
+
<Component path={[...path, index]} {mark} {annotations} />
|
|
175
|
+
{:else}
|
|
176
|
+
<UnknownNode path={[...path, index]} />
|
|
177
|
+
{/if}
|
|
178
|
+
{/each}
|
|
179
|
+
{/snippet}
|
|
180
|
+
|
|
181
|
+
{#each fragments as fragment, fragment_index (fragment_index)}
|
|
182
|
+
{#if fragment.type === 'nodes'}
|
|
183
|
+
{@render render_nodes(fragment.nodes, fragment.start_index)}
|
|
184
|
+
{:else if fragment.type === 'mark'}
|
|
185
|
+
{@const MarkComponent = svedit.session.config.node_components[fragment.node.type]}
|
|
186
|
+
{#if MarkComponent}
|
|
187
|
+
<MarkComponent path={[...path, 'marks', fragment.mark_index, 'node_id']}>
|
|
188
|
+
{@render render_nodes(fragment.nodes, fragment.start_index)}
|
|
189
|
+
</MarkComponent>
|
|
190
|
+
{:else}
|
|
191
|
+
{@render render_nodes(fragment.nodes, fragment.start_index)}
|
|
192
|
+
{/if}
|
|
69
193
|
{/if}
|
|
70
194
|
{/each}
|
|
71
195
|
{#if node_ids.length > 0}
|
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,9 +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
|
+
import { should_position_gap } from './node_visibility.svelte.js';
|
|
17
|
+
import type { DocumentPath, SveditRenderContext } from './types.js';
|
|
16
18
|
|
|
17
19
|
/**
|
|
18
20
|
* ┌─────────────────────────────────────────────────────────────────┐
|
|
@@ -37,10 +39,13 @@
|
|
|
37
39
|
*
|
|
38
40
|
* Always present in the DOM when editable (stable structure for
|
|
39
41
|
* selection anchoring, scrollTo, etc.). Anchor positioning is
|
|
40
|
-
* activated lazily via the `.positioned` class,
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
42
|
+
* activated lazily via the `.positioned` class, derived reactively
|
|
43
|
+
* from the visibility registry's array_indices / edge_map. Svelte's
|
|
44
|
+
* reactive collections track reads at per-key granularity, so a
|
|
45
|
+
* registry write only
|
|
46
|
+
* re-evaluates the adjacent gaps. Applying the class declaratively
|
|
47
|
+
* (instead of imperative classList toggles) means it survives DOM
|
|
48
|
+
* recreation without a document change (e.g. dev-mode HMR).
|
|
44
49
|
*
|
|
45
50
|
* `position-visibility: anchors-visible` does NOT skip anchor
|
|
46
51
|
* resolution — the browser resolves all anchor() then hides the
|
|
@@ -54,14 +59,35 @@
|
|
|
54
59
|
* sure your custom component also handles read-only mode by rendering a
|
|
55
60
|
* plain `.node-gap` placeholder without editable internals.
|
|
56
61
|
*/
|
|
57
|
-
const svedit = getContext('svedit');
|
|
58
|
-
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();
|
|
59
69
|
let is_editable = $derived(svedit.editable);
|
|
60
70
|
|
|
61
71
|
let is_first = $derived(offset === 0);
|
|
62
72
|
let is_last = $derived(offset === count);
|
|
63
73
|
let type = $derived(is_first ? 'gap-before' : 'gap-after');
|
|
64
74
|
|
|
75
|
+
let path_str = $derived(serialize_path(array_path));
|
|
76
|
+
// Two-derived split is load-bearing: get_array_indices lazily
|
|
77
|
+
// creates the set, and Svelte doesn't track deps on state created
|
|
78
|
+
// inside the reading derived — so acquire here, read in `positioned`.
|
|
79
|
+
let near_indices = $derived(svedit.visibility_registry.get_array_indices(path_str));
|
|
80
|
+
let positioned = $derived(
|
|
81
|
+
is_editable &&
|
|
82
|
+
should_position_gap(
|
|
83
|
+
near_indices,
|
|
84
|
+
svedit.visibility_registry.edge_map.get(path_str),
|
|
85
|
+
offset,
|
|
86
|
+
is_last,
|
|
87
|
+
empty
|
|
88
|
+
)
|
|
89
|
+
);
|
|
90
|
+
|
|
65
91
|
let gap_style = $derived.by(() => {
|
|
66
92
|
if (!is_editable) return '';
|
|
67
93
|
const prev_idx = offset - 1;
|
|
@@ -87,8 +113,9 @@
|
|
|
87
113
|
class:gap-after={!is_first}
|
|
88
114
|
class:empty
|
|
89
115
|
class:last={is_last}
|
|
116
|
+
class:positioned
|
|
90
117
|
data-type={type}
|
|
91
|
-
data-gap-array-path={
|
|
118
|
+
data-gap-array-path={path_str}
|
|
92
119
|
data-gap-offset={offset}
|
|
93
120
|
style={gap_style}
|
|
94
121
|
>
|
|
@@ -111,9 +138,30 @@
|
|
|
111
138
|
}
|
|
112
139
|
|
|
113
140
|
/* ------------------------------------------------------------------ */
|
|
114
|
-
/* Un-positioned:
|
|
141
|
+
/* Un-positioned: no layout box at all */
|
|
115
142
|
/* ------------------------------------------------------------------ */
|
|
116
143
|
|
|
144
|
+
/*
|
|
145
|
+
* display: none is load-bearing for large documents. A zero-size
|
|
146
|
+
* absolutely-positioned selectable still belongs to the containing
|
|
147
|
+
* block's out-of-flow list, and the browser lays out EVERY such box
|
|
148
|
+
* on EVERY layout pass — measured ~430ms per pass at 2000 nodes
|
|
149
|
+
* (~3500 gaps) in Chrome vs ~23ms with the boxes removed. That cost
|
|
150
|
+
* hits every keystroke (the per-change reconcile reads a rect for
|
|
151
|
+
* every node, and selection rendering forces layout), every window
|
|
152
|
+
* resize frame, and every scroll-triggered layout.
|
|
153
|
+
*
|
|
154
|
+
* Off-screen gaps therefore contribute no layout box. DOM structure
|
|
155
|
+
* stays stable (the .node-gap wrapper and selectable elements remain),
|
|
156
|
+
* and DOM Ranges may still point into display:none elements, so
|
|
157
|
+
* programmatic node selections targeting off-screen gaps keep working.
|
|
158
|
+
* Empty-array gaps are excluded: they must stay clickable/visible even
|
|
159
|
+
* before reconcile positions them.
|
|
160
|
+
*/
|
|
161
|
+
:global(.node-gap:not(.positioned):not(.empty) .svedit-selectable) {
|
|
162
|
+
display: none;
|
|
163
|
+
}
|
|
164
|
+
|
|
117
165
|
:global(.node-gap:not(.positioned) .svedit-selectable) {
|
|
118
166
|
position: absolute;
|
|
119
167
|
pointer-events: none;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
<script>
|
|
1
|
+
<script lang="ts">
|
|
2
2
|
import { getContext } from 'svelte';
|
|
3
3
|
import NodeCaret from './NodeCaret.svelte';
|
|
4
4
|
import { PATH_SEPARATOR, serialize_path } from './utils.js';
|
|
5
|
+
import type { DocumentPath, SveditRenderContext } from './types.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* ┌─────────────────────────────────────────────────────────────────┐
|
|
@@ -28,9 +29,9 @@
|
|
|
28
29
|
* re-evaluates when THIS array's visible-index set changes.
|
|
29
30
|
*/
|
|
30
31
|
|
|
31
|
-
let { path, count } = $props();
|
|
32
|
+
let { path, count }: { path: DocumentPath; count: number } = $props();
|
|
32
33
|
|
|
33
|
-
const svedit = getContext('svedit');
|
|
34
|
+
const svedit = getContext<SveditRenderContext>('svedit');
|
|
34
35
|
let path_str = $derived(serialize_path(path));
|
|
35
36
|
let visible = $derived(svedit.visibility_registry.get_array_indices(path_str));
|
|
36
37
|
|
|
@@ -51,8 +52,16 @@
|
|
|
51
52
|
? `;--_f:${anchor_prefix}${PATH_SEPARATOR}0;--_s:${anchor_prefix}${PATH_SEPARATOR}1`
|
|
52
53
|
: '';
|
|
53
54
|
|
|
54
|
-
|
|
55
|
-
|
|
55
|
+
type GapMarker = {
|
|
56
|
+
key: string;
|
|
57
|
+
offset: number;
|
|
58
|
+
type: string;
|
|
59
|
+
vars: string;
|
|
60
|
+
is_first: boolean;
|
|
61
|
+
is_last: boolean;
|
|
62
|
+
has_pair: boolean;
|
|
63
|
+
};
|
|
64
|
+
const result: GapMarker[] = [];
|
|
56
65
|
|
|
57
66
|
if (count === 0) {
|
|
58
67
|
if (visible.has(0)) {
|
|
@@ -73,15 +82,15 @@
|
|
|
73
82
|
// 1000 nodes with ~30 visible, this drops 1001 SvelteSet/SvelteMap
|
|
74
83
|
// reads per re-derivation to ~30. Filter stale indices >= count
|
|
75
84
|
// that may briefly remain after a node delete (until MO fires).
|
|
76
|
-
const sorted = [];
|
|
85
|
+
const sorted: number[] = [];
|
|
77
86
|
for (const i of visible) {
|
|
78
87
|
if (i >= 0 && i < count) sorted.push(i);
|
|
79
88
|
}
|
|
80
89
|
sorted.sort((a, b) => a - b);
|
|
81
90
|
if (sorted.length === 0) return result;
|
|
82
91
|
|
|
83
|
-
const offsets = [];
|
|
84
|
-
function add_offset(offset) {
|
|
92
|
+
const offsets: number[] = [];
|
|
93
|
+
function add_offset(offset: number) {
|
|
85
94
|
if (!offsets.includes(offset)) offsets.push(offset);
|
|
86
95
|
}
|
|
87
96
|
|
|
@@ -112,7 +121,7 @@
|
|
|
112
121
|
? `${g_prefix}${PATH_SEPARATOR}0-gap-before`
|
|
113
122
|
: `${g_prefix}${PATH_SEPARATOR}${offset - 1}-gap-after`;
|
|
114
123
|
|
|
115
|
-
let type, vars;
|
|
124
|
+
let type: string, vars: string;
|
|
116
125
|
if (is_first || is_last) {
|
|
117
126
|
type = 'gap-edge';
|
|
118
127
|
const adjacent = is_first
|
|
@@ -646,4 +655,4 @@
|
|
|
646
655
|
outline: 1px solid blue;
|
|
647
656
|
outline-offset: 0.5px;
|
|
648
657
|
} */
|
|
649
|
-
</style>
|
|
658
|
+
</style>
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
<script>
|
|
1
|
+
<script lang="ts">
|
|
2
2
|
import { getContext } from 'svelte';
|
|
3
3
|
import { serialize_path } from './utils.js';
|
|
4
|
+
import type { DocumentPath, SveditContext } from './types.js';
|
|
4
5
|
|
|
5
|
-
const svedit = getContext('svedit');
|
|
6
|
+
const svedit = getContext<SveditContext>('svedit');
|
|
6
7
|
let selected_node_paths = $derived(get_selected_node_paths());
|
|
7
8
|
|
|
8
|
-
function get_selected_node_paths() {
|
|
9
|
-
const paths = [];
|
|
9
|
+
function get_selected_node_paths(): DocumentPath[] | undefined {
|
|
10
|
+
const paths: DocumentPath[] = [];
|
|
10
11
|
const selection = svedit.session.selection;
|
|
11
12
|
if (!selection) return;
|
|
12
13
|
if (selection.type !== 'node' || selection.anchor_offset === selection.focus_offset) return;
|
|
@@ -36,10 +37,23 @@
|
|
|
36
37
|
<style>
|
|
37
38
|
.selected-node-overlay,
|
|
38
39
|
.selected-property-overlay {
|
|
40
|
+
/* Selection frame: a single 1px outline, offset -0.5px so the
|
|
41
|
+
hairline centers on the node's edge and neighboring frames merge
|
|
42
|
+
into one shared line. Alternatives tried and rejected:
|
|
43
|
+
- Plain 1px border: adjacent selected nodes stack their borders
|
|
44
|
+
into a 2px seam between them, which looks ugly.
|
|
45
|
+
- Thick translucent frame (8px border) plus an inner hairline
|
|
46
|
+
outline: multiple nested lines take more cognitive effort to
|
|
47
|
+
parse than a single line.
|
|
48
|
+
- Inset box-shadow rings (1px stroke + 8px translucent): same
|
|
49
|
+
layered-frame look, same objection.
|
|
50
|
+
Performance note: outlines don't participate in layout, so
|
|
51
|
+
selection changes can't trigger reflows. */
|
|
39
52
|
position: absolute;
|
|
40
53
|
background: var(--svedit-editing-fill);
|
|
41
|
-
|
|
42
|
-
|
|
54
|
+
outline: 1px solid var(--svedit-editing-stroke);
|
|
55
|
+
outline-offset: -0.5px;
|
|
56
|
+
border-radius: 1px;
|
|
43
57
|
top: anchor(top);
|
|
44
58
|
left: anchor(left);
|
|
45
59
|
bottom: anchor(bottom);
|