vue-reader
Version:
<div align="center"> <img width=250 src="https://raw.githubusercontent.com/jinhuan138/vue-reader/master/public/logo.png" /> <h1>VueReader</h1> </div>
437 lines (436 loc) • 20.8 kB
JavaScript
(function(global, factory) {
typeof exports === "object" && typeof module !== "undefined" ? factory(exports, require("vue"), require("epubjs")) : typeof define === "function" && define.amd ? define(["exports", "vue", "epubjs"], factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self, factory(global["vue-reader"] = {}, global.Vue, global.Epub));
})(this, (function(exports2, vue, Epub) {
"use strict";
var __vite_style__ = document.createElement("style");
__vite_style__.textContent = "\n.reader[data-v-d944dcc1] {\r\n position: absolute;\r\n inset: 50px 50px 20px;\n}\n.viewHolder[data-v-d944dcc1] {\r\n height: 100%;\r\n width: 100%;\r\n position: relative;\n}\n#viewer[data-v-d944dcc1] {\r\n height: 100%;\n}\r\n\r\n/* ↓ */\n.tocAreaButton .expansion[data-v-590afc46]::before {\r\n transform: rotate(-45deg) translateX(2.5px);\n}\n.tocAreaButton .expansion[data-v-590afc46]::after {\r\n transform: rotate(45deg) translateX(-2.5px);\n}\r\n\r\n/* ↑ */\n.tocAreaButton .open[data-v-590afc46]::before {\r\n transform: rotate(45deg) translateX(2.5px);\n}\n.tocAreaButton .open[data-v-590afc46]::after {\r\n transform: rotate(-45deg) translateX(-2.5px);\n}\n.tocAreaButton[data-v-590afc46] {\r\n user-select: none;\r\n appearance: none;\r\n background: none;\r\n border: none;\r\n display: block;\r\n font-family: sans-serif;\r\n width: 100%;\r\n font-size: 0.9em;\r\n text-align: left;\r\n padding: 0.9em 1em;\r\n border-bottom: 1px solid #ddd;\r\n color: #aaa;\r\n box-sizing: border-box;\r\n outline: none;\r\n cursor: pointer;\r\n position: relative;\n}\n.tocAreaButton[data-v-590afc46]:hover {\r\n background: rgba(0, 0, 0, 0.05);\n}\n.tocAreaButton[data-v-590afc46]:active {\r\n background: rgba(0, 0, 0, 0.1);\n}\n.active[data-v-590afc46] {\r\n color: #1565c0;\r\n border-bottom: 2px solid #1565c0;\n}\r\n\r\n/* 二级目录 */\n.tocAreaButton .expansion[data-v-590afc46] {\r\n cursor: pointer;\r\n transform: translateY(-50%);\r\n top: 50%;\r\n right: 12px;\r\n position: absolute;\r\n width: 10px;\r\n background-color: #a2a5b4;\r\n transition: transform 0.3s ease-in-out, top 0.3s ease-in-out;\n}\n.tocAreaButton .expansion[data-v-590afc46]::after,\r\n.tocAreaButton .expansion[data-v-590afc46]::before {\r\n content: '';\r\n position: absolute;\r\n width: 6px;\r\n height: 2px;\r\n background-color: currentcolor;\r\n border-radius: 2px;\r\n transition: transform 0.3s ease-in-out, top 0.3s ease-in-out;\n}\r\n\r\n/* container */\n.container[data-v-10874f24] {\r\n overflow: hidden;\r\n position: relative;\r\n height: 100%;\n}\n.containerExpanded[data-v-10874f24] {\r\n transform: translateX(256px);\n}\n.readerArea[data-v-10874f24] {\r\n position: relative;\r\n z-index: 1;\r\n height: 100%;\r\n width: 100%;\r\n background-color: #fff;\r\n transition: all 0.3s ease;\n}\n.container .titleArea[data-v-10874f24] {\r\n position: absolute;\r\n top: 20px;\r\n left: 50px;\r\n right: 50px;\r\n text-align: center;\r\n color: #999;\r\n white-space: nowrap;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\n}\r\n\r\n/* toc */\n.tocArea[data-v-10874f24] {\r\n position: absolute;\r\n left: 0;\r\n top: 0;\r\n bottom: 0;\r\n z-index: 0;\r\n width: 256px;\r\n overflow-y: auto;\r\n -webkit-overflow-scrolling: touch;\r\n background: #f2f2f2;\r\n padding: 10px 0;\n}\r\n\r\n/* 滚动条 */\n.tocArea[data-v-10874f24]::-webkit-scrollbar {\r\n width: 5px;\r\n height: 5px;\n}\n.tocArea[data-v-10874f24]::-webkit-scrollbar-thumb:vertical {\r\n height: 5px;\r\n background-color: rgba(0, 0, 0, 0.1);\r\n border-radius: 0.5rem;\n}\n.tocBackground[data-v-10874f24] {\r\n position: absolute;\r\n left: 256px;\r\n top: 0;\r\n bottom: 0;\r\n right: 0;\r\n z-index: 1;\n}\r\n\r\n/* tocButton */\n.tocButton[data-v-10874f24] {\r\n background: none;\r\n border: none;\r\n width: 32px;\r\n height: 32px;\r\n position: absolute;\r\n top: 10px;\r\n left: 10px;\r\n border-radius: 2px;\r\n outline: none;\r\n cursor: pointer;\n}\n.tocButtonBar[data-v-10874f24] {\r\n position: absolute;\r\n width: 60%;\r\n background: #ccc;\r\n height: 2px;\r\n left: 50%;\r\n margin: -1px -30%;\r\n top: 50%;\r\n transition: all 0.5s ease;\n}\n.tocButtonExpanded[data-v-10874f24] {\r\n background: #f2f2f2;\n}\r\n\r\n/* 翻页 */\n.arrow[data-v-10874f24] {\r\n outline: none;\r\n border: none;\r\n background: none;\r\n position: absolute;\r\n top: 50%;\r\n margin-top: -32px;\r\n font-size: 64px;\r\n padding: 0 10px;\r\n color: #e2e2e2;\r\n font-family: arial, sans-serif;\r\n cursor: pointer;\r\n user-select: none;\r\n appearance: none;\r\n font-weight: normal;\n}\n.arrow[data-v-10874f24]:hover {\r\n color: #777;\n}\n.arrow[data-v-10874f24]:disabled {\r\n cursor: not-allowed;\r\n color: #e2e2e2;\n}\n.prev[data-v-10874f24] {\r\n left: 1px;\n}\n.next[data-v-10874f24] {\r\n right: 1px;\n}\r\n\r\n/* loading */\n.loadingView[data-v-10874f24] {\r\n position: absolute;\r\n top: 50%;\r\n left: 10%;\r\n right: 10%;\r\n color: #ccc;\r\n text-align: center;\r\n margin-top: -0.5em;\n}\r\n\r\n/* errorView */\n.errorView[data-v-10874f24] {\r\n position: absolute;\r\n top: 50%;\r\n left: 10%;\r\n right: 10%;\r\n color: #c00;\r\n text-align: center;\r\n margin-top: -.5em;\n}\r\n/*$vite$:1*/";
document.head.appendChild(__vite_style__);
function keyListener(el, fn) {
el.addEventListener(
"keyup",
(e) => {
if (e.key === "ArrowUp" || e.key === "ArrowRight") {
fn("next");
} else if (e.key === "ArrowDown" || e.key === "ArrowLeft") {
fn("prev");
}
},
false
);
}
function wheelListener(el, fn) {
const threshold = 750;
const allowedTime = 50;
let dist = 0;
let isScrolling = void 0;
el.addEventListener("wheel", (e) => {
if (e.ignore) return;
e.ignore = true;
window.clearTimeout(isScrolling);
dist += e.deltaY;
isScrolling = window.setTimeout(() => {
if (Math.abs(dist) >= threshold) {
let direction = Math.sign(dist) > 0 ? "next" : "prev";
fn(direction);
dist = 0;
}
dist = 0;
}, allowedTime);
});
}
function swipListener(document2, fn) {
const threshold = 50;
const allowedTime = 500;
const restraint = 200;
let startX;
let startY;
let startTime;
document2.addEventListener(
"touchstart",
(e) => {
if (e.ignore) return;
e.ignore = true;
startX = e.changedTouches[0].pageX;
startY = e.changedTouches[0].pageY;
startTime = Date.now();
},
false
);
document2.addEventListener(
"touchend",
(e) => {
if (e.ignore) return;
e.ignore = true;
const distX = e.changedTouches[0].pageX - startX;
const distY = e.changedTouches[0].pageY - startY;
const elapsedTime = Date.now() - startTime;
if (elapsedTime <= allowedTime) {
if (Math.abs(distX) >= threshold && Math.abs(distY) <= restraint)
fn(distX < 0 ? "next" : "prev");
else if (Math.abs(distY) >= threshold && Math.abs(distX) <= restraint)
fn(distY < 0 ? "up" : "down");
else {
document2?.defaultView?.getSelection()?.removeAllRanges();
document2.dispatchEvent(
new MouseEvent("click", {
clientX: startX,
clientY: startY
})
);
e.preventDefault();
}
}
},
false
);
}
const _hoisted_1$2 = { class: "reader" };
const _hoisted_2$2 = { class: "viewHolder" };
const _hoisted_3$1 = { key: 0 };
const _sfc_main$2 = /* @__PURE__ */ vue.defineComponent({
__name: "EpubView",
props: {
url: {},
location: {},
tocChanged: { type: Function },
getRendition: { type: Function },
handleTextSelected: { type: Function },
handleKeyPress: { type: Function },
epubInitOptions: {},
epubOptions: {}
},
emits: ["update:location"],
setup(__props, { expose: __expose, emit: __emit }) {
const props = __props;
const {
epubInitOptions = {},
epubOptions = {},
handleKeyPress,
handleTextSelected,
getRendition,
tocChanged
} = props;
const { url, location } = vue.toRefs(props);
const emit = __emit;
const viewer = vue.ref(null);
const toc = vue.ref([]);
const isLoaded = vue.ref(false);
const isError = vue.ref(false);
let book = null, rendition = null;
const initBook = async () => {
if (book) book.destroy();
if (url.value) {
book = Epub(vue.unref(url), epubInitOptions);
book.on("openFailed", (error) => {
isError.value = true;
});
book.loaded.navigation.then(({ toc: _toc }) => {
isLoaded.value = true;
toc.value = _toc;
tocChanged && tocChanged(_toc);
initReader();
});
}
};
const initReader = () => {
rendition = book.renderTo(viewer.value, {
width: "100%",
height: "100%",
...epubOptions
});
registerEvents();
getRendition && getRendition(rendition);
if (typeof location.value === "string") {
rendition.display(location.value);
} else if (typeof location === "number") {
rendition.display(location);
} else if (toc.value.length > 0 && toc?.value[0]?.href) {
rendition.display(toc.value[0].href);
} else {
rendition.display();
}
};
const flipPage = (direction) => {
if (direction === "next") nextPage();
else if (direction === "prev") prevPage();
};
const registerEvents = () => {
if (rendition) {
rendition.on("rendered", (e, iframe) => {
iframe?.iframe?.contentWindow.focus();
if (!epubOptions?.flow?.includes("scrolled"))
wheelListener(iframe.document, flipPage);
swipListener(iframe.document, flipPage);
keyListener(iframe.document, flipPage);
});
rendition.on("locationChanged", onLocationChange);
rendition.on("displayError", () => console.error("error rendering book"));
if (handleTextSelected) {
rendition.on("selected", handleTextSelected);
}
if (handleKeyPress) {
rendition.on("selected", handleKeyPress);
}
}
};
const onLocationChange = (loc) => {
const newLocation = loc.start;
if (location.value !== newLocation) {
emit("update:location", loc.start);
}
};
vue.watch(url, initBook);
const nextPage = () => {
rendition?.next();
};
const prevPage = () => {
rendition?.prev();
};
const setLocation = (href) => {
if (typeof href === "string") rendition.display(href);
if (typeof href === "number") rendition.display(href);
};
vue.onMounted(() => {
initBook();
});
vue.onUnmounted(() => {
book?.destroy();
});
__expose({
nextPage,
prevPage,
setLocation
});
return (_ctx, _cache) => {
return vue.openBlock(), vue.createElementBlock("div", _hoisted_1$2, [
vue.createElementVNode("div", _hoisted_2$2, [
vue.withDirectives(vue.createElementVNode("div", {
ref_key: "viewer",
ref: viewer,
id: "viewer"
}, null, 512), [
[vue.vShow, isLoaded.value]
]),
!isLoaded.value ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_3$1, [
!isError.value ? vue.renderSlot(_ctx.$slots, "loadingView", { key: 0 }, void 0, true) : vue.renderSlot(_ctx.$slots, "errorView", { key: 1 }, void 0, true)
])) : vue.createCommentVNode("", true)
])
]);
};
}
});
const _export_sfc = (sfc, props) => {
const target = sfc.__vccOpts || sfc;
for (const [key, val] of props) {
target[key] = val;
}
return target;
};
const EpubView = /* @__PURE__ */ _export_sfc(_sfc_main$2, [["__scopeId", "data-v-d944dcc1"]]);
const _hoisted_1$1 = ["onClick"];
const _hoisted_2$1 = { key: 0 };
const _sfc_main$1 = /* @__PURE__ */ vue.defineComponent({
__name: "Toc",
props: {
toc: {},
current: {},
setLocation: {},
isSubmenu: { type: Boolean, default: false }
},
setup(__props) {
const bookToc = vue.ref([]);
const props = __props;
const { setLocation } = props;
const { toc, current, isSubmenu } = vue.toRefs(props);
const handleClick = (item) => {
if (item.subitems && item?.subitems?.length > 0) {
item.expansion = !item.expansion;
setLocation(item.href, false);
} else {
setLocation(item.href);
}
};
vue.watchEffect(() => {
bookToc.value = toc.value.map((item) => ({
...item,
expansion: false
}));
});
return (_ctx, _cache) => {
const _component_Toc = vue.resolveComponent("Toc", true);
return vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(bookToc.value, (item, index) => {
return vue.openBlock(), vue.createElementBlock("div", { key: index }, [
vue.createElementVNode("button", {
class: vue.normalizeClass(["tocAreaButton", { active: item.href.split("#")[0] === vue.unref(current)?.start.href }]),
onClick: ($event) => handleClick(item)
}, [
vue.createTextVNode(vue.toDisplayString(vue.unref(isSubmenu) ? " ".repeat(4) + item.label : item.label) + " ", 1),
item.subitems && item.subitems.length > 0 ? (vue.openBlock(), vue.createElementBlock("div", {
key: 0,
class: vue.normalizeClass(["expansion", { open: item.expansion }])
}, null, 2)) : vue.createCommentVNode("", true)
], 10, _hoisted_1$1),
item.subitems && item.subitems.length > 0 ? vue.withDirectives((vue.openBlock(), vue.createElementBlock("div", _hoisted_2$1, [
vue.createVNode(_component_Toc, {
toc: item.subitems,
current: vue.unref(current),
setLocation: vue.unref(setLocation),
isSubmenu: true
}, null, 8, ["toc", "current", "setLocation"])
], 512)), [
[vue.vShow, item.expansion]
]) : vue.createCommentVNode("", true)
]);
}), 128);
};
}
});
const Toc = /* @__PURE__ */ _export_sfc(_sfc_main$1, [["__scopeId", "data-v-590afc46"]]);
const _hoisted_1 = { class: "container" };
const _hoisted_2 = ["title"];
const _hoisted_3 = ["disabled"];
const _hoisted_4 = ["disabled"];
const _hoisted_5 = { key: 0 };
const _hoisted_6 = { class: "tocArea" };
const _sfc_main = /* @__PURE__ */ vue.defineComponent({
__name: "VueReader",
props: {
url: {},
title: {},
showToc: { type: Boolean, default: true },
tocChanged: {},
getRendition: {}
},
emits: ["progress"],
setup(__props, { emit: __emit }) {
const props = __props;
const emit = __emit;
const { tocChanged, getRendition } = props;
const { url, title, showToc } = vue.toRefs(props);
const epubRef = vue.ref();
const currentLocation = vue.ref(null);
const toc = vue.ref([]);
const expandedToc = vue.ref(false);
const bookName = vue.ref("");
const toggleToc = () => {
expandedToc.value = !expandedToc.value;
};
const onTocChange = (val) => {
toc.value = val;
tocChanged && tocChanged(val);
};
const onGetRendition = (rendition) => {
getRendition && getRendition(rendition);
rendition.on("relocated", (location) => {
currentLocation.value = location;
});
const book = rendition.book;
book.ready.then(() => {
const meta = book.package.metadata;
bookName.value = meta.title;
});
};
const setLocation = (href, close = true) => {
epubRef?.value?.setLocation(href);
expandedToc.value = !close;
};
const originalOpen = XMLHttpRequest.prototype.open;
const onProgress = (e) => {
emit("progress", Math.floor(e.loaded / e.total * 100));
};
XMLHttpRequest.prototype.open = function(method, requestUrl) {
if (typeof vue.unref(url) === "string" && requestUrl === vue.unref(url)) {
this.addEventListener("progress", onProgress);
}
originalOpen.apply(this, arguments);
};
vue.onUnmounted(() => {
XMLHttpRequest.prototype.open = originalOpen;
});
const next = () => {
epubRef.value?.nextPage();
};
const pre = () => {
epubRef.value?.prevPage();
};
return (_ctx, _cache) => {
return vue.openBlock(), vue.createElementBlock("div", _hoisted_1, [
vue.createElementVNode("div", {
class: vue.normalizeClass(["readerArea", { containerExpanded: expandedToc.value }])
}, [
vue.unref(showToc) ? (vue.openBlock(), vue.createElementBlock("button", {
key: 0,
class: vue.normalizeClass(["tocButton", { tocButtonExpanded: expandedToc.value }]),
type: "button",
onClick: toggleToc
}, [..._cache[0] || (_cache[0] = [
vue.createElementVNode("span", {
class: "tocButtonBar",
style: { "top": "35%" }
}, null, -1),
vue.createElementVNode("span", {
class: "tocButtonBar",
style: { "top": "66%" }
}, null, -1)
])], 2)) : vue.createCommentVNode("", true),
vue.renderSlot(_ctx.$slots, "title", {}, () => [
vue.createElementVNode("div", {
class: "titleArea",
title: vue.unref(title) || bookName.value
}, vue.toDisplayString(vue.unref(title) || bookName.value), 9, _hoisted_2)
], true),
vue.createVNode(EpubView, vue.mergeProps({
ref_key: "epubRef",
ref: epubRef
}, _ctx.$attrs, {
url: vue.unref(url),
tocChanged: onTocChange,
getRendition: onGetRendition
}), {
loadingView: vue.withCtx(() => [
vue.renderSlot(_ctx.$slots, "loadingView", {}, () => [
_cache[1] || (_cache[1] = vue.createElementVNode("div", { class: "loadingView" }, "Loading…", -1))
], true)
]),
errorView: vue.withCtx(() => [
vue.renderSlot(_ctx.$slots, "errorView", {}, () => [
_cache[2] || (_cache[2] = vue.createElementVNode("div", { class: "errorView" }, "Error loading book", -1))
], true)
]),
_: 3
}, 16, ["url"]),
vue.createElementVNode("button", {
class: "arrow pre",
onClick: pre,
disabled: currentLocation.value?.atStart
}, " ‹ ", 8, _hoisted_3),
vue.createElementVNode("button", {
class: "arrow next",
onClick: next,
disabled: currentLocation.value?.atEnd
}, " › ", 8, _hoisted_4)
], 2),
vue.unref(showToc) ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_5, [
vue.createElementVNode("div", _hoisted_6, [
vue.createVNode(Toc, {
toc: toc.value,
current: currentLocation.value,
setLocation
}, null, 8, ["toc", "current"])
]),
expandedToc.value ? (vue.openBlock(), vue.createElementBlock("div", {
key: 0,
class: "tocBackground",
onClick: toggleToc
})) : vue.createCommentVNode("", true)
])) : vue.createCommentVNode("", true)
]);
};
}
});
const VueReader = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-10874f24"]]);
exports2.EpubView = EpubView;
exports2.VueReader = VueReader;
exports2.default = VueReader;
Object.defineProperties(exports2, { __esModule: { value: true }, [Symbol.toStringTag]: { value: "Module" } });
}));