Easily integrate real-time document collaboration into your application with synchronized user permissions, annotations, comments, and status updates.
This demo allows you to:
Implementation steps
To add real-time document collaboration with WebViewer:
Step 1: Choose your preferred web stack.
Step 2: Add the ES6 JavaScript sample code provided in this guide.
Once you generate your license key, it will automatically be included in your sample code below.
Apryse collects some data regarding your usage of the SDK for product improvement.
The data that Apryse collects include:
For clarity, no other data is collected by the SDK and Apryse has no access to the contents of your documents.
If you wish to continue without data collection, contact us and we will email you a no-tracking trial key for you to get started.
1// ES6 Compliant Syntax
2// GitHub Copilot v1.0, Claude Sonnet 4, October 28, 2025
3// File: index.js
4
5import WebViewer, { Core } from '@pdftron/webviewer';
6
7// Document Collaboration Demo
8// This code demonstrates how to embed real-time document collaboration in WebViewer, supporting user permissions and syncing of annotation data.
9
10const viewers = [
11 { elementId: 'leftPanel' },
12 { elementId: 'rightPanel' },
13];
14
15// Example users for each viewer
16// Ruby is using the left viewer, Cedrick is using the right viewer
17const userList = {
18 Ruby: { permissions: 'user', canView: true, hidden: [] },
19 Cedrick: { permissions: 'user', canView: true, hidden: [] },
20};
21
22// This will store WebViewer instances for each user
23let userInstances = {
24 Ruby: null,
25 Cedrick: null,
26};
27
28// URL and history management
29const searchParams = new URLSearchParams(window.location.search);
30const history = window.history || window.parent.history || window.top.history;
31
32// License key for WebViewer and PDF worker
33const licenseKey = 'YOUR_WEBVIEWER_LICENSE_KEY';
34
35// Normally, the webviewer automatically creates and manages the PDF worker internally
36// Since multiple WebViewer instances are being created,
37// implicitly create the worker and share it across all instances to save resources
38let workerTransportPromise = null;
39
40// Initialize webviewer function
41const initializeWebViewer = (viewerElement, viewerUser) => {
42 return new Promise((resolve, reject) => {
43 // This code initializes the WebViewer with the basic settings
44 WebViewer({
45 path: '/lib',
46 licenseKey: licenseKey,
47 useDownloader: false
48 }, viewerElement).then((instance) => {
49 // Enable the measurement toolbar so it appears with all the other tools, and disable Cloudy rectangular tool
50 const cloudyTools = [
51 instance.Core.Tools.ToolNames.CLOUDY_RECTANGULAR_AREA_MEASUREMENT,
52 instance.Core.Tools.ToolNames.CLOUDY_RECTANGULAR_AREA_MEASUREMENT2,
53 instance.Core.Tools.ToolNames.CLOUDY_RECTANGULAR_AREA_MEASUREMENT3,
54 instance.Core.Tools.ToolNames.CLOUDY_RECTANGULAR_AREA_MEASUREMENT4,
55 ];
56 instance.UI.enableFeatures([instance.UI.Feature.Measurement, instance.UI.Feature.Initials]);
57 instance.UI.disableTools(cloudyTools);
58
59 // Set default toolbar group to Annotate
60 instance.UI.setToolbarGroup('toolbarGroup-Annotate');
61
62 // Set default tool on mobile devices to Pan.
63 if (UIElements.isMobileDevice()) {
64 instance.UI.setToolMode(instance.Core.Tools.ToolNames.PAN);
65 }
66
67 instance.Core.documentViewer.addEventListener('documentUnloaded', () => {
68 if (searchParams.has('file')) {
69 searchParams.delete('file');
70 history.replaceState(null, '', '?' + searchParams.toString());
71 }
72 });
73
74 instance.Core.annotationManager.enableAnnotationNumbering();
75 instance.UI.NotesPanel.enableAttachmentPreview();
76
77 // Add the demo-specific functionality
78 customizeUI(instance, viewerUser).then(resolve).catch(reject);
79 }).catch(reject);
80 });
81};
82
83const customizeUI = async (instance, viewerUser) => {
84 // Store the instance for this user
85 userInstances[viewerUser] = instance;
86
87 // Since multiple WebViewer instances are being created,
88 // Share the PDF worker across all instances to save resources
89 instance.Core.setWorkerTransportPromise(workerTransportPromise);
90
91 // Customize UI elements to show only relevant tools
92 instance.UI.disableElements([
93 'freeHandToolButton',
94 'freeHandHighlightToolButton',
95 'toolbarGroup-Insert',
96 'toolbarGroup-Measure',
97 'toolbarGroup-Edit',
98 'toolbarGroup-Forms',
99 'toolbarGroup-FillAndSign',
100 'toolbarGroup-EditText',
101 ]);
102 instance.UI.setActiveRibbonItem('ribbonGroup-Annotate');
103
104 // Load the default document
105 await instance.UI.loadDocument('https://apryse.s3.us-west-1.amazonaws.com/public/files/samples/WebviewerDemoDoc.pdf');
106
107 return new Promise((resolve) => {
108 // Set up the mentions user data using the userList defined above
109 // This allows users to mention each other in comments using @username
110 const userData = Object.keys(userList).map((user) => ({
111 value: user,
112 email: `${user.toLowerCase()}@apryse.com`,
113 }));
114 instance.UI.mentions.setUserData(userData);
115
116 // Set the current user for the annotation manager
117 const { annotationManager } = instance.Core;
118 annotationManager.setCurrentUser(viewerUser);
119 annotationManager.disableReadOnlyMode();
120 annotationManager.demoteUserFromAdmin();
121
122 const allAnnots = annotationManager.getAnnotationsList();
123 annotationManager.showAnnotations(allAnnots);
124
125 // Configure annotation change listener to store export xfdf strings for each user
126 annotationManager.addEventListener('annotationChanged', (annotations, action) => {
127 if (action === 'add' || action === 'modify' || action === 'delete') {
128 annotationManager.exportAnnotationCommand().then((xfdfString) => {
129 syncAnnotations(xfdfString, viewerUser);
130 });
131 }
132 });
133
134 // Default annotation tool: Highlight text
135 instance.UI.setToolMode(instance.Core.Tools.ToolNames.HIGHLIGHT);
136
137 resolve();
138 });
139};
140
141// Function to sync annotations across all user instances except the one who made the change
142const syncAnnotations = (xfdfString, viewerUser) => {
143 Object.keys(userInstances).forEach((user) => {
144 if (user === viewerUser) return; // Skip syncing to self
145 else {
146 const instance = userInstances[user];
147 instance.Core.annotationManager.importAnnotationCommand(xfdfString).then(() => {
148 instance.Core.documentViewer.refreshAll();
149 instance.Core.documentViewer.updateView();
150 });
151 }
152 });
153};
154
155// Cleanup function for when the demo is closed or page is unloaded
156const cleanup = (instance) => {
157 if (typeof instance !== 'undefined' && instance.UI) {
158 if (instance.Core.documentViewer.getDocument()) {
159 // Insert any other cleanup code here
160 }
161 console.log('Cleaning up document-collaboration demo');
162 }
163};
164
165// Register cleanup for page unload
166window.addEventListener('beforeunload', () => cleanup(instance));
167window.addEventListener('unload', () => cleanup(instance));
168
169// Helper function to load the ui-elements.js script
170function loadUIElementsScript() {
171 return new Promise((resolve, reject) => {
172 if (window.UIElements) {
173 console.log('UIElements already loaded');
174 resolve();
175 return;
176 }
177
178 const script = document.createElement('script');
179 script.src = '/showcase-demos/document-collaboration/ui-elements.js';
180 script.onload = function () {
181 console.log('✅ UIElements script loaded successfully');
182 resolve();
183 };
184 script.onerror = function () {
185 console.error('Failed to load UIElements script');
186 reject(new Error('Failed to load ui-elements.js'));
187 };
188 document.head.appendChild(script);
189 });
190}
191
192// Initialize both viewers side by side
193const initializeViewers = async () => {
194 try {
195 const viewerElement = document.getElementById('viewer');
196
197 // Create left and right panels for two viewers
198 const leftPanel = document.createElement('div');
199 leftPanel.id = viewers[0].elementId;
200 viewerElement.appendChild(leftPanel);
201
202 const rightPanel = document.createElement('div');
203 rightPanel.id = viewers[1].elementId;
204 viewerElement.appendChild(rightPanel);
205
206 // Set worker path
207 Core.setWorkerPath('./lib/core');
208
209 // Get backend type and wait for worker transport to initialize
210 const pdftype = await Core.getDefaultBackendType();
211 Core.preloadPDFWorker(pdftype);
212 workerTransportPromise = Core.initPDFWorkerTransports(pdftype, {}, licenseKey);
213
214 // Initialize both WebViewer instances
215 await Promise.all([
216 initializeWebViewer(leftPanel, Object.keys(userList)[0]), // Ruby
217 initializeWebViewer(rightPanel, Object.keys(userList)[1]) // Cedrick
218 ]);
219 } catch (error) {
220 console.error('❌ Error initializing viewers:', error);
221 }
222};
223
224// Load UIElements script first, then initialize WebViewer
225loadUIElementsScript().then(() => {
226 initializeViewers();
227}).catch((error) => {
228 console.error('Failed to load UIElements:', error);
229});
230
1/* Styles for the document collaboration demo */
2
3/* Left panel styles */
4#leftPanel {
5 width: 50%;
6 height: 100%;
7 float: left;
8}
9
10/* Right panel styles */
11#rightPanel {
12 width: 50%;
13 height: 100%;
14 float: right;
15}
16
17/* Viewer container styles */
18#viewer {
19 display: flex;
20 gap: 10px;
21}
22
Did you find this helpful?
Trial setup questions?
Ask experts on DiscordNeed other help?
Contact SupportPricing or product questions?
Contact Sales