Annotations Permissions Showcase Demo Code Sample

Enable customization of user permissions to interact with annotations in the PDF file, completely client-side with three different levels of permissions:

  • Administrator
  • User
  • Read-Only

This demo lets you:

  • Upload a PDF file and add user permissions
  • Add permissions to users as administrator, user, or read-only
  • Interact with annotations in the PDF file according to the set permission

Implementation steps
To add annotations permissions capability to a PDF with WebViewer:

Step 1: Choose your preferred web stack
Step 2: Download required modules listed in the Demo Dependencies section below
Step 3: Add the ES6 JavaScript sample code provided in this guide

Demo Dependencies
This sample uses the following:

Want to see a live version of this demo?

Try the Annotations Permissions demo

1
2/* ES6 Compliant Syntax */
3/* GitHub Copilot v1.0, Claude 3.5 Sonnet, September 1, 2025 */
4/* File: index.js */
5
6import WebViewer from '@pdftron/webviewer';
7
8// Annotation Permission section
9//
10// Code to customize user permissions, completely client-side with 3
11// different levels of permissions: administrator, user, and read-only
12//
13
14// Default Document
15const defaultDoc = 'https://apryse.s3.us-west-1.amazonaws.com/public/files/samples/WebviewerDemoDoc.pdf';
16
17// Set default user
18let currentUser = 'Justin';
19
20// List of users with permissions
21let userList = {
22 Justin: { permissions: 'administrator', canView: true, hidden: [] },
23 Sally: { permissions: 'user', canView: true, hidden: [] },
24 Brian: { permissions: 'read-only', canView: true, hidden: [] }
25};
26
27// Annotation types that can be toggled for visibility
28let toggleableTypes = [];
29
30// Customize UI
31const customizeUI = async (instance) => {
32 const { Annotations } = instance.Core;
33
34 // Load default document
35 await instance.Core.documentViewer.loadDocument(defaultDoc, {
36 extension: 'pdf',
37 });
38 instance.UI.setToolbarGroup('toolbarGroup-Annotate', true);
39
40 // Add sticky note, free hand, and highlight to toggleable types
41 toggleableTypes = [
42 {
43 displayName: 'Sticky notes',
44 annotationType: Annotations.StickyAnnotation,
45 },
46 {
47 displayName: 'Free hand',
48 annotationType: Annotations.FreeHandAnnotation,
49 },
50 {
51 displayName: 'Highlight',
52 annotationType: Annotations.TextHighlightAnnotation,
53 },
54 ];
55
56 // Set user data for mentions in notes tool
57 const userData = Object.keys(userList).map((user) => ({
58 value: user,
59 email: `${user.toLowerCase()}@pdftron.com`,
60 }));
61 instance.UI.mentions.setUserData(userData);
62
63 // Set default user in the WebViewer
64 setUser(instance, currentUser);
65};
66
67// Set selected user in the WebViewer
68const setUser = (instance, username) => {
69 const { annotationManager }= instance.Core;
70 annotationManager.setCurrentUser(username);
71 const permissions = userList[username].permissions;
72
73 if (permissions === 'administrator') {
74 annotationManager.promoteUserToAdmin();
75 annotationManager.disableReadOnlyMode();
76 } else if (permissions === 'read-only') {
77 annotationManager.enableReadOnlyMode();
78 annotationManager.demoteUserFromAdmin();
79 } else {
80 annotationManager.disableReadOnlyMode();
81 annotationManager.demoteUserFromAdmin();
82 }
83
84 currentUser = username;
85 setAnnotationsForUser(instance);
86 updateUIControls();
87};
88
89// Set annotations for current user
90const setAnnotationsForUser = (instance) => {
91 const { annotationManager } = instance.Core;
92
93 const { hidden } = userList[currentUser];
94
95 // First get a list of all the types that should be hidden
96 const hiddenTypeMap = toggleableTypes.reduce((acc, type) => {
97 if (hidden.indexOf(type.displayName) > -1) {
98 acc.push(type.annotationType);
99 }
100 return acc;
101 }, []);
102
103 const allAnnots = annotationManager.getAnnotationsList();
104 const toShow = [];
105 const toHide = [];
106
107 // Generate lists of annotations to show and hide
108 allAnnots.forEach((annot) => {
109 const isType = hiddenTypeMap.some((type) => annot instanceof type);
110 if (isType) {
111 toHide.push(annot);
112 } else {
113 toShow.push(annot);
114 }
115 });
116
117 // Show and hide annotations
118 annotationManager.showAnnotations(toShow);
119 annotationManager.hideAnnotations(toHide);
120};
121
122// Add user to user list
123const addUser = (instance, name, p) => {
124 userList = {
125 ...userList,
126 [name]: { permissions: p, canView: true, hidden: [] },
127 };
128};
129
130// Toggle annotation type visibility for current user
131const toggleAnnotations = (instance, type) => {
132 const { displayName } = type;
133 const { hidden } = userList[currentUser];
134 const idx = hidden.indexOf(displayName);
135 const newArray = hidden.slice(0);
136
137 if (idx !== -1) {
138 newArray.splice(idx, 1);
139 } else {
140 newArray.push(displayName);
141 }
142
143 userList = {
144 ...userList,
145 [currentUser]: {
146 ...userList[currentUser],
147 hidden: newArray
148 }
149 };
150
151 setAnnotationsForUser(instance);
152 updateUIControls();
153};
154
155// Check if annotation type is visible for current user
156const isChecked = (type) => {
157 if (!currentUser) return;
158 const { displayName } = type;
159 const user = currentUser;
160 return userList[user].hidden.indexOf(displayName) === -1;
161};
162
163
164// Helper functions for configuration snippet modal
165const perm = () => {
166 return currentUser ? userList[currentUser].permissions : null;
167}
168const hiddenList = () => {
169 return currentUser ? userList[currentUser].hidden : [];
170};
171
172let text = '';
173let annotText = '';
174
175const setText = () => {
176 const permission = perm();
177 if (permission === 'administrator') {
178 text = 'annotationManager.promoteUserToAdmin()';
179 } else if (permission === 'read-only') {
180 text = 'annotationManager.enableReadOnlyMode()';
181 } else {
182 text = 'annotationManager.demoteUserFromAdmin();\n annotationManager.disableReadOnlyMode();';
183 }
184
185 return text;
186};
187
188const setAnnotText = () => {
189 const list = hiddenList();
190 if (list.length === 0) {
191 annotText = `
192 annotationManager.showAnnotations(allAnnots);
193 `;
194 } else {
195 let ifStatement = list.reduce((acc, hidden) => {
196 if (hidden === 'Sticky notes') {
197 acc += ' annot instanceof Annotations.StickyAnnotation || \n';
198 }
199 if (hidden === 'Free hand') {
200 acc += ' annot instanceof Annotations.FreeHandAnnotation || \n';
201 }
202 if (hidden === 'Highlight') {
203 acc += ' annot instanceof Annotations.TextHighlightAnnotation || \n';
204 }
205
206 return acc;
207 }, '');
208
209 ifStatement = ifStatement.substring(8, ifStatement.length - 5);
210
211 annotText = `
212 const hideList = allAnnots.filter(annot => {
213 return ${ifStatement};
214 });
215 annotationManager.hideAnnotations(hideList);
216 `;
217 }
218
219 return annotText;
220};
221
222// WebViewer section
223//
224// This code initializes the WebViewer with the basic settings
225// that are found in the default showcase WebViewer
226//
227
228const searchParams = new URLSearchParams(window.location.search);
229const history = window.history || window.parent.history || window.top.history;
230const licenseKey = 'YOUR_LICENSE_KEY_HERE';
231const element = document.getElementById('viewer');
232
233// Initialize WebViewer with the specified settings
234WebViewer({
235 path: '/lib',
236 licenseKey: licenseKey,
237 enableFilePicker: true,
238}, element).then((instance) => {
239 // Enable the measurement toolbar so it appears with all the other tools, and disable Cloudy rectangular tool
240 const cloudyTools = [
241 instance.Core.Tools.ToolNames.CLOUDY_RECTANGULAR_AREA_MEASUREMENT,
242 instance.Core.Tools.ToolNames.CLOUDY_RECTANGULAR_AREA_MEASUREMENT2,
243 instance.Core.Tools.ToolNames.CLOUDY_RECTANGULAR_AREA_MEASUREMENT3,
244 instance.Core.Tools.ToolNames.CLOUDY_RECTANGULAR_AREA_MEASUREMENT4,
245 ];
246 instance.UI.enableFeatures([instance.UI.Feature.Measurement, instance.UI.Feature.Initials]);
247 instance.UI.disableTools(cloudyTools);
248
249 // Set default toolbar group to Annotate
250 instance.UI.setToolbarGroup('toolbarGroup-Annotate');
251
252 // Set default tool on mobile devices to Pan.
253 // https://apryse.atlassian.net/browse/WVR-3134
254 if (isMobileDevice()) {
255 instance.UI.setToolMode(instance.Core.Tools.ToolNames.PAN);
256 }
257
258 instance.Core.documentViewer.addEventListener('documentUnloaded', () => {
259 if (searchParams.has('file')) {
260 searchParams.delete('file');
261 history.replaceState(null, '', '?' + searchParams.toString());
262 }
263 });
264
265 instance.Core.annotationManager.enableAnnotationNumbering();
266
267 instance.UI.NotesPanel.enableAttachmentPreview();
268
269 // Add the demo-specific functionality
270 customizeUI(instance).then(() => {
271 // Create UI controls after demo is initialized
272 createUIControls(instance);
273 });
274});
275
276// Function to check if the user is on a mobile device
277const isMobileDevice = () => {
278 return (
279 /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|ipad|iris|kindle|Android|Silk|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i.test(
280 window.navigator.userAgent
281 ) ||
282 /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw-(n|u)|c55\/|capi|ccwa|cdm-|cell|chtm|cldc|cmd-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc-s|devi|dica|dmob|do(c|p)o|ds(12|-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(-|_)|g1 u|g560|gene|gf-5|g-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd-(m|p|t)|hei-|hi(pt|ta)|hp( i|ip)|hs-c|ht(c(-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i-(20|go|ma)|i230|iac( |-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|-[a-w])|libw|lynx|m1-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|-([1-8]|c))|phil|pire|pl(ay|uc)|pn-2|po(ck|rt|se)|prox|psio|pt-g|qa-a|qc(07|12|21|32|60|-[2-7]|i-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h-|oo|p-)|sdk\/|se(c(-|0|1)|47|mc|nd|ri)|sgh-|shar|sie(-|m)|sk-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h-|v-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl-|tdg-|tel(i|m)|tim-|t-mo|to(pl|sh)|ts(70|m-|m3|m5)|tx-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas-|your|zeto|zte-/i.test(
283 window.navigator.userAgent.substring(0, 4)
284 )
285 );
286}
287
288// Cleanup function for when the demo is closed or page is unloaded
289const cleanup = (instance) => {
290 const { documentViewer } = instance.Core;
291
292 if (typeof instance !== 'undefined' && instance.UI) {
293
294 // Reset annotation user to default 'Guest' and promote to admin
295 const annotationManager = documentViewer.getAnnotationManager();
296 annotationManager.setCurrentUser('Guest');
297 annotationManager.promoteUserToAdmin();
298 annotationManager.disableReadOnlyMode();
299
300 console.log('Cleaning up compare-files demo');
301 }
302};
303
304// Register cleanup for page unload
305window.addEventListener('beforeunload', () => cleanup());
306window.addEventListener('unload', () => cleanup());
307
308
309// UI section
310//
311// Helper code to add controls to the viewer holding the buttons
312// This code creates a container for the buttons, styles them, and adds them to the viewer
313//
314
315// User selection
316const userPageSection = (instance) => {
317 // Create a wrapper div for the user section
318 const wrapper = document.createElement('div');
319 wrapper.className = 'user-page-section';
320
321 // Select User label
322 const selectUserLabel = document.createElement('h2');
323 selectUserLabel.className = 'header select-user-header';
324 selectUserLabel.textContent = 'Select User';
325
326 wrapper.appendChild(selectUserLabel);
327
328 // Users buttons container
329 const buttonsContainer = document.createElement('div');
330 buttonsContainer.className = 'buttons-container';
331
332 Object.keys(userList).map((username) => {
333 const button = document.createElement('button');
334 button.className = 'btn btn-user';
335 button.textContent = `${username} (${userList[username].permissions})`;
336 button.onclick = () => {
337 setUser(instance, username);
338 };
339
340 buttonsContainer.appendChild(button);
341 });
342 wrapper.appendChild(buttonsContainer);
343
344 // Add User clickable label
345 const addUserLabel = document.createElement('label');
346 addUserLabel.className = 'add-user-label';
347 addUserLabel.textContent = 'Add user';
348 addUserLabel.onclick = () => {
349 const addUserContainer = document.querySelector('.add-user-container');
350 if (addUserContainer.classList.contains('visible')) {
351 addUserContainer.classList.remove('visible');
352 addUserLabel.textContent = 'Add user';
353 } else {
354 addUserContainer.classList.add('visible');
355 addUserLabel.textContent = 'Close';
356 }
357 };
358
359 wrapper.appendChild(addUserLabel);
360
361 // Add User container
362 const addUserContainer = document.createElement('div');
363 addUserContainer.className = 'add-user-container';
364
365 // Add User input field
366 const input = document.createElement('input');
367 input.type = 'text';
368 input.placeholder = 'Username';
369 input.className = 'input add-user-input';
370
371 addUserContainer.appendChild(input);
372
373 // Add User permission dropdown
374 const permission = document.createElement('select');
375 permission.className = 'input add-user-permission';
376 permission.options.add(new Option('Administrator', 'administrator'));
377 permission.options.add(new Option('User', 'user'));
378 permission.options.add(new Option('Read-Only', 'read-only'));
379
380 addUserContainer.appendChild(permission);
381
382 // Add User add button
383 const addButton = document.createElement('button');
384 addButton.className = 'btn btn-submit-user';
385 addButton.textContent = 'Add';
386 addButton.onclick = () => {
387 // Validate input
388 const name = input.value;
389 if (name === '') return;
390 const p = permission.value;
391
392 // Add user to user list
393 addUser(instance, name, p);
394
395 // Add button for new user
396 const buttonsContainer = document.querySelector('.buttons-container');
397 const button = document.createElement('button');
398 button.className = 'btn btn-user';
399 button.textContent = `${name} (${p})`;
400 button.onclick = () => {
401 setUser(instance, name);
402 };
403 buttonsContainer.appendChild(button);
404
405 // Reset input fields
406 input.value = '';
407 permission.options.selectedIndex = 0;
408
409 // Close add user section
410 addUserLabel.click();
411 };
412
413 addUserContainer.appendChild(addButton);
414
415 wrapper.appendChild(addUserContainer);
416
417 return wrapper;
418};
419
420// Role permissions description
421const rolePermissionsPageSection = () => {
422 const wrapper = document.createElement('div');
423 wrapper.className = 'role-permissions-section';
424
425 const rolePermissionsLabel = document.createElement('h2');
426 rolePermissionsLabel.className = 'header role-permissions-header';
427 rolePermissionsLabel.textContent = 'Role Permissions';
428
429 wrapper.appendChild(rolePermissionsLabel);
430
431 const rolePermissionDescription = document.createElement('p');
432 rolePermissionDescription.className = 'text role-permission-paragraph';
433 const permission = perm();
434 if (permission === 'administrator') {
435 rolePermissionDescription.innerHTML = '<b> Admin: </b> Can add, edit, or remove any annotations created by anyone';
436 } else if (permission === 'read-only') {
437 rolePermissionDescription.innerHTML = '<b> Read-Only: </b> Can only view annotations';
438 } else { // user
439 rolePermissionDescription.innerHTML = '<b> User: </b> Can create, and edit or remove annotations created by themself';
440 }
441
442 wrapper.appendChild(rolePermissionDescription);
443
444 return wrapper;
445};
446
447// Viewing permissions checkboxes for selected user
448const viewingPermissionsPageSection = (instance) => {
449 const wrapper = document.createElement('div');
450 wrapper.className = 'viewing-permissions-section';
451
452 // Viewing Permissions label
453 const viewingPermissionsLabel = document.createElement('label');
454 viewingPermissionsLabel.className = 'header viewing-permissions-label';
455 viewingPermissionsLabel.textContent = `Set viewing permissions for ` + (currentUser ? currentUser : '...');
456
457 wrapper.appendChild(viewingPermissionsLabel);
458
459 // Viewing Permissions for each annotation type
460 const checkboxContainer = document.createElement('div');
461 checkboxContainer.className = 'checkbox-container';
462
463 toggleableTypes.map((type) => {
464 const checkboxRow = document.createElement('div');
465
466 const checkbox = document.createElement('input');
467 checkbox.type = 'checkbox';
468 checkbox.id = `view-${type.displayName}-checkbox`;
469 checkbox.checked = isChecked(type);
470 checkbox.onchange = () => {
471 toggleAnnotations(instance, type);
472 };
473
474 checkboxRow.appendChild(checkbox);
475
476 const label = document.createElement('label');
477 label.className = 'text checkbox-label';
478 label.ariaLabel = `Toggle ${type.displayName} annotations`;
479 label.textContent = `${type.displayName}`;
480
481 checkboxRow.appendChild(label);
482 checkboxContainer.appendChild(checkboxRow);
483 });
484 wrapper.appendChild(checkboxContainer);
485
486 return wrapper;
487};
488
489
490// Helper function to create UI controls
491const createUIControls = (instance) => {
492 // Create a container for all controls (label, dropdown, and buttons)
493 const controlsContainer = document.createElement('div');
494 controlsContainer.className = 'controls-container';
495
496 // Add user section
497 controlsContainer.appendChild(userPageSection(instance));
498
499 // Add role permissions and viewing permissions sections side by side
500 const roleViewingPermissionsContainer = document.createElement('div');
501 roleViewingPermissionsContainer.className = 'role-viewing-permissions-container';
502 roleViewingPermissionsContainer.appendChild(rolePermissionsPageSection());
503 roleViewingPermissionsContainer.appendChild(viewingPermissionsPageSection(instance));
504 controlsContainer.appendChild(roleViewingPermissionsContainer);
505
506
507
508 element.insertBefore(controlsContainer, element.firstChild);
509};
510
511// Helper function to update UI controls
512const updateUIControls = () => {
513 // Update role permission description
514 const rolePermissionDescription = document.querySelector('.role-permission-paragraph');
515 if (rolePermissionDescription) {
516 const permission = perm();
517 if (permission === 'administrator') {
518 rolePermissionDescription.innerHTML = '<b> Admin: </b> Can add, edit, or remove any annotations created by anyone';
519 } else if (permission === 'read-only') {
520 rolePermissionDescription.innerHTML = '<b> Read-Only: </b> Can only view annotations';
521 } else { // user
522 rolePermissionDescription.innerHTML = '<b> User: </b> Can create, and edit or remove annotations created by themself';
523 }
524 }
525
526 // Update viewing permissions label
527 const viewingPermissionsLabel = document.querySelector('.viewing-permissions-label');
528 if (viewingPermissionsLabel) {
529 viewingPermissionsLabel.textContent = `Set viewing permissions for ` + (currentUser ? currentUser : '...');
530 }
531
532 // Update checkboxes
533 toggleableTypes.map((type) => {
534 const checkbox = document.getElementById(`view-${type.displayName}-checkbox`);
535 if (checkbox) {
536 checkbox.checked = isChecked(type);
537 }
538 });
539
540 // Update configuration snippet text
541 const codeBlock = document.getElementById('config-snippet-code-block');
542 if (codeBlock) {
543 codeBlock.textContent = `const wvElement = document.getElementById('viewer');
544WebViewer({ ...options }, wvElement)
545.then(instance => {
546 const { annotationManager } = instance.Core;
547 annotationManager.setCurrentUser('${currentUser}');
548 ${setText()}
549 const allAnnots = annotationManager.getAnnotationsList();
550 ${setAnnotText()}
551})`;
552 }
553};

Did you find this helpful?

Trial setup questions?

Ask experts on Discord

Need other help?

Contact Support

Pricing or product questions?

Contact Sales