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
PDF, DOCX, PPTX, XLSX Document Collaboration with Firebase, JavaScript Sample Code | Apryse documentation