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_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 fullAPI: true,
49 }, viewerElement).then((instance) => {
50 // Enable the measurement toolbar so it appears with all the other tools, and disable Cloudy rectangular tool.
51 const cloudyTools = [
52 instance.Core.Tools.ToolNames.CLOUDY_RECTANGULAR_AREA_MEASUREMENT,
53 instance.Core.Tools.ToolNames.CLOUDY_RECTANGULAR_AREA_MEASUREMENT2,
54 instance.Core.Tools.ToolNames.CLOUDY_RECTANGULAR_AREA_MEASUREMENT3,
55 instance.Core.Tools.ToolNames.CLOUDY_RECTANGULAR_AREA_MEASUREMENT4,
56 ];
57 instance.UI.enableFeatures([instance.UI.Feature.Measurement, instance.UI.Feature.Initials]);
58 instance.UI.disableTools(cloudyTools);
59
60 // Set default toolbar group to Annotate.
61 instance.UI.setToolbarGroup('toolbarGroup-Annotate');
62
63 // Set default tool on mobile devices to Pan.
64 if (UIElements.isMobileDevice()) {
65 instance.UI.setToolMode(instance.Core.Tools.ToolNames.PAN);
66 }
67
68 instance.Core.documentViewer.addEventListener('documentUnloaded', () => {
69 if (searchParams.has('file')) {
70 searchParams.delete('file');
71 history.replaceState(null, '', '?' + searchParams.toString());
72 }
73 });
74
75 instance.Core.annotationManager.enableAnnotationNumbering();
76 instance.UI.NotesPanel.enableAttachmentPreview();
77
78 // Add the demo-specific functionality.
79 customizeUI(instance, viewerUser).then(resolve).catch(reject);
80 }).catch(reject);
81 });
82};
83
84const customizeUI = async (instance, viewerUser) => {
85 // Store the instance for this user.
86 userInstances[viewerUser] = instance;
87
88 // Since multiple WebViewer instances are being created,
89 // share the PDF worker across all instances to save resources.
90 instance.Core.setWorkerTransportPromise(workerTransportPromise);
91
92 // Customize UI elements to show only relevant tools.
93 instance.UI.disableElements([
94 'freeHandToolButton',
95 'freeHandHighlightToolButton',
96 'toolbarGroup-Insert',
97 'toolbarGroup-Measure',
98 'toolbarGroup-Edit',
99 'toolbarGroup-Forms',
100 'toolbarGroup-FillAndSign',
101 'toolbarGroup-EditText',
102 ]);
103 instance.UI.setActiveRibbonItem('ribbonGroup-Annotate');
104
105 // Load the default document.
106 await instance.UI.loadDocument('https://apryse.s3.us-west-1.amazonaws.com/public/files/samples/WebviewerDemoDoc.pdf');
107
108 return new Promise((resolve) => {
109 // Set up the mentions user data using the userList defined above.
110 // This allows users to mention each other in comments using @username.
111 const userData = Object.keys(userList).map((user) => ({
112 value: user,
113 email: `${user.toLowerCase()}@apryse.com`,
114 }));
115 instance.UI.mentions.setUserData(userData);
116
117 // Set the current user for the annotation manager.
118 const { annotationManager } = instance.Core;
119 annotationManager.setCurrentUser(viewerUser);
120 annotationManager.disableReadOnlyMode();
121 annotationManager.demoteUserFromAdmin();
122
123 const allAnnots = annotationManager.getAnnotationsList();
124 annotationManager.showAnnotations(allAnnots);
125
126 // Configure annotation change listener to store export xfdf strings for each user.
127 annotationManager.addEventListener('annotationChanged', (annotations, action) => {
128 if (action === 'add' || action === 'modify' || action === 'delete') {
129 annotationManager.exportAnnotationCommand().then((xfdfString) => {
130 syncAnnotations(xfdfString, viewerUser);
131 });
132 }
133 });
134
135 // Default annotation tool: Highlight text.
136 instance.UI.setToolMode(instance.Core.Tools.ToolNames.HIGHLIGHT);
137
138 resolve();
139 });
140};
141
142// Function to sync annotations across all user instances except the one who made the change.
143const syncAnnotations = (xfdfString, viewerUser) => {
144 Object.keys(userInstances).forEach((user) => {
145 if (user === viewerUser) return; // Skip syncing to self.
146 else {
147 const instance = userInstances[user];
148 instance.Core.annotationManager.importAnnotationCommand(xfdfString).then(() => {
149 instance.Core.documentViewer.refreshAll();
150 instance.Core.documentViewer.updateView();
151 });
152 }
153 });
154};
155
156// Cleanup function for when the demo is closed or page is unloaded.
157const cleanup = (instance) => {
158 if (typeof instance !== 'undefined' && instance.UI) {
159 if (instance.Core.documentViewer.getDocument()) {
160 // Insert any other cleanup code here.
161 }
162 console.log('Cleaning up document-collaboration demo');
163 }
164};
165
166// Register cleanup for page unload.
167window.addEventListener('beforeunload', () => cleanup(instance));
168window.addEventListener('unload', () => cleanup(instance));
169
170// Helper function to load the ui-elements.js script.
171function loadUIElementsScript() {
172 return new Promise((resolve, reject) => {
173 if (window.UIElements) {
174 console.log('UIElements already loaded');
175 resolve();
176 return;
177 }
178
179 const script = document.createElement('script');
180 script.src = '/showcase-demos/document-collaboration/ui-elements.js';
181 script.onload = function () {
182 console.log('✅ UIElements script loaded successfully');
183 resolve();
184 };
185 script.onerror = function () {
186 console.error('Failed to load UIElements script');
187 reject(new Error('Failed to load ui-elements.js'));
188 };
189 document.head.appendChild(script);
190 });
191}
192
193// Initialize both viewers side by side.
194const initializeViewers = async () => {
195 try {
196 const viewerElement = document.getElementById('viewer');
197
198 // Create left and right panels for two viewers.
199 const leftPanel = document.createElement('div');
200 leftPanel.id = viewers[0].elementId;
201 viewerElement.appendChild(leftPanel);
202
203 const rightPanel = document.createElement('div');
204 rightPanel.id = viewers[1].elementId;
205 viewerElement.appendChild(rightPanel);
206
207 // Set worker path.
208 Core.setWorkerPath('./lib/core');
209
210 // Get backend type and wait for worker transport to initialize.
211 const pdftype = await Core.getDefaultBackendType();
212 Core.preloadPDFWorker(pdftype);
213 workerTransportPromise = Core.initPDFWorkerTransports(pdftype, {}, licenseKey);
214
215 // Initialize both WebViewer instances.
216 await Promise.all([
217 initializeWebViewer(leftPanel, Object.keys(userList)[0]), // Ruby
218 initializeWebViewer(rightPanel, Object.keys(userList)[1]) // Cedrick
219 ]);
220 } catch (error) {
221 console.error('❌ Error initializing viewers:', error);
222 }
223};
224
225// Load UIElements script first, then initialize WebViewer.
226loadUIElementsScript().then(() => {
227 initializeViewers();
228}).catch((error) => {
229 console.error('Failed to load UIElements:', error);
230});
231
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