refactor(ebook-reader): switch to CSS columns pagination approach

Replace HTML-splitting pagination with CSS columns for true paginated
viewing. This simplifies the implementation and relies on the browser's
native column-fill behavior for accurate page breaks.

Changes:
- view-modes.ts: Use CSS columns with column-fill: auto instead of
  pre-splitting HTML content into page chunks. Calculate page count
  from scrollHeight / viewportHeight.

- reader-navigation.ts: Navigate by scrolling viewport height instead of
  extracting discrete page content. Track page position via scroll
  offset. Simplified renderSpineItem to load full spine content.

- page-calculator.ts: Simplified to track spine info (charCount,
  estimatedPages) only. No more height-based content splitting.
  Page calculation happens in real-time from DOM scroll position.

Benefits:
- Accurate pagination without estimation errors
- Works correctly across different font sizes and screen sizes
- Simpler code with fewer edge cases
- Natural page breaks at element boundaries via CSS
This commit is contained in:
2026-04-07 21:02:18 -04:00
parent 1e7d13dc85
commit 1a5eb40d82
3 changed files with 234 additions and 498 deletions
+26 -35
View File
@@ -72,15 +72,15 @@ function setViewMode(container: HTMLElement, mode: ViewMode): void {
function applyPaginatedMode(element: HTMLElement): void {
element.classList.add("paginated");
// True pagination: content fits exactly in viewport, no scrolling
element.style.height = "calc(100vh - 120px)"; // Account for chrome (top 60px + bottom 60px)
element.style.overflow = "hidden";
// Set up for CSS columns
element.style.height = "calc(100vh - 120px)";
element.style.overflowY = "auto";
element.style.overflowX = "hidden";
element.style.columnCount = "1";
element.style.columnFill = "auto";
element.style.columnGap = "0";
element.style.position = "relative";
// Inject CSS for page clipping
// Inject CSS for CSS column pagination
injectPaginatedStyles();
}
@@ -92,23 +92,22 @@ function injectPaginatedStyles(): void {
const style = document.createElement("style");
style.id = "paginated-styles";
style.textContent = `
.paginated .ebook-page {
height: 100%;
overflow: hidden;
display: flex;
flex-direction: column;
.paginated {
height: calc(100vh - 120px) !important;
overflow-y: auto !important;
overflow-x: hidden !important;
column-fill: auto !important;
column-count: 1 !important;
column-gap: 0 !important;
position: relative !important;
}
.paginated .ebook-page > * {
max-height: 100%;
overflow: hidden;
.paginated > * {
max-width: 100%;
column-fill: auto;
}
.paginated .ebook-content {
height: 100%;
overflow: hidden !important;
position: relative;
}
.paginated .ebook-content * {
overflow-wrap: break-word;
height: auto !important;
min-height: 100%;
}
`;
document.head.appendChild(style);
@@ -144,29 +143,22 @@ function applyDoubleColumn(element: HTMLElement): void {
function goToPage(container: HTMLElement, pageNumber: number): void {
const content = container.querySelector(".ebook-content") as HTMLElement;
if (!content) return;
const pageHeight = content.clientHeight;
const scrollTop = (pageNumber - 1) * pageHeight;
const totalPages = getTotalPageCount(container);
const targetPage = Math.min(Math.max(1, pageNumber), totalPages);
const viewportHeight = window.innerHeight - 120;
const scrollTop = (targetPage - 1) * viewportHeight;
content.scrollTo({
top: scrollTop,
behavior: "smooth",
});
const pageInfo = container.querySelector(".page-info");
if (pageInfo) {
pageInfo.textContent = `Page ${pageNumber} of ${getTotalPageCount(container)}`;
}
}
function getTotalPageCount(container: HTMLElement): number {
const content = container.querySelector(".ebook-content") as HTMLElement;
if (!content) return 1;
const totalHeight = content.scrollHeight;
const pageHeight = content.clientHeight;
return Math.ceil(totalHeight / pageHeight);
const viewportHeight = window.innerHeight - 120;
const contentHeight = content.scrollHeight;
return Math.max(1, Math.ceil(contentHeight / viewportHeight));
}
export function getCurrentViewMode(container: HTMLElement): ViewMode {
@@ -180,4 +172,3 @@ export function getCurrentViewMode(container: HTMLElement): ViewMode {
return "paginated";
}