// ES6 Compliant Syntax
// GitHub Copilot v1.0, Claude Sonnet 4, October 28, 2025
// File: index.js
import WebViewer, { Core } from '@pdftron/webviewer';
// Document Collaboration Demo
// This code demonstrates how to embed real-time document collaboration in WebViewer, supporting user permissions and syncing of annotation data.
const viewers = [
{ elementId: 'leftPanel' },
{ elementId: 'rightPanel' },
];
// Example users for each viewer
// Ruby is using the left viewer, Cedrick is using the right viewer
const userList = {
Ruby: { permissions: 'user', canView: true, hidden: [] },
Cedrick: { permissions: 'user', canView: true, hidden: [] },
};
// This will store WebViewer instances for each user
let userInstances = {
Ruby: null,
Cedrick: null,
};
// URL and history management
const searchParams = new URLSearchParams(window.location.search);
const history = window.history || window.parent.history || window.top.history;
// License key for WebViewer and PDF worker
const licenseKey = 'YOUR_WEBVIEWER_LICENSE_KEY';
// Normally, the webviewer automatically creates and manages the PDF worker internally
// Since multiple WebViewer instances are being created,
// implicitly create the worker and share it across all instances to save resources
let workerTransportPromise = null;
// Initialize webviewer function
const initializeWebViewer = (viewerElement, viewerUser) => {
return new Promise((resolve, reject) => {
// This code initializes the WebViewer with the basic settings
WebViewer({
path: '/lib',
licenseKey: licenseKey,
useDownloader: false
}, viewerElement).then((instance) => {
// Enable the measurement toolbar so it appears with all the other tools, and disable Cloudy rectangular tool
const cloudyTools = [
instance.Core.Tools.ToolNames.CLOUDY_RECTANGULAR_AREA_MEASUREMENT,
instance.Core.Tools.ToolNames.CLOUDY_RECTANGULAR_AREA_MEASUREMENT2,
instance.Core.Tools.ToolNames.CLOUDY_RECTANGULAR_AREA_MEASUREMENT3,
instance.Core.Tools.ToolNames.CLOUDY_RECTANGULAR_AREA_MEASUREMENT4,
];
instance.UI.enableFeatures([instance.UI.Feature.Measurement, instance.UI.Feature.Initials]);
instance.UI.disableTools(cloudyTools);
// Set default toolbar group to Annotate
instance.UI.setToolbarGroup('toolbarGroup-Annotate');
// Set default tool on mobile devices to Pan.
if (UIElements.isMobileDevice()) {
instance.UI.setToolMode(instance.Core.Tools.ToolNames.PAN);
}
instance.Core.documentViewer.addEventListener('documentUnloaded', () => {
if (searchParams.has('file')) {
searchParams.delete('file');
history.replaceState(null, '', '?' + searchParams.toString());
}
});
instance.Core.annotationManager.enableAnnotationNumbering();
instance.UI.NotesPanel.enableAttachmentPreview();
// Add the demo-specific functionality
customizeUI(instance, viewerUser).then(resolve).catch(reject);
}).catch(reject);
});
};
const customizeUI = async (instance, viewerUser) => {
// Store the instance for this user
userInstances[viewerUser] = instance;
// Since multiple WebViewer instances are being created,
// Share the PDF worker across all instances to save resources
instance.Core.setWorkerTransportPromise(workerTransportPromise);
// Customize UI elements to show only relevant tools
instance.UI.disableElements([
'freeHandToolButton',
'freeHandHighlightToolButton',
'toolbarGroup-Insert',
'toolbarGroup-Measure',
'toolbarGroup-Edit',
'toolbarGroup-Forms',
'toolbarGroup-FillAndSign',
'toolbarGroup-EditText',
]);
instance.UI.setActiveRibbonItem('ribbonGroup-Annotate');
// Load the default document
await instance.UI.loadDocument('https://apryse.s3.us-west-1.amazonaws.com/public/files/samples/WebviewerDemoDoc.pdf');
return new Promise((resolve) => {
// Set up the mentions user data using the userList defined above
// This allows users to mention each other in comments using @username
const userData = Object.keys(userList).map((user) => ({
value: user,
email: `${user.toLowerCase()}@apryse.com`,
}));
instance.UI.mentions.setUserData(userData);
// Set the current user for the annotation manager
const { annotationManager } = instance.Core;
annotationManager.setCurrentUser(viewerUser);
annotationManager.disableReadOnlyMode();
annotationManager.demoteUserFromAdmin();
const allAnnots = annotationManager.getAnnotationsList();
annotationManager.showAnnotations(allAnnots);
// Configure annotation change listener to store export xfdf strings for each user
annotationManager.addEventListener('annotationChanged', (annotations, action) => {
if (action === 'add' || action === 'modify' || action === 'delete') {
annotationManager.exportAnnotationCommand().then((xfdfString) => {
syncAnnotations(xfdfString, viewerUser);
});
}
});
// Default annotation tool: Highlight text
instance.UI.setToolMode(instance.Core.Tools.ToolNames.HIGHLIGHT);
resolve();
});
};
// Function to sync annotations across all user instances except the one who made the change
const syncAnnotations = (xfdfString, viewerUser) => {
Object.keys(userInstances).forEach((user) => {
if (user === viewerUser) return; // Skip syncing to self
else {
const instance = userInstances[user];
instance.Core.annotationManager.importAnnotationCommand(xfdfString).then(() => {
instance.Core.documentViewer.refreshAll();
instance.Core.documentViewer.updateView();
});
}
});
};
// Cleanup function for when the demo is closed or page is unloaded
const cleanup = (instance) => {
if (typeof instance !== 'undefined' && instance.UI) {
if (instance.Core.documentViewer.getDocument()) {
// Insert any other cleanup code here
}
console.log('Cleaning up document-collaboration demo');
}
};
// Register cleanup for page unload
window.addEventListener('beforeunload', () => cleanup(instance));
window.addEventListener('unload', () => cleanup(instance));
// Helper function to load the ui-elements.js script
function loadUIElementsScript() {
return new Promise((resolve, reject) => {
if (window.UIElements) {
console.log('UIElements already loaded');
resolve();
return;
}
const script = document.createElement('script');
script.src = '/showcase-demos/document-collaboration/ui-elements.js';
script.onload = function () {
console.log('✅ UIElements script loaded successfully');
resolve();
};
script.onerror = function () {
console.error('Failed to load UIElements script');
reject(new Error('Failed to load ui-elements.js'));
};
document.head.appendChild(script);
});
}
// Initialize both viewers side by side
const initializeViewers = async () => {
try {
const viewerElement = document.getElementById('viewer');
// Create left and right panels for two viewers
const leftPanel = document.createElement('div');
leftPanel.id = viewers[0].elementId;
viewerElement.appendChild(leftPanel);
const rightPanel = document.createElement('div');
rightPanel.id = viewers[1].elementId;
viewerElement.appendChild(rightPanel);
// Set worker path
Core.setWorkerPath('./lib/core');
// Get backend type and wait for worker transport to initialize
const pdftype = await Core.getDefaultBackendType();
Core.preloadPDFWorker(pdftype);
workerTransportPromise = Core.initPDFWorkerTransports(pdftype, {}, licenseKey);
// Initialize both WebViewer instances
await Promise.all([
initializeWebViewer(leftPanel, Object.keys(userList)[0]), // Ruby
initializeWebViewer(rightPanel, Object.keys(userList)[1]) // Cedrick
]);
} catch (error) {
console.error('❌ Error initializing viewers:', error);
}
};
// Load UIElements script first, then initialize WebViewer
loadUIElementsScript().then(() => {
initializeViewers();
}).catch((error) => {
console.error('Failed to load UIElements:', error);
});