A cover with `data-duokan-page-fullscreen` on <html> renders in paginated mode but is blank in scrolled mode; only the first cover is affected, other images are fine in both modes. The paginator's fullscreen branch pins such images with position:absolute and height:100% and forces their ancestors to height:100%. That fills the fixed-height page when columnized, but in scrolled mode the container height is `auto`, so height:100% resolves to 0 and the cover collapses out of view. Bump foliate-js to gate the fullscreen treatment on column mode and reset any stale absolute pinning when a fullscreen-cover doc is laid out scrolled (so toggling paginated -> scrolled also recovers). Add a browser regression test + repro-4379.epub fixture. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,7 @@
|
||||
- [Reading ruler line/column-aware](reading-ruler-line-aware.md) — ruler snaps to real lines; multi-column band spans one column; Range.getClientRects() returns tall block boxes that must be dropped; iframe frame-offset mapping; synthetic-key throttling
|
||||
- [TOC expand + auto-scroll](toc-expand-and-autoscroll.md) — #4059 collapse-by-default policy in `tocTree.ts`; pinned-sidebar mounts before progress → dynamic expansion breaks scroll-to-current via (1) spurious onScroll clearing pending and (2) Virtuoso scrollToIndex landing short after row growth (re-assert on rAF)
|
||||
- [Swipe page-turn bg flash](paginator-swipe-bg-flash.md) — white↔black flash on swipe+animation only; `#background` was static screen-space and didn't track content during drag/snap; fix = sliding per-view full-bleed segments (`computeBackgroundSegments`) rebuilt on scroll + per-rAF synced to the view transform during snap
|
||||
- [Duokan fullscreen cover hidden in scroll mode](duokan-fullscreen-cover-scroll.md) — #4379 `data-duokan-page-fullscreen` cover pinned `position:absolute height:100%` collapses against auto-height scroll container; gate fullscreen on `this.#column` + reset stale absolute props on toggle (`setImageSize` in paginator.js)
|
||||
|
||||
## Critical Files (Most Bug-Prone)
|
||||
- `src/utils/style.ts` - Central EPUB CSS transformation hub (14+ bug fixes)
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
name: duokan-fullscreen-cover-scroll
|
||||
description: "Duokan fullscreen cover image invisible in scrolled mode (#4379) — paginator pins it position:absolute height:100% which collapses against auto-height scroll container"
|
||||
metadata:
|
||||
node_type: memory
|
||||
type: project
|
||||
originSessionId: c45aabf0-e8a3-42b6-a5fd-c04d6eb2345c
|
||||
---
|
||||
|
||||
Issue #4379: an EPUB cover with `data-duokan-page-fullscreen` on `<html>` (Duokan/DangDang convention) shows in paginated mode but is **blank in scrolled mode**; only the first cover image, other images fine in both modes.
|
||||
|
||||
**Root cause** — `View.setImageSize()` in `packages/foliate-js/paginator.js` has a `pageFullscreen` branch that pins each img with `position:absolute; inset:0; width:100%; height:100%` and forces ancestors + `<html>` to `height:100%`/`position:relative`. This fills the page in paginated/columnized mode (html has a fixed pixel height). In scrolled mode `scrolled()` sets `html`/`body` height to `auto`, so the `height:100%` chain resolves to **0** and the absolutely-positioned cover collapses out of view (offsetHeight 0).
|
||||
|
||||
**Fix** — gate the fullscreen treatment on column mode: `const applyFullscreen = pageFullscreen && this.#column`. Use `applyFullscreen` for the max-height/max-width margin term and the `if` block. Add an `else if (pageFullscreen)` that `removeProperty`s the stale `position/inset/width/height/margin` on the img (and `width/height/margin/padding` on ancestors, `position` on html) so toggling paginated→scrolled doesn't leave the cover collapsed (same iframe/img is reused via `view.render(layout)` on `flow` change). In scrolled mode the cover then flows like a normal full-page image bounded by `max-height = availableHeight`.
|
||||
|
||||
**Key facts**
|
||||
- `this.#column = layout.flow !== 'scrolled'` (set in `render()` before `setImageSize`), so it's reliable inside `setImageSize`.
|
||||
- Foliate writes these styles as **inline `!important`** → cannot be overridden from `src/utils/style.ts`; the fix must live in the paginator.
|
||||
- Regression test: `src/__tests__/document/paginator-duokan-cover.browser.test.ts` + fixture `repro-4379.epub` (cover xhtml with the duokan attr + dimensionless `<img>`). Asserts cover `img.offsetHeight > 0` in scrolled mode, paginated sanity, and paginated→scrolled toggle. Browser test (real layout) is required — jsdom can't compute the collapse.
|
||||
|
||||
Related: [[paginator-swipe-bg-flash]], [[css-style-fixes]].
|
||||
@@ -0,0 +1,150 @@
|
||||
import { describe, it, expect, beforeAll, afterEach } from 'vitest';
|
||||
import { DocumentLoader } from '@/libs/document';
|
||||
import type { BookDoc } from '@/libs/document';
|
||||
import type { Renderer } from '@/types/view';
|
||||
|
||||
// repro-4379: a Duokan/DangDang full-page cover — `data-duokan-page-fullscreen`
|
||||
// on <html> with a dimensionless <img> inside a `.cover` wrapper. The paginator
|
||||
// pins such covers with `position:absolute; inset:0; height:100%`, which fills
|
||||
// the fixed-height page in paginated mode but collapses to zero against the
|
||||
// auto-height scroll container in scrolled mode (the cover disappears).
|
||||
const EPUB_URL = new URL('../fixtures/data/repro-4379.epub', import.meta.url).href;
|
||||
|
||||
let book: BookDoc;
|
||||
|
||||
const loadEPUB = async () => {
|
||||
const resp = await fetch(EPUB_URL);
|
||||
const buffer = await resp.arrayBuffer();
|
||||
const file = new File([buffer], 'repro-4379.epub', { type: 'application/epub+zip' });
|
||||
const loader = new DocumentLoader(file);
|
||||
const { book } = await loader.open();
|
||||
return book;
|
||||
};
|
||||
|
||||
const waitForStabilized = (el: HTMLElement, timeout = 10000) =>
|
||||
new Promise<void>((resolve, reject) => {
|
||||
const timer = setTimeout(() => reject(new Error('stabilized timeout')), timeout);
|
||||
el.addEventListener(
|
||||
'stabilized',
|
||||
() => {
|
||||
clearTimeout(timer);
|
||||
resolve();
|
||||
},
|
||||
{ once: true },
|
||||
);
|
||||
});
|
||||
|
||||
const getCoverImg = (paginator: Renderer): HTMLImageElement | null => {
|
||||
const cover = paginator.getContents().find((c) => c.index === 0);
|
||||
return (cover?.doc.body.querySelector('img') as HTMLImageElement | undefined) ?? null;
|
||||
};
|
||||
|
||||
/** Wait for the cover <img> resource to decode so layout has a natural size. */
|
||||
const waitForImgLoaded = async (img: HTMLImageElement, timeout = 5000) => {
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < timeout) {
|
||||
if (img.complete && img.naturalHeight > 0) return;
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
}
|
||||
};
|
||||
|
||||
/** Poll until the element has a non-zero rendered height (or time out). */
|
||||
const waitForVisibleHeight = async (img: HTMLImageElement, timeout = 3000) => {
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < timeout) {
|
||||
if (img.offsetHeight > 0) return;
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
}
|
||||
};
|
||||
|
||||
describe('Paginator Duokan fullscreen cover (#4379)', () => {
|
||||
let paginator: Renderer;
|
||||
|
||||
beforeAll(async () => {
|
||||
book = await loadEPUB();
|
||||
await import('foliate-js/paginator.js');
|
||||
}, 30000);
|
||||
|
||||
const createPaginator = () => {
|
||||
const el = document.createElement('foliate-paginator') as Renderer;
|
||||
Object.assign(el.style, {
|
||||
width: '800px',
|
||||
height: '600px',
|
||||
position: 'absolute',
|
||||
left: '0',
|
||||
top: '0',
|
||||
});
|
||||
document.body.appendChild(el);
|
||||
return el;
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
if (paginator) {
|
||||
try {
|
||||
paginator.destroy();
|
||||
} catch {
|
||||
/* iframe body may already be torn down */
|
||||
}
|
||||
paginator.remove();
|
||||
}
|
||||
});
|
||||
|
||||
it('shows the cover image in paginated mode (sanity)', async () => {
|
||||
paginator = createPaginator();
|
||||
paginator.open(book);
|
||||
|
||||
const stabilized = waitForStabilized(paginator);
|
||||
await paginator.goTo({ index: 0 });
|
||||
await stabilized;
|
||||
|
||||
const img = getCoverImg(paginator);
|
||||
expect(img).toBeTruthy();
|
||||
await waitForImgLoaded(img!);
|
||||
await waitForVisibleHeight(img!);
|
||||
expect(img!.offsetHeight).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('shows the cover image in scrolled mode', async () => {
|
||||
paginator = createPaginator();
|
||||
paginator.open(book);
|
||||
paginator.setAttribute('flow', 'scrolled');
|
||||
|
||||
const stabilized = waitForStabilized(paginator);
|
||||
await paginator.goTo({ index: 0 });
|
||||
await stabilized;
|
||||
|
||||
const img = getCoverImg(paginator);
|
||||
expect(img).toBeTruthy();
|
||||
await waitForImgLoaded(img!);
|
||||
await waitForVisibleHeight(img!);
|
||||
|
||||
// Before the fix the cover is absolutely positioned at height:100% inside
|
||||
// an auto-height (zero) container, so it collapses and never renders.
|
||||
expect(img!.offsetHeight).toBeGreaterThan(0);
|
||||
// It must also stay bounded by the viewport rather than overflowing it.
|
||||
expect(img!.offsetHeight).toBeLessThanOrEqual(600);
|
||||
});
|
||||
|
||||
it('shows the cover image after toggling paginated -> scrolled', async () => {
|
||||
paginator = createPaginator();
|
||||
paginator.open(book);
|
||||
|
||||
const stabilized = waitForStabilized(paginator);
|
||||
await paginator.goTo({ index: 0 });
|
||||
await stabilized;
|
||||
|
||||
const img = getCoverImg(paginator);
|
||||
expect(img).toBeTruthy();
|
||||
await waitForImgLoaded(img!);
|
||||
await waitForVisibleHeight(img!);
|
||||
|
||||
const stabilized2 = waitForStabilized(paginator);
|
||||
paginator.setAttribute('flow', 'scrolled');
|
||||
await stabilized2;
|
||||
|
||||
// The same <img> element is re-rendered; stale absolute positioning from
|
||||
// the paginated render must not leave it collapsed in scrolled mode.
|
||||
await waitForVisibleHeight(img!);
|
||||
expect(img!.offsetHeight).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
Binary file not shown.
+1
-1
Submodule packages/foliate-js updated: 167757a102...e8995daeb6
Reference in New Issue
Block a user