Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,8 @@ export class CbxReaderComponent implements OnInit, OnDestroy {
private readerLayoutGraceUntilMs = signal(0);
/** Invalidates delayed scroll/layout work after book or scroll-mode changes. */
private readerLayoutGeneration = signal(0);
/** Invalidates a pending loadMorePages() append when infiniteScrollPages is wholesale replaced (e.g. ensurePageLoaded via goToPage). */
private infiniteScrollWindowVersion = 0;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
/** Avoid continuation hint flicker when scroll height changes (hysteresis). */
private continuationHintLatched = signal(false);

Expand Down Expand Up @@ -1154,15 +1156,60 @@ export class CbxReaderComponent implements OnInit, OnDestroy {

this.isLoadingMore.set(true);
const endIndex = Math.min(lastLoadedIndex + this.preloadCount + 1, this.pages().length);
const layoutGen = this.readerLayoutGeneration();
const scrollModeAtStart = this.scrollMode();
const windowVersion = this.infiniteScrollWindowVersion;

requestAnimationFrame(() => {
if (windowVersion !== this.infiniteScrollWindowVersion) {
// infiniteScrollPages was wholesale replaced (e.g. goToPage) while this frame was
// pending — that reset already owns isLoadingMore, don't touch it here.
return;
}
if (layoutGen !== this.readerLayoutGeneration() || this.scrollMode() !== scrollModeAtStart) {
this.isLoadingMore.set(false);
return;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// Trimming the DOM window below can drop pages above the viewport (see
// trimInfiniteScrollPages). With overflow-anchor disabled on this container,
// the browser won't compensate on its own, so anchor on the last already-loaded
// page — guaranteed to survive a tail trim — and restore its position after the
// trim, same technique as loadPreviousPages. We don't anchor on currentPage()
// here: it's updated on a debounce and can still reference a page that's about
// to fall off the (soon to be trimmed) head.
const container = this.getImageScrollContainer();
const anchorEl = container?.querySelector(
`.infinite-scroll-wrapper img.page-image[data-page="${lastLoadedIndex}"]`
) as HTMLElement | null;
const beforeTop = anchorEl?.getBoundingClientRect().top;

const added: number[] = [];
for (let i = lastLoadedIndex + 1; i < endIndex; i++) {
added.push(i);
}
this.infiniteScrollPages.update(p => [...p, ...added]);
this.trimInfiniteScrollPages('tail');
this.isLoadingMore.set(false);

if (container && anchorEl?.isConnected && beforeTop !== undefined) {
this.afterNextPaint(() => {
if (windowVersion !== this.infiniteScrollWindowVersion) {
return;
}
if (layoutGen !== this.readerLayoutGeneration() || this.scrollMode() !== scrollModeAtStart) {
this.isLoadingMore.set(false);
return;
}
if (!anchorEl.isConnected) {
this.isLoadingMore.set(false);
return;
}
container.scrollTop += anchorEl.getBoundingClientRect().top - beforeTop;
this.isLoadingMore.set(false);
});
} else {
this.isLoadingMore.set(false);
}
});
}

Expand Down Expand Up @@ -1668,7 +1715,11 @@ export class CbxReaderComponent implements OnInit, OnDestroy {
for (let i = startIndex; i < endIndex; i++) {
pages.push(i);
}
// Replacing the window wholesale invalidates any in-flight loadMorePages() append
// (e.g. jumping to a page via goToPage while auto-loading near the scroll edge).
this.infiniteScrollWindowVersion++;
this.infiniteScrollPages.set(pages);
this.isLoadingMore.set(false);
}

onImageClick(): void {
Expand Down