├── .eslintignore ├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── codacy-analysis.yaml │ ├── codeql-analysis.yaml │ └── continuous-integration.yaml ├── .gitignore ├── .husky ├── .gitignore └── pre-commit ├── .npmignore ├── .prettierignore ├── .prettierrc.js ├── .stylelintrc.js ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── babel.config.js ├── dist └── pdf-viewer-reactjs.js ├── example ├── .env ├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ └── manifest.json ├── src │ ├── App.css │ ├── App.js │ ├── Navigation.js │ ├── Sources.js │ ├── __tests__ │ │ ├── App.spec.js │ │ └── __snapshots__ │ │ │ └── App.spec.js.snap │ ├── index.css │ ├── index.js │ └── registerServiceWorker.js ├── test │ ├── pageobjects │ │ └── app.page.js │ └── specs │ │ └── app.e2e.js ├── wdio.conf.js └── wdio.conf.local.js ├── jest.config.js ├── package-lock.json ├── package.json ├── rollup.config.js ├── setupTests.js └── src ├── components ├── Alert.js ├── Loader.js ├── NavigationBar.js ├── RenderPdf.js ├── __tests__ │ ├── Alert.spec.js │ ├── Loader.spec.js │ ├── NavigationBar.spec.js │ └── __snapshots__ │ │ ├── Loader.spec.js.snap │ │ └── NavigationBar.spec.js.snap └── navigationComponents │ ├── NextPageButton.js │ ├── PageIndicator.js │ ├── PreviousPageButton.js │ ├── ResetRotation.js │ ├── ResetZoom.js │ ├── RotateLeft.js │ ├── RotateRight.js │ ├── ZoomIn.js │ ├── ZoomOut.js │ └── __tests__ │ ├── NextPageButton.spec.js │ ├── PageIndicator.spec.js │ ├── PreviousPageButton.spec.js │ ├── ResetRotation.spec.js │ ├── ResetZoom.spec.js │ ├── RotateLeft.spec.js │ ├── RotateRight.spec.js │ ├── ZoomIn.spec.js │ └── ZoomOut.spec.js └── index.js /.eslintignore: -------------------------------------------------------------------------------- 1 | .history 2 | node_modules 3 | example/node_modules 4 | example/src/Sources.js 5 | dist/pdf-viewer-reactjs.js -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es6: true, 5 | node: true, 6 | jest: true, 7 | mocha: true, 8 | }, 9 | extends: [ 10 | 'eslint:recommended', 11 | 'plugin:react/recommended', 12 | 'plugin:prettier/recommended', 13 | 'plugin:jest/recommended', 14 | 'plugin:wdio/recommended', 15 | ], 16 | globals: { 17 | Atomics: 'readonly', 18 | SharedArrayBuffer: 'readonly', 19 | }, 20 | parserOptions: { 21 | ecmaFeatures: { 22 | jsx: true, 23 | }, 24 | ecmaVersion: 2018, 25 | sourceType: 'module', 26 | }, 27 | plugins: ['jest', 'wdio'], 28 | rules: { 29 | 'arrow-parens': [2, 'always'], 30 | 'jest/no-disabled-tests': 'warn', 31 | 'jest/no-focused-tests': 'error', 32 | 'jest/no-identical-title': 'error', 33 | 'jest/prefer-to-have-length': 'warn', 34 | 'jest/valid-expect': 'error', 35 | }, 36 | settings: { 37 | react: { 38 | version: '>=16.8.6', 39 | }, 40 | }, 41 | } 42 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to fix a bug in this project 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: ansu5555 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **How 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 | Please add screenshots to help explain your problem. 25 | 26 | **PDF File** 27 | Please attach the pdf file. 28 | 29 | **Details (please complete the following information):** 30 | - Device: [e.g. Desktop, iPhone6, Pixel2] 31 | - OS: [e.g. Windows 10, Ubuntu 18.04, iOS8.1, Android Pie] 32 | - Browser [e.g. stock browser, safari, chrome] 33 | - Version [e.g. 22] 34 | 35 | **Additional context** 36 | Add any other context about the problem here. 37 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[FEATURE]" 5 | labels: enhancement 6 | assignees: ansu5555 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 | -------------------------------------------------------------------------------- /.github/workflows/codacy-analysis.yaml: -------------------------------------------------------------------------------- 1 | name: Codacy Analysis 2 | 3 | on: ['push'] 4 | 5 | jobs: 6 | analyze: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - name: Checkout repository 11 | uses: actions/checkout@v2 12 | 13 | - name: Run Codacy Analysis CLI 14 | uses: codacy/codacy-analysis-cli-action@master 15 | with: 16 | tool: clang-tidy 17 | project-token: ${{ secrets.CODACY_PROJECT_TOKEN }} 18 | upload: true 19 | max-allowed-issues: 2147483647 20 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yaml: -------------------------------------------------------------------------------- 1 | name: CodeQL Analysis 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | schedule: 9 | - cron: '15 00 * * 0' 10 | 11 | jobs: 12 | analyze: 13 | runs-on: ubuntu-latest 14 | 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | language: ['javascript'] 19 | 20 | steps: 21 | - name: Checkout repository 22 | uses: actions/checkout@v2 23 | 24 | - name: Initialize CodeQL 25 | uses: github/codeql-action/init@v1 26 | with: 27 | languages: ${{ matrix.language }} 28 | 29 | - name: Autobuild 30 | uses: github/codeql-action/autobuild@v1 31 | 32 | - name: Perform CodeQL Analysis 33 | uses: github/codeql-action/analyze@v1 34 | -------------------------------------------------------------------------------- /.github/workflows/continuous-integration.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | env: 9 | BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} 10 | BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }} 11 | CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }} 12 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 13 | 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v2 17 | 18 | - name: Install NodeJS 19 | uses: actions/setup-node@v2 20 | with: 21 | node-version: '14' 22 | 23 | - name: Update npm 24 | run: npm install -g npm@7.6.2 25 | 26 | - name: Install npm packages for PDF-viewer-reactjs Package 27 | run: npm install 28 | 29 | - name: Build PDF-viewer-reactjs Package 30 | run: npm run build 31 | 32 | - name: Upload coverage to Codacy 33 | run: bash <(curl -Ls https://coverage.codacy.com/get.sh) report 34 | 35 | - name: Upload coverage to Codecov 36 | run: bash <(curl -s https://codecov.io/bash) 37 | 38 | - name: Install npm packages for Example App 39 | run: npm install --prefix example/ 40 | 41 | - name: Run tests for Example App 42 | run: npm test --prefix example/ 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | # Ignore MacOS config file 40 | .DS_Store 41 | 42 | # local history 43 | .history -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm run unittest --prefix example/ && npm run build && git add dist 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .github 2 | .history 3 | .husky 4 | .vscode 5 | coverage 6 | example 7 | src 8 | .eslintignore 9 | .eslintrc.js 10 | .gitignore 11 | .prettierignore 12 | .prettierrc.js 13 | .stylelintrc.js 14 | babel.config.js 15 | jest.config.js 16 | rollup.config.js 17 | setupTests.js -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .history 2 | node_modules 3 | example/node_modules 4 | example/src/Sources.js 5 | dist/pdf-viewer-reactjs.js -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | trailingComma: 'es5', 3 | tabWidth: 2, 4 | semi: false, 5 | singleQuote: true, 6 | jsxBracketSameLine: true, 7 | jsxSingleQuote: true, 8 | } 9 | -------------------------------------------------------------------------------- /.stylelintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['stylelint-config-recommended'], 3 | rules: { 4 | indentation: 2, 5 | 'max-empty-lines': 2, 6 | 'no-empty-first-line': true, 7 | }, 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 2, 3 | "editor.defaultFormatter": "esbenp.prettier-vscode", 4 | "prettier.configPath": ".prettierrc.js" 5 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 MGrin 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pdf-viewer-reactjs 2 | 3 | ![GitHub Repo stars](https://img.shields.io/github/stars/ansu5555/pdf-viewer-reactjs?style=social) 4 | 5 | ![npm](https://img.shields.io/npm/v/pdf-viewer-reactjs) ![npm](https://img.shields.io/npm/dw/pdf-viewer-reactjs) ![npm bundle size](https://img.shields.io/bundlephobia/minzip/pdf-viewer-reactjs) ![NPM](https://img.shields.io/npm/l/pdf-viewer-reactjs) 6 | 7 | [![Codacy Badge](https://app.codacy.com/project/badge/Grade/171e095d2873453b8f2f8cea3b5f7be9)](https://www.codacy.com/gh/ansu5555/pdf-viewer-reactjs/dashboard?utm_source=github.com&utm_medium=referral&utm_content=ansu5555/pdf-viewer-reactjs&utm_campaign=Badge_Grade) [![Total alerts](https://img.shields.io/lgtm/alerts/g/ansu5555/pdf-viewer-reactjs.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/ansu5555/pdf-viewer-reactjs/alerts/) [![codecov](https://codecov.io/gh/ansu5555/pdf-viewer-reactjs/branch/master/graph/badge.svg?token=ESMHLXF9WR)](https://codecov.io/gh/ansu5555/pdf-viewer-reactjs) ![GiHub CI workflow](https://github.com/ansu5555/pdf-viewer-reactjs/actions/workflows/continuous-integration.yaml/badge.svg) 8 | 9 | Simple react PDF Viewer component with controls like 10 | 11 | - Page navigation 12 | - Zoom 13 | - Rotation 14 | 15 | Every element can be styled upon your preferences using default classes your own and also custom react element can be passed. 16 | 17 | Initially it was forked from [mgr-pdf-viewer-react](https://github.com/MGrin/mgr-pdf-viewer-react) 18 | 19 | ### Hit the :star: on GitHub if you like it 20 | 21 | ### Example: Live demo is available [here](https://ansu5555.github.io/pdf-viewer-reactjs/) 22 | 23 | # How to install 24 | 25 | ```js 26 | 27 | npm i pdf-viewer-reactjs 28 | 29 | ``` 30 | 31 | # Note: 32 | 33 | > ### Due to causing [broken CSS issue](https://github.com/ansu5555/pdf-viewer-reactjs/issues/27) **_bulma_** & **_material-design-icons_** are removed from dependencies and code as well and added as peerDependencies. 34 | > 35 | > ### Please install **_bulma_** & **_material-design-icons_** from npm by yourself 36 | > 37 | > ```js 38 | > npm i bulma material-design-icons 39 | > ``` 40 | > 41 | > ### then import them in your CSS as below 42 | > 43 | > ```css 44 | > @import url('bulma/css/bulma.css'); 45 | > @import url('material-design-icons/iconfont/material-icons.css'); 46 | > ``` 47 | > 48 | > ### Else provide custom CSS styles as per your requirement 49 | 50 | # How to use 51 | 52 | ```js 53 | import React from 'react' 54 | 55 | import PDFViewer from 'pdf-viewer-reactjs' 56 | 57 | const ExamplePDFViewer = () => { 58 | return ( 59 | 64 | ) 65 | } 66 | 67 | export default ExamplePDFViewer 68 | ``` 69 | 70 | # Documentation 71 | 72 | React component prop. types: 73 | 74 | **`document`:** 75 | 76 | - Type: 77 | 78 | ```js 79 | PropTypes.shape({ 80 | url: String, // URL to the pdf 81 | 82 | base64: String, // PDF file encoded in base64 83 | }) 84 | ``` 85 | 86 | - Required: **true** 87 | 88 | - Description: Provides a way to fetch the PDF document 89 | 90 | **`password`:** 91 | 92 | - Type: _String_ 93 | 94 | - Required: **false** 95 | 96 | - Description: For decrypting password-protected PDFs 97 | 98 | **`withCredentials`:** 99 | 100 | - Type: _Boolean_ 101 | 102 | - Required: **false** 103 | 104 | - Description: Indicates whether or not cross-site Access-Control requests should be made using credentials such as cookies or authorization headers. The default is false 105 | 106 | **`externalInput`:** 107 | 108 | - Type: _Boolean_ 109 | 110 | - Required: **false** 111 | 112 | - Description: By default page number, scale and rotation angle can be set initially using the props but cannot be changed dynamically, to make these props dynamic pass this prop (after passing this prop navbar will become hidden) 113 | 114 | **`page`:** 115 | 116 | - Type: _Number_ 117 | 118 | - Required: **false** 119 | 120 | - Description: The page that will be shown first on document load, also can be used to change the page number dynamically after passing `externalInput` prop 121 | 122 | **`scale`:** 123 | 124 | - Type: _Number_ 125 | 126 | - Required: **false** 127 | 128 | - Description: Scale factor relative to the component parent element, also can be used to change the scale dynamically after passing `externalInput` prop 129 | 130 | **`rotationAngle`:** 131 | 132 | - Type: _Number_ 133 | 134 | - Required: **false** 135 | 136 | - Description: Initial rotation of the document, values can be -90, 0 or 90, also can be used to change the rotation angle dynamically after passing `externalInput` prop 137 | 138 | **`scaleStep`:** 139 | 140 | - Type: _Number_ 141 | 142 | - Required: **false** 143 | 144 | - Description: Scale factor to be increased or decreased on Zoom-in or zoom-out 145 | 146 | **`minScale`:** 147 | 148 | - Type: _Number_ 149 | 150 | - Required: **false** 151 | 152 | - Description: Minimum scale factor for zoom-out 153 | 154 | **`maxScale`:** 155 | 156 | - Type: _Number_ 157 | 158 | - Required: **false** 159 | 160 | - Description: Maximum scale factor for zoom-in 161 | 162 | **`onDocumentClick`:** 163 | 164 | - Type: _Function_ 165 | 166 | - Required: **false** 167 | 168 | - Description: A function that will be called only on clicking the PDF page itself, NOT on the navbar 169 | 170 | **`onPrevBtnClick`:** 171 | 172 | - Type: _Function_ 173 | 174 | - Required: **false** 175 | 176 | - Description: A function that will be called on clicking on the previous page button, page number can be accessed in the function. 177 | 178 | **`onNextBtnClick`:** 179 | 180 | - Type: _Function_ 181 | 182 | - Required: **false** 183 | 184 | - Description: A function that will be called on clicking on the next page button, page number can be accessed in the function. 185 | 186 | **`onZoom`:** 187 | 188 | - Type: _Function_ 189 | 190 | - Required: **false** 191 | 192 | - Description: A function that will be called on clicking on Zoom controls, zoom scale can be accessed in the function. 193 | 194 | **`onRotation`:** 195 | 196 | - Type: _Function_ 197 | 198 | - Required: **false** 199 | 200 | - Description: A function that will be called on clicking on Rotation controls, rotation angle can be accessed in the function. 201 | 202 | **`getMaxPageCount`:** 203 | 204 | - Type: _Function_ 205 | 206 | - Required: **false** 207 | 208 | - Description: A function that will be called on document load, total page count can be accessed in the function. 209 | 210 | **`css`:** 211 | 212 | - Type: _String_ 213 | 214 | - Required: **false** 215 | 216 | - Description: CSS classes that will be setted for the component wrapper 217 | 218 | **`canvasCss`:** 219 | 220 | - Type: _String_ 221 | 222 | - Required: **false** 223 | 224 | - Description: CSS classes that will be setted for the PDF page 225 | 226 | **`navbarOnTop`:** 227 | 228 | - Type: _Boolean_ 229 | 230 | - Required: **false** 231 | 232 | - Description: By default navbar is displayed on bottom, but can be placed on top by passing this prop 233 | 234 | **`hideNavbar`:** 235 | 236 | - Type: _Boolean_ 237 | 238 | - Required: **false** 239 | 240 | - Description: By default navbar is displayed, but can be hidden by passing this prop 241 | 242 | **`hideZoom`:** 243 | 244 | - Type: _Boolean_ 245 | 246 | - Required: **false** 247 | 248 | - Description: By default zoom buttons are displayed, but can be hidden by passing this prop 249 | 250 | **`hideRotation`:** 251 | 252 | - Type: _Boolean_ 253 | 254 | - Required: **false** 255 | 256 | - Description: By default rotation buttons are displayed, but can be hidden by passing this prop 257 | 258 | **`loader`:** 259 | 260 | - Type: _Node_ 261 | 262 | - Required: **false** 263 | 264 | - Description: A custom loader element that will be shown while the PDF is loading 265 | 266 | **`alert`:** 267 | 268 | - Type: _Node_ 269 | 270 | - Required: **false** 271 | 272 | - Description: A custom alert element that will be shown on error 273 | 274 | **`showThumbnail`:** 275 | 276 | - Type: 277 | 278 | ```js 279 | PropTypes.shape({ 280 | scale: PropTypes.number, // Thumbnail scale, ranges from 1 to 5 281 | 282 | rotationAngle: PropTypes.number, // Thumbnail rotation angle, values can be -90, 0 or 90. Default is 0 283 | 284 | onTop: PropTypes.bool, // Thumbnail position, if set to true thumbnail will be placed on top 285 | 286 | backgroundColor: PropTypes.string, // Color(hex or rgb) of the thumbnail container 287 | 288 | thumbCss: PropTypes.string, // Custom css class for thumbnails 289 | 290 | selectedThumbCss: PropTypes.string, // Custom css class for selected thumbnail 291 | }) 292 | ``` 293 | 294 | - Required: **false** 295 | 296 | - Description: Details of the thumbnails, not shown if not provided 297 | 298 | **`protectContent`:** 299 | 300 | - Type: _Boolean_ 301 | 302 | - Required: **false** 303 | 304 | - Description: By default Right Click and Context Menu are enabled, but can be disabled by passing this prop 305 | 306 | **`watermark`:** 307 | 308 | - Type: 309 | 310 | ```js 311 | PropTypes.shape({ 312 | text: PropTypes.string, // Watermark text 313 | 314 | diagonal: PropTypes.bool, // Watermark placement true for Diagonal, false for Horizontal 315 | 316 | opacity: PropTypes.string, // Watermark opacity, ranges from 0 to 1 317 | 318 | font: PropTypes.string, // custom font name default is 'Comic Sans MS' 319 | 320 | size: PropTypes.string, // Font Size of Watermark 321 | 322 | color: PropTypes.string, // Color(hex or rgb) of the watermark 323 | }) 324 | ``` 325 | 326 | - Required: **false** 327 | 328 | - Description: Details of the watermark, not shown if not provided 329 | 330 | **`navigation`:** 331 | 332 | - Type: 333 | 334 | ```js 335 | 336 | PropTypes.oneOfType([ 337 | 338 | // Can be an object with css classes or react elements to be rendered 339 | 340 | PropTypes.shape({ 341 | 342 | css: PropTypes.shape({ 343 | 344 | navbarWrapper: String, // CSS Class for the Navbar Wrapper 345 | 346 | zoomOutBtn: String, // CSS Class for the ZoomOut Button 347 | 348 | resetZoomBtn: String, // CSS Class for the Reset Zoom Button 349 | 350 | zoomInBtn: String, // CSS Class for the ZoomIn Button 351 | 352 | previousPageBtn: String, // CSS Class for the Previous Page button 353 | 354 | pageIndicator: String, // CSS Class for the Page Indicator 355 | 356 | nextPageBtn: String, // CSS Class for the Next Page button 357 | 358 | rotateLeftBtn: String, // CSS Class for the RotateLeft button 359 | 360 | resetRotationBtn: String, // CSS Class for the Reset Rotation button 361 | 362 | rotateRightBtn: String // CSS Class for the RotateRight button 363 | 364 | }) 365 | 366 | // Or a full navigation component 367 | 368 | PropTypes.any // Full navigation React element 369 | 370 | ]); 371 | 372 | ``` 373 | 374 | - Required: **false** 375 | 376 | - Description: Defines the navigation bar styles and/or elements. 377 | 378 | # Author 379 | 380 | ![Image of Author](https://avatars3.githubusercontent.com/u/5373653?s=150) 381 | 382 | ### Ansuman Ghosh 383 | 384 | [ansu5555.com](https://www.ansu5555.com/) 385 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@babel/preset-env', '@babel/react'], 3 | plugins: ['@babel/transform-runtime'], 4 | } 5 | -------------------------------------------------------------------------------- /dist/pdf-viewer-reactjs.js: -------------------------------------------------------------------------------- 1 | "use strict";var e=require("@babel/runtime/helpers/typeof"),t=require("@babel/runtime/helpers/classCallCheck"),a=require("@babel/runtime/helpers/createClass"),n=require("@babel/runtime/helpers/assertThisInitialized"),l=require("@babel/runtime/helpers/inherits"),s=require("@babel/runtime/helpers/possibleConstructorReturn"),r=require("@babel/runtime/helpers/getPrototypeOf"),i=require("react"),o=require("prop-types"),u=require("@babel/runtime/helpers/asyncToGenerator"),d=require("@babel/runtime/helpers/slicedToArray"),c=require("@babel/runtime/regenerator"),f=require("pdfjs-dist"),m=require("pdfjs-dist/build/pdf.worker.entry");function h(e){return e&&"object"==typeof e&&"default"in e?e:{default:e}}var p=h(e),g=h(t),b=h(a),v=h(n),R=h(l),x=h(s),E=h(r),y=h(i),k=h(o),w=h(u),C=h(d),N=h(c),S=h(m),P=function(e){var t=e.message;return y.default.createElement("div",{className:"columns has-text-danger mt-2 is-mobile"},y.default.createElement("div",{className:"column is-4 has-text-right p-2"},y.default.createElement("span",{className:"icon"},y.default.createElement("i",{className:"material-icons"},"error_outline"))),y.default.createElement("div",{className:"column is-8 has-text-left p-2"},y.default.createElement("small",null,t)))};function q(e){var t=i.useRef();return i.useEffect((function(){t.current=e})),t.current}P.propTypes={message:k.default.string.isRequired},f.GlobalWorkerOptions.workerSrc=S.default;var Z=function(e){var t=e.document,a=e.withCredentials,n=e.password,l=e.pageNum,s=e.scale,r=e.rotation,o=e.pageCount,u=e.changePage,d=e.showThumbnail,c=e.protectContent,m=e.watermark,h=e.alert,p=e.canvasCss,g=i.useState(null),b=C.default(g,2),v=b[0],R=b[1],x=i.useState(null),E=C.default(x,2),k=E[0],S=E[1],Z=i.useState(null),A=C.default(Z,2),T=A[0],B=A[1],O=i.useState({status:!1,message:""}),I=C.default(O,2),_=I[0],z=I[1],L=i.useRef(null),j=i.useRef(null),D=i.useRef(null),F=i.useState([]),M=C.default(F,2),W=M[0],H=M[1],V=q(t),G=q(n),J=h||P,U=function(){var e=w.default(N.default.mark((function e(){var l,s,r;return N.default.wrap((function(e){for(;;)switch(e.prev=e.next){case 0:if(l=null,s=null,e.prev=2,JSON.stringify(V)===JSON.stringify(t)&&G.base64===n.url){e.next=15;break}return r={withCredentials:a,password:n},null==t.url?r.data=atob(t.base64):r.url=t.url,e.next=8,f.getDocument(r).promise;case 8:return l=e.sent,e.next=11,K(l);case 11:s=e.sent,Q(s),R(l),S(s);case 15:return e.next=17,X(l);case 17:o(l.numPages),e.next=25;break;case 20:e.prev=20,e.t0=e.catch(2),console.warn("Error while opening the document !\n",e.t0),o(-1),z({status:!0,message:"Error while opening the document !"});case 25:case"end":return e.stop()}}),e,null,[[2,20]])})));return function(){return e.apply(this,arguments)}}(),X=function(){var e=w.default(N.default.mark((function e(){var t,a,n,i,u,d,c,f,h,p,g,b,R,x,E,y,k=arguments;return N.default.wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return null==(t=k.length>0&&void 0!==k[0]?k[0]:null)&&(t=v),e.prev=2,e.next=5,t.getPage(l);case 5:return a=e.sent,n=a.getViewport({scale:s,rotation:r}),(i=L.current).height=n.height,i.width=n.width,(u=i.getContext("2d")).clearRect(0,0,i.width,i.height),u.beginPath(),d={canvasContext:u,viewport:n},c=a.render(d),null!=T&&T._internalRenderTask.cancel(),e.prev=16,e.next=19,c.promise;case 19:0!==Object.entries(m).length&&(f=m.text,h=m.diagonal,p=m.opacity,g=m.font,b=m.size,R=m.color,u.globalAlpha=p,u.font="".concat(b*s,"px ").concat(""!==g?g:"Comic Sans MS"),u.fillStyle=R,x=u.measureText(f),E=x.width,y=b*s,u.translate(n.width/2,n.height/2),h&&u.rotate(-.785),u.fillText(f,-E/2,y/2)),e.next=27;break;case 22:e.prev=22,e.t0=e.catch(16),console.warn("Error occured while rendering !\n",e.t0),o(-1),z({status:!0,message:"Error occured while rendering !"});case 27:B(c),e.next=35;break;case 30:e.prev=30,e.t1=e.catch(2),console.warn("Error while reading the pages !\n",e.t1),o(-1),z({status:!0,message:"Error while reading the pages !"});case 35:case"end":return e.stop()}}),e,null,[[2,30],[16,22]])})));return function(){return e.apply(this,arguments)}}(),K=function(){var e=w.default(N.default.mark((function e(t){var a,n,l,s,r,i,o,u,c,f,h,p,g,b,v,R,x,E,y;return N.default.wrap((function(e){for(;;)switch(e.prev=e.next){case 0:if(a=[],0===Object.entries(d).length){e.next=27;break}n=.1,l=0,1<=d.scale&&d.scale<=5&&(n=d.scale/10),-90!==d.rotationAngle&&90!==d.rotationAngle||(l=d.rotationAngle),s=1;case 7:if(!(s<=t.numPages)){e.next=27;break}return e.next=10,t.getPage(s);case 10:return r=e.sent,i=r.getViewport({scale:n,rotation:l}),(o=j.current).height=i.height,o.width=i.width,(u=o.getContext("2d")).clearRect(0,0,o.width,o.height),u.beginPath(),c={canvasContext:u,viewport:i},f=r.render(c),e.next=22,f.promise;case 22:0!==Object.entries(m).length&&(h=m.text,p=m.diagonal,g=m.opacity,b=m.font,v=m.size,R=m.color,u.globalAlpha=g,u.font="".concat(v*n,"px ").concat(""!==b?b:"Comic Sans MS"),u.fillStyle=R,x=u.measureText(h),E=x.width,y=v*n,u.translate(i.width/2,i.height/2),p&&u.rotate(-.785),u.fillText(h,-E/2,y/2)),a.push({image:o.toDataURL("image/png"),height:i.height,width:i.width});case 24:s++,e.next=7;break;case 27:return e.abrupt("return",a);case 28:case"end":return e.stop()}}),e)})));return function(t){return e.apply(this,arguments)}}(),Q=function(e){if(0!==Object.entries(d).length&&null!==e){for(var t=[],a=function(a){var n=e[a-1].image,s="",r={height:e[a-1].height,width:e[a-1].width,display:"flex",cursor:"pointer"};d.thumbCss&&d.selectedThumbCss?s=l===a?d.selectedThumbCss:d.thumbCss:l===a?(r.margin="10px 20px",r.border="5px solid rgba(58, 58, 64, 1)",r.boxShadow="rgba(0, 0, 0, 0.6) 0 4px 8px 0, rgba(0, 0, 0, 0.58) 0 6px 20px 0"):(r.margin="15px 25px",r.boxShadow="rgba(0, 0, 0, 0.6) 0px 2px 2px 0px"),t.push(y.default.createElement("img",{style:r,className:s,onClick:function(){return u(a)},ref:l===a?D:null,key:a,alt:"thumbnail of page ".concat(a),src:n}))},n=1;n<=e.length;n++)a(n);t.push(y.default.createElement("div",{key:0,style:{padding:"0px 10px"}})),H(t)}};if(i.useEffect((function(){U()}),[t,n,l,s,r]),i.useEffect((function(){Q(k),null!==D.current&&0!==Object.entries(d).length&&D.current.scrollIntoView({behavior:"smooth",block:"nearest",inline:"center"})}),[l]),0!==Object.entries(d).length){var Y={backgroundColor:d.backgroundColor?d.backgroundColor:"#EAE6DA",display:"flex",flexDirection:"row",overflowX:"auto"};return d.onTop?y.default.createElement(y.default.Fragment,null,y.default.createElement("div",{className:p||"",style:p?{}:{height:"1000px",overflow:"auto"}},y.default.createElement("div",{style:_.status?{display:"block"}:{display:"none"}},y.default.createElement(J,{message:_.message})),y.default.createElement("div",{style:Y},W),y.default.createElement("canvas",{style:_.status?{display:"none"}:null,onContextMenu:function(e){return c?e.preventDefault():null},ref:L,width:"undefined"!=typeof window&&window.innerWidth,height:"undefined"!=typeof window&&window.innerHeight})),y.default.createElement("canvas",{ref:j,style:{display:"none"}})):y.default.createElement(y.default.Fragment,null,y.default.createElement("div",{className:p||"",style:p?{}:{height:"1000px",overflow:"auto"}},y.default.createElement("div",{style:_.status?{display:"block"}:{display:"none"}},y.default.createElement(J,{message:_.message})),y.default.createElement("canvas",{style:_.status?{display:"none"}:null,onContextMenu:function(e){return c?e.preventDefault():null},ref:L,width:"undefined"!=typeof window&&window.innerWidth,height:"undefined"!=typeof window&&window.innerHeight})),y.default.createElement("div",{style:Y},W),y.default.createElement("canvas",{ref:j,style:{display:"none"}}))}return y.default.createElement("div",null,y.default.createElement("div",{className:p||"",style:p?{}:{height:"1000px",overflow:"auto"}},y.default.createElement("div",{style:_.status?{display:"block"}:{display:"none"}},y.default.createElement(J,{message:_.message})),y.default.createElement("canvas",{style:_.status?{display:"none"}:null,onContextMenu:function(e){return c?e.preventDefault():null},ref:L,width:"undefined"!=typeof window&&window.innerWidth,height:"undefined"!=typeof window&&window.innerHeight})))};Z.propTypes={document:k.default.any.isRequired,withCredentials:k.default.bool,password:k.default.string,pageNum:k.default.number.isRequired,scale:k.default.number.isRequired,rotation:k.default.number.isRequired,changePage:k.default.func,pageCount:k.default.func,showThumbnail:k.default.shape({scale:k.default.number,rotationAngle:k.default.number,onTop:k.default.bool,backgroundColor:k.default.string,thumbCss:k.default.string,selectedThumbCss:k.default.string}),protectContent:k.default.bool,watermark:k.default.shape({text:k.default.string,diagonal:k.default.bool,opacity:k.default.string,size:k.default.string,color:k.default.string}),canvasCss:k.default.string},Z.defaultProps={changePage:function(){},pageCount:function(){},showThumbnail:{},protectContent:!1,watermark:{},canvasCss:""};var A=function(e){var t=e.css,a=e.page,n=e.pages,l=e.handleNextClick,s=t||"button is-black my-0 mx-3";return a===n?y.default.createElement("button",{className:s,disabled:!0},y.default.createElement("span",{className:"icon is-small"},y.default.createElement("i",{className:"material-icons"},"keyboard_arrow_right"))):y.default.createElement("button",{className:s,onClick:l},y.default.createElement("span",{className:"icon is-small"},y.default.createElement("i",{className:"material-icons"},"keyboard_arrow_right")))};A.propTypes={css:k.default.string,page:k.default.number.isRequired,pages:k.default.number.isRequired,handleNextClick:k.default.func.isRequired};var T=function(e){var t=e.css,a=e.page,n=e.pages,l=t||"is-size-7 has-text-centered my-0 mx-3";return y.default.createElement("span",{className:l},"Page ".concat(a," / ").concat(n))};T.propTypes={css:k.default.string,page:k.default.number.isRequired,pages:k.default.number.isRequired};var B=function(e){var t=e.css,a=e.page,n=e.handlePrevClick,l=t||"button is-black my-0 mx-3";return 1===a?y.default.createElement("button",{className:l,disabled:!0},y.default.createElement("span",{className:"icon is-small"},y.default.createElement("i",{className:"material-icons"},"keyboard_arrow_left"))):y.default.createElement("button",{className:l,onClick:n},y.default.createElement("span",{className:"icon is-small"},y.default.createElement("i",{className:"material-icons"},"keyboard_arrow_left")))};B.propTypes={css:k.default.string,page:k.default.number.isRequired,handlePrevClick:k.default.func.isRequired};var O=function(e){var t=e.css,a=e.scale,n=e.defaultScale,l=e.maxScale,s=e.handleZoomIn,r=t||"button is-black my-0 mx-3",i=l;return n>l&&(i=n),a.toFixed(2)===i.toFixed(2)?y.default.createElement("button",{className:r,disabled:!0},y.default.createElement("span",{className:"icon is-small"},y.default.createElement("i",{className:"material-icons"},"zoom_in"))):y.default.createElement("button",{className:r,onClick:s},y.default.createElement("span",{className:"icon is-small"},y.default.createElement("i",{className:"material-icons"},"zoom_in")))};O.propTypes={css:k.default.string,scale:k.default.number.isRequired,defaultScale:k.default.number.isRequired,maxScale:k.default.number.isRequired,handleZoomIn:k.default.func.isRequired};var I=function(e){var t=e.css,a=e.scale,n=e.defaultScale,l=e.minScale,s=e.handleZoomOut,r=t||"button is-black my-0 mx-3",i=l;return nthis.props.maxScale&&(e=this.state.defaultScale),this.state.scalee&&this.setState((function(e,t){return{scale:e.scale-t.scaleStep}})),this.props.onZoom&&this.props.onZoom(this.state.scale-this.props.scaleStep)}},{key:"handleRotateLeft",value:function(){-90!==this.state.rotationAngle&&this.setState({rotationAngle:-90}),this.props.onRotation&&this.props.onRotation(-90)}},{key:"handleResetRotation",value:function(){0!==this.state.rotationAngle&&this.setState({rotationAngle:0}),this.props.onRotation&&this.props.onRotation(0)}},{key:"handleRotateRight",value:function(){90!==this.state.rotationAngle&&this.setState({rotationAngle:90}),this.props.onRotation&&this.props.onRotation(90)}},{key:"render",value:function(){var e=this,t=this.props,a=t.document,n=t.withCredentials,l=t.password,s=t.loader,r=t.maxScale,i=t.minScale,o=t.externalInput,u=t.hideNavbar,d=t.hideZoom,c=t.hideRotation,f=t.navbarOnTop,m=t.navigation,h=t.css,g=t.canvasCss,b=t.onDocumentClick,v=t.showThumbnail,R=t.protectContent,x=t.watermark,E=t.alert,k=this.state,w=k.page,C=k.pages,N=k.scale,S=k.defaultScale,P=k.rotationAngle,q=m,A=y.default.createElement(Z,{document:a,withCredentials:n,password:l,pageNum:w,scale:N,rotation:P,changePage:function(t){return e.handleThumbnailClick(t)},pageCount:function(t){return e.getPageCount(t)},showThumbnail:v,protectContent:R,watermark:x,alert:E,canvasCss:g}),T=null,B=u;return o&&(B=!0),!B&&C>0&&(T=m&&"object"!==p.default(m)?y.default.createElement(q,{page:w,pages:C,scale:N,defaultScale:S,maxScale:r,minScale:i,rotationAngle:P,hideZoom:d,hideRotation:c,handleNextClick:this.handleNextClick,handlePrevClick:this.handlePrevClick,handleZoomIn:this.handleZoomIn,handleResetZoom:this.handleResetZoom,handleZoomOut:this.handleZoomOut,handleRotateLeft:this.handleRotateLeft,handleResetRotation:this.handleResetRotation,handleRotateRight:this.handleRotateRight}):y.default.createElement(D,{page:w,pages:C,scale:N,defaultScale:S,maxScale:r,minScale:i,rotationAngle:P,hideZoom:d,hideRotation:c,css:m?m.css:void 0,handleNextClick:this.handleNextClick,handlePrevClick:this.handlePrevClick,handleZoomIn:this.handleZoomIn,handleResetZoom:this.handleResetZoom,handleZoomOut:this.handleZoomOut,handleRotateLeft:this.handleRotateLeft,handleResetRotation:this.handleResetRotation,handleRotateRight:this.handleRotateRight})),y.default.createElement("div",{className:h||"container text-center"},y.default.createElement("div",{style:{display:this.state.isReady?"none":"block"}},y.default.createElement("div",{className:g||"",style:g?{}:{height:"1000px",overflow:"auto"}},s||y.default.createElement(M,null))),y.default.createElement("div",{style:{display:this.state.isReady?"block":"none"}},f?y.default.createElement("div",null,y.default.createElement("div",null,T),y.default.createElement("div",{onClick:b},A)):y.default.createElement("div",null,y.default.createElement("div",{onClick:b},A),y.default.createElement("div",null,T))))}}],[{key:"getDerivedStateFromProps",value:function(e,t){return e.externalInput?e.page!==t.prevPropPage&&1<=e.page&&e.page<=t.pages?{page:e.page,prevPropPage:e.page}:e.scale!==t.prevPropScale&&e.minScale<=e.scale&&e.scale<=e.maxScale?{scale:e.scale,prevPropScale:e.scale}:e.rotationAngle===t.prevPropRotationAngle||90!==e.rotationAngle&&0!==e.rotationAngle&&-90!==e.rotationAngle?null:{rotationAngle:e.rotationAngle,prevPropRotationAngle:e.rotationAngle}:null}}]),a}(y.default.Component);H.propTypes={document:k.default.shape({url:k.default.string,base64:k.default.string}).isRequired,withCredentials:k.default.bool,password:k.default.string,loader:k.default.node,externalInput:k.default.bool,page:k.default.number,scale:k.default.number,scaleStep:k.default.number,maxScale:k.default.number,minScale:k.default.number,css:k.default.string,canvasCss:k.default.string,rotationAngle:k.default.number,onDocumentClick:k.default.func,onPrevBtnClick:k.default.func,onNextBtnClick:k.default.func,onZoom:k.default.func,onRotation:k.default.func,getMaxPageCount:k.default.func,hideNavbar:k.default.bool,navbarOnTop:k.default.bool,hideZoom:k.default.bool,hideRotation:k.default.bool,showThumbnail:k.default.shape({scale:k.default.number,rotationAngle:k.default.number,onTop:k.default.bool,backgroundColor:k.default.string,thumbCss:k.default.string,selectedThumbCss:k.default.string}),protectContent:k.default.bool,watermark:k.default.shape({text:k.default.string,diagonal:k.default.bool,opacity:k.default.string,font:k.default.string,size:k.default.string,color:k.default.string}),alert:k.default.any,navigation:k.default.oneOfType([k.default.shape({css:k.default.shape({navbarWrapper:k.default.string,zoomOutBtn:k.default.string,resetZoomBtn:k.default.string,zoomInBtn:k.default.string,previousPageBtn:k.default.string,pageIndicator:k.default.string,nextPageBtn:k.default.string,rotateLeftBtn:k.default.string,resetRotationBtn:k.default.string,rotateRightBtn:k.default.string})}),k.default.any])},H.defaultProps={page:1,withCredentials:!1,password:"",scale:1,scaleStep:1,maxScale:3,minScale:1,rotationAngle:0,externalInput:!1,hideNavbar:!1,hideZoom:!1,hideRotation:!1,navbarOnTop:!1,canvasCss:""},module.exports=H; 2 | -------------------------------------------------------------------------------- /example/.env: -------------------------------------------------------------------------------- 1 | SKIP_PREFLIGHT_CHECK=true -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | 4 | # testing 5 | /coverage 6 | 7 | # production 8 | /build 9 | 10 | # misc 11 | .DS_Store 12 | .env.local 13 | .env.development.local 14 | .env.test.local 15 | .env.production.local 16 | 17 | npm-debug.log* 18 | yarn-debug.log* 19 | yarn-error.log* 20 | 21 | # browserstack 22 | browserstack.err 23 | local.log -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # pdf-viewer-reactjs example 2 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "homepage": "https://ansu5555.github.io/pdf-viewer-reactjs/", 6 | "dependencies": { 7 | "bulma": "^0.9.2", 8 | "material-design-icons": "^3.0.1", 9 | "pdf-viewer-reactjs": "file:..", 10 | "typescript": "^4.1.5" 11 | }, 12 | "devDependencies": { 13 | "@wdio/browserstack-service": "^7.1.1", 14 | "@wdio/cli": "^7.1.0", 15 | "@wdio/local-runner": "^7.1.0", 16 | "@wdio/mocha-framework": "^7.0.7", 17 | "@wdio/spec-reporter": "^7.0.7", 18 | "@wdio/sync": "^7.1.0", 19 | "chromedriver": "^89.0.0", 20 | "gh-pages": "^3.1.0", 21 | "serve": "^11.3.2", 22 | "start-server-and-test": "^1.12.0", 23 | "wdio-chromedriver-service": "^7.0.0" 24 | }, 25 | "scripts": { 26 | "start": "react-scripts start", 27 | "start-server": "PUBLIC_URL=/ react-scripts build && serve -s build -l 3000", 28 | "build": "react-scripts build", 29 | "unittest": "react-scripts test --env=jsdom --watchAll=false", 30 | "e2etest": "wdio run ./wdio.conf.js", 31 | "testalllocal": "react-scripts test --env=jsdom --watchAll=false && wdio run ./wdio.conf.local.js", 32 | "testall": "npm run unittest && npm run e2etest", 33 | "test": "start-server-and-test start-server http://localhost:3000 testall", 34 | "predeploy": "npm run build", 35 | "deploy": "gh-pages -d build" 36 | }, 37 | "browserslist": { 38 | "production": [ 39 | ">0.2%", 40 | "not dead", 41 | "not op_mini all" 42 | ], 43 | "development": [ 44 | "last 1 chrome version", 45 | "last 1 firefox version", 46 | "last 1 safari version" 47 | ] 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /example/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansu5555/pdf-viewer-reactjs/3907cc4de70b9c20acf5f2eac40fc4bdbf20f9ba/example/public/favicon.ico -------------------------------------------------------------------------------- /example/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | pdf-viewer-reactjs 23 | 24 | 25 | 26 |
27 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /example/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "pdf-viewer-reactjs", 3 | "name": "pdf-viewer-reactjs examples", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64", 8 | "type": "image/png" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /example/src/App.css: -------------------------------------------------------------------------------- 1 | .bordered { 2 | border-color: #383838; 3 | border-width: 3px; 4 | border-style: solid; 5 | } 6 | 7 | .customViewer { 8 | border-color: #fa5b35; 9 | border-width: 1px; 10 | border-style: solid; 11 | } 12 | .customCanvas { 13 | height: 500px; 14 | max-height: 500px; 15 | min-width: 500px; 16 | max-width: 500px; 17 | overflow: auto; 18 | } 19 | .customPrevBtn { 20 | cursor: pointer; 21 | display: inline-block; 22 | margin: 0; 23 | width: auto; 24 | color: #fa5b35; 25 | background-color: #f3f3f3; 26 | border-color: #fa5b35; 27 | border-width: 1px; 28 | border-style: solid; 29 | border-radius: 6px; 30 | } 31 | .customNextBtn { 32 | cursor: pointer; 33 | display: inline-block; 34 | margin: 0; 35 | width: auto; 36 | color: #fa5b35; 37 | background-color: #f3f3f3; 38 | border-color: #333333; 39 | border-width: 1px; 40 | border-style: solid; 41 | border-radius: 6px; 42 | } 43 | .customResetBtn { 44 | cursor: pointer; 45 | display: inline-block; 46 | margin: 0; 47 | width: auto; 48 | color: #45fa35; 49 | background-color: #f3f3f3; 50 | border-color: #45fa35; 51 | border-width: 1px; 52 | border-style: solid; 53 | border-radius: 6px; 54 | } 55 | .customPages { 56 | font-size: medium; 57 | margin: 0.5rem; 58 | } 59 | .customWrapper { 60 | height: 40px; 61 | font-size: 24px; 62 | padding: 12px; 63 | padding-bottom: 0; 64 | background-color: #fff; 65 | } 66 | -------------------------------------------------------------------------------- /example/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import PDFViewer from 'pdf-viewer-reactjs' 3 | 4 | import CustomNavigation from './Navigation' 5 | import sources from './Sources' 6 | 7 | import './App.css' 8 | 9 | const minScale = 0.5 10 | const maxScale = 3 11 | 12 | const FromUrl = () => ( 13 |
14 |
15 | Fetch PDF by URL 16 |
17 | 23 |
24 | ) 25 | 26 | const FromBase64 = () => ( 27 |
28 |
29 | Load PDF from base 64 string 30 |
31 | 36 |
37 | ) 38 | 39 | const ErrorHandling = () => ( 40 |
41 |
42 | Error message for failures 43 |
44 | 49 |
50 | ) 51 | 52 | const CustomErrorHandling = () => ( 53 |
54 |
55 | Custom Error component for failures 56 |
57 | ( 62 |
67 |

Failed To load !!!

68 |
{err.message}
69 |
70 | )} 71 | /> 72 |
73 | ) 74 | 75 | const WithCustomLoader = () => ( 76 |
77 |
78 | Custom loader element 79 |
80 | Custom loader element} 85 | /> 86 |
87 | ) 88 | 89 | const WithCustomStartingPage = () => ( 90 |
91 |
92 | Custom starting page 93 |
94 | 100 |
101 | ) 102 | 103 | const WithCustomScale = () => ( 104 |
105 |
106 | Custom scale 107 |
108 | 117 |
118 | ) 119 | 120 | const WithCustomNavigationStyles = () => ( 121 |
122 |
123 | Custom css classes 124 |
125 | 146 |
147 | ) 148 | 149 | const WithCustomNavigation = () => ( 150 |
151 |
152 | Custom navigation 153 |
154 | 161 |
162 | ) 163 | 164 | const WithOnDocumentClick = () => ( 165 |
166 |
167 | 168 | With onDocumentClick, onPrevBtnClick, onNextBtnClick, onZoom and 169 | onRotation handler 170 | 171 |
172 | alert('Document was clicked')} 177 | onPrevBtnClick={(page) => alert(`Page ${page} selected`)} 178 | onNextBtnClick={(page) => alert(`Page ${page} selected`)} 179 | onZoom={(scale) => alert(`Zoom scale is ${scale}`)} 180 | onRotation={(angle) => alert(`Page angle is ${angle}`)} 181 | /> 182 |
183 | ) 184 | 185 | const WithoutNavigation = () => ( 186 |
187 |
188 | Without Navigation 189 |
190 | 196 |
197 | ) 198 | 199 | const WithoutZoomRotation = () => ( 200 |
201 |
202 | Without Zoom and Rotation 203 |
204 | 211 |
212 | ) 213 | 214 | const WithNavbarTop = () => ( 215 |
216 |
217 | Navigation Bar on top 218 |
219 | 225 |
226 | ) 227 | 228 | const WithWatermark = () => ( 229 |
230 |
231 | Watermark and Protected 232 |
233 | 247 |
248 | ) 249 | 250 | const WithExternalControl = () => { 251 | let [doc, setDoc] = useState({ 252 | url: 'https://arxiv.org/pdf/quant-ph/0410100.pdf', 253 | }) 254 | let [pages, setPages] = useState(undefined) 255 | let [pageNo, setPageNo] = useState(1) 256 | let [scale, setScale] = useState(0.5) 257 | let [rotation, setRotation] = useState(0) 258 | 259 | return ( 260 |
261 |
262 | External Controls 263 |
264 |
265 | setPages(pageCount)} 275 | /> 276 |
277 |
278 |
282 | 292 |
293 | Page Number is {pageNo} / {pages} 294 |
295 | 305 |
306 |
307 |
308 |
312 | 322 |
Scale is {scale}
323 | 333 |
334 |
335 |
336 |
340 | 350 |
Rotation Angle is {rotation}
351 | 352 | 362 |
363 |
364 |
365 | 366 |
367 |
368 | 381 |
382 |
383 | 396 |
397 |
398 |
399 |
400 | ) 401 | } 402 | 403 | function App() { 404 | return ( 405 | <> 406 |
407 | 408 |
409 |
410 | 411 |
412 |
413 | 414 |
415 |
416 | 417 |
418 |
419 | 420 |
421 |
422 | 423 |
424 |
425 | 426 |
427 |
428 | 429 |
430 |
431 | 432 |
433 |
434 | 435 |
436 |
437 | 438 |
439 |
440 | 441 |
442 |
443 | 444 |
445 |
446 | 447 |
448 |
449 | 450 |
451 | 452 | ) 453 | } 454 | 455 | export default App 456 | -------------------------------------------------------------------------------- /example/src/Navigation.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | export const CustomPrevButton = (props) => { 5 | const { page, handlePrevClick } = props 6 | if (page === 1) return
7 | 8 | return ( 9 |

17 | Previous page 18 |

19 | ) 20 | } 21 | CustomPrevButton.propTypes = { 22 | page: PropTypes.number.isRequired, 23 | pages: PropTypes.number.isRequired, 24 | handlePrevClick: PropTypes.func.isRequired, 25 | } 26 | 27 | export const CustomNextButton = (props) => { 28 | const { page, pages, handleNextClick } = props 29 | if (page === pages) return
30 | 31 | return ( 32 |

40 | Next page 41 |

42 | ) 43 | } 44 | CustomNextButton.propTypes = { 45 | page: PropTypes.number.isRequired, 46 | pages: PropTypes.number.isRequired, 47 | handleNextClick: PropTypes.func.isRequired, 48 | } 49 | 50 | export const CustomPages = (props) => { 51 | const { page, pages } = props 52 | return ( 53 |

54 | Page {page} from {pages} 55 |

56 | ) 57 | } 58 | CustomPages.propTypes = { 59 | page: PropTypes.number.isRequired, 60 | pages: PropTypes.number.isRequired, 61 | } 62 | 63 | const CustomNavigation = (props) => { 64 | const { page, pages } = props 65 | 66 | const { handlePrevClick, handleNextClick } = props 67 | 68 | return ( 69 |
70 | 75 | 76 | 81 |
82 | ) 83 | } 84 | CustomNavigation.propTypes = { 85 | page: PropTypes.number.isRequired, 86 | pages: PropTypes.number.isRequired, 87 | handlePrevClick: PropTypes.func.isRequired, 88 | handleNextClick: PropTypes.func.isRequired, 89 | } 90 | 91 | export default CustomNavigation 92 | -------------------------------------------------------------------------------- /example/src/Sources.js: -------------------------------------------------------------------------------- 1 | export default { 2 | url: 'https://arxiv.org/pdf/quant-ph/0410100.pdf', 3 | base64: 4 | 'JVBERi0xLjMNJeLjz9MNCjcgMCBvYmoNPDwvTGluZWFyaXplZCAxL0wgNzk0NS9PIDkvRSAzNTI0L04gMS9UIDc2NTYvSCBbIDQ1MSAxMzddPj4NZW5kb2JqDSAgICAgICAgICAgICAgICAgICAgICAgDQoxMyAwIG9iag08PC9EZWNvZGVQYXJtczw8L0NvbHVtbnMgNC9QcmVkaWN0b3IgMTI+Pi9GaWx0ZXIvRmxhdGVEZWNvZGUvSURbPDREQzkxQTE4NzVBNkQ3MDdBRUMyMDNCQjAyMUM5M0EwPjxGNkM5MkIzNjhBOEExMzQwODQ1N0ExRDM5NUEzN0VCOT5dL0luZGV4WzcgMjFdL0luZm8gNiAwIFIvTGVuZ3RoIDUyL1ByZXYgNzY1Ny9Sb290IDggMCBSL1NpemUgMjgvVHlwZS9YUmVmL1dbMSAyIDFdPj5zdHJlYW0NCmjeYmJkEGBgYmCyARIMIIKxAUgwpwIJNkcg8eUYAxMjwzSQLAMjucR/xp1fAAIMAEykBvANCmVuZHN0cmVhbQ1lbmRvYmoNc3RhcnR4cmVmDQowDQolJUVPRg0KICAgICAgICANCjI3IDAgb2JqDTw8L0ZpbHRlci9GbGF0ZURlY29kZS9JIDY5L0xlbmd0aCA1OC9TIDM4Pj5zdHJlYW0NCmjeYmBgYGFgYPzPAATcNgyogJEBJMvRgCzGAsUMDA0M3Azc0x50JoA4zAwMWgIQLYwsAAEGAL/iBRkNCmVuZHN0cmVhbQ1lbmRvYmoNOCAwIG9iag08PC9NZXRhZGF0YSAxIDAgUi9QYWdlcyA1IDAgUi9UeXBlL0NhdGFsb2c+Pg1lbmRvYmoNOSAwIG9iag08PC9Db250ZW50cyAxMSAwIFIvQ3JvcEJveFswIDAgNTk1IDg0Ml0vTWVkaWFCb3hbMCAwIDU5NSA4NDJdL1BhcmVudCA1IDAgUi9SZXNvdXJjZXMgMTQgMCBSL1JvdGF0ZSAwL1R5cGUvUGFnZT4+DWVuZG9iag0xMCAwIG9iag08PC9GaWx0ZXIvRmxhdGVEZWNvZGUvRmlyc3QgOTQvTGVuZ3RoIDc3My9OIDEzL1R5cGUvT2JqU3RtPj5zdHJlYW0NCmjevFRtb9owEP4r/gPgl9hxIlVI0I6u0lqhJls/RPmQgguRQoISV6P/fncJLoG1K6XSiMz55e58vue545IwwhXhnibcJyKAlSaeCAgPiOeDCImUighGVMiI4CQUoCYIZ1oS4YGt5kRIsGIhEeAokLAGFcYkubigl1VR1dEmmxtcNAovY+R+NKLftvY6spnFg+uI4/XdwbQqLexNBcYAWzSOBQbQTSXe3k19vLibBnhnZz6rq3lkbEJnV1Mam61NR6OEXmbF/fUEr8rW6ywRQwE/iPRQpvQ2s3W+TdhQcnQ+FBwdDxkPPRCe0rjSXEFe2JDzUKAImEIdjZENQ8VUSh9WuTWzKi9t0m0ReOGQBSFEk0IY0Zg8ZUVjaHSLpoLG9/RmYUqb2xcav2zMPj+jEehf5U9Ppjbl3DQJp4/PRWFsulMs59UiL5et3iRrDCaQRi/rx6p4PURYMVXR86NFI7TkNK5+ljkoGMJ3ScUztG+djZs5RERCpiB/m+8mX64sYfTKdPsDwTmdFtmyAca0VpNJtU0GPtBn4GkkgQfMYDJI29O7bG3ouM6zYjCpisVtTG9sVuTzcbksDPiNrFn/Aip6+zDwqjrf2Ko+fN2BF/dG+pCX47LJX9fTvG7s5SqrXXx7d0hsfPCPbKfBub9PTv1sYpel1hBcL+yqSYRGSn7ta2nyKn3O39Dxff2hH6X81rovuxMXpZPuDi8IWy3P89I+wEHI3wPYdwDLHsDKR4CZBoCxUzCmewDH+do0d+b3fbXOyln0DsrsY4z/dnQW0IIfAa3lKUCrw2RDjWPa2tGmVu3/T4UcQe1me6iOAXXQO8hCKd/QlLCr2KHEyHCOo08ADcPt49i9A6ggeie7uBgj/+vTPku/1GV8BSQUypHQ08dd5nzqOfPzCOcdEg40Tmosny3JMOiXpNRdSXLBfMyGeL8k277ZZeYoRQOuPtOF/+n3vNypo2IV/Ixi3X+nFuipPfeDjsxccbr/rqgP+zHu9IoRCtEVo4tiV9JAiD8CDAA+0IrxDQplbmRzdHJlYW0NZW5kb2JqDTExIDAgb2JqDTw8L0ZpbHRlci9GbGF0ZURlY29kZS9MZW5ndGggMTUzMD4+c3RyZWFtDQpIibRXS2/jNhBGr/4Vc1uqiBW9H8d0tynQ02IroIduD7LEJCpk0RDppPlT/Y2dB2l7nS0KLFoEUPgacuabmW/GP3Sb267LIIXuYZMWcVJAgn8yytI8rqukgrqscZ7k0O03t+9tCYPlYwnYYXP70y8pPNrNNomTJKugGzY0qhroXja/qbsoTeJMjdG2jlNldhqibUpD3GjiWg3RNlNrtK3iCnd7Bx8/3MP9RAuNmrWNfu9+Jh0Lr2MmCmbQtHGbkXJZG+eZKMc6JK3XIaMR6zDiu3/BR7O6fjdr+GBQhyRu1XDc68XBfVTGucJFWlv3uJmjgqjLZ4Xa8ObnCCZLqieqh+MyPevV9rMsPEwzWZXhyKx7FONV9xRGh5WMb5W2en32L+sow2+4cZ7ZzAS2aZyW0H1gCJPGG9K2mRhiHqIcYYGI79dRgaDxRNbN4uzN5TxK8LvymKyKC9WzjHPTEm1b9MsjuadRN3ySRQc+IaKzOYq05S0RXkZ4lFWZH54mkbFRosDIvV5RL8GXvcpTYrLFm0XKWzEamR5JUdJUX4i6G5AXdbQtcc9r3dMs9waOorGIWQuIFWHafe+jogiRSSMCEwGE/nCYp6F3k1mgR8MOc+/IiXC0rEam9AjOwLBqCdEe3yqU0zC5OPgsi3PvspTC8BRxjJkEUCvYTh7HRWYjX1rypaWaxXMSQg8Somgc6NkfG/iYW80yDYQXQ5XhEsXwOFm3TrujmGJRPzAYpIPZawsUK1cBJqDUJ1BqUfywGsyQvQUU3Jtl5hda8h1mmQK9sFqYtua4OM2BXRNGL5N7Ik0HVs9LDcCpYZ96MgBTC4M+V9PyGNFlgt/tvWcfAbJhJFkrUkh9F3V/UPpX/lBcVJj+eAYBlZ3GE4NwV0id0htWtSXfc7e8mkXfoJNfX540elOEPaugEV6YYUm9cJ0KKDCgx8xBI7BIT9G2wUAjr2aKDYzhbiYqyBPGSZmjxPiiCR4OIZ4HAqHAE+JA/DCm/YxihoJOhfmw+oUeccMkYLy2rCu5sQjGpj6006SpROFPmrXr+TtGkk40XjE7ChVzpH3SA69NxHuNOkxyZOHjTiIVk4gEZExRdL7E8wwNEQOPBk8N3yCn9nK5aOJkYsFiVMrK5AcYcBcqL4Rxpd5FmIJVEEMPyPKlnvClBhZ2+vKiIx+yXj0yYIu1jbjoq+nwhiNGs7zDYEXw4akX7iYoiQPgzB+eGij1LDLHP1EGCZzTtqK0tVdJgPqU35gHxdfyQEJjG4ZkEhFSTYx7jVyotD6hsAUoLy4qzxeVclE/v/SvXByR+JEF4LBOSESDL6ZoiVpXzTNZc/PrVTXHRGov8i7JTvj7ggfMy1RbUUUmoca/MwkTUQXjxVE/iyPEP/U1vZDfi+K/xDb0GWndppfQpgRtjnQ3cTGqEdqe/xOZIgwvyIYp4fEaZdQKEHoogwSO1efLrWufUOvwluXkcS6NtfqzH97inF3hHDRvQ4dEFYNJh6OWbOi5QXF6pNIr7YtsEN5hex1n3yz5fobKLtYu7kOseXBkKwmtTL2jMBgKNPmZwr5MvSqkHvLt2gc3F/ysb3awNGdpiAes9Q7rlVAakfJlG0QlXQTZBmx/qFkJzQxnJ9WkSkmtXoyD2VgspkdNKRy6gbMtLIG2SNvmDbpq29LsnCo+jJ8xDZgQM/Y2Zh3G9bRgWnCiZGp/QL5CNtxN8+SIiNX/yQzbs5oUvkHLDvnpQfyPSQR3g4xWbss/6X4MLdFKvbA/1zN+5BJ2CJVGgm40L8ts+pG7KoksrKG7U+ELr2D8ZESPQfTUxiCJ7i5Z+hwqeXMR9UQOFE90QYW6YdtEs7CqsSX9dyC/mV1zgbBoGt8+vTfsSYz4gb9OflOcOsEaSfFUOHNPvumpvabxKnksG2D3sjr7kyvLYSmRZSqCPKXKGIQm/0NGjlKnzaPBX3n9tL9p9D6Tm2QR3fdVF4SI4ah9pHAFjl9EXUYghV0eY680/EukCF0CF2hl3QXtEelReBHnc6uh4Ff67sSBP3abvwcArRiH3QoNCmVuZHN0cmVhbQ1lbmRvYmoNMTIgMCBvYmoNPDwvRmlsdGVyL0ZsYXRlRGVjb2RlL0xlbmd0aCAyMDg+PnN0cmVhbQ0KSIlUkL0OwjAMhPc+hUcQQ9rOVRdYOvAjCuxp4laRiBO56dC3JykFxBBL9uXTnS32zaEhE0Bc2KkWA/SGNOPoJlYIHQ6GoChBGxXWbqnKSg8iwu08BrQN9Q6qKhPXKI6BZ9i0s+3cc5dvQZxZIxsaYHMr7o84aCfvn2iRAuRQ16Cxz8T+KP1JWozyii7zYjV0GkcvFbKkAaHKi/pdkPS/9iG6/t3+vlZlXpZ1FomPluC0yddbTcwx1rLukihlMITfi3jnk2V62UuAAQBDyGk/Cg0KZW5kc3RyZWFtDWVuZG9iag0xIDAgb2JqDTw8L0xlbmd0aCAzNjU2L1N1YnR5cGUvWE1ML1R5cGUvTWV0YWRhdGE+PnN0cmVhbQ0KPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4KPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNC4yLjEtYzA0MyA1Mi4zNzI3MjgsIDIwMDkvMDEvMTgtMTU6MDg6MDQgICAgICAgICI+CiAgIDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+CiAgICAgIDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiCiAgICAgICAgICAgIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyI+CiAgICAgICAgIDxkYzpmb3JtYXQ+YXBwbGljYXRpb24vcGRmPC9kYzpmb3JtYXQ+CiAgICAgICAgIDxkYzpjcmVhdG9yPgogICAgICAgICAgICA8cmRmOlNlcT4KICAgICAgICAgICAgICAgPHJkZjpsaT5jZGFpbHk8L3JkZjpsaT4KICAgICAgICAgICAgPC9yZGY6U2VxPgogICAgICAgICA8L2RjOmNyZWF0b3I+CiAgICAgICAgIDxkYzp0aXRsZT4KICAgICAgICAgICAgPHJkZjpBbHQ+CiAgICAgICAgICAgICAgIDxyZGY6bGkgeG1sOmxhbmc9IngtZGVmYXVsdCI+VGhpcyBpcyBhIHRlc3QgUERGIGZpbGU8L3JkZjpsaT4KICAgICAgICAgICAgPC9yZGY6QWx0PgogICAgICAgICA8L2RjOnRpdGxlPgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgICAgPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIKICAgICAgICAgICAgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIj4KICAgICAgICAgPHhtcDpDcmVhdGVEYXRlPjIwMDAtMDYtMjlUMTA6MjE6MDgrMTE6MDA8L3htcDpDcmVhdGVEYXRlPgogICAgICAgICA8eG1wOkNyZWF0b3JUb29sPk1pY3Jvc29mdCBXb3JkIDguMDwveG1wOkNyZWF0b3JUb29sPgogICAgICAgICA8eG1wOk1vZGlmeURhdGU+MjAxMy0xMC0yOFQxNToyNDoxMy0wNDowMDwveG1wOk1vZGlmeURhdGU+CiAgICAgICAgIDx4bXA6TWV0YWRhdGFEYXRlPjIwMTMtMTAtMjhUMTU6MjQ6MTMtMDQ6MDA8L3htcDpNZXRhZGF0YURhdGU+CiAgICAgIDwvcmRmOkRlc2NyaXB0aW9uPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczpwZGY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vcGRmLzEuMy8iPgogICAgICAgICA8cGRmOlByb2R1Y2VyPkFjcm9iYXQgRGlzdGlsbGVyIDQuMCBmb3IgV2luZG93czwvcGRmOlByb2R1Y2VyPgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgICAgPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIKICAgICAgICAgICAgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iPgogICAgICAgICA8eG1wTU06RG9jdW1lbnRJRD51dWlkOjA4MDVlMjIxLTgwYTgtNDU5ZS1hNTIyLTYzNWVkNWMxZTJlNjwveG1wTU06RG9jdW1lbnRJRD4KICAgICAgICAgPHhtcE1NOkluc3RhbmNlSUQ+dXVpZDo2MmQ2YWU2ZC00M2M0LTQ3MmQtOWIyOC03YzRhZGQ4ZjllNDY8L3htcE1NOkluc3RhbmNlSUQ+CiAgICAgIDwvcmRmOkRlc2NyaXB0aW9uPgogICA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgCjw/eHBhY2tldCBlbmQ9InciPz4NCmVuZHN0cmVhbQ1lbmRvYmoNMiAwIG9iag08PC9GaWx0ZXIvRmxhdGVEZWNvZGUvRmlyc3QgNC9MZW5ndGggNDgvTiAxL1R5cGUvT2JqU3RtPj5zdHJlYW0NCmjeMlUwULCx0XfOL80rUTDU985MKY62BIoFxeqHVBak6gckpqcW29kBBBgA1ncLgA0KZW5kc3RyZWFtDWVuZG9iag0zIDAgb2JqDTw8L0ZpbHRlci9GbGF0ZURlY29kZS9GaXJzdCA0L0xlbmd0aCAxNjcvTiAxL1R5cGUvT2JqU3RtPj5zdHJlYW0NCmjePMvBCsIwEEXRX5mdDaKdxCpVSqFY3AkuBNexSelA6EAyRfx7A4qPu3znAAhNU3aLTByLwVkKb1Weo7dCPPdWfNGfDOYdzFGj0VivtV4hrn6vrK40RE48Cjw4Oqi3qMoruz/WuwxrvTeV3m2w+uJbZLcMPhZdxk8r0FMSCsFHqLYII0d40Oz4lVR5Jwm+uE+UIGdBfBK49RcYKXjVth8BBgBnZztkDQplbmRzdHJlYW0NZW5kb2JqDTQgMCBvYmoNPDwvRGVjb2RlUGFybXM8PC9Db2x1bW5zIDMvUHJlZGljdG9yIDEyPj4vRmlsdGVyL0ZsYXRlRGVjb2RlL0lEWzw0REM5MUExODc1QTZENzA3QUVDMjAzQkIwMjFDOTNBMD48RjZDOTJCMzY4QThBMTM0MDg0NTdBMUQzOTVBMzdFQjk+XS9JbmZvIDYgMCBSL0xlbmd0aCAzNy9Sb290IDggMCBSL1NpemUgNy9UeXBlL1hSZWYvV1sxIDIgMF0+PnN0cmVhbQ0KaN5iYmBgYGLkPcLEwD+ViYGhh4mBkYWJ8bEkkM0IEGAAKlkDFA0KZW5kc3RyZWFtDWVuZG9iag1zdGFydHhyZWYNCjExNg0KJSVFT0YNCg==', 5 | } 6 | -------------------------------------------------------------------------------- /example/src/__tests__/App.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import renderer from 'react-test-renderer' 3 | import App from '../App' 4 | 5 | describe('App', () => { 6 | it('Should render correctly', () => { 7 | const tree = renderer.create().toJSON() 8 | expect(tree).toMatchSnapshot() 9 | }) 10 | }) 11 | -------------------------------------------------------------------------------- /example/src/index.css: -------------------------------------------------------------------------------- 1 | @import url('bulma/css/bulma.css'); 2 | @import url('material-design-icons/iconfont/material-icons.css'); 3 | 4 | html { 5 | background-color: #fafafa; 6 | } 7 | 8 | body { 9 | margin: 0; 10 | padding: 0; 11 | font-family: sans-serif; 12 | } 13 | 14 | a, 15 | u { 16 | text-decoration: none; 17 | } 18 | -------------------------------------------------------------------------------- /example/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import './index.css' 4 | import App from './App' 5 | import registerServiceWorker from './registerServiceWorker' 6 | 7 | ReactDOM.render(, document.getElementById('root')) 8 | registerServiceWorker() 9 | -------------------------------------------------------------------------------- /example/src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the app load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | const isLocalhost = Boolean( 12 | window.location.hostname === 'localhost' || 13 | // [::1] is the IPv6 localhost address. 14 | window.location.hostname === '[::1]' || 15 | // 127.0.0.1/8 is considered localhost for IPv4. 16 | window.location.hostname.match( 17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 18 | ) 19 | ) 20 | 21 | export default function register() { 22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 23 | // The URL constructor is available in all browsers that support SW. 24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location) 25 | if (publicUrl.origin !== window.location.origin) { 26 | // Our service worker won't work if PUBLIC_URL is on a different origin 27 | // from what our page is served on. This might happen if a CDN is used to 28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 29 | return 30 | } 31 | 32 | window.addEventListener('load', () => { 33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js` 34 | 35 | if (!isLocalhost) { 36 | // Is not local host. Just register service worker 37 | registerValidSW(swUrl) 38 | } else { 39 | // This is running on localhost. Lets check if a service worker still exists or not. 40 | checkValidServiceWorker(swUrl) 41 | } 42 | }) 43 | } 44 | } 45 | 46 | function registerValidSW(swUrl) { 47 | navigator.serviceWorker 48 | .register(swUrl) 49 | .then((registration) => { 50 | registration.onupdatefound = () => { 51 | const installingWorker = registration.installing 52 | installingWorker.onstatechange = () => { 53 | if (installingWorker.state === 'installed') { 54 | if (navigator.serviceWorker.controller) { 55 | // At this point, the old content will have been purged and 56 | // the fresh content will have been added to the cache. 57 | // It's the perfect time to display a "New content is 58 | // available; please refresh." message in your web app. 59 | console.log('New content is available; please refresh.') 60 | } else { 61 | // At this point, everything has been precached. 62 | // It's the perfect time to display a 63 | // "Content is cached for offline use." message. 64 | console.log('Content is cached for offline use.') 65 | } 66 | } 67 | } 68 | } 69 | }) 70 | .catch((error) => { 71 | console.error('Error during service worker registration:', error) 72 | }) 73 | } 74 | 75 | function checkValidServiceWorker(swUrl) { 76 | // Check if the service worker can be found. If it can't reload the page. 77 | fetch(swUrl) 78 | .then((response) => { 79 | // Ensure service worker exists, and that we really are getting a JS file. 80 | if ( 81 | response.status === 404 || 82 | response.headers.get('content-type').indexOf('javascript') === -1 83 | ) { 84 | // No service worker found. Probably a different app. Reload the page. 85 | navigator.serviceWorker.ready.then((registration) => { 86 | registration.unregister().then(() => { 87 | window.location.reload() 88 | }) 89 | }) 90 | } else { 91 | // Service worker found. Proceed as normal. 92 | registerValidSW(swUrl) 93 | } 94 | }) 95 | .catch(() => { 96 | console.log( 97 | 'No internet connection found. App is running in offline mode.' 98 | ) 99 | }) 100 | } 101 | 102 | export function unregister() { 103 | if ('serviceWorker' in navigator) { 104 | navigator.serviceWorker.ready.then((registration) => { 105 | registration.unregister() 106 | }) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /example/test/pageobjects/app.page.js: -------------------------------------------------------------------------------- 1 | class AppPage { 2 | constructor() { 3 | this.title = 'pdf-viewer-reactjs example' 4 | } 5 | 6 | open() { 7 | browser.url('/') 8 | } 9 | 10 | wait(second) { 11 | browser.pause(second * 1000) 12 | } 13 | 14 | /** 15 | * @param {string} id 16 | */ 17 | set section(id) { 18 | this.selectedSection = `#${id}` 19 | } 20 | 21 | get section() { 22 | return $(this.selectedSection) 23 | } 24 | 25 | get alert() { 26 | return this.section 27 | .$('.container') 28 | .$('div*=Error while opening the document') 29 | } 30 | 31 | get loader() { 32 | return this.section.$('.container').$$('div')[0] 33 | } 34 | 35 | get canvas() { 36 | return this.section.$('canvas') 37 | } 38 | 39 | get thumbnails() { 40 | return this.section.$$('img') 41 | } 42 | 43 | get zoomOutButton() { 44 | return this.section.$('button=zoom_out') 45 | } 46 | 47 | get zoomResetButton() { 48 | return this.section.$$('button=refresh')[0] 49 | } 50 | 51 | get zoomInButton() { 52 | return this.section.$('button=zoom_in') 53 | } 54 | 55 | get prevPageButton() { 56 | return this.section.$('button=keyboard_arrow_left') 57 | } 58 | 59 | get pageIndicator() { 60 | return this.section.$('span*=Page') 61 | } 62 | 63 | get nextPageButton() { 64 | return this.section.$('button=keyboard_arrow_right') 65 | } 66 | 67 | get rotateLeftButton() { 68 | return this.section.$('button=rotate_left') 69 | } 70 | 71 | get rotateResetButton() { 72 | return this.section.$$('button=refresh')[1] 73 | } 74 | 75 | get rotateRightButton() { 76 | return this.section.$('button=rotate_right') 77 | } 78 | 79 | get extZoomInButton() { 80 | return this.section.$('button=+') 81 | } 82 | 83 | get extZoomOutButton() { 84 | return this.section.$('button=-') 85 | } 86 | } 87 | 88 | module.exports = new AppPage() 89 | -------------------------------------------------------------------------------- /example/test/specs/app.e2e.js: -------------------------------------------------------------------------------- 1 | const AppPage = require('../pageobjects/app.page') 2 | 3 | describe('Example App for "pdf-viewer-reactjs"', () => { 4 | before(() => { 5 | AppPage.open() 6 | }) 7 | 8 | describe('"Custom loader element" section', () => { 9 | before(() => { 10 | AppPage.section = 'cl' 11 | }) 12 | 13 | it('Should be displayed', () => { 14 | expect(AppPage.section).toBeExisting() 15 | }) 16 | 17 | it('Should display the custom loader message', () => { 18 | expect(AppPage.loader).toBeExisting() 19 | }) 20 | }) 21 | 22 | describe('"Fetch PDF by URL" section', () => { 23 | before(() => { 24 | AppPage.section = 'url' 25 | }) 26 | 27 | it('Should be displayed', () => { 28 | expect(AppPage.section).toBeExisting() 29 | }) 30 | 31 | it('Should display the loader', () => { 32 | expect(AppPage.loader).toBeExisting() 33 | }) 34 | 35 | it('Loader should become hidden', () => { 36 | expect( 37 | AppPage.loader.waitForDisplayed({ 38 | timeout: 60000, 39 | reverse: true, 40 | interval: 5000, 41 | }) 42 | ).toBe(true) 43 | }) 44 | 45 | it('Should display thumbnails of all the pages', () => { 46 | expect(AppPage.thumbnails).toBeElementsArrayOfSize(65) 47 | }) 48 | }) 49 | 50 | describe('"Load PDF from base 64 string" section', () => { 51 | before(() => { 52 | AppPage.section = 'base64' 53 | }) 54 | 55 | it('Should be displayed', () => { 56 | expect(AppPage.section).toBeExisting() 57 | }) 58 | 59 | it('Should display first page initially', () => { 60 | expect(AppPage.pageIndicator).toHaveText('Page 1 / 1') 61 | }) 62 | 63 | it('Zoom Out & Zoom Reset button should be disabled', () => { 64 | expect(AppPage.zoomOutButton.isEnabled()).toBe(false) 65 | expect(AppPage.zoomResetButton.isEnabled()).toBe(false) 66 | }) 67 | 68 | it('Previous & Next page button should be disabled', () => { 69 | expect(AppPage.nextPageButton.isEnabled()).toBe(false) 70 | expect(AppPage.prevPageButton.isEnabled()).toBe(false) 71 | }) 72 | 73 | it('Rotation Reset button should be disabled', () => { 74 | expect(AppPage.rotateResetButton.isEnabled()).toBe(false) 75 | }) 76 | }) 77 | 78 | describe('"Custom starting page" section', () => { 79 | before(() => { 80 | AppPage.section = 'csp' 81 | }) 82 | 83 | it('Should be displayed', () => { 84 | expect(AppPage.section).toBeExisting() 85 | }) 86 | 87 | it('Should display Fifth page initially', () => { 88 | expect(AppPage.pageIndicator).toHaveText('Page 5 / 65') 89 | }) 90 | 91 | it('Should change the page on next & previous button click', () => { 92 | AppPage.nextPageButton.click() 93 | expect(AppPage.pageIndicator).toHaveText('Page 6 / 65') 94 | AppPage.prevPageButton.click() 95 | expect(AppPage.pageIndicator).toHaveText('Page 5 / 65') 96 | }) 97 | 98 | it('Should change the rotation on left & right rotation button click', () => { 99 | const H = AppPage.canvas.getSize('height') 100 | const W = AppPage.canvas.getSize('width') 101 | AppPage.rotateRightButton.click() 102 | expect(AppPage.canvas.getSize('height')).not.toBe(H) 103 | expect(AppPage.canvas.getSize('width')).not.toBe(W) 104 | AppPage.rotateResetButton.click() 105 | expect(AppPage.canvas.getSize('height')).toBe(H) 106 | expect(AppPage.canvas.getSize('width')).toBe(W) 107 | AppPage.rotateLeftButton.click() 108 | expect(AppPage.canvas.getSize('height')).not.toBe(H) 109 | expect(AppPage.canvas.getSize('width')).not.toBe(W) 110 | }) 111 | 112 | it('Should change the scale on Zoom Out & Zoom In button click', () => { 113 | const H = AppPage.canvas.getSize('height') 114 | const W = AppPage.canvas.getSize('width') 115 | AppPage.zoomInButton.click() 116 | expect(AppPage.canvas.getSize('height')).not.toBe(H) 117 | expect(AppPage.canvas.getSize('width')).not.toBe(W) 118 | AppPage.zoomResetButton.click() 119 | expect(AppPage.canvas.getSize('height')).toBe(H) 120 | expect(AppPage.canvas.getSize('width')).toBe(W) 121 | AppPage.zoomInButton.click() 122 | expect(AppPage.canvas.getSize('height')).not.toBe(H) 123 | expect(AppPage.canvas.getSize('width')).not.toBe(W) 124 | AppPage.zoomOutButton.click() 125 | expect(AppPage.canvas.getSize('height')).toBe(H) 126 | expect(AppPage.canvas.getSize('width')).toBe(W) 127 | }) 128 | }) 129 | 130 | describe('"Without Navigation" section', () => { 131 | before(() => { 132 | AppPage.section = 'wn' 133 | }) 134 | 135 | it('Should be displayed', () => { 136 | expect(AppPage.section).toBeExisting() 137 | }) 138 | 139 | it('Should not display the navigation', () => { 140 | expect(AppPage.prevPageButton.isExisting()).toBe(false) 141 | expect(AppPage.pageIndicator.isExisting()).toBe(false) 142 | expect(AppPage.nextPageButton.isExisting()).toBe(false) 143 | }) 144 | }) 145 | 146 | describe('"Without Zoom and Rotation" section', () => { 147 | before(() => { 148 | AppPage.section = 'wzr' 149 | }) 150 | 151 | it('Should be displayed', () => { 152 | expect(AppPage.section).toBeExisting() 153 | }) 154 | 155 | it('Should not display the zoom controls', () => { 156 | expect(AppPage.zoomOutButton.isExisting()).toBe(false) 157 | expect(AppPage.zoomInButton.isExisting()).toBe(false) 158 | }) 159 | 160 | it('Should not display the rotation controls', () => { 161 | expect(AppPage.rotateLeftButton.isExisting()).toBe(false) 162 | expect(AppPage.rotateRightButton.isExisting()).toBe(false) 163 | }) 164 | }) 165 | 166 | describe('"External Controls" section', () => { 167 | before(() => { 168 | AppPage.section = 'ec' 169 | }) 170 | 171 | it('Should be displayed', () => { 172 | expect(AppPage.section).toBeExisting() 173 | }) 174 | 175 | it('External Controls should work', () => { 176 | const H = AppPage.canvas.getSize('height') 177 | const W = AppPage.canvas.getSize('width') 178 | AppPage.extZoomInButton.click() 179 | expect(AppPage.canvas.getSize('height')).not.toBe(H) 180 | expect(AppPage.canvas.getSize('width')).not.toBe(W) 181 | AppPage.extZoomOutButton.click() 182 | expect(AppPage.canvas.getSize('height')).toBe(H) 183 | expect(AppPage.canvas.getSize('width')).toBe(W) 184 | }) 185 | }) 186 | 187 | describe('"Error message for failures" section', () => { 188 | before(() => { 189 | AppPage.section = 'eh' 190 | }) 191 | 192 | it('Should be displayed', () => { 193 | expect(AppPage.section).toBeExisting() 194 | }) 195 | 196 | it('Should display the error message', () => { 197 | AppPage.alert.waitForDisplayed({ 198 | timeout: 60000, 199 | interval: 5000, 200 | }) 201 | expect( 202 | AppPage.alert.getText() === 203 | 'error_outline\nError while opening the document !' || 204 | AppPage.alert.getText() === 205 | 'error_outlineError while opening the document !' 206 | ).toBe(true) 207 | }) 208 | }) 209 | 210 | describe('"Custom Error component for failures" section', () => { 211 | before(() => { 212 | AppPage.section = 'ceh' 213 | }) 214 | 215 | it('Should be displayed', () => { 216 | expect(AppPage.section).toBeExisting() 217 | }) 218 | 219 | it('Should display the custom error message', () => { 220 | AppPage.alert.waitForDisplayed({ 221 | timeout: 60000, 222 | interval: 5000, 223 | }) 224 | expect( 225 | AppPage.alert.getText() === 226 | 'Failed To load !!!\nError while opening the document !' || 227 | AppPage.alert.getText() === 228 | 'Failed To load !!!Error while opening the document !' 229 | ).toBe(true) 230 | }) 231 | }) 232 | }) 233 | -------------------------------------------------------------------------------- /example/wdio.conf.js: -------------------------------------------------------------------------------- 1 | exports.config = { 2 | user: process.env.BROWSERSTACK_USERNAME, 3 | key: process.env.BROWSERSTACK_ACCESS_KEY, 4 | runner: 'local', 5 | specs: ['./test/specs/**/*.js'], 6 | exclude: [], 7 | maxInstances: 10, 8 | capabilities: [ 9 | { 10 | 'bstack:options': { 11 | projectName: 'PDF-Viewer-reactjs example test', 12 | os: 'Windows', 13 | }, 14 | browserVersion: 'latest', 15 | acceptInsecureCerts: true, 16 | browserName: 'firefox', 17 | }, 18 | { 19 | 'bstack:options': { 20 | projectName: 'PDF-Viewer-reactjs example test', 21 | os: 'Windows', 22 | }, 23 | browserVersion: 'latest', 24 | acceptInsecureCerts: true, 25 | browserName: 'edge', 26 | }, 27 | { 28 | 'bstack:options': { 29 | projectName: 'PDF-Viewer-reactjs example test', 30 | os: 'OS X', 31 | }, 32 | browserVersion: 'latest', 33 | acceptInsecureCerts: true, 34 | browserName: 'chrome', 35 | }, 36 | { 37 | 'bstack:options': { 38 | projectName: 'PDF-Viewer-reactjs example test', 39 | os: 'OS X', 40 | }, 41 | browserVersion: 'latest', 42 | acceptInsecureCerts: true, 43 | browserName: 'safari', 44 | }, 45 | ], 46 | logLevel: 'warn', // Level of logging verbosity: trace | debug | info | warn | error | silent 47 | coloredLogs: true, 48 | bail: 0, 49 | baseUrl: 'http://localhost:3000', 50 | waitforTimeout: 10000, 51 | connectionRetryTimeout: 120000, 52 | connectionRetryCount: 3, 53 | services: [['browserstack', { browserstackLocal: true }]], 54 | framework: 'mocha', 55 | reporters: ['spec'], 56 | mochaOpts: { 57 | ui: 'bdd', 58 | timeout: 60000, 59 | }, 60 | } 61 | -------------------------------------------------------------------------------- /example/wdio.conf.local.js: -------------------------------------------------------------------------------- 1 | exports.config = { 2 | runner: 'local', 3 | specs: ['./test/specs/**/*.js'], 4 | exclude: [], 5 | maxInstances: 10, 6 | capabilities: [ 7 | { 8 | maxInstances: 5, 9 | browserName: 'chrome', 10 | acceptInsecureCerts: true, 11 | }, 12 | ], 13 | logLevel: 'warn', // Level of logging verbosity: trace | debug | info | warn | error | silent 14 | coloredLogs: true, 15 | bail: 0, 16 | baseUrl: 'http://localhost:3000', 17 | waitforTimeout: 10000, 18 | connectionRetryTimeout: 120000, 19 | connectionRetryCount: 3, 20 | services: ['chromedriver'], 21 | framework: 'mocha', 22 | reporters: ['spec'], 23 | mochaOpts: { 24 | ui: 'bdd', 25 | timeout: 60000, 26 | }, 27 | } 28 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | coverageDirectory: 'coverage', 3 | collectCoverage: true, 4 | setupFilesAfterEnv: ['./setupTests.js'], 5 | testMatch: ['**/__tests__/**/*spec.js?(x)'], 6 | testPathIgnorePatterns: ['/node_modules/', '/example/'], 7 | verbose: true, 8 | } 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pdf-viewer-reactjs", 3 | "version": "3.0.0-beta.2", 4 | "description": "Simple react PDF viewer component with controls based on PDF.js.", 5 | "source": "src/index.js", 6 | "main": "dist/pdf-viewer-reactjs.js", 7 | "scripts": { 8 | "watch": "jest --watch", 9 | "test": "jest", 10 | "lintJS": "node_modules/.bin/eslint --fix './*js' 'src/**/*.js' 'example/src/**/*.js'", 11 | "lintCSS": "node_modules/.bin/stylelint --fix 'example/src/**/*.css'", 12 | "lint": "npm run lintJS && npm run lintCSS", 13 | "prebuild": "npm run lint && npm run test", 14 | "build": "rollup -c", 15 | "prepare": "husky install" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+ssh://git@github.com/ansu5555/pdf-viewer-reactjs.git" 20 | }, 21 | "keywords": [ 22 | "pdf", 23 | "document", 24 | "pages", 25 | "pdf viewer", 26 | "pdf-viewer", 27 | "rotate", 28 | "rotate pdf", 29 | "zoom", 30 | "zoom pdf", 31 | "react", 32 | "reactjs", 33 | "react-pdf", 34 | "react-pdf-viewer" 35 | ], 36 | "author": "ansu5555", 37 | "license": "MIT", 38 | "bugs": { 39 | "url": "https://github.com/ansu5555/pdf-viewer-reactjs/issues" 40 | }, 41 | "homepage": "https://github.com/ansu5555/pdf-viewer-reactjs#readme", 42 | "peerDependencies": { 43 | "bulma": ">=0.8.1", 44 | "material-design-icons": ">=3.0.1", 45 | "react": ">=16.8.6", 46 | "react-dom": ">=16.8.6" 47 | }, 48 | "devDependencies": { 49 | "@babel/core": "^7.13.8", 50 | "@babel/plugin-transform-runtime": "^7.13.9", 51 | "@babel/preset-env": "^7.13.9", 52 | "@babel/preset-react": "^7.12.13", 53 | "@babel/runtime": "^7.13.9", 54 | "@rollup/plugin-babel": "^5.3.0", 55 | "babel-jest": "^26.6.3", 56 | "enzyme": "^3.11.0", 57 | "enzyme-adapter-react-16": "^1.15.6", 58 | "eslint": "^7.20.0", 59 | "eslint-config-prettier": "^7.2.0", 60 | "eslint-plugin-import": "^2.22.1", 61 | "eslint-plugin-jest": "^24.1.5", 62 | "eslint-plugin-jsx-a11y": "^6.4.1", 63 | "eslint-plugin-prettier": "^3.3.1", 64 | "eslint-plugin-react": "^7.22.0", 65 | "eslint-plugin-react-hooks": "^4.2.0", 66 | "eslint-plugin-wdio": "^7.0.0", 67 | "husky": "^5.1.3", 68 | "jest": "^26.6.3", 69 | "prettier": "^2.2.1", 70 | "react-scripts": "^4.0.3", 71 | "react-test-renderer": "^17.0.1", 72 | "rollup": "^2.39.0", 73 | "rollup-plugin-peer-deps-external": "^2.2.4", 74 | "rollup-plugin-terser": "^7.0.2", 75 | "stylelint": "^13.10.0", 76 | "stylelint-config-standard": "^20.0.0" 77 | }, 78 | "dependencies": { 79 | "pdfjs-dist": "^2.6.347", 80 | "prop-types": "^15.7.2" 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import peerDepsExternal from 'rollup-plugin-peer-deps-external' 2 | import babel from '@rollup/plugin-babel' 3 | import { terser } from 'rollup-plugin-terser' 4 | 5 | export default { 6 | external: [ 7 | 'react', 8 | 'react-dom', 9 | 'prop-types', 10 | 'material-design-icons', 11 | 'bulma', 12 | ], 13 | input: 'src/index.js', 14 | output: { 15 | file: 'dist/pdf-viewer-reactjs.js', 16 | format: 'cjs', 17 | exports: 'auto', 18 | }, 19 | plugins: [ 20 | peerDepsExternal(), 21 | babel({ 22 | babelHelpers: 'runtime', 23 | exclude: 'node_modules/**', 24 | }), 25 | terser(), 26 | ], 27 | } 28 | -------------------------------------------------------------------------------- /setupTests.js: -------------------------------------------------------------------------------- 1 | import { configure } from 'enzyme' 2 | import Adapter from 'enzyme-adapter-react-16' 3 | configure({ adapter: new Adapter() }) 4 | -------------------------------------------------------------------------------- /src/components/Alert.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | const Alert = ({ message }) => ( 5 |
6 |
7 | 8 | error_outline 9 | 10 |
11 |
12 | {message} 13 |
14 |
15 | ) 16 | 17 | Alert.propTypes = { 18 | message: PropTypes.string.isRequired, 19 | } 20 | 21 | export default Alert 22 | -------------------------------------------------------------------------------- /src/components/Loader.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const loader = { 4 | display: 'inline-block', 5 | verticalAlign: 'text-bottom', 6 | backgroundColor: 'currentColor', 7 | borderRadius: '50%', 8 | width: '0.5rem', 9 | height: '0.5rem', 10 | } 11 | 12 | const Loader = () => ( 13 |
14 |

Loading

15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | ) 26 | 27 | export default Loader 28 | -------------------------------------------------------------------------------- /src/components/NavigationBar.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import NextPageButton from './navigationComponents/NextPageButton' 4 | import PageIndicator from './navigationComponents/PageIndicator' 5 | import PreviousPageButton from './navigationComponents/PreviousPageButton' 6 | import ZoomIn from './navigationComponents/ZoomIn' 7 | import ZoomOut from './navigationComponents/ZoomOut' 8 | import ResetZoom from './navigationComponents/ResetZoom' 9 | import RotateLeft from './navigationComponents/RotateLeft' 10 | import ResetRotation from './navigationComponents/ResetRotation' 11 | import RotateRight from './navigationComponents/RotateRight' 12 | 13 | const Navigation = ({ 14 | page, 15 | pages, 16 | scale, 17 | defaultScale, 18 | maxScale, 19 | minScale, 20 | rotationAngle, 21 | hideZoom, 22 | hideRotation, 23 | css, 24 | handlePrevClick, 25 | handleNextClick, 26 | handleZoomIn, 27 | handleResetZoom, 28 | handleZoomOut, 29 | handleRotateLeft, 30 | handleResetRotation, 31 | handleRotateRight, 32 | }) => { 33 | return ( 34 |
39 | {hideZoom ? ( 40 |
41 | ) : ( 42 |
43 | 50 | 56 | 63 |
64 | )} 65 |
66 | 72 | 73 | 79 |
80 | {hideRotation ? ( 81 |
82 | ) : ( 83 |
84 | 89 | 94 | 99 |
100 | )} 101 |
102 | ) 103 | } 104 | 105 | Navigation.propTypes = { 106 | page: PropTypes.number.isRequired, 107 | pages: PropTypes.number.isRequired, 108 | scale: PropTypes.number, 109 | defaultScale: PropTypes.number, 110 | maxScale: PropTypes.number, 111 | minScale: PropTypes.number, 112 | rotationAngle: PropTypes.number, 113 | hideZoom: PropTypes.bool, 114 | hideRotation: PropTypes.bool, 115 | 116 | css: PropTypes.shape({ 117 | navbarWrapper: PropTypes.string, 118 | pages: PropTypes.string, 119 | pageIndicator: PropTypes.string, 120 | previousPageBtn: PropTypes.string, 121 | nextPageBtn: PropTypes.string, 122 | zoomOutBtn: PropTypes.string, 123 | resetZoomBtn: PropTypes.string, 124 | zoomInBtn: PropTypes.string, 125 | rotateLeftBtn: PropTypes.string, 126 | resetRotationBtn: PropTypes.string, 127 | rotateRightBtn: PropTypes.string, 128 | }), 129 | 130 | elements: PropTypes.shape({ 131 | previousPageBtn: PropTypes.any, 132 | nextPageBtn: PropTypes.any, 133 | pages: PropTypes.any, 134 | }), 135 | 136 | handlePrevClick: PropTypes.func.isRequired, 137 | handleNextClick: PropTypes.func.isRequired, 138 | handleZoomIn: PropTypes.func.isRequired, 139 | handleResetZoom: PropTypes.func.isRequired, 140 | handleZoomOut: PropTypes.func.isRequired, 141 | handleRotateLeft: PropTypes.func.isRequired, 142 | handleResetRotation: PropTypes.func.isRequired, 143 | handleRotateRight: PropTypes.func.isRequired, 144 | } 145 | 146 | Navigation.defaultProps = { 147 | css: {}, 148 | elements: {}, 149 | } 150 | 151 | export default Navigation 152 | -------------------------------------------------------------------------------- /src/components/RenderPdf.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from 'react' 2 | import PropTypes from 'prop-types' 3 | import * as pdfjs from 'pdfjs-dist' 4 | import pdfjsWorker from 'pdfjs-dist/build/pdf.worker.entry' 5 | 6 | import Alert from './Alert' 7 | 8 | pdfjs.GlobalWorkerOptions.workerSrc = pdfjsWorker 9 | 10 | function usePrevious(value) { 11 | const ref = useRef() 12 | useEffect(() => { 13 | ref.current = value 14 | }) 15 | return ref.current 16 | } 17 | 18 | const RenderPdf = ({ 19 | document, 20 | withCredentials, 21 | password, 22 | pageNum, 23 | scale, 24 | rotation, 25 | pageCount, 26 | changePage, 27 | showThumbnail, 28 | protectContent, 29 | watermark, 30 | alert, 31 | canvasCss, 32 | }) => { 33 | const [pdf, setPDF] = useState(null) 34 | const [thumbnailImages, setThumbnailImages] = useState(null) 35 | const [prevRenderTask, setPrevRenderTask] = useState(null) 36 | const [error, setError] = useState({ status: false, message: '' }) 37 | const canvasRef = useRef(null) 38 | const thumbnailRef = useRef(null) 39 | const selectedRef = useRef(null) 40 | 41 | const [thumbnails, setThumbnails] = useState([]) 42 | 43 | const prevDocument = usePrevious(document) 44 | const prevPassword = usePrevious(password) 45 | 46 | const AlertComponent = alert ? alert : Alert 47 | 48 | const fetchPDF = async () => { 49 | // Get PDF file 50 | let pdfDoc = null 51 | let thumbImages = null 52 | try { 53 | if ( 54 | JSON.stringify(prevDocument) !== JSON.stringify(document) || 55 | prevPassword.base64 !== password.url 56 | ) { 57 | let objDocInit = { withCredentials, password } 58 | if (document.url == undefined) { 59 | objDocInit.data = atob(document.base64) 60 | } else { 61 | objDocInit.url = document.url 62 | } 63 | pdfDoc = await pdfjs.getDocument(objDocInit).promise 64 | thumbImages = await createImages(pdfDoc) 65 | 66 | displayThumbnails(thumbImages) 67 | 68 | setPDF(pdfDoc) 69 | setThumbnailImages(thumbImages) 70 | } 71 | await displayPage(pdfDoc) 72 | // call pageCountfunction to update page count 73 | pageCount(pdfDoc.numPages) 74 | } catch (error) { 75 | console.warn('Error while opening the document !\n', error) 76 | pageCount(-1) // set page count to -1 on error 77 | setError({ 78 | status: true, 79 | message: 'Error while opening the document !', 80 | }) 81 | } 82 | } 83 | 84 | const displayPage = async (pdfDoc = null) => { 85 | // display pdf page 86 | if (pdfDoc == null) { 87 | pdfDoc = pdf 88 | } 89 | try { 90 | const page = await pdfDoc.getPage(pageNum) 91 | const viewport = page.getViewport({ scale, rotation }) 92 | 93 | // Prepare canvas using PDF page dimensions 94 | const canvas = canvasRef.current 95 | canvas.height = viewport.height 96 | canvas.width = viewport.width 97 | 98 | // Render PDF page into canvas context 99 | let canvasContext = canvas.getContext('2d') 100 | canvasContext.clearRect(0, 0, canvas.width, canvas.height) 101 | canvasContext.beginPath() 102 | const renderContext = { 103 | canvasContext, 104 | viewport, 105 | } 106 | const renderTask = page.render(renderContext) 107 | // cancel previous render task 108 | if (prevRenderTask != null) { 109 | prevRenderTask._internalRenderTask.cancel() 110 | } 111 | try { 112 | await renderTask.promise 113 | if (Object.entries(watermark).length !== 0) { 114 | //watermark config 115 | const { text, diagonal, opacity, font, size, color } = watermark 116 | // setup watermark text for filling 117 | canvasContext.globalAlpha = opacity 118 | canvasContext.font = `${size * scale}px ${ 119 | font !== '' ? font : 'Comic Sans MS' 120 | }` 121 | canvasContext.fillStyle = color 122 | 123 | // get the metrics with font settings 124 | var metrics = canvasContext.measureText(text) 125 | var width = metrics.width 126 | var height = size * scale // height is font size 127 | canvasContext.translate(viewport.width / 2, viewport.height / 2) 128 | 129 | // rotate the context and center the text 130 | if (diagonal) { 131 | canvasContext.rotate(-0.785) 132 | } 133 | canvasContext.fillText(text, -width / 2, height / 2) 134 | } 135 | } catch (error) { 136 | console.warn('Error occured while rendering !\n', error) 137 | pageCount(-1) // set page count to -1 on error 138 | setError({ 139 | status: true, 140 | message: 'Error occured while rendering !', 141 | }) 142 | } 143 | setPrevRenderTask(renderTask) 144 | } catch (error) { 145 | console.warn('Error while reading the pages !\n', error) 146 | pageCount(-1) // set page count to -1 on error 147 | setError({ 148 | status: true, 149 | message: 'Error while reading the pages !', 150 | }) 151 | } 152 | } 153 | 154 | const createImages = async (pdf) => { 155 | // create images for all pages 156 | const imgList = [] 157 | 158 | if (Object.entries(showThumbnail).length !== 0) { 159 | let scale = 0.1 160 | let rotation = 0 161 | if (1 <= showThumbnail.scale && showThumbnail.scale <= 5) { 162 | scale = showThumbnail.scale / 10 163 | } 164 | if ( 165 | showThumbnail.rotationAngle === -90 || 166 | showThumbnail.rotationAngle === 90 167 | ) { 168 | rotation = showThumbnail.rotationAngle 169 | } 170 | 171 | for (let pageNo = 1; pageNo <= pdf.numPages; pageNo++) { 172 | const page = await pdf.getPage(pageNo) 173 | const viewport = page.getViewport({ scale, rotation }) 174 | 175 | // Prepare canvas using PDF page dimensions 176 | const canvas = thumbnailRef.current 177 | canvas.height = viewport.height 178 | canvas.width = viewport.width 179 | 180 | // Render PDF page into canvas context 181 | let canvasContext = canvas.getContext('2d') 182 | canvasContext.clearRect(0, 0, canvas.width, canvas.height) 183 | canvasContext.beginPath() 184 | const renderContext = { 185 | canvasContext, 186 | viewport, 187 | } 188 | const renderTask = page.render(renderContext) 189 | await renderTask.promise 190 | if (Object.entries(watermark).length !== 0) { 191 | //watermark config 192 | const { text, diagonal, opacity, font, size, color } = watermark 193 | // setup watermark text for filling 194 | canvasContext.globalAlpha = opacity 195 | canvasContext.font = `${size * scale}px ${ 196 | font !== '' ? font : 'Comic Sans MS' 197 | }` 198 | canvasContext.fillStyle = color 199 | 200 | // get the metrics with font settings 201 | var metrics = canvasContext.measureText(text) 202 | var width = metrics.width 203 | var height = size * scale // height is font size 204 | canvasContext.translate(viewport.width / 2, viewport.height / 2) 205 | 206 | // rotate the context and center the text 207 | if (diagonal) { 208 | canvasContext.rotate(-0.785) 209 | } 210 | canvasContext.fillText(text, -width / 2, height / 2) 211 | } 212 | 213 | // create image from canvas and push into array 214 | imgList.push({ 215 | image: canvas.toDataURL('image/png'), 216 | height: viewport.height, 217 | width: viewport.width, 218 | }) 219 | } 220 | } 221 | return imgList 222 | } 223 | 224 | const displayThumbnails = (images) => { 225 | if (Object.entries(showThumbnail).length !== 0 && images !== null) { 226 | // display thumbnails for all pages 227 | const thumbnailList = [] 228 | 229 | for (let pageNo = 1; pageNo <= images.length; pageNo++) { 230 | let image = images[pageNo - 1].image 231 | let height = images[pageNo - 1].height 232 | let width = images[pageNo - 1].width 233 | let thumbnailCss = '' 234 | let thumbnailStyle = { 235 | height, 236 | width, 237 | display: 'flex', 238 | cursor: 'pointer', 239 | } 240 | if (showThumbnail.thumbCss && showThumbnail.selectedThumbCss) { 241 | if (pageNum === pageNo) { 242 | thumbnailCss = showThumbnail.selectedThumbCss 243 | } else { 244 | thumbnailCss = showThumbnail.thumbCss 245 | } 246 | } else { 247 | if (pageNum === pageNo) { 248 | thumbnailStyle.margin = '10px 20px' 249 | thumbnailStyle.border = '5px solid rgba(58, 58, 64, 1)' 250 | thumbnailStyle.boxShadow = 251 | 'rgba(0, 0, 0, 0.6) 0 4px 8px 0, rgba(0, 0, 0, 0.58) 0 6px 20px 0' 252 | } else { 253 | thumbnailStyle.margin = '15px 25px' 254 | thumbnailStyle.boxShadow = 'rgba(0, 0, 0, 0.6) 0px 2px 2px 0px' 255 | } 256 | } 257 | 258 | thumbnailList.push( 259 | changePage(pageNo)} 263 | ref={pageNum === pageNo ? selectedRef : null} 264 | key={pageNo} 265 | alt={`thumbnail of page ${pageNo}`} 266 | src={image} 267 | /> 268 | ) 269 | } 270 | // insert space at the end of all pages 271 | thumbnailList.push(
) 272 | setThumbnails(thumbnailList) 273 | } 274 | } 275 | 276 | const scrollThumbnail = () => { 277 | // scroll selected thumbnail into view 278 | if ( 279 | selectedRef.current !== null && 280 | Object.entries(showThumbnail).length !== 0 281 | ) { 282 | selectedRef.current.scrollIntoView({ 283 | behavior: 'smooth', 284 | block: 'nearest', 285 | inline: 'center', 286 | }) 287 | } 288 | } 289 | 290 | useEffect(() => { 291 | fetchPDF() 292 | }, [document, password, pageNum, scale, rotation]) 293 | 294 | useEffect(() => { 295 | displayThumbnails(thumbnailImages) 296 | scrollThumbnail() 297 | }, [pageNum]) 298 | 299 | if (Object.entries(showThumbnail).length !== 0) { 300 | let thumbContainerStyle = { 301 | backgroundColor: showThumbnail.backgroundColor 302 | ? showThumbnail.backgroundColor 303 | : '#EAE6DA', 304 | display: 'flex', 305 | flexDirection: 'row', 306 | overflowX: 'auto', 307 | } 308 | 309 | if (showThumbnail.onTop) { 310 | return ( 311 | <> 312 |
322 |
324 | 325 |
326 |
{thumbnails}
327 | 330 | protectContent ? e.preventDefault() : null 331 | } 332 | ref={canvasRef} 333 | width={typeof window !== 'undefined' && window.innerWidth} 334 | height={typeof window !== 'undefined' && window.innerHeight} 335 | /> 336 |
337 | 338 | 339 | 340 | ) 341 | } else { 342 | return ( 343 | <> 344 |
354 |
356 | 357 |
358 | 361 | protectContent ? e.preventDefault() : null 362 | } 363 | ref={canvasRef} 364 | width={typeof window !== 'undefined' && window.innerWidth} 365 | height={typeof window !== 'undefined' && window.innerHeight} 366 | /> 367 |
368 |
{thumbnails}
369 | 370 | 371 | ) 372 | } 373 | } else { 374 | return ( 375 |
376 |
386 |
388 | 389 |
390 | (protectContent ? e.preventDefault() : null)} 393 | ref={canvasRef} 394 | width={typeof window !== 'undefined' && window.innerWidth} 395 | height={typeof window !== 'undefined' && window.innerHeight} 396 | /> 397 |
398 |
399 | ) 400 | } 401 | } 402 | 403 | RenderPdf.propTypes = { 404 | document: PropTypes.any.isRequired, 405 | withCredentials: PropTypes.bool, 406 | password: PropTypes.string, 407 | pageNum: PropTypes.number.isRequired, 408 | scale: PropTypes.number.isRequired, 409 | rotation: PropTypes.number.isRequired, 410 | changePage: PropTypes.func, 411 | pageCount: PropTypes.func, 412 | showThumbnail: PropTypes.shape({ 413 | scale: PropTypes.number, 414 | rotationAngle: PropTypes.number, 415 | onTop: PropTypes.bool, 416 | backgroundColor: PropTypes.string, 417 | thumbCss: PropTypes.string, 418 | selectedThumbCss: PropTypes.string, 419 | }), 420 | protectContent: PropTypes.bool, 421 | watermark: PropTypes.shape({ 422 | text: PropTypes.string, 423 | diagonal: PropTypes.bool, 424 | opacity: PropTypes.string, 425 | size: PropTypes.string, 426 | color: PropTypes.string, 427 | }), 428 | canvasCss: PropTypes.string, 429 | } 430 | 431 | RenderPdf.defaultProps = { 432 | changePage() {}, 433 | pageCount() {}, 434 | showThumbnail: {}, 435 | protectContent: false, 436 | watermark: {}, 437 | canvasCss: '', 438 | } 439 | 440 | export default RenderPdf 441 | -------------------------------------------------------------------------------- /src/components/__tests__/Alert.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { shallow } from 'enzyme' 3 | import Alert from '../Alert' 4 | 5 | describe('Alert', () => { 6 | const errMsg = 'error msg for alert' 7 | 8 | it('Should display the Error Message', () => { 9 | const alert = shallow() 10 | expect(alert.text()).toEqual(`error_outline${errMsg}`) 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /src/components/__tests__/Loader.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import renderer from 'react-test-renderer' 3 | import { shallow } from 'enzyme' 4 | import Loader from '../Loader' 5 | 6 | describe('Loader', () => { 7 | it('Should render correctly', () => { 8 | const tree = renderer.create().toJSON() 9 | expect(tree).toMatchSnapshot() 10 | }) 11 | 12 | it('Should display the "Loading" text', () => { 13 | const loader = shallow() 14 | expect(loader.text()).toEqual('Loading') 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /src/components/__tests__/NavigationBar.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import renderer from 'react-test-renderer' 3 | import { shallow } from 'enzyme' 4 | import Navigation from '../NavigationBar' 5 | 6 | describe('Navigation Bar', () => { 7 | const mockFn = jest.fn() 8 | 9 | it('Should render correctly', () => { 10 | const tree = renderer 11 | .create( 12 | 29 | ) 30 | .toJSON() 31 | expect(tree).toMatchSnapshot() 32 | }) 33 | 34 | it('Should hide control on passing props', () => { 35 | const loader = shallow( 36 | 55 | ) 56 | expect(loader.text()).toEqual( 57 | '' 58 | ) 59 | }) 60 | }) 61 | -------------------------------------------------------------------------------- /src/components/__tests__/__snapshots__/Loader.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Loader Should render correctly 1`] = ` 4 |
7 |

10 | Loading 11 |

12 |
15 |
27 |
28 |
31 |
43 |
44 |
47 |
59 |
60 |
61 | `; 62 | -------------------------------------------------------------------------------- /src/components/__tests__/__snapshots__/NavigationBar.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Navigation Bar Should render correctly 1`] = ` 4 |
7 |
10 | 24 | 38 | 52 |
53 |
56 | 70 | 73 | Page 1 / 10 74 | 75 | 89 |
90 |
93 | 107 | 121 | 135 |
136 |
137 | `; 138 | -------------------------------------------------------------------------------- /src/components/navigationComponents/NextPageButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | const NextPageButton = ({ css, page, pages, handleNextClick }) => { 5 | const nextClass = css || 'button is-black my-0 mx-3' 6 | 7 | if (page === pages) { 8 | return ( 9 | 14 | ) 15 | } 16 | 17 | return ( 18 | 23 | ) 24 | } 25 | NextPageButton.propTypes = { 26 | css: PropTypes.string, 27 | page: PropTypes.number.isRequired, 28 | pages: PropTypes.number.isRequired, 29 | handleNextClick: PropTypes.func.isRequired, 30 | } 31 | 32 | export default NextPageButton 33 | -------------------------------------------------------------------------------- /src/components/navigationComponents/PageIndicator.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | const PageIndicator = ({ css, page, pages }) => { 5 | const pagesClass = css || 'is-size-7 has-text-centered my-0 mx-3' 6 | 7 | return {`Page ${page} / ${pages}`} 8 | } 9 | 10 | PageIndicator.propTypes = { 11 | css: PropTypes.string, 12 | page: PropTypes.number.isRequired, 13 | pages: PropTypes.number.isRequired, 14 | } 15 | 16 | export default PageIndicator 17 | -------------------------------------------------------------------------------- /src/components/navigationComponents/PreviousPageButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | const PreviousPageButton = ({ css, page, handlePrevClick }) => { 5 | const prevClass = css || 'button is-black my-0 mx-3' 6 | 7 | if (page === 1) { 8 | return ( 9 | 14 | ) 15 | } 16 | 17 | return ( 18 | 23 | ) 24 | } 25 | PreviousPageButton.propTypes = { 26 | css: PropTypes.string, 27 | page: PropTypes.number.isRequired, 28 | handlePrevClick: PropTypes.func.isRequired, 29 | } 30 | 31 | export default PreviousPageButton 32 | -------------------------------------------------------------------------------- /src/components/navigationComponents/ResetRotation.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | const ResetRotation = ({ css, rotationAngle, handleResetRotation }) => { 5 | const resetRotationClass = css || 'button is-black my-0 mx-3' 6 | 7 | if (rotationAngle === 0) { 8 | return ( 9 | 14 | ) 15 | } 16 | 17 | return ( 18 | 23 | ) 24 | } 25 | ResetRotation.propTypes = { 26 | css: PropTypes.string, 27 | rotationAngle: PropTypes.number.isRequired, 28 | handleResetRotation: PropTypes.func.isRequired, 29 | } 30 | 31 | export default ResetRotation 32 | -------------------------------------------------------------------------------- /src/components/navigationComponents/ResetZoom.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | const ResetZoom = ({ css, scale, defaultScale, handleResetZoom }) => { 5 | const resetZoomClass = css || 'button is-black my-0 mx-3' 6 | 7 | if (scale.toFixed(2) === defaultScale.toFixed(2)) { 8 | return ( 9 | 14 | ) 15 | } 16 | 17 | return ( 18 | 23 | ) 24 | } 25 | 26 | ResetZoom.propTypes = { 27 | css: PropTypes.string, 28 | scale: PropTypes.number.isRequired, 29 | defaultScale: PropTypes.number.isRequired, 30 | handleResetZoom: PropTypes.func.isRequired, 31 | } 32 | 33 | export default ResetZoom 34 | -------------------------------------------------------------------------------- /src/components/navigationComponents/RotateLeft.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | const RotateLeft = ({ css, rotationAngle, handleRotateLeft }) => { 5 | const rotateLeftClass = css || 'button is-black my-0 mx-3' 6 | 7 | if (rotationAngle === -90) { 8 | return ( 9 | 14 | ) 15 | } 16 | 17 | return ( 18 | 23 | ) 24 | } 25 | 26 | RotateLeft.propTypes = { 27 | css: PropTypes.string, 28 | rotationAngle: PropTypes.number.isRequired, 29 | handleRotateLeft: PropTypes.func.isRequired, 30 | } 31 | 32 | export default RotateLeft 33 | -------------------------------------------------------------------------------- /src/components/navigationComponents/RotateRight.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | const RotateRight = ({ css, rotationAngle, handleRotateRight }) => { 5 | const rotateRightClass = css || 'button is-black my-0 mx-3' 6 | 7 | if (rotationAngle === 90) { 8 | return ( 9 | 14 | ) 15 | } 16 | 17 | return ( 18 | 23 | ) 24 | } 25 | RotateRight.propTypes = { 26 | css: PropTypes.string, 27 | rotationAngle: PropTypes.number.isRequired, 28 | handleRotateRight: PropTypes.func.isRequired, 29 | } 30 | 31 | export default RotateRight 32 | -------------------------------------------------------------------------------- /src/components/navigationComponents/ZoomIn.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | const ZoomIn = ({ css, scale, defaultScale, maxScale, handleZoomIn }) => { 5 | const zoomInClass = css || 'button is-black my-0 mx-3' 6 | 7 | let checkScale = maxScale 8 | if (defaultScale > maxScale) { 9 | checkScale = defaultScale 10 | } 11 | 12 | if (scale.toFixed(2) === checkScale.toFixed(2)) { 13 | return ( 14 | 19 | ) 20 | } 21 | 22 | return ( 23 | 28 | ) 29 | } 30 | 31 | ZoomIn.propTypes = { 32 | css: PropTypes.string, 33 | scale: PropTypes.number.isRequired, 34 | defaultScale: PropTypes.number.isRequired, 35 | maxScale: PropTypes.number.isRequired, 36 | handleZoomIn: PropTypes.func.isRequired, 37 | } 38 | 39 | export default ZoomIn 40 | -------------------------------------------------------------------------------- /src/components/navigationComponents/ZoomOut.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | const ZoomOut = ({ css, scale, defaultScale, minScale, handleZoomOut }) => { 5 | const zoomOutClass = css || 'button is-black my-0 mx-3' 6 | 7 | let checkScale = minScale 8 | if (defaultScale < minScale) { 9 | checkScale = defaultScale 10 | } 11 | 12 | if (scale.toFixed(2) === checkScale.toFixed(2)) { 13 | return ( 14 | 19 | ) 20 | } 21 | 22 | return ( 23 | 28 | ) 29 | } 30 | 31 | ZoomOut.propTypes = { 32 | css: PropTypes.string, 33 | scale: PropTypes.number.isRequired, 34 | defaultScale: PropTypes.number.isRequired, 35 | minScale: PropTypes.number.isRequired, 36 | handleZoomOut: PropTypes.func.isRequired, 37 | } 38 | 39 | export default ZoomOut 40 | -------------------------------------------------------------------------------- /src/components/navigationComponents/__tests__/NextPageButton.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { shallow } from 'enzyme' 3 | import NextPageButton from '../NextPageButton' 4 | 5 | describe('Next Page Button', () => { 6 | const page = 1 7 | const pages = 10 8 | const css = 'custom-class' 9 | const mockFn = jest.fn() 10 | 11 | it('Should display the "Right Arrow" Icon', () => { 12 | const btn = shallow( 13 | 14 | ) 15 | expect(btn.text()).toEqual('keyboard_arrow_right') 16 | }) 17 | 18 | it('Should have all default classes', () => { 19 | const btn = shallow( 20 | 21 | ) 22 | expect(btn.hasClass('button is-black my-0 mx-3')).toBe(true) 23 | }) 24 | 25 | it('Should have custom class when custom class is provided', () => { 26 | const btn = shallow( 27 | 33 | ) 34 | expect(btn.hasClass(css)).toBe(true) 35 | }) 36 | 37 | it('Should be disabled when page is equal to total page count', () => { 38 | const btn = shallow( 39 | 40 | ) 41 | expect(btn.is('[disabled]')).toBe(true) 42 | }) 43 | 44 | it('Should call the mock function on click', () => { 45 | const btn = shallow( 46 | 47 | ) 48 | btn.simulate('click') 49 | expect(mockFn).toHaveBeenCalledTimes(1) 50 | }) 51 | 52 | // negative tests 53 | it('Should not have default classes when custom class is provided', () => { 54 | const btn = shallow( 55 | 61 | ) 62 | expect(btn.hasClass('button is-black my-0 mx-3')).toBe(false) 63 | }) 64 | 65 | it('Should not be disabled when page is not equal to total page count', () => { 66 | const btn = shallow( 67 | 68 | ) 69 | expect(btn.is('[disabled]')).toBe(false) 70 | }) 71 | }) 72 | -------------------------------------------------------------------------------- /src/components/navigationComponents/__tests__/PageIndicator.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { shallow } from 'enzyme' 3 | import PageIndicator from '../PageIndicator' 4 | 5 | describe('Page Indicator', () => { 6 | const page = 10 7 | const css = 'custom-class' 8 | 9 | it('Should display page details', () => { 10 | const indicator = shallow() 11 | expect(indicator.text()).toEqual(`Page ${page} / ${page}`) 12 | }) 13 | 14 | it('Should have all default classes', () => { 15 | const indicator = shallow() 16 | expect(indicator.hasClass('is-size-7 has-text-centered my-0 mx-3')).toBe( 17 | true 18 | ) 19 | }) 20 | 21 | it('Should have custom class when custom class is provided', () => { 22 | const indicator = shallow( 23 | 24 | ) 25 | expect(indicator.hasClass(css)).toBe(true) 26 | }) 27 | 28 | // negative tests 29 | it('Should not have default classes when custom class is provided', () => { 30 | const indicator = shallow( 31 | 32 | ) 33 | expect(indicator.hasClass('is-size-7 has-text-centered my-0 mx-3')).toBe( 34 | false 35 | ) 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /src/components/navigationComponents/__tests__/PreviousPageButton.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { shallow } from 'enzyme' 3 | import PreviousPageButton from '../PreviousPageButton' 4 | 5 | describe('Previous Page Button', () => { 6 | const page = 10 7 | const css = 'custom-class' 8 | const mockFn = jest.fn() 9 | 10 | it('Should display the "Left Arrow" Icon', () => { 11 | const btn = shallow( 12 | 13 | ) 14 | expect(btn.text()).toEqual('keyboard_arrow_left') 15 | }) 16 | 17 | it('Should have all default classes', () => { 18 | const btn = shallow( 19 | 20 | ) 21 | expect(btn.hasClass('button is-black my-0 mx-3')).toBe(true) 22 | }) 23 | 24 | it('Should have custom class when custom class is provided', () => { 25 | const btn = shallow( 26 | 27 | ) 28 | expect(btn.hasClass(css)).toBe(true) 29 | }) 30 | 31 | it('Should be disabled when page is equal to 1', () => { 32 | const btn = shallow( 33 | 34 | ) 35 | expect(btn.is('[disabled]')).toBe(true) 36 | }) 37 | 38 | it('Should call the mock function on click', () => { 39 | const btn = shallow( 40 | 41 | ) 42 | btn.simulate('click') 43 | expect(mockFn).toHaveBeenCalledTimes(1) 44 | }) 45 | 46 | // negative tests 47 | it('Should not have default classes when custom class is provided', () => { 48 | const btn = shallow( 49 | 50 | ) 51 | expect(btn.hasClass('button is-black my-0 mx-3')).toBe(false) 52 | }) 53 | 54 | it('Should not be disabled when page is not equal to 1', () => { 55 | const btn = shallow( 56 | 57 | ) 58 | expect(btn.is('[disabled]')).toBe(false) 59 | }) 60 | }) 61 | -------------------------------------------------------------------------------- /src/components/navigationComponents/__tests__/ResetRotation.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { shallow } from 'enzyme' 3 | import ResetRotation from '../ResetRotation' 4 | 5 | describe('Reset Rotation Button', () => { 6 | const rotationAngle = 10 7 | const css = 'custom-class' 8 | const mockFn = jest.fn() 9 | 10 | it('Should display the "Reset" Icon', () => { 11 | const btn = shallow( 12 | 16 | ) 17 | expect(btn.text()).toEqual('refresh') 18 | }) 19 | 20 | it('Should have all default classes', () => { 21 | const btn = shallow( 22 | 26 | ) 27 | expect(btn.hasClass('button is-black my-0 mx-3')).toBe(true) 28 | }) 29 | 30 | it('Should have custom class when custom class is provided', () => { 31 | const btn = shallow( 32 | 37 | ) 38 | expect(btn.hasClass(css)).toBe(true) 39 | }) 40 | 41 | it('Should be disabled when rotationAngle is equal to 0', () => { 42 | const btn = shallow( 43 | 44 | ) 45 | expect(btn.is('[disabled]')).toBe(true) 46 | }) 47 | 48 | it('Should call the mock function on click', () => { 49 | const btn = shallow( 50 | 54 | ) 55 | btn.simulate('click') 56 | expect(mockFn).toHaveBeenCalledTimes(1) 57 | }) 58 | 59 | // negative tests 60 | it('Should not have default classes when custom class is provided', () => { 61 | const btn = shallow( 62 | 67 | ) 68 | expect(btn.hasClass('button is-black my-0 mx-3')).toBe(false) 69 | }) 70 | 71 | it('Should not be disabled when rotationAngle is not equal to 0', () => { 72 | const btn = shallow( 73 | 77 | ) 78 | expect(btn.is('[disabled]')).toBe(false) 79 | }) 80 | }) 81 | -------------------------------------------------------------------------------- /src/components/navigationComponents/__tests__/ResetZoom.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { shallow } from 'enzyme' 3 | import ResetZoom from '../ResetZoom' 4 | 5 | describe('Reset Zoom Button', () => { 6 | const scale = 10 7 | const defaultScale = 5 8 | const css = 'custom-class' 9 | const mockFn = jest.fn() 10 | 11 | it('Should display the "Reset" Icon', () => { 12 | const btn = shallow( 13 | 18 | ) 19 | expect(btn.text()).toEqual('refresh') 20 | }) 21 | 22 | it('Should have all default classes', () => { 23 | const btn = shallow( 24 | 29 | ) 30 | expect(btn.hasClass('button is-black my-0 mx-3')).toBe(true) 31 | }) 32 | 33 | it('Should have custom class when custom class is provided', () => { 34 | const btn = shallow( 35 | 41 | ) 42 | expect(btn.hasClass(css)).toBe(true) 43 | }) 44 | 45 | it('Should be disabled when scale is equal to DefaultScale', () => { 46 | const btn = shallow( 47 | 52 | ) 53 | expect(btn.is('[disabled]')).toBe(true) 54 | }) 55 | 56 | it('Should call the mock function on click', () => { 57 | const btn = shallow( 58 | 63 | ) 64 | btn.simulate('click') 65 | expect(mockFn).toHaveBeenCalledTimes(1) 66 | }) 67 | 68 | // negative tests 69 | it('Should not have default classes when custom class is provided', () => { 70 | const btn = shallow( 71 | 77 | ) 78 | expect(btn.hasClass('button is-black my-0 mx-3')).toBe(false) 79 | }) 80 | 81 | it('Should not be disabled when scale is not equal to 0', () => { 82 | const btn = shallow( 83 | 88 | ) 89 | expect(btn.is('[disabled]')).toBe(false) 90 | }) 91 | }) 92 | -------------------------------------------------------------------------------- /src/components/navigationComponents/__tests__/RotateLeft.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { shallow } from 'enzyme' 3 | import RotateLeft from '../RotateLeft' 4 | 5 | describe('Rotate Left Button', () => { 6 | const rotationAngle = 10 7 | const css = 'custom-class' 8 | const mockFn = jest.fn() 9 | 10 | it('Should display the "Rotate Left" Icon', () => { 11 | const btn = shallow( 12 | 13 | ) 14 | expect(btn.text()).toEqual('rotate_left') 15 | }) 16 | 17 | it('Should have all default classes', () => { 18 | const btn = shallow( 19 | 20 | ) 21 | expect(btn.hasClass('button is-black my-0 mx-3')).toBe(true) 22 | }) 23 | 24 | it('Should have custom class when custom class is provided', () => { 25 | const btn = shallow( 26 | 31 | ) 32 | expect(btn.hasClass(css)).toBe(true) 33 | }) 34 | 35 | it('Should be disabled when rotationAngle is equal to -90', () => { 36 | const btn = shallow( 37 | 38 | ) 39 | expect(btn.is('[disabled]')).toBe(true) 40 | }) 41 | 42 | it('Should call the mock function on click', () => { 43 | const btn = shallow( 44 | 45 | ) 46 | btn.simulate('click') 47 | expect(mockFn).toHaveBeenCalledTimes(1) 48 | }) 49 | 50 | // negative tests 51 | it('Should not have default classes when custom class is provided', () => { 52 | const btn = shallow( 53 | 58 | ) 59 | expect(btn.hasClass('button is-black my-0 mx-3')).toBe(false) 60 | }) 61 | 62 | it('Should not be disabled when rotationAngle is not equal to -90', () => { 63 | const btn = shallow( 64 | 65 | ) 66 | expect(btn.is('[disabled]')).toBe(false) 67 | }) 68 | }) 69 | -------------------------------------------------------------------------------- /src/components/navigationComponents/__tests__/RotateRight.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { shallow } from 'enzyme' 3 | import RotateRight from '../RotateRight' 4 | 5 | describe('Rotate Left Button', () => { 6 | const rotationAngle = 10 7 | const css = 'custom-class' 8 | const mockFn = jest.fn() 9 | 10 | it('Should display the "Rotate Right" Icon', () => { 11 | const btn = shallow( 12 | 13 | ) 14 | expect(btn.text()).toEqual('rotate_right') 15 | }) 16 | 17 | it('Should have all default classes', () => { 18 | const btn = shallow( 19 | 20 | ) 21 | expect(btn.hasClass('button is-black my-0 mx-3')).toBe(true) 22 | }) 23 | 24 | it('Should have custom class when custom class is provided', () => { 25 | const btn = shallow( 26 | 31 | ) 32 | expect(btn.hasClass(css)).toBe(true) 33 | }) 34 | 35 | it('Should be disabled when rotationAngle is equal to 90', () => { 36 | const btn = shallow( 37 | 38 | ) 39 | expect(btn.is('[disabled]')).toBe(true) 40 | }) 41 | 42 | it('Should call the mock function on click', () => { 43 | const btn = shallow( 44 | 45 | ) 46 | btn.simulate('click') 47 | expect(mockFn).toHaveBeenCalledTimes(1) 48 | }) 49 | 50 | // negative tests 51 | it('Should not have default classes when custom class is provided', () => { 52 | const btn = shallow( 53 | 58 | ) 59 | expect(btn.hasClass('button is-black my-0 mx-3')).toBe(false) 60 | }) 61 | 62 | it('Should not be disabled when rotationAngle is not equal to 90', () => { 63 | const btn = shallow( 64 | 65 | ) 66 | expect(btn.is('[disabled]')).toBe(false) 67 | }) 68 | }) 69 | -------------------------------------------------------------------------------- /src/components/navigationComponents/__tests__/ZoomIn.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { shallow } from 'enzyme' 3 | import ZoomIn from '../ZoomIn' 4 | 5 | describe('Zoom In Button', () => { 6 | const scale = 5 7 | const maxScale = 10 8 | const css = 'custom-class' 9 | const mockFn = jest.fn() 10 | 11 | it('Should display the "Zoom In" Icon', () => { 12 | const btn = shallow( 13 | 19 | ) 20 | expect(btn.text()).toEqual('zoom_in') 21 | }) 22 | 23 | it('Should have all default classes', () => { 24 | const btn = shallow( 25 | 31 | ) 32 | expect(btn.hasClass('button is-black my-0 mx-3')).toBe(true) 33 | }) 34 | 35 | it('Should have custom class when custom class is provided', () => { 36 | const btn = shallow( 37 | 44 | ) 45 | expect(btn.hasClass(css)).toBe(true) 46 | }) 47 | 48 | it('Should be disabled when scale is equal to MaxScale', () => { 49 | const btn = shallow( 50 | 56 | ) 57 | expect(btn.is('[disabled]')).toBe(true) 58 | }) 59 | 60 | it('Should be disabled when scale is equal to DefaultScale and DefaultScale is greater than MaxScale', () => { 61 | const btn = shallow( 62 | 68 | ) 69 | expect(btn.is('[disabled]')).toBe(true) 70 | }) 71 | 72 | it('Should call the mock function on click', () => { 73 | const btn = shallow( 74 | 80 | ) 81 | btn.simulate('click') 82 | expect(mockFn).toHaveBeenCalledTimes(1) 83 | }) 84 | 85 | // negative tests 86 | it('Should not have default classes when custom class is provided', () => { 87 | const btn = shallow( 88 | 95 | ) 96 | expect(btn.hasClass('button is-black my-0 mx-3')).toBe(false) 97 | }) 98 | 99 | it('Should not be disabled when scale is not equal to MaxScale', () => { 100 | const btn = shallow( 101 | 107 | ) 108 | expect(btn.is('[disabled]')).toBe(false) 109 | }) 110 | }) 111 | -------------------------------------------------------------------------------- /src/components/navigationComponents/__tests__/ZoomOut.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { shallow } from 'enzyme' 3 | import ZoomOut from '../ZoomOut' 4 | 5 | describe('Zoom Out Button', () => { 6 | const scale = 5 7 | const minScale = 1 8 | const css = 'custom-class' 9 | const mockFn = jest.fn() 10 | 11 | it('Should display the "Zoom Out" Icon', () => { 12 | const btn = shallow( 13 | 19 | ) 20 | expect(btn.text()).toEqual('zoom_out') 21 | }) 22 | 23 | it('Should have all default classes', () => { 24 | const btn = shallow( 25 | 31 | ) 32 | expect(btn.hasClass('button is-black my-0 mx-3')).toBe(true) 33 | }) 34 | 35 | it('Should have custom class when custom class is provided', () => { 36 | const btn = shallow( 37 | 44 | ) 45 | expect(btn.hasClass(css)).toBe(true) 46 | }) 47 | 48 | it('Should be disabled when scale is equal to MinScale', () => { 49 | const btn = shallow( 50 | 56 | ) 57 | expect(btn.is('[disabled]')).toBe(true) 58 | }) 59 | 60 | it('Should be disabled when scale is equal to DefaultScale and DefaultScale is less than MinScale', () => { 61 | const btn = shallow( 62 | 68 | ) 69 | expect(btn.is('[disabled]')).toBe(true) 70 | }) 71 | 72 | it('Should call the mock function on click', () => { 73 | const btn = shallow( 74 | 80 | ) 81 | btn.simulate('click') 82 | expect(mockFn).toHaveBeenCalledTimes(1) 83 | }) 84 | 85 | // negative tests 86 | it('Should not have default classes when custom class is provided', () => { 87 | const btn = shallow( 88 | 95 | ) 96 | expect(btn.hasClass('button is-black my-0 mx-3')).toBe(false) 97 | }) 98 | 99 | it('Should not be disabled when scale is not equal to minScale', () => { 100 | const btn = shallow( 101 | 107 | ) 108 | expect(btn.is('[disabled]')).toBe(false) 109 | }) 110 | }) 111 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import PDF from './components/RenderPdf' 4 | import Navigation from './components/NavigationBar' 5 | import Loader from './components/Loader' 6 | 7 | class PDFViewer extends React.Component { 8 | constructor(props) { 9 | super(props) 10 | this.state = { 11 | page: this.props.page, 12 | prevPropPage: this.props.page, 13 | scale: this.props.scale, 14 | prevPropScale: this.props.scale, 15 | rotationAngle: this.props.rotationAngle, 16 | prevPropRotationAngle: this.props.rotationAngle, 17 | pages: 0, 18 | defaultScale: this.props.scale, 19 | isReady: false, 20 | } 21 | this.getPageCount = this.getPageCount.bind(this) 22 | this.handleThumbnailClick = this.handleThumbnailClick.bind(this) 23 | this.handlePrevClick = this.handlePrevClick.bind(this) 24 | this.handleNextClick = this.handleNextClick.bind(this) 25 | this.handleZoomIn = this.handleZoomIn.bind(this) 26 | this.handleResetZoom = this.handleResetZoom.bind(this) 27 | this.handleZoomOut = this.handleZoomOut.bind(this) 28 | this.handleRotateLeft = this.handleRotateLeft.bind(this) 29 | this.handleResetRotation = this.handleResetRotation.bind(this) 30 | this.handleRotateRight = this.handleRotateRight.bind(this) 31 | } 32 | 33 | getPageCount(pages) { 34 | if (this.state.pages !== pages) { 35 | this.setState({ pages, isReady: true }) 36 | if (this.props.getMaxPageCount) { 37 | this.props.getMaxPageCount(pages) 38 | } 39 | } 40 | } 41 | 42 | handleThumbnailClick(page) { 43 | if (this.state.page !== page) { 44 | this.setState({ page }) 45 | } 46 | } 47 | 48 | handlePrevClick() { 49 | if (this.state.page === 1) return 50 | 51 | this.setState((state) => ({ 52 | page: state.page - 1, 53 | })) 54 | 55 | if (this.props.onPrevBtnClick) { 56 | this.props.onPrevBtnClick(this.state.page - 1) 57 | } 58 | } 59 | 60 | handleNextClick() { 61 | if (this.state.page === this.pages) return 62 | 63 | this.setState((state) => ({ 64 | page: state.page + 1, 65 | })) 66 | 67 | if (this.props.onNextBtnClick) { 68 | this.props.onNextBtnClick(this.state.page + 1) 69 | } 70 | } 71 | 72 | handleZoomIn() { 73 | let checkScale = this.props.maxScale 74 | if (this.state.defaultScale > this.props.maxScale) { 75 | checkScale = this.state.defaultScale 76 | } 77 | 78 | if (this.state.scale < checkScale) { 79 | this.setState((state, props) => ({ 80 | scale: state.scale + props.scaleStep, 81 | })) 82 | } 83 | 84 | if (this.props.onZoom) { 85 | this.props.onZoom(this.state.scale + this.props.scaleStep) 86 | } 87 | } 88 | 89 | handleResetZoom() { 90 | this.setState((state) => ({ 91 | scale: state.defaultScale, 92 | })) 93 | 94 | if (this.props.onZoom) { 95 | this.props.onZoom(this.state.defaultScale) 96 | } 97 | } 98 | 99 | handleZoomOut() { 100 | let checkScale = this.props.minScale 101 | if (this.state.defaultScale < this.props.minScale) { 102 | checkScale = this.state.defaultScale 103 | } 104 | 105 | if (this.state.scale > checkScale) { 106 | this.setState((state, props) => ({ 107 | scale: state.scale - props.scaleStep, 108 | })) 109 | } 110 | 111 | if (this.props.onZoom) { 112 | this.props.onZoom(this.state.scale - this.props.scaleStep) 113 | } 114 | } 115 | 116 | handleRotateLeft() { 117 | if (this.state.rotationAngle !== -90) { 118 | this.setState({ 119 | rotationAngle: -90, 120 | }) 121 | } 122 | 123 | if (this.props.onRotation) { 124 | this.props.onRotation(-90) 125 | } 126 | } 127 | 128 | handleResetRotation() { 129 | if (this.state.rotationAngle !== 0) { 130 | this.setState({ 131 | rotationAngle: 0, 132 | }) 133 | } 134 | 135 | if (this.props.onRotation) { 136 | this.props.onRotation(0) 137 | } 138 | } 139 | 140 | handleRotateRight() { 141 | if (this.state.rotationAngle !== 90) { 142 | this.setState({ 143 | rotationAngle: 90, 144 | }) 145 | } 146 | 147 | if (this.props.onRotation) { 148 | this.props.onRotation(90) 149 | } 150 | } 151 | 152 | static getDerivedStateFromProps(props, state) { 153 | if (!props.externalInput) { 154 | return null 155 | } 156 | 157 | if (props.page !== state.prevPropPage) { 158 | if (1 <= props.page && props.page <= state.pages) { 159 | return { page: props.page, prevPropPage: props.page } 160 | } 161 | } 162 | if (props.scale !== state.prevPropScale) { 163 | if (props.minScale <= props.scale && props.scale <= props.maxScale) { 164 | return { scale: props.scale, prevPropScale: props.scale } 165 | } 166 | } 167 | if (props.rotationAngle !== state.prevPropRotationAngle) { 168 | if ( 169 | props.rotationAngle === 90 || 170 | props.rotationAngle === 0 || 171 | props.rotationAngle === -90 172 | ) { 173 | return { 174 | rotationAngle: props.rotationAngle, 175 | prevPropRotationAngle: props.rotationAngle, 176 | } 177 | } 178 | } 179 | return null 180 | } 181 | 182 | render() { 183 | const { 184 | document, 185 | withCredentials, 186 | password, 187 | loader, 188 | maxScale, 189 | minScale, 190 | externalInput, 191 | hideNavbar, 192 | hideZoom, 193 | hideRotation, 194 | navbarOnTop, 195 | navigation, 196 | css, 197 | canvasCss, 198 | onDocumentClick, 199 | showThumbnail, 200 | protectContent, 201 | watermark, 202 | alert, 203 | } = this.props 204 | 205 | const { page, pages, scale, defaultScale, rotationAngle } = this.state 206 | 207 | const NavigationElement = navigation 208 | 209 | const pdf = ( 210 | this.handleThumbnailClick(idx)} 218 | pageCount={(num) => this.getPageCount(num)} 219 | showThumbnail={showThumbnail} 220 | protectContent={protectContent} 221 | watermark={watermark} 222 | alert={alert} 223 | canvasCss={canvasCss} 224 | /> 225 | ) 226 | 227 | let nav = null 228 | let hideNavbarDisplay = hideNavbar 229 | if (externalInput) { 230 | hideNavbarDisplay = true 231 | } 232 | if (!hideNavbarDisplay && pages > 0) { 233 | nav = 234 | !navigation || typeof navigation === 'object' ? ( 235 | 255 | ) : ( 256 | 275 | ) 276 | } 277 | 278 | return ( 279 |
280 |
281 |
291 | {loader ? loader : } 292 |
293 |
294 |
295 | {navbarOnTop ? ( 296 |
297 |
{nav}
298 |
{pdf}
299 |
300 | ) : ( 301 |
302 |
{pdf}
303 |
{nav}
304 |
305 | )} 306 |
307 |
308 | ) 309 | } 310 | } 311 | 312 | PDFViewer.propTypes = { 313 | document: PropTypes.shape({ 314 | url: PropTypes.string, // File path 315 | base64: PropTypes.string, // PDF file encoded in base64 316 | }).isRequired, 317 | withCredentials: PropTypes.bool, 318 | password: PropTypes.string, 319 | loader: PropTypes.node, 320 | externalInput: PropTypes.bool, 321 | page: PropTypes.number, 322 | scale: PropTypes.number, 323 | scaleStep: PropTypes.number, 324 | maxScale: PropTypes.number, 325 | minScale: PropTypes.number, 326 | css: PropTypes.string, 327 | canvasCss: PropTypes.string, 328 | rotationAngle: PropTypes.number, 329 | onDocumentClick: PropTypes.func, 330 | onPrevBtnClick: PropTypes.func, 331 | onNextBtnClick: PropTypes.func, 332 | onZoom: PropTypes.func, 333 | onRotation: PropTypes.func, 334 | getMaxPageCount: PropTypes.func, 335 | hideNavbar: PropTypes.bool, 336 | navbarOnTop: PropTypes.bool, 337 | hideZoom: PropTypes.bool, 338 | hideRotation: PropTypes.bool, 339 | showThumbnail: PropTypes.shape({ 340 | scale: PropTypes.number, 341 | rotationAngle: PropTypes.number, 342 | onTop: PropTypes.bool, 343 | backgroundColor: PropTypes.string, 344 | thumbCss: PropTypes.string, 345 | selectedThumbCss: PropTypes.string, 346 | }), 347 | protectContent: PropTypes.bool, 348 | watermark: PropTypes.shape({ 349 | text: PropTypes.string, 350 | diagonal: PropTypes.bool, 351 | opacity: PropTypes.string, 352 | font: PropTypes.string, 353 | size: PropTypes.string, 354 | color: PropTypes.string, 355 | }), 356 | alert: PropTypes.any, 357 | navigation: PropTypes.oneOfType([ 358 | // Can be an object with css classes or react elements to be rendered 359 | PropTypes.shape({ 360 | css: PropTypes.shape({ 361 | navbarWrapper: PropTypes.string, 362 | zoomOutBtn: PropTypes.string, 363 | resetZoomBtn: PropTypes.string, 364 | zoomInBtn: PropTypes.string, 365 | previousPageBtn: PropTypes.string, 366 | pageIndicator: PropTypes.string, 367 | nextPageBtn: PropTypes.string, 368 | rotateLeftBtn: PropTypes.string, 369 | resetRotationBtn: PropTypes.string, 370 | rotateRightBtn: PropTypes.string, 371 | }), 372 | }), 373 | // Or a full navigation component 374 | PropTypes.any, 375 | ]), 376 | } 377 | 378 | PDFViewer.defaultProps = { 379 | page: 1, 380 | withCredentials: false, 381 | password: '', 382 | scale: 1, 383 | scaleStep: 1, 384 | maxScale: 3, 385 | minScale: 1, 386 | rotationAngle: 0, 387 | externalInput: false, 388 | hideNavbar: false, 389 | hideZoom: false, 390 | hideRotation: false, 391 | navbarOnTop: false, 392 | canvasCss: '', 393 | } 394 | 395 | export default PDFViewer 396 | --------------------------------------------------------------------------------