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:
John Factotum
2022-10-27 09:05:29 +00:00
committed by GitHub
parent 88ac253f30
commit 967a00f5b8
3 changed files with 39 additions and 34 deletions
-1
View File
@@ -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
View File
@@ -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)
-9
View File
@@ -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]'))