fix(reader): report frame index in fixed-layout contents

Ensure that fixed-layout EPUB content structures expose their frame index and overlayer attributes during retrieval. This prevents the navigation subsystem from falling back to the final page of the document due to unverified page position indexes.

Closes #541
This commit is contained in:
neon
2026-07-04 17:54:24 +00:00
parent 784d390671
commit 68c8ab2f49
2 changed files with 75 additions and 7 deletions
+22 -7
View File
@@ -85,7 +85,7 @@ export class FixedLayout extends HTMLElement {
iframe.setAttribute('scrolling', 'no')
iframe.setAttribute('part', 'filter')
this.#root.append(element)
if (!src) return { blank: true, element, iframe }
if (!src) return { index, blank: true, element, iframe }
return new Promise((resolve) => {
iframe.addEventListener(
'load',
@@ -93,13 +93,24 @@ export class FixedLayout extends HTMLElement {
const doc = iframe.contentDocument
this.dispatchEvent(new CustomEvent('load', { detail: { doc, index } }))
const { width, height } = getViewport(doc, this.defaultViewport)
resolve({
const frame = {
index,
element,
iframe,
width: parseFloat(width),
height: parseFloat(height),
onZoom,
})
}
this.dispatchEvent(
new CustomEvent('create-overlayer', {
detail: {
doc,
index,
attach: (overlayer) => (frame.overlayer = overlayer),
},
}),
)
resolve(frame)
},
{ once: true },
)
@@ -301,10 +312,14 @@ export class FixedLayout extends HTMLElement {
if (!s) return this.goToSpread(this.#index - 1, this.rtl ? 'left' : 'right', 'page')
}
getContents() {
return Array.from(this.#root.querySelectorAll('iframe'), (frame) => ({
doc: frame.contentDocument,
// TODO: index, overlayer
}))
const frames = this.#center ? [this.#center] : [this.#left, this.#right]
return frames
.filter((frame) => frame && !frame.blank && typeof frame.index === 'number' && frame.index >= 0 && frame.iframe?.contentDocument)
.map((frame) => ({
index: frame.index,
overlayer: frame.overlayer,
doc: frame.iframe.contentDocument,
}))
}
destroy() {
this.#observer.unobserve(this)
@@ -45,6 +45,11 @@ describe('Foliate navigation', () => {
let getKoreaderDocFragmentIndex: (sections: { id: string }[], index: number) => number | null
let getKoboSpanValue: (range: Range | null) => string | null
let getPageProgressionRtl: (bookDir: string | null | undefined, contentRtl: boolean) => boolean
let FixedLayout: new () => HTMLElement & {
open: (book: { rendition: { layout: string; spread: string }; sections: { load: () => Promise<string> }[] }) => void
goTo: (target: { index: number }) => Promise<void>
getContents: () => { index?: number; doc?: Document }[]
}
let Paginator: new () => {
sections: { load: () => Promise<string | null> }[]
goTo: (target: { index: number }) => Promise<void>
@@ -54,6 +59,7 @@ describe('Foliate navigation', () => {
installBrowserGlobals()
const viewModulePath = '../../../../../public/assets/foliate/view.js'
const fixedLayoutModulePath = '../../../../../public/assets/foliate/fixed-layout.js'
const paginatorModulePath = '../../../../../public/assets/foliate/paginator.js'
;({ View, getKoreaderProgress, getKoreaderDocFragmentIndex, getKoboSpanValue } = (await import(viewModulePath)) as {
View: typeof View
@@ -61,6 +67,7 @@ describe('Foliate navigation', () => {
getKoreaderDocFragmentIndex: typeof getKoreaderDocFragmentIndex
getKoboSpanValue: typeof getKoboSpanValue
})
;({ FixedLayout } = (await import(fixedLayoutModulePath)) as { FixedLayout: typeof FixedLayout })
;({ Paginator, getPageProgressionRtl } = (await import(paginatorModulePath)) as {
Paginator: typeof Paginator
getPageProgressionRtl: typeof getPageProgressionRtl
@@ -117,6 +124,52 @@ describe('Foliate navigation', () => {
await expect(paginator.goTo({ index: 0 })).rejects.toThrow('Failed to load section 0')
})
it('reports fixed-layout frame indexes for navigation success checks', async () => {
const originalCreateElement = document.createElement.bind(document)
const createElementSpy = vi.spyOn(document, 'createElement').mockImplementation((tagName: string) => {
const element = originalCreateElement(tagName)
if (tagName === 'iframe') {
const frameDoc = document.implementation.createHTMLDocument('fixed-layout-frame')
frameDoc.head.innerHTML = '<meta name="viewport" content="width=100,height=200">'
Object.defineProperty(element, 'contentDocument', { configurable: true, value: frameDoc })
Object.defineProperty(element, 'src', {
configurable: true,
get() {
return this.getAttribute('src') ?? ''
},
set(value) {
this.setAttribute('src', value)
queueMicrotask(() => this.dispatchEvent(new window.Event('load')))
},
})
}
return element
})
const fixedLayout = new FixedLayout()
document.body.append(fixedLayout)
try {
fixedLayout.open({
rendition: { layout: 'pre-paginated', spread: 'none' },
sections: [
{ load: vi.fn<() => Promise<string>>().mockResolvedValue('section-0.xhtml') },
{ load: vi.fn<() => Promise<string>>().mockResolvedValue('section-1.xhtml') },
],
})
await fixedLayout.goTo({ index: 0 })
const contents = fixedLayout.getContents()
expect(contents).toHaveLength(1)
expect(contents[0]).toMatchObject({ index: 0 })
expect(contents[0]?.doc?.documentElement).toBeTruthy()
} finally {
fixedLayout.remove()
createElementSpy.mockRestore()
}
})
it('uses explicit OPF page progression before content direction', () => {
expect(getPageProgressionRtl('rtl', false)).toBe(true)
expect(getPageProgressionRtl('ltr', true)).toBe(false)