├── .github ├── CODEOWNERS ├── FUNDING.yml ├── renovate.json ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ └── codeql-analysis.yml ├── src ├── react-app-env.d.ts ├── types │ ├── snackbar.ts │ ├── toc.ts │ ├── loc.ts │ ├── page.ts │ ├── selection.ts │ ├── viewerLayout.ts │ ├── highlight.ts │ ├── book.ts │ └── index.d.ts ├── modules │ ├── index.ts │ ├── epubViewer │ │ ├── styles.ts │ │ └── EpubViewer.tsx │ └── reactViewer │ │ ├── viewerStyle.ts │ │ └── ReactViewer.tsx ├── setupTests.ts ├── LoadingView.tsx ├── index.tsx └── lib │ └── utils │ └── commonUtil.ts ├── .npmignore ├── public ├── files │ └── Alices Adventures in Wonderland.epub ├── css │ └── index.css └── index.html ├── .eslintignore ├── .prettierignore ├── .gitmessage.txt ├── .prettierrc ├── .babelrc ├── tsconfig.json ├── CONTRIBUTING.md ├── LICENSE ├── .eslintrc.json ├── package.json ├── .gitignore ├── README.md └── CODE_OF_CONDUCT.md /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @altmshfkgudtjr -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [altmshfkgudtjr] 4 | -------------------------------------------------------------------------------- /src/types/snackbar.ts: -------------------------------------------------------------------------------- 1 | type Snackbar = 'INFO' | 'SUCCESS' | 'WARNING' | 'ERROR'; 2 | 3 | export default Snackbar; 4 | -------------------------------------------------------------------------------- /src/types/toc.ts: -------------------------------------------------------------------------------- 1 | /** @type 목차 단일 Spine 타입 */ 2 | interface Toc { 3 | label: string; 4 | href: string; 5 | } 6 | 7 | export default Toc; 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | test/ 3 | .gitignore 4 | .babelrc 5 | .babelrc.js 6 | tsconfig.json 7 | public 8 | build 9 | desktop.ini 10 | .DS_Store 11 | .github 12 | -------------------------------------------------------------------------------- /public/files/Alices Adventures in Wonderland.epub: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/altmshfkgudtjr/react-epub-viewer/HEAD/public/files/Alices Adventures in Wonderland.epub -------------------------------------------------------------------------------- /src/modules/index.ts: -------------------------------------------------------------------------------- 1 | import ReactEpubViewer from 'modules/reactViewer/ReactViewer'; 2 | import EpubViewer from 'modules/epubViewer/EpubViewer'; 3 | 4 | export { EpubViewer, ReactEpubViewer }; 5 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # ---------------------------------- Tyepscript --------------------------------- # 2 | tsconfig.json 3 | 4 | # ------------------------------------ Assets ----------------------------------- # 5 | public/**/* 6 | node_modules/**/* -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # ---------------------------------- Tyepscript --------------------------------- # 2 | tsconfig.json 3 | 4 | # ------------------------------------ Assets ----------------------------------- # 5 | public/**/* 6 | node_modules/**/* -------------------------------------------------------------------------------- /public/css/index.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | width: 100vw; 6 | height: 100vh; 7 | } 8 | 9 | #root { 10 | height: 100%; 11 | display: flex; 12 | align-items: center; 13 | justify-content: center; 14 | } 15 | -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /src/types/loc.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @type Epub CFI 타입 3 | * @example 4 | * "epubcfi(/6/2[titlepage1]!/4/1:0)" 5 | */ 6 | type EpubCFI = string; 7 | 8 | /** @type Epub 위치 타입 */ 9 | interface Loc { 10 | index: number; 11 | href: string; 12 | start: EpubCFI; 13 | end: EpubCFI; 14 | percentage: number; 15 | } 16 | 17 | export default Loc; 18 | -------------------------------------------------------------------------------- /src/modules/epubViewer/styles.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * EpubViewer wrapper style 3 | */ 4 | const styles: { 5 | boxSizing: any; 6 | margin: any; 7 | width: any; 8 | height: any; 9 | overflowY: any; 10 | } = { 11 | boxSizing: 'border-box', 12 | margin: '0px auto', 13 | width: '100%', 14 | height: '100%', 15 | overflowY: 'hidden', 16 | }; 17 | 18 | export default styles; 19 | -------------------------------------------------------------------------------- /src/modules/reactViewer/viewerStyle.ts: -------------------------------------------------------------------------------- 1 | const viewerStyle = { 2 | '::selection': { 3 | 'background-color': '#d4e9ff', 4 | }, 5 | 6 | img: { 7 | '-webkit-touch-callout': 'none', 8 | '-webkit-user-select': 'none', 9 | '-khtml-user-select': 'none', 10 | '-moz-user-select': 'none', 11 | '-ms-user-select': 'none', 12 | 'user-select': 'none', 13 | }, 14 | }; 15 | 16 | export default viewerStyle; 17 | -------------------------------------------------------------------------------- /src/types/page.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @type Page 3 | * @param chapterName 현재 챕터 4 | * @param currentpage 현재 페이지 5 | * @param totalPage 총 페이지 수 6 | * @param startCfi 현재 페이지 시작 CFI 7 | * @param endCfi 현재 페이지 끝 CFI 8 | * @param base 현재 페이지 CFI base 9 | */ 10 | interface Page { 11 | chapterName: string; 12 | currentPage: number; 13 | totalPage: number; 14 | startCfi: string; 15 | endCfi: string; 16 | base: string; 17 | } 18 | 19 | export default Page; 20 | -------------------------------------------------------------------------------- /.gitmessage.txt: -------------------------------------------------------------------------------- 1 | # Please write in this format 2 | # : 3 | 4 | ################ 5 | # Write body 6 | 7 | ################ 8 | # Write footer or issue number 9 | 10 | ################ 11 | # feature : New feature 12 | # fix : Fix bug 13 | # docs : Fix docs 14 | # test : Mofiy test code 15 | # refactor : Refactoring code 16 | # style : Changes that don't affect code semantics 17 | # chore : Build parts or package manager modifications 18 | ################ -------------------------------------------------------------------------------- /src/types/selection.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @type Selection 타입 3 | * @param update Highlight 클릭 진입 여부 4 | * @param x Contextmenu X 좌표 5 | * @param y Contextmenu Y 좌표 6 | * @param height Selection 높이 7 | * @param cfiRange 선택된 cfi 범위 8 | * @param content 선택된 텍스트 9 | */ 10 | interface Selection { 11 | update: boolean; 12 | x: number; 13 | y: number; 14 | height: number; 15 | cfiRange: string; 16 | content: string; 17 | } 18 | 19 | export default Selection; 20 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "printWidth": 80, 4 | "useTabs": false, 5 | "tabWidth": 2, 6 | "semi": true, 7 | "quoteProps": "as-needed", 8 | "jsxSingleQuote": false, 9 | "trailingComma": "all", 10 | "arrowParens": "avoid", 11 | "endOfLine": "lf", 12 | "bracketSpacing": true, 13 | "jsxBracketSameLine": false, 14 | "requirePragma": false, 15 | "insertPragma": false, 16 | "proseWrap": "preserve", 17 | "vueIndentScriptAndStyle": false 18 | } 19 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html lang="en"> 3 | <head> 4 | <meta charset="UTF-8"> 5 | <meta http-equiv="X-UA-Compatible" content="IE=edge"> 6 | <meta name="viewport" content="width=device-width, initial-scale=1.0"> 7 | <title>React Epub Viewer 8 | 12 | 13 | 14 | 15 |
16 | 17 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [":timezone(Asia/Seoul)", "config:js-app"], 3 | "labels": ["dependencies"], 4 | "npm": { 5 | "separateMinorPatch": true, 6 | "packageRules": [ 7 | { 8 | "packagePatterns": ["^@types/"], 9 | "automerge": true, 10 | "major": { 11 | "automerge": false 12 | } 13 | }, 14 | { 15 | "groupName": "CODE_SYNTAX", 16 | "packageNames": ["eslint", "prettier"], 17 | "packagePatterns": ["^eslint-", "^prettier-"] 18 | } 19 | ] 20 | }, 21 | "enabledManagers": ["npm"], 22 | "ignorePaths": [] 23 | } 24 | -------------------------------------------------------------------------------- /src/types/viewerLayout.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Epub viewer layout size type 3 | * @type 4 | * @param MIN_VIEWER_WIDTH Viewer min width (px) 5 | * @param MIN_VIEWER_HEIGHT Viewer min height (px) 6 | * @param VIEWER_HEADER_HEIGHT Viewer header height (px) 7 | * @param VIEWER_FOOTER_HEIGHT Viewer footer height (px) 8 | * @param VIEWER_SIDEMENU_WIDTH Viewer sideMenu width (px) 9 | */ 10 | interface ViewerLayout { 11 | MIN_VIEWER_WIDTH: number; 12 | MIN_VIEWER_HEIGHT: number; 13 | VIEWER_HEADER_HEIGHT: number; 14 | VIEWER_FOOTER_HEIGHT: number; 15 | VIEWER_SIDEMENU_WIDTH: number; 16 | } 17 | 18 | export default ViewerLayout; 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | [ 5 | "@babel/preset-react", 6 | { 7 | "runtime": "automatic" 8 | } 9 | ], 10 | "@babel/preset-typescript" 11 | ], 12 | "plugins": [ 13 | [ 14 | "module-resolver", 15 | { 16 | "root": "./src", 17 | "extensions": [ 18 | ".ts", 19 | ".tsx" 20 | ] 21 | } 22 | ], 23 | "@babel/plugin-proposal-class-properties", 24 | "@babel/plugin-proposal-object-rest-spread", 25 | "@babel/plugin-proposal-optional-chaining" 26 | ], 27 | "ignore": [ 28 | "src/components", 29 | "src/containers", 30 | "src/lib/hooks", 31 | "src/lib/styles", 32 | "src/lib/svg", 33 | "src/slices", 34 | "src/types", 35 | "src/index.tsx", 36 | "src/react-app-env.d.ts", 37 | "src/setupTests.ts" 38 | ] 39 | } -------------------------------------------------------------------------------- /src/types/highlight.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @type 하이라이트 타입 3 | * @param key 식별값 (paragraphCfi + cfiRange) 4 | * @param accessTime 접근 시간 5 | * @param createTime 생성 시간 6 | * @param color 하이라이트 색상 (HEX 값) 7 | * @param paragraphCfi 하이라이트 단락 CFI 8 | * @param cfiRange 하이라이트 CFI 범위 9 | * @param chapterName 하이라이트 챕터명 10 | * @param pageNum 하이라이트 페이지 11 | * @param content 하이라이트 텍스트 12 | */ 13 | interface Highlight { 14 | key: string; 15 | accessTime: string; 16 | createTime: string; 17 | color: string; 18 | paragraphCfi: string; 19 | cfiRange: string; 20 | chpaterName: string; 21 | pageNum: number; 22 | content: string; 23 | } 24 | 25 | /** 26 | * @type 색상 타입 27 | * @param name 색상명 28 | * @param code 색상 Hex 코드 29 | */ 30 | export interface Color { 31 | name: string; 32 | code: string; 33 | } 34 | 35 | export default Highlight; 36 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "declaration": true, 10 | "outDir": "./dist", 11 | "allowJs": true, 12 | "skipLibCheck": true, 13 | "esModuleInterop": true, 14 | "allowSyntheticDefaultImports": true, 15 | "strict": true, 16 | "forceConsistentCasingInFileNames": true, 17 | "noFallthroughCasesInSwitch": true, 18 | "module": "esnext", 19 | "moduleResolution": "node", 20 | "resolveJsonModule": true, 21 | "isolatedModules": true, 22 | "noEmit": true, 23 | "jsx": "react-jsx", 24 | "sourceMap": false, 25 | "typeRoots": [ 26 | "./node_modules/@types", 27 | "./@types" 28 | ], 29 | "baseUrl": "./src" 30 | }, 31 | "include": [ 32 | "src" 33 | ] 34 | } -------------------------------------------------------------------------------- /src/LoadingView.tsx: -------------------------------------------------------------------------------- 1 | import { CSSProperties } from 'react'; 2 | 3 | const LoadingView = () => { 4 | return ( 5 |
6 |
7 |
Loading
8 |
9 |
10 | ); 11 | }; 12 | 13 | const LoadingWrapper: CSSProperties = { 14 | width: '100%', 15 | height: '100%', 16 | display: 'flex', 17 | alignItems: 'center', 18 | justifyContent: 'center', 19 | }; 20 | 21 | const Wrapper: CSSProperties = { 22 | width: '100%', 23 | height: '30px', 24 | margin: '30px 0px', 25 | textAlign: 'center', 26 | }; 27 | 28 | const Content: CSSProperties = { 29 | display: 'inline-block', 30 | width: 'auto', 31 | height: '24px', 32 | color: '#3972ff', 33 | padding: '0 8px', 34 | borderBottom: '3px solid #3972ff', 35 | }; 36 | 37 | export default LoadingView; 38 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { EpubViewer, ReactEpubViewer } from 'modules'; 4 | import { ViewerRef } from 'types'; 5 | 6 | interface Props { 7 | VIEWER_TYPE?: 'ReactViewer' | 'EpubViewer'; 8 | } 9 | 10 | const App = ({ VIEWER_TYPE = 'ReactViewer' }: Props) => { 11 | const EPUB_URL = 12 | '/react-epub-viewer/files/Alices Adventures in Wonderland.epub'; 13 | const ref = useRef(null); 14 | 15 | return ( 16 | <> 17 | {VIEWER_TYPE === 'ReactViewer' && ( 18 | <> 19 | 20 | 21 | )} 22 | {VIEWER_TYPE === 'EpubViewer' && ( 23 | <> 24 | 25 | 26 | )} 27 | 28 | ); 29 | }; 30 | 31 | ReactDOM.render(, document.getElementById('root')); 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to React-Epub_Viewer 2 | 3 | 🎈 First off, thanks for taking the time to contribute! 🎈 4 | 5 | The following is a set of guidelines for contributing to `React-Epub_Viewer`, which are hosted by NB. These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request. 6 | 7 | First open an issue and let us know what you're contributing to! 8 | 9 | --- 10 | 11 | **Table Of Contents** 12 | 13 | [Code Commit](#Code-Commit) 14 | 15 | 16 | 17 | --- 18 | 19 | # Code Commit 20 | 21 | 1. Please create a branch in this format, **`-`** 22 | 2. Open a terminal and navigate to your project path. And enter this. 23 | **`git config --global commit.template .gitmessage.txt`** 24 | 3. You can use the template, with `git commit` through vi. **Not** `git commit -m` 25 | 4. If you want to merge your work, please pull request to the `dev` branch 26 | 5. Enjoy contributing! 27 | 28 | 29 | --- 30 | 31 | If you have any other opinions, please feel free to suggest! 😀 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 NB 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/types/book.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @type 3 | * @param coverURL 표지 사진 url 4 | * @param title 제목 5 | * @param description 설명 6 | * @param published_date 출판일 7 | * @param modified_date 수정일 8 | * @param author 저자 9 | * @param publisher 발행자 10 | * @param language 도서 언어 11 | */ 12 | type Book = { 13 | coverURL: string; 14 | title: string; 15 | description: string; 16 | published_date: string; 17 | modified_date: string; 18 | author: string; 19 | publisher: string; 20 | language: string; 21 | }; 22 | 23 | /** 24 | * @type 25 | * @param fontFamily 폰트 26 | * @param fontSize 폰트 크기 27 | * @param lineHeight 줄 간격 28 | * @param marginHorizontal 가로 여백 29 | * @param marginVertical 세로 여백 30 | */ 31 | export type BookStyle = { 32 | fontFamily: BookFontFamily; 33 | fontSize: number; 34 | lineHeight: number; 35 | marginHorizontal: number; 36 | marginVertical: number; 37 | }; 38 | 39 | /** 40 | * @type 41 | * - Origin: 원본 42 | * - *: 커스텀 폰트 43 | */ 44 | export type BookFontFamily = 'Origin' | 'Roboto'; 45 | 46 | export type BookFlow = 'paginated' | 'scrolled-doc'; 47 | 48 | /** 49 | * @type 50 | * @param flow 가로읽기 or 세로읽기(스크롤) 51 | * @param resizeOnOrientationChange 방향 전환시 크기 조절 여부 52 | * @param spread 펼쳐보기 여부 53 | */ 54 | export type BookOption = { 55 | flow: BookFlow; 56 | resizeOnOrientationChange: boolean; 57 | spread: 'auto' | 'none'; 58 | }; 59 | 60 | export default Book; 61 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "parserOptions": { 4 | "project": ["./tsconfig.json"], 5 | "ecmaFeatures": { 6 | "jsx": true 7 | }, 8 | "ecmaVersion": 11, 9 | "sourceType": "module" 10 | }, 11 | 12 | "plugins": ["react", "react-hooks", "@typescript-eslint", "@typescript-eslint/eslint-plugin"], 13 | 14 | "extends": ["eslint:recommended", "plugin:react/recommended", "prettier"], 15 | 16 | "settings": { 17 | "react": { 18 | "version": "detect" 19 | } 20 | }, 21 | 22 | "env": { 23 | "browser": true, 24 | "commonjs": true, 25 | "es2020": true, 26 | "node": true, 27 | "jest": true 28 | }, 29 | 30 | "rules": { 31 | "no-console": "off", 32 | "no-unused-vars": "warn", 33 | "no-unreachable": "warn", 34 | "strict": ["error", "global"], 35 | "curly": "warn", 36 | 37 | /* React Options */ 38 | "react/jsx-uses-react": "off", 39 | "react/react-in-jsx-scope": "off", 40 | "react/prefer-stateless-function": 0, 41 | "react/jsx-filename-extension": 0, 42 | "react/jsx-one-expression-per-line": 0, 43 | "react/prop-types": 0, 44 | 45 | /* React Hooks Options */ 46 | "react-hooks/rules-of-hooks": "error", 47 | "react-hooks/exhaustive-deps": "warn" 48 | }, 49 | 50 | "overrides": [ 51 | { 52 | "files": ["*.ts", "*.tsx"], 53 | "rules": { 54 | "no-undef": "off", 55 | "no-unused-vars": "off", 56 | "@typescript-eslint/no-unused-vars": 1 57 | } 58 | } 59 | ] 60 | } 61 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '38 6 * * 1' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v2 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v1 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 54 | # If this step fails, then you should remove it and run the build manually (see below) 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@v1 57 | 58 | # ℹ️ Command-line programs to run using the OS shell. 59 | # 📚 https://git.io/JvXDl 60 | 61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 62 | # and modify them (or add more) to build your code if your project 63 | # uses a compiled language 64 | 65 | #- run: | 66 | # make bootstrap 67 | # make release 68 | 69 | - name: Perform CodeQL Analysis 70 | uses: github/codeql-action/analyze@v1 71 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-epub-viewer", 3 | "version": "0.2.0", 4 | "author": { 5 | "name": "NB", 6 | "email": "altmshfkgudtjr@naver.com" 7 | }, 8 | "description": "Epub viewer for React.js powered by Epub.js", 9 | "license": "MIT", 10 | "private": false, 11 | "keywords": [ 12 | "epub", 13 | "viewer", 14 | "React" 15 | ], 16 | "main": "lib/modules/index.js", 17 | "files": [ 18 | "lib", 19 | "README.md" 20 | ], 21 | "homepage": "https://altmshfkgudtjr.github.io/react-epub-viewer", 22 | "repository": { 23 | "type": "git", 24 | "url": "https://github.com/altmshfkgudtjr/react-epub-viewer" 25 | }, 26 | "bugs": { 27 | "url": "https://github.com/altmshfkgudtjr/react-epub-viewer/issues", 28 | "email": "altmshfkgudtjr@naver.com" 29 | }, 30 | "scripts": { 31 | "start": "react-scripts start", 32 | "prebuild": "react-scripts test --watchAll=false --passWithNoTests", 33 | "build": " react-scripts build", 34 | "test": "react-scripts test", 35 | "clean": "rimraf lib", 36 | "precompile": "react-scripts test --watchAll=false --passWithNoTests && yarn clean", 37 | "compile": "babel src --out-dir lib --extensions .ts,.tsx", 38 | "postcompile": "cp -r ./src/types ./lib/types" 39 | }, 40 | "dependencies": { 41 | "epubjs": "0.3.93", 42 | "react-scripts": "5.0.0" 43 | }, 44 | "devDependencies": { 45 | "@babel/cli": "7.18.10", 46 | "@babel/plugin-proposal-class-properties": "7.18.6", 47 | "@babel/plugin-proposal-object-rest-spread": "7.18.9", 48 | "@babel/plugin-proposal-optional-chaining": "7.18.9", 49 | "@babel/preset-env": "7.18.10", 50 | "@babel/preset-react": "7.18.6", 51 | "@babel/preset-typescript": "7.18.6", 52 | "@testing-library/jest-dom": "5.16.5", 53 | "@testing-library/react": "13.3.0", 54 | "@testing-library/user-event": "12.8.3", 55 | "@types/jest": "26.0.24", 56 | "@types/node": "16.11.49", 57 | "@types/react": "17.0.50", 58 | "@types/react-dom": "17.0.17", 59 | "@types/styled-components": "5.1.26", 60 | "@typescript-eslint/eslint-plugin": "5.33.1", 61 | "babel-plugin-module-resolver": "4.0.0", 62 | "eslint": "8.24.0", 63 | "eslint-config-prettier": "8.5.0", 64 | "eslint-plugin-react": "7.31.8", 65 | "eslint-plugin-react-hooks": "4.6.0", 66 | "prettier": "2.7.1", 67 | "react": "17.0.2", 68 | "react-dom": "17.0.2", 69 | "styled-components": "5.3.5", 70 | "typescript": "4.7.4" 71 | }, 72 | "peerDependencies": { 73 | "react": ">=17.0.1", 74 | "react-dom": ">=17.0.1" 75 | }, 76 | "eslintConfig": { 77 | "extends": [ 78 | "react-app", 79 | "react-app/jest" 80 | ] 81 | }, 82 | "browserslist": { 83 | "production": [ 84 | ">0.2%", 85 | "not dead", 86 | "not op_mini all" 87 | ], 88 | "development": [ 89 | "last 1 chrome version", 90 | "last 1 firefox version", 91 | "last 1 safari version" 92 | ] 93 | }, 94 | "types": "lib/types/index.d.ts" 95 | } 96 | -------------------------------------------------------------------------------- /src/types/index.d.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Book, { BookOptions } from 'epubjs/types/book'; 3 | import { RenditionOptions } from 'epubjs/types/rendition'; 4 | import { Contents, Rendition } from 'epubjs'; 5 | import Toc from 'types/toc'; 6 | import Page from 'types/page'; 7 | import ViewerLayout from 'types/viewerLayout'; 8 | import BookType, { BookStyle, BookOption } from 'types/book'; 9 | 10 | /** 11 | * DOM Element wrapping the Epub viewer 12 | * - Provides special methods. 13 | */ 14 | export interface ViewerRef extends HTMLDivElement { 15 | /** Move the viewer to the previous page */ 16 | prevPage: () => void; 17 | /** Move the viewer to the next page */ 18 | nextPage: () => void; 19 | /** Get CFI in current page */ 20 | getCurrentCfi: () => string; 21 | /** 22 | * Highlighting specific CFIRange 23 | * @param cfiRange CFIRange 24 | * @param callback Callback after click highlight 25 | * @param color Highlight color 26 | */ 27 | onHighlight: ( 28 | cfiRange: string, 29 | callback?: (e: any) => void, 30 | color?: string, 31 | ) => void; 32 | /** 33 | * Remove specific highlight 34 | * @param cfiRange CFIRange 35 | */ 36 | offHighlight: (cfiRange: string) => void; 37 | /** 38 | * Move the viewer to the cfi or href 39 | * @param location CFI or Href 40 | */ 41 | setLocation: (location: string) => void; 42 | } 43 | 44 | /** 45 | * Epub Viewer Props 46 | * @type 47 | * @param url Epub file path 48 | * @param epubFileOptions Epub file option 49 | * @param epubOptions Epub viewer option 50 | * @param style Epub Wrapper style 51 | * @param location Epub CFI or href 52 | * @param bookChanged Run when book changed 53 | * @param rendtionChanged Run when rendition changed 54 | * @param pageChanged Run when page changed 55 | * @param tocChanged Run when toc changed 56 | * @param selectionChanged Run when selected 57 | * @param loadingView Loading Component 58 | */ 59 | export interface EpubViewerProps { 60 | url: string; 61 | epubFileOptions?: BookOptions; 62 | epubOptions?: RenditionOptions; 63 | style?: React.CSSProperties; 64 | location?: string; 65 | bookChanged?(book: Book): void; 66 | rendtionChanged?(rendition: Rendition): void; 67 | pageChanged?(page: Page): void; 68 | tocChanged?(value: Toc[]): void; 69 | selectionChanged?(cfiRange: string, contents: Contents): void; 70 | loadingView?: React.ReactNode; 71 | } 72 | 73 | declare class EpubViewer extends React.Component {} 74 | 75 | /** 76 | * React Epub Viewer Props 77 | * @type 78 | * @param url Epub file path 79 | * @param viewerLayout Viewer layout 80 | * @param viewerStyle Viewer style 81 | * @param viewerStyleURL Viewer style - CSS URL 82 | * @param viewerOption Viewer option 83 | * @param onBookInfoChange Run when book information changed 84 | * @param onPageChange Run when page changed 85 | * @param onTocChange Run when toc changed 86 | * @param onSelection Run when selected 87 | * @param loadingView Loading component 88 | */ 89 | export interface ReactViewerProps { 90 | url: string; 91 | viewerLayout?: ViewerLayout; 92 | viewerStyle?: BookStyle; 93 | viewerStyleURL?: string; 94 | viewerOption?: BookOption; 95 | onBookInfoChange?: (book: BookType) => void; 96 | onPageChange?: (page: Page) => void; 97 | onTocChange?: (toc: Toc[]) => void; 98 | onSelection?: (cfiRange: string, contents: Contents) => void; 99 | loadingView?: React.ReactNode; 100 | } 101 | 102 | declare class ReactEpubViewer extends React.Component< 103 | ReactViewerProps, 104 | ViewerRef 105 | > {} 106 | 107 | export { EpubViewer, ReactEpubViewer }; 108 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/windows,macos,linux,node 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=windows,macos,linux,node 4 | 5 | ### Linux ### 6 | *~ 7 | 8 | # temporary files which can be created if a process still has a handle open of a deleted file 9 | .fuse_hidden* 10 | 11 | # KDE directory preferences 12 | .directory 13 | 14 | # Linux trash folder which might appear on any partition or disk 15 | .Trash-* 16 | 17 | # .nfs files are created when an open file is removed but is still being accessed 18 | .nfs* 19 | 20 | ### macOS ### 21 | # General 22 | .DS_Store 23 | .AppleDouble 24 | .LSOverride 25 | 26 | # Icon must end with two \r 27 | Icon 28 | 29 | 30 | # Thumbnails 31 | ._* 32 | 33 | # Files that might appear in the root of a volume 34 | .DocumentRevisions-V100 35 | .fseventsd 36 | .Spotlight-V100 37 | .TemporaryItems 38 | .Trashes 39 | .VolumeIcon.icns 40 | .com.apple.timemachine.donotpresent 41 | 42 | # Directories potentially created on remote AFP share 43 | .AppleDB 44 | .AppleDesktop 45 | Network Trash Folder 46 | Temporary Items 47 | .apdisk 48 | 49 | ### Node ### 50 | # Logs 51 | logs 52 | *.log 53 | npm-debug.log* 54 | yarn-debug.log* 55 | yarn-error.log* 56 | lerna-debug.log* 57 | 58 | # Diagnostic reports (https://nodejs.org/api/report.html) 59 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 60 | 61 | # Runtime data 62 | pids 63 | *.pid 64 | *.seed 65 | *.pid.lock 66 | 67 | # Directory for instrumented libs generated by jscoverage/JSCover 68 | lib-cov 69 | 70 | # Coverage directory used by tools like istanbul 71 | coverage 72 | *.lcov 73 | 74 | # nyc test coverage 75 | .nyc_output 76 | 77 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 78 | .grunt 79 | 80 | # Bower dependency directory (https://bower.io/) 81 | bower_components 82 | 83 | # node-waf configuration 84 | .lock-wscript 85 | 86 | # Compiled binary addons (https://nodejs.org/api/addons.html) 87 | build/Release 88 | 89 | # Dependency directories 90 | node_modules/ 91 | jspm_packages/ 92 | 93 | # TypeScript v1 declaration files 94 | typings/ 95 | 96 | # TypeScript cache 97 | *.tsbuildinfo 98 | 99 | # Optional npm cache directory 100 | .npm 101 | 102 | # Optional eslint cache 103 | .eslintcache 104 | 105 | # Optional stylelint cache 106 | .stylelintcache 107 | 108 | # Microbundle cache 109 | .rpt2_cache/ 110 | .rts2_cache_cjs/ 111 | .rts2_cache_es/ 112 | .rts2_cache_umd/ 113 | 114 | # Optional REPL history 115 | .node_repl_history 116 | 117 | # Output of 'npm pack' 118 | *.tgz 119 | 120 | # Yarn Integrity file 121 | .yarn-integrity 122 | 123 | # dotenv environment variables file 124 | .env 125 | .env.test 126 | .env*.local 127 | 128 | # parcel-bundler cache (https://parceljs.org/) 129 | .cache 130 | .parcel-cache 131 | 132 | # Next.js build output 133 | .next 134 | 135 | # Nuxt.js build / generate output 136 | .nuxt 137 | dist 138 | 139 | # Storybook build outputs 140 | .out 141 | .storybook-out 142 | storybook-static 143 | 144 | # rollup.js default build output 145 | dist/ 146 | 147 | # Gatsby files 148 | .cache/ 149 | # Comment in the public line in if your project uses Gatsby and not Next.js 150 | # https://nextjs.org/blog/next-9-1#public-directory-support 151 | # public 152 | 153 | # vuepress build output 154 | .vuepress/dist 155 | 156 | # Serverless directories 157 | .serverless/ 158 | 159 | # FuseBox cache 160 | .fusebox/ 161 | 162 | # DynamoDB Local files 163 | .dynamodb/ 164 | 165 | # TernJS port file 166 | .tern-port 167 | 168 | # Stores VSCode versions used for testing VSCode extensions 169 | .vscode-test 170 | 171 | # Temporary folders 172 | tmp/ 173 | temp/ 174 | 175 | ### Windows ### 176 | # Windows thumbnail cache files 177 | Thumbs.db 178 | Thumbs.db:encryptable 179 | ehthumbs.db 180 | ehthumbs_vista.db 181 | 182 | # Dump file 183 | *.stackdump 184 | 185 | # Folder config file 186 | [Dd]esktop.ini 187 | 188 | # Recycle Bin used on file shares 189 | $RECYCLE.BIN/ 190 | 191 | # Windows Installer files 192 | *.cab 193 | *.msi 194 | *.msix 195 | *.msm 196 | *.msp 197 | 198 | # Windows shortcuts 199 | *.lnk 200 | 201 | # End of https://www.toptal.com/developers/gitignore/api/windows,macos,linux,node 202 | 203 | # Custom ignore files or folders 204 | 205 | /lib 206 | build -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |
3 | react_epub 4 |
5 |
6 |
7 | 8 | 9 | # React-Epub-Viewer 10 | 11 | [![Latest Stable Version](https://img.shields.io/npm/v/react-epub-viewer.svg?style=for-the-badge)](https://www.npmjs.com/package/react-epub-viewer) [![License](https://img.shields.io/badge/license-mit-red.svg?style=for-the-badge)](https://www.npmjs.com/package/react-epub-viewer) 12 | 13 | **React-Epub-Viewer** is Epub Viewer for React.js powered by [Epub.js](https://github.com/futurepress/epub.js/) v0.3 14 | 15 | You can use React-Epub-Viewer together with React. 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | ## Getting Started 24 | 25 | 📢 **[Online Demo](https://altmshfkgudtjr.github.io/react-epub-viewer)** 26 | 👉 You can check the **[Demo Code](https://github.com/altmshfkgudtjr/react-epub-viewer/tree/demo)** 27 | 28 | 29 | 30 | **Features** 31 | 32 | - Table of contents 33 | - Setting 34 | - Font 35 | - Font size 36 | - Line height 37 | - Viewer horizontal margin 38 | - Viewer vertical margin 39 | - Change viewer type 40 | - Scrolled-doc [`true`/`false`] 41 | - Spread [`true`/`false`] 42 | 43 | - Current Page Information 44 | - Current chapter name 45 | - Current page number 46 | - Total page number 47 | - Move page by arrow keys 48 | - Highlight (Using `mouseup` event) 49 | - Select highlight color 50 | 51 | 52 | 53 | ### Getting the Code 54 | 55 | Install library from [NPM](https://www.npmjs.com/package/react-epub-viewer) 56 | 57 | ```shell 58 | npm install react-epub-viewer 59 | ``` 60 | 61 | Import viewer component 62 | 63 | ```javascript 64 | import { useRef } from 'react' 65 | import { 66 | EpubViewer, 67 | ReactEpubViewer 68 | } from 'react-epub-viewer' 69 | 70 | const App = () => { 71 | const viewerRef = useRef(null); 72 | 73 | return ( 74 |
75 | 79 |
80 | ); 81 | } 82 | 83 | export default App 84 | ``` 85 | 86 | You can find other parameters in [Component Props](##Component Props). 87 | 88 | 89 | 90 |
91 | 92 | 93 | 94 | ## Component Props 95 | 96 | You can see also Types for React-Epub-Viewer [here](https://github.com/altmshfkgudtjr/react-epub-viewer/blob/main/src/types/index.d.ts). 97 | 98 | 99 | 100 | ### EpubViewer Props 101 | 102 | - `ref` - [RefObject] Viewer Ref 103 | 104 | - `url` - [string] - Epub file path 105 | - `epubFileOptions` - [[object](http://epubjs.org/documentation/0.3/#book)] Epub file option (Epub.js BookOption) 106 | - `epubOptions` - [[object](http://epubjs.org/documentation/0.3/#rendition)] Epub viewer option (Epub.js RenditionOption) 107 | - `style` - [object] Epub wrapper style 108 | - `location` - [string] Epub [CFI](http://idpf.org/epub/linking/cfi/epub-cfi.html) or Spine href 109 | - `bookChanged` - [function] Run when epub book changed 110 | - `renditionChanged` - [function] Run when rendition changed 111 | - `pageChanged` - [function] Run when page changed 112 | - `tocChanged` - [function] Run when toc changed 113 | - `selectionChanged` - [function] Run when selected 114 | - `loadingView` - [ReactNode] React Loading Component 115 | 116 | 117 | 118 | ### ReactEpubViewer Props 119 | 120 | - `ref` - [RefObject] Viewer Ref 121 | 122 | - `url` - [string] Epub file path 123 | - `viewerLayout` - [object] Viewer layout values (header height, footer height, etc...) 124 | - `viewerOption` - [object] Viewer option (whether is flow or is spread) 125 | - `onBookInfoChange` - [function] Run when book information changed 126 | - `onPageChange ` - [function] Run when page changed 127 | - `onTocChange ` - [function] Run when toc changed 128 | - `onSelection ` - [function] Run when selected 129 | - `loadingView` - [ReactNode] React Loading Component 130 | 131 | 132 | 133 |
134 | 135 | 136 | 137 | --- 138 | 139 | 140 | 141 | # Contribuing 142 | 143 | If you would like to contribute, please follow the [guideline](https://github.com/altmshfkgudtjr/react-epub-viewer/blob/main/CONTRIBUTING.md)! Thank you! 😀 144 | 145 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | altmgudtjr@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /src/lib/utils/commonUtil.ts: -------------------------------------------------------------------------------- 1 | import { EpubCFI } from 'epubjs'; 2 | // types 3 | import { BookStyle, BookFlow } from 'types/book'; 4 | 5 | /** 6 | * DateTime to `yyyy-mm-dd` 7 | * @param {Date} time 8 | */ 9 | export const timeFormatter = (time: Date): string => { 10 | const yyyy = time.getFullYear(); 11 | const mm = time.getMonth() + 1; 12 | const dd = time.getDate(); 13 | const msg = `${yyyy}-${mm}-${dd}`; 14 | 15 | return msg; 16 | }; 17 | 18 | /** 19 | * Comparison of two CFI sizes 20 | * - -1 : CFI 1 < CFI 2 21 | * - 0 : CFI 1 == CFI 2 22 | * - 1 : CFI 1 > CFI 2 23 | * @param cfi_1 CFI 1 24 | * @param cfi_2 CFI 2 25 | */ 26 | export const compareCfi = (cfi_1: string, cfi_2: string): number => { 27 | const epubcfi = new EpubCFI(); 28 | return epubcfi.compare(cfi_1, cfi_2); 29 | }; 30 | 31 | /** 32 | * Split CFI range into two starting CFI and ending CFI 33 | * - null : Invalid CFIRange 34 | * @param cfiRange CFIRange 35 | */ 36 | export const cfiRangeSpliter = (cfiRange: string) => { 37 | const content = cfiRange.slice(8, -1); 38 | const [origin, start, end] = content.split(','); 39 | 40 | if (!origin || !start || !end) return null; 41 | 42 | const startCfi = `epubcfi(${origin + start})`; 43 | const endCfi = `epubcfi(${origin + end})`; 44 | return { startCfi, endCfi }; 45 | }; 46 | 47 | /** 48 | * Whether the two CFI ranges nested 49 | * - true : Nested 50 | * - false : Not nested 51 | * - null : Invalid CFIRange 52 | * @param cfiRange1 First CFIRange 53 | * @param cfiRange2 Second CFIRange 54 | */ 55 | export const clashCfiRange = (baseCfiRange: string, targetCfiRange: string) => { 56 | const splitCfi1 = cfiRangeSpliter(baseCfiRange); 57 | const splitCfi2 = cfiRangeSpliter(targetCfiRange); 58 | 59 | if (!splitCfi1 || !splitCfi2) return null; 60 | 61 | const { startCfi: s1, endCfi: e1 } = splitCfi1; 62 | const { startCfi: s2, endCfi: e2 } = splitCfi2; 63 | 64 | if ( 65 | (compareCfi(s2, s1) <= 0 && compareCfi(s1, e2) <= 0) || 66 | (compareCfi(s2, e1) <= 0 && compareCfi(e1, e2) <= 0) || 67 | (compareCfi(s1, s2) <= 0 && compareCfi(e2, e1) <= 0) 68 | ) { 69 | return true; 70 | } 71 | return false; 72 | }; 73 | 74 | /** 75 | * Extract paragraph CFI from CFIRange 76 | * - null : Invalid CFIRange 77 | * @param cfiRange CFIRange 78 | */ 79 | export const getParagraphCfi = (cfiRange: string) => { 80 | if (!cfiRange) return; 81 | 82 | const content = cfiRange.slice(8, -1); 83 | const [origin, start, end] = content.split(','); 84 | 85 | if (!origin || !start || !end) return null; 86 | 87 | const cfi = `epubcfi(${origin})`; 88 | return cfi; 89 | }; 90 | 91 | /** 92 | * Get specific DOM Element from CFI 93 | * - **※ Warning**: Other Iframe must not exist in the Reader page! 94 | * @param cfi CFI 95 | * @returns HTML Element or Null 96 | */ 97 | export const getNodefromCfi = (cfi: string): HTMLElement | null => { 98 | const epubcfi = cfi.slice(8, -1); 99 | 100 | /* Remove Id */ 101 | const pureCfi = epubcfi.replace(/\[.*?\]/gi, ''); 102 | 103 | /* Only CFI Base */ 104 | const splitCfi = pureCfi.split('!'); 105 | if (splitCfi.length < 1 || splitCfi[1] === '') { 106 | return null; 107 | } 108 | 109 | /* Remove Body tag CFI */ 110 | const cfiPath: number[] = splitCfi[1] 111 | .split('/') 112 | .slice(2) 113 | .map(x => Number(x)); 114 | 115 | const iframe = document.querySelector('iframe'); 116 | if (!iframe) return null; 117 | 118 | const iframeBody = iframe.contentWindow && iframe.contentWindow.document.body; 119 | if (!iframeBody) return null; 120 | 121 | let component: HTMLElement | null = iframeBody; 122 | 123 | /* Find Node based on CFI */ 124 | for (let idx of cfiPath) { 125 | const childNodes: any = component && component.childNodes; 126 | 127 | /* Bookmark / Highlight filtering.. */ 128 | const filtered = [...childNodes].filter( 129 | n => !n.dataset || !n.dataset.bookmark || !n.dataset.highlight, 130 | ); 131 | component = filtered[idx - 1]; 132 | 133 | if (!component) { 134 | component = null; 135 | break; 136 | } 137 | } 138 | 139 | return component; 140 | }; 141 | 142 | /** 143 | * Selection absolute location 144 | * @param viewer viewerRef.current 145 | * @param bookStyle bookStyle 146 | * @param bookFlow book-flow 147 | * @param MIN_VIEWER_WIDTH min viewer width 148 | * @param MIN_VIEWER_HEIGHT min viewer height 149 | * @param VIEWER_HEADER_HEIGHT viewer header height 150 | * @param CONTEXTMENU_WIDTH contextmenu width 151 | * @returns Contextmenu location 152 | */ 153 | export const getSelectionPosition = ( 154 | viewer: any, 155 | bookStyle: BookStyle, 156 | bookFlow: BookFlow, 157 | MIN_VIEWER_WIDTH: number, 158 | MIN_VIEWER_HEIGHT: number, 159 | VIEWER_HEADER_HEIGHT: number, 160 | CONTEXTMENU_WIDTH: number, 161 | ): { x: number; y: number; height: number; width: number } | null => { 162 | const { innerWidth: windowWidth, innerHeight: windowHeight } = window; 163 | 164 | const iframeWidth = viewer.offsetWidth; 165 | 166 | const scrollTop = viewer.querySelector('div').scrollTop; 167 | 168 | const iframe = viewer.querySelector('iframe'); 169 | const selection_ = 170 | iframe && iframe.contentWindow && iframe.contentWindow.getSelection(); 171 | if (!selection_ || selection_.rangeCount === 0) return null; 172 | 173 | const range = selection_.getRangeAt(0); 174 | const { 175 | x: selectionX, 176 | y: selectionY, 177 | height: selectionHeight, 178 | width: selectionWidth, 179 | } = range.getBoundingClientRect(); 180 | 181 | const marginLeft = ~~( 182 | (((windowWidth - MIN_VIEWER_WIDTH) / 100) * bookStyle.marginHorizontal) / 183 | 2 184 | ); 185 | const marginTop = 186 | bookFlow === 'scrolled-doc' 187 | ? 0 188 | : ~~( 189 | (((windowHeight - VIEWER_HEADER_HEIGHT - MIN_VIEWER_HEIGHT) / 100) * 190 | bookStyle.marginVertical) / 191 | 2 192 | ); 193 | 194 | const x = ~~( 195 | (selectionX % iframeWidth) + 196 | marginLeft + 197 | (selectionWidth / 2 - CONTEXTMENU_WIDTH / 2) 198 | ); 199 | const y = ~~( 200 | selectionY + 201 | selectionHeight + 202 | VIEWER_HEADER_HEIGHT + 203 | marginTop - 204 | scrollTop 205 | ); 206 | 207 | return { 208 | x, 209 | y, 210 | height: selectionHeight, 211 | width: selectionWidth, 212 | }; 213 | }; 214 | 215 | /** 216 | * Debounce 217 | * @param func Target function 218 | * @param timeout delay 219 | */ 220 | export function debounce( 221 | timeout: number, 222 | func: (...args: Params) => any, 223 | ): (...args: Params) => void { 224 | let timer: NodeJS.Timeout; 225 | return (...args: Params) => { 226 | clearTimeout(timer); 227 | timer = setTimeout(() => { 228 | func(...args); 229 | }, timeout); 230 | }; 231 | } 232 | -------------------------------------------------------------------------------- /src/modules/epubViewer/EpubViewer.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | useState, 3 | useEffect, 4 | useCallback, 5 | useRef, 6 | CSSProperties, 7 | } from 'react'; 8 | import { Book, Rendition } from 'epubjs'; 9 | // style 10 | import styles from 'modules/epubViewer/styles'; 11 | // types 12 | import { EpubViewerProps, ViewerRef } from 'types'; 13 | import Toc from 'types/toc'; 14 | import Loc from 'types/loc'; 15 | 16 | /** 17 | * EpubViewer Module 18 | * @class 19 | * @param props 20 | * @param props.url Epub path 21 | * @param props.epubFileOptions Epub file option 22 | * @param props.epubOptions Epub viewer option 23 | * @param props.style Epub Wrapper style 24 | * @param props.location Epub CFI or href 25 | * @param props.bookChanged Run when book changed 26 | * @param props.rendtionChanged Run when rendition changed 27 | * @param props.pageChanged Run when page changed 28 | * @param props.tocChanged Run when toc changed 29 | * @param props.selectionChanged Run when selected 30 | * @param props.loadingView Loading Component 31 | * @param ref Viewer ref 32 | */ 33 | const EpubViewer = ( 34 | { 35 | url, 36 | epubFileOptions, 37 | epubOptions, 38 | style, 39 | location, 40 | bookChanged, 41 | rendtionChanged, 42 | pageChanged, 43 | tocChanged, 44 | selectionChanged, 45 | loadingView, 46 | }: EpubViewerProps, 47 | ref: React.RefObject | any, 48 | ) => { 49 | // TODO Fix the ref type correctly instead 'any' type. 50 | const viewerStyle: CSSProperties = style ? { ...styles, ...style } : styles; 51 | 52 | const [isLoaded, setIsLoaded] = useState(false); 53 | 54 | const [book, setBook] = useState(null); 55 | 56 | const [rendition, setRendition] = useState(null); 57 | 58 | const currentCfi = useRef(''); 59 | 60 | /** 61 | * Move page 62 | * @method 63 | * @param type direction 64 | */ 65 | const movePage = useCallback( 66 | (type: 'PREV' | 'NEXT') => { 67 | if (!rendition) return; 68 | if (type === 'PREV') rendition.prev(); 69 | else rendition.next(); 70 | }, 71 | [rendition], 72 | ); 73 | 74 | /** 75 | * Move page by arrow key 76 | * @method 77 | * @param props Keyboard Event 78 | * @param props.key 79 | */ 80 | const handleKeyPress = useCallback( 81 | ({ key }: any) => { 82 | key && key === 'ArrowLeft' && movePage('PREV'); 83 | key && key === 'ArrowRight' && movePage('NEXT'); 84 | }, 85 | [movePage], 86 | ); 87 | 88 | /** 89 | * Run when location changed 90 | * @method 91 | * @param loc 92 | * - Set location state 93 | * - Run 'locationChanged()' through startCFI 94 | */ 95 | const onLocationChange = useCallback( 96 | (loc: Loc) => { 97 | const startCfi = loc && loc.start; 98 | const endCfi = loc && loc.end; 99 | const base = loc && loc.start.slice(8).split('!')[0]; 100 | 101 | if (!book) return; 102 | 103 | const spineItem = book.spine.get(startCfi); 104 | const navItem = book.navigation.get(spineItem.href); 105 | const chapterName = navItem && navItem.label.trim(); 106 | 107 | const locations: any = book.locations; 108 | const currentPage = locations.locationFromCfi(startCfi); 109 | const totalPage = locations.total; 110 | 111 | pageChanged && 112 | pageChanged({ 113 | chapterName, 114 | currentPage, 115 | totalPage, 116 | startCfi, 117 | endCfi, 118 | base, 119 | }); 120 | 121 | currentCfi.current = startCfi; 122 | }, 123 | [book, pageChanged], 124 | ); 125 | 126 | /** 127 | * Highlight function 128 | * @param cfiRange Selected CFIRange 129 | * @param callback Highlight callback function when click it 130 | * @param color Highlight color 131 | */ 132 | const onHighlight = useCallback( 133 | (cfiRange: string, callback?: (e: any) => void, color?: string) => { 134 | if (!rendition) return; 135 | 136 | rendition.annotations.remove(cfiRange, 'highlight'); 137 | rendition.annotations.highlight( 138 | cfiRange, 139 | {}, 140 | callback, 141 | 'epub-highlight', 142 | { 143 | fill: color || '#fdf183', 144 | }, 145 | ); 146 | }, 147 | [rendition], 148 | ); 149 | 150 | /** 151 | * Highlight remove function 152 | * @param cfiRange Selected CFIRange 153 | */ 154 | const onRemoveHighlight = useCallback( 155 | (cfiRange: string) => { 156 | if (!rendition) return; 157 | 158 | rendition.annotations.remove(cfiRange, 'highlight'); 159 | }, 160 | [rendition], 161 | ); 162 | 163 | /** 164 | * Register viewer control function 165 | * @method 166 | * - REF.CURRENT.prevPage() : Move prev page 167 | * - REF.CURRENT.nextPage() : Move next page 168 | * - REF.CURRENT.getCurrentCfi() : Get current CFI 169 | * - REF.CURRENT.onHighlight(): Set highlight 170 | * - REF.CURRENT.offHighlight(): Remove specific highliht 171 | * - REF.CURRENT.seLocation(): Move to specific cfi or href 172 | */ 173 | const registerGlobalFunc = useCallback(() => { 174 | if (!ref.current) return; 175 | if (movePage) { 176 | ref.current.prevPage = () => movePage('PREV'); 177 | ref.current.nextPage = () => movePage('NEXT'); 178 | } 179 | ref.current.getCurrentCfi = () => currentCfi.current; 180 | if (onHighlight) { 181 | ref.current.onHighlight = onHighlight; 182 | } 183 | if (onRemoveHighlight) { 184 | ref.current.offHighlight = onRemoveHighlight; 185 | } 186 | if (rendition) { 187 | ref.current.setLocation = (location: string) => 188 | rendition.display(location); 189 | } 190 | }, [ref, rendition, movePage, onHighlight, onRemoveHighlight]); 191 | 192 | /** Ref Checker */ 193 | useEffect(() => { 194 | if (!ref) { 195 | throw new Error( 196 | '[React-Epub-Viewer] Put a ref argument that has a ViewerRef type.', 197 | ); 198 | } 199 | }, [ref]); 200 | 201 | /** Epub init options Changed */ 202 | useEffect(() => { 203 | if (!url) return; 204 | 205 | let mounted: boolean = true; 206 | let book_: Book | any = null; 207 | 208 | if (!mounted) return; 209 | 210 | if (book_) { 211 | book_.destroy(); 212 | } 213 | 214 | book_ = new Book(url, epubFileOptions); 215 | setBook(book_); 216 | 217 | return () => { 218 | mounted = false; 219 | }; 220 | }, [url, epubFileOptions, setBook, setIsLoaded]); 221 | 222 | /** Book Changed */ 223 | useEffect(() => { 224 | if (!book) return; 225 | 226 | if (bookChanged) bookChanged(book); 227 | 228 | book.loaded.navigation.then(({ toc }) => { 229 | const toc_: Toc[] = toc.map(t => ({ 230 | label: t.label, 231 | href: t.href, 232 | })); 233 | 234 | setIsLoaded(true); 235 | if (tocChanged) tocChanged(toc_); 236 | }); 237 | 238 | book.ready 239 | .then(function () { 240 | if (!book) return; 241 | 242 | const stored = localStorage.getItem(book.key() + '-locations'); 243 | if (stored) { 244 | return book.locations.load(stored); 245 | } else { 246 | return book.locations.generate(1024); 247 | } 248 | }) 249 | .then(() => { 250 | if (!book) return; 251 | localStorage.setItem(book.key() + '-locations', book.locations.save()); 252 | }); 253 | }, [book, bookChanged, tocChanged]); 254 | 255 | /** Rendition Changed */ 256 | useEffect(() => { 257 | if (!rendition) return; 258 | 259 | if (rendtionChanged) rendtionChanged(rendition); 260 | }, [rendition, rendtionChanged]); 261 | 262 | /** Viewer Option Changed */ 263 | useEffect(() => { 264 | let mounted = true; 265 | if (!book) return; 266 | 267 | const node = ref.current; 268 | if (!node) return; 269 | node.innerHTML = ''; 270 | 271 | book.ready.then(function () { 272 | if (!mounted) return; 273 | 274 | if (book.spine) { 275 | const loc = book.rendition?.location?.start?.cfi; 276 | 277 | // if (book.rendition) book.rendition.destroy(); 278 | 279 | const rendition_ = book.renderTo(node, { 280 | width: '100%', 281 | height: '100%', 282 | ...epubOptions, 283 | }); 284 | setRendition(rendition_); 285 | 286 | if (loc) { 287 | rendition_.display(loc); 288 | } else { 289 | rendition_.display(); 290 | } 291 | } 292 | }); 293 | 294 | return () => { 295 | mounted = false; 296 | }; 297 | }, [ref, book, epubOptions, style, setRendition]); 298 | 299 | /** Location Changed */ 300 | useEffect(() => { 301 | if (!ref.current || !location) return; 302 | if (ref.current.setLocation) ref.current.setLocation(location); 303 | }, [ref, location]); 304 | 305 | /** 306 | * Emit Viewer Event 307 | * - Register move event 308 | * - Register location changed event 309 | * - Register selection event 310 | */ 311 | /* eslint-disable */ 312 | useEffect(() => { 313 | if (!rendition) return; 314 | 315 | // Emit global control function 316 | registerGlobalFunc(); 317 | 318 | document.addEventListener('keyup', handleKeyPress, false); 319 | rendition.on('keyup', handleKeyPress); 320 | rendition.on('locationChanged', onLocationChange); 321 | selectionChanged && rendition.on('selected', selectionChanged); 322 | 323 | return () => { 324 | document.removeEventListener('keyup', handleKeyPress, false); 325 | rendition.off('keyup', handleKeyPress); 326 | rendition.off('locationChanged', onLocationChange); 327 | selectionChanged && rendition.off('selected', selectionChanged); 328 | }; 329 | }, [rendition, registerGlobalFunc, handleKeyPress]); 330 | /* eslint-enable */ 331 | 332 | return ( 333 | <> 334 | {!isLoaded && loadingView} 335 |
336 | 337 | ); 338 | }; 339 | 340 | export default React.forwardRef(EpubViewer); 341 | -------------------------------------------------------------------------------- /src/modules/reactViewer/ReactViewer.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | useState, 3 | useEffect, 4 | useRef, 5 | useCallback, 6 | useMemo, 7 | } from 'react'; 8 | import { Book, Rendition, Contents } from 'epubjs'; 9 | // modules 10 | import EpubViewer from 'modules/epubViewer/EpubViewer'; 11 | // components 12 | import LoadingView from 'LoadingView'; 13 | // utils 14 | import { debounce, timeFormatter } from 'lib/utils/commonUtil'; 15 | // styles 16 | import viewerDefaultStyles from 'modules/reactViewer/viewerStyle'; 17 | // types 18 | import { ReactViewerProps, ViewerRef } from 'types'; 19 | import BookType, { BookStyle, BookOption } from 'types/book'; 20 | 21 | /** 22 | * Epub React Viewer Module 23 | * @class 24 | * @param props 25 | * @param props.url Epub file path 26 | * @param props.viewerLayout Viewer layout 27 | * @param props.viewerStyle Viewer style 28 | * @param props.viewerStyleURL Viwer style - CSS URL 29 | * @param props.viewerOption Viewer option 30 | * @param props.onBookInfoChange Run when book information changed 31 | * @param props.onPageChange Run when page changed 32 | * @param props.onTocChange Run when toc changed 33 | * @param props.onSelection Run when selected 34 | * @param props.loadingView Loading component 35 | * @param ref Viewer ref 36 | */ 37 | const ReactViewer = ( 38 | { 39 | url, 40 | viewerLayout, 41 | viewerStyle, 42 | viewerStyleURL, 43 | viewerOption, 44 | onBookInfoChange, 45 | onPageChange, 46 | onTocChange, 47 | onSelection, 48 | loadingView, 49 | }: ReactViewerProps, 50 | ref: React.RefObject | any, 51 | ) => { 52 | // TODO Fix the ref type correctly instead 'any' type. 53 | const [book, setBook] = useState(null); 54 | 55 | const [rendition, setRendition] = useState(null); 56 | 57 | const [layoutStyle, setLayoutStyle] = useState<{ [key: string]: any }>({}); 58 | 59 | const [bookStyle, setBookStyle] = useState({ 60 | fontFamily: 'Origin', 61 | fontSize: 16, 62 | lineHeight: 1.4, 63 | marginHorizontal: 0, 64 | marginVertical: 0, 65 | }); 66 | 67 | const [bookOption, setBookOption] = useState({ 68 | flow: 'paginated', 69 | resizeOnOrientationChange: true, 70 | spread: 'auto', 71 | }); 72 | 73 | const currentSelection = useRef<{ 74 | cfiRange: string; 75 | contents: Contents | null; 76 | }>({ 77 | cfiRange: '', 78 | contents: null, 79 | }); 80 | 81 | /** 82 | * Run book changed 83 | * @method 84 | * @param book Epub Book 85 | */ 86 | const bookChanged = (book: Book) => setBook(book); 87 | 88 | /** 89 | * Run rendition changed 90 | * @method 91 | * @param rendition Epub Rendition 92 | */ 93 | const rendtionChanged = (rendition: Rendition) => setRendition(rendition); 94 | 95 | /** 96 | * Run selection changed [Debounce] 97 | * @method 98 | * @param cfiRange CFIRange 99 | * @param contents Selection Epub Contents 100 | */ 101 | const selectionChanged = (cfiRange: string, contents: Contents) => { 102 | currentSelection.current = { cfiRange, contents }; 103 | }; 104 | 105 | /** 106 | * Viewer resizing function 107 | * @method 108 | */ 109 | const onResize = useMemo( 110 | () => 111 | debounce(250, () => { 112 | if (!rendition) return; 113 | 114 | const viewerLayout_ = viewerLayout || { 115 | MIN_VIEWER_WIDTH: 600, 116 | MIN_VIEWER_HEIGHT: 300, 117 | VIEWER_HEADER_HEIGHT: 0, 118 | VIEWER_FOOTER_HEIGHT: 0, 119 | VIEWER_SIDEMENU_WIDTH: 0, 120 | }; 121 | 122 | const { innerWidth: win_w, innerHeight: win_h } = window; 123 | const componentHeight = 124 | viewerLayout_.VIEWER_HEADER_HEIGHT + 125 | viewerLayout_.VIEWER_FOOTER_HEIGHT; 126 | const w = 127 | win_w - 128 | ~~( 129 | ((win_w - viewerLayout_.MIN_VIEWER_WIDTH) / 100) * 130 | bookStyle.marginHorizontal 131 | ); 132 | const h = 133 | bookOption.flow === 'scrolled-doc' 134 | ? win_h - componentHeight 135 | : win_h - 136 | componentHeight - 137 | ~~( 138 | ((win_h - componentHeight - viewerLayout_.MIN_VIEWER_HEIGHT) / 139 | 100) * 140 | bookStyle.marginVertical 141 | ); 142 | const marginVertical = 143 | bookOption.flow === 'scrolled-doc' 144 | ? '' 145 | : `${ 146 | ~~( 147 | ((win_h - componentHeight - viewerLayout_.MIN_VIEWER_HEIGHT) / 148 | 100) * 149 | bookStyle.marginVertical 150 | ) / 2 151 | }px`; 152 | 153 | setLayoutStyle(layout => { 154 | if ( 155 | layout.width !== w || 156 | layout.height !== h || 157 | layout.marginTop !== marginVertical 158 | ) { 159 | return { 160 | ...layout, 161 | width: w, 162 | height: h, 163 | marginTop: marginVertical, 164 | marginBottom: marginVertical, 165 | }; 166 | } 167 | return layout; 168 | }); 169 | 170 | try { 171 | rendition.resize(w, h); 172 | } catch {} 173 | }), 174 | [ 175 | rendition, 176 | viewerLayout, 177 | bookStyle.marginHorizontal, 178 | bookStyle.marginVertical, 179 | bookOption.flow, 180 | ], 181 | ); 182 | 183 | /** 184 | * Selection Event, run when run mouseup event 185 | * @method
186 | * - Fire after the Epubjs selected event. [about 300ms] 187 | */ 188 | const onSelected = useCallback(async () => { 189 | if (!ref.current) return; 190 | 191 | const iframe = ref.current.querySelector('iframe'); 192 | if (!iframe) return; 193 | 194 | const iframeWin = iframe.contentWindow; 195 | if (!iframeWin) return; 196 | 197 | const selection_ = iframeWin.getSelection(); 198 | if (!selection_) return; 199 | 200 | const selectionText = selection_.toString(); 201 | if (selectionText === '') return; 202 | 203 | const cfiRange: string = await new Promise(resolve => 204 | setTimeout(() => resolve(currentSelection.current.cfiRange), 350), 205 | ); 206 | if (!cfiRange) return; 207 | 208 | const contents = currentSelection.current.contents; 209 | if (!contents) return; 210 | 211 | onSelection && onSelection(cfiRange, contents); 212 | }, [ref, onSelection]); 213 | 214 | /** Ref checker */ 215 | useEffect(() => { 216 | if (!ref) { 217 | throw new Error( 218 | '[React-Epub-Viewer] Put a ref argument that has a ViewerRef type.', 219 | ); 220 | } 221 | }, [ref]); 222 | 223 | /** Epub parsing */ 224 | // TODO Fix the infinite re-rendering issue, when inlcude `onBookInfoChange` to dependencies array. 225 | /* eslint-disable */ 226 | useEffect(() => { 227 | if (!book) return; 228 | 229 | Promise.all([book.loaded.metadata, book.opened]) 230 | .then(([metaData, bookData]: any[]) => { 231 | const newBookData: BookType = { 232 | coverURL: bookData.archive.urlCache[bookData.cover], 233 | title: metaData.title, 234 | description: metaData.description, 235 | published_date: timeFormatter(new Date(metaData.pubdate)), 236 | modified_date: timeFormatter(new Date(metaData.modified_date)), 237 | author: metaData.creator, 238 | publisher: metaData.publisher, 239 | language: metaData.language, 240 | }; 241 | 242 | onBookInfoChange && onBookInfoChange(newBookData); 243 | }) 244 | .catch(error => { 245 | throw `${error.stack} \n\n Message : Epub parsing failed.`; 246 | }); 247 | }, [book]); 248 | /* eslint-enable */ 249 | 250 | /** Set viewer Styles/Options */ 251 | useEffect(() => { 252 | viewerStyle && setBookStyle(v => ({ ...v, ...viewerStyle })); 253 | viewerOption && setBookOption(v => ({ ...v, ...viewerOption })); 254 | }, [viewerStyle, viewerOption]); 255 | 256 | /** Apply viewer Styles/Options */ 257 | useEffect(() => { 258 | if (!rendition) return; 259 | 260 | onResize(); 261 | 262 | const newStyle = { 263 | ...viewerDefaultStyles, 264 | body: { 265 | 'padding-top': '0px !important', 266 | 'padding-bottom': '0px !important', 267 | 'font-size': `${bookStyle.fontSize}px !important`, 268 | }, 269 | p: { 270 | 'font-size': `${bookStyle.fontSize}px !important`, 271 | 'line-height': `${bookStyle.lineHeight} !important`, 272 | }, 273 | }; 274 | 275 | rendition.flow(bookOption.flow); 276 | rendition.spread(bookOption.spread); 277 | 278 | if (bookStyle.fontFamily !== 'Origin') { 279 | Object.assign(newStyle.body, { 280 | 'font-family': `${bookStyle.fontFamily} !important`, 281 | }); 282 | } 283 | 284 | if (bookOption.flow === 'scrolled-doc') { 285 | // Scroll type 286 | Object.assign(newStyle.body, { 287 | margin: 'auto !important', 288 | }); 289 | } else if (bookOption.spread === 'auto') { 290 | // View 2 pages 291 | Object.assign(newStyle.body, {}); 292 | } else { 293 | // View 1 page 294 | Object.assign(newStyle.body, {}); 295 | } 296 | 297 | if (!!viewerStyleURL) { 298 | rendition.themes.registerUrl('main', viewerStyleURL); 299 | } 300 | 301 | rendition.themes.register('default', newStyle); 302 | 303 | rendition.themes.select('main'); 304 | }, [ 305 | rendition, 306 | bookStyle.fontFamily, 307 | bookStyle.fontSize, 308 | bookStyle.lineHeight, 309 | viewerStyleURL, 310 | bookOption, 311 | onResize, 312 | ]); 313 | 314 | /** Emit screen resizing event */ 315 | useEffect(() => { 316 | window.addEventListener('resize', onResize); 317 | return () => window.removeEventListener('resize', onResize); 318 | }, [onResize]); 319 | 320 | /** Emit selection event */ 321 | useEffect(() => { 322 | if (!rendition) return; 323 | rendition.on('mouseup', onSelected); 324 | return () => rendition.off('mouseup', onSelected); 325 | }, [rendition, onSelected]); 326 | 327 | return ( 328 | <> 329 | } 338 | ref={ref} 339 | /> 340 | 341 | ); 342 | }; 343 | 344 | export default React.forwardRef(ReactViewer); 345 | --------------------------------------------------------------------------------