From 797fe9c604bbc188b98f9355caf704c727fbbca0 Mon Sep 17 00:00:00 2001 From: Huang Xin Date: Mon, 30 Mar 2026 20:50:59 +0800 Subject: [PATCH] fix(layout): fixed infinite expand calls and freeze in the paginator, closes #3683 (#3690) --- .../document/paginator-expand.browser.test.ts | 292 ++++++++++++++++++ .../paginator-multiview.browser.test.ts | 35 +-- .../paginator-stabilization.browser.test.ts | 37 +-- .../__tests__/fixtures/data/repro-3583.epub | Bin 0 -> 3350 bytes .../__tests__/fixtures/data/repro-3683.epub | Bin 0 -> 2066 bytes apps/readest-app/src/types/view.ts | 108 ++++--- packages/foliate-js | 2 +- 7 files changed, 363 insertions(+), 111 deletions(-) create mode 100644 apps/readest-app/src/__tests__/document/paginator-expand.browser.test.ts create mode 100644 apps/readest-app/src/__tests__/fixtures/data/repro-3583.epub create mode 100644 apps/readest-app/src/__tests__/fixtures/data/repro-3683.epub diff --git a/apps/readest-app/src/__tests__/document/paginator-expand.browser.test.ts b/apps/readest-app/src/__tests__/document/paginator-expand.browser.test.ts new file mode 100644 index 00000000..de3f54b8 --- /dev/null +++ b/apps/readest-app/src/__tests__/document/paginator-expand.browser.test.ts @@ -0,0 +1,292 @@ +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-3683: cover page with display:table + position:absolute + width:100% on body +const REPRO_3683_URL = new URL('../fixtures/data/repro-3683.epub', import.meta.url).href; +// repro-3583: vertical writing mode with height/width:100% divs and SVG image +const REPRO_3583_URL = new URL('../fixtures/data/repro-3583.epub', import.meta.url).href; +const ALICE_URL = new URL('../fixtures/data/sample-alice.epub', import.meta.url).href; + +const loadEPUB = async (url: string) => { + const resp = await fetch(url); + const buffer = await resp.arrayBuffer(); + const name = url.split('/').pop() ?? 'book.epub'; + const file = new File([buffer], name, { type: 'application/epub+zip' }); + const loader = new DocumentLoader(file); + const { book } = await loader.open(); + return book; +}; + +const waitForStabilized = (el: HTMLElement, timeout = 5000) => + new Promise((resolve, reject) => { + const timer = setTimeout(() => reject(new Error('stabilized timeout')), timeout); + el.addEventListener( + 'stabilized', + () => { + clearTimeout(timer); + resolve(); + }, + { once: true }, + ); + }); + +const waitForFillComplete = async (el: Renderer, timeout = 5000) => { + const start = Date.now(); + let lastCount = -1; + let stableFor = 0; + while (Date.now() - start < timeout) { + const count = el.getContents().length; + if (count === lastCount) { + stableFor += 50; + if (stableFor >= 300) return; + } else { + stableFor = 0; + lastCount = count; + } + await new Promise((r) => setTimeout(r, 50)); + } +}; + +let repro3683: BookDoc; +let repro3583: BookDoc; +let aliceBook: BookDoc; + +/** Check all iframes and SVGs in shadow DOM stay within a size bound. */ +const assertBoundedWidths = (paginator: Renderer, maxWidth: number) => { + const shadow = paginator.shadowRoot; + expect(shadow).toBeDefined(); + for (const iframe of shadow!.querySelectorAll('iframe')) { + const w = parseFloat(iframe.style.width) || iframe.getBoundingClientRect().width; + expect(w, `iframe width ${w}px exceeds ${maxWidth}px`).toBeLessThan(maxWidth); + } + for (const svg of shadow!.querySelectorAll('svg')) { + const w = parseFloat(svg.style.width) || svg.getBoundingClientRect().width; + expect(w, `svg width ${w}px exceeds ${maxWidth}px`).toBeLessThan(maxWidth); + } +}; + +const assertBoundedHeights = (paginator: Renderer, maxHeight: number) => { + const shadow = paginator.shadowRoot; + expect(shadow).toBeDefined(); + for (const iframe of shadow!.querySelectorAll('iframe')) { + const h = parseFloat(iframe.style.height) || iframe.getBoundingClientRect().height; + expect(h, `iframe height ${h}px exceeds ${maxHeight}px`).toBeLessThan(maxHeight); + } + for (const svg of shadow!.querySelectorAll('svg')) { + const h = parseFloat(svg.style.height) || svg.getBoundingClientRect().height; + expect(h, `svg height ${h}px exceeds ${maxHeight}px`).toBeLessThan(maxHeight); + } +}; + +describe('Paginator expand loop regression', () => { + let paginator: Renderer; + + const suppressHandler = (e: ErrorEvent) => { + if (e.message?.includes('getComputedStyle')) e.preventDefault(); + }; + + beforeAll(async () => { + window.addEventListener('error', suppressHandler); + [repro3683, repro3583, aliceBook] = await Promise.all([ + loadEPUB(REPRO_3683_URL), + loadEPUB(REPRO_3583_URL), + loadEPUB(ALICE_URL), + ]); + 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(async () => { + if (paginator) { + await new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r))); + try { + paginator.destroy(); + } catch { + /* iframe body may already be torn down */ + } + paginator.remove(); + } + window.removeEventListener('error', suppressHandler); + }); + + describe('Table-layout cover page (repro EPUB)', () => { + it('should stabilize without freezing on the problematic cover section', async () => { + paginator = createPaginator(); + paginator.open(repro3683); + + // Section 0 is the cover with display:table + position:absolute + width:100% + const stabilized = waitForStabilized(paginator); + await paginator.goTo({ index: 0 }); + await stabilized; + + // If we reach here, the paginator didn't loop infinitely + expect(paginator.primaryIndex).toBe(0); + expect(paginator.getContents().length).toBeGreaterThanOrEqual(1); + }); + + it('should render the cover content correctly', async () => { + paginator = createPaginator(); + paginator.open(repro3683); + + const stabilized = waitForStabilized(paginator); + await paginator.goTo({ index: 0 }); + await stabilized; + + const contents = paginator.getContents(); + const cover = contents.find((c) => c.index === 0); + expect(cover).toBeDefined(); + expect(cover!.doc.body.textContent).toContain('COVER PAGE'); + }); + + it('should navigate from cover to chapter without freezing', async () => { + paginator = createPaginator(); + paginator.open(repro3683); + + // Go to cover first + const stabilized = waitForStabilized(paginator); + await paginator.goTo({ index: 0 }); + await stabilized; + await waitForFillComplete(paginator); + + // Navigate to chapter 1 — it may already be loaded as an adjacent + // section during fill, so goTo reuses the view without emitting + // 'stabilized'. Just await goTo and verify the result. + await paginator.goTo({ index: 1 }); + // Allow layout to settle + await new Promise((r) => setTimeout(r, 200)); + + expect(paginator.primaryIndex).toBe(1); + const contents = paginator.getContents(); + const ch1 = contents.find((c) => c.index === 1); + expect(ch1).toBeDefined(); + expect(ch1!.doc.body.textContent).toContain('Hello, world.'); + }); + + it('should stabilize the cover in scrolled mode', async () => { + paginator = createPaginator(); + paginator.open(repro3683); + paginator.setAttribute('flow', 'scrolled'); + + const stabilized = waitForStabilized(paginator); + await paginator.goTo({ index: 0 }); + await stabilized; + + expect(paginator.scrolled).toBe(true); + expect(paginator.primaryIndex).toBe(0); + }); + + it('should keep element sizes bounded in paginated mode', async () => { + // Without the fix, expand() diverges: body (position:absolute, + // width:100%) mirrors the iframe width, each expand computes an + // even larger expandedSize. Iframes/SVGs grow to millions of px. + paginator = createPaginator(); + paginator.open(repro3683); + + const stabilized = waitForStabilized(paginator); + await paginator.goTo({ index: 0 }); + await stabilized; + await waitForFillComplete(paginator); + + // Let ResizeObserver cycles run — divergence would explode here + await new Promise((r) => setTimeout(r, 500)); + assertBoundedWidths(paginator, 800 * 20); + assertBoundedHeights(paginator, 600 * 20); + }); + }); + + describe('Vertical writing mode with 100% divs in scrolled mode (repro-3583)', () => { + it('should stabilize without freezing', async () => { + paginator = createPaginator(); + paginator.open(repro3583); + paginator.setAttribute('flow', 'scrolled'); + + const stabilized = waitForStabilized(paginator); + await paginator.goTo({ index: 0 }); + await stabilized; + + expect(paginator.scrolled).toBe(true); + expect(paginator.primaryIndex).toBe(0); + }); + + it('should keep element sizes bounded', async () => { + paginator = createPaginator(); + paginator.open(repro3583); + paginator.setAttribute('flow', 'scrolled'); + + const stabilized = waitForStabilized(paginator); + await paginator.goTo({ index: 0 }); + await stabilized; + await waitForFillComplete(paginator); + await new Promise((r) => setTimeout(r, 500)); + + assertBoundedWidths(paginator, 800 * 20); + assertBoundedHeights(paginator, 600 * 20); + }); + }); + + describe('Normal EPUB regression check', () => { + it('should stabilize normally on a regular section', async () => { + paginator = createPaginator(); + paginator.open(aliceBook); + + const idx = aliceBook.sections!.findIndex((s) => s.linear !== 'no'); + const stabilized = waitForStabilized(paginator); + await paginator.goTo({ index: idx }); + await stabilized; + + expect(paginator.primaryIndex).toBe(idx); + expect(paginator.getContents().length).toBeGreaterThanOrEqual(1); + }); + + it('should allow legitimate re-expansion after render', async () => { + paginator = createPaginator(); + paginator.open(aliceBook); + + const idx = aliceBook.sections!.findIndex((s) => s.linear !== 'no'); + const stabilized = waitForStabilized(paginator); + await paginator.goTo({ index: idx }); + await stabilized; + await waitForFillComplete(paginator); + + // Re-render should work (expand history resets on layout changes) + const stabilized2 = waitForStabilized(paginator); + paginator.render?.(); + await stabilized2; + + expect(paginator.primaryIndex).toBe(idx); + }); + + it('should handle flow mode switch with convergence detection', async () => { + paginator = createPaginator(); + paginator.open(aliceBook); + + const idx = aliceBook.sections!.findIndex((s) => s.linear !== 'no'); + const stabilized = waitForStabilized(paginator); + await paginator.goTo({ index: idx }); + await stabilized; + await waitForFillComplete(paginator); + + // Switch to scrolled — triggers full re-layout + const stabilized2 = waitForStabilized(paginator); + paginator.setAttribute('flow', 'scrolled'); + await stabilized2; + + expect(paginator.scrolled).toBe(true); + expect(paginator.primaryIndex).toBe(idx); + }); + }); +}); diff --git a/apps/readest-app/src/__tests__/document/paginator-multiview.browser.test.ts b/apps/readest-app/src/__tests__/document/paginator-multiview.browser.test.ts index 37a7685e..7e869867 100644 --- a/apps/readest-app/src/__tests__/document/paginator-multiview.browser.test.ts +++ b/apps/readest-app/src/__tests__/document/paginator-multiview.browser.test.ts @@ -1,34 +1,11 @@ import { describe, it, expect, beforeAll, afterEach } from 'vitest'; import { DocumentLoader } from '@/libs/document'; import type { BookDoc } from '@/libs/document'; -import type { FoliateView } from '@/types/view'; +import type { FoliateView, Renderer } from '@/types/view'; // Vite serves fixture files; fetch the EPUB at runtime in the browser. const EPUB_URL = new URL('../fixtures/data/sample-alice.epub', import.meta.url).href; -interface PaginatorElement extends HTMLElement { - open: (book: BookDoc) => void; - goTo: (target: { - index: number; - anchor?: number | (() => number); - select?: boolean; - }) => Promise; - prev: () => Promise; - next: () => Promise; - destroy: () => void; - getContents: () => Array<{ index: number; doc: Document; overlayer: unknown }>; - setStyles: (styles: string | [string, string]) => void; - render: () => void; - primaryIndex: number; - pages: number; - page: number; - size: number; - viewSize: number; - scrolled: boolean; - columnCount: number; - sections: Array<{ linear?: string; load: () => Promise }>; -} - let book: BookDoc; const loadEPUB = async () => { @@ -59,7 +36,7 @@ const waitForStabilized = (el: HTMLElement, timeout = 10000) => }); /** Wait until `getContents().length >= n` or timeout. */ -const waitForViews = async (el: PaginatorElement, n: number, timeout = 10000) => { +const waitForViews = async (el: Renderer, n: number, timeout = 10000) => { const start = Date.now(); while (Date.now() - start < timeout) { if (el.getContents().length >= n) return; @@ -68,7 +45,7 @@ const waitForViews = async (el: PaginatorElement, n: number, timeout = 10000) => }; /** Wait for fill to complete by polling until getContents count stabilizes. */ -const waitForFillComplete = async (el: PaginatorElement, timeout = 10000) => { +const waitForFillComplete = async (el: Renderer, timeout = 10000) => { const start = Date.now(); let lastCount = -1; let stableFor = 0; @@ -86,7 +63,7 @@ const waitForFillComplete = async (el: PaginatorElement, timeout = 10000) => { }; describe('Paginator multi-view architecture (browser)', () => { - let paginator: PaginatorElement; + let paginator: Renderer; beforeAll(async () => { book = await loadEPUB(); @@ -94,7 +71,7 @@ describe('Paginator multi-view architecture (browser)', () => { }, 30000); const createPaginator = () => { - const el = document.createElement('foliate-paginator') as PaginatorElement; + const el = document.createElement('foliate-paginator') as Renderer; // The paginator needs non-zero dimensions for layout calculations Object.assign(el.style, { width: '800px', @@ -210,7 +187,7 @@ describe('Paginator multi-view architecture (browser)', () => { await waitForViews(paginator, 2); const contents = paginator.getContents(); const indices = contents.map((c) => c.index); - expect(indices).toEqual([...indices].sort((a, b) => a - b)); + expect(indices).toEqual([...indices].sort((a, b) => (a ?? 0) - (b ?? 0))); }); it('should include the primary section in getContents', async () => { diff --git a/apps/readest-app/src/__tests__/document/paginator-stabilization.browser.test.ts b/apps/readest-app/src/__tests__/document/paginator-stabilization.browser.test.ts index 99190ba0..5bfe9baa 100644 --- a/apps/readest-app/src/__tests__/document/paginator-stabilization.browser.test.ts +++ b/apps/readest-app/src/__tests__/document/paginator-stabilization.browser.test.ts @@ -1,34 +1,11 @@ import { describe, it, expect, beforeAll, afterAll, afterEach } from 'vitest'; import { DocumentLoader } from '@/libs/document'; import type { BookDoc } from '@/libs/document'; +import type { Renderer } from '@/types/view'; // Vite serves fixture files; fetch the EPUB at runtime in the browser. const EPUB_URL = new URL('../fixtures/data/sample-alice.epub', import.meta.url).href; -interface PaginatorElement extends HTMLElement { - open: (book: BookDoc) => void; - goTo: (target: { - index: number; - anchor?: number | (() => number); - select?: boolean; - }) => Promise; - prev: () => Promise; - next: () => Promise; - destroy: () => void; - getContents: () => Array<{ index: number; doc: Document; overlayer: unknown }>; - setStyles: (styles: string | [string, string]) => void; - render: () => void; - scrollToAnchor: (anchor: number | Range, select?: boolean, smooth?: boolean) => Promise; - primaryIndex: number; - pages: number; - page: number; - size: number; - viewSize: number; - scrolled: boolean; - columnCount: number; - sections: Array<{ linear?: string; load: () => Promise }>; -} - let book: BookDoc; const loadEPUB = async () => { @@ -59,7 +36,7 @@ const waitForStabilized = (el: HTMLElement, timeout = 10000) => }); /** Wait until `getContents().length >= n` or timeout. */ -const waitForViews = async (el: PaginatorElement, n: number, timeout = 10000) => { +const waitForViews = async (el: Renderer, n: number, timeout = 10000) => { const start = Date.now(); while (Date.now() - start < timeout) { if (el.getContents().length >= n) return; @@ -68,7 +45,7 @@ const waitForViews = async (el: PaginatorElement, n: number, timeout = 10000) => }; /** Wait for fill to complete by polling until getContents count stabilizes. */ -const waitForFillComplete = async (el: PaginatorElement, timeout = 10000) => { +const waitForFillComplete = async (el: Renderer, timeout = 10000) => { const start = Date.now(); let lastCount = -1; let stableFor = 0; @@ -86,7 +63,7 @@ const waitForFillComplete = async (el: PaginatorElement, timeout = 10000) => { }; describe('Paginator stabilization (browser)', () => { - let paginator: PaginatorElement; + let paginator: Renderer; // Suppress unhandled errors from paginator's #replaceBackground firing // after views are destroyed (queued iframe loads from rapid navigation). @@ -106,7 +83,7 @@ describe('Paginator stabilization (browser)', () => { }); const createPaginator = () => { - const el = document.createElement('foliate-paginator') as PaginatorElement; + const el = document.createElement('foliate-paginator') as Renderer; Object.assign(el.style, { width: '800px', height: '600px', @@ -255,7 +232,7 @@ describe('Paginator stabilization (browser)', () => { await waitForFillComplete(paginator); const stabilizedFromRender = waitForStabilized(paginator); - paginator.render(); + paginator.render?.(); // render() dispatches stabilized in a RAF await stabilizedFromRender; // If we get here, stabilized was emitted @@ -269,7 +246,7 @@ describe('Paginator stabilization (browser)', () => { const indexBefore = paginator.primaryIndex; const stabilized = waitForStabilized(paginator); - paginator.render(); + paginator.render?.(); await stabilized; expect(paginator.primaryIndex).toBe(indexBefore); }); diff --git a/apps/readest-app/src/__tests__/fixtures/data/repro-3583.epub b/apps/readest-app/src/__tests__/fixtures/data/repro-3583.epub new file mode 100644 index 0000000000000000000000000000000000000000..12a17e7137566723ea1456b5b50af97492a18174 GIT binary patch literal 3350 zcmb7G3p|r+7~j}1w~%X5lt>p%g&m5>Dz~`|QH#W^WgE7cNfw1vB1}kAAt^LXsh@jE zIGscyU0jOXg_Fx6<$OytoYeWg-}8O%_xpYC|9zhS^S=M*u{P(22!ps+)UF{<#`uJkx#-qOYe(>F=+q*S=@l6<_KN41<+(*M zUtCX5m&`yGA32E1{v_Y4afUWrETncoyYF67nN9K4Z8qe?M#GH=bQ*4lm>Ogt%XD0! ziVo9q8}-n;gj-(8HvWoeZ)dywV6PAeeU>_4AOM7Jg*3KCPgBSI6?NSC&CoS2&A{Bv zU=V1@?0k|dO_P3@`kk|%0NF=rq08W+p<|Q6ZLGDS;F#E^tEEEQHAN#xS!%@;qRV@= z?ve_Uni?x@tXcgXqsURG4xumrX^E4;wd=9Z^+KochAUjgon`+S9Q8eXJZgXtE|)Zc zapdHi4yrt^0d06yLCY7`_mjVBoD=UX7`<`Rx%AZOPCMIuhql(n zOxopk+gspN2unjg7njDq)OCV~p`LC~*Bz4GGuWk{+=*|?Ht|$lrxQVyExe|E!slWG zxxk!(mMO`g77%>#C{|#o zwTk-YjYD@sdLU=0zh77};(Eu(m#=x<9*1QVYxQmvL_AGR0MkM>v3qfSZd2O=4knGL z7Wp4*`e@jaeC4Xcxc|0Nw0`{Uuu9cSV}UD2{G#);FK>!BH;9neIU%)X*Ilm%)lA4! ztFNcj&C$>fqGAh$sJPz>^jrYh{gl}!UgMaxSr$3TLJ2FenIGE_$-6@-qFwHAw-Ejz z_)JbDlXYNG0s>nNv&nAl)>05ryP^Aar=jwB0#LK)Yw%Oj=lI|fP<(|c?Uh89~yf+__b;Za@msv{I3SItc8PSw_=Y&J!K3Xnj zX}3;rIn`Hki2x^?EdZs3=)636F9%7#5Z{ob_tC^tn&GrzwO`iGr#m_fnj1UGnAd}Y ztdbu((t3o3S!oy)kOCyg0#F#q*(`HK1WTf7dU<<%laMr037!MkJV}Z}TSi@iixv&A z!^zpo!EvvJ!2!*^G^-*z4K0h;5}ebCQO8}D%6sJXtXSt$S{?B0StQz!wfjk=L`ov2 z>@0>Xa4YGK&(1PjUoLy{Mqox!4oRWGdDCt#9QXLRh2k$xb>Vm2zDr?_TZKMUJ%C zY_d&>t~e~^e;n$-+B`K!Kpy-6TRtC#;!5-;U|q;qcTEZnw^#x4jW87tQWgab6sC0Z zudPi28>|*my$@N|`>CEjiioXhWT@S5$oD;!IGzx0V)7(o?80IA;~KQ`9@r%%13s0C zn9avqWrB!49W8tpY%|Mb`T6{@EtgAvD`qs+pYYo;cadqBL+S|k7eKsyz*$k4jrlu_ z2f@n^4u@-Lx=|?KB7StN1fS!);Jlo(enJ9DZsF(kqEfq4hr?n-N;r{n0ii3SorLbZ zZ}BWQBJMzr5If(oSb+n|m(PEg_)GBaem`S`xHALh)SPfyW1mdS0|#gLmXTKb(I%5j z$&qNsqUfg3nyR-QQ+)2`x8p2FmK?MgvZP2Z%O?ot(3O)%!cZqq?O%V_>dWnmiZQ-# z|BOn1RVfpyf(@&TQ>@DQe0F~tRih4f21DjB{HkNUqwA{wn2Lo}U&=>{7z(w;$yD=3fW(2)f&+Qs=JiU8VNVCRu>Ih5l@2U1 z(~JOE+#X^PKNq$lynf!F#o}p976bi}f$!vfdx^~9vIVHZJQdj9Zn21eusOGg^LnWY zR1F@$IVJtq*bD0qFLpU(276950${n-=s$`5Zw|!faKI6mft}U8Uwg{!hvuptuczbl zA?Iu0Vhj9^&*_3QKF{jbVvy6BTc}#RKKIXuoU35~JXgaOunYx41%X6?yAMdcG>rT8 EFIc=>PXGV_ literal 0 HcmV?d00001 diff --git a/apps/readest-app/src/__tests__/fixtures/data/repro-3683.epub b/apps/readest-app/src/__tests__/fixtures/data/repro-3683.epub new file mode 100644 index 0000000000000000000000000000000000000000..82bf9b42b123a7cfad00e717a8e5504fba5a0ade GIT binary patch literal 2066 zcmWIWW@h1H00Ga$x|saL)jA?THVAV7ac*XAYDr~5YGOe_PG)jqNoIbYeriE!l6F;Q zK>&(Qpkg@S1hRZxLmYKI{oM3H${@ypED+ne`OZ3^^dBG=LspiYpI4HYnU`9mSCN|& z+jo%bkb!{9`@dZq!dG6L-LkA{K{)GI=A>O$6Lv|@3i3-vXsj+Y1;R}1)jF|m0VA%AE#L7bn@8x+$voDmNCOgScOYtq37>!#;&tM zGR>3gzx{VSe*0+WS)J9qaZb_Z3O(|NGXAPOeE$3M2R`@Ei*qEBudEk(HYKTl$NTEN z`>!R=-0*f6U;fNSrmt#o{{Bm3AOAeK&*ZFh+m5CJt8@wepZhK@UX(J~_?35l*`>}_ z4)694hU;I59NWFBMPa@9MB`s2{ccWN%unq6o3vS{pVPXO6n7@_kid`0rG+8#KOI;6 zG%)`EsQ5Sgc`LQe9p5S&H>e11;$EFtm>ha9=qg$$6oonMm=5&)F(5{SLUM*7Bousm z9r+F^h`4;8)3hOM-;rL^{UwP>HVtwMPW78ODopX%yEnmHFa6nG9=_A3KbkMCG zx8!-7lxsI7pP915DT2G@{+SPQXWWg?U0hYaYFU_Dt>W`TZPO>rnaHtGYlD^Os`;~O z!Lf}<&x_9mXVm~>QH7a-K@c93pv0V-SE83+kT!RcZ+?q{K->G@+8o#2F222fZN=WN z>?UuQnsYt&>T(eL{`h{mOZnYx%gq$q8Y}0|tlU^*Q!?w#f$yhgbv?^;0c4+y^bGczt40q=W_TK-h-k&Z@>Qz@w~p|TK-L|)9+@M^2QciSI#qdRovj> zY2aweRpZL8nz>hL$s2`&lg#sXw2Gu}k>1l6p(&B^BH+$uo3oK3i$c1R9EDWVUp??O ze6a4s_3P%_%fHV~x%1~o&vX7iu~Xhirz*0Gnp`>mV2`fHgZENGXZy=ITq5T5Wn{;S z_;;qyeU@guz~6L6MQz{mp2C4a_-;;E6MKz@*!uM! zcr|{rW*2v{8?QO*uklHK#>3sLZ3p8M)bERY5$-rUEoMi+H$TZ8E4o@Yd`LR7eVU$r z;i;MC%3fywKYe}0jau|fi*!FJ0*tEDK#Uw!WuP(#lZU-()}PTlj+@tKDYoTvw@m70 z=!=gW>o#P@x?j3j0_5;f2INv-+w zh;8aF302!m*EF}P-i_VHQD+zR!Bo~Te&3RXA6QlxZQGHMt+VJX|9Sc2;T4q?R$TY= zT;9#xe8B2uYt4ts56l7Hj7)OOxGM-?k_Uq&jUWnEU7+iNsDY56$^ild7~VQs0hvfu z1;{c)=0dM5K-z$j$FQU^5m_5b)q&4yP;m?aAgi5$M!^baRD02jXOJF Range; +export interface Renderer extends HTMLElement { + scrolled?: boolean; + scrollLocked: boolean; + size: number; // current page height + viewSize: number; // whole document view height + start: number; + end: number; + page: number; // section page index (0-based) + pages: number; // section page count + atStart: boolean; + atEnd: boolean; + containerPosition: number; + sideProp: 'width' | 'height'; + pageColors?: { + background: string; + foreground: string; + }; + columnCount?: number; + open: (book: BookDoc) => Promise; + setAttribute: (name: string, value: string | number) => void; + removeAttribute: (name: string) => void; + next: () => Promise; + prev: () => Promise; + nextSection?: () => Promise; + prevSection?: () => Promise; + render?: () => Promise; + goTo: (params: { index: number; anchor?: number | RangeAnchor }) => void; + setStyles?: (css: string) => void; + primaryIndex: number; + getContents: () => { doc: Document; index?: number; overlayer?: unknown }[]; + scrollToAnchor?: (anchor: number | Range, reason?: string, smooth?: boolean) => void; + addEventListener: ( + type: string, + listener: EventListener, + option?: AddEventListenerOptions, + ) => void; + removeEventListener: (type: string, listener: EventListener) => void; + showLoupe?: ( + x: number, + y: number, + options: { + isVertical: boolean; + color: string; + gap: number; + margin: number; + radius: number; + magnification: number; + }, + ) => void; + hideLoupe?: () => void; + destroyLoupe?: () => void; + pinchZoom?: (ratio: number) => void; + pinchEnd?: () => void; + destroy: () => void; +} + export interface FoliateView extends HTMLElement { open: (book: BookDoc) => Promise; close: () => void; @@ -59,57 +115,7 @@ export interface FoliateView extends HTMLElement { forward: () => void; clear: () => void; }; - renderer: { - scrolled?: boolean; - scrollLocked: boolean; - size: number; // current page height - viewSize: number; // whole document view height - start: number; - end: number; - page: number; // section page index (0-based) - pages: number; // section page count - atStart: boolean; - atEnd: boolean; - containerPosition: number; - sideProp: 'width' | 'height'; - pageColors?: { - background: string; - foreground: string; - }; - setAttribute: (name: string, value: string | number) => void; - removeAttribute: (name: string) => void; - next: () => Promise; - prev: () => Promise; - nextSection?: () => Promise; - prevSection?: () => Promise; - goTo?: (params: { index: number; anchor?: number | RangeAnchor }) => void; - setStyles?: (css: string) => void; - primaryIndex: number; - getContents: () => { doc: Document; index?: number; overlayer?: unknown }[]; - scrollToAnchor?: (anchor: number | Range, reason?: string, smooth?: boolean) => void; - addEventListener: ( - type: string, - listener: EventListener, - option?: AddEventListenerOptions, - ) => void; - removeEventListener: (type: string, listener: EventListener) => void; - showLoupe?: ( - x: number, - y: number, - options: { - isVertical: boolean; - color: string; - gap: number; - margin: number; - radius: number; - magnification: number; - }, - ) => void; - hideLoupe?: () => void; - destroyLoupe?: () => void; - pinchZoom?: (ratio: number) => void; - pinchEnd?: () => void; - }; + renderer: Renderer; } export const wrappedFoliateView = (originalView: FoliateView): FoliateView => { diff --git a/packages/foliate-js b/packages/foliate-js index 7de55d4e..2300db0d 160000 --- a/packages/foliate-js +++ b/packages/foliate-js @@ -1 +1 @@ -Subproject commit 7de55d4ebfff3264abc638586212e380e934cae1 +Subproject commit 2300db0dee97dd4879e1c50e16464a56c9161669