UNPKG

vue-book-reader

Version:

<div align="center"> <img width=250 src="https://raw.githubusercontent.com/jinhuan138/vue--book-reader/master/public/logo.png" /> <h1>VueReader</h1> </div>

1,099 lines (1,098 loc) 38 kB
"use strict"; Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); const debounce = (f, wait2, immediate) => { let timeout; return (...args) => { const later = () => { timeout = null; if (!immediate) f(...args); }; const callNow = immediate && !timeout; if (timeout) clearTimeout(timeout); timeout = setTimeout(later, wait2); if (callNow) f(...args); }; }; const lerp = (min, max, x) => x * (max - min) + min; const easeOutQuad = (x) => 1 - (1 - x) * (1 - x); const animate = (a, b, duration, ease, render) => new Promise((resolve) => { let start; const step = (now) => { if (document.hidden) { render(lerp(a, b, 1)); return resolve(); } start ??= now; const fraction = Math.min(1, (now - start) / duration); render(lerp(a, b, ease(fraction))); if (fraction < 1) requestAnimationFrame(step); else resolve(); }; if (document.hidden) { render(lerp(a, b, 1)); return resolve(); } requestAnimationFrame(step); }); const uncollapse = (range) => { if (!(range == null ? void 0 : range.collapsed)) return range; const { endOffset, endContainer } = range; if (endContainer.nodeType === 1) { const node = endContainer.childNodes[endOffset]; if ((node == null ? void 0 : node.nodeType) === 1) return node; return endContainer; } if (endOffset + 1 < endContainer.length) range.setEnd(endContainer, endOffset + 1); else if (endOffset > 1) range.setStart(endContainer, endOffset - 1); else return endContainer.parentNode; return range; }; const makeRange = (doc, node, start, end = start) => { const range = doc.createRange(); range.setStart(node, start); range.setEnd(node, end); return range; }; const bisectNode = (doc, node, cb, start = 0, end = node.nodeValue.length) => { if (end - start === 1) { const result2 = cb(makeRange(doc, node, start), makeRange(doc, node, end)); return result2 < 0 ? start : end; } const mid = Math.floor(start + (end - start) / 2); const result = cb(makeRange(doc, node, start, mid), makeRange(doc, node, mid, end)); return result < 0 ? bisectNode(doc, node, cb, start, mid) : result > 0 ? bisectNode(doc, node, cb, mid, end) : mid; }; const { SHOW_ELEMENT, SHOW_TEXT, SHOW_CDATA_SECTION, FILTER_ACCEPT, FILTER_REJECT, FILTER_SKIP } = NodeFilter; const filter = SHOW_ELEMENT | SHOW_TEXT | SHOW_CDATA_SECTION; const getBoundingClientRect = (target) => { let top = Infinity, right = -Infinity, left = Infinity, bottom = -Infinity; for (const rect of target.getClientRects()) { left = Math.min(left, rect.left); top = Math.min(top, rect.top); right = Math.max(right, rect.right); bottom = Math.max(bottom, rect.bottom); } return new DOMRect(left, top, right - left, bottom - top); }; const getVisibleRange = (doc, start, end, mapRect) => { const acceptNode = (node) => { var _a, _b; const name = (_a = node.localName) == null ? void 0 : _a.toLowerCase(); if (name === "script" || name === "style") return FILTER_REJECT; if (node.nodeType === 1) { const { left, right } = mapRect(node.getBoundingClientRect()); if (right < start || left > end) return FILTER_REJECT; if (left >= start && right <= end) return FILTER_ACCEPT; } else { if (!((_b = node.nodeValue) == null ? void 0 : _b.trim())) return FILTER_SKIP; const range2 = doc.createRange(); range2.selectNodeContents(node); const { left, right } = mapRect(range2.getBoundingClientRect()); if (right >= start && left <= end) return FILTER_ACCEPT; } return FILTER_SKIP; }; const walker = doc.createTreeWalker(doc.body, filter, { acceptNode }); const nodes = []; for (let node = walker.nextNode(); node; node = walker.nextNode()) nodes.push(node); const from = nodes[0] ?? doc.body; const to = nodes[nodes.length - 1] ?? from; const startOffset = from.nodeType === 1 ? 0 : bisectNode(doc, from, (a, b) => { const p = mapRect(getBoundingClientRect(a)); const q = mapRect(getBoundingClientRect(b)); if (p.right < start && q.left > start) return 0; return q.left > start ? -1 : 1; }); const endOffset = to.nodeType === 1 ? 0 : bisectNode(doc, to, (a, b) => { const p = mapRect(getBoundingClientRect(a)); const q = mapRect(getBoundingClientRect(b)); if (p.right < end && q.left > end) return 0; return q.left > end ? -1 : 1; }); const range = doc.createRange(); range.setStart(from, startOffset); range.setEnd(to, endOffset); return range; }; const selectionIsBackward = (sel) => { const range = document.createRange(); range.setStart(sel.anchorNode, sel.anchorOffset); range.setEnd(sel.focusNode, sel.focusOffset); return range.collapsed; }; const setSelectionTo = (target, collapse) => { let range; if (target.startContainer) range = target.cloneRange(); else if (target.nodeType) { range = document.createRange(); range.selectNode(target); } if (range) { const sel = range.startContainer.ownerDocument.defaultView.getSelection(); if (sel) { sel.removeAllRanges(); if (collapse === -1) range.collapse(true); else if (collapse === 1) range.collapse(); sel.addRange(range); } } }; const getDirection = (doc) => { const { defaultView } = doc; const { writingMode, direction } = defaultView.getComputedStyle(doc.body); const vertical = writingMode === "vertical-rl" || writingMode === "vertical-lr"; const rtl = doc.body.dir === "rtl" || direction === "rtl" || doc.documentElement.dir === "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; }; 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; }); const setStylesImportant = (el, styles) => { const { style } = el; for (const [k, v] of Object.entries(styles)) style.setProperty(k, v, "important"); }; class View { #observer = new ResizeObserver(() => this.expand()); #element = document.createElement("div"); #iframe = document.createElement("iframe"); #contentRange = document.createRange(); #overlayer; #vertical = false; #rtl = false; #column = true; #size; #layout = {}; constructor({ container, onExpand }) { this.container = container; this.onExpand = onExpand; this.#iframe.setAttribute("part", "filter"); this.#element.append(this.#iframe); Object.assign(this.#element.style, { boxSizing: "content-box", position: "relative", overflow: "hidden", flex: "0 0 auto", width: "100%", height: "100%", display: "flex", justifyContent: "center", alignItems: "center" }); Object.assign(this.#iframe.style, { overflow: "hidden", border: "0", display: "none", width: "100%", height: "100%" }); this.#iframe.setAttribute("sandbox", "allow-same-origin allow-scripts"); this.#iframe.setAttribute("scrolling", "no"); } get element() { return this.#element; } get document() { return this.#iframe.contentDocument; } async load(src, afterLoad, beforeRender) { if (typeof src !== "string") throw new Error(`${src} is not string`); return new Promise((resolve) => { this.#iframe.addEventListener("load", () => { const doc = this.document; afterLoad == null ? void 0 : afterLoad(doc); 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 == null ? void 0 : beforeRender({ vertical, rtl, background }); this.#iframe.style.display = "block"; this.render(layout); this.#observer.observe(doc.body); doc.fonts.ready.then(() => this.expand()); resolve(); }, { once: true }); this.#iframe.src = src; }); } render(layout) { if (!layout) return; this.#column = layout.flow !== "scrolled"; this.#layout = layout; if (this.#column) this.columnize(layout); else this.scrolled(layout); } scrolled({ gap, columnWidth }) { const vertical = this.#vertical; const doc = this.document; setStylesImportant(doc.documentElement, { "box-sizing": "border-box", "padding": vertical ? `${gap}px 0` : `0 ${gap}px`, "column-width": "auto", "height": "auto", "width": "auto" }); setStylesImportant(doc.body, { [vertical ? "max-height" : "max-width"]: `${columnWidth}px`, "margin": "auto" }); this.setImageSize(); this.expand(); } columnize({ width, height, gap, columnWidth }) { const vertical = this.#vertical; this.#size = vertical ? height : width; const doc = this.document; setStylesImportant(doc.documentElement, { "box-sizing": "border-box", "column-width": `${Math.trunc(columnWidth)}px`, "column-gap": `${gap}px`, "column-fill": "auto", ...vertical ? { "width": `${width}px` } : { "height": `${height}px` }, "padding": vertical ? `${gap / 2}px 0` : `0 ${gap / 2}px`, "overflow": "hidden", // force wrap long words "overflow-wrap": "break-word", // reset some potentially problematic props "position": "static", "border": "0", "margin": "0", "max-height": "none", "max-width": "none", "min-height": "none", "min-width": "none", // fix glyph clipping in WebKit "-webkit-line-box-contain": "block glyphs replaced" }); setStylesImportant(doc.body, { "max-height": "none", "max-width": "none", "margin": "0" }); this.setImageSize(); this.expand(); } setImageSize() { const { width, height, margin } = this.#layout; const vertical = this.#vertical; const doc = this.document; for (const el of doc.body.querySelectorAll("img, svg, video")) { const { maxHeight, maxWidth } = doc.defaultView.getComputedStyle(el); setStylesImportant(el, { "max-height": vertical ? maxHeight !== "none" && maxHeight !== "0px" ? maxHeight : "100%" : `${height - margin * 2}px`, "max-width": vertical ? `${width - margin * 2}px` : maxWidth !== "none" && maxWidth !== "0px" ? maxWidth : "100%", "object-fit": "contain", "page-break-inside": "avoid", "break-inside": "avoid", "box-sizing": "border-box" }); } } expand() { const { documentElement } = this.document; if (this.#column) { const side = this.#vertical ? "height" : "width"; const otherSide = this.#vertical ? "width" : "height"; const contentRect = this.#contentRange.getBoundingClientRect(); const rootRect = documentElement.getBoundingClientRect(); const contentStart = this.#vertical ? 0 : this.#rtl ? rootRect.right - contentRect.right : contentRect.left - rootRect.left; const contentSize = contentStart + contentRect[side]; const pageCount = Math.ceil(contentSize / this.#size); const expandedSize = pageCount * this.#size; this.#element.style.padding = "0"; this.#iframe.style[side] = `${expandedSize}px`; this.#element.style[side] = `${expandedSize + this.#size * 2}px`; this.#iframe.style[otherSide] = "100%"; this.#element.style[otherSide] = "100%"; documentElement.style[side] = `${this.#size}px`; if (this.#overlayer) { this.#overlayer.element.style.margin = "0"; this.#overlayer.element.style.left = this.#vertical ? "0" : `${this.#size}px`; this.#overlayer.element.style.top = this.#vertical ? `${this.#size}px` : "0"; this.#overlayer.element.style[side] = `${expandedSize}px`; this.#overlayer.redraw(); } } else { const side = this.#vertical ? "width" : "height"; const otherSide = this.#vertical ? "height" : "width"; const contentSize = documentElement.getBoundingClientRect()[side]; const expandedSize = contentSize; const { margin } = this.#layout; const padding = this.#vertical ? `0 ${margin}px` : `${margin}px 0`; this.#element.style.padding = padding; this.#iframe.style[side] = `${expandedSize}px`; this.#element.style[side] = `${expandedSize}px`; this.#iframe.style[otherSide] = "100%"; this.#element.style[otherSide] = "100%"; if (this.#overlayer) { this.#overlayer.element.style.margin = padding; this.#overlayer.element.style.left = "0"; this.#overlayer.element.style.top = "0"; this.#overlayer.element.style[side] = `${expandedSize}px`; this.#overlayer.redraw(); } } this.onExpand(); } set overlayer(overlayer) { this.#overlayer = overlayer; this.#element.append(overlayer.element); } get overlayer() { return this.#overlayer; } destroy() { if (this.document) this.#observer.unobserve(this.document.body); } } class Paginator extends HTMLElement { static observedAttributes = [ "flow", "gap", "margin", "max-inline-size", "max-block-size", "max-column-count" ]; #root = this.attachShadow({ mode: "closed" }); #observer = new ResizeObserver(() => this.render()); #top; #background; #container; #header; #footer; #view; #vertical = false; #rtl = false; #margin = 0; #index = -1; #anchor = 0; // anchor view to a fraction (0-1), Range, or Element #justAnchored = false; #locked = false; // while true, prevent any further navigation #styles; #styleMap = /* @__PURE__ */ new WeakMap(); #mediaQuery = matchMedia("(prefers-color-scheme: dark)"); #mediaQueryListener; #scrollBounds; #touchState; #touchScrolled; #lastVisibleRange; constructor() { super(); this.#root.innerHTML = `<style> :host { display: block; container-type: size; } :host, #top { box-sizing: border-box; position: relative; overflow: hidden; width: 100%; height: 100%; } #top { --_gap: 7%; --_margin: 48px; --_max-inline-size: 720px; --_max-block-size: 1440px; --_max-column-count: 2; --_max-column-count-portrait: 1; --_max-column-count-spread: var(--_max-column-count); --_half-gap: calc(var(--_gap) / 2); --_max-width: calc(var(--_max-inline-size) * var(--_max-column-count-spread)); --_max-height: var(--_max-block-size); display: grid; grid-template-columns: minmax(var(--_half-gap), 1fr) var(--_half-gap) minmax(0, calc(var(--_max-width) - var(--_gap))) var(--_half-gap) minmax(var(--_half-gap), 1fr); grid-template-rows: minmax(var(--_margin), 1fr) minmax(0, var(--_max-height)) minmax(var(--_margin), 1fr); &.vertical { --_max-column-count-spread: var(--_max-column-count-portrait); --_max-width: var(--_max-block-size); --_max-height: calc(var(--_max-inline-size) * var(--_max-column-count-spread)); } @container (orientation: portrait) { & { --_max-column-count-spread: var(--_max-column-count-portrait); } &.vertical { --_max-column-count-spread: var(--_max-column-count); } } } #background { grid-column: 1 / -1; grid-row: 1 / -1; } #container { grid-column: 2 / 5; grid-row: 2; overflow: hidden; } :host([flow="scrolled"]) #container { grid-column: 1 / -1; grid-row: 1 / -1; overflow: auto; } #header { grid-column: 3 / 4; grid-row: 1; } #footer { grid-column: 3 / 4; grid-row: 3; align-self: end; } #header, #footer { display: grid; height: var(--_margin); } :is(#header, #footer) > * { display: flex; align-items: center; min-width: 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="top"> <div id="background" part="filter"></div> <div id="header"></div> <div id="container"></div> <div id="footer"></div> </div> `; this.#top = this.#root.getElementById("top"); this.#background = this.#root.getElementById("background"); this.#container = this.#root.getElementById("container"); this.#header = this.#root.getElementById("header"); this.#footer = this.#root.getElementById("footer"); this.#observer.observe(this.#container); this.#container.addEventListener("scroll", () => this.dispatchEvent(new Event("scroll"))); this.#container.addEventListener("scroll", debounce(() => { if (this.scrolled) { if (this.#justAnchored) this.#justAnchored = false; else this.#afterScroll("scroll"); } }, 250)); const opts = { passive: false }; this.addEventListener("touchstart", this.#onTouchStart.bind(this), opts); this.addEventListener("touchmove", this.#onTouchMove.bind(this), opts); this.addEventListener("touchend", this.#onTouchEnd.bind(this)); this.addEventListener("load", ({ detail: { doc } }) => { doc.addEventListener("touchstart", this.#onTouchStart.bind(this), opts); doc.addEventListener("touchmove", this.#onTouchMove.bind(this), opts); doc.addEventListener("touchend", this.#onTouchEnd.bind(this)); }); this.addEventListener("relocate", ({ detail }) => { if (detail.reason === "selection") setSelectionTo(this.#anchor, 0); else if (detail.reason === "navigation") { if (this.#anchor === 1) setSelectionTo(detail.range, 1); else if (typeof this.#anchor === "number") setSelectionTo(detail.range, -1); else setSelectionTo(this.#anchor, -1); } }); const checkPointerSelection = debounce((range, sel) => { if (!sel.rangeCount) return; const selRange = sel.getRangeAt(0); const backward = selectionIsBackward(sel); if (backward && selRange.compareBoundaryPoints(Range.START_TO_START, range) < 0) this.prev(); else if (!backward && selRange.compareBoundaryPoints(Range.END_TO_END, range) > 0) this.next(); }, 700); this.addEventListener("load", ({ detail: { doc } }) => { let isPointerSelecting = false; doc.addEventListener("pointerdown", () => isPointerSelecting = true); doc.addEventListener("pointerup", () => isPointerSelecting = false); let isKeyboardSelecting = false; doc.addEventListener("keydown", () => isKeyboardSelecting = true); doc.addEventListener("keyup", () => isKeyboardSelecting = false); doc.addEventListener("selectionchange", () => { if (this.scrolled) return; const range = this.#lastVisibleRange; if (!range) return; const sel = doc.getSelection(); if (!sel.rangeCount) return; if (isPointerSelecting && sel.type === "Range") checkPointerSelection(range, sel); else if (isKeyboardSelecting) { const selRange = sel.getRangeAt(0).cloneRange(); const backward = selectionIsBackward(sel); if (!backward) selRange.collapse(); this.#scrollToAnchor(selRange); } }); doc.addEventListener("focusin", (e) => this.scrolled ? null : ( // NOTE: `requestAnimationFrame` is needed in WebKit requestAnimationFrame(() => this.#scrollToAnchor(e.target)) )); }); this.#mediaQueryListener = () => { if (!this.#view) return; this.#background.style.background = getBackground(this.#view.document); }; this.#mediaQuery.addEventListener("change", this.#mediaQueryListener); } attributeChangedCallback(name, _, value) { switch (name) { case "flow": this.render(); break; case "gap": case "margin": case "max-block-size": case "max-column-count": this.#top.style.setProperty("--_" + name, value); break; case "max-inline-size": this.#top.style.setProperty("--_" + name, value); this.render(); break; } } open(book) { var _a; this.bookDir = book.dir; this.sections = book.sections; (_a = book.transformTarget) == null ? void 0 : _a.addEventListener("data", ({ detail }) => { if (detail.type !== "text/css") return; const w = innerWidth; const h = innerHeight; detail.data = Promise.resolve(detail.data).then((data) => data.replace(new RegExp("(?<=[{\\s;])-epub-", "gi"), "").replace(/(\d*\.?\d+)vw/gi, (_, d) => parseFloat(d) * w / 100 + "px").replace(/(\d*\.?\d+)vh/gi, (_, d) => parseFloat(d) * h / 100 + "px").replace(/page-break-(after|before|inside)\s*:/gi, (_, x) => `-webkit-column-break-${x}:`).replace(/break-(after|before|inside)\s*:\s*(avoid-)?page/gi, (_, x, y) => `break-${x}: ${y ?? ""}column`)); }); } #createView() { if (this.#view) { this.#view.destroy(); this.#container.removeChild(this.#view.element); } this.#view = new View({ container: this, onExpand: () => this.#scrollToAnchor(this.#anchor) }); this.#container.append(this.#view.element); return this.#view; } #beforeRender({ vertical, rtl, background }) { this.#vertical = vertical; this.#rtl = rtl; this.#top.classList.toggle("vertical", vertical); this.#background.style.background = background; const { width, height } = this.#container.getBoundingClientRect(); const size = vertical ? height : width; const style = getComputedStyle(this.#top); const maxInlineSize = parseFloat(style.getPropertyValue("--_max-inline-size")); const maxColumnCount = parseInt(style.getPropertyValue("--_max-column-count-spread")); const margin = parseFloat(style.getPropertyValue("--_margin")); this.#margin = margin; const g = parseFloat(style.getPropertyValue("--_gap")) / 100; const gap = -g / (g - 1) * size; const flow = this.getAttribute("flow"); if (flow === "scrolled") { this.setAttribute("dir", vertical ? "rtl" : "ltr"); this.#top.style.padding = "0"; const columnWidth2 = maxInlineSize; this.heads = null; this.feet = null; this.#header.replaceChildren(); this.#footer.replaceChildren(); return { flow, margin, gap, columnWidth: columnWidth2 }; } const divisor = Math.min(maxColumnCount, Math.ceil(size / maxInlineSize)); const columnWidth = size / divisor - gap; this.setAttribute("dir", rtl ? "rtl" : "ltr"); const marginalDivisor = vertical ? Math.min(2, Math.ceil(width / maxInlineSize)) : divisor; const marginalStyle = { gridTemplateColumns: `repeat(${marginalDivisor}, 1fr)`, gap: `${gap}px`, direction: this.bookDir === "rtl" ? "rtl" : "ltr" }; Object.assign(this.#header.style, marginalStyle); Object.assign(this.#footer.style, marginalStyle); 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); this.#footer.replaceChildren(...feet); return { height, width, margin, gap, columnWidth }; } render() { if (!this.#view) return; this.#view.render(this.#beforeRender({ vertical: this.#vertical, rtl: this.#rtl })); this.#scrollToAnchor(this.#anchor); } get scrolled() { return this.getAttribute("flow") === "scrolled"; } get scrollProp() { const { scrolled } = this; return this.#vertical ? scrolled ? "scrollLeft" : "scrollTop" : scrolled ? "scrollTop" : "scrollLeft"; } get sideProp() { const { scrolled } = this; return this.#vertical ? scrolled ? "width" : "height" : scrolled ? "height" : "width"; } get size() { return this.#container.getBoundingClientRect()[this.sideProp]; } get viewSize() { return this.#view.element.getBoundingClientRect()[this.sideProp]; } get start() { return Math.abs(this.#container[this.scrollProp]); } get end() { return this.start + this.size; } get page() { return Math.floor((this.start + this.end) / 2 / this.size); } get pages() { return Math.round(this.viewSize / this.size); } scrollBy(dx, dy) { const delta = this.#vertical ? dy : dx; const element = this.#container; const { scrollProp } = this; const [offset, a, b] = this.#scrollBounds; const rtl = this.#rtl; const min = rtl ? offset - b : offset - a; const max = rtl ? offset + a : offset + b; element[scrollProp] = Math.max(min, Math.min( max, element[scrollProp] + delta )); } snap(vx, vy) { const velocity = this.#vertical ? vy : vx; const [offset, a, b] = this.#scrollBounds; const { start, end, pages, size } = this; const min = Math.abs(offset) - a; const max = Math.abs(offset) + b; const d = velocity * (this.#rtl ? -size : size); const page = Math.floor( Math.max(min, Math.min(max, (start + end) / 2 + (isNaN(d) ? 0 : d))) / size ); this.#scrollToPage(page, "snap").then(() => { const dir = page <= 0 ? -1 : page >= pages - 1 ? 1 : null; if (dir) return this.#goTo({ index: this.#adjacentIndex(dir), anchor: dir < 0 ? () => 1 : () => 0 }); }); } #onTouchStart(e) { const touch = e.changedTouches[0]; this.#touchState = { x: touch == null ? void 0 : touch.screenX, y: touch == null ? void 0 : touch.screenY, t: e.timeStamp, vx: 0, xy: 0 }; } #onTouchMove(e) { const state = this.#touchState; if (state.pinched) return; state.pinched = globalThis.visualViewport.scale > 1; if (this.scrolled || state.pinched) return; if (e.touches.length > 1) { if (this.#touchScrolled) e.preventDefault(); return; } e.preventDefault(); const touch = e.changedTouches[0]; const x = touch.screenX, y = touch.screenY; const dx = state.x - x, dy = state.y - y; const dt = e.timeStamp - state.t; state.x = x; state.y = y; state.t = e.timeStamp; state.vx = dx / dt; state.vy = dy / dt; this.#touchScrolled = true; this.scrollBy(dx, dy); } #onTouchEnd() { this.#touchScrolled = false; if (this.scrolled) return; requestAnimationFrame(() => { if (globalThis.visualViewport.scale === 1) this.snap(this.#touchState.vx, this.#touchState.vy); }); } // allows one to process rects as if they were LTR and horizontal #getRectMapper() { if (this.scrolled) { const size = this.viewSize; const margin = this.#margin; return this.#vertical ? ({ left, right }) => ({ left: size - right - margin, right: size - left - margin }) : ({ top, bottom }) => ({ left: top + margin, right: bottom + margin }); } const pxSize = this.pages * this.size; return this.#rtl ? ({ left, right }) => ({ left: pxSize - right, right: pxSize - left }) : this.#vertical ? ({ top, bottom }) => ({ left: top, right: bottom }) : (f) => f; } async #scrollToRect(rect, reason) { if (this.scrolled) { const offset2 = this.#getRectMapper()(rect).left - this.#margin; return this.#scrollTo(offset2, reason); } const offset = this.#getRectMapper()(rect).left; return this.#scrollToPage(Math.floor(offset / this.size) + (this.#rtl ? -1 : 1), reason); } async #scrollTo(offset, reason, smooth) { const element = this.#container; const { scrollProp, size } = this; if (element[scrollProp] === offset) { this.#scrollBounds = [offset, this.atStart ? 0 : size, this.atEnd ? 0 : size]; this.#afterScroll(reason); return; } if (this.scrolled && this.#vertical) offset = -offset; if ((reason === "snap" || smooth) && this.hasAttribute("animated")) return animate( element[scrollProp], offset, 300, easeOutQuad, (x) => element[scrollProp] = x ).then(() => { this.#scrollBounds = [offset, this.atStart ? 0 : size, this.atEnd ? 0 : size]; this.#afterScroll(reason); }); else { element[scrollProp] = offset; this.#scrollBounds = [offset, this.atStart ? 0 : size, this.atEnd ? 0 : size]; this.#afterScroll(reason); } } async #scrollToPage(page, reason, smooth) { const offset = this.size * (this.#rtl ? -page : page); return this.#scrollTo(offset, reason, smooth); } async scrollToAnchor(anchor, select) { return this.#scrollToAnchor(anchor, select ? "selection" : "navigation"); } async #scrollToAnchor(anchor, reason = "anchor") { var _a, _b; this.#anchor = anchor; const rects = (_b = (_a = uncollapse(anchor)) == null ? void 0 : _a.getClientRects) == null ? void 0 : _b.call(_a); if (rects) { const rect = Array.from(rects).find((r) => r.width > 0 && r.height > 0) || rects[0]; if (!rect) return; await this.#scrollToRect(rect, reason); return; } if (this.scrolled) { await this.#scrollTo(anchor * this.viewSize, reason); return; } const { pages } = this; if (!pages) return; const textPages = pages - 2; const newPage = Math.round(anchor * (textPages - 1)); await this.#scrollToPage(newPage + 1, reason); } #getVisibleRange() { if (this.scrolled) return getVisibleRange( this.#view.document, this.start + this.#margin, this.end - this.#margin, this.#getRectMapper() ); const size = this.#rtl ? -this.size : this.size; return getVisibleRange( this.#view.document, this.start - size, this.end - size, this.#getRectMapper() ); } #afterScroll(reason) { const range = this.#getVisibleRange(); this.#lastVisibleRange = range; if (reason !== "selection" && reason !== "navigation" && reason !== "anchor") this.#anchor = range; else this.#justAnchored = true; const index = this.#index; const detail = { reason, 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 > 1 ? "visible" : "hidden"; detail.fraction = (page - 1) / (pages - 2); detail.size = 1 / (pages - 2); } this.dispatchEvent(new CustomEvent("relocate", { detail })); } async #display(promise) { var _a, _b; const { index, src, anchor, onLoad, select } = await promise; this.#index = index; const hasFocus = (_b = (_a = this.#view) == null ? void 0 : _a.document) == null ? void 0 : _b.hasFocus(); if (src) { const view = this.#createView(); const afterLoad = (doc) => { if (doc.head) { const $styleBefore = doc.createElement("style"); doc.head.prepend($styleBefore); const $style = doc.createElement("style"); doc.head.append($style); this.#styleMap.set(doc, [$styleBefore, $style]); } onLoad == null ? void 0 : onLoad({ doc, index }); }; const beforeRender = this.#beforeRender.bind(this); await view.load(src, afterLoad, beforeRender); this.dispatchEvent(new CustomEvent("create-overlayer", { detail: { doc: view.document, index, attach: (overlayer) => view.overlayer = overlayer } })); this.#view = view; } await this.scrollToAnchor((typeof anchor === "function" ? anchor(this.#view.document) : anchor) ?? 0, select); if (hasFocus) this.focusView(); } #canGoToIndex(index) { return index >= 0 && index <= this.sections.length - 1; } async #goTo({ index, anchor, select }) { if (index === this.#index) await this.#display({ index, anchor, select }); else { const oldIndex = this.#index; const onLoad = (detail) => { var _a, _b; (_b = (_a = this.sections[oldIndex]) == null ? void 0 : _a.unload) == null ? void 0 : _b.call(_a); this.setStyles(this.#styles); this.dispatchEvent(new CustomEvent("load", { detail })); }; await this.#display(Promise.resolve(this.sections[index].load()).then((src) => ({ index, src, anchor, onLoad, select })).catch((e) => { console.warn(e); console.warn(new Error(`Failed to load section ${index}`)); return {}; })); } } async goTo(target) { if (this.#locked) return; const resolved = await target; if (this.#canGoToIndex(resolved.index)) return this.#goTo(resolved); } #scrollPrev(distance) { if (!this.#view) return true; if (this.scrolled) { if (this.start > 0) return this.#scrollTo( Math.max(0, this.start - (distance ?? this.size)), null, true ); return true; } if (this.atStart) return; const page = this.page - 1; return this.#scrollToPage(page, "page", true).then(() => page <= 0); } #scrollNext(distance) { if (!this.#view) return true; if (this.scrolled) { if (this.viewSize - this.end > 2) return this.#scrollTo( Math.min(this.viewSize, distance ? this.start + distance : this.end), null, true ); return true; } if (this.atEnd) return; const page = this.page + 1; const pages = this.pages; return this.#scrollToPage(page, "page", true).then(() => page >= pages - 1); } get atStart() { return this.#adjacentIndex(-1) == null && this.page <= 1; } get atEnd() { return this.#adjacentIndex(1) == null && this.page >= this.pages - 2; } #adjacentIndex(dir) { var _a; for (let index = this.#index + dir; this.#canGoToIndex(index); index += dir) if (((_a = this.sections[index]) == null ? void 0 : _a.linear) !== "no") return index; } async #turnPage(dir, distance) { if (this.#locked) return; this.#locked = true; const prev = dir === -1; const shouldGo = await (prev ? this.#scrollPrev(distance) : this.#scrollNext(distance)); if (shouldGo) await this.#goTo({ index: this.#adjacentIndex(dir), anchor: prev ? () => 1 : () => 0 }); if (shouldGo || !this.hasAttribute("animated")) await wait(100); this.#locked = false; } prev(distance) { return this.#turnPage(-1, distance); } next(distance) { return this.#turnPage(1, distance); } prevSection() { return this.goTo({ index: this.#adjacentIndex(-1) }); } nextSection() { return this.goTo({ index: this.#adjacentIndex(1) }); } firstSection() { const index = this.sections.findIndex((section) => section.linear !== "no"); return this.goTo({ index }); } lastSection() { const index = this.sections.findLastIndex((section) => section.linear !== "no"); return this.goTo({ index }); } getContents() { if (this.#view) return [{ index: this.#index, overlayer: this.#view.overlayer, doc: this.#view.document }]; return []; } setStyles(styles) { var _a, _b, _c, _d, _e; this.#styles = styles; const $$styles = this.#styleMap.get((_a = this.#view) == null ? void 0 : _a.document); if (!$$styles) return; const [$beforeStyle, $style] = $$styles; if (Array.isArray(styles)) { const [beforeStyle, style] = styles; $beforeStyle.textContent = beforeStyle; $style.textContent = style; } else $style.textContent = styles; requestAnimationFrame(() => this.#background.style.background = getBackground(this.#view.document)); (_e = (_d = (_c = (_b = this.#view) == null ? void 0 : _b.document) == null ? void 0 : _c.fonts) == null ? void 0 : _d.ready) == null ? void 0 : _e.then(() => this.#view.expand()); } focusView() { this.#view.document.defaultView.focus(); } destroy() { var _a, _b; this.#observer.unobserve(this); this.#view.destroy(); this.#view = null; (_b = (_a = this.sections[this.#index]) == null ? void 0 : _a.unload) == null ? void 0 : _b.call(_a); this.#mediaQuery.removeEventListener("change", this.#mediaQueryListener); } } customElements.define("foliate-paginator", Paginator); exports.Paginator = Paginator;