This sample demonstrates several features to customize WebViewer into a React app to render without <iFrame>
, <button>
elements for expanding functionality, and how to leverage the search APIs. 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.
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
15const 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 <div className="App">
108 <div id="main-column">
109 <div className="center" id="tools">
110 <button onClick={zoomOut}>
111 <ZoomOut />
112 </button>
113 <button onClick={zoomIn}>
114 <ZoomIn />
115 </button>
116 <button onClick={startEditingContent} title="Switch to edit mode">
117 <EditContent />
118 </button>
119 <button onClick={addParagraph} title="Add new paragraph">
120 <AddParagraph />
121 </button>
122 <button onClick={addImageContent} title="Add new content image">
123 <AddImageContent />
124 </button>
125 <button onClick={endEditingContent} title="End edit mode">
126 Finish Editing
127 </button>
128 <button onClick={createRectangle}>
129 <AnnotationRectangle />
130 </button>
131 <button onClick={createRedaction} title="Create Redaction">
132 <AnnotationRedact />
133 </button>
134 <button onClick={applyRedactions} title="Apply Redactions">
135 <AnnotationApplyRedact />
136 </button>
137 <button onClick={selectTool}>
138 <Select />
139 </button>
140 <button
141 onClick={() => {
142 // Flip the boolean
143 setSearchContainerOpen(prevState => !prevState);
144 }}
145 >
146 <Search />
147 </button>
148 </div>
149 <div className="flexbox-container" id="scroll-view" ref={scrollView}>
150 <div id="viewer" ref={viewer}></div>
151 </div>
152 </div>
153 <div className="flexbox-container">
154 <SearchContainer
155 Annotations={Annotations}
156 annotationManager={annotationManager}
157 documentViewer={documentViewer}
158 searchTermRef={searchTerm}
159 searchContainerRef={searchContainerRef}
160 open={searchContainerOpen}
161 />
162 </div>
163 </div>
164 );
165};
166
167export default App;
168
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 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 ]);
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.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 <span
196 id="search-container"
197 ref={searchContainerRef}
198 >
199 <div id="search-input">
200 <input
201 ref={searchTerm}
202 type={'text'}
203 placeholder={'Search'}
204 onKeyUp={listenForEnter}
205 />
206 <button onClick={performSearch}>
207 <img src={Search} alt="Search"/>
208 </button>
209 </div>
210 <div>
211 <span>
212 <input
213 type="checkbox"
214 value={toggledSearchModes.includes(window.Core.Search.Mode.CASE_SENSITIVE)}
215 onChange={toggleCaseSensitive}
216 />
217 Case sensitive
218 </span>
219 <span>
220 <input
221 type="checkbox"
222 value={toggledSearchModes.includes(window.Core.Search.Mode.WHOLE_WORD)}
223 onChange={toggleWholeWord}
224 />
225 Whole word
226 </span>
227 </div>
228 <div className="divider"></div>
229 <div id='search-buttons'>
230 <span>
231 <button onClick={clearSearchResults}>
232 <img src={ClearSearch} alt="Clear Search"/>
233 </button>
234 </span>
235 <span id="search-iterators">
236 <button
237 onClick={() => { changeActiveSearchResult(activeResultIndex - 1); }}
238 disabled={activeResultIndex < 0}
239 >
240 <img src={LeftChevronArrow} alt="Previous Search Result"/>
241 </button>
242 <button
243 onClick={() => { changeActiveSearchResult(activeResultIndex + 1); }}
244 disabled={activeResultIndex < 0}
245 >
246 <img src={RightChevronArrow} alt="Next Search Result"/>
247 </button>
248 </span>
249 </div>
250 <div>
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 = <div>Page {pageNum}</div>
269 }
270 return (
271 <div key={`search-result-${idx}`} >
272 {pageHeader}
273 <div
274 className='search-result'
275 onClick={() => {documentViewer.setActiveSearchResult(result)}}
276 >
277 {textBeforeSearchValue}
278 <span className="search-value">
279 {searchValue}
280 </span>
281 {textAfterSearchValue}
282 </div>
283 </div>
284 )
285 })
286 }
287 </div>
288 </span>
289 );
290};
291
292export default SearchContainer;
293
Did you find this helpful?
Trial setup questions?
Ask experts on DiscordNeed other help?
Contact SupportPricing or product questions?
Contact Sales