bunnyquery 1.8.15 → 1.8.16
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/bunnyquery.js +40 -11
- package/dist/engine.cjs +28 -10
- package/dist/engine.cjs.map +1 -1
- package/dist/engine.d.mts +11 -0
- package/dist/engine.d.ts +11 -0
- package/dist/engine.mjs +28 -10
- package/dist/engine.mjs.map +1 -1
- package/package.json +1 -1
- package/src/engine/scroll_anchor.ts +50 -12
package/package.json
CHANGED
|
@@ -153,6 +153,17 @@ export interface ScrollAnchor {
|
|
|
153
153
|
settleReturn: () => boolean;
|
|
154
154
|
/** A return is armed: its position, not the host's, decides scrollTop. */
|
|
155
155
|
isReturning: () => boolean;
|
|
156
|
+
/**
|
|
157
|
+
* The reader is driving. Retire any armed return, at once.
|
|
158
|
+
*
|
|
159
|
+
* Call from the raw gesture handlers (wheel, touchstart, touchmove, keydown),
|
|
160
|
+
* which fire synchronously on a real user action and never for a programmatic
|
|
161
|
+
* scroll. Waiting for the scroll EVENT is not good enough: a background refresh
|
|
162
|
+
* can settle in between, and a settle with a return still armed puts the reader
|
|
163
|
+
* back where they were before they left, which lands as "I touched the scroll
|
|
164
|
+
* and it threw me somewhere else".
|
|
165
|
+
*/
|
|
166
|
+
release: () => void;
|
|
156
167
|
/** Pin to the bottom, instantly, recording the write. The ONLY way to pin. */
|
|
157
168
|
pinBottom: () => void;
|
|
158
169
|
/** The box is not being painted (hidden tab). Shared so hosts agree. */
|
|
@@ -187,6 +198,9 @@ var ROW_POS_ATTR = 'data-row-pos';
|
|
|
187
198
|
var MAX_ALTS = 2;
|
|
188
199
|
/** How far past the anchor collectAlts will look for them. */
|
|
189
200
|
var ALT_SCAN_LIMIT = 64;
|
|
201
|
+
/** Settles an armed return may act on: the two halves of one head refresh, plus
|
|
202
|
+
* one spare for a refresh that was already in flight when the reader left. */
|
|
203
|
+
var MAX_RETURN_SETTLES = 3;
|
|
190
204
|
/** data-row-pos is present but empty: the row cannot say where it is anchored. */
|
|
191
205
|
var UNKNOWN_ROW_POS = '\u0000?';
|
|
192
206
|
|
|
@@ -313,6 +327,12 @@ export function createScrollAnchor(options: ScrollAnchorOptions): ScrollAnchor {
|
|
|
313
327
|
// head refresh, which is what puts a pinned reader on the bottom that exists
|
|
314
328
|
// after the deferred batch merges rather than the surface page's bottom.
|
|
315
329
|
var returning = false;
|
|
330
|
+
// How many more settles an armed return may act on. A return exists to survive
|
|
331
|
+
// the two halves of one head refresh; beyond that it is a stale instruction
|
|
332
|
+
// lying in wait, and the reader has moved on. Bounded so a blur that never got
|
|
333
|
+
// a matching focus (clicking into devtools, another window, an iframe — all of
|
|
334
|
+
// which fire window blur) cannot arm something that fires minutes later.
|
|
335
|
+
var returnBudget = 0;
|
|
316
336
|
// The last scrollTop THIS module wrote. A scroll event reporting anything else
|
|
317
337
|
// is the reader, and the reader always wins — that is the whole retirement rule
|
|
318
338
|
// for an armed return, and it is why every write below records it.
|
|
@@ -451,9 +471,7 @@ export function createScrollAnchor(options: ScrollAnchorOptions): ScrollAnchor {
|
|
|
451
471
|
// module did not write is the reader, and the reader always wins. Checked
|
|
452
472
|
// before the frozen bail because it is the one thing allowed to end a return.
|
|
453
473
|
var b0 = options.getBox();
|
|
454
|
-
if (returning && b0 && b0.scrollTop !== wroteTop)
|
|
455
|
-
parked = null; parkedStuck = false; returning = false;
|
|
456
|
-
}
|
|
474
|
+
if (returning && b0 && b0.scrollTop !== wroteTop) release();
|
|
457
475
|
// A scroll event while the tab is hidden is not the reader moving.
|
|
458
476
|
if (frozen()) return;
|
|
459
477
|
held = capture();
|
|
@@ -461,11 +479,12 @@ export function createScrollAnchor(options: ScrollAnchorOptions): ScrollAnchor {
|
|
|
461
479
|
|
|
462
480
|
function hold(): void {
|
|
463
481
|
if (frozen()) return;
|
|
464
|
-
// Coming out of
|
|
465
|
-
//
|
|
466
|
-
//
|
|
467
|
-
//
|
|
468
|
-
|
|
482
|
+
// Coming out of a FROZEN stretch, and only that. An armed return is settled
|
|
483
|
+
// by the host, from its focus/visible handler and the refresh settles that
|
|
484
|
+
// follow — never from here. hold() runs on every render, including the many
|
|
485
|
+
// that happen WHILE the reader is still away, and letting those settle the
|
|
486
|
+
// return spent it before they were back to see it.
|
|
487
|
+
if (sawFrozen) { settleReturn(); return; }
|
|
469
488
|
var box = options.getBox();
|
|
470
489
|
if (!box || options.isStuck()) { held = null; return; }
|
|
471
490
|
if (!held) { held = capture(); return; }
|
|
@@ -570,6 +589,15 @@ export function createScrollAnchor(options: ScrollAnchorOptions): ScrollAnchor {
|
|
|
570
589
|
parked = capture();
|
|
571
590
|
parkedStuck = !!options.isStuck();
|
|
572
591
|
returning = true;
|
|
592
|
+
returnBudget = MAX_RETURN_SETTLES;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/** The reader is driving: an armed return is no longer anyone's business. */
|
|
596
|
+
function release(): void {
|
|
597
|
+
parked = null;
|
|
598
|
+
parkedStuck = false;
|
|
599
|
+
returning = false;
|
|
600
|
+
returnBudget = 0;
|
|
573
601
|
}
|
|
574
602
|
|
|
575
603
|
/**
|
|
@@ -605,7 +633,17 @@ export function createScrollAnchor(options: ScrollAnchorOptions): ScrollAnchor {
|
|
|
605
633
|
*/
|
|
606
634
|
function settleReturn(): boolean {
|
|
607
635
|
if (frozen()) return false;
|
|
636
|
+
var wasFrozen = sawFrozen;
|
|
608
637
|
sawFrozen = false;
|
|
638
|
+
// Nothing is armed, so there is nothing to settle. Without this the call was
|
|
639
|
+
// a licence to re-impose `held` UNBOUNDED at any time — which is exactly how
|
|
640
|
+
// a reader who had already taken control got thrown back: they released the
|
|
641
|
+
// return, and the next background settle simply used the live anchor instead.
|
|
642
|
+
if (!returning && !wasFrozen) return false;
|
|
643
|
+
if (returning) {
|
|
644
|
+
if (returnBudget <= 0) { release(); return false; }
|
|
645
|
+
returnBudget--;
|
|
646
|
+
}
|
|
609
647
|
// A reader who left pinned has no row to hold; the bottom is the place. Stay
|
|
610
648
|
// armed so the NEXT settle re-pins after the deferred batch merges, which is
|
|
611
649
|
// the bottom they actually mean.
|
|
@@ -619,7 +657,8 @@ export function createScrollAnchor(options: ScrollAnchorOptions): ScrollAnchor {
|
|
|
619
657
|
// place reachable again. Believing the clamped write was a success is what
|
|
620
658
|
// made the error permanent.
|
|
621
659
|
parked = (!restorePinned || !restoreExact) ? target : null;
|
|
622
|
-
returning = !!parked;
|
|
660
|
+
returning = !!parked && returnBudget > 0;
|
|
661
|
+
if (!returning) { parked = null; parkedStuck = false; returnBudget = 0; }
|
|
623
662
|
return false;
|
|
624
663
|
}
|
|
625
664
|
|
|
@@ -661,9 +700,7 @@ export function createScrollAnchor(options: ScrollAnchorOptions): ScrollAnchor {
|
|
|
661
700
|
|
|
662
701
|
function forget(): void {
|
|
663
702
|
held = null;
|
|
664
|
-
|
|
665
|
-
parkedStuck = false;
|
|
666
|
-
returning = false;
|
|
703
|
+
release();
|
|
667
704
|
}
|
|
668
705
|
|
|
669
706
|
return {
|
|
@@ -675,6 +712,7 @@ export function createScrollAnchor(options: ScrollAnchorOptions): ScrollAnchor {
|
|
|
675
712
|
park: park,
|
|
676
713
|
settleReturn: settleReturn,
|
|
677
714
|
isReturning: isReturning,
|
|
715
|
+
release: release,
|
|
678
716
|
pinBottom: pinBottom,
|
|
679
717
|
isFrozen: frozen,
|
|
680
718
|
thaw: thaw,
|