Realtime collaboration - Firebase

Enable real-time collaboration on PDF, DOCX, PPTX and XLSX documents using this JavaScript 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. This sample works on all browsers (including IE11) and mobile devices without using plug-ins. For more details, refer to our real-time collaboration guide or visit our collaboration demo. Note: this example is setup with a Firebase backend, but you can use whichever backend you prefer. Learn more about our Web SDK.

1const IDS = {
2 'https://pdftron.s3.amazonaws.com/downloads/pl/demo-annotated.pdf': 'foo-12',
3 'https://pdftron.s3.amazonaws.com/downloads/pl/report.docx': 'foo-13',
4 'https://pdftron.s3.amazonaws.com/downloads/pl/presentation.pptx': 'foo-14',
5};
6
7// eslint-disable-next-line no-undef
8const server = new Server();
9const initialDoc = 'https://pdftron.s3.amazonaws.com/downloads/pl/demo-annotated.pdf';
10
11// NOTE: not for production use, delete this function when in production
12const periodicallyRemoveOldAnnotations = data => {
13 // "1 * 60 * 60 * 1000" is one hour
14 const twentyFourHours = 24 * 60 * 60 * 1000;
15 const item = data.val();
16 if (!item.timestamp) {
17 return false;
18 }
19 const now = Date.now();
20 const passedTime = now - item.timestamp;
21 // If the timestamp of the annotation is passed
22 // 24 hours we delete this annotation here.
23 if (passedTime > twentyFourHours) {
24 server.deleteAnnotation(data.key);
25 return true;
26 }
27 return false;
28};
29
30// eslint-disable-next-line no-undef
31const WebViewerConstructor = isWebComponent() ? WebViewer.WebComponent : WebViewer;
32
33WebViewerConstructor(
34 {
35 path: '../../../lib',
36 initialDoc,
37 documentId: IDS[initialDoc],
38 },
39 document.getElementById('viewer')
40).then(instance => {
41 samplesSetup(instance);
42
43 const { documentViewer, annotationManager } = instance.Core;
44
45 let authorId = null;
46 const urlInput = document.getElementById('url');
47 const copyButton = document.getElementById('copy');
48 instance.UI.openElements(['notesPanel']);
49
50 let hasSeenPopup = false;
51
52 if (window.location.origin === 'http://localhost:3000') {
53 const xhttp = new XMLHttpRequest();
54 xhttp.onreadystatechange = () => {
55 if (xhttp.readyState === 4 && xhttp.status === 200) {
56 urlInput.value = `http://${xhttp.responseText}:3000/samples/annotation/realtime-collaboration/`;
57 }
58 };
59 xhttp.open('GET', '/ip', true);
60 xhttp.send();
61 } else {
62 urlInput.value = 'https://docs.apryse.com/samples/web/samples/annotation/realtime-collaboration/';
63 }
64
65 copyButton.onclick = () => {
66 urlInput.select();
67 document.execCommand('copy');
68 document.getSelection().empty();
69 };
70
71 documentViewer.addEventListener('documentLoaded', () => {
72 const documentId = documentViewer.getDocument().getDocumentId();
73
74 server.selectDocument(documentId);
75
76 const onAnnotationCreated = async data => {
77 // NOTE: not for production use, delete this "if block" when in production
78 if (periodicallyRemoveOldAnnotations(data)) {
79 return;
80 }
81
82 // Import the annotation based on xfdf command
83 const annotations = await annotationManager.importAnnotationCommand(data.val().xfdf);
84 const annotation = annotations[0];
85 if (annotation) {
86 await annotation.resourcesLoaded();
87 // Set a custom field authorId to be used in client-side permission check
88 annotation.authorId = data.val().authorId;
89 annotationManager.redrawAnnotation(annotation);
90 // viewerInstance.fireEvent('updateAnnotationPermission', [annotation]); //TODO
91 }
92 };
93
94 const onAnnotationUpdated = async data => {
95 // Import the annotation based on xfdf command
96 const annotations = await annotationManager.importAnnotationCommand(data.val().xfdf);
97 const annotation = annotations[0];
98 if (annotation) {
99 await annotation.resourcesLoaded();
100 // Set a custom field authorId to be used in client-side permission check
101 annotation.authorId = data.val().authorId;
102 annotationManager.redrawAnnotation(annotation);
103 }
104 };
105
106 const onAnnotationDeleted = data => {
107 // data.key would return annotationId since our server method is designed as
108 // annotationsRef.child(annotationId).set(annotationData)
109 const command = `<delete><id>${data.key}</id></delete>`;
110 annotationManager.importAnnotationCommand(command);
111 };
112
113 const openReturningAuthorPopup = authorName => {
114 if (hasSeenPopup) {
115 return;
116 }
117 // The author name will be used for both WebViewer and annotations in PDF
118 annotationManager.setCurrentUser(authorName);
119 // Open popup for the returning author
120 window.alert(`Welcome back ${authorName}`);
121 hasSeenPopup = true;
122 };
123
124 const updateAuthor = authorName => {
125 // The author name will be used for both WebViewer and annotations in PDF
126 annotationManager.setCurrentUser(authorName);
127 // Create/update author information in the server
128 server.updateAuthor(authorId, { authorName });
129 };
130
131 const openNewAuthorPopup = () => {
132 // Open prompt for a new author
133 const name = window.prompt('Welcome! Tell us your name :)');
134 if (name) {
135 updateAuthor(name);
136 }
137 };
138
139 // Bind server-side authorization state change to a callback function
140 // The event is triggered in the beginning as well to check if author has already signed in
141 server.bind('onAuthStateChanged', user => {
142 // Author is logged in
143 if (user) {
144 // Using uid property from Firebase Database as an author id
145 // It is also used as a reference for server-side permission
146 authorId = user.uid;
147 // Check if author exists, and call appropriate callback functions
148 server.checkAuthor(authorId, openReturningAuthorPopup, openNewAuthorPopup);
149 // Bind server-side data events to callback functions
150 // When loaded for the first time, onAnnotationCreated event will be triggered for all database entries
151 server.bind('onAnnotationCreated', onAnnotationCreated);
152 server.bind('onAnnotationUpdated', onAnnotationUpdated);
153 server.bind('onAnnotationDeleted', onAnnotationDeleted);
154 } else {
155 // Author is not logged in
156 server.signInAnonymously();
157 }
158 });
159 });
160
161 // Bind annotation change events to a callback function
162 annotationManager.addEventListener('annotationChanged', async (annotations, type, info) => {
163 // info.imported is true by default for annotations from pdf and annotations added by importAnnotationCommand
164 if (info.imported) {
165 return;
166 }
167
168 const xfdf = await annotationManager.exportAnnotationCommand();
169 // Iterate through all annotations and call appropriate server methods
170 annotations.forEach(annotation => {
171 let parentAuthorId = null;
172 if (type === 'add') {
173 // In case of replies, add extra field for server-side permission to be granted to the
174 // parent annotation's author
175 if (annotation.InReplyTo) {
176 parentAuthorId = annotationManager.getAnnotationById(annotation.InReplyTo).authorId || 'default';
177 }
178
179 if (authorId) {
180 annotation.authorId = authorId;
181 }
182
183 server.createAnnotation(annotation.Id, {
184 authorId,
185 parentAuthorId,
186 xfdf,
187 timestamp: Date.now(),
188 });
189 } else if (type === 'modify') {
190 // In case of replies, add extra field for server-side permission to be granted to the
191 // parent annotation's author
192 if (annotation.InReplyTo) {
193 parentAuthorId = annotationManager.getAnnotationById(annotation.InReplyTo).authorId || 'default';
194 }
195 server.updateAnnotation(annotation.Id, {
196 authorId,
197 parentAuthorId,
198 xfdf,
199 });
200 } else if (type === 'delete') {
201 server.deleteAnnotation(annotation.Id);
202 }
203 });
204 });
205
206 // Overwrite client-side permission check method on the annotation manager
207 // The default was set to compare the authorName
208 // Instead of the authorName, we will compare authorId created from the server
209 // Note that authorId can be undefined when annotation has just been created, need to handle it for fixing WVR-3217
210 annotationManager.setPermissionCheckCallback((author, annotation) => !annotation.authorId || annotation.authorId === authorId);
211
212 document.getElementById('select').onchange = e => {
213 const documentId = IDS[e.target.value];
214 instance.UI.loadDocument(e.target.value, { documentId });
215 };
216});

Did you find this helpful?

Trial setup questions?

Ask experts on Discord

Need other help?

Contact Support

Pricing or product questions?

Contact Sales