├── .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 |
2 |
3 |
4 |
5 |
6 |
7 | 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 |
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 |

4 |
5 |
6 |
7 |
8 |
9 | # React-Epub-Viewer
10 |
11 | [](https://www.npmjs.com/package/react-epub-viewer) [](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 |
--------------------------------------------------------------------------------