-
Notifications
You must be signed in to change notification settings - Fork 17
Expand file tree
/
Copy pathNodeArrayProperty.svelte
More file actions
217 lines (198 loc) · 7 KB
/
Copy pathNodeArrayProperty.svelte
File metadata and controls
217 lines (198 loc) · 7 KB
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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
<script lang="ts">
import { getContext, setContext } from 'svelte';
import UnknownNode from './UnknownNode.svelte';
import { serialize_path, calculate_fragment_ranges } from './utils.js';
import DefaultNodeGap from './NodeGap.svelte';
import DefaultNodeGapMarkers from './NodeGapMarkers.svelte';
import type {
NodeArrayPropertyProps,
NodeArrayAttachmentContext,
NodeArrayFragment,
NodeId,
Attachment,
Mark,
NodeArrayRenderContext,
SveditRenderContext
} from './types.js';
const svedit = getContext<SveditRenderContext>('svedit');
let NodeGap = $derived(svedit.session.config.system_components?.node_gap ?? DefaultNodeGap);
let NodeGapMarkers = $derived(
svedit.session.config.system_components?.node_gap_markers ?? DefaultNodeGapMarkers
);
let {
path,
tag = 'div',
class: css_class,
style = '',
...rest
}: NodeArrayPropertyProps = $props();
// Pre-joined once per path change; reused by data-path and by every
// NodeGap's should_position_gap call to avoid N+1 joins per render.
let path_str = $derived(serialize_path(path));
let raw_value = $derived(svedit.session.get(path));
let node_ids = $derived(raw_value.nodes);
let marks = $derived(raw_value.marks);
let annotations = $derived(raw_value.annotations);
let fragments = $derived(get_fragments(node_ids, marks));
function get_fragments(node_ids: NodeId[], marks: Mark[]): NodeArrayFragment[] {
const ranges = calculate_fragment_ranges(node_ids.length, marks);
const fragments: NodeArrayFragment[] = [];
for (const range of ranges) {
const nodes_slice = node_ids.slice(range.start_offset, range.end_offset);
if (range.type === 'mark') {
const node = svedit.session.get(range.node_id);
fragments.push({
type: 'mark',
node,
nodes: nodes_slice,
start_index: range.start_offset,
mark_index: range.mark_index!
});
} else if (range.type === 'content') {
fragments.push({
type: 'nodes',
nodes: nodes_slice,
start_index: range.start_offset
});
}
}
return fragments;
}
function get_node_key(node_id: NodeId, index: number): string {
let occurrence = 0;
for (let current_index = 0; current_index < index; current_index++) {
if (node_ids[current_index] === node_id) occurrence += 1;
}
return `${node_id}:${occurrence}`;
}
function get_range_context(
range: Attachment,
index: number,
node_index: number
): NodeArrayAttachmentContext {
const node = svedit.session.get(range.node_id);
const is_start = node_index === range.start_offset;
const is_end = node_index === range.end_offset - 1;
return {
...range,
index,
node,
is_start,
is_middle: !is_start && !is_end,
is_end
};
}
function get_covering_ranges(
ranges: Attachment[],
node_index: number
): NodeArrayAttachmentContext[] {
return ranges
.map((range, index) => ({ range, index }))
.filter(({ range }) => range.start_offset <= node_index && node_index < range.end_offset)
.map(({ range, index }) => get_range_context(range, index, node_index));
}
function get_annotation_contexts(node_index: number): NodeArrayAttachmentContext[] {
return get_covering_ranges(annotations, node_index);
}
function get_mark(node_index: number): NodeArrayAttachmentContext | null {
// The single mark wrapping this node, or null. Mark exclusivity
// guarantees at most one. Annotations never wrap and are exposed via
// `annotations` instead.
return get_covering_ranges(marks, node_index)[0] ?? null;
}
// Mirrors the TextProperty pattern: a `focused` class follows
// the model selection. Used by the CSS rule below to hide the empty-array
// NodeGap while the caret is in the placeholder, so its <br> isn't a
// second arrow-key stop (issue #260).
let is_focused = $derived(
svedit.session.selection?.type === 'node' &&
serialize_path(svedit.session.selection.path) === path_str
);
setContext<NodeArrayRenderContext>('node_array_meta', {
get length() {
return node_ids.length;
},
// Lets Node self-serve the marks and annotations covering a child, so
// node wrappers can carry range classes without every node component
// having to thread the `mark`/`annotations` props through.
mark_for(node_index: number) {
return get_mark(node_index);
},
annotations_for(node_index: number) {
return get_annotation_contexts(node_index);
}
});
</script>
<!-- we use the anchor of node_array in Overlays.svelte to position the last insertion point in a horizontal layout based on the right edge of the container -->
<svelte:element
this={tag}
class={css_class}
class:focused={is_focused}
data-type="node_array"
data-path={path_str}
style="anchor-name: --{path_str};{style ? ` ${style}` : ''}"
{...rest}
{@attach svedit.visibility_registry.track_array(path_str)}
>
{#if node_ids.length === 0 && svedit.editable}
<div
class="empty-node-placeholder"
data-path={serialize_path([...path, 0])}
data-type="node"
style="anchor-name: --{serialize_path([...path, 0])};"
{@attach svedit.visibility_registry.track_node(serialize_path([...path, 0]))}
>
<NodeGap array_path={path} offset={0} count={0} empty />
</div>
{/if}
{#snippet render_nodes(nodes_slice: NodeId[], start_index: number)}
{#each nodes_slice as id, slice_index (get_node_key(id, start_index + slice_index))}
{@const index = start_index + slice_index}
{@const node = svedit.session.get(id)}
{@const mark = get_mark(index)}
{@const annotations = get_annotation_contexts(index)}
<NodeGap array_path={path} offset={index} count={node_ids.length} />
{@const Component = svedit.session.config.node_components[node.type]}
{#if Component}
<Component path={[...path, index]} {mark} {annotations} />
{:else}
<UnknownNode path={[...path, index]} />
{/if}
{/each}
{/snippet}
{#each fragments as fragment, fragment_index (fragment_index)}
{#if fragment.type === 'nodes'}
{@render render_nodes(fragment.nodes, fragment.start_index)}
{:else if fragment.type === 'mark'}
{@const MarkComponent = svedit.session.config.node_components[fragment.node.type]}
{#if MarkComponent}
<MarkComponent path={[...path, 'marks', fragment.mark_index, 'node_id']}>
{@render render_nodes(fragment.nodes, fragment.start_index)}
</MarkComponent>
{:else}
{@render render_nodes(fragment.nodes, fragment.start_index)}
{/if}
{/if}
{/each}
{#if node_ids.length > 0}
<NodeGap array_path={path} offset={node_ids.length} count={node_ids.length} />
{/if}
{#if svedit.editable && NodeGapMarkers}
<NodeGapMarkers {path} count={node_ids.length} />
{/if}
</svelte:element>
<style>
/* position: relative makes this the containing block for the gap's
.svedit-selectable, which fills it via inset:0. You may override to
position: absolute (e.g. so an empty array doesn't occupy flow space)
— still a containing block; then also set position: relative on the
parent node-array container so this placeholder's inset:0 resolves
against it. */
:where(.empty-node-placeholder) {
position: relative;
inset: 0;
min-height: 40px;
min-width: 24px;
cursor: pointer;
}
</style>