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
|
@@ -15,11 +15,31 @@
|
|
|
15
15
|
* by viewport visibility. Off-screen NodeGaps remain as zero-size
|
|
16
16
|
* absolute elements with no anchor cost.
|
|
17
17
|
*
|
|
18
|
-
* ##
|
|
18
|
+
* ## Element registration via attachments
|
|
19
19
|
*
|
|
20
|
-
*
|
|
21
|
-
* `
|
|
22
|
-
*
|
|
20
|
+
* Node elements (Node.svelte and the empty-array placeholder) register
|
|
21
|
+
* themselves with `{@attach registry.track_node(path)}`; node-array
|
|
22
|
+
* containers use `{@attach registry.track_array(path)}`. Attachments
|
|
23
|
+
* re-run whenever Svelte recreates the element or its path changes, so
|
|
24
|
+
* observation and per-path state follow the DOM's actual lifecycle —
|
|
25
|
+
* including DOM recreation without a document change (e.g. dev-mode
|
|
26
|
+
* HMR component swaps), which a document-driven reconciliation pass
|
|
27
|
+
* would miss.
|
|
28
|
+
*
|
|
29
|
+
* During keyed updates several elements can change paths at once
|
|
30
|
+
* (e.g. 0→1 while a new element claims 0), and Svelte doesn't
|
|
31
|
+
* guarantee attachment ordering across elements. Per-path ownership
|
|
32
|
+
* maps make unregistration collision-safe: a teardown only clears a
|
|
33
|
+
* path's state if that element still owns the path.
|
|
34
|
+
*
|
|
35
|
+
* ## Two observers
|
|
36
|
+
*
|
|
37
|
+
* **Overscan IO** (`rootMargin: 500px`, threshold `0`) — fills the
|
|
38
|
+
* per-array `array_indices` sets. NodeGap derives its `.positioned`
|
|
39
|
+
* class reactively from them. Registration seeds
|
|
40
|
+
* the state synchronously from one getBoundingClientRect (so there's
|
|
41
|
+
* no unpositioned flash on mount); the IO owns subsequent viewport
|
|
42
|
+
* transitions, including layout-shift-induced ones.
|
|
23
43
|
*
|
|
24
44
|
* **Viewport IO** (`rootMargin: 0`, threshold `[0, 1]`) — toggles
|
|
25
45
|
* view classes (`.in-view`, `.fully-in-view`, …) on node elements.
|
|
@@ -38,13 +58,26 @@
|
|
|
38
58
|
* immediately) and a RAF flush that reads only scrollLeft / scrollWidth /
|
|
39
59
|
* clientWidth on the dirty array.
|
|
40
60
|
*
|
|
61
|
+
* **Document changes** — content changes alter an array's scroll
|
|
62
|
+
* extent without recreating the array element, so after Svelte
|
|
63
|
+
* updates the DOM all registered arrays re-sync their edge state in
|
|
64
|
+
* one batch.
|
|
65
|
+
*
|
|
66
|
+
* **Array ResizeObserver** — box-size changes (window resize, sidebar
|
|
67
|
+
* toggle, responsive breakpoint) can flip an array between fitting
|
|
68
|
+
* and overflowing without a scroll event or document change; a shared
|
|
69
|
+
* ResizeObserver on registered arrays re-syncs their edge state.
|
|
70
|
+
*
|
|
41
71
|
* ## Reactivity
|
|
42
72
|
*
|
|
43
|
-
* State lives in
|
|
44
|
-
*
|
|
45
|
-
*
|
|
73
|
+
* State lives in per-array SvelteSets (array_indices, the single
|
|
74
|
+
* source of truth for node nearness) and a SvelteMap (edge_map).
|
|
75
|
+
* Consumers (NodeGap and NodeGapMarkers, both via
|
|
76
|
+
* $derived) read via .has()/.get() — Svelte's runtime tracks at
|
|
46
77
|
* per-key granularity, so only consumers whose specific dependencies
|
|
47
|
-
* changed re-evaluate.
|
|
78
|
+
* changed re-evaluate. Because the `.positioned` class is applied
|
|
79
|
+
* declaratively by NodeGap, it survives DOM recreation that happens
|
|
80
|
+
* without a document change (e.g. dev-mode HMR component swaps).
|
|
48
81
|
*
|
|
49
82
|
* View classes are toggled imperatively (no Svelte consumers — they're
|
|
50
83
|
* pure CSS hooks for app code).
|
|
@@ -53,12 +86,9 @@
|
|
|
53
86
|
*/
|
|
54
87
|
|
|
55
88
|
import { SvelteMap, SvelteSet } from 'svelte/reactivity';
|
|
56
|
-
import { untrack } from 'svelte';
|
|
89
|
+
import { tick, untrack } from 'svelte';
|
|
57
90
|
import { PATH_SEPARATOR } from './utils.js';
|
|
58
91
|
|
|
59
|
-
const NODE_SELECTOR = '[data-type="node"][data-path]';
|
|
60
|
-
const GAP_SELECTOR = '.node-gap[data-gap-array-path]';
|
|
61
|
-
|
|
62
92
|
/**
|
|
63
93
|
* Overscan margin around the viewport (px). Nodes within this distance
|
|
64
94
|
* of the viewport get anchor positioning activated and gap markers
|
|
@@ -78,17 +108,17 @@ const EDGE_TOLERANCE_PX = 10;
|
|
|
78
108
|
|
|
79
109
|
/**
|
|
80
110
|
* Single source of truth for "which node DOM elements are near or in
|
|
81
|
-
* the viewport". Owns the
|
|
82
|
-
* consumed by NodeGap and NodeGapMarkers.
|
|
111
|
+
* the viewport". Owns the IntersectionObservers and the reactive maps
|
|
112
|
+
* consumed by NodeGap and NodeGapMarkers. Elements register through
|
|
113
|
+
* the track_node / track_array attachments.
|
|
83
114
|
*/
|
|
84
115
|
class VisibilityRegistry {
|
|
85
|
-
/** Per-node "in overscan zone" flag. NodeGap reads via `.has(path)`. */
|
|
86
|
-
near_map = new SvelteMap();
|
|
87
|
-
|
|
88
116
|
/**
|
|
89
|
-
* Per-array set of near child indices
|
|
90
|
-
*
|
|
91
|
-
*
|
|
117
|
+
* Per-array set of near ("in overscan zone") child indices — the
|
|
118
|
+
* single source of truth for node nearness. NodeGap reads via
|
|
119
|
+
* `.has(index)`, NodeGapMarkers iterates the set for its array.
|
|
120
|
+
* Stable references — once a SvelteSet is created for an array path
|
|
121
|
+
* it survives re-renders so consumers stay subscribed.
|
|
92
122
|
*
|
|
93
123
|
* @type {Map<string, SvelteSet<number>>}
|
|
94
124
|
*/
|
|
@@ -98,10 +128,11 @@ class VisibilityRegistry {
|
|
|
98
128
|
* Per-array edge-proximity state. `first` is true when the first
|
|
99
129
|
* node's leading edge is within EDGE_TOLERANCE_PX of the array
|
|
100
130
|
* container's leading edge; `last` is the symmetric trailing-edge
|
|
101
|
-
* check on the last node. Recomputed on scroll
|
|
102
|
-
*
|
|
103
|
-
* threshold crossings and would
|
|
104
|
-
* scroll changes that don't
|
|
131
|
+
* check on the last node. Recomputed on scroll, on array box
|
|
132
|
+
* resize and after document changes because IntersectionObserver
|
|
133
|
+
* only fires on intersection-ratio threshold crossings and would
|
|
134
|
+
* otherwise miss horizontal-overflow scroll changes that don't
|
|
135
|
+
* change the visibility ratio.
|
|
105
136
|
*
|
|
106
137
|
* NodeGap and NodeGapMarkers read this to gate edge gaps so they
|
|
107
138
|
* only render when the edge node has actually reached the matching
|
|
@@ -117,9 +148,43 @@ class VisibilityRegistry {
|
|
|
117
148
|
/** @type {IntersectionObserver | null} */
|
|
118
149
|
#view_io = null;
|
|
119
150
|
|
|
151
|
+
/**
|
|
152
|
+
* Re-syncs edge state when a registered array's box changes size
|
|
153
|
+
* (window resize, sidebar toggle, responsive breakpoint, late-
|
|
154
|
+
* loading content). Size changes can flip an array between fitting
|
|
155
|
+
* and overflowing without firing a scroll event or a document
|
|
156
|
+
* change, so neither of the other two triggers would catch them.
|
|
157
|
+
* Writes are change-detected, so redundant firings are no-ops.
|
|
158
|
+
*
|
|
159
|
+
* @type {ResizeObserver | null}
|
|
160
|
+
*/
|
|
161
|
+
#array_ro = null;
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Registered node elements and the path each was registered under.
|
|
165
|
+
* The attachment closure carries the path, so a stale data-path
|
|
166
|
+
* attribute can never desync state.
|
|
167
|
+
*
|
|
168
|
+
* @type {Map<HTMLElement, string>}
|
|
169
|
+
*/
|
|
170
|
+
#registered_paths = new Map();
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Which element currently owns a node path. Guards unregistration
|
|
174
|
+
* against keyed-update collisions (see module doc).
|
|
175
|
+
*
|
|
176
|
+
* @type {Map<string, HTMLElement>}
|
|
177
|
+
*/
|
|
178
|
+
#path_owners = new Map();
|
|
179
|
+
|
|
120
180
|
/** @type {Set<HTMLElement>} */
|
|
121
|
-
#
|
|
122
|
-
|
|
181
|
+
#near_nodes = new Set();
|
|
182
|
+
|
|
183
|
+
/** Registered node-array container elements. @type {Set<Element>} */
|
|
184
|
+
#array_els = new Set();
|
|
185
|
+
|
|
186
|
+
/** Which element currently owns an array path. @type {Map<string, Element>} */
|
|
187
|
+
#array_owners = new Map();
|
|
123
188
|
|
|
124
189
|
/**
|
|
125
190
|
* When true, a second IO (rootMargin 0, actual viewport) toggles
|
|
@@ -138,14 +203,10 @@ class VisibilityRegistry {
|
|
|
138
203
|
view_classes = true;
|
|
139
204
|
|
|
140
205
|
/**
|
|
141
|
-
* When true, the overscan IO populates
|
|
142
|
-
* `.positioned`
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
* overscan zone, so every gap is `.positioned` and every marker
|
|
146
|
-
* renders. Useful for diagnosing whether a layout bug originates
|
|
147
|
-
* in the IO culling logic vs. the underlying CSS / anchor
|
|
148
|
-
* positioning.
|
|
206
|
+
* When true, the overscan IO populates array_indices and NodeGap gates
|
|
207
|
+
* `.positioned` by viewport proximity. When false, the overscan IO
|
|
208
|
+
* is skipped and every registered node counts as near, so every gap
|
|
209
|
+
* is `.positioned` and every marker renders.
|
|
149
210
|
*
|
|
150
211
|
* Disabling makes anchor positioning O(N) over every node in the
|
|
151
212
|
* document and is intended for debugging only; large documents
|
|
@@ -174,6 +235,11 @@ class VisibilityRegistry {
|
|
|
174
235
|
threshold: [0, 1]
|
|
175
236
|
});
|
|
176
237
|
}
|
|
238
|
+
if (!this.#array_ro) {
|
|
239
|
+
this.#array_ro = new ResizeObserver((entries) => {
|
|
240
|
+
for (const entry of entries) this.sync_edge_state(entry.target);
|
|
241
|
+
});
|
|
242
|
+
}
|
|
177
243
|
}
|
|
178
244
|
|
|
179
245
|
stop() {
|
|
@@ -181,10 +247,13 @@ class VisibilityRegistry {
|
|
|
181
247
|
this.#io = null;
|
|
182
248
|
this.#view_io?.disconnect();
|
|
183
249
|
this.#view_io = null;
|
|
184
|
-
|
|
185
|
-
this.#
|
|
186
|
-
this.#
|
|
187
|
-
this.
|
|
250
|
+
this.#array_ro?.disconnect();
|
|
251
|
+
this.#array_ro = null;
|
|
252
|
+
this.#registered_paths.clear();
|
|
253
|
+
this.#path_owners.clear();
|
|
254
|
+
this.#near_nodes.clear();
|
|
255
|
+
this.#array_els.clear();
|
|
256
|
+
this.#array_owners.clear();
|
|
188
257
|
this.edge_map.clear();
|
|
189
258
|
for (const set of this.#array_indices.values()) set.clear();
|
|
190
259
|
}
|
|
@@ -195,6 +264,13 @@ class VisibilityRegistry {
|
|
|
195
264
|
* subscription — the same set is returned on subsequent calls so
|
|
196
265
|
* Svelte's per-set tracking remains intact across re-evaluations.
|
|
197
266
|
*
|
|
267
|
+
* IMPORTANT: hold the returned set in its own $derived and read
|
|
268
|
+
* membership from a different one (see NodeGap / NodeGapMarkers).
|
|
269
|
+
* Svelte does not track dependencies on state created inside the
|
|
270
|
+
* currently-evaluating reaction, so a consumer that lazily creates
|
|
271
|
+
* the set and reads `.has()` in the same derived never subscribes
|
|
272
|
+
* and never re-evaluates.
|
|
273
|
+
*
|
|
198
274
|
* @param {string} array_path_str
|
|
199
275
|
* @returns {SvelteSet<number>}
|
|
200
276
|
*/
|
|
@@ -207,85 +283,148 @@ class VisibilityRegistry {
|
|
|
207
283
|
return set;
|
|
208
284
|
}
|
|
209
285
|
|
|
210
|
-
/**
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
286
|
+
/**
|
|
287
|
+
* Attachment factory for node elements (Node.svelte and the
|
|
288
|
+
* empty-array placeholder). Registers the element under `path`,
|
|
289
|
+
* seeds its near / view-class state synchronously from one BCR, and
|
|
290
|
+
* hands the element to the IntersectionObservers for subsequent
|
|
291
|
+
* viewport transitions. Re-runs when the element is recreated or
|
|
292
|
+
* its path changes; the teardown unregisters collision-safely.
|
|
293
|
+
*
|
|
294
|
+
* @param {string} path
|
|
295
|
+
*/
|
|
296
|
+
track_node(path) {
|
|
297
|
+
return (/** @type {HTMLElement} */ el) => {
|
|
298
|
+
this.#register_node(el, path);
|
|
299
|
+
return () => this.#unregister_node(el, path);
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Attachment factory for node-array container elements. Registered
|
|
305
|
+
* arrays get their edge state synced on mount, on scroll (via the
|
|
306
|
+
* document scroll listener) and after document changes.
|
|
307
|
+
*
|
|
308
|
+
* @param {string} path
|
|
309
|
+
*/
|
|
310
|
+
track_array(path) {
|
|
311
|
+
return (/** @type {Element} */ el) => {
|
|
312
|
+
this.start();
|
|
313
|
+
this.#array_els.add(el);
|
|
314
|
+
this.#array_owners.set(path, el);
|
|
315
|
+
this.#array_ro?.observe(el);
|
|
316
|
+
// Sync synchronously — the RO's initial callback fires a frame
|
|
317
|
+
// later and would flash edge gaps un-positioned on mount.
|
|
318
|
+
this.sync_edge_state(el);
|
|
319
|
+
return () => {
|
|
320
|
+
this.#array_els.delete(el);
|
|
321
|
+
this.#array_ro?.unobserve(el);
|
|
228
322
|
untrack(() => {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
this.
|
|
232
|
-
any_change = true;
|
|
233
|
-
}
|
|
234
|
-
const split = this.#split_path(path);
|
|
235
|
-
if (split) {
|
|
236
|
-
const set = this.get_array_indices(split.array_path);
|
|
237
|
-
if (!set.has(split.index)) {
|
|
238
|
-
set.add(split.index);
|
|
239
|
-
any_change = true;
|
|
240
|
-
}
|
|
323
|
+
if (this.#array_owners.get(path) === el) {
|
|
324
|
+
this.#array_owners.delete(path);
|
|
325
|
+
this.edge_map.delete(path);
|
|
241
326
|
}
|
|
242
|
-
if (any_change) this.sync_gaps_around_node(el);
|
|
243
327
|
});
|
|
244
|
-
}
|
|
328
|
+
};
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Whether an element is a registered node-array container of this
|
|
334
|
+
* registry instance. Scopes the document-level scroll listener to
|
|
335
|
+
* this editor without any DOM traversal.
|
|
336
|
+
*
|
|
337
|
+
* @param {Element} el
|
|
338
|
+
*/
|
|
339
|
+
has_array(el) {
|
|
340
|
+
return this.#array_els.has(el);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Re-sync edge state for every registered array. Called after
|
|
345
|
+
* document changes: content changes alter scroll extents without
|
|
346
|
+
* recreating the array element, so the attachments can't see them.
|
|
347
|
+
*/
|
|
348
|
+
sync_all_edge_states() {
|
|
349
|
+
for (const el of this.#array_els) {
|
|
350
|
+
if (el.isConnected) this.sync_edge_state(el);
|
|
245
351
|
}
|
|
246
|
-
this.#view_io?.observe(el);
|
|
247
352
|
}
|
|
248
353
|
|
|
249
|
-
/**
|
|
250
|
-
|
|
354
|
+
/**
|
|
355
|
+
* @param {HTMLElement} el
|
|
356
|
+
* @param {string} path
|
|
357
|
+
*/
|
|
358
|
+
#register_node(el, path) {
|
|
359
|
+
this.start();
|
|
360
|
+
untrack(() => {
|
|
361
|
+
this.#registered_paths.set(el, path);
|
|
362
|
+
this.#path_owners.set(path, el);
|
|
363
|
+
this.#io?.observe(el);
|
|
364
|
+
this.#view_io?.observe(el);
|
|
365
|
+
|
|
366
|
+
// Seed state synchronously so freshly mounted (or recreated)
|
|
367
|
+
// elements don't wait a frame for the IO's initial callback —
|
|
368
|
+
// that would flash gaps un-positioned. The IO owns all later
|
|
369
|
+
// transitions.
|
|
370
|
+
const needs_rect = this.visibility_culling || this.view_classes;
|
|
371
|
+
const bcr = needs_rect ? el.getBoundingClientRect() : null;
|
|
372
|
+
const vh = window.innerHeight;
|
|
373
|
+
const vw = window.innerWidth;
|
|
374
|
+
const is_near =
|
|
375
|
+
!this.visibility_culling ||
|
|
376
|
+
(bcr.bottom > -OVERSCAN_PX &&
|
|
377
|
+
bcr.top < vh + OVERSCAN_PX &&
|
|
378
|
+
bcr.right > -OVERSCAN_PX &&
|
|
379
|
+
bcr.left < vw + OVERSCAN_PX);
|
|
380
|
+
if (is_near) this.#add_near_node(el, path);
|
|
381
|
+
|
|
382
|
+
if (this.view_classes) {
|
|
383
|
+
const in_viewport = bcr.bottom > 0 && bcr.top < vh && bcr.right > 0 && bcr.left < vw;
|
|
384
|
+
this.#apply_view_classes(el, in_viewport, bcr, vh);
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* @param {HTMLElement} el
|
|
391
|
+
* @param {string} path
|
|
392
|
+
*/
|
|
393
|
+
#unregister_node(el, path) {
|
|
394
|
+
this.#registered_paths.delete(el);
|
|
395
|
+
this.#near_nodes.delete(el);
|
|
251
396
|
this.#io?.unobserve(el);
|
|
252
397
|
this.#view_io?.unobserve(el);
|
|
253
|
-
|
|
254
|
-
|
|
398
|
+
untrack(() => {
|
|
399
|
+
// Ownership guard: during keyed updates another element may
|
|
400
|
+
// have already claimed this path — its state must survive.
|
|
401
|
+
if (this.#path_owners.get(path) === el) {
|
|
402
|
+
this.#path_owners.delete(path);
|
|
403
|
+
const split = this.#split_path(path);
|
|
404
|
+
if (split) this.#array_indices.get(split.array_path)?.delete(split.index);
|
|
405
|
+
}
|
|
406
|
+
});
|
|
255
407
|
}
|
|
256
408
|
|
|
257
409
|
/**
|
|
258
|
-
*
|
|
259
|
-
*
|
|
260
|
-
* gap context (no Svelte reactivity).
|
|
261
|
-
*
|
|
262
|
-
* @param {Element} gap_el
|
|
410
|
+
* @param {HTMLElement} el
|
|
411
|
+
* @param {string} path
|
|
263
412
|
*/
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
const
|
|
267
|
-
if (
|
|
268
|
-
const offset = parseInt(el.dataset.gapOffset, 10);
|
|
269
|
-
const is_last = el.classList.contains('last');
|
|
270
|
-
const empty = el.classList.contains('empty');
|
|
271
|
-
el.classList.toggle(
|
|
272
|
-
'positioned',
|
|
273
|
-
should_position_gap_imperative(this, arr, offset, is_last, empty)
|
|
274
|
-
);
|
|
413
|
+
#add_near_node(el, path) {
|
|
414
|
+
this.#near_nodes.add(el);
|
|
415
|
+
const split = this.#split_path(path);
|
|
416
|
+
if (split) this.get_array_indices(split.array_path).add(split.index);
|
|
275
417
|
}
|
|
276
418
|
|
|
277
419
|
/**
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
* bootstrap. O(1) DOM sibling traversal.
|
|
281
|
-
*
|
|
282
|
-
* @param {Element} node_el
|
|
420
|
+
* @param {HTMLElement} el
|
|
421
|
+
* @param {string} path
|
|
283
422
|
*/
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
if (
|
|
287
|
-
const
|
|
288
|
-
if (
|
|
423
|
+
#remove_near_node(el, path) {
|
|
424
|
+
this.#near_nodes.delete(el);
|
|
425
|
+
if (this.#path_owners.get(path) !== el) return;
|
|
426
|
+
const split = this.#split_path(path);
|
|
427
|
+
if (split) this.#array_indices.get(split.array_path)?.delete(split.index);
|
|
289
428
|
}
|
|
290
429
|
|
|
291
430
|
/**
|
|
@@ -314,13 +453,11 @@ class VisibilityRegistry {
|
|
|
314
453
|
const path = /** @type {HTMLElement} */ (array_el).dataset.path;
|
|
315
454
|
if (!path) return false;
|
|
316
455
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
const ch = array_el.clientHeight;
|
|
323
|
-
|
|
456
|
+
// Check computed overflow BEFORE touching scroll metrics. Reading
|
|
457
|
+
// scrollLeft/scrollWidth forces a synchronous layout when the DOM is
|
|
458
|
+
// dirty, while getComputedStyle only forces style recalc. Non-clipping
|
|
459
|
+
// arrays — the common case (a plain column body) — resolve to
|
|
460
|
+
// first=last=true without any scroll-metric read.
|
|
324
461
|
const style = getComputedStyle(array_el);
|
|
325
462
|
const clips_x = style.overflowX !== 'visible';
|
|
326
463
|
const clips_y = style.overflowY !== 'visible';
|
|
@@ -329,12 +466,18 @@ class VisibilityRegistry {
|
|
|
329
466
|
// Normal wrapping/grid node arrays often have harmless layout overflow
|
|
330
467
|
// (scrollWidth > clientWidth) while overflow remains visible; treating
|
|
331
468
|
// that as scroll position would incorrectly hide edge gaps.
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
469
|
+
let first = true;
|
|
470
|
+
let last = true;
|
|
471
|
+
if (clips_x) {
|
|
472
|
+
const sl = array_el.scrollLeft;
|
|
473
|
+
first = sl <= EDGE_TOLERANCE_PX;
|
|
474
|
+
last = sl + array_el.clientWidth >= array_el.scrollWidth - EDGE_TOLERANCE_PX;
|
|
475
|
+
}
|
|
476
|
+
if (clips_y) {
|
|
477
|
+
const st = array_el.scrollTop;
|
|
478
|
+
first = first && st <= EDGE_TOLERANCE_PX;
|
|
479
|
+
last = last && st + array_el.clientHeight >= array_el.scrollHeight - EDGE_TOLERANCE_PX;
|
|
480
|
+
}
|
|
338
481
|
|
|
339
482
|
return untrack(() => {
|
|
340
483
|
const prev = this.edge_map.get(path);
|
|
@@ -344,81 +487,6 @@ class VisibilityRegistry {
|
|
|
344
487
|
});
|
|
345
488
|
}
|
|
346
489
|
|
|
347
|
-
/**
|
|
348
|
-
* Sync edge state and toggle .positioned on first/last gaps. Early-
|
|
349
|
-
* exits when edge_map is unchanged — for the structural-mutation
|
|
350
|
-
* path where the edge gap may need re-evaluation regardless of
|
|
351
|
-
* edge_map, see resync_array_edge_gaps below.
|
|
352
|
-
*
|
|
353
|
-
* @param {Element} array_el
|
|
354
|
-
*/
|
|
355
|
-
sync_array_edge_gaps(array_el) {
|
|
356
|
-
if (!this.sync_edge_state(array_el)) return;
|
|
357
|
-
this.#sync_first_last_gaps(array_el);
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
/**
|
|
361
|
-
* Force-resync first/last gaps after a structural mutation. The
|
|
362
|
-
* trailing gap is a reused Svelte component whose `.positioned`
|
|
363
|
-
* state may need re-deriving even when edge_map didn't change.
|
|
364
|
-
*
|
|
365
|
-
* @param {Element} array_el
|
|
366
|
-
*/
|
|
367
|
-
resync_array_edge_gaps(array_el) {
|
|
368
|
-
this.sync_edge_state(array_el);
|
|
369
|
-
this.#sync_first_last_gaps(array_el);
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
/** @param {Element} array_el */
|
|
373
|
-
#sync_first_last_gaps(array_el) {
|
|
374
|
-
const first_gap = array_el.querySelector(':scope > .node-gap.gap-before:not(.empty)');
|
|
375
|
-
const last_gap = array_el.querySelector(':scope > .node-gap.gap-after.last');
|
|
376
|
-
if (first_gap) this.sync_gap_class(first_gap);
|
|
377
|
-
if (last_gap) this.sync_gap_class(last_gap);
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
/**
|
|
381
|
-
* Queue a node for deferred view-class sync. Unobserves from the
|
|
382
|
-
* viewport IO immediately to prevent its initial callback from
|
|
383
|
-
* racing with the RAF, then re-observes in #flush_view_sync after
|
|
384
|
-
* classes are applied via BCR. Multiple MO-triggered insertions
|
|
385
|
-
* coalesce into a single RAF / layout pass.
|
|
386
|
-
*
|
|
387
|
-
* @param {HTMLElement} el
|
|
388
|
-
*/
|
|
389
|
-
schedule_view_sync(el) {
|
|
390
|
-
if (!this.view_classes) return;
|
|
391
|
-
this.#view_io?.unobserve(el);
|
|
392
|
-
this.#pending_view_sync.add(el);
|
|
393
|
-
if (!this.#pending_view_raf) {
|
|
394
|
-
this.#pending_view_raf = requestAnimationFrame(() => this.#flush_view_sync());
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
#flush_view_sync() {
|
|
399
|
-
this.#pending_view_raf = 0;
|
|
400
|
-
const vh = window.innerHeight;
|
|
401
|
-
const vw = window.innerWidth;
|
|
402
|
-
// Read all BCRs first, then write all classes. A read-write-read
|
|
403
|
-
// interleave (the natural single-pass shape) forces a layout
|
|
404
|
-
// flush on every iteration when the toggled classes affect
|
|
405
|
-
// layout — which user apps are free to do via `.in-view` /
|
|
406
|
-
// `.fully-in-view` / etc. Two passes batch the layout reads into
|
|
407
|
-
// one flush regardless of what the consumer wires up.
|
|
408
|
-
/** @type {Array<{el: HTMLElement, bcr: DOMRectReadOnly}>} */
|
|
409
|
-
const items = [];
|
|
410
|
-
for (const el of this.#pending_view_sync) {
|
|
411
|
-
if (el.isConnected) items.push({ el, bcr: el.getBoundingClientRect() });
|
|
412
|
-
}
|
|
413
|
-
for (const { el, bcr } of items) {
|
|
414
|
-
const in_viewport =
|
|
415
|
-
bcr.bottom > 0 && bcr.top < vh && bcr.right > 0 && bcr.left < vw;
|
|
416
|
-
this.#apply_view_classes(el, in_viewport, bcr, vh);
|
|
417
|
-
this.#view_io?.observe(el);
|
|
418
|
-
}
|
|
419
|
-
this.#pending_view_sync.clear();
|
|
420
|
-
}
|
|
421
|
-
|
|
422
490
|
/**
|
|
423
491
|
* @param {HTMLElement} el
|
|
424
492
|
* @param {boolean} in_viewport
|
|
@@ -440,18 +508,6 @@ class VisibilityRegistry {
|
|
|
440
508
|
}
|
|
441
509
|
}
|
|
442
510
|
|
|
443
|
-
/** @param {string} path */
|
|
444
|
-
#forget(path) {
|
|
445
|
-
this.near_map.delete(path);
|
|
446
|
-
const split = this.#split_path(path);
|
|
447
|
-
if (split) {
|
|
448
|
-
const set = this.#array_indices.get(split.array_path);
|
|
449
|
-
set?.delete(split.index);
|
|
450
|
-
// Keep the SvelteSet alive — its NodeGapMarkers consumer may
|
|
451
|
-
// still be mounted. Recreating would orphan the subscription.
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
|
|
455
511
|
/**
|
|
456
512
|
* @param {string} path
|
|
457
513
|
* @returns {{ array_path: string, index: number } | null}
|
|
@@ -465,39 +521,21 @@ class VisibilityRegistry {
|
|
|
465
521
|
}
|
|
466
522
|
|
|
467
523
|
/**
|
|
468
|
-
* Overscan IO callback. Manages
|
|
469
|
-
*
|
|
524
|
+
* Overscan IO callback. Manages the array_indices sets; NodeGap
|
|
525
|
+
* reacts to the map writes and updates .positioned declaratively.
|
|
470
526
|
*
|
|
471
527
|
* @param {IntersectionObserverEntry[]} entries
|
|
472
528
|
*/
|
|
473
529
|
#process_near(entries) {
|
|
474
530
|
for (const entry of entries) {
|
|
475
531
|
const el = /** @type {HTMLElement} */ (entry.target);
|
|
476
|
-
const path = el
|
|
477
|
-
if (!path) continue;
|
|
532
|
+
const path = this.#registered_paths.get(el);
|
|
533
|
+
if (!path || !el.isConnected) continue;
|
|
478
534
|
|
|
479
535
|
const is_near = entry.isIntersecting;
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
if (is_near) {
|
|
484
|
-
if (!this.near_map.has(path)) {
|
|
485
|
-
this.near_map.set(path, true);
|
|
486
|
-
any_change = true;
|
|
487
|
-
}
|
|
488
|
-
} else if (this.near_map.has(path)) {
|
|
489
|
-
this.near_map.delete(path);
|
|
490
|
-
any_change = true;
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
const split = this.#split_path(path);
|
|
494
|
-
if (split) {
|
|
495
|
-
const set = this.get_array_indices(split.array_path);
|
|
496
|
-
if (is_near) set.add(split.index);
|
|
497
|
-
else set.delete(split.index);
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
if (any_change) this.sync_gaps_around_node(el);
|
|
536
|
+
if (is_near === this.#near_nodes.has(el)) continue;
|
|
537
|
+
if (is_near) this.#add_near_node(el, path);
|
|
538
|
+
else this.#remove_near_node(el, path);
|
|
501
539
|
}
|
|
502
540
|
}
|
|
503
541
|
|
|
@@ -512,25 +550,21 @@ class VisibilityRegistry {
|
|
|
512
550
|
const vh = window.innerHeight;
|
|
513
551
|
for (const entry of entries) {
|
|
514
552
|
const el = /** @type {HTMLElement} */ (entry.target);
|
|
515
|
-
if (!el.
|
|
553
|
+
if (!this.#registered_paths.has(el) || !el.isConnected) continue;
|
|
516
554
|
this.#apply_view_classes(el, entry.isIntersecting, entry.boundingClientRect, vh);
|
|
517
555
|
}
|
|
518
556
|
}
|
|
519
|
-
|
|
520
|
-
// #clip_bits and clip_map were removed: their only consumer was the
|
|
521
|
-
// edge-gap visibility check, and they went stale on nested horizontal
|
|
522
|
-
// scroll (IO doesn't re-fire when the intersection ratio doesn't
|
|
523
|
-
// cross a threshold). Replaced by edge_map, derived from the array's
|
|
524
|
-
// own scrollLeft / clientWidth / scrollWidth — one element, six
|
|
525
|
-
// property reads per dirty array per RAF, no per-node BCRs.
|
|
526
557
|
}
|
|
527
558
|
|
|
528
559
|
/**
|
|
529
|
-
* Install the visibility registry on the svedit context
|
|
530
|
-
* the IntersectionObserver + MutationObserver lifecycle to editable
|
|
531
|
-
* state.
|
|
560
|
+
* Install the visibility registry on the svedit context.
|
|
532
561
|
*
|
|
533
|
-
*
|
|
562
|
+
* Elements register themselves via the track_node / track_array
|
|
563
|
+
* attachments, so registration follows the DOM's actual lifecycle.
|
|
564
|
+
* The IntersectionObservers own viewport changes; document changes
|
|
565
|
+
* only trigger an edge-state re-sync over the registered arrays.
|
|
566
|
+
*
|
|
567
|
+
* @param {object} svedit - Svedit context (session, editable)
|
|
534
568
|
*/
|
|
535
569
|
export function create_node_visibility(svedit) {
|
|
536
570
|
const registry = new VisibilityRegistry();
|
|
@@ -540,35 +574,7 @@ export function create_node_visibility(svedit) {
|
|
|
540
574
|
|
|
541
575
|
$effect(() => {
|
|
542
576
|
if (typeof window === 'undefined') return;
|
|
543
|
-
|
|
544
|
-
// Empty node-array placeholders only exist in edit mode, so re-run
|
|
545
|
-
// the bootstrap when editability changes to observe them reliably.
|
|
546
|
-
svedit.editable;
|
|
547
|
-
|
|
548
577
|
registry.start();
|
|
549
|
-
// Observe every existing node. NO getBoundingClientRect — it
|
|
550
|
-
// would force layout on each call, and at 2000+ nodes the
|
|
551
|
-
// bootstrap interleaves with Svelte's mount work, retriggering
|
|
552
|
-
// layout for the growing DOM (~1.5 s of forced reflow when
|
|
553
|
-
// traced). IO populates near_map / array_indices on its first
|
|
554
|
-
// callback, ~1 frame after observe(). The lag is invisible:
|
|
555
|
-
// un-positioned NodeGaps have zero layout presence.
|
|
556
|
-
for (const el of document.querySelectorAll(NODE_SELECTOR)) {
|
|
557
|
-
registry.observe(/** @type {HTMLElement} */ (el));
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
// Bootstrap edge_map for arrays already in the DOM. One-time
|
|
561
|
-
// scrollLeft/clientWidth pass — cheap (one element, six property
|
|
562
|
-
// reads each) and never repeated outside this mount.
|
|
563
|
-
for (const arr of document.querySelectorAll('[data-type="node_array"]')) {
|
|
564
|
-
registry.sync_array_edge_gaps(arr);
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
const canvas =
|
|
568
|
-
untrack(() => svedit.canvas_el) || document.querySelector('.svedit-canvas');
|
|
569
|
-
/** @type {MutationObserver | null} */
|
|
570
|
-
let mo = null;
|
|
571
|
-
let raf = 0;
|
|
572
578
|
|
|
573
579
|
/**
|
|
574
580
|
* Scroll listener for nested scroll containers. IntersectionObserver
|
|
@@ -588,7 +594,7 @@ export function create_node_visibility(svedit) {
|
|
|
588
594
|
function flush_scroll_sync() {
|
|
589
595
|
scroll_raf = 0;
|
|
590
596
|
for (const arr of pending_scroll_arrays) {
|
|
591
|
-
if (arr.isConnected) registry.
|
|
597
|
+
if (arr.isConnected) registry.sync_edge_state(arr);
|
|
592
598
|
}
|
|
593
599
|
pending_scroll_arrays.clear();
|
|
594
600
|
}
|
|
@@ -596,123 +602,81 @@ export function create_node_visibility(svedit) {
|
|
|
596
602
|
function on_scroll(event) {
|
|
597
603
|
const target = /** @type {Element | null} */ (event.target);
|
|
598
604
|
const arr = target?.closest?.('[data-type="node_array"]');
|
|
599
|
-
if (!arr) return;
|
|
605
|
+
if (!arr || !registry.has_array(arr)) return;
|
|
600
606
|
pending_scroll_arrays.add(arr);
|
|
601
607
|
if (!scroll_raf) scroll_raf = requestAnimationFrame(flush_scroll_sync);
|
|
602
608
|
}
|
|
603
609
|
document.addEventListener('scroll', on_scroll, { capture: true, passive: true });
|
|
604
610
|
|
|
605
|
-
if (canvas) {
|
|
606
|
-
mo = new MutationObserver((mutations) => {
|
|
607
|
-
/** @type {Set<Element>} */
|
|
608
|
-
// eslint-disable-next-line svelte/prefer-svelte-reactivity -- Non-reactive per-mutation queue.
|
|
609
|
-
const dirty_arrays = new Set();
|
|
610
|
-
for (const m of mutations) {
|
|
611
|
-
for (const added of m.addedNodes) {
|
|
612
|
-
if (added.nodeType !== Node.ELEMENT_NODE) continue;
|
|
613
|
-
const el = /** @type {HTMLElement} */ (added);
|
|
614
|
-
if (el.matches(NODE_SELECTOR)) {
|
|
615
|
-
registry.observe(el);
|
|
616
|
-
registry.schedule_view_sync(el);
|
|
617
|
-
const arr = el.closest('[data-type="node_array"]');
|
|
618
|
-
if (arr) dirty_arrays.add(arr);
|
|
619
|
-
}
|
|
620
|
-
// Newly-mounted gaps need immediate .positioned sync —
|
|
621
|
-
// the IO won't fire for the EXISTING adjacent node
|
|
622
|
-
// (its intersection didn't change), so the gap would
|
|
623
|
-
// stay un-positioned until the next scroll.
|
|
624
|
-
if (el.matches(GAP_SELECTOR)) {
|
|
625
|
-
registry.sync_gap_class(el);
|
|
626
|
-
}
|
|
627
|
-
for (const child of el.querySelectorAll(NODE_SELECTOR)) {
|
|
628
|
-
const node_el = /** @type {HTMLElement} */ (child);
|
|
629
|
-
registry.observe(node_el);
|
|
630
|
-
registry.schedule_view_sync(node_el);
|
|
631
|
-
const arr = node_el.closest('[data-type="node_array"]');
|
|
632
|
-
if (arr) dirty_arrays.add(arr);
|
|
633
|
-
}
|
|
634
|
-
for (const child of el.querySelectorAll(GAP_SELECTOR)) {
|
|
635
|
-
registry.sync_gap_class(child);
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
for (const removed of m.removedNodes) {
|
|
639
|
-
if (removed.nodeType !== Node.ELEMENT_NODE) continue;
|
|
640
|
-
const el = /** @type {HTMLElement} */ (removed);
|
|
641
|
-
if (el.matches(NODE_SELECTOR)) registry.unobserve(el);
|
|
642
|
-
for (const child of el.querySelectorAll(NODE_SELECTOR)) {
|
|
643
|
-
registry.unobserve(/** @type {HTMLElement} */ (child));
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
for (const arr of dirty_arrays) registry.resync_array_edge_gaps(arr);
|
|
648
|
-
});
|
|
649
|
-
// Defer one frame to skip the initial Svelte mount burst,
|
|
650
|
-
// which the bootstrap above already covered.
|
|
651
|
-
raf = requestAnimationFrame(() => {
|
|
652
|
-
mo.observe(canvas, { childList: true, subtree: true });
|
|
653
|
-
});
|
|
654
|
-
}
|
|
655
|
-
|
|
656
611
|
return () => {
|
|
657
612
|
document.removeEventListener('scroll', on_scroll, true);
|
|
658
|
-
cancelAnimationFrame(raf);
|
|
659
613
|
cancelAnimationFrame(scroll_raf);
|
|
660
614
|
pending_scroll_arrays.clear();
|
|
661
|
-
mo?.disconnect();
|
|
662
615
|
registry.stop();
|
|
663
616
|
};
|
|
664
617
|
});
|
|
618
|
+
|
|
619
|
+
$effect(() => {
|
|
620
|
+
if (typeof window === 'undefined') return;
|
|
621
|
+
|
|
622
|
+
// Structural inputs whose changes can alter array scroll extents
|
|
623
|
+
// without recreating array elements. Selection intentionally does
|
|
624
|
+
// not participate; NodeGapMarkers tracks it directly for caret state.
|
|
625
|
+
svedit.session.doc;
|
|
626
|
+
svedit.editable;
|
|
627
|
+
|
|
628
|
+
let cancelled = false;
|
|
629
|
+
tick().then(() => {
|
|
630
|
+
if (!cancelled) registry.sync_all_edge_states();
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
return () => {
|
|
634
|
+
cancelled = true;
|
|
635
|
+
};
|
|
636
|
+
});
|
|
665
637
|
}
|
|
666
638
|
|
|
667
639
|
/**
|
|
668
|
-
*
|
|
669
|
-
*
|
|
670
|
-
*
|
|
671
|
-
*
|
|
672
|
-
*
|
|
673
|
-
*
|
|
674
|
-
* don't need `count` here.
|
|
640
|
+
* Pure visibility check for a gap's `.positioned` class. NodeGap calls
|
|
641
|
+
* this from a $derived, so the `near.has()` reads are tracked at
|
|
642
|
+
* per-key granularity — a gap re-evaluates only when its own
|
|
643
|
+
* dependencies change. Applying the class declaratively means it
|
|
644
|
+
* survives DOM recreation without a document change (e.g. dev-mode
|
|
645
|
+
* HMR component swaps).
|
|
675
646
|
*
|
|
676
|
-
*
|
|
677
|
-
*
|
|
647
|
+
* `near` must come from `get_array_indices` held in a SEPARATE
|
|
648
|
+
* $derived (see the note there): membership reads on a set created
|
|
649
|
+
* within the same derived are not tracked by Svelte.
|
|
650
|
+
*
|
|
651
|
+
* @param {SvelteSet<number>} near - near child indices of the array
|
|
652
|
+
* @param {{ first: boolean, last: boolean } | undefined} edge_state
|
|
678
653
|
* @param {number} offset
|
|
679
654
|
* @param {boolean} is_last
|
|
680
655
|
* @param {boolean} empty
|
|
681
656
|
*/
|
|
682
|
-
function
|
|
683
|
-
if (!
|
|
684
|
-
|
|
685
|
-
// untrack() is critical: this function is called transitively from
|
|
686
|
-
// the bootstrap $effect (via sync_array_edge_gaps → sync_gap_class),
|
|
687
|
-
// and the reactive-Map `.has()` / `.get()` reads here would
|
|
688
|
-
// otherwise subscribe that effect to the same maps the bootstrap
|
|
689
|
-
// then writes to — infinite re-run cycle that empties near_map
|
|
690
|
-
// every time the IO tries to populate it.
|
|
691
|
-
return untrack(() => {
|
|
692
|
-
if (empty) {
|
|
693
|
-
return registry.near_map.has(`${array_path_str}${PATH_SEPARATOR}0`);
|
|
694
|
-
}
|
|
657
|
+
export function should_position_gap(near, edge_state, offset, is_last, empty) {
|
|
658
|
+
if (!near) return false;
|
|
695
659
|
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
// (within EDGE_TOLERANCE_PX). We can't rely on IO clip_map here
|
|
700
|
-
// because IO only fires on intersection-ratio threshold
|
|
701
|
-
// crossings and misses scroll changes within a horizontal-
|
|
702
|
-
// overflow container that don't change the visibility ratio.
|
|
703
|
-
if (offset === 0) {
|
|
704
|
-
if (!registry.near_map.has(`${array_path_str}${PATH_SEPARATOR}0`)) return false;
|
|
705
|
-
return registry.edge_map.get(array_path_str)?.first === true;
|
|
706
|
-
}
|
|
660
|
+
if (empty) {
|
|
661
|
+
return near.has(0);
|
|
662
|
+
}
|
|
707
663
|
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
664
|
+
// Edge gaps gate on nearness AND edge state. edge_state is the
|
|
665
|
+
// scroll-aware check that the adjacent edge node has actually
|
|
666
|
+
// reached the matching end of its array container (within
|
|
667
|
+
// EDGE_TOLERANCE_PX). We can't rely on the IO here because it only
|
|
668
|
+
// fires on intersection-ratio threshold crossings and misses scroll
|
|
669
|
+
// changes within a horizontal-overflow container that don't change
|
|
670
|
+
// the visibility ratio.
|
|
671
|
+
if (offset === 0) {
|
|
672
|
+
if (!near.has(0)) return false;
|
|
673
|
+
return edge_state?.first === true;
|
|
674
|
+
}
|
|
712
675
|
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
676
|
+
if (is_last) {
|
|
677
|
+
if (!near.has(offset - 1)) return false;
|
|
678
|
+
return edge_state?.last === true;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
return near.has(offset - 1) && near.has(offset);
|
|
718
682
|
}
|