This sample demonstrates several features to customize WebViewer into a React app. The UI customization includes:
<iFrame><button> elements and implement functionality from the Apryse SDK such asWebViewer provides a slick out-of-the-box responsive UI that enables you to view, annotate and manipulate PDFs and other document types inside any web project. Check out getting started with WebViewer for React.
Click the button below to view the full project in GitHub.
1import React, { useRef, useEffect, useState } from 'react';
2import SearchContainer from './components/SearchContainer';
3import { ReactComponent as ZoomIn } from './assets/icons/ic_zoom_in_black_24px.svg';
4import { ReactComponent as ZoomOut } from './assets/icons/ic_zoom_out_black_24px.svg';
5import { ReactComponent as AnnotationRectangle } from './assets/icons/ic_annotation_square_black_24px.svg';
6import { ReactComponent as AnnotationRedact } from './assets/icons/ic_annotation_add_redact_black_24px.svg';
7import { ReactComponent as AnnotationApplyRedact} from './assets/icons/ic_annotation_apply_redact_black_24px.svg';
8import { ReactComponent as Search } from './assets/icons/ic_search_black_24px.svg';
9import { ReactComponent as Select } from './assets/icons/ic_select_black_24px.svg';
10import { ReactComponent as EditContent } from './assets/icons/ic_edit_page_24px.svg';
11import { ReactComponent as AddParagraph } from './assets/icons/ic_paragraph_24px.svg';
12import { ReactComponent as AddImageContent } from './assets/icons/ic_add_image_24px.svg';
13import './App.css';
14
15/* global globalThis */
16const root = globalThis;
17
18const App = () => {
19 const viewer = useRef(null);
20 const scrollView = useRef(null);
21 const searchTerm = useRef(null);
22 const searchContainerRef = useRef(null);
23
24 const [documentViewer, setDocumentViewer] = useState(null);
25 const [annotationManager, setAnnotationManager] = useState(null);
26 const [searchContainerOpen, setSearchContainerOpen] = useState(false);
27 const [isInContentEditMode, setIsInContentEditMode] = useState(false);
28
29 const Annotations = root.Core?.Annotations;
30
31 // if using a class, equivalent of componentDidMount
32 useEffect(() => {
33 let pollTimer;
34
35 const initViewer = () => {
36 const Core = root.Core;
37 if (!Core) {
38 pollTimer = setTimeout(initViewer, 50);
39 return;
40 }
41
42 Core.setWorkerPath('/webviewer');
43 Core.enableFullPDF();
44
45 const documentViewer = new Core.DocumentViewer();
46 documentViewer.setScrollViewElement(scrollView.current);
47 documentViewer.setViewerElement(viewer.current);
48 documentViewer.enableAnnotations();
49 documentViewer.loadDocument('/files/demo.pdf');
50
51 setDocumentViewer(documentViewer);
52
53 documentViewer.addEventListener('documentLoaded', () => {
54 console.log('document loaded');
55 documentViewer.setToolMode(documentViewer.getTool(Core.Tools.ToolNames.EDIT));
56 setAnnotationManager(documentViewer.getAnnotationManager());
57 });
58 };
59
60 initViewer();
61
62 return () => {
63 if (pollTimer) {
64 clearTimeout(pollTimer);
65 }
66 };
67 }, []);
68
69 const zoomOut = () => {
70 documentViewer.zoomTo(documentViewer.getZoomLevel() - 0.25);
71 };
72
73 const zoomIn = () => {
74 documentViewer.zoomTo(documentViewer.getZoomLevel() + 0.25);
75 };
76
77 const startEditingContent = () => {
78 const contentEditManager = documentViewer.getContentEditManager();
79 contentEditManager.startContentEditMode();
80 setIsInContentEditMode(true);
81 }
82
83 const endEditingContent = () => {
84 setIsInContentEditMode(false);
85 documentViewer.setToolMode(documentViewer.getTool(root.Core.Tools.ToolNames.EDIT));
86 const contentEditManager = documentViewer.getContentEditManager();
87 contentEditManager.endContentEditMode();
88 }
89
90 const addParagraph = () => {
91 if (isInContentEditMode) {
92 const addParagraphTool = documentViewer.getTool(root.Core.Tools.ToolNames.ADD_PARAGRAPH);
93 documentViewer.setToolMode(addParagraphTool);
94 } else {
95 alert('Content Edit mode is not enabled.')
96 }
97 };
98
99 const addImageContent = () => {
100 if (isInContentEditMode) {
101 const addImageContentTool = documentViewer.getTool(root.Core.Tools.ToolNames.ADD_IMAGE_CONTENT);
102 documentViewer.setToolMode(addImageContentTool);
103 } else {
104 alert('Content Edit mode is not enabled.')
105 }
106 };
107
108 const createRectangle = () => {
109 documentViewer.setToolMode(documentViewer.getTool(root.Core.Tools.ToolNames.RECTANGLE));
110 };
111
112 const selectTool = () => {
113 documentViewer.setToolMode(documentViewer.getTool(root.Core.Tools.ToolNames.EDIT));
114 };
115
116 const createRedaction = () => {
117 documentViewer.setToolMode(documentViewer.getTool(root.Core.Tools.ToolNames.REDACTION));
118 };
119
120 const applyRedactions = async () => {
121 const annotationManager = documentViewer.getAnnotationManager();
122 annotationManager.enableRedaction();
123 await annotationManager.applyRedactions();
124 };
125
126 return (
127 <div className="App">
128 <div id="main-column">
129 <div className="center" id="tools">
130 <button onClick={zoomOut}>
131 <ZoomOut />
132 </button>
133 <button onClick={zoomIn}>
134 <ZoomIn />
135 </button>
136 <button onClick={startEditingContent} title="Switch to edit mode">
137 <EditContent />
138 </button>
139 <button onClick={addParagraph} title="Add new paragraph">
140 <AddParagraph />
141 </button>
142 <button onClick={addImageContent} title="Add new content image">
143 <AddImageContent />
144 </button>
145 <button onClick={endEditingContent} title="End edit mode">
146 Finish Editing
147 </button>
148 <button onClick={createRectangle}>
149 <AnnotationRectangle />
150 </button>
151 <button onClick={createRedaction} title="Create Redaction">
152 <AnnotationRedact />
153 </button>
154 <button onClick={applyRedactions} title="Apply Redactions">
155 <AnnotationApplyRedact />
156 </button>
157 <button onClick={selectTool}>
158 <Select />
159 </button>
160 <button
161 onClick={() => {
162 // Flip the boolean
163 setSearchContainerOpen(prevState => !prevState);
164 }}
165 >
166 <Search />
167 </button>
168 </div>
169 <div className="flexbox-container" id="scroll-view" ref={scrollView}>
170 <div id="viewer" ref={viewer}></div>
171 </div>
172 </div>
173 <div className="flexbox-container">
174 <SearchContainer
175 Annotations={Annotations}
176 annotationManager={annotationManager}
177 documentViewer={documentViewer}
178 searchTermRef={searchTerm}
179 searchContainerRef={searchContainerRef}
180 open={searchContainerOpen}
181 />
182 </div>
183 </div>
184 );
185};
186
187export default App;
188
1import React, { useState, useEffect } from 'react';
2import ClearSearch from '../../assets/icons/ic_close_black_24px.svg'
3import LeftChevronArrow from '../../assets/icons/ic_chevron_left_black_24px.svg'
4import RightChevronArrow from '../../assets/icons/ic_chevron_right_black_24px.svg'
5import Search from '../../assets/icons/ic_search_black_24px.svg'
6import './SearchContainer.css';
7
8const SearchContainer = (props) => {
9 const [searchResults, setSearchResults] = useState([]);
10 const [activeResultIndex, setActiveResultIndex] = useState(-1);
11 const [toggledSearchModes, setToggledSearchModes] = useState([]);
12
13 const {
14 annotationManager,
15 documentViewer,
16 open = false,
17 searchContainerRef,
18 searchTermRef: searchTerm,
19 } = props;
20
21 const pageRenderTracker = {};
22
23 /**
24 * Coupled with the function `changeActiveSearchResult`
25 */
26 useEffect(() => {
27 if (activeResultIndex >= 0 && activeResultIndex < searchResults.length) {
28 documentViewer.setActiveSearchResult(searchResults[activeResultIndex]);
29 }
30 }, [ activeResultIndex ]);
31
32 /**
33 * Side-effect function that invokes `documentViewer.textSearchInit`, and stores
34 * every result in the state Array `searchResults`, and jumps the user to the
35 * first result is found.
36 */
37 const performSearch = () => {
38 clearSearchResults(false);
39
40 if (!documentViewer || !window.Core?.Search) {
41 return;
42 }
43
44 const {
45 current: {
46 value: textToSearch
47 }
48 } = searchTerm;
49
50 const {
51 PAGE_STOP,
52 HIGHLIGHT,
53 AMBIENT_STRING
54 } = window.Core.Search.Mode;
55
56 const mode = toggledSearchModes.reduce(
57 (prev, value) => prev | value,
58 (PAGE_STOP | HIGHLIGHT | AMBIENT_STRING),
59 );
60 const fullSearch = true;
61 let jumped = false;
62 documentViewer.textSearchInit(textToSearch, mode, {
63 fullSearch,
64 onResult: (result) => {
65 const foundCode =
66 window.Core?.Search?.ResultCode?.FOUND ??
67 window.PDFNet?.TextSearch?.ResultCode?.FOUND;
68
69 if (result.resultCode !== foundCode) {
70 return;
71 }
72
73 setSearchResults((prevState) => [...prevState, result]);
74
75 if (!jumped) {
76 jumped = true;
77 const pageNumber = result.pageNum ?? result.page_number;
78 // This is the first result found, so set `activeResult` accordingly
79 setActiveResultIndex(0);
80 documentViewer.displaySearchResult(result, () => {
81 /**
82 * The page number in documentViewer.displayPageLocation is not
83 * 0-indexed
84 */
85 documentViewer.displayPageLocation(pageNumber, 0, 0, true);
86 });
87 }
88 },
89 });
90 };
91
92 /**
93 * Side-effect function that invokes the internal functions to clear the
94 * search results
95 *
96 * @param {Boolean} clearSearchTermValue For the guard clause to determine
97 * if `searchTerm.current.value` should be mutated (would not want this to
98 * occur in the case where a subsequent search is being performed after a
99 * previous search)
100 */
101 const clearSearchResults = (clearSearchTermValue = true) => {
102 if (clearSearchTermValue) {
103 searchTerm.current.value = '';
104 }
105 documentViewer.clearSearchResults();
106 annotationManager.deleteAnnotations(annotationManager.getAnnotationsList());
107 setSearchResults([]);
108 setActiveResultIndex(-1);
109 };
110
111 /**
112 * Checks if the key that has been released was the `Enter` key, and invokes
113 * `performSearch` if so
114 *
115 * @param {SyntheticEvent} event The event passed from the `input` element
116 * upon the function being invoked from a listener attribute, such as
117 * `onKeyUp`
118 */
119 const listenForEnter = (event) => {
120 const {
121 keyCode,
122 } = event;
123 // The key code for the enter button
124 if (keyCode === 13) {
125 // Cancel the default action, if needed
126 event.preventDefault();
127 // Trigger the button element with a click
128 performSearch();
129 }
130 };
131
132 /**
133 * Changes the active search result in `documentViewer`
134 *
135 * @param {Number} newSearchResult The index to set `activeResult` to,
136 * indicating which `result` object that should be passed to
137 * `documentViewer.setActiveSearchResult`
138 */
139 const changeActiveSearchResult = (newSearchResult) => {
140 /**
141 * @todo Figure out why only the middle set of search results can be
142 * iterated through, but not the first or last results.
143 */
144 /**
145 * Do not try to set a search result that is outside of the index range of
146 * searchResults
147 */
148 if (newSearchResult >= 0 && newSearchResult < searchResults.length) {
149 setActiveResultIndex(newSearchResult);
150 }
151 };
152
153 /**
154 * Toggles the given `searchMode` value within `toggledSearchModes`
155 *
156 * @param {CoreControls.DocumentViewer.SearchMode} searchMode The bitwise
157 * search mode value to toggle on or off
158 */
159 const toggleSearchMode = (searchMode) => {
160 if (!toggledSearchModes.includes(searchMode)) {
161 setToggledSearchModes(prevState => [...prevState, searchMode])
162 } else {
163 setToggledSearchModes(
164 prevState => prevState.filter(value => value !== searchMode)
165 )
166 }
167 }
168
169 /**
170 * Side-effect function that toggles whether or not to perform a text search
171 * with case sensitivty
172 */
173 const toggleCaseSensitive = () => {
174 toggleSearchMode(window.Core.Search.Mode.CASE_SENSITIVE);
175 }
176
177 /**
178 * Side-effect function that toggles whether or not to perform a text search
179 * that finds the whole word
180 */
181 const toggleWholeWord = () => {
182 toggleSearchMode(window.Core.Search.Mode.WHOLE_WORD);
183 }
184
185 if (!open) {
186 return (null);
187 }
188
189 return (
190 <span
191 id="search-container"
192 ref={searchContainerRef}
193 >
194 <div id="search-input">
195 <input
196 ref={searchTerm}
197 type={'text'}
198 placeholder={'Search'}
199 onKeyUp={listenForEnter}
200 />
201 <button onClick={performSearch}>
202 <img src={Search} alt="Search"/>
203 </button>
204 </div>
205 <div>
206 <span>
207 <input
208 type="checkbox"
209 value={toggledSearchModes.includes(window.Core.Search.Mode.CASE_SENSITIVE)}
210 onChange={toggleCaseSensitive}
211 />
212 Case sensitive
213 </span>
214 <span>
215 <input
216 type="checkbox"
217 value={toggledSearchModes.includes(window.Core.Search.Mode.WHOLE_WORD)}
218 onChange={toggleWholeWord}
219 />
220 Whole word
221 </span>
222 </div>
223 <div className="divider"></div>
224 <div id='search-buttons'>
225 <span>
226 <button onClick={clearSearchResults}>
227 <img src={ClearSearch} alt="Clear Search"/>
228 </button>
229 </span>
230 <span id="search-iterators">
231 <button
232 onClick={() => { changeActiveSearchResult(activeResultIndex - 1); }}
233 disabled={activeResultIndex < 0}
234 >
235 <img src={LeftChevronArrow} alt="Previous Search Result"/>
236 </button>
237 <button
238 onClick={() => { changeActiveSearchResult(activeResultIndex + 1); }}
239 disabled={activeResultIndex < 0}
240 >
241 <img src={RightChevronArrow} alt="Next Search Result"/>
242 </button>
243 </span>
244 </div>
245 <div>
246 {
247 searchResults.map((result, idx) => {
248 const {
249 ambient_str: ambientStr,
250 page_num: pageNum,
251 result_str_start: resultStrStart,
252 result_str_end: resultStrEnd,
253 } = result;
254 const textBeforeSearchValue = ambientStr.slice(0, resultStrStart);
255 const searchValue = ambientStr.slice(
256 resultStrStart,
257 resultStrEnd,
258 );
259 const textAfterSearchValue = ambientStr.slice(resultStrEnd);
260 let pageHeader = null;
261 if (!pageRenderTracker[pageNum]) {
262 pageRenderTracker[pageNum] = true;
263 pageHeader = <div>Page {pageNum}</div>
264 }
265 return (
266 <div key={`search-result-${idx}`} >
267 {pageHeader}
268 <div
269 className='search-result'
270 onClick={() => {documentViewer.setActiveSearchResult(result)}}
271 >
272 {textBeforeSearchValue}
273 <span className="search-value">
274 {searchValue}
275 </span>
276 {textAfterSearchValue}
277 </div>
278 </div>
279 )
280 })
281 }
282 </div>
283 </span>
284 );
285};
286
287export default SearchContainer;
288
Did you find this helpful?
Trial setup questions?
Ask experts on DiscordNeed other help?
Contact SupportPricing or product questions?
Contact Sales