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
JavaScript
"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));
}
(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;