diff --git a/package.json b/package.json index 7e85a51..f91d146 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,8 @@ "postcompile": "cp -r ./src/types ./lib/types" }, "dependencies": { - "epubjs": "^0.3.88" + "epubjs": "^0.3.88", + "react-epub-viewer": "^0.1.7" }, "devDependencies": { "@babel/cli": "^7.13.14", diff --git a/public/css/global.css b/public/css/global.css index e245d85..3af7b13 100644 --- a/public/css/global.css +++ b/public/css/global.css @@ -34,4 +34,13 @@ a { #root { height : 100%; +} + +.epub-container { + overflow-x: hidden !important; +} + +.epub-container::-webkit-scrollbar { + width: 0px; + background: transparent; } \ No newline at end of file diff --git a/src/containers/Reader.tsx b/src/containers/Reader.tsx index a10a621..d3b45b6 100644 --- a/src/containers/Reader.tsx +++ b/src/containers/Reader.tsx @@ -1,8 +1,8 @@ import { useState, useRef } from 'react' import { useSelector, useDispatch } from 'react-redux' import { Provider } from 'react-redux' +import { ReactEpubViewer } from 'react-epub-viewer' // containers -import Viewer from 'modules/reactViewer/ReactViewer' import Header from 'containers/Header' import Footer from 'containers/Footer' import Nav from 'containers/menu/Nav' @@ -34,7 +34,7 @@ const Reader = ({ url, loadingView }: Props) => { const dispatch = useDispatch(); const currentLocation = useSelector(state => state.book.currentLocation); - const viewerRef = useRef(null); + const viewerRef = useRef(null); const navRef = useRef(null); const optionRef = useRef(null); const learningRef = useRef(null); @@ -142,7 +142,7 @@ const Reader = ({ url, loadingView }: Props) => { onLearningToggle={onLearningToggle} /> - | any) => { - // TODO Fix the ref type correctly instead 'any' type. - const [isLoaded, setIsLoaded] = useState(false); - - const [book, setBook] = useState(null); - - const [rendition, setRendition] = useState(null); - - const [toc, setToc] = useState([]); - - const currentCfi = useRef(''); - - - - /** - * Init Book - * @method - * - Set book state - * - Set Epub path state - * - Set isLoaded state - * - Set toc state - */ - const initBook = () => { - if (book) { - book.destroy(); - } - - const book_ = new Book(url, epubFileOptions); - setBook(book_); - - bookChanged && bookChanged(book_); - - book_.loaded.navigation.then(({ toc }) => { - const toc_: Toc[] = toc.map(t => ({ - label: t.label, - href: t.href - })); - - setIsLoaded(true); - setToc(toc_); - tocChanged && tocChanged(toc_); - initRendition(book_); - }); - - // Init Location & Pagination - book_.ready.then(function() { - const stored = localStorage.getItem(book_.key() + '-locations'); - if (stored) { - return book_.locations.load(stored); - } else { - return book_.locations.generate(1024); - } - }).then(() => { - localStorage.setItem(book_.key() + '-locations', book_.locations.save()); - }); - }; - - /** - * Init Rendition - * @method - * - Init iframe tag on DOM element - * - Set rendition - * - Set start page - */ - const initRendition = (book_: Book) => { - const node = ref.current; - if (!node || !book_) return; - - const rendition_ = book_.renderTo(node, { - width: "100%", - height: "100%", - ...epubOptions - }); - setRendition(rendition_); - - rendtionChanged && rendtionChanged(rendition_); - - if(typeof location === "string") { - rendition_.display(location); - } else if(toc.length > 0 && toc[0].href) { - rendition_.display(toc[0].href); - } else { - rendition_.display(); - } - } - - /** - * Move page - * @method - * @param type direction - */ - const movePage = useCallback((type: "PREV" | "NEXT") => { - if (!rendition) return; - if (type === "PREV") rendition.prev(); - else rendition.next(); - }, [rendition]); - - /** - * Move page by arrow key - * @method - * @param props Keyboard Event - * @param props.key - */ - const handleKeyPress = useCallback(({ key }: any) => { - key && key === "ArrowLeft" && movePage("PREV"); - key && key === "ArrowRight" && movePage("NEXT"); - }, [movePage]); - - /** - * Run when location changed - * @method - * @param loc - * - Set location state - * - Run 'locationChanged()' through startCFI - */ - const onLocationChange = useCallback((loc: Loc) => { - const startCfi = loc && loc.start; - const endCfi = loc && loc.end; - const base = loc && loc.start.slice(8).split('!')[0]; - - if (!book) return; - - const spineItem = book.spine.get(startCfi); - const navItem = book.navigation.get(spineItem.href); - const chapterName = navItem && navItem.label.trim(); - - const locations: any = book.locations; - const currentPage = locations.locationFromCfi(startCfi); - const totalPage = locations.total; - - pageChanged && pageChanged({ - chapterName, - currentPage, - totalPage, - startCfi, - endCfi, - base - }); - - currentCfi.current = startCfi; - }, [book, pageChanged]); - - /** - * Highlight function - * @param cfiRange Selecton CFIRange - * @param color Highlight color - * @param callback Highlight callback function when click it - */ - const onHighlight = useCallback(( - cfiRange: string, - callback: (e: any) => void, - color?: string - ) => { - if (!rendition) return; - - rendition.annotations.remove(cfiRange, 'highlight'); - rendition.annotations.highlight( - cfiRange, - {}, - callback, - "epub-highlight", - { 'fill': color || '#fdf183' } - ); - }, [rendition]); - - /** - * Register viewer control function - * @method - * - REF.CURRENT.prevPage() : Move prev page - * - REF.CURRENT.nextPage() : Move next page - * - REF.CURRENT.getCurrentCfi() : Get current CFI - * - REF.CURRENT.onHighlight(): Set highlight - * - REF.CURRENT.offHighlight(): Remove specific highliht - * - REF.CURRENT.seLocation(): Move to specific cfi or href - */ - const registerGlobalFunc = useCallback(() => { - if (!ref.current) return; - if (movePage) { - ref.current.prevPage = () => movePage("PREV"); - ref.current.nextPage = () => movePage("NEXT"); - } - ref.current.getCurrentCfi = () => currentCfi.current; - if (onHighlight) { - ref.current.onHighlight = onHighlight; - } - if (rendition) { - ref.current.offHighlight = (cfiRange: string) => rendition.annotations.remove(cfiRange, 'highlight'); - ref.current.setLocation = (location: string) => rendition.display(location); - } - }, [ref, rendition, movePage, onHighlight]); - - - - /** Ref checker */ - useEffect(() => { - if (!ref) { - throw new Error("[React-Epub-Viewer] Put a ref argument that has a ViewerRef type."); - } - }, [ref]); - - /** Epub Init */ - /* eslint-disable */ - useEffect(() => { - if (!url) return; - initBook(); - }, [url]); - /* eslint-enable */ - - /** - * Emit Viewer Event - * - Register move event - * - Register location changed event - * - Register selection event - */ - /* eslint-disable */ - useEffect(() => { - if (!rendition) return; - - // Emit global control function - registerGlobalFunc(); - - document.addEventListener('keyup', handleKeyPress, false); - rendition.on("keyup", handleKeyPress); - rendition.on("locationChanged", onLocationChange); - selectionChanged && rendition.on('selected', selectionChanged); - - return (() => { - document.removeEventListener('keyup', handleKeyPress, false); - rendition.off("keyup", handleKeyPress); - rendition.off("locationChanged", onLocationChange); - selectionChanged && rendition.off('selected', selectionChanged); - }); - }, [ - rendition, - registerGlobalFunc, - handleKeyPress - ]); - /* eslint-enable */ - - /** Display page by changed location */ - useEffect(() => { - if (!location || !rendition) return; - - rendition.display(location); - }, [location, rendition]); - - - return (<> - {!isLoaded && loadingView} - - ); -} - -export default React.forwardRef(EpubViewer) \ No newline at end of file diff --git a/src/modules/epubViewer/style.ts b/src/modules/epubViewer/style.ts deleted file mode 100644 index 57780a0..0000000 --- a/src/modules/epubViewer/style.ts +++ /dev/null @@ -1,15 +0,0 @@ -import styled from 'styled-components' - -const EpubViewerWrapper = styled.div` - width: 100%; - box-sizing: border-box; - margin: 0 auto; - - & > div { - &::-webkit-scrollbar { - display: none; - } - } -`; - -export default EpubViewerWrapper \ No newline at end of file diff --git a/src/modules/index.ts b/src/modules/index.ts deleted file mode 100644 index e64d7b8..0000000 --- a/src/modules/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -import ReactEpubViewer from 'modules/reactViewer/ReactViewer' -import EpubViewer from 'modules/epubViewer/EpubViewer'; - -export { EpubViewer, ReactEpubViewer }; \ No newline at end of file diff --git a/src/modules/reactViewer/ReactViewer.tsx b/src/modules/reactViewer/ReactViewer.tsx deleted file mode 100644 index 3daac52..0000000 --- a/src/modules/reactViewer/ReactViewer.tsx +++ /dev/null @@ -1,292 +0,0 @@ -import React, { useState, useEffect, useRef, useCallback, useMemo } from "react" -import { Book, Rendition, Contents } from "epubjs" -// modules -import EpubViewer from "modules/epubViewer/EpubViewer" -// components -import LoadingView from 'LoadingView' -// utils -import { debounce, timeFormatter } from 'lib/utils/commonUtil' -// styles -import viewerDefaultStyles from 'modules/reactViewer/viewerStyle' -// types -import { ReactViewerProps, ViewerRef } from 'types' -import BookType, { BookStyle, BookOption } from 'types/book' - - -/** - * Epub React Viewer Module - * @class - * @param props - * @param props.url Epub file path - * @param props.viewerLayout Viewer layout - * @param props.viewerStyle Viewer style - * @param props.viewerOption Viewer option - * @param props.onBookInfoChange Run when book information changed - * @param props.onPageChange Run when page changed - * @param props.onTocChange Run when toc changed - * @param props.onSelection Run when selected - * @param props.loadingView Loading component - * @param ref Viewer ref - */ -const ReactViewer = ({ - url, - viewerLayout, - viewerStyle, - viewerOption, - onBookInfoChange, - onPageChange, - onTocChange, - onSelection, - loadingView -}: ReactViewerProps, ref: React.RefObject | any) => { - // TODO Fix the ref type correctly instead 'any' type. - const [book, setBook] = useState(null); - - const [rendition, setRendition] = useState(null); - - const [layoutStyle, setLayoutStyle] = useState({}); - - const [bookStyle, setBookStyle] = useState({ - fontFamily: 'Origin', - fontSize: 18, - lineHeight: 1.4, - marginHorizontal: 15, - marginVertical: 5 - }); - - const [bookOption, setBookOption] = useState({ - flow: "paginated", - resizeOnOrientationChange: true, - spread: "auto" - }); - - const currentSelection = useRef<{cfiRange:string, contents:Contents | null}>({ - cfiRange: '', - contents: null - }); - - - - /** - * Run book changed - * @method - * @param book Epub Book - */ - const bookChanged = (book: Book) => setBook(book); - - /** - * Run rendition changed - * @method - * @param rendition Epub Rendition - */ - const rendtionChanged = (rendition: Rendition) => setRendition(rendition); - - /** - * Run selection changed [Debounce] - * @method - * @param cfiRange CFIRange - * @param contents Selection Epub Contents - */ - const selectionChanged = (cfiRange: string, contents: Contents) => { - currentSelection.current = { cfiRange, contents }; - } - - /** - * Viewer resizing function - * @method - */ - const onResize = useMemo(() => debounce(250, () => { - if (!rendition) return; - - const viewerLayout_ = viewerLayout || { - MIN_VIEWER_WIDTH: 600, - MIN_VIEWER_HEIGHT: 300, - VIEWER_HEADER_HEIGHT: 0, - VIEWER_FOOTER_HEIGHT: 0, - VIEWER_SIDEMENU_WIDTH: 0 - }; - - const { innerWidth: win_w, innerHeight: win_h } = window; - const componentHeight = viewerLayout_.VIEWER_HEADER_HEIGHT + viewerLayout_.VIEWER_FOOTER_HEIGHT; - const w = win_w - ~~((win_w - viewerLayout_.MIN_VIEWER_WIDTH) / 100 * bookStyle.marginHorizontal); - const h = bookOption.flow === "scrolled-doc" - ? win_h - componentHeight - : win_h - componentHeight - ~~((win_h - componentHeight - viewerLayout_.MIN_VIEWER_HEIGHT) / 100 * bookStyle.marginVertical); - const marginTop = bookOption.flow === "scrolled-doc" - ? "" - : `${~~((win_h - componentHeight - viewerLayout_.MIN_VIEWER_HEIGHT) / 100 * bookStyle.marginVertical) / 2}px`; - - setLayoutStyle(l => ({ - ...l, - width: w, - height: h, - marginTop - })); - - try { - rendition.resize(w, h); - } catch { } - }), [ - rendition, - viewerLayout, - bookStyle.marginHorizontal, - bookStyle.marginVertical, - bookOption.flow - ]); - - /** - * Selection Event, run when run mouseup event - * @method
- * - Fire after the Epubjs selected event. [about 300ms] - */ - const onSelected = useCallback(async () => { - if (!ref.current) return; - - const iframe = ref.current.querySelector('iframe'); - if (!iframe) return; - - const iframeWin = iframe.contentWindow; - if (!iframeWin) return; - - const selection_ = iframeWin.getSelection(); - if (!selection_) return; - - const selectionText = selection_.toString(); - if (selectionText === "") return; - - const cfiRange: string = await new Promise(resolve => - setTimeout(() => resolve(currentSelection.current.cfiRange), 350) - ); - if (!cfiRange) return; - - const contents = currentSelection.current.contents; - if (!contents) return; - - onSelection && onSelection(cfiRange, contents); - }, [ref, onSelection]); - - - - /** Ref checker */ - useEffect(() => { - if (!ref) { - throw new Error("[React-Epub-Viewer] Put a ref argument that has a ViewerRef type."); - } - }, [ref]); - - /** Epub parsing */ - // TODO Fix the infinite re-rendering issue, when inlcude `onBookInfoChange` to dependencies array. - /* eslint-disable */ - useEffect(() => { - if (!book) return; - - Promise.all([ - book.loaded.metadata, - book.opened - ]) - .then(([metaData, bookData]: any[]) => { - const newBookData: BookType = { - coverURL: bookData.archive.urlCache[bookData.cover], - title: metaData.title, - description: metaData.description, - published_date: timeFormatter(new Date(metaData.pubdate)), - modified_date: timeFormatter(new Date(metaData.modified_date)), - author: metaData.creator, - publisher: metaData.publisher, - language: metaData.language - } - - onBookInfoChange && onBookInfoChange(newBookData); - }) - .catch(error => { - throw `${error.stack} \n\n Message : Epub parsing failed.`; - }); - }, [book]); - /* eslint-enable */ - - /** Set viewer Styles/Options */ - useEffect(() => { - viewerStyle && setBookStyle(viewerStyle); - viewerOption && setBookOption(viewerOption); - }, [viewerStyle, viewerOption]); - - /** Apply viewer Styles/Options */ - useEffect(() => { - if (!rendition) return; - - onResize(); - - const newStyle = { - "body": { - "padding-top": "0px !important", - "padding-bottom": "0px !important" - }, - "p": { - "font-size": `${bookStyle.fontSize}px !important`, - "line-height": `${bookStyle.lineHeight} !important` - }, - }; - - rendition.flow(bookOption.flow); - rendition.spread(bookOption.spread); - - if (bookStyle.fontFamily !== "Origin") { - Object.assign(newStyle.body, { - "font-family": `${bookStyle.fontFamily} !important` - }); - } - - if (bookOption.flow === "scrolled-doc") { // Scroll type - Object.assign(newStyle.body, { - "margin": "auto !important" - }); - } else if (bookOption.spread === "auto") { // View 2 pages - Object.assign(newStyle.body, { }); - } else { // View 1 page - Object.assign(newStyle.body, { }); - } - - rendition.themes.register("main", viewerDefaultStyles); - rendition.themes.register("default", newStyle); - - rendition.themes.select("main"); - }, [ - rendition, - bookStyle.fontFamily, - bookStyle.fontSize, - bookStyle.lineHeight, - bookOption, - onResize - ]); - - /** Emit screen resizing event */ - useEffect(() => { - window.addEventListener('resize', onResize); - return () => window.removeEventListener('resize', onResize); - }, [onResize]); - - /** Emit selection event */ - useEffect(() => { - if (!rendition) return; - rendition.on("mouseup", onSelected); - return () => rendition.off("mouseup", onSelected); - }, [rendition, onSelected]); - - - - return (<> - } - ref={ref} - /> - ); -} - -export default React.forwardRef(ReactViewer) \ No newline at end of file diff --git a/src/modules/reactViewer/viewerStyle.ts b/src/modules/reactViewer/viewerStyle.ts deleted file mode 100644 index b7a0523..0000000 --- a/src/modules/reactViewer/viewerStyle.ts +++ /dev/null @@ -1,16 +0,0 @@ -const viewerStyle = { - "::selection": { - "background-color": "#d4e9ff" - }, - - "img": { - "-webkit-touch-callout": "none", - "-webkit-user-select": "none", - "-khtml-user-select": "none", - "-moz-user-select": "none", - "-ms-user-select": "none", - "user-select": "none" - } -} - -export default viewerStyle \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 1fe252e..7fdbca5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9522,6 +9522,13 @@ react-dom@^17.0.2: object-assign "^4.1.1" scheduler "^0.20.2" +react-epub-viewer@^0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/react-epub-viewer/-/react-epub-viewer-0.1.7.tgz#4360bdb4ca3f7e0df22b813d4323ae1290be2f60" + integrity sha512-QWEi0GxDCGOCONC3GNsLP4K6KU23c9SuW55U/5C2rzWnJuTVWrx4d3qivqwTH+ypMhJ0UXG4+xxUSdz1dZbtqw== + dependencies: + epubjs "^0.3.88" + react-error-overlay@^6.0.9: version "6.0.9" resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.9.tgz#3c743010c9359608c375ecd6bc76f35d93995b0a"