Enable real-time collaboration on PDF documents using this PDF.js Express sample. When a user creates a new annotation it will immediately be displayed in another user’s browser, where they can reply to annotations in real-time by adding their own comments. Note: this example is setup with a Firebase backend, but you can use whichever backend you prefer. View demo
Note: This sample uses a custom Server
class created in this guide.
// eslint-disable-next-line no-undef
const server = new Server();
WebViewer({
path: '/static/WebViewer/lib/',
initialDoc: 'https://pdftron.s3.amazonaws.com/downloads/pl/demo-annotated.pdf',
}, document.getElementById('viewer'))
.then((instance) => {
const { docViewer, annotManager } = instance;
const urlInput = document.getElementById('url');
const copyButton = document.getElementById('copy');
instance.openElement('notesPanel');
if (window.location.origin === 'http://localhost:3000') {
const xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = () => {
if (xhttp.readyState === 4 && xhttp.status === 200) {
urlInput.value = `http://${xhttp.responseText}:3000/samples/annotation/realtime-collaboration/`;
}
};
xhttp.open('GET', '/ip', true);
xhttp.send();
} else {
urlInput.value = 'https://pdfjs.express/documentation/samples/realtime-collaboration/';
}
copyButton.onclick = () => {
urlInput.select();
document.execCommand('copy');
document.getSelection().empty();
};
docViewer.on('documentLoaded', () => {
let authorId = null;
async function onAnnotationCreated(data) {
// Import the annotation based on xfdf command
const [annotation] = await annotManager.importAnnotCommand(data.val().xfdf);
// Set a custom field authorId to be used in client-side permission check
annotation.authorId = data.val().authorId;
annotManager.redrawAnnotation(annotation);
}
async function onAnnotationUpdated(data) {
// Import the annotation based on xfdf command
const [annotation] = await annotManager.importAnnotCommand(data.val().xfdf);
// Set a custom field authorId to be used in client-side permission check
annotation.authorId = data.val().authorId;
annotManager.redrawAnnotation(annotation);
}
function onAnnotationDeleted(data) {
// data.key would return annotationId since our server method is designed as
// annotationsRef.child(annotationId).set(annotationData)
const command = `<delete><id>${data.key}</id></delete>`;
annotManager.importAnnotCommand(command).then(importedAnnotations => {});
}
function openReturningAuthorPopup(authorName) {
// The author name will be used for both WebViewer and annotations in PDF
annotManager.setCurrentUser(authorName);
// Open popup for the returning author
window.alert(`Welcome back ${authorName}`);
}
function updateAuthor(authorName) {
// The author name will be used for both WebViewer and annotations in PDF
annotManager.setCurrentUser(authorName);
// Create/update author information in the server
server.updateAuthor(authorId, { authorName });
}
function openNewAuthorPopup() {
// Open prompt for a new author
const name = window.prompt('Welcome! Tell us your name :)');
if (name) {
updateAuthor(name);
}
}
// Bind server-side authorization state change to a callback function
// The event is triggered in the beginning as well to check if author has already signed in
server.bind('onAuthStateChanged', (user) => {
// Author is logged in
if (user) {
// Using uid property from Firebase Database as an author id
// It is also used as a reference for server-side permission
authorId = user.uid;
// Check if author exists, and call appropriate callback functions
server.checkAuthor(authorId, openReturningAuthorPopup, openNewAuthorPopup);
// Bind server-side data events to callback functions
// When loaded for the first time, onAnnotationCreated event will be triggered for all database entries
server.bind('onAnnotationCreated', onAnnotationCreated);
server.bind('onAnnotationUpdated', onAnnotationUpdated);
server.bind('onAnnotationDeleted', onAnnotationDeleted);
} else {
// Author is not logged in
server.signInAnonymously();
}
});
// Bind annotation change events to a callback function
annotManager.on('annotationChanged', (annotations, type, e) => {
// e.imported is true by default for annotations from pdf and annotations added by importAnnotCommand
if (e.imported) {
return;
}
// Iterate through all annotations and call appropriate server methods
annotations.forEach((annotation) => {
annotManager.exportAnnotCommand().then( xfdf => {
let parentAuthorId = null;
if (type === 'add') {
// In case of replies, add extra field for server-side permission to be granted to the
// parent annotation's author
if (annotation.InReplyTo) {
parentAuthorId = annotManager.getAnnotationById(annotation.InReplyTo).authorId || 'default';
}
server.createAnnotation(annotation.Id, {
authorId,
parentAuthorId,
xfdf,
});
} else if (type === 'modify') {
// In case of replies, add extra field for server-side permission to be granted to the
// parent annotation's author
if (annotation.InReplyTo) {
parentAuthorId = annotManager.getAnnotationById(annotation.InReplyTo).authorId || 'default';
}
server.updateAnnotation(annotation.Id, {
authorId,
parentAuthorId,
xfdf,
});
} else if (type === 'delete') {
server.deleteAnnotation(annotation.Id);
}
});
});
});
// Overwrite client-side permission check method on the annotation manager
// The default was set to compare the authorName
// Instead of the authorName, we will compare authorId created from the server
annotManager.setPermissionCheckCallback((author, annotation) => annotation.authorId === authorId);
});
});
const server = new Server();
WebViewer({
path: '/static/WebViewer/lib/',
initialDoc: 'https://pdftron.s3.amazonaws.com/downloads/pl/demo-annotated.pdf',
}, document.getElementById('viewer'))
.then((instance) => {
const { documentViewer, annotationManager } = instance.Core;
const urlInput = document.getElementById('url');
const copyButton = document.getElementById('copy');
instance.UI.openElement('notesPanel');
if (window.location.origin === 'http://localhost:3000') {
const xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = () => {
if (xhttp.readyState === 4 && xhttp.status === 200) {
urlInput.value = `http://${xhttp.responseText}:3000/samples/annotation/realtime-collaboration/`;
}
};
xhttp.open('GET', '/ip', true);
xhttp.send();
} else {
urlInput.value = 'https://pdfjs.express/documentation/samples/realtime-collaboration/';
}
copyButton.onclick = () => {
urlInput.select();
document.execCommand('copy');
document.getSelection().empty();
};
documentViewer.addEventListener('documentLoaded', () => {
let authorId = null;
async function onAnnotationCreated(data) {
// Import the annotation based on xfdf command
const [annotation] = await annotationManager.importAnnotCommand(data.val().xfdf);
// Set a custom field authorId to be used in client-side permission check
annotation.authorId = data.val().authorId;
annotationManager.redrawAnnotation(annotation);
}
async function onAnnotationUpdated(data) {
// Import the annotation based on xfdf command
const [annotation] = await annotationManager.importAnnotCommand(data.val().xfdf);
// Set a custom field authorId to be used in client-side permission check
annotation.authorId = data.val().authorId;
annotationManager.redrawAnnotation(annotation);
}
function onAnnotationDeleted(data) {
// data.key would return annotationId since our server method is designed as
// annotationsRef.child(annotationId).set(annotationData)
const command = `<delete><id>${data.key}</id></delete>`;
annotationManager.importAnnotCommand(command).then(importedAnnotations => {});
}
function openReturningAuthorPopup(authorName) {
// The author name will be used for both WebViewer and annotations in PDF
annotationManager.setCurrentUser(authorName);
// Open popup for the returning author
window.alert(`Welcome back ${authorName}`);
}
function updateAuthor(authorName) {
// The author name will be used for both WebViewer and annotations in PDF
annotationManager.setCurrentUser(authorName);
// Create/update author information in the server
server.updateAuthor(authorId, { authorName });
}
function openNewAuthorPopup() {
// Open prompt for a new author
const name = window.prompt('Welcome! Tell us your name :)');
if (name) {
updateAuthor(name);
}
}
// Bind server-side authorization state change to a callback function
// The event is triggered in the beginning as well to check if author has already signed in
server.bind('onAuthStateChanged', (user) => {
// Author is logged in
if (user) {
// Using uid property from Firebase Database as an author id
// It is also used as a reference for server-side permission
authorId = user.uid;
// Check if author exists, and call appropriate callback functions
server.checkAuthor(authorId, openReturningAuthorPopup, openNewAuthorPopup);
// Bind server-side data events to callback functions
// When loaded for the first time, onAnnotationCreated event will be triggered for all database entries
server.bind('onAnnotationCreated', onAnnotationCreated);
server.bind('onAnnotationUpdated', onAnnotationUpdated);
server.bind('onAnnotationDeleted', onAnnotationDeleted);
} else {
// Author is not logged in
server.signInAnonymously();
}
});
// Bind annotation change events to a callback function
annotationManager.on('annotationChanged', (annotations, type, e) => {
// e.imported is true by default for annotations from pdf and annotations added by importAnnotCommand
if (e.imported) {
return;
}
// Iterate through all annotations and call appropriate server methods
annotations.forEach((annotation) => {
annotationManager.exportAnnotCommand().then( xfdf => {
let parentAuthorId = null;
if (type === 'add') {
// In case of replies, add extra field for server-side permission to be granted to the
// parent annotation's author
if (annotation.InReplyTo) {
parentAuthorId = annotationManager.getAnnotationById(annotation.InReplyTo).authorId || 'default';
}
server.createAnnotation(annotation.Id, {
authorId,
parentAuthorId,
xfdf,
});
} else if (type === 'modify') {
// In case of replies, add extra field for server-side permission to be granted to the
// parent annotation's author
if (annotation.InReplyTo) {
parentAuthorId = annotationManager.getAnnotationById(annotation.InReplyTo).authorId || 'default';
}
server.updateAnnotation(annotation.Id, {
authorId,
parentAuthorId,
xfdf,
});
} else if (type === 'delete') {
server.deleteAnnotation(annotation.Id);
}
});
});
});
// Overwrite client-side permission check method on the annotation manager
// The default was set to compare the authorName
// Instead of the authorName, we will compare authorId created from the server
annotationManager.setPermissionCheckCallback((author, annotation) => annotation.authorId === authorId);
});
});
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<link rel="stylesheet" href="../../style.css">
<script src="/static/WebViewer/lib/webviewer.min.js"></script>
<script src='../../old-browser-checker.js'></script>
<script src="https://www.gstatic.com/firebasejs/3.5.3/firebase.js"></script>
<script src="server.js"></script>
</head>
<body>
<header>
<div className="title sample">Realtime collaboration sample</div>
</header>
<aside>
<h1>Controls</h1>
<h2>Share this url:</h2>
<input id="url" type="text" style={{"width":"calc(100% - 8px)","padding":"2px"}} readonly>
<br />
<button id="copy">Copy</button>
<hr />
<h1>Instructions</h1>
<p>Type your name when the prompt comes up. It will set your username and log you in to the realtime collaboration backend. Please note that the backend is being shared globally, so you might see annotations created by strangers.</p>
<p>Share the link with others in your local network to start collaborating!</p>
</aside>
<div id="viewer"></div>
<script src="../../menu-button.js"></script>
<script src="realtime-collaboration.js"></script>
</body>
</html>