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 document from 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.

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

Did you find this helpful?

Trial setup questions?

Ask experts on Discord

Need other help?

Contact Support

Pricing or product questions?

Contact Sales