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
|
### 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
@@ -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
@@ -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,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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user