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>
432 lines (431 loc) • 14.6 kB
JavaScript
;
Object.defineProperties(exports, { __esModule: { value: true }, [Symbol.toStringTag]: { value: "Module" } });
require('./index.css');const vue = require("vue");
const Epub = require("epubjs");
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(document, fn) {
const threshold = 50;
const allowedTime = 500;
const restraint = 200;
let startX;
let startY;
let startTime;
document.addEventListener(
"touchstart",
(e) => {
if (e.ignore) return;
e.ignore = true;
startX = e.changedTouches[0].pageX;
startY = e.changedTouches[0].pageY;
startTime = Date.now();
},
false
);
document.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 {
document?.defaultView?.getSelection()?.removeAllRanges();
document.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"]]);
exports.EpubView = EpubView;
exports.VueReader = VueReader;
exports.default = VueReader;