Use Shadow DOM

This commit is contained in:
John Factotum
2023-05-15 20:00:35 +08:00
parent 4acfc050ea
commit 0a3f4b8f27
5 changed files with 182 additions and 178 deletions
+15 -18
View File
@@ -51,8 +51,7 @@ The repo also includes a still higher level reader, though strictly speaking, `r
### Basic Usage ### Basic Usage
```js ```js
import { View } from './view.js' import from './view.js'
customElements.define('foliate-view', View)
const view = document.createElement('foliate-view') const view = document.createElement('foliate-view')
document.body.append(view) document.body.append(view)
@@ -154,7 +153,15 @@ The paginator uses the same pagination strategy as [Epub.js](https://github.com/
To simplify things, it has a totally separate renderer for fixed layout books. As such there's no support for mixed layout books. To simplify things, it has a totally separate renderer for fixed layout books. As such there's no support for mixed layout books.
Both renderers has the style class `.foliate-filter`, which you can apply CSS filters to, to e.g. invert colors or adjust brightness. By using this class, you can apply filters only to the book itself, leaving overlaid elements such as highlights unaffected. Both renderers have the [`part`](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/part) named `filter`, which you can apply CSS filters to, to e.g. invert colors or adjust brightness:
```css
foliate-view::part(filter) {
filter: invert(1) hue-rotate(180deg);
}
```
The filter only applies to the book itself, leaving overlaid elements such as highlights unaffected.
### The Paginator ### The Paginator
@@ -165,24 +172,14 @@ The layout can be configured from the `.layout` object, which has the following
- `.maxColumnWidth`: number, in pixels. The maximum width of the text in each column. - `.maxColumnWidth`: number, in pixels. The maximum width of the text in each column.
- `.maxColumns`: integer. The maximum number of columns. Has no effect in scrolled mode. - `.maxColumns`: integer. The maximum number of columns. Has no effect in scrolled mode.
It has built-in header and footer regions accessible via the `.heads` and `.feet` properties of the paginator instance. These can be used to display running heads and reading progress. They are only available in paginated mode, and there will be one element for each column. They are contained in parent divs that have the style classes `.foliate-header` and `.foliate-footer` respectively. These don't have any styles by default, but you will probably want to set something like the following: It has built-in header and footer regions accessible via the `.heads` and `.feet` properties of the paginator instance. These can be used to display running heads and reading progress. They are only available in paginated mode, and there will be one element for each column. They are styleable with `::part(head)` and `::part(foot)`. E.g., to add a border under the running heads,
```css ```css
.foliate-header > *, .foliate-footer > * { foliate-view::part(head) {
display: flex; padding-bottom: 4px;
min-width: 0; border-bottom: 1px solid graytext;
align-items: center;
text-align: center;
font-size: .75em;
opacity: .6;
} }
.foliate-header > * > *, .foliate-footer > * > * { ``
flex: 1;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
```
### EPUB CFI ### EPUB CFI
+49 -58
View File
@@ -29,9 +29,11 @@ const getViewport = (doc, viewport) => {
return { width: 1000, height: 2000 } return { width: 1000, height: 2000 }
} }
class Container { export class FixedLayout extends HTMLElement {
#observer = new ResizeObserver(() => this.render()) #root = this.attachShadow({ mode: 'closed' })
#element = document.createElement('div') #observer = new ResizeObserver(() => this.#render())
#spreads
#index = -1
defaultViewport defaultViewport
spread spread
#portrait = false #portrait = false
@@ -40,20 +42,19 @@ class Container {
#center #center
#side #side
constructor() { constructor() {
Object.assign(this.#element.style, { super()
width: '100%',
height: '100%', const sheet = new CSSStyleSheet()
display: 'flex', this.#root.adoptedStyleSheets = [sheet]
justifyContent: 'center', sheet.replaceSync(`:host {
alignItems: 'center', width: 100%;
}) height: 100%;
this.#observer.observe(this.#element) display: flex;
} justify-content: center;
get element() { align-items: center;
return this.#element }`)
}
get side() { this.#observer.observe(this)
return this.#side
} }
async #createFrame({ index, src }) { async #createFrame({ index, src }) {
const element = document.createElement('div') const element = document.createElement('div')
@@ -68,14 +69,14 @@ class Container {
// https://bugs.webkit.org/show_bug.cgi?id=218086 // https://bugs.webkit.org/show_bug.cgi?id=218086
iframe.setAttribute('sandbox', 'allow-same-origin allow-scripts') iframe.setAttribute('sandbox', 'allow-same-origin allow-scripts')
iframe.setAttribute('scrolling', 'no') iframe.setAttribute('scrolling', 'no')
iframe.classList.add('foliate-filter') iframe.setAttribute('part', 'filter')
this.#element.append(element) this.#root.append(element)
if (!src) return { blank: true, element, iframe } if (!src) return { blank: true, element, iframe }
return new Promise(resolve => { return new Promise(resolve => {
const onload = () => { const onload = () => {
iframe.removeEventListener('load', onload) iframe.removeEventListener('load', onload)
const doc = iframe.contentDocument const doc = iframe.contentDocument
this.onLoad?.(doc, index) this.dispatchEvent(new CustomEvent('load', { detail: { doc, index } }))
const { width, height } = getViewport(doc, this.defaultViewport) const { width, height } = getViewport(doc, this.defaultViewport)
resolve({ resolve({
element, iframe, element, iframe,
@@ -87,12 +88,12 @@ class Container {
iframe.src = src iframe.src = src
}) })
} }
render(side = this.#side) { #render(side = this.#side) {
if (!side) return if (!side) return
const left = this.#left ?? {} const left = this.#left ?? {}
const right = this.#center ?? this.#right const right = this.#center ?? this.#right
const target = side === 'left' ? left : right const target = side === 'left' ? left : right
const { width, height } = this.#element.getBoundingClientRect() const { width, height } = this.getBoundingClientRect()
const portrait = this.spread !== 'both' && this.spread !== 'portrait' const portrait = this.spread !== 'both' && this.spread !== 'portrait'
&& height > width && height > width
this.#portrait = portrait this.#portrait = portrait
@@ -135,23 +136,23 @@ class Container {
transform(right) transform(right)
} }
} }
async showSpread({ left, right, center, side }) { async #showSpread({ left, right, center, side }) {
this.#element.replaceChildren() this.#root.replaceChildren()
this.#left = null this.#left = null
this.#right = null this.#right = null
this.#center = null this.#center = null
if (center) { if (center) {
this.#center = await this.#createFrame(center) this.#center = await this.#createFrame(center)
this.#side = 'center' this.#side = 'center'
this.render() this.#render()
} else { } else {
this.#left = await this.#createFrame(left) this.#left = await this.#createFrame(left)
this.#right = await this.#createFrame(right) this.#right = await this.#createFrame(right)
this.#side = side this.#side = side
this.render() this.#render()
} }
} }
goLeft() { #goLeft() {
if (this.#center) return if (this.#center) return
if (this.#left?.blank) return true if (this.#left?.blank) return true
if (this.#portrait && this.#left?.element?.style?.display === 'none') { if (this.#portrait && this.#left?.element?.style?.display === 'none') {
@@ -161,7 +162,7 @@ class Container {
return true return true
} }
} }
goRight() { #goRight() {
if (this.#center) return if (this.#center) return
if (this.#right?.blank) return true if (this.#right?.blank) return true
if (this.#portrait && this.#right?.element?.style?.display === 'none') { if (this.#portrait && this.#right?.element?.style?.display === 'none') {
@@ -171,24 +172,11 @@ class Container {
return true return true
} }
} }
destroy() { open(book) {
this.#observer.unobserve(this.#element)
this.#element.remove()
}
}
export class FixedLayout {
#spreads
#index = -1
#container = new Container()
constructor({ book, onLoad, onRelocate }) {
this.book = book this.book = book
this.#container.onLoad = onLoad
this.onRelocate = onRelocate
const { rendition } = book const { rendition } = book
this.#container.spread = rendition?.spread this.spread = rendition?.spread
this.#container.defaultViewport = rendition?.viewport this.defaultViewport = rendition?.viewport
const rtl = book.dir === 'rtl' const rtl = book.dir === 'rtl'
const ltr = !rtl const ltr = !rtl
@@ -227,15 +215,16 @@ export class FixedLayout {
return arr return arr
}, [{}]) }, [{}])
} }
get element() {
return this.#container.element
}
get index() { get index() {
const spread = this.#spreads[this.#index] const spread = this.#spreads[this.#index]
const section = spread?.center ?? (this.#container.side === 'left' const section = spread?.center ?? (this.side === 'left'
? spread.left ?? spread.right : spread.right ?? spread.left) ? spread.left ?? spread.right : spread.right ?? spread.left)
return this.book.sections.indexOf(section) return this.book.sections.indexOf(section)
} }
#reportLocation() {
this.dispatchEvent(new CustomEvent('relocate', { detail:
{ range: null, index: this.index, fraction: 0, size: 1 } }))
}
getSpreadOf(section) { getSpreadOf(section) {
const spreads = this.#spreads const spreads = this.#spreads
for (let index = 0; index < spreads.length; index++) { for (let index = 0; index < spreads.length; index++) {
@@ -248,7 +237,7 @@ export class FixedLayout {
async goToSpread(index, side) { async goToSpread(index, side) {
if (index < 0 || index > this.#spreads.length - 1) return if (index < 0 || index > this.#spreads.length - 1) return
if (index === this.#index) { if (index === this.#index) {
this.#container.render(side) this.#render(side)
return return
} }
this.#index = index this.#index = index
@@ -256,7 +245,7 @@ export class FixedLayout {
if (spread.center) { if (spread.center) {
const index = this.book.sections.indexOf(spread.center) const index = this.book.sections.indexOf(spread.center)
const src = await spread.center?.load?.() const src = await spread.center?.load?.()
await this.#container.showSpread({ center: { index, src } }) await this.#showSpread({ center: { index, src } })
} else { } else {
const indexL = this.book.sections.indexOf(spread.left) const indexL = this.book.sections.indexOf(spread.left)
const indexR = this.book.sections.indexOf(spread.right) const indexR = this.book.sections.indexOf(spread.right)
@@ -264,9 +253,9 @@ export class FixedLayout {
const srcR = await spread.right?.load?.() const srcR = await spread.right?.load?.()
const left = { index: indexL, src: srcL } const left = { index: indexL, src: srcL }
const right = { index: indexR, src: srcR } const right = { index: indexR, src: srcR }
await this.#container.showSpread({ left, right, side }) await this.#showSpread({ left, right, side })
} }
this.onRelocate?.(null, this.index, 0, 1) this.#reportLocation()
} }
async select(target) { async select(target) {
await this.goTo(target) await this.goTo(target)
@@ -281,20 +270,22 @@ export class FixedLayout {
await this.goToSpread(index, side) await this.goToSpread(index, side)
} }
async next() { async next() {
const s = this.rtl ? this.#container.goLeft() : this.#container.goRight() const s = this.rtl ? this.#goLeft() : this.#goRight()
if (s) this.onRelocate?.(null, this.index, 0, 1) if (s) this.#reportLocation()
else return this.goToSpread(this.#index + 1, this.rtl ? 'right' : 'left') else return this.goToSpread(this.#index + 1, this.rtl ? 'right' : 'left')
} }
async prev() { async prev() {
const s = this.rtl ? this.#container.goRight() : this.#container.goLeft() const s = this.rtl ? this.#goRight() : this.#goLeft()
if (s) this.onRelocate?.(null, this.index, 0, 1) if (s) this.#reportLocation()
else return this.goToSpread(this.#index - 1, this.rtl ? 'left' : 'right') else return this.goToSpread(this.#index - 1, this.rtl ? 'left' : 'right')
} }
deselect() { deselect() {
for (const frame of this.#container.element.querySelectorAll('iframe')) for (const frame of this.#root.querySelectorAll('iframe'))
frame.contentWindow.getSelection().removeAllRanges() frame.contentWindow.getSelection().removeAllRanges()
} }
destroy() { destroy() {
this.#container.destroy() this.#observer.unobserve(this)
} }
} }
customElements.define('foliate-fxl', FixedLayout)
+100 -85
View File
@@ -128,10 +128,11 @@ const getBackground = doc => {
: bodyStyle.background : bodyStyle.background
} }
const makeMarginals = length => Array.from({ length }, () => { const makeMarginals = (length, part) => Array.from({ length }, () => {
const div = document.createElement('div') const div = document.createElement('div')
const child = document.createElement('div') const child = document.createElement('div')
div.append(child) div.append(child)
child.setAttribute('part', part)
return div return div
}) })
@@ -148,7 +149,7 @@ class View {
constructor({ container, onExpand }) { constructor({ container, onExpand }) {
this.container = container this.container = container
this.onExpand = onExpand this.onExpand = onExpand
this.#iframe.classList.add('foliate-filter') this.#iframe.setAttribute('part', 'filter')
this.#element.append(this.#iframe) this.#element.append(this.#iframe)
Object.assign(this.#element.style, { Object.assign(this.#element.style, {
boxSizing: 'content-box', boxSizing: 'content-box',
@@ -328,17 +329,17 @@ class View {
} }
// NOTE: everything here assumes the so-called "negative scroll type" for RTL // NOTE: everything here assumes the so-called "negative scroll type" for RTL
export class Paginator { export class Paginator extends HTMLElement {
#root = this.attachShadow({ mode: 'closed' })
#gap = 0 #gap = 0
#shouldUpdateGap = true #shouldUpdateGap = true
#observer = new ResizeObserver(() => this.render()) #observer = new ResizeObserver(() => this.render())
#element = document.createElement('div') #background
#background = document.createElement('div') #maxWidthContainer
#maxWidthContainer = document.createElement('div') #maxHeightContainer
#maxHeightContainer = document.createElement('div') #container
#container = document.createElement('div') #header
#header = document.createElement('div') #footer
#footer = document.createElement('div')
#view #view
#vertical = false #vertical = false
#rtl = false #rtl = false
@@ -351,75 +352,82 @@ export class Paginator {
gap: 0.05, gap: 0.05,
maxColumnWidth: 700, maxColumnWidth: 700,
} }
constructor({ book, onLoad, onRelocate, createOverlayer }) { constructor() {
this.bookDir = book.dir super()
this.sections = book.sections this.#root.innerHTML = `<style>
this.onLoad = onLoad :host {
this.onRelocate = onRelocate box-sizing: border-box;
this.createOverlayer = createOverlayer position: relative;
Object.assign(this.#element.style, { overflow: hidden;
boxSizing: 'border-box', display: flex;
display: 'flex',
width: '100%',
height: '100%',
position: 'relative',
overflow: 'hidden',
})
this.#element.append(this.#background)
Object.assign(this.#background.style, {
width: '100%',
height: '100%',
position: 'absolute',
top: '0', left: '0',
})
this.#background.classList.add('foliate-filter')
this.#element.append(this.#maxWidthContainer)
Object.assign(this.#maxWidthContainer.style, {
width: '100%',
height: '100%',
margin: 'auto',
position: 'relative',
display: 'flex',
})
this.#maxWidthContainer.append(this.#maxHeightContainer)
Object.assign(this.#maxHeightContainer.style, {
width: '100%',
height: '100%',
margin: 'auto',
})
this.#maxHeightContainer.append(this.#container)
Object.assign(this.#container.style, {
width: '100%',
height: '100%',
margin: 'auto',
})
const marginalStyle = {
position: 'absolute', left: '0', right: '0',
display: 'grid',
margin: 'auto',
} }
Object.assign(this.#header.style, marginalStyle) #background {
Object.assign(this.#footer.style, marginalStyle) position: absolute;
this.#header.classList.add('foliate-header') top: 0;
this.#footer.classList.add('foliate-footer') left: 0;
this.#maxWidthContainer.append(this.#header) }
this.#maxWidthContainer.append(this.#footer) #max-width {
position: relative;
display: flex;
}
:host, #background, #max-width, #max-height, #container {
width: 100%;
height: 100%;
margin: auto;
}
#header, #footer {
position: absolute;
left: 0;
right: 0;
display: grid;
margin: auto;
z-index: 1;
}
:is(#header, #footer) > * {
display: flex;
align-items: center;
minWidth: 0;
}
:is(#header, #footer) > * > * {
width: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
text-align: center;
font-size: .75em;
opacity: .6;
}
</style>
<div id="background" part="filter"></div>
<div id="max-width">
<div id="header"></div>
<div id="max-height">
<div id="container"></div>
</div>
<div id="footer"></div>
</div>
`
this.#observer.observe(this.#element) this.#background = this.#root.getElementById('background')
this.#maxWidthContainer = this.#root.getElementById('max-width')
this.#maxHeightContainer = this.#root.getElementById('max-height')
this.#container = this.#root.getElementById('container')
this.#header = this.#root.getElementById('header')
this.#footer = this.#root.getElementById('footer')
this.#observer.observe(this)
this.#container.addEventListener('scroll', debounce(() => { this.#container.addEventListener('scroll', debounce(() => {
if (this.scrolled) this.#afterScroll('scroll') if (this.scrolled) this.#afterScroll('scroll')
}, 250)) }, 250))
} }
get element() { open(book) {
return this.#element this.bookDir = book.dir
this.sections = book.sections
} }
#createView() { #createView() {
if (this.#view) this.#container.removeChild(this.#view.element) if (this.#view) this.#container.removeChild(this.#view.element)
this.#view = new View({ this.#view = new View({
container: this.#element, container: this,
onExpand: this.#scrollToAnchor.bind(this), onExpand: this.#scrollToAnchor.bind(this),
}) })
this.#container.append(this.#view.element) this.#container.append(this.#view.element)
@@ -437,8 +445,8 @@ export class Paginator {
if (flow === 'scrolled') { if (flow === 'scrolled') {
// FIXME: vertical-rl only, not -lr // FIXME: vertical-rl only, not -lr
this.#element.setAttribute('dir', vertical ? 'rtl' : 'ltr') this.setAttribute('dir', vertical ? 'rtl' : 'ltr')
this.#element.style.padding = '0' this.style.padding = '0'
this.#container.style.overflow ='scroll' this.#container.style.overflow ='scroll'
this.#maxWidthContainer.style.maxWidth = 'none' this.#maxWidthContainer.style.maxWidth = 'none'
this.#maxHeightContainer.style.maxHeight = 'none' this.#maxHeightContainer.style.maxHeight = 'none'
@@ -473,7 +481,7 @@ export class Paginator {
? Math.max(margin - gap / 2, gap / 2) ? Math.max(margin - gap / 2, gap / 2)
: margin}px` : margin}px`
this.#gap = gap this.#gap = gap
this.#element.style.padding = `${paddingV} ${paddingH}` this.style.padding = `${paddingV} ${paddingH}`
this.#header.style.top = `-${paddingV}` this.#header.style.top = `-${paddingV}`
this.#footer.style.bottom = `-${paddingV}` this.#footer.style.bottom = `-${paddingV}`
@@ -491,7 +499,7 @@ export class Paginator {
const size = vertical ? height : width const size = vertical ? height : width
const divisor = Math.ceil(size / maxColumnWidth) const divisor = Math.ceil(size / maxColumnWidth)
const columnWidth = (size / divisor) - gap const columnWidth = (size / divisor) - gap
this.#element.setAttribute('dir', rtl ? 'rtl' : 'ltr') this.setAttribute('dir', rtl ? 'rtl' : 'ltr')
this.#container.style.overflow ='hidden' this.#container.style.overflow ='hidden'
const marginalDivisor = vertical const marginalDivisor = vertical
@@ -507,8 +515,8 @@ export class Paginator {
} }
Object.assign(this.#header.style, marginalStyle) Object.assign(this.#header.style, marginalStyle)
Object.assign(this.#footer.style, marginalStyle) Object.assign(this.#footer.style, marginalStyle)
const heads = makeMarginals(marginalDivisor) const heads = makeMarginals(marginalDivisor, 'head')
const feet = makeMarginals(marginalDivisor) const feet = makeMarginals(marginalDivisor, 'foot')
this.heads = heads.map(el => el.children[0]) this.heads = heads.map(el => el.children[0])
this.feet = feet.map(el => el.children[0]) this.feet = feet.map(el => el.children[0])
this.#header.replaceChildren(...heads) this.#header.replaceChildren(...heads)
@@ -657,13 +665,15 @@ export class Paginator {
if (reason !== 'anchor') this.#anchor = range if (reason !== 'anchor') this.#anchor = range
const index = this.#index const index = this.#index
if (this.scrolled) const detail = { range, index }
this.onRelocate?.(range, index, this.start / this.viewSize) if (this.scrolled) detail.fraction = this.start / this.viewSize
else if (this.pages > 0) { else if (this.pages > 0) {
const { page, pages } = this const { page, pages } = this
this.#header.style.visibility = page > 0 ? 'visible' : 'hidden' this.#header.style.visibility = page > 0 ? 'visible' : 'hidden'
this.onRelocate?.(range, index, page / pages, 1 / pages) detail.fraction = page / pages
detail.size = 1 / pages
} }
this.dispatchEvent(new CustomEvent('relocate', { detail }))
} }
async #display(promise) { async #display(promise) {
const { index, src, anchor, onLoad, select } = await promise const { index, src, anchor, onLoad, select } = await promise
@@ -678,12 +688,16 @@ export class Paginator {
doc.head.append($style) doc.head.append($style)
this.#styleMap.set(doc, [$styleBefore, $style]) this.#styleMap.set(doc, [$styleBefore, $style])
} }
onLoad?.(doc, index) onLoad?.({ doc, index })
} }
const beforeRender = this.#beforeRender.bind(this) const beforeRender = this.#beforeRender.bind(this)
await view.load(src, afterLoad, beforeRender) await view.load(src, afterLoad, beforeRender)
const overlayer = this.createOverlayer?.(view.document, index) this.dispatchEvent(new CustomEvent('create-overlayer', {
if (overlayer) view.overlayer = overlayer detail: {
doc: view.document, index,
attach: overlayer => view.overlayer = overlayer,
},
}))
this.#view = view this.#view = view
} }
this.#anchor = (typeof anchor === 'function' this.#anchor = (typeof anchor === 'function'
@@ -732,9 +746,9 @@ export class Paginator {
if (index === this.#index) await this.#display({ index, anchor, select }) if (index === this.#index) await this.#display({ index, anchor, select })
else { else {
const oldIndex = this.#index const oldIndex = this.#index
const onLoad = (...args) => { const onLoad = detail => {
this.sections[oldIndex]?.unload?.() this.sections[oldIndex]?.unload?.()
this.onLoad?.(...args) this.dispatchEvent(new CustomEvent('load', { detail }))
} }
await this.#display(Promise.resolve(this.sections[index].load()) await this.#display(Promise.resolve(this.sections[index].load())
.then(src => ({ index, src, anchor, onLoad, select })) .then(src => ({ index, src, anchor, onLoad, select }))
@@ -801,7 +815,8 @@ export class Paginator {
sel.removeAllRanges() sel.removeAllRanges()
} }
destroy() { destroy() {
this.#observer.unobserve(this.#element) this.#observer.unobserve(this)
this.#element.remove()
} }
} }
customElements.define('foliate-paginator', Paginator)
+1 -3
View File
@@ -1,9 +1,7 @@
import { View } from './view.js' import './view.js'
import { createTOCView } from './ui/tree.js' import { createTOCView } from './ui/tree.js'
import { createMenu } from './ui/menu.js' import { createMenu } from './ui/menu.js'
customElements.define('foliate-view', View)
const isZip = async file => { const isZip = async file => {
const arr = new Uint8Array(await file.slice(0, 4).arrayBuffer()) const arr = new Uint8Array(await file.slice(0, 4).arrayBuffer())
return arr[0] === 0x50 && arr[1] === 0x4b && arr[2] === 0x03 && arr[3] === 0x04 return arr[0] === 0x50 && arr[1] === 0x4b && arr[2] === 0x03 && arr[3] === 0x04
+17 -14
View File
@@ -41,6 +41,7 @@ const languageInfo = lang => {
} }
export class View extends HTMLElement { export class View extends HTMLElement {
#root = this.attachShadow({ mode: 'closed' })
#sectionProgress #sectionProgress
#tocProgress #tocProgress
#pageProgress #pageProgress
@@ -61,21 +62,21 @@ export class View extends HTMLElement {
toc: book.pageList ?? [], ids, splitHref, getFragment }) toc: book.pageList ?? [], ids, splitHref, getFragment })
} }
const opts = {
book: this.book,
onLoad: this.#onLoad.bind(this),
onRelocate: this.#onRelocate.bind(this),
createOverlayer: this.#createOverlayer.bind(this),
}
this.isFixedLayout = this.book.rendition?.layout === 'pre-paginated' this.isFixedLayout = this.book.rendition?.layout === 'pre-paginated'
if (this.isFixedLayout) { if (this.isFixedLayout) {
const { FixedLayout } = await import('./fixed-layout.js') await import('./fixed-layout.js')
this.renderer = new FixedLayout(opts) this.renderer = document.createElement('foliate-fxl')
} else { } else {
const { Paginator } = await import('./paginator.js') await import('./paginator.js')
this.renderer = new Paginator(opts) this.renderer = document.createElement('foliate-paginator')
} }
this.append(this.renderer.element) this.renderer.setAttribute('exportparts', 'head,foot,filter')
this.renderer.addEventListener('load', e => this.#onLoad(e.detail))
this.renderer.addEventListener('relocate', e => this.#onRelocate(e.detail))
this.renderer.addEventListener('create-overlayer', e =>
e.detail.attach(this.#createOverlayer(e.detail)))
this.renderer.open(book)
this.#root.append(this.renderer)
} }
async init({ lastLocation }) { async init({ lastLocation }) {
if (lastLocation) { if (lastLocation) {
@@ -87,7 +88,7 @@ export class View extends HTMLElement {
#emit(name, detail, cancelable) { #emit(name, detail, cancelable) {
return this.dispatchEvent(new CustomEvent(name, { detail, cancelable })) return this.dispatchEvent(new CustomEvent(name, { detail, cancelable }))
} }
#onRelocate(range, index, fraction, size) { #onRelocate({ range, index, fraction, size }) {
if (!this.#sectionProgress) return if (!this.#sectionProgress) return
const progress = this.#sectionProgress.getProgress(index, fraction, size) const progress = this.#sectionProgress.getProgress(index, fraction, size)
const tocItem = this.#tocProgress.getProgress(index, range) const tocItem = this.#tocProgress.getProgress(index, range)
@@ -95,7 +96,7 @@ export class View extends HTMLElement {
const cfi = this.getCFI(index, range) const cfi = this.getCFI(index, range)
this.#emit('relocate', { ...progress, tocItem, pageItem, cfi, range }) this.#emit('relocate', { ...progress, tocItem, pageItem, cfi, range })
} }
#onLoad(doc, index) { #onLoad({ doc, index }) {
// set language and dir if not already set // set language and dir if not already set
doc.documentElement.lang ||= this.language.canonical ?? '' doc.documentElement.lang ||= this.language.canonical ?? ''
if (!this.language.isCJK) if (!this.language.isCJK)
@@ -146,7 +147,7 @@ export class View extends HTMLElement {
const obj = this.renderer.getOverlayer() const obj = this.renderer.getOverlayer()
if (obj.index === index) return obj if (obj.index === index) return obj
} }
#createOverlayer(doc, index) { #createOverlayer({ doc, index }) {
const overlayer = new Overlayer() const overlayer = new Overlayer()
doc.addEventListener('click', e => { doc.addEventListener('click', e => {
const [value, range] = overlayer.hitTest(e) const [value, range] = overlayer.hitTest(e)
@@ -278,3 +279,5 @@ export class View extends HTMLElement {
this.renderer?.destroy?.() this.renderer?.destroy?.()
} }
} }
customElements.define('foliate-view', View)