bunnyquery 1.8.14 → 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 +251 -26
- package/dist/engine.cjs +189 -12
- package/dist/engine.cjs.map +1 -1
- package/dist/engine.d.mts +73 -0
- package/dist/engine.d.ts +73 -0
- package/dist/engine.mjs +189 -12
- package/dist/engine.mjs.map +1 -1
- package/package.json +1 -1
- package/src/engine/host.ts +14 -0
- package/src/engine/scroll_anchor.ts +366 -11
- package/src/engine/session.ts +11 -1
package/dist/engine.d.mts
CHANGED
|
@@ -1618,6 +1618,20 @@ interface ChatHost {
|
|
|
1618
1618
|
* that cannot scroll has no way to reach page 2 (see viewport_fill). Only
|
|
1619
1619
|
* the view can measure that, which is why the engine merely announces it. */
|
|
1620
1620
|
onHistoryLoaded?(fetchMore: boolean, token: number): void;
|
|
1621
|
+
/**
|
|
1622
|
+
* A list refresh just changed heights: put the reader back where they were.
|
|
1623
|
+
*
|
|
1624
|
+
* Called at BOTH moments a first-page refresh moves things — the surface page
|
|
1625
|
+
* landing, and the deferred background-indexing batch merging on top of it a
|
|
1626
|
+
* round trip later — because leaving a wrong position on screen between the two
|
|
1627
|
+
* is what reads as "the scroll jumped, then travelled somewhere else".
|
|
1628
|
+
*
|
|
1629
|
+
* The view owns the decision (it is the only side that can measure): pinned to
|
|
1630
|
+
* the bottom means the bottom AFTER the batch merged, anywhere else means the
|
|
1631
|
+
* exact line the reader was on. Falls back to scrollToBottomIfSticky when a host
|
|
1632
|
+
* does not implement it, which is the old behaviour.
|
|
1633
|
+
*/
|
|
1634
|
+
settleScroll?(): void;
|
|
1621
1635
|
cancelRequest(opts: {
|
|
1622
1636
|
url: string;
|
|
1623
1637
|
method: string;
|
|
@@ -1874,6 +1888,23 @@ interface RowAnchor {
|
|
|
1874
1888
|
* key. Never trusted without re-checking that it is still in the box.
|
|
1875
1889
|
*/
|
|
1876
1890
|
el: AnchorRowEl | null;
|
|
1891
|
+
/**
|
|
1892
|
+
* The next few anchorable rows below the primary, each with its own offset.
|
|
1893
|
+
*
|
|
1894
|
+
* The primary row does not always survive: a refresh can drop it, a collapsed
|
|
1895
|
+
* indexing row can be re-identified, an expanded group can fold. Without a
|
|
1896
|
+
* fallback the only thing left is lost(), which guesses from the list's total
|
|
1897
|
+
* growth — and total growth includes everything added BELOW the reader, so a
|
|
1898
|
+
* merge that lands rows on both sides of them over-pays. A second row that is
|
|
1899
|
+
* still there beats any guess, and collecting them costs nothing: capture is
|
|
1900
|
+
* already walking these rows.
|
|
1901
|
+
*/
|
|
1902
|
+
alts?: Array<{
|
|
1903
|
+
key: string;
|
|
1904
|
+
top: number;
|
|
1905
|
+
pos: string | null;
|
|
1906
|
+
el: AnchorRowEl;
|
|
1907
|
+
}>;
|
|
1877
1908
|
}
|
|
1878
1909
|
interface ScrollAnchorOptions {
|
|
1879
1910
|
/** The scrolling message box, or null when it is not mounted. */
|
|
@@ -1883,6 +1914,21 @@ interface ScrollAnchorOptions {
|
|
|
1883
1914
|
* scrollToBottom* paths own the position, so every method here no-ops.
|
|
1884
1915
|
*/
|
|
1885
1916
|
isStuck: () => boolean;
|
|
1917
|
+
/**
|
|
1918
|
+
* The reader cannot see this box right now (the tab is hidden), so FREEZE:
|
|
1919
|
+
* remember where they were and refuse to move them.
|
|
1920
|
+
*
|
|
1921
|
+
* A hidden tab still runs everything that mutates the list — a resumed poll, a
|
|
1922
|
+
* head refresh and its deferred background batch, a settling request — and each
|
|
1923
|
+
* of those would otherwise write scrollTop against a layout nobody is looking at
|
|
1924
|
+
* and re-stamp the remembered position on the way through. The reader then comes
|
|
1925
|
+
* back to wherever the last of those writes happened to land, which is the
|
|
1926
|
+
* "somehow placed in the middle" they see, and only the NEXT correction puts
|
|
1927
|
+
* them right. So while frozen, reads still happen but nothing writes and nothing
|
|
1928
|
+
* re-stamps: the anchor holds the last position the reader actually had, and one
|
|
1929
|
+
* hold() on return puts them back on it.
|
|
1930
|
+
*/
|
|
1931
|
+
isFrozen?: () => boolean;
|
|
1886
1932
|
/**
|
|
1887
1933
|
* Fall back to the raw scrollTop when the anchored row cannot be found again.
|
|
1888
1934
|
*
|
|
@@ -1905,6 +1951,33 @@ interface ScrollAnchor {
|
|
|
1905
1951
|
remember: () => void;
|
|
1906
1952
|
/** Put the remembered place back, if it is still the reader's own. */
|
|
1907
1953
|
hold: () => void;
|
|
1954
|
+
/** The reader is going away: park the exact place they are leaving. */
|
|
1955
|
+
park: () => void;
|
|
1956
|
+
/**
|
|
1957
|
+
* They are back. Puts them on the parked place, and STAYS ARMED until it has
|
|
1958
|
+
* actually landed. Returns true when the host must pin to the bottom instead
|
|
1959
|
+
* (the reader left pinned), which only the host can do meaningfully.
|
|
1960
|
+
*/
|
|
1961
|
+
settleReturn: () => boolean;
|
|
1962
|
+
/** A return is armed: its position, not the host's, decides scrollTop. */
|
|
1963
|
+
isReturning: () => boolean;
|
|
1964
|
+
/**
|
|
1965
|
+
* The reader is driving. Retire any armed return, at once.
|
|
1966
|
+
*
|
|
1967
|
+
* Call from the raw gesture handlers (wheel, touchstart, touchmove, keydown),
|
|
1968
|
+
* which fire synchronously on a real user action and never for a programmatic
|
|
1969
|
+
* scroll. Waiting for the scroll EVENT is not good enough: a background refresh
|
|
1970
|
+
* can settle in between, and a settle with a return still armed puts the reader
|
|
1971
|
+
* back where they were before they left, which lands as "I touched the scroll
|
|
1972
|
+
* and it threw me somewhere else".
|
|
1973
|
+
*/
|
|
1974
|
+
release: () => void;
|
|
1975
|
+
/** Pin to the bottom, instantly, recording the write. The ONLY way to pin. */
|
|
1976
|
+
pinBottom: () => void;
|
|
1977
|
+
/** The box is not being painted (hidden tab). Shared so hosts agree. */
|
|
1978
|
+
isFrozen: () => boolean;
|
|
1979
|
+
/** Deprecated alias of settleReturn, kept so a stale dist does not break. */
|
|
1980
|
+
thaw: () => void;
|
|
1908
1981
|
/** Absorb one element's own resize. See below. */
|
|
1909
1982
|
absorb: (el: AnchorGrowableEl | null | undefined) => void;
|
|
1910
1983
|
/** Drop the remembered place (chat switch, unmount). */
|
package/dist/engine.d.ts
CHANGED
|
@@ -1618,6 +1618,20 @@ interface ChatHost {
|
|
|
1618
1618
|
* that cannot scroll has no way to reach page 2 (see viewport_fill). Only
|
|
1619
1619
|
* the view can measure that, which is why the engine merely announces it. */
|
|
1620
1620
|
onHistoryLoaded?(fetchMore: boolean, token: number): void;
|
|
1621
|
+
/**
|
|
1622
|
+
* A list refresh just changed heights: put the reader back where they were.
|
|
1623
|
+
*
|
|
1624
|
+
* Called at BOTH moments a first-page refresh moves things — the surface page
|
|
1625
|
+
* landing, and the deferred background-indexing batch merging on top of it a
|
|
1626
|
+
* round trip later — because leaving a wrong position on screen between the two
|
|
1627
|
+
* is what reads as "the scroll jumped, then travelled somewhere else".
|
|
1628
|
+
*
|
|
1629
|
+
* The view owns the decision (it is the only side that can measure): pinned to
|
|
1630
|
+
* the bottom means the bottom AFTER the batch merged, anywhere else means the
|
|
1631
|
+
* exact line the reader was on. Falls back to scrollToBottomIfSticky when a host
|
|
1632
|
+
* does not implement it, which is the old behaviour.
|
|
1633
|
+
*/
|
|
1634
|
+
settleScroll?(): void;
|
|
1621
1635
|
cancelRequest(opts: {
|
|
1622
1636
|
url: string;
|
|
1623
1637
|
method: string;
|
|
@@ -1874,6 +1888,23 @@ interface RowAnchor {
|
|
|
1874
1888
|
* key. Never trusted without re-checking that it is still in the box.
|
|
1875
1889
|
*/
|
|
1876
1890
|
el: AnchorRowEl | null;
|
|
1891
|
+
/**
|
|
1892
|
+
* The next few anchorable rows below the primary, each with its own offset.
|
|
1893
|
+
*
|
|
1894
|
+
* The primary row does not always survive: a refresh can drop it, a collapsed
|
|
1895
|
+
* indexing row can be re-identified, an expanded group can fold. Without a
|
|
1896
|
+
* fallback the only thing left is lost(), which guesses from the list's total
|
|
1897
|
+
* growth — and total growth includes everything added BELOW the reader, so a
|
|
1898
|
+
* merge that lands rows on both sides of them over-pays. A second row that is
|
|
1899
|
+
* still there beats any guess, and collecting them costs nothing: capture is
|
|
1900
|
+
* already walking these rows.
|
|
1901
|
+
*/
|
|
1902
|
+
alts?: Array<{
|
|
1903
|
+
key: string;
|
|
1904
|
+
top: number;
|
|
1905
|
+
pos: string | null;
|
|
1906
|
+
el: AnchorRowEl;
|
|
1907
|
+
}>;
|
|
1877
1908
|
}
|
|
1878
1909
|
interface ScrollAnchorOptions {
|
|
1879
1910
|
/** The scrolling message box, or null when it is not mounted. */
|
|
@@ -1883,6 +1914,21 @@ interface ScrollAnchorOptions {
|
|
|
1883
1914
|
* scrollToBottom* paths own the position, so every method here no-ops.
|
|
1884
1915
|
*/
|
|
1885
1916
|
isStuck: () => boolean;
|
|
1917
|
+
/**
|
|
1918
|
+
* The reader cannot see this box right now (the tab is hidden), so FREEZE:
|
|
1919
|
+
* remember where they were and refuse to move them.
|
|
1920
|
+
*
|
|
1921
|
+
* A hidden tab still runs everything that mutates the list — a resumed poll, a
|
|
1922
|
+
* head refresh and its deferred background batch, a settling request — and each
|
|
1923
|
+
* of those would otherwise write scrollTop against a layout nobody is looking at
|
|
1924
|
+
* and re-stamp the remembered position on the way through. The reader then comes
|
|
1925
|
+
* back to wherever the last of those writes happened to land, which is the
|
|
1926
|
+
* "somehow placed in the middle" they see, and only the NEXT correction puts
|
|
1927
|
+
* them right. So while frozen, reads still happen but nothing writes and nothing
|
|
1928
|
+
* re-stamps: the anchor holds the last position the reader actually had, and one
|
|
1929
|
+
* hold() on return puts them back on it.
|
|
1930
|
+
*/
|
|
1931
|
+
isFrozen?: () => boolean;
|
|
1886
1932
|
/**
|
|
1887
1933
|
* Fall back to the raw scrollTop when the anchored row cannot be found again.
|
|
1888
1934
|
*
|
|
@@ -1905,6 +1951,33 @@ interface ScrollAnchor {
|
|
|
1905
1951
|
remember: () => void;
|
|
1906
1952
|
/** Put the remembered place back, if it is still the reader's own. */
|
|
1907
1953
|
hold: () => void;
|
|
1954
|
+
/** The reader is going away: park the exact place they are leaving. */
|
|
1955
|
+
park: () => void;
|
|
1956
|
+
/**
|
|
1957
|
+
* They are back. Puts them on the parked place, and STAYS ARMED until it has
|
|
1958
|
+
* actually landed. Returns true when the host must pin to the bottom instead
|
|
1959
|
+
* (the reader left pinned), which only the host can do meaningfully.
|
|
1960
|
+
*/
|
|
1961
|
+
settleReturn: () => boolean;
|
|
1962
|
+
/** A return is armed: its position, not the host's, decides scrollTop. */
|
|
1963
|
+
isReturning: () => boolean;
|
|
1964
|
+
/**
|
|
1965
|
+
* The reader is driving. Retire any armed return, at once.
|
|
1966
|
+
*
|
|
1967
|
+
* Call from the raw gesture handlers (wheel, touchstart, touchmove, keydown),
|
|
1968
|
+
* which fire synchronously on a real user action and never for a programmatic
|
|
1969
|
+
* scroll. Waiting for the scroll EVENT is not good enough: a background refresh
|
|
1970
|
+
* can settle in between, and a settle with a return still armed puts the reader
|
|
1971
|
+
* back where they were before they left, which lands as "I touched the scroll
|
|
1972
|
+
* and it threw me somewhere else".
|
|
1973
|
+
*/
|
|
1974
|
+
release: () => void;
|
|
1975
|
+
/** Pin to the bottom, instantly, recording the write. The ONLY way to pin. */
|
|
1976
|
+
pinBottom: () => void;
|
|
1977
|
+
/** The box is not being painted (hidden tab). Shared so hosts agree. */
|
|
1978
|
+
isFrozen: () => boolean;
|
|
1979
|
+
/** Deprecated alias of settleReturn, kept so a stale dist does not break. */
|
|
1980
|
+
thaw: () => void;
|
|
1908
1981
|
/** Absorb one element's own resize. See below. */
|
|
1909
1982
|
absorb: (el: AnchorGrowableEl | null | undefined) => void;
|
|
1910
1983
|
/** Drop the remembered place (chat switch, unmount). */
|
package/dist/engine.mjs
CHANGED
|
@@ -2578,6 +2578,9 @@ function shouldRescueInFlightMessage(m, ctx) {
|
|
|
2578
2578
|
// src/engine/scroll_anchor.ts
|
|
2579
2579
|
var ROW_KEY_ATTR = "data-row-key";
|
|
2580
2580
|
var ROW_POS_ATTR = "data-row-pos";
|
|
2581
|
+
var MAX_ALTS = 2;
|
|
2582
|
+
var ALT_SCAN_LIMIT = 64;
|
|
2583
|
+
var MAX_RETURN_SETTLES = 3;
|
|
2581
2584
|
var UNKNOWN_ROW_POS = "\0?";
|
|
2582
2585
|
function createScrollAnchor(options) {
|
|
2583
2586
|
var held = null;
|
|
@@ -2588,6 +2591,7 @@ function createScrollAnchor(options) {
|
|
|
2588
2591
|
var boxTop = box.getBoundingClientRect().top;
|
|
2589
2592
|
var kids = box.children;
|
|
2590
2593
|
var fallback = null;
|
|
2594
|
+
var fallbackAt = -1;
|
|
2591
2595
|
for (var i = 0; i < kids.length; i++) {
|
|
2592
2596
|
var el = kids[i];
|
|
2593
2597
|
if (!el || typeof el.getAttribute !== "function") continue;
|
|
@@ -2606,10 +2610,20 @@ function createScrollAnchor(options) {
|
|
|
2606
2610
|
scrollHeight: box.scrollHeight,
|
|
2607
2611
|
el
|
|
2608
2612
|
};
|
|
2609
|
-
if (rawPos === null)
|
|
2610
|
-
|
|
2613
|
+
if (rawPos === null) {
|
|
2614
|
+
cand.alts = collectAlts(box, boxTop, i + 1);
|
|
2615
|
+
return cand;
|
|
2616
|
+
}
|
|
2617
|
+
if (!fallback) {
|
|
2618
|
+
fallback = cand;
|
|
2619
|
+
fallbackAt = i;
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
if (fallback) {
|
|
2623
|
+
fallback.alts = collectAlts(box, boxTop, fallbackAt + 1, true);
|
|
2624
|
+
return fallback;
|
|
2611
2625
|
}
|
|
2612
|
-
return
|
|
2626
|
+
return {
|
|
2613
2627
|
key: null,
|
|
2614
2628
|
top: 0,
|
|
2615
2629
|
pos: null,
|
|
@@ -2618,6 +2632,26 @@ function createScrollAnchor(options) {
|
|
|
2618
2632
|
el: null
|
|
2619
2633
|
};
|
|
2620
2634
|
}
|
|
2635
|
+
function collectAlts(box, boxTop, from, ordinaryOnly) {
|
|
2636
|
+
var out = [];
|
|
2637
|
+
var kids = box.children;
|
|
2638
|
+
var stop = Math.min(kids.length, from + ALT_SCAN_LIMIT);
|
|
2639
|
+
for (var i = from; i < stop && out.length < MAX_ALTS; i++) {
|
|
2640
|
+
var el = kids[i];
|
|
2641
|
+
if (!el || typeof el.getAttribute !== "function") continue;
|
|
2642
|
+
var key = el.getAttribute(ROW_KEY_ATTR);
|
|
2643
|
+
if (!key) continue;
|
|
2644
|
+
var rawPos = el.getAttribute(ROW_POS_ATTR);
|
|
2645
|
+
if (ordinaryOnly && rawPos !== null) continue;
|
|
2646
|
+
out.push({
|
|
2647
|
+
key,
|
|
2648
|
+
top: el.getBoundingClientRect().top - boxTop,
|
|
2649
|
+
pos: rawPos === null ? null : rawPos || UNKNOWN_ROW_POS,
|
|
2650
|
+
el
|
|
2651
|
+
});
|
|
2652
|
+
}
|
|
2653
|
+
return out.length ? out : void 0;
|
|
2654
|
+
}
|
|
2621
2655
|
function findRow(box, anchor) {
|
|
2622
2656
|
var el = anchor.el;
|
|
2623
2657
|
if (el && el.parentNode === box) return el;
|
|
@@ -2630,31 +2664,80 @@ function createScrollAnchor(options) {
|
|
|
2630
2664
|
}
|
|
2631
2665
|
return null;
|
|
2632
2666
|
}
|
|
2633
|
-
|
|
2667
|
+
var sawFrozen = false;
|
|
2668
|
+
var parked = null;
|
|
2669
|
+
var parkedStuck = false;
|
|
2670
|
+
var returning = false;
|
|
2671
|
+
var returnBudget = 0;
|
|
2672
|
+
var wroteTop = -1;
|
|
2673
|
+
var restoreExact = true;
|
|
2674
|
+
var restorePinned = false;
|
|
2675
|
+
function frozen() {
|
|
2676
|
+
var f = !!options.isFrozen && options.isFrozen();
|
|
2677
|
+
if (f) sawFrozen = true;
|
|
2678
|
+
return f;
|
|
2679
|
+
}
|
|
2680
|
+
function restore(anchor, unbounded) {
|
|
2681
|
+
if (frozen()) return;
|
|
2634
2682
|
var box = options.getBox();
|
|
2635
2683
|
if (!box || !anchor || options.isStuck()) return;
|
|
2636
2684
|
var el = findRow(box, anchor);
|
|
2637
2685
|
if (el) {
|
|
2638
2686
|
var livePos = el.getAttribute(ROW_POS_ATTR) || UNKNOWN_ROW_POS;
|
|
2639
|
-
if (anchor.pos !== null && anchor.pos !== UNKNOWN_ROW_POS && livePos !== UNKNOWN_ROW_POS && livePos !== anchor.pos)
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
}
|
|
2687
|
+
if (anchor.pos !== null && anchor.pos !== UNKNOWN_ROW_POS && livePos !== UNKNOWN_ROW_POS && livePos !== anchor.pos) el = null;
|
|
2688
|
+
}
|
|
2689
|
+
if (el) {
|
|
2643
2690
|
var boxTop = box.getBoundingClientRect().top;
|
|
2644
2691
|
var delta = el.getBoundingClientRect().top - boxTop - anchor.top;
|
|
2645
2692
|
var slack = Math.abs(box.scrollHeight - anchor.scrollHeight) + box.clientHeight;
|
|
2646
|
-
if (delta > slack || delta < -slack) {
|
|
2693
|
+
if (!unbounded && (delta > slack || delta < -slack)) {
|
|
2647
2694
|
lost(box, anchor);
|
|
2648
2695
|
return;
|
|
2649
2696
|
}
|
|
2697
|
+
var want = box.scrollTop + delta;
|
|
2650
2698
|
if (delta >= 1 || delta <= -1) box.scrollTop += delta;
|
|
2699
|
+
wroteTop = box.scrollTop;
|
|
2700
|
+
restorePinned = true;
|
|
2701
|
+
restoreExact = box.scrollTop >= want - 1 && box.scrollTop <= want + 1;
|
|
2651
2702
|
held = {
|
|
2652
2703
|
key: anchor.key,
|
|
2653
2704
|
top: anchor.top,
|
|
2654
2705
|
pos: anchor.pos,
|
|
2655
2706
|
scrollTop: box.scrollTop,
|
|
2656
2707
|
scrollHeight: box.scrollHeight,
|
|
2657
|
-
el
|
|
2708
|
+
el,
|
|
2709
|
+
// Carried, not dropped: one successful restore used to disarm the
|
|
2710
|
+
// standbys for every later hold.
|
|
2711
|
+
alts: anchor.alts
|
|
2712
|
+
};
|
|
2713
|
+
return;
|
|
2714
|
+
}
|
|
2715
|
+
var alts = anchor.alts;
|
|
2716
|
+
for (var ai = 0; alts && ai < alts.length; ai++) {
|
|
2717
|
+
var alt = alts[ai];
|
|
2718
|
+
var ael = findRow(box, { key: alt.key, top: alt.top, pos: alt.pos, scrollTop: anchor.scrollTop, scrollHeight: anchor.scrollHeight, el: alt.el });
|
|
2719
|
+
if (!ael) continue;
|
|
2720
|
+
if (alt.pos !== null && alt.pos !== UNKNOWN_ROW_POS) {
|
|
2721
|
+
var altLive = ael.getAttribute(ROW_POS_ATTR) || UNKNOWN_ROW_POS;
|
|
2722
|
+
if (altLive !== UNKNOWN_ROW_POS && altLive !== alt.pos) continue;
|
|
2723
|
+
}
|
|
2724
|
+
var aboxTop = box.getBoundingClientRect().top;
|
|
2725
|
+
var adelta = ael.getBoundingClientRect().top - aboxTop - alt.top;
|
|
2726
|
+
var aslack = Math.abs(box.scrollHeight - anchor.scrollHeight) + box.clientHeight;
|
|
2727
|
+
if (!unbounded && (adelta > aslack || adelta < -aslack)) continue;
|
|
2728
|
+
var awant = box.scrollTop + adelta;
|
|
2729
|
+
if (adelta >= 1 || adelta <= -1) box.scrollTop += adelta;
|
|
2730
|
+
wroteTop = box.scrollTop;
|
|
2731
|
+
restorePinned = true;
|
|
2732
|
+
restoreExact = box.scrollTop >= awant - 1 && box.scrollTop <= awant + 1;
|
|
2733
|
+
held = {
|
|
2734
|
+
key: alt.key,
|
|
2735
|
+
top: alt.top,
|
|
2736
|
+
pos: alt.pos,
|
|
2737
|
+
scrollTop: box.scrollTop,
|
|
2738
|
+
scrollHeight: box.scrollHeight,
|
|
2739
|
+
el: ael,
|
|
2740
|
+
alts: alts.slice(ai + 1)
|
|
2658
2741
|
};
|
|
2659
2742
|
return;
|
|
2660
2743
|
}
|
|
@@ -2665,9 +2748,13 @@ function createScrollAnchor(options) {
|
|
|
2665
2748
|
var grew = box.scrollHeight - anchor.scrollHeight;
|
|
2666
2749
|
if (grew > 0) {
|
|
2667
2750
|
box.scrollTop = anchor.scrollTop + grew;
|
|
2751
|
+
wroteTop = box.scrollTop;
|
|
2668
2752
|
return;
|
|
2669
2753
|
}
|
|
2670
|
-
if (options.rawFallback)
|
|
2754
|
+
if (options.rawFallback) {
|
|
2755
|
+
box.scrollTop = anchor.scrollTop;
|
|
2756
|
+
wroteTop = box.scrollTop;
|
|
2757
|
+
}
|
|
2671
2758
|
}
|
|
2672
2759
|
function preserve(mutate) {
|
|
2673
2760
|
var anchor = capture();
|
|
@@ -2676,9 +2763,17 @@ function createScrollAnchor(options) {
|
|
|
2676
2763
|
return result;
|
|
2677
2764
|
}
|
|
2678
2765
|
function remember() {
|
|
2766
|
+
var b0 = options.getBox();
|
|
2767
|
+
if (returning && b0 && b0.scrollTop !== wroteTop) release();
|
|
2768
|
+
if (frozen()) return;
|
|
2679
2769
|
held = capture();
|
|
2680
2770
|
}
|
|
2681
2771
|
function hold() {
|
|
2772
|
+
if (frozen()) return;
|
|
2773
|
+
if (sawFrozen) {
|
|
2774
|
+
settleReturn();
|
|
2775
|
+
return;
|
|
2776
|
+
}
|
|
2682
2777
|
var box = options.getBox();
|
|
2683
2778
|
if (!box || options.isStuck()) {
|
|
2684
2779
|
held = null;
|
|
@@ -2705,12 +2800,86 @@ function createScrollAnchor(options) {
|
|
|
2705
2800
|
if (prev === void 0) prev = 0;
|
|
2706
2801
|
var delta = h - prev;
|
|
2707
2802
|
if (delta === 0) return;
|
|
2803
|
+
if (frozen()) {
|
|
2804
|
+
foldFrozenGrowth(box, el, delta);
|
|
2805
|
+
return;
|
|
2806
|
+
}
|
|
2708
2807
|
if (el.getBoundingClientRect().top >= box.getBoundingClientRect().top) return;
|
|
2709
2808
|
box.scrollTop += delta;
|
|
2809
|
+
wroteTop = box.scrollTop;
|
|
2710
2810
|
if (held) held = capture();
|
|
2711
2811
|
}
|
|
2812
|
+
function park() {
|
|
2813
|
+
parked = capture();
|
|
2814
|
+
parkedStuck = !!options.isStuck();
|
|
2815
|
+
returning = true;
|
|
2816
|
+
returnBudget = MAX_RETURN_SETTLES;
|
|
2817
|
+
}
|
|
2818
|
+
function release() {
|
|
2819
|
+
parked = null;
|
|
2820
|
+
parkedStuck = false;
|
|
2821
|
+
returning = false;
|
|
2822
|
+
returnBudget = 0;
|
|
2823
|
+
}
|
|
2824
|
+
function pinBottom() {
|
|
2825
|
+
var box = options.getBox();
|
|
2826
|
+
if (!box) return;
|
|
2827
|
+
box.scrollTop = box.scrollHeight;
|
|
2828
|
+
wroteTop = box.scrollTop;
|
|
2829
|
+
}
|
|
2830
|
+
function settleReturn() {
|
|
2831
|
+
if (frozen()) return false;
|
|
2832
|
+
var wasFrozen = sawFrozen;
|
|
2833
|
+
sawFrozen = false;
|
|
2834
|
+
if (!returning && !wasFrozen) return false;
|
|
2835
|
+
if (returning) {
|
|
2836
|
+
if (returnBudget <= 0) {
|
|
2837
|
+
release();
|
|
2838
|
+
return false;
|
|
2839
|
+
}
|
|
2840
|
+
returnBudget--;
|
|
2841
|
+
}
|
|
2842
|
+
if (parkedStuck) {
|
|
2843
|
+
pinBottom();
|
|
2844
|
+
return true;
|
|
2845
|
+
}
|
|
2846
|
+
var target = parked || held;
|
|
2847
|
+
restoreExact = true;
|
|
2848
|
+
restorePinned = false;
|
|
2849
|
+
restore(target, true);
|
|
2850
|
+
parked = !restorePinned || !restoreExact ? target : null;
|
|
2851
|
+
returning = !!parked && returnBudget > 0;
|
|
2852
|
+
if (!returning) {
|
|
2853
|
+
parked = null;
|
|
2854
|
+
parkedStuck = false;
|
|
2855
|
+
returnBudget = 0;
|
|
2856
|
+
}
|
|
2857
|
+
return false;
|
|
2858
|
+
}
|
|
2859
|
+
function isReturning() {
|
|
2860
|
+
return returning;
|
|
2861
|
+
}
|
|
2862
|
+
function thaw() {
|
|
2863
|
+
settleReturn();
|
|
2864
|
+
}
|
|
2865
|
+
function foldFrozenGrowth(box, el, delta) {
|
|
2866
|
+
var elTop = el.getBoundingClientRect().top;
|
|
2867
|
+
foldInto(box, held, elTop, delta);
|
|
2868
|
+
foldInto(box, parked, elTop, delta);
|
|
2869
|
+
}
|
|
2870
|
+
function foldInto(box, a, elTop, delta) {
|
|
2871
|
+
if (!a || a.top >= 0) return;
|
|
2872
|
+
var rowEl = findRow(box, a);
|
|
2873
|
+
if (!rowEl) return;
|
|
2874
|
+
var within = elTop - rowEl.getBoundingClientRect().top;
|
|
2875
|
+
if (within < 0 || within >= rowEl.offsetHeight) return;
|
|
2876
|
+
if (within >= -a.top) return;
|
|
2877
|
+
a.top -= delta;
|
|
2878
|
+
a.scrollHeight += delta;
|
|
2879
|
+
}
|
|
2712
2880
|
function forget() {
|
|
2713
2881
|
held = null;
|
|
2882
|
+
release();
|
|
2714
2883
|
}
|
|
2715
2884
|
return {
|
|
2716
2885
|
capture,
|
|
@@ -2718,6 +2887,13 @@ function createScrollAnchor(options) {
|
|
|
2718
2887
|
preserve,
|
|
2719
2888
|
remember,
|
|
2720
2889
|
hold,
|
|
2890
|
+
park,
|
|
2891
|
+
settleReturn,
|
|
2892
|
+
isReturning,
|
|
2893
|
+
release,
|
|
2894
|
+
pinBottom,
|
|
2895
|
+
isFrozen: frozen,
|
|
2896
|
+
thaw,
|
|
2721
2897
|
absorb,
|
|
2722
2898
|
forget
|
|
2723
2899
|
};
|
|
@@ -5820,6 +5996,7 @@ var ChatSession = class {
|
|
|
5820
5996
|
releaseBgFlag();
|
|
5821
5997
|
self.updateHistoryCache();
|
|
5822
5998
|
self.host.notify();
|
|
5999
|
+
if (self.host.settleScroll) self.host.settleScroll();
|
|
5823
6000
|
}, function() {
|
|
5824
6001
|
releaseBgFlag();
|
|
5825
6002
|
self.host.notify();
|
|
@@ -5901,7 +6078,7 @@ var ChatSession = class {
|
|
|
5901
6078
|
self.drainBgTaskQueue();
|
|
5902
6079
|
}
|
|
5903
6080
|
if (!fetchMore) self.refreshLiveIndexState();
|
|
5904
|
-
if (!fetchMore) return self.host.scrollToBottomIfSticky();
|
|
6081
|
+
if (!fetchMore) return self.host.settleScroll ? self.host.settleScroll() : self.host.scrollToBottomIfSticky();
|
|
5905
6082
|
}).catch(function(err) {
|
|
5906
6083
|
console.warn("[chat-engine] getChatHistory failed", err);
|
|
5907
6084
|
}).then(function() {
|