Document Collaboration Showcase Demo Code Sample

Requirements
View Demo

Easily integrate real-time document collaboration into your application with synchronized user permissions, annotations, comments, and status updates.

This demo allows you to:

  • Add users and their permissions to edit documents. In this sample: "Ruby" and "Cedrick".
  • Display two viewers that load a document from the same URL.
  • Add edits on one document and view the same on the other document:
    • Annotations
    • Shapes
    • Comments
    • Status icons

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.

License Key

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

Did you find this helpful?

Trial setup questions?

Ask experts on Discord

Need other help?

Contact Support

Pricing or product questions?

Contact Sales