From 967a00f5b85eb5f360733c221b8f47f3cbd9cecb Mon Sep 17 00:00:00 2001 From: John Factotum <50942278+johnfactotum@users.noreply.github.com> Date: Thu, 27 Oct 2022 09:05:29 +0000 Subject: [PATCH] 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. --- README.md | 1 - paginator.js | 63 ++++++++++++++++++++++++++++++++-------------------- view.js | 9 -------- 3 files changed, 39 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 874b3f8..3454322 100644 --- a/README.md +++ b/README.md @@ -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: - 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. -- 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 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. diff --git a/paginator.js b/paginator.js index 49ecd7d..17884e5 100644 --- a/paginator.js +++ b/paginator.js @@ -120,6 +120,14 @@ const getDirection = doc => { 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 { #element = document.createElement('div') #iframe = document.createElement('iframe') @@ -167,13 +175,14 @@ class View { // it needs to be visible for Firefox to get computed style this.#iframe.style.display = 'block' const { vertical, rtl } = getDirection(doc) + const background = getBackground(doc) this.#iframe.style.display = 'none' this.#vertical = vertical this.#rtl = rtl this.#contentRange.selectNodeContents(doc.body) - const layout = beforeRender?.({ vertical, rtl }) + const layout = beforeRender?.({ vertical, rtl, background }) this.#iframe.style.display = 'block' this.render(layout) new ResizeObserver(() => this.expand()).observe(doc.body) @@ -212,7 +221,7 @@ class View { const doc = this.document const gapPadding = `${gap / 2}px` - const marginPadding = `${margin}px` + const marginPadding = `0` Object.assign(doc.documentElement.style, { boxSizing: 'border-box', columnWidth: `${columnWidth}px`, @@ -310,6 +319,7 @@ class View { // NOTE: everything here assumes the so-called "negative scroll type" for RTL export class Paginator { #element = document.createElement('div') + #container = document.createElement('div') #view #vertical = false #rtl = false @@ -328,13 +338,21 @@ export class Paginator { this.onRelocated = onRelocated this.createOverlayer = createOverlayer 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', flexWrap: 'nowrap', overflow: 'hidden', - position: 'absolute', }) new ResizeObserver(() => this.render()).observe(this.#element) - this.#element.addEventListener('scroll', debounce(() => { + this.#container.addEventListener('scroll', debounce(() => { if (this.scrolled) this.#afterScroll('scroll') }, 250)) } @@ -342,39 +360,36 @@ export class Paginator { return this.#element } #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.#element.append(this.#view.element) + this.#container.append(this.#view.element) return this.#view } - #beforeRender({ vertical, rtl }) { + #beforeRender({ vertical, rtl, background }) { this.#vertical = vertical 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 if (flow === 'scrolled') { // FIXME: vertical-rl only, not -lr this.#element.setAttribute('dir', vertical ? 'rtl' : 'ltr') - Object.assign(this.#element.style, { - width: '100%', - height: '100%', - margin: '0', - overflow: 'scroll', - }) + this.#element.style.padding = '0' + this.#container.style.overflow ='scroll' const columnWidth = this.layout.maxColumnWidth return { flow, margin, gap, columnWidth } } - const { width, height } = this.#element.getBoundingClientRect() + const { width, height } = this.#container.getBoundingClientRect() const size = vertical ? height : width const divisor = Math.ceil(size / maxColumnWidth) const columnWidth = (size / divisor) - gap this.#element.setAttribute('dir', rtl ? 'rtl' : 'ltr') - Object.assign(this.#element.style, { - width: vertical ? '100%' : `calc(100% - ${gap}px)`, - height: vertical ? `calc(100% - ${margin}px)` : '100%', - marginLeft: vertical ? '0' : `${gap / 2}px`, - marginTop: vertical ? `${margin / 2}px` : '0', - overflow: 'hidden', - }) + const paddingH = `${vertical ? margin : gap / 2}px` + const paddingV = `${vertical ? margin - gap / 2 : margin}px` + this.#element.style.padding = `${paddingV} ${paddingH}` + this.#container.style.overflow ='hidden' return { height, width, margin, gap, columnWidth } } render() { @@ -399,13 +414,13 @@ export class Paginator { : scrolled ? 'height' : 'width' } get size() { - return this.#element.getBoundingClientRect()[this.sideProp] + return this.#container.getBoundingClientRect()[this.sideProp] } get viewSize() { return this.#view.element.getBoundingClientRect()[this.sideProp] } get start() { - return Math.abs(this.#element[this.scrollProp]) + return Math.abs(this.#container[this.scrollProp]) } get end() { return this.start + this.size @@ -444,7 +459,7 @@ export class Paginator { return this.#scrollToPage(Math.floor(offset / this.size), reason) } async #scrollTo(offset, reason) { - const element = this.#element + const element = this.#container const { scrollProp } = this if (element[scrollProp] === offset) { this.#afterScroll(reason) diff --git a/view.js b/view.js index f7d80db..44bfc3e 100644 --- a/view.js +++ b/view.js @@ -174,15 +174,6 @@ export class View { 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 const section = book.sections[index] for (const a of doc.querySelectorAll('a[href]'))