UNPKG

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
"use strict"; 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;