parent
2a0ca26c8c
commit
45d054571d
9 changed files with 21 additions and 624 deletions
@ -1,293 +0,0 @@ |
||||
import React, { useState, useEffect, useCallback, useRef } from 'react' |
||||
import { Book, Rendition } from "epubjs"; |
||||
// style
|
||||
import EpubViewerWrapper from 'modules/epubViewer/style' |
||||
// types
|
||||
import { EpubViewerProps, ViewerRef } from 'types' |
||||
import Toc from 'types/toc' |
||||
import Loc from 'types/loc' |
||||
|
||||
|
||||
/** |
||||
* EpubViewer Module |
||||
* @class |
||||
* @param props |
||||
* @param props.url Epub path |
||||
* @param props.epubFileOptions Epub file option |
||||
* @param props.epubOptions Epub viewer option |
||||
* @param props.style Epub Wrapper style |
||||
* @param props.location Epub CFI or href |
||||
* @param props.bookChanged Run when book changed |
||||
* @param props.rendtionChanged Run when rendition changed |
||||
* @param props.pageChanged Run when page changed |
||||
* @param props.tocChanged Run when toc changed |
||||
* @param props.selectionChanged Run when selected |
||||
* @param props.loadingView Loading Component |
||||
* @param ref Viewer ref |
||||
*/ |
||||
const EpubViewer = ({ |
||||
url,
|
||||
epubFileOptions,
|
||||
epubOptions, |
||||
style, |
||||
location, |
||||
bookChanged, |
||||
rendtionChanged, |
||||
pageChanged, |
||||
tocChanged, |
||||
selectionChanged, |
||||
loadingView, |
||||
}: EpubViewerProps, ref: React.RefObject<ViewerRef> | any) => { |
||||
// TODO Fix the ref type correctly instead 'any' type.
|
||||
const [isLoaded, setIsLoaded] = useState(false); |
||||
|
||||
const [book, setBook] = useState<Book | null>(null); |
||||
|
||||
const [rendition, setRendition] = useState<Rendition | null>(null); |
||||
|
||||
const [toc, setToc] = useState<Toc[]>([]); |
||||
|
||||
const currentCfi = useRef<string>(''); |
||||
|
||||
|
||||
|
||||
/** |
||||
* 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} |
||||
<EpubViewerWrapper ref={ref} style={style} /> |
||||
</>); |
||||
} |
||||
|
||||
export default React.forwardRef(EpubViewer) |
@ -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 |
@ -1,4 +0,0 @@ |
||||
import ReactEpubViewer from 'modules/reactViewer/ReactViewer' |
||||
import EpubViewer from 'modules/epubViewer/EpubViewer'; |
||||
|
||||
export { EpubViewer, ReactEpubViewer }; |
@ -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<ViewerRef> | any) => { |
||||
// TODO Fix the ref type correctly instead 'any' type.
|
||||
const [book, setBook] = useState<Book | null>(null); |
||||
|
||||
const [rendition, setRendition] = useState<Rendition | null>(null); |
||||
|
||||
const [layoutStyle, setLayoutStyle] = useState<Object>({}); |
||||
|
||||
const [bookStyle, setBookStyle] = useState<BookStyle>({ |
||||
fontFamily: 'Origin', |
||||
fontSize: 18, |
||||
lineHeight: 1.4, |
||||
marginHorizontal: 15, |
||||
marginVertical: 5 |
||||
}); |
||||
|
||||
const [bookOption, setBookOption] = useState<BookOption>({ |
||||
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 <br/> |
||||
* - 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 (<> |
||||
<EpubViewer
|
||||
url={url}
|
||||
style={layoutStyle} |
||||
bookChanged={bookChanged}
|
||||
rendtionChanged={rendtionChanged}
|
||||
tocChanged={onTocChange}
|
||||
pageChanged={onPageChange} |
||||
selectionChanged={selectionChanged} |
||||
loadingView={loadingView || <LoadingView />} |
||||
ref={ref} |
||||
/> |
||||
</>); |
||||
} |
||||
|
||||
export default React.forwardRef(ReactViewer) |
@ -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 |
Loading…
Reference in new issue