├── .gitignore
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── docs
└── screenshots
│ └── webviewer-custom-ui-01.png
├── package.json
├── public
├── assets
│ └── favicon.ico
├── files
│ └── demo.pdf
├── index.html
├── manifest.json
└── serve.json
├── src
├── App.css
├── App.js
├── App.test.js
├── assets
│ └── icons
│ │ ├── ic_add_image_24px.svg
│ │ ├── ic_annotation_add_redact_black_24px.svg
│ │ ├── ic_annotation_apply_redact_black_24px.svg
│ │ ├── ic_annotation_square_black_24px.svg
│ │ ├── ic_chevron_left_black_24px.svg
│ │ ├── ic_chevron_right_black_24px.svg
│ │ ├── ic_close_black_24px.svg
│ │ ├── ic_edit_page_24px.svg
│ │ ├── ic_paragraph_24px.svg
│ │ ├── ic_search_black_24px.svg
│ │ ├── ic_select_black_24px.svg
│ │ ├── ic_zoom_in_black_24px.svg
│ │ └── ic_zoom_out_black_24px.svg
├── components
│ └── SearchContainer
│ │ ├── SearchContainer.css
│ │ ├── SearchContainer.js
│ │ └── index.js
├── index.css
└── index.js
└── tools
└── copy-webviewer-files.js
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # WebViewer
4 | public/webviewer
5 |
6 | # dependencies
7 | /node_modules
8 | /.pnp
9 | .pnp.js
10 |
11 | # testing
12 | /coverage
13 |
14 | # production
15 | /build
16 |
17 | # misc
18 | .DS_Store
19 | .env.local
20 | .env.development.local
21 | .env.test.local
22 | .env.production.local
23 | package-lock.json
24 | /public/assets/webviewer/core
25 | /public/assets/webviewer/ui
26 |
27 | npm-debug.log*
28 | yarn-debug.log*
29 | yarn-error.log*
30 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to WebViewer - React sample
2 |
3 | ## Issues
4 | 1. Check existing issues (open/closed) to avoid duplicates.
5 | 2. Be clear about what the problem is.
6 | 3. Make sure to paste error output or logs.
7 | 4. Code snapshot or demos on online code editor will be very helpful.
8 |
9 | ## Pull requests
10 | 1. Fork the repository.
11 | 2. Create a branch from `master`.
12 | 3. Update the source code using style guides described below.
13 | 4. Lint your code with `npm run lint`.
14 | 5. Commit and push the changes with descriptive messages.
15 | 6. Create a pull request to `master`.
16 |
17 | \* Please note that all pull requests should be tied to an issue, and all but the most trivial pull requests should be discussed before hand.
18 |
19 | ## Style guides
20 | - Tab indentation (size of 2 spaces).
21 | - `'` instead of `"`.
22 | - Curly braces for block statements.
23 | - 1TBS brace style.
24 | - Semicolon at the end of each statement.
25 | - Object shorthand for ES6.
26 | - Parenthesis around arrow function argument.
27 | - Minimum line breaks.
28 | - No `use strict`.
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2024 Apryse Systems Inc. All rights reserved.
2 | WebViewer UI project/codebase or any derived works is only permitted in solutions with an active commercial Apryse WebViewer license. For exact licensing terms refer to the commercial WebViewer license. For licensing, pricing, or product questions, Contact [Sales](https://apryse.com/form/contact-sales).
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # WebViewer - Custom UI
2 |
3 | ⚠️ This sample has been moved to the [webviewer-samples repo](https://github.com/ApryseSDK/webviewer-samples/tree/main/webviewer-custom-ui). ⚠️
--------------------------------------------------------------------------------
/docs/screenshots/webviewer-custom-ui-01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ApryseSDK/webviewer-custom-ui/fdd383e7d9927f4f6da0977dc973eb8cc1f8c876/docs/screenshots/webviewer-custom-ui-01.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "webviewer-react-sample",
3 | "version": "1.0.0",
4 | "private": true,
5 | "dependencies": {
6 | "@pdftron/webviewer": "^11.0.0",
7 | "react": "^18.3.1",
8 | "react-dom": "^18.3.1",
9 | "react-scripts": "^5.0.1"
10 | },
11 | "devDependencies": {
12 | "btoa": "^1.2.1",
13 | "download": "^8.0.0",
14 | "fs-extra": "^11.2.0"
15 | },
16 | "scripts": {
17 | "start": "react-scripts start",
18 | "build": "react-scripts build",
19 | "test": "react-scripts test",
20 | "eject": "react-scripts eject",
21 | "postinstall": "node tools/copy-webviewer-files.js"
22 | },
23 | "eslintConfig": {
24 | "extends": "react-app"
25 | },
26 | "browserslist": [
27 | ">0.2%",
28 | "not dead",
29 | "not ie <= 11",
30 | "not op_mini all"
31 | ]
32 | }
--------------------------------------------------------------------------------
/public/assets/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ApryseSDK/webviewer-custom-ui/fdd383e7d9927f4f6da0977dc973eb8cc1f8c876/public/assets/favicon.ico
--------------------------------------------------------------------------------
/public/files/demo.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ApryseSDK/webviewer-custom-ui/fdd383e7d9927f4f6da0977dc973eb8cc1f8c876/public/files/demo.pdf
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
22 | React Sample
23 |
24 |
25 |
28 |
29 |
30 |
31 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/public/serve.json:
--------------------------------------------------------------------------------
1 | {
2 | "cleanUrls": false
3 | }
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --pdf-background: var(--grey-2);
3 | --tools-header-background: var(--grey-3);
4 | }
5 |
6 | .App {
7 | width: 100%;
8 | height: 100%;
9 | display: flex;
10 | flex-direction: row;
11 | overflow: hidden;
12 | }
13 |
14 | .App .header {
15 | width: 100%;
16 | height: 100px;
17 | padding: 8px 8px 8px 16px;
18 | box-sizing: border-box;
19 | background: #00a5e4;
20 | font-size: 1.2em;
21 | line-height: 44px;
22 | color: white;
23 | }
24 |
25 | #scroll-view {
26 | bottom: 0;
27 | /* leave room for 100px header */
28 | height: 100%;
29 | width: 100%;
30 | overflow: auto;
31 | flex: 3;
32 | background-color: var(--pdf-background);
33 | }
34 |
35 | #viewer {
36 | margin: auto;
37 | }
38 |
39 | #main-column {
40 | flex-direction: row;
41 | flex: 3;
42 | }
43 |
44 | #tools {
45 | display:flex;
46 | flex-direction: row;
47 | justify-content: center;
48 | background-color: var(--tools-header-background);
49 | }
50 |
51 | #tools button {
52 | background-color: var(--tools-header-background);
53 | fill: #757575;
54 | width: fit-content;
55 | height: 35px;
56 | margin: 5px;
57 | border: none;
58 | outline: none;
59 | box-shadow: none;
60 | cursor: pointer;
61 | }
62 |
63 | #tools button:hover {
64 | background-color: #c7d2dd;
65 | border-radius: 4px;
66 | }
67 |
68 | .pageContainer {
69 | border: 1px solid #ececec;
70 | box-shadow: 5px 10px 18px #ececec;
71 | position: relative;
72 | }
73 |
74 | .flexbox-container {
75 | display: flex;
76 | }
77 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { useRef, useEffect, useState } from 'react';
2 | import SearchContainer from './components/SearchContainer';
3 | import { ReactComponent as ZoomIn } from './assets/icons/ic_zoom_in_black_24px.svg';
4 | import { ReactComponent as ZoomOut } from './assets/icons/ic_zoom_out_black_24px.svg';
5 | import { ReactComponent as AnnotationRectangle } from './assets/icons/ic_annotation_square_black_24px.svg';
6 | import { ReactComponent as AnnotationRedact } from './assets/icons/ic_annotation_add_redact_black_24px.svg';
7 | import { ReactComponent as AnnotationApplyRedact} from './assets/icons/ic_annotation_apply_redact_black_24px.svg';
8 | import { ReactComponent as Search } from './assets/icons/ic_search_black_24px.svg';
9 | import { ReactComponent as Select } from './assets/icons/ic_select_black_24px.svg';
10 | import { ReactComponent as EditContent } from './assets/icons/ic_edit_page_24px.svg';
11 | import { ReactComponent as AddParagraph } from './assets/icons/ic_paragraph_24px.svg';
12 | import { ReactComponent as AddImageContent } from './assets/icons/ic_add_image_24px.svg';
13 | import './App.css';
14 |
15 | const App = () => {
16 | const viewer = useRef(null);
17 | const scrollView = useRef(null);
18 | const searchTerm = useRef(null);
19 | const searchContainerRef = useRef(null);
20 |
21 | const [documentViewer, setDocumentViewer] = useState(null);
22 | const [annotationManager, setAnnotationManager] = useState(null);
23 | const [searchContainerOpen, setSearchContainerOpen] = useState(false);
24 | const [isInContentEditMode, setIsInContentEditMode] = useState(false);
25 |
26 | const Annotations = window.Core.Annotations;
27 |
28 | // if using a class, equivalent of componentDidMount
29 | useEffect(() => {
30 | const Core = window.Core;
31 | Core.setWorkerPath('/webviewer');
32 | Core.enableFullPDF();
33 |
34 | const documentViewer = new Core.DocumentViewer();
35 | documentViewer.setScrollViewElement(scrollView.current);
36 | documentViewer.setViewerElement(viewer.current);
37 | documentViewer.enableAnnotations();
38 | documentViewer.loadDocument('/files/demo.pdf');
39 |
40 | setDocumentViewer(documentViewer);
41 |
42 | documentViewer.addEventListener('documentLoaded', () => {
43 | console.log('document loaded');
44 | documentViewer.setToolMode(documentViewer.getTool(Core.Tools.ToolNames.EDIT));
45 | setAnnotationManager(documentViewer.getAnnotationManager());
46 | });
47 | }, []);
48 |
49 | const zoomOut = () => {
50 | documentViewer.zoomTo(documentViewer.getZoomLevel() - 0.25);
51 | };
52 |
53 | const zoomIn = () => {
54 | documentViewer.zoomTo(documentViewer.getZoomLevel() + 0.25);
55 | };
56 |
57 | const startEditingContent = () => {
58 | const contentEditManager = documentViewer.getContentEditManager();
59 | contentEditManager.startContentEditMode();
60 | setIsInContentEditMode(true);
61 | }
62 |
63 | const endEditingContent = () => {
64 | setIsInContentEditMode(false);
65 | documentViewer.setToolMode(documentViewer.getTool(window.Core.Tools.ToolNames.EDIT));
66 | const contentEditManager = documentViewer.getContentEditManager();
67 | contentEditManager.endContentEditMode();
68 | }
69 |
70 | const addParagraph = () => {
71 | if (isInContentEditMode) {
72 | const addParagraphTool = documentViewer.getTool(window.Core.Tools.ToolNames.ADD_PARAGRAPH);
73 | documentViewer.setToolMode(addParagraphTool);
74 | } else {
75 | alert('Content Edit mode is not enabled.')
76 | }
77 | };
78 |
79 | const addImageContent = () => {
80 | if (isInContentEditMode) {
81 | const addImageContentTool = documentViewer.getTool(window.Core.Tools.ToolNames.ADD_IMAGE_CONTENT);
82 | documentViewer.setToolMode(addImageContentTool);
83 | } else {
84 | alert('Content Edit mode is not enabled.')
85 | }
86 | };
87 |
88 | const createRectangle = () => {
89 | documentViewer.setToolMode(documentViewer.getTool(window.Core.Tools.ToolNames.RECTANGLE));
90 | };
91 |
92 | const selectTool = () => {
93 | documentViewer.setToolMode(documentViewer.getTool(window.Core.Tools.ToolNames.EDIT));
94 | };
95 |
96 | const createRedaction = () => {
97 | documentViewer.setToolMode(documentViewer.getTool(window.Core.Tools.ToolNames.REDACTION));
98 | };
99 |
100 | const applyRedactions = async () => {
101 | const annotationManager = documentViewer.getAnnotationManager();
102 | annotationManager.enableRedaction();
103 | await annotationManager.applyRedactions();
104 | };
105 |
106 | return (
107 |
108 |
153 |
154 |
162 |
163 |
164 | );
165 | };
166 |
167 | export default App;
168 |
--------------------------------------------------------------------------------
/src/App.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { createRoot } from 'react-dom/client';
3 | import App from './App';
4 |
5 | it('renders without crashing', () => {
6 | const root = createRoot(document.createElement('div'));
7 | root.render();
8 | root.unmount();
9 | });
10 |
--------------------------------------------------------------------------------
/src/assets/icons/ic_add_image_24px.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/ic_annotation_add_redact_black_24px.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
24 |
--------------------------------------------------------------------------------
/src/assets/icons/ic_annotation_apply_redact_black_24px.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
12 |
--------------------------------------------------------------------------------
/src/assets/icons/ic_annotation_square_black_24px.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/ic_chevron_left_black_24px.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/ic_chevron_right_black_24px.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/ic_close_black_24px.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/ic_edit_page_24px.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/assets/icons/ic_paragraph_24px.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/ic_search_black_24px.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/ic_select_black_24px.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/ic_zoom_in_black_24px.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/ic_zoom_out_black_24px.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/SearchContainer/SearchContainer.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --search-background: var(--grey-2);
3 | --search-buttons: var(--grey-3);
4 | --border: var(--grey-5);
5 | }
6 |
7 | #search-container {
8 | overflow: auto;
9 | flex: 1;
10 | background-color: var(--search-background);
11 | padding: 1em;
12 | min-width: 293px;
13 | }
14 |
15 | #search-container button {
16 | background-color: var(--search-buttons);
17 | border: none;
18 | outline: none;
19 | box-shadow: none;
20 | }
21 |
22 | #search-input * {
23 | vertical-align: middle;
24 | box-sizing: content-box;
25 | height: 28px;
26 | border: none;
27 | }
28 |
29 | #search-input input {
30 | border: 1px solid var(--grey-1);
31 | border-right: none;
32 | }
33 |
34 | #search-input input:focus {
35 | outline-style: solid;
36 | outline-color: var(--border);
37 | }
38 |
39 | #search-input button {
40 | border: 1px solid var(--border);
41 | }
42 |
43 | #search-buttons {
44 | display: flex;
45 | }
46 |
47 | #search-buttons button {
48 | margin: 0.5em;
49 | border: 1px solid var(--border);
50 | }
51 |
52 | #search-buttons > span:first-of-type {
53 | flex: 1;
54 | }
55 |
56 | .search-result {
57 | margin: 1em;
58 | padding: 1em;
59 | border: 1px solid var(--border);
60 | cursor: pointer;
61 | max-width: 275px;
62 | }
63 |
64 | .search-value {
65 | background-color: rgba(250,206,0,.50196);
66 | }
67 |
--------------------------------------------------------------------------------
/src/components/SearchContainer/SearchContainer.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import ClearSearch from '../../assets/icons/ic_close_black_24px.svg'
3 | import LeftChevronArrow from '../../assets/icons/ic_chevron_left_black_24px.svg'
4 | import RightChevronArrow from '../../assets/icons/ic_chevron_right_black_24px.svg'
5 | import Search from '../../assets/icons/ic_search_black_24px.svg'
6 | import './SearchContainer.css';
7 |
8 | const SearchContainer = (props) => {
9 | const [searchResults, setSearchResults] = useState([]);
10 | const [activeResultIndex, setActiveResultIndex] = useState(-1);
11 | const [toggledSearchModes, setToggledSearchModes] = useState([]);
12 |
13 | const {
14 | Annotations,
15 | annotationManager,
16 | documentViewer,
17 | open = false,
18 | searchContainerRef,
19 | searchTermRef: searchTerm,
20 | } = props;
21 |
22 | const pageRenderTracker = {};
23 |
24 | /**
25 | * Coupled with the function `changeActiveSearchResult`
26 | */
27 | useEffect(() => {
28 | if (activeResultIndex >= 0 && activeResultIndex < searchResults.length) {
29 | documentViewer.setActiveSearchResult(searchResults[activeResultIndex]);
30 | }
31 | }, [ activeResultIndex, searchResults, documentViewer ]);
32 |
33 | /**
34 | * Side-effect function that invokes `documentViewer.textSearchInit`, and stores
35 | * every result in the state Array `searchResults`, and jumps the user to the
36 | * first result is found.
37 | */
38 | const performSearch = () => {
39 | clearSearchResults(false);
40 | const {
41 | current: {
42 | value: textToSearch
43 | }
44 | } = searchTerm;
45 |
46 | const {
47 | PAGE_STOP,
48 | HIGHLIGHT,
49 | AMBIENT_STRING
50 | } = window.Core.Search.Mode;
51 |
52 | const mode = toggledSearchModes.reduce(
53 | (prev, value) => prev | value,
54 | (PAGE_STOP | HIGHLIGHT | AMBIENT_STRING),
55 | );
56 | const fullSearch = true;
57 | let jumped = false;
58 | documentViewer.textSearchInit(textToSearch, mode, {
59 | fullSearch,
60 | onResult: result => {
61 | setSearchResults(prevState => [...prevState, result]);
62 | const {
63 | resultCode,
64 | quads,
65 | page_num: pageNumber,
66 | } = result;
67 | const {
68 | e_found: eFound,
69 | } = window.Core.PDFNet.TextSearch.ResultCode
70 | if (resultCode === eFound) {
71 | const highlight = new Annotations.TextHighlightAnnotation();
72 | /**
73 | * The page number in Annotations.TextHighlightAnnotation is not
74 | * 0-indexed
75 | */
76 | highlight.setPageNumber(pageNumber);
77 | highlight.Quads.push(quads[0].getPoints());
78 | annotationManager.addAnnotation(highlight);
79 | annotationManager.drawAnnotations(highlight.PageNumber);
80 | if (!jumped) {
81 | jumped = true;
82 | // This is the first result found, so set `activeResult` accordingly
83 | setActiveResultIndex(0);
84 | documentViewer.displaySearchResult(result, () => {
85 | /**
86 | * The page number in documentViewer.displayPageLocation is not
87 | * 0-indexed
88 | */
89 | documentViewer.displayPageLocation(pageNumber, 0, 0, true);
90 | });
91 | }
92 | }
93 | }
94 | });
95 | };
96 |
97 | /**
98 | * Side-effect function that invokes the internal functions to clear the
99 | * search results
100 | *
101 | * @param {Boolean} clearSearchTermValue For the guard clause to determine
102 | * if `searchTerm.current.value` should be mutated (would not want this to
103 | * occur in the case where a subsequent search is being performed after a
104 | * previous search)
105 | */
106 | const clearSearchResults = (clearSearchTermValue = true) => {
107 | if (clearSearchTermValue) {
108 | searchTerm.current.value = '';
109 | }
110 | documentViewer.clearSearchResults();
111 | annotationManager.deleteAnnotations(annotationManager.getAnnotationsList());
112 | setSearchResults([]);
113 | setActiveResultIndex(-1);
114 | };
115 |
116 | /**
117 | * Checks if the key that has been released was the `Enter` key, and invokes
118 | * `performSearch` if so
119 | *
120 | * @param {SyntheticEvent} event The event passed from the `input` element
121 | * upon the function being invoked from a listener attribute, such as
122 | * `onKeyUp`
123 | */
124 | const listenForEnter = (event) => {
125 | const {
126 | keyCode,
127 | } = event;
128 | // The key code for the enter button
129 | if (keyCode === 13) {
130 | // Cancel the default action, if needed
131 | event.preventDefault();
132 | // Trigger the button element with a click
133 | performSearch();
134 | }
135 | };
136 |
137 | /**
138 | * Changes the active search result in `documentViewer`
139 | *
140 | * @param {Number} newSearchResult The index to set `activeResult` to,
141 | * indicating which `result` object that should be passed to
142 | * `documentViewer.setActiveSearchResult`
143 | */
144 | const changeActiveSearchResult = (newSearchResult) => {
145 | /**
146 | * @todo Figure out why only the middle set of search results can be
147 | * iterated through, but not the first or last results.
148 | */
149 | /**
150 | * Do not try to set a search result that is outside of the index range of
151 | * searchResults
152 | */
153 | if (newSearchResult >= 0 && newSearchResult < searchResults.length) {
154 | setActiveResultIndex(newSearchResult);
155 | }
156 | };
157 |
158 | /**
159 | * Toggles the given `searchMode` value within `toggledSearchModes`
160 | *
161 | * @param {CoreControls.DocumentViewer.SearchMode} searchMode The bitwise
162 | * search mode value to toggle on or off
163 | */
164 | const toggleSearchMode = (searchMode) => {
165 | if (!toggledSearchModes.includes(searchMode)) {
166 | setToggledSearchModes(prevState => [...prevState, searchMode])
167 | } else {
168 | setToggledSearchModes(
169 | prevState => prevState.filter(value => value !== searchMode)
170 | )
171 | }
172 | }
173 |
174 | /**
175 | * Side-effect function that toggles whether or not to perform a text search
176 | * with case sensitivty
177 | */
178 | const toggleCaseSensitive = () => {
179 | toggleSearchMode(window.Core.Search.Mode.CASE_SENSITIVE);
180 | }
181 |
182 | /**
183 | * Side-effect function that toggles whether or not to perform a text search
184 | * that finds the whole word
185 | */
186 | const toggleWholeWord = () => {
187 | toggleSearchMode(window.Core.Search.Mode.WHOLE_WORD);
188 | }
189 |
190 | if (!open) {
191 | return (null);
192 | }
193 |
194 | return (
195 |
199 |
210 |
211 |
212 |
217 | Case sensitive
218 |
219 |
220 |
225 | Whole word
226 |
227 |
228 |
229 |
250 |
251 | {
252 | searchResults.map((result, idx) => {
253 | const {
254 | ambient_str: ambientStr,
255 | page_num: pageNum,
256 | result_str_start: resultStrStart,
257 | result_str_end: resultStrEnd,
258 | } = result;
259 | const textBeforeSearchValue = ambientStr.slice(0, resultStrStart);
260 | const searchValue = ambientStr.slice(
261 | resultStrStart,
262 | resultStrEnd,
263 | );
264 | const textAfterSearchValue = ambientStr.slice(resultStrEnd);
265 | let pageHeader = null;
266 | if (!pageRenderTracker[pageNum]) {
267 | pageRenderTracker[pageNum] = true;
268 | pageHeader =
Page {pageNum}
269 | }
270 | return (
271 |
272 | {pageHeader}
273 |
{documentViewer.setActiveSearchResult(result)}}
276 | >
277 | {textBeforeSearchValue}
278 |
279 | {searchValue}
280 |
281 | {textAfterSearchValue}
282 |
283 |
284 | )
285 | })
286 | }
287 |
288 |
289 | );
290 | };
291 |
292 | export default SearchContainer;
293 |
--------------------------------------------------------------------------------
/src/components/SearchContainer/index.js:
--------------------------------------------------------------------------------
1 | import SearchContainer from './SearchContainer';
2 |
3 | export default SearchContainer;
4 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --grey-1: #F8F9FA;
3 | --grey-2: #F1F3F5;
4 | --grey-3: #E7EBEE;
5 | --grey-4: #DEE2E6;
6 | --grey-5: #CFD4DA;
7 | --divider: var(--grey-5);
8 | }
9 |
10 | html, body, #root {
11 | width: 100%;
12 | height: 100%;
13 | margin: 0;
14 | padding: 0;
15 | font-family: Arial, Helvetica, sans-serif;
16 | }
17 |
18 | .webviewer {
19 | flex: 1;
20 | margin: 8px;
21 | -webkit-box-shadow: 1px 1px 10px #999;
22 | box-shadow: 1px 1px 10px #999;
23 | }
24 |
25 | .divider {
26 | width: 100%;
27 | margin: 16px 0;
28 | border-top: 1px solid var(--divider);
29 | }
30 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import { createRoot } from 'react-dom/client';
2 | import './index.css';
3 | import App from './App';
4 |
5 | const root = createRoot(document.getElementById('root'));
6 | root.render();
7 |
--------------------------------------------------------------------------------
/tools/copy-webviewer-files.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs-extra');
2 |
3 | const copyFiles = async () => {
4 | try {
5 | await fs.copy('./node_modules/@pdftron/webviewer/public/core', './public/webviewer/');
6 | console.log('WebViewer files copied over successfully');
7 | } catch (err) {
8 | console.error(err);
9 | }
10 | };
11 |
12 | copyFiles();
13 |
--------------------------------------------------------------------------------