mirror of
https://github.com/john-okeefe/foliate-js.git
synced 2026-09-09 11:29:14 -04:00
Make paginator more self-contained
Don't rely on absolute positioning and margin. And set background on the containing element rather than the document body.
This commit is contained in:
@@ -98,7 +98,6 @@ A renderer's interface is currently mainly:
|
|||||||
The paginator uses the same pagination strategy as [Epub.js](https://github.com/futurepress/epub.js): it uses CSS multi-column. As such it shares much of the same limitations (it's slow, some CSS styles do not work as expected, and other bugs). There are a few differences:
|
The paginator uses the same pagination strategy as [Epub.js](https://github.com/futurepress/epub.js): it uses CSS multi-column. As such it shares much of the same limitations (it's slow, some CSS styles do not work as expected, and other bugs). There are a few differences:
|
||||||
- It is a totally standalone module. You can use it to paginate any content.
|
- It is a totally standalone module. You can use it to paginate any content.
|
||||||
- It is much simpler, but currently there's no support for continuous scrolling.
|
- It is much simpler, but currently there's no support for continuous scrolling.
|
||||||
- To simplify things, it always spans the whole viewport. One can always put everything in an iframe if desired.
|
|
||||||
- It has no concept of CFIs and operates on `Range` objects directly.
|
- It has no concept of CFIs and operates on `Range` objects directly.
|
||||||
- It uses bisecting to find the current visible range, which is more accurate than what Epub.js does.
|
- It uses bisecting to find the current visible range, which is more accurate than what Epub.js does.
|
||||||
- It has an internal `#anchor` property, which can be a `Range`, `Element`, or a fraction that represents the current location. The view is *anchored* to it no matter how you resize the window.
|
- It has an internal `#anchor` property, which can be a `Range`, `Element`, or a fraction that represents the current location. The view is *anchored* to it no matter how you resize the window.
|
||||||
|
|||||||
+39
-24
@@ -120,6 +120,14 @@ const getDirection = doc => {
|
|||||||
return { vertical, rtl }
|
return { vertical, rtl }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getBackground = doc => {
|
||||||
|
const bodyStyle = doc.defaultView.getComputedStyle(doc.body)
|
||||||
|
return bodyStyle.backgroundColor === 'rgba(0, 0, 0, 0)'
|
||||||
|
&& bodyStyle.backgroundImage === 'none'
|
||||||
|
? doc.defaultView.getComputedStyle(doc.documentElement).background
|
||||||
|
: bodyStyle.background
|
||||||
|
}
|
||||||
|
|
||||||
class View {
|
class View {
|
||||||
#element = document.createElement('div')
|
#element = document.createElement('div')
|
||||||
#iframe = document.createElement('iframe')
|
#iframe = document.createElement('iframe')
|
||||||
@@ -167,13 +175,14 @@ class View {
|
|||||||
// it needs to be visible for Firefox to get computed style
|
// it needs to be visible for Firefox to get computed style
|
||||||
this.#iframe.style.display = 'block'
|
this.#iframe.style.display = 'block'
|
||||||
const { vertical, rtl } = getDirection(doc)
|
const { vertical, rtl } = getDirection(doc)
|
||||||
|
const background = getBackground(doc)
|
||||||
this.#iframe.style.display = 'none'
|
this.#iframe.style.display = 'none'
|
||||||
|
|
||||||
this.#vertical = vertical
|
this.#vertical = vertical
|
||||||
this.#rtl = rtl
|
this.#rtl = rtl
|
||||||
|
|
||||||
this.#contentRange.selectNodeContents(doc.body)
|
this.#contentRange.selectNodeContents(doc.body)
|
||||||
const layout = beforeRender?.({ vertical, rtl })
|
const layout = beforeRender?.({ vertical, rtl, background })
|
||||||
this.#iframe.style.display = 'block'
|
this.#iframe.style.display = 'block'
|
||||||
this.render(layout)
|
this.render(layout)
|
||||||
new ResizeObserver(() => this.expand()).observe(doc.body)
|
new ResizeObserver(() => this.expand()).observe(doc.body)
|
||||||
@@ -212,7 +221,7 @@ class View {
|
|||||||
|
|
||||||
const doc = this.document
|
const doc = this.document
|
||||||
const gapPadding = `${gap / 2}px`
|
const gapPadding = `${gap / 2}px`
|
||||||
const marginPadding = `${margin}px`
|
const marginPadding = `0`
|
||||||
Object.assign(doc.documentElement.style, {
|
Object.assign(doc.documentElement.style, {
|
||||||
boxSizing: 'border-box',
|
boxSizing: 'border-box',
|
||||||
columnWidth: `${columnWidth}px`,
|
columnWidth: `${columnWidth}px`,
|
||||||
@@ -310,6 +319,7 @@ 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 {
|
||||||
#element = document.createElement('div')
|
#element = document.createElement('div')
|
||||||
|
#container = document.createElement('div')
|
||||||
#view
|
#view
|
||||||
#vertical = false
|
#vertical = false
|
||||||
#rtl = false
|
#rtl = false
|
||||||
@@ -328,13 +338,21 @@ export class Paginator {
|
|||||||
this.onRelocated = onRelocated
|
this.onRelocated = onRelocated
|
||||||
this.createOverlayer = createOverlayer
|
this.createOverlayer = createOverlayer
|
||||||
Object.assign(this.#element.style, {
|
Object.assign(this.#element.style, {
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
position: 'absolute',
|
||||||
|
})
|
||||||
|
this.#element.append(this.#container)
|
||||||
|
Object.assign(this.#container.style, {
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexWrap: 'nowrap',
|
flexWrap: 'nowrap',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
position: 'absolute',
|
|
||||||
})
|
})
|
||||||
new ResizeObserver(() => this.render()).observe(this.#element)
|
new ResizeObserver(() => this.render()).observe(this.#element)
|
||||||
this.#element.addEventListener('scroll', debounce(() => {
|
this.#container.addEventListener('scroll', debounce(() => {
|
||||||
if (this.scrolled) this.#afterScroll('scroll')
|
if (this.scrolled) this.#afterScroll('scroll')
|
||||||
}, 250))
|
}, 250))
|
||||||
}
|
}
|
||||||
@@ -342,39 +360,36 @@ export class Paginator {
|
|||||||
return this.#element
|
return this.#element
|
||||||
}
|
}
|
||||||
#createView() {
|
#createView() {
|
||||||
if (this.#view) this.#element.removeChild(this.#view.element)
|
if (this.#view) this.#container.removeChild(this.#view.element)
|
||||||
this.#view = new View({ container: this.#element })
|
this.#view = new View({ container: this.#element })
|
||||||
this.#element.append(this.#view.element)
|
this.#container.append(this.#view.element)
|
||||||
return this.#view
|
return this.#view
|
||||||
}
|
}
|
||||||
#beforeRender({ vertical, rtl }) {
|
#beforeRender({ vertical, rtl, background }) {
|
||||||
this.#vertical = vertical
|
this.#vertical = vertical
|
||||||
this.#rtl = rtl
|
this.#rtl = rtl
|
||||||
|
// set `document` background to `doc` background
|
||||||
|
// this is needed cause the iframe does not fill the whole element
|
||||||
|
this.#element.style.background = background
|
||||||
|
|
||||||
const { flow, margin, gap, maxColumnWidth } = this.layout
|
const { flow, margin, gap, maxColumnWidth } = this.layout
|
||||||
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.#element.setAttribute('dir', vertical ? 'rtl' : 'ltr')
|
||||||
Object.assign(this.#element.style, {
|
this.#element.style.padding = '0'
|
||||||
width: '100%',
|
this.#container.style.overflow ='scroll'
|
||||||
height: '100%',
|
|
||||||
margin: '0',
|
|
||||||
overflow: 'scroll',
|
|
||||||
})
|
|
||||||
const columnWidth = this.layout.maxColumnWidth
|
const columnWidth = this.layout.maxColumnWidth
|
||||||
return { flow, margin, gap, columnWidth }
|
return { flow, margin, gap, columnWidth }
|
||||||
}
|
}
|
||||||
const { width, height } = this.#element.getBoundingClientRect()
|
const { width, height } = this.#container.getBoundingClientRect()
|
||||||
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.#element.setAttribute('dir', rtl ? 'rtl' : 'ltr')
|
||||||
Object.assign(this.#element.style, {
|
const paddingH = `${vertical ? margin : gap / 2}px`
|
||||||
width: vertical ? '100%' : `calc(100% - ${gap}px)`,
|
const paddingV = `${vertical ? margin - gap / 2 : margin}px`
|
||||||
height: vertical ? `calc(100% - ${margin}px)` : '100%',
|
this.#element.style.padding = `${paddingV} ${paddingH}`
|
||||||
marginLeft: vertical ? '0' : `${gap / 2}px`,
|
this.#container.style.overflow ='hidden'
|
||||||
marginTop: vertical ? `${margin / 2}px` : '0',
|
|
||||||
overflow: 'hidden',
|
|
||||||
})
|
|
||||||
return { height, width, margin, gap, columnWidth }
|
return { height, width, margin, gap, columnWidth }
|
||||||
}
|
}
|
||||||
render() {
|
render() {
|
||||||
@@ -399,13 +414,13 @@ export class Paginator {
|
|||||||
: scrolled ? 'height' : 'width'
|
: scrolled ? 'height' : 'width'
|
||||||
}
|
}
|
||||||
get size() {
|
get size() {
|
||||||
return this.#element.getBoundingClientRect()[this.sideProp]
|
return this.#container.getBoundingClientRect()[this.sideProp]
|
||||||
}
|
}
|
||||||
get viewSize() {
|
get viewSize() {
|
||||||
return this.#view.element.getBoundingClientRect()[this.sideProp]
|
return this.#view.element.getBoundingClientRect()[this.sideProp]
|
||||||
}
|
}
|
||||||
get start() {
|
get start() {
|
||||||
return Math.abs(this.#element[this.scrollProp])
|
return Math.abs(this.#container[this.scrollProp])
|
||||||
}
|
}
|
||||||
get end() {
|
get end() {
|
||||||
return this.start + this.size
|
return this.start + this.size
|
||||||
@@ -444,7 +459,7 @@ export class Paginator {
|
|||||||
return this.#scrollToPage(Math.floor(offset / this.size), reason)
|
return this.#scrollToPage(Math.floor(offset / this.size), reason)
|
||||||
}
|
}
|
||||||
async #scrollTo(offset, reason) {
|
async #scrollTo(offset, reason) {
|
||||||
const element = this.#element
|
const element = this.#container
|
||||||
const { scrollProp } = this
|
const { scrollProp } = this
|
||||||
if (element[scrollProp] === offset) {
|
if (element[scrollProp] === offset) {
|
||||||
this.#afterScroll(reason)
|
this.#afterScroll(reason)
|
||||||
|
|||||||
@@ -174,15 +174,6 @@ export class View {
|
|||||||
|
|
||||||
this.renderer.setStyle(this.#css)
|
this.renderer.setStyle(this.#css)
|
||||||
|
|
||||||
// set `document` background to `doc` background
|
|
||||||
// this is needed cause the iframe does not fill the whole viewport
|
|
||||||
const bodyStyle = doc.defaultView.getComputedStyle(doc.body)
|
|
||||||
document.body.style.background =
|
|
||||||
bodyStyle.backgroundColor === 'rgba(0, 0, 0, 0)'
|
|
||||||
&& bodyStyle.backgroundImage === 'none'
|
|
||||||
? doc.defaultView.getComputedStyle(doc.documentElement).background
|
|
||||||
: bodyStyle.background
|
|
||||||
|
|
||||||
// set handlers for links
|
// set handlers for links
|
||||||
const section = book.sections[index]
|
const section = book.sections[index]
|
||||||
for (const a of doc.querySelectorAll('a[href]'))
|
for (const a of doc.querySelectorAll('a[href]'))
|
||||||
|
|||||||
Reference in New Issue
Block a user