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 DiscordNeed other help?
Contact SupportPricing or product questions?
Contact Sales