mirror of
https://github.com/john-okeefe/foliate-js.git
synced 2026-09-09 11:29:14 -04:00
Use Shadow DOM
This commit is contained in:
@@ -51,8 +51,7 @@ The repo also includes a still higher level reader, though strictly speaking, `r
|
||||
### Basic Usage
|
||||
|
||||
```js
|
||||
import { View } from './view.js'
|
||||
customElements.define('foliate-view', View)
|
||||
import from './view.js'
|
||||
|
||||
const view = document.createElement('foliate-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.
|
||||
|
||||
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
|
||||
|
||||
@@ -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.
|
||||
- `.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
|
||||
.foliate-header > *, .foliate-footer > * {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
font-size: .75em;
|
||||
opacity: .6;
|
||||
foliate-view::part(head) {
|
||||
padding-bottom: 4px;
|
||||
border-bottom: 1px solid graytext;
|
||||
}
|
||||
.foliate-header > * > *, .foliate-footer > * > * {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
```
|
||||
``
|
||||
|
||||
### EPUB CFI
|
||||
|
||||
|
||||
+49
-58
@@ -29,9 +29,11 @@ const getViewport = (doc, viewport) => {
|
||||
return { width: 1000, height: 2000 }
|
||||
}
|
||||
|
||||
class Container {
|
||||
#observer = new ResizeObserver(() => this.render())
|
||||
#element = document.createElement('div')
|
||||
export class FixedLayout extends HTMLElement {
|
||||
#root = this.attachShadow({ mode: 'closed' })
|
||||
#observer = new ResizeObserver(() => this.#render())
|
||||
#spreads
|
||||
#index = -1
|
||||
defaultViewport
|
||||
spread
|
||||
#portrait = false
|
||||
@@ -40,20 +42,19 @@ class Container {
|
||||
#center
|
||||
#side
|
||||
constructor() {
|
||||
Object.assign(this.#element.style, {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
})
|
||||
this.#observer.observe(this.#element)
|
||||
}
|
||||
get element() {
|
||||
return this.#element
|
||||
}
|
||||
get side() {
|
||||
return this.#side
|
||||
super()
|
||||
|
||||
const sheet = new CSSStyleSheet()
|
||||
this.#root.adoptedStyleSheets = [sheet]
|
||||
sheet.replaceSync(`:host {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}`)
|
||||
|
||||
this.#observer.observe(this)
|
||||
}
|
||||
async #createFrame({ index, src }) {
|
||||
const element = document.createElement('div')
|
||||
@@ -68,14 +69,14 @@ class Container {
|
||||
// https://bugs.webkit.org/show_bug.cgi?id=218086
|
||||
iframe.setAttribute('sandbox', 'allow-same-origin allow-scripts')
|
||||
iframe.setAttribute('scrolling', 'no')
|
||||
iframe.classList.add('foliate-filter')
|
||||
this.#element.append(element)
|
||||
iframe.setAttribute('part', 'filter')
|
||||
this.#root.append(element)
|
||||
if (!src) return { blank: true, element, iframe }
|
||||
return new Promise(resolve => {
|
||||
const onload = () => {
|
||||
iframe.removeEventListener('load', onload)
|
||||
const doc = iframe.contentDocument
|
||||
this.onLoad?.(doc, index)
|
||||
this.dispatchEvent(new CustomEvent('load', { detail: { doc, index } }))
|
||||
const { width, height } = getViewport(doc, this.defaultViewport)
|
||||
resolve({
|
||||
element, iframe,
|
||||
@@ -87,12 +88,12 @@ class Container {
|
||||
iframe.src = src
|
||||
})
|
||||
}
|
||||
render(side = this.#side) {
|
||||
#render(side = this.#side) {
|
||||
if (!side) return
|
||||
const left = this.#left ?? {}
|
||||
const right = this.#center ?? this.#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'
|
||||
&& height > width
|
||||
this.#portrait = portrait
|
||||
@@ -135,23 +136,23 @@ class Container {
|
||||
transform(right)
|
||||
}
|
||||
}
|
||||
async showSpread({ left, right, center, side }) {
|
||||
this.#element.replaceChildren()
|
||||
async #showSpread({ left, right, center, side }) {
|
||||
this.#root.replaceChildren()
|
||||
this.#left = null
|
||||
this.#right = null
|
||||
this.#center = null
|
||||
if (center) {
|
||||
this.#center = await this.#createFrame(center)
|
||||
this.#side = 'center'
|
||||
this.render()
|
||||
this.#render()
|
||||
} else {
|
||||
this.#left = await this.#createFrame(left)
|
||||
this.#right = await this.#createFrame(right)
|
||||
this.#side = side
|
||||
this.render()
|
||||
this.#render()
|
||||
}
|
||||
}
|
||||
goLeft() {
|
||||
#goLeft() {
|
||||
if (this.#center) return
|
||||
if (this.#left?.blank) return true
|
||||
if (this.#portrait && this.#left?.element?.style?.display === 'none') {
|
||||
@@ -161,7 +162,7 @@ class Container {
|
||||
return true
|
||||
}
|
||||
}
|
||||
goRight() {
|
||||
#goRight() {
|
||||
if (this.#center) return
|
||||
if (this.#right?.blank) return true
|
||||
if (this.#portrait && this.#right?.element?.style?.display === 'none') {
|
||||
@@ -171,24 +172,11 @@ class Container {
|
||||
return true
|
||||
}
|
||||
}
|
||||
destroy() {
|
||||
this.#observer.unobserve(this.#element)
|
||||
this.#element.remove()
|
||||
}
|
||||
}
|
||||
|
||||
export class FixedLayout {
|
||||
#spreads
|
||||
#index = -1
|
||||
#container = new Container()
|
||||
constructor({ book, onLoad, onRelocate }) {
|
||||
open(book) {
|
||||
this.book = book
|
||||
this.#container.onLoad = onLoad
|
||||
this.onRelocate = onRelocate
|
||||
|
||||
const { rendition } = book
|
||||
this.#container.spread = rendition?.spread
|
||||
this.#container.defaultViewport = rendition?.viewport
|
||||
this.spread = rendition?.spread
|
||||
this.defaultViewport = rendition?.viewport
|
||||
|
||||
const rtl = book.dir === 'rtl'
|
||||
const ltr = !rtl
|
||||
@@ -227,15 +215,16 @@ export class FixedLayout {
|
||||
return arr
|
||||
}, [{}])
|
||||
}
|
||||
get element() {
|
||||
return this.#container.element
|
||||
}
|
||||
get 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)
|
||||
return this.book.sections.indexOf(section)
|
||||
}
|
||||
#reportLocation() {
|
||||
this.dispatchEvent(new CustomEvent('relocate', { detail:
|
||||
{ range: null, index: this.index, fraction: 0, size: 1 } }))
|
||||
}
|
||||
getSpreadOf(section) {
|
||||
const spreads = this.#spreads
|
||||
for (let index = 0; index < spreads.length; index++) {
|
||||
@@ -248,7 +237,7 @@ export class FixedLayout {
|
||||
async goToSpread(index, side) {
|
||||
if (index < 0 || index > this.#spreads.length - 1) return
|
||||
if (index === this.#index) {
|
||||
this.#container.render(side)
|
||||
this.#render(side)
|
||||
return
|
||||
}
|
||||
this.#index = index
|
||||
@@ -256,7 +245,7 @@ export class FixedLayout {
|
||||
if (spread.center) {
|
||||
const index = this.book.sections.indexOf(spread.center)
|
||||
const src = await spread.center?.load?.()
|
||||
await this.#container.showSpread({ center: { index, src } })
|
||||
await this.#showSpread({ center: { index, src } })
|
||||
} else {
|
||||
const indexL = this.book.sections.indexOf(spread.left)
|
||||
const indexR = this.book.sections.indexOf(spread.right)
|
||||
@@ -264,9 +253,9 @@ export class FixedLayout {
|
||||
const srcR = await spread.right?.load?.()
|
||||
const left = { index: indexL, src: srcL }
|
||||
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) {
|
||||
await this.goTo(target)
|
||||
@@ -281,20 +270,22 @@ export class FixedLayout {
|
||||
await this.goToSpread(index, side)
|
||||
}
|
||||
async next() {
|
||||
const s = this.rtl ? this.#container.goLeft() : this.#container.goRight()
|
||||
if (s) this.onRelocate?.(null, this.index, 0, 1)
|
||||
const s = this.rtl ? this.#goLeft() : this.#goRight()
|
||||
if (s) this.#reportLocation()
|
||||
else return this.goToSpread(this.#index + 1, this.rtl ? 'right' : 'left')
|
||||
}
|
||||
async prev() {
|
||||
const s = this.rtl ? this.#container.goRight() : this.#container.goLeft()
|
||||
if (s) this.onRelocate?.(null, this.index, 0, 1)
|
||||
const s = this.rtl ? this.#goRight() : this.#goLeft()
|
||||
if (s) this.#reportLocation()
|
||||
else return this.goToSpread(this.#index - 1, this.rtl ? 'left' : 'right')
|
||||
}
|
||||
deselect() {
|
||||
for (const frame of this.#container.element.querySelectorAll('iframe'))
|
||||
for (const frame of this.#root.querySelectorAll('iframe'))
|
||||
frame.contentWindow.getSelection().removeAllRanges()
|
||||
}
|
||||
destroy() {
|
||||
this.#container.destroy()
|
||||
this.#observer.unobserve(this)
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('foliate-fxl', FixedLayout)
|
||||
|
||||
+100
-85
@@ -128,10 +128,11 @@ const getBackground = doc => {
|
||||
: bodyStyle.background
|
||||
}
|
||||
|
||||
const makeMarginals = length => Array.from({ length }, () => {
|
||||
const makeMarginals = (length, part) => Array.from({ length }, () => {
|
||||
const div = document.createElement('div')
|
||||
const child = document.createElement('div')
|
||||
div.append(child)
|
||||
child.setAttribute('part', part)
|
||||
return div
|
||||
})
|
||||
|
||||
@@ -148,7 +149,7 @@ class View {
|
||||
constructor({ container, onExpand }) {
|
||||
this.container = container
|
||||
this.onExpand = onExpand
|
||||
this.#iframe.classList.add('foliate-filter')
|
||||
this.#iframe.setAttribute('part', 'filter')
|
||||
this.#element.append(this.#iframe)
|
||||
Object.assign(this.#element.style, {
|
||||
boxSizing: 'content-box',
|
||||
@@ -328,17 +329,17 @@ class View {
|
||||
}
|
||||
|
||||
// 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
|
||||
#shouldUpdateGap = true
|
||||
#observer = new ResizeObserver(() => this.render())
|
||||
#element = document.createElement('div')
|
||||
#background = document.createElement('div')
|
||||
#maxWidthContainer = document.createElement('div')
|
||||
#maxHeightContainer = document.createElement('div')
|
||||
#container = document.createElement('div')
|
||||
#header = document.createElement('div')
|
||||
#footer = document.createElement('div')
|
||||
#background
|
||||
#maxWidthContainer
|
||||
#maxHeightContainer
|
||||
#container
|
||||
#header
|
||||
#footer
|
||||
#view
|
||||
#vertical = false
|
||||
#rtl = false
|
||||
@@ -351,75 +352,82 @@ export class Paginator {
|
||||
gap: 0.05,
|
||||
maxColumnWidth: 700,
|
||||
}
|
||||
constructor({ book, onLoad, onRelocate, createOverlayer }) {
|
||||
this.bookDir = book.dir
|
||||
this.sections = book.sections
|
||||
this.onLoad = onLoad
|
||||
this.onRelocate = onRelocate
|
||||
this.createOverlayer = createOverlayer
|
||||
Object.assign(this.#element.style, {
|
||||
boxSizing: 'border-box',
|
||||
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',
|
||||
constructor() {
|
||||
super()
|
||||
this.#root.innerHTML = `<style>
|
||||
:host {
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
}
|
||||
Object.assign(this.#header.style, marginalStyle)
|
||||
Object.assign(this.#footer.style, marginalStyle)
|
||||
this.#header.classList.add('foliate-header')
|
||||
this.#footer.classList.add('foliate-footer')
|
||||
this.#maxWidthContainer.append(this.#header)
|
||||
this.#maxWidthContainer.append(this.#footer)
|
||||
#background {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
#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(() => {
|
||||
if (this.scrolled) this.#afterScroll('scroll')
|
||||
}, 250))
|
||||
}
|
||||
get element() {
|
||||
return this.#element
|
||||
open(book) {
|
||||
this.bookDir = book.dir
|
||||
this.sections = book.sections
|
||||
}
|
||||
#createView() {
|
||||
if (this.#view) this.#container.removeChild(this.#view.element)
|
||||
this.#view = new View({
|
||||
container: this.#element,
|
||||
container: this,
|
||||
onExpand: this.#scrollToAnchor.bind(this),
|
||||
})
|
||||
this.#container.append(this.#view.element)
|
||||
@@ -437,8 +445,8 @@ export class Paginator {
|
||||
|
||||
if (flow === 'scrolled') {
|
||||
// FIXME: vertical-rl only, not -lr
|
||||
this.#element.setAttribute('dir', vertical ? 'rtl' : 'ltr')
|
||||
this.#element.style.padding = '0'
|
||||
this.setAttribute('dir', vertical ? 'rtl' : 'ltr')
|
||||
this.style.padding = '0'
|
||||
this.#container.style.overflow ='scroll'
|
||||
this.#maxWidthContainer.style.maxWidth = 'none'
|
||||
this.#maxHeightContainer.style.maxHeight = 'none'
|
||||
@@ -473,7 +481,7 @@ export class Paginator {
|
||||
? Math.max(margin - gap / 2, gap / 2)
|
||||
: margin}px`
|
||||
this.#gap = gap
|
||||
this.#element.style.padding = `${paddingV} ${paddingH}`
|
||||
this.style.padding = `${paddingV} ${paddingH}`
|
||||
this.#header.style.top = `-${paddingV}`
|
||||
this.#footer.style.bottom = `-${paddingV}`
|
||||
|
||||
@@ -491,7 +499,7 @@ export class Paginator {
|
||||
const size = vertical ? height : width
|
||||
const divisor = Math.ceil(size / maxColumnWidth)
|
||||
const columnWidth = (size / divisor) - gap
|
||||
this.#element.setAttribute('dir', rtl ? 'rtl' : 'ltr')
|
||||
this.setAttribute('dir', rtl ? 'rtl' : 'ltr')
|
||||
this.#container.style.overflow ='hidden'
|
||||
|
||||
const marginalDivisor = vertical
|
||||
@@ -507,8 +515,8 @@ export class Paginator {
|
||||
}
|
||||
Object.assign(this.#header.style, marginalStyle)
|
||||
Object.assign(this.#footer.style, marginalStyle)
|
||||
const heads = makeMarginals(marginalDivisor)
|
||||
const feet = makeMarginals(marginalDivisor)
|
||||
const heads = makeMarginals(marginalDivisor, 'head')
|
||||
const feet = makeMarginals(marginalDivisor, 'foot')
|
||||
this.heads = heads.map(el => el.children[0])
|
||||
this.feet = feet.map(el => el.children[0])
|
||||
this.#header.replaceChildren(...heads)
|
||||
@@ -657,13 +665,15 @@ export class Paginator {
|
||||
if (reason !== 'anchor') this.#anchor = range
|
||||
|
||||
const index = this.#index
|
||||
if (this.scrolled)
|
||||
this.onRelocate?.(range, index, this.start / this.viewSize)
|
||||
const detail = { range, index }
|
||||
if (this.scrolled) detail.fraction = this.start / this.viewSize
|
||||
else if (this.pages > 0) {
|
||||
const { page, pages } = this
|
||||
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) {
|
||||
const { index, src, anchor, onLoad, select } = await promise
|
||||
@@ -678,12 +688,16 @@ export class Paginator {
|
||||
doc.head.append($style)
|
||||
this.#styleMap.set(doc, [$styleBefore, $style])
|
||||
}
|
||||
onLoad?.(doc, index)
|
||||
onLoad?.({ doc, index })
|
||||
}
|
||||
const beforeRender = this.#beforeRender.bind(this)
|
||||
await view.load(src, afterLoad, beforeRender)
|
||||
const overlayer = this.createOverlayer?.(view.document, index)
|
||||
if (overlayer) view.overlayer = overlayer
|
||||
this.dispatchEvent(new CustomEvent('create-overlayer', {
|
||||
detail: {
|
||||
doc: view.document, index,
|
||||
attach: overlayer => view.overlayer = overlayer,
|
||||
},
|
||||
}))
|
||||
this.#view = view
|
||||
}
|
||||
this.#anchor = (typeof anchor === 'function'
|
||||
@@ -732,9 +746,9 @@ export class Paginator {
|
||||
if (index === this.#index) await this.#display({ index, anchor, select })
|
||||
else {
|
||||
const oldIndex = this.#index
|
||||
const onLoad = (...args) => {
|
||||
const onLoad = detail => {
|
||||
this.sections[oldIndex]?.unload?.()
|
||||
this.onLoad?.(...args)
|
||||
this.dispatchEvent(new CustomEvent('load', { detail }))
|
||||
}
|
||||
await this.#display(Promise.resolve(this.sections[index].load())
|
||||
.then(src => ({ index, src, anchor, onLoad, select }))
|
||||
@@ -801,7 +815,8 @@ export class Paginator {
|
||||
sel.removeAllRanges()
|
||||
}
|
||||
destroy() {
|
||||
this.#observer.unobserve(this.#element)
|
||||
this.#element.remove()
|
||||
this.#observer.unobserve(this)
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('foliate-paginator', Paginator)
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { View } from './view.js'
|
||||
import './view.js'
|
||||
import { createTOCView } from './ui/tree.js'
|
||||
import { createMenu } from './ui/menu.js'
|
||||
|
||||
customElements.define('foliate-view', View)
|
||||
|
||||
const isZip = async file => {
|
||||
const arr = new Uint8Array(await file.slice(0, 4).arrayBuffer())
|
||||
return arr[0] === 0x50 && arr[1] === 0x4b && arr[2] === 0x03 && arr[3] === 0x04
|
||||
|
||||
@@ -41,6 +41,7 @@ const languageInfo = lang => {
|
||||
}
|
||||
|
||||
export class View extends HTMLElement {
|
||||
#root = this.attachShadow({ mode: 'closed' })
|
||||
#sectionProgress
|
||||
#tocProgress
|
||||
#pageProgress
|
||||
@@ -61,21 +62,21 @@ export class View extends HTMLElement {
|
||||
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'
|
||||
if (this.isFixedLayout) {
|
||||
const { FixedLayout } = await import('./fixed-layout.js')
|
||||
this.renderer = new FixedLayout(opts)
|
||||
await import('./fixed-layout.js')
|
||||
this.renderer = document.createElement('foliate-fxl')
|
||||
} else {
|
||||
const { Paginator } = await import('./paginator.js')
|
||||
this.renderer = new Paginator(opts)
|
||||
await import('./paginator.js')
|
||||
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 }) {
|
||||
if (lastLocation) {
|
||||
@@ -87,7 +88,7 @@ export class View extends HTMLElement {
|
||||
#emit(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
|
||||
const progress = this.#sectionProgress.getProgress(index, fraction, size)
|
||||
const tocItem = this.#tocProgress.getProgress(index, range)
|
||||
@@ -95,7 +96,7 @@ export class View extends HTMLElement {
|
||||
const cfi = this.getCFI(index, range)
|
||||
this.#emit('relocate', { ...progress, tocItem, pageItem, cfi, range })
|
||||
}
|
||||
#onLoad(doc, index) {
|
||||
#onLoad({ doc, index }) {
|
||||
// set language and dir if not already set
|
||||
doc.documentElement.lang ||= this.language.canonical ?? ''
|
||||
if (!this.language.isCJK)
|
||||
@@ -146,7 +147,7 @@ export class View extends HTMLElement {
|
||||
const obj = this.renderer.getOverlayer()
|
||||
if (obj.index === index) return obj
|
||||
}
|
||||
#createOverlayer(doc, index) {
|
||||
#createOverlayer({ doc, index }) {
|
||||
const overlayer = new Overlayer()
|
||||
doc.addEventListener('click', e => {
|
||||
const [value, range] = overlayer.hitTest(e)
|
||||
@@ -278,3 +279,5 @@ export class View extends HTMLElement {
|
||||
this.renderer?.destroy?.()
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('foliate-view', View)
|
||||
|
||||
Reference in New Issue
Block a user