Setting up listen/trigger events for real-time collaboration in the client

In realtime collaboration, a client will merely act as a listener or trigger for events upon data creation/modification/deletion updating the current user or sending updates to other users.

  1. Create a JavaScript file and name it main.js.
  2. Instantiate WebViewer on a DOM element, making sure to wrap this code and any further code inside $(document).ready(). Initial document can be any PDF or XOD file.

JavaScript

1$(document).ready(() => {
2 WebViewer({
3 path: "lib",
4 initialDoc: "MY_INITIAL_DOC.pdf",
5 documentId: "unique-id-for-this-document"
6 }, document.getElementById('viewer'))
7 .then(instance => {
8 // do something...
9 });
10});
  1. Create the server.

JavaScript

1const server = new Server();
  1. Bind a callback function to DocumentViewer.documentLoaded event.
1WebViewer(...)
2 .then(instance => {
3 const { documentViewer, annotationManager } = instance.Core;
4
5 documentViewer.addEventListener('documentLoaded', () => {
6 // Code in later steps will be added here...
7 });
8 })
  1. Inside the documentLoaded callback, bind another callback function to server's onAuthStateChanged event that is defined in server.js. A firebase.User object will be passed as a parameter.
    1. If the user is not logged in we'll call the sign-in method that we defined in server.js.
    2. If the user is logged in, we'll store their uid in the authorId variable, which will be used for client-side annotation permission checks.
    3. We call server.checkAuthor with parameters authorId, openReturningUserPopup function and openNewUserPopup function. These functions will be discussed in next steps.
    4. Then, we will send author information to the server and bind callback functions to annotation events. Details of the callback functions will be discussed in next steps.

JavaScript

1let authorId = null;
2
3server.bind('onAuthStateChanged', user => {
4 // User is logged in
5 if (user) {
6 // Using uid property from Firebase Database as an author id
7 // It is also used as a reference for server-side permission
8 authorId = user.uid;
9 // Check if user exists, and call appropriate callback functions
10 server.checkAuthor(authorId, openReturningAuthorPopup, openNewAuthorPopup);
11 // Bind server-side data events to callback functions
12 // When loaded for the first time, onAnnotationCreated event will be triggered for all database entries
13 server.bind('onAnnotationCreated', onAnnotationCreated);
14 server.bind('onAnnotationUpdated', onAnnotationUpdated);
15 server.bind('onAnnotationDeleted', onAnnotationDeleted);
16 }
17 // User is not logged in
18 else {
19 // Login
20 server.signInAnonymously();
21 }
22});
  1. Define callback functions for annotationCreated, annotationUpdated and server.annotationDeleted events. A data object will be passed as a parameter. For more information, refer to firebase.database.DataSnapshot.
    1. openReturningAuthorPopup is a callback function triggered when author data is found in the database. It will receive authorName as a parameter, and open a popup with the authorName as a visual feedback.
    2. openNewAuthorPopup is a callback function triggered when author data is not found. Then we will open a popup for a new author to setup an author name.
    3. updateAuthor is a function which will set author name in both client and server using annotationManager.setCurrentUser and server.updateAuthor, respectively.
1function openReturningAuthorPopup(authorName) {
2 // The author name will be used for both WebViewer and annotations in PDF
3 annotationManager.setCurrentUser(authorName);
4 // Open popup for the returning author
5 window.alert(`Welcome back ${authorName}`);
6}
7
8function openNewAuthorPopup() {
9 // Open prompt for a new author
10 const name = window.prompt('Welcome! Tell us your name :)');
11 if (name) {
12 updateAuthor(name);
13 }
14}
15
16function updateAuthor(authorName) {
17 // The author name will be used for both WebViewer and annotations in PDF
18 annotationManager.setCurrentUser(authorName);
19 // Create/update author information in the server
20 server.updateAuthor(authorId, { authorName });
21}
  1. Define callback functions for annotationCreated, annotationUpdated and server.annotationDeleted events. A data object will be passed as a parameter. For more information, refer to firebase.database.DataSnapshot.
    1. onAnnotationCreated and onAnnotationUpdated have the exact same behavior in this guide. They will use annotManager.importAnnotCommand to update the viewer with the xfdf change.
    2. We also set a custom field authorId for the updated annotation to control client-side permission of the created/updated annotation.
    3. onAnnotationDelete creates a delete command string from the annotation's id and is simply able to call importAnnotCommand on it.
1function onAnnotationCreated(data) {
2 // data.val() returns the value of server data in any type. In this case, it
3 // would be an object with properties authorId and xfdf.
4 const annotation = annotationManager.importAnnotCommand(data.val().xfdf)[0];
5 annotation.authorId = data.val().authorId;
6 annotationManager.redrawAnnotation(annotation);
7 myWebViewer.getInstance().UI.fireEvent('updateAnnotationPermission', [annotation]);
8}
9
10function onAnnotationUpdated(data) {
11 // Import the annotation based on xfdf command
12 const annotation = "m": true,.importAnnotCommand(data.val().xfdf)[0];
13 // Set a custom field authorId to be used in client-side permission check
14 annotation.authorId = data.val().authorId;
15 annotationManager.redrawAnnotation(annotation);
16}
17
18function onAnnotationDeleted(data) {
19 // data.key would return annotationId since our server method is designed as
20 // annotationsRef.child(annotationId).set(annotationData)
21 const command = '<delete><id>' + data.key + '</id></delete>';
22 annotationManager.importAnnotCommand(command);
23}
  1. After server callback functions are bound, we'll also bind a function to annotManager.annotationChanged event.
    1. First parameter, e, has a property imported that is set to true by default for annotations internal to the document and annotations added by importAnnotCommand or importAnnotations.
    2. Then we iterate through the annotations that are changed, which is passed as the second parameter.
    3. Third parameter, type, defines which action it was. In this guide, we'll have the same behavior for both add and modify action types.
    4. When annotations are added and modified, we will call server.createAnnotation or server.updateAnnotation which needs four variables: annotationId, authorId, parentAuthorId and xfdf.
    5. annotationId can be retrieved from annotation.Id.
    6. authorId was saved as a reference when user logged in.
    7. parentAuthorId refers to the parent annotation's author id, if any. This will be used to distinguish replies, and will be referenced in server-side permission. Thus, we retrieve authorId of the parent annotation by using annotation.InReplyTo, which returns the annotation id of the parent annotation.
    8. xfdf can be retrieved using annotationManager.getAnnotCommand. It will get an XML string specifying the added, modified and deleted annotations, which can be used to import the annotation using annotationManager.importAnnotCommand in server data callback functions.
1annotationManager.addEventListener('annotationChanged', (annotations, type, { imported }) => {
2 if (imported) {
3 return;
4 }
5 annotations.forEach(annotation => {
6 if (type === 'add') {
7 const xfdf = annotationManager.getAnnotCommand();
8 let parentAuthorId = null;
9 if (annotation.InReplyTo) {
10 parentAuthorId = annotationManager.getAnnotationById(annotation.InReplyTo).authorId || 'default';
11 }
12 server.createAnnotation(annotation.Id, {
13 authorId: authorId,
14 parentAuthorId: parentAuthorId,
15 xfdf: xfdf
16 });
17 } else if (type === 'modify'){
18 const xfdf = annotationManager.getAnnotCommand();
19 let parentAuthorId = null;
20 if (annotation.InReplyTo) {
21 parentAuthorId = annotationManager.getAnnotationById(annotation.InReplyTo).authorId || 'default';
22 }
23 server.updateAnnotation(annotation.Id, {
24 authorId: authorId,
25 parentAuthorId: parentAuthorId,
26 xfdf: xfdf
27 });
28 } else if (type === 'delete') {
29 server.deleteAnnotation(annotation.Id);
30 }
31 });
32});
  1. Lastly, we will overwrite the client-side permission checking function using annotManager.setPermissionCheckCallback. The default is set to compare the authorName. Instead, we will compare authorId created from the server.
1annotationManager.setPermissionCheckCallback((author, annotation) => annotation.authorId === authorId);

Did you find this helpful?

Trial setup questions?

Ask experts on Discord

Need other help?

Contact Support

Pricing or product questions?

Contact Sales