> For the complete documentation index, see [llms.txt](https://docs.apryse.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.apryse.com/web/collaboration/realtime-collaboration-client.md).

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

Enhance real-time collaboration in the client by setting up listen/trigger events. Learn to handle data updates efficiently with step-by-step instructions for creating event listeners and triggers for

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.

{% tabs %}
{% tab title="JavaScript" %}
{% code lineNumbers="true" %}

```js
$(document).ready(() => {
  WebViewer({
    path: "lib",
    initialDoc: "MY_INITIAL_DOC.pdf",
    documentId: "unique-id-for-this-document"
  }, document.getElementById('viewer'))
    .then(instance => {
      // do something...
    });
});
```

{% endcode %}
{% endtab %}
{% endtabs %}

1. Create the server.

{% tabs %}
{% tab title="JavaScript" %}
{% code lineNumbers="true" %}

```js
const server = new Server();
```

{% endcode %}
{% endtab %}
{% endtabs %}

1. Bind a callback function to [DocumentViewer.documentLoaded](https://sdk.apryse.com/api/web/Core.DocumentViewer.html#event:documentLoaded) event.

{% tabs %}
{% tab title="JavaScript (SDK v8.0+)" %}
{% code lineNumbers="true" %}

```js
WebViewer(...)
  .then(instance => {
    const { documentViewer, annotationManager } = instance.Core;

    documentViewer.addEventListener('documentLoaded', () => {
      // Code in later steps will be added here...
    });
  })
```

{% endcode %}

[WebViewerInstance](https://sdk.apryse.com/api/web/WebViewerInstance.html) [Core](https://sdk.apryse.com/api/web/Core.html) [AnnotationManager](https://sdk.apryse.com/api/web/Core.AnnotationManager.html) [DocumentViewer#documentLoaded](https://sdk.apryse.com/api/web/Core.DocumentViewer.html#event:documentLoaded__anchor)
{% endtab %}

{% tab title="JavaScript (SDK v6.0+)" %}
{% code lineNumbers="true" %}

```js
WebViewer(...)
  .then(instance => {
    const { docViewer, annotManager } = instance;

    docViewer.on('documentLoaded', () => {
      // Code in later steps will be added here...
    });
  })
```

{% endcode %}

[WebViewerInstance](https://sdk.apryse.com/api/web/WebViewerInstance.html) [AnnotationManager](https://sdk.apryse.com/api/web/Core.AnnotationManager.html) [DocumentViewer#documentLoaded](https://sdk.apryse.com/api/web/Core.DocumentViewer.html#event:documentLoaded__anchor)
{% endtab %}
{% endtabs %}

1. Inside the documentLoaded callback, bind another callback function to server's onAuthStateChanged event that is defined in `server.js`. A [firebase.User](https://firebase.google.com/docs/reference/js/firebase.User) object will be passed as a parameter.
2. If the user is not logged in we'll call the sign-in method that we defined in `server.js`.
3. 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.
4. We call `server.checkAuthor` with parameters `authorId`, `openReturningUserPopup` function and `openNewUserPopup` function. These functions will be discussed in next steps.
5. 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.

{% tabs %}
{% tab title="JavaScript" %}
{% code lineNumbers="true" %}

```js
let authorId = null;

server.bind('onAuthStateChanged', user => {
  // User 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 user 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);
  }
  // User is not logged in
  else {
    // Login
    server.signInAnonymously();
  }
});
```

{% endcode %}
{% endtab %}
{% endtabs %}

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](https://firebase.google.com/docs/reference/js/firebase.database.DataSnapshot).
2. `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.
3. `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.
4. `updateAuthor` is a function which will set author name in both client and server using [annotationManager.setCurrentUser](https://sdk.apryse.com/api/web/Core.AnnotationManager.html#setCurrentUser) and `server.updateAuthor`, respectively.

{% tabs %}
{% tab title="JavaScript (SDK v8.0+)" %}
{% code lineNumbers="true" %}

```js
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 openNewAuthorPopup() {
  // Open prompt for a new author
  const name = window.prompt('Welcome! Tell us your name :)');
  if (name) {
    updateAuthor(name);
  }
}

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 });
}
```

{% endcode %}

[AnnotationManager.setCurrentUser](https://sdk.apryse.com/api/web/Core.AnnotationManager.html#setCurrentUser__anchor)
{% endtab %}

{% tab title="JavaScript (SDK v6.0+)" %}
{% code lineNumbers="true" %}

```js
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 openNewAuthorPopup() {
  // Open prompt for a new author
  const name = window.prompt('Welcome! Tell us your name :)');
  if (name) {
    updateAuthor(name);
  }
}

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 });
}
```

{% endcode %}

[AnnotationManager.setCurrentUser](https://sdk.apryse.com/api/web/Core.AnnotationManager.html#setCurrentUser__anchor)
{% endtab %}
{% endtabs %}

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](https://firebase.google.com/docs/reference/js/firebase.database.DataSnapshot).
2. `onAnnotationCreated` and `onAnnotationUpdated` have the exact same behavior in this guide. They will use [`annotManager.importAnnotCommand`](https://sdk.apryse.com/api/web/Core.AnnotationManager.html#importAnnotCommand) to update the viewer with the xfdf change.
3. We also set a custom field `authorId` for the updated annotation to control client-side permission of the created/updated annotation.
4. `onAnnotationDelete` creates a delete command string from the annotation's id and is simply able to call importAnnotCommand on it.

{% tabs %}
{% tab title="JavaScript (SDK v8.0+)" %}
{% code lineNumbers="true" %}

```js
function onAnnotationCreated(data) {
  // data.val() returns the value of server data in any type. In this case, it
  // would be an object with properties authorId and xfdf.
  const annotation = annotationManager.importAnnotCommand(data.val().xfdf)[0];
  annotation.authorId = data.val().authorId;
  annotationManager.redrawAnnotation(annotation);
  myWebViewer.getInstance().UI.fireEvent('updateAnnotationPermission', [annotation]);
}

function onAnnotationUpdated(data) {
  // Import the annotation based on xfdf command
  const annotation = "m": true,.importAnnotCommand(data.val().xfdf)[0];
  // 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);
}
```

{% endcode %}

[AnnotationManager.redrawAnnotation](https://sdk.apryse.com/api/web/Core.AnnotationManager.html#redrawAnnotation__anchor) [AnnotationManager.importAnnotCommand](https://sdk.apryse.com/api/web/Core.AnnotationManager.html#importAnnotCommand__anchor)
{% endtab %}

{% tab title="JavaScript (SDK v6.0+)" %}
{% code lineNumbers="true" %}

```js
function onAnnotationCreated(data) {
  // data.val() returns the value of server data in any type. In this case, it
  // would be an object with properties authorId and xfdf.
  const annotation = annotManager.importAnnotCommand(data.val().xfdf)[0];
  annotation.authorId = data.val().authorId;
  annotManager.redrawAnnotation(annotation);
  myWebViewer.getInstance().fireEvent('updateAnnotationPermission', [annotation]);
}

function onAnnotationUpdated(data) {
  // Import the annotation based on xfdf command
  const annotation = annotManager.importAnnotCommand(data.val().xfdf)[0];
  // 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);
}
```

{% endcode %}

[AnnotationManager.redrawAnnotation](https://sdk.apryse.com/api/web/Core.AnnotationManager.html#redrawAnnotation__anchor) [AnnotationManager.importAnnotCommand](https://sdk.apryse.com/api/web/Core.AnnotationManager.html#importAnnotCommand__anchor)
{% endtab %}
{% endtabs %}

1. After server callback functions are bound, we'll also bind a function to [annotManager.annotationChanged](https://sdk.apryse.com/api/web/Core.AnnotationManager.html#event:annotationChanged) event.
2. 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`.
3. Then we iterate through the annotations that are changed, which is passed as the second parameter.
4. Third parameter, type, defines which action it was. In this guide, we'll have the same behavior for both `add` and `modify` action types.
5. When annotations are added and modified, we will call `server.createAnnotation` or `server.updateAnnotation` which needs four variables: `annotationId`, `authorId`, `parentAuthorId` and `xfdf`.
6. `annotationId` can be retrieved from [annotation.Id](https://sdk.apryse.com/api/web/Core.Annotations.Annotation.html#Id).
7. `authorId` was saved as a reference when user logged in.
8. `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.
9. `xfdf` can be retrieved using [`annotationManager.getAnnotCommand`](https://sdk.apryse.com/api/web/Core.AnnotationManager.html#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`](https://sdk.apryse.com/api/web/Core.AnnotationManager.html#importAnnotCommand) in server data callback functions.

{% tabs %}
{% tab title="JavaScript (SDK v8.0+)" %}
{% code lineNumbers="true" %}

```js
annotationManager.addEventListener('annotationChanged', (annotations, type, { imported }) => {
  if (imported) {
    return;
  }
  annotations.forEach(annotation => {
    if (type === 'add') {
      const xfdf = annotationManager.getAnnotCommand();
      let parentAuthorId = null;
      if (annotation.InReplyTo) {
        parentAuthorId = annotationManager.getAnnotationById(annotation.InReplyTo).authorId || 'default';
      }
      server.createAnnotation(annotation.Id, {
        authorId: authorId,
        parentAuthorId: parentAuthorId,
        xfdf: xfdf
      });
    } else if (type === 'modify'){
      const xfdf = annotationManager.getAnnotCommand();
      let parentAuthorId = null;
      if (annotation.InReplyTo) {
        parentAuthorId = annotationManager.getAnnotationById(annotation.InReplyTo).authorId || 'default';
      }
      server.updateAnnotation(annotation.Id, {
        authorId: authorId,
        parentAuthorId: parentAuthorId,
        xfdf: xfdf
      });
    } else if (type === 'delete') {
      server.deleteAnnotation(annotation.Id);
    }
  });
});
```

{% endcode %}
{% endtab %}

{% tab title="JavaScript (SDK v6.0+)" %}
{% code lineNumbers="true" %}

```js
annotManager.on('annotationChanged', (annotations, type, { imported }) => {
  if (imported) {
    return;
  }
  annotations.forEach(annotation => {
    if (type === 'add') {
      const xfdf = annotManager.getAnnotCommand();
      let parentAuthorId = null;
      if (annotation.InReplyTo) {
        parentAuthorId = annotManager.getAnnotationById(annotation.InReplyTo).authorId || 'default';
      }
      server.createAnnotation(annotation.Id, {
        authorId: authorId,
        parentAuthorId: parentAuthorId,
        xfdf: xfdf
      });
    } else if (type === 'modify'){
      const xfdf = annotManager.getAnnotCommand();
      let parentAuthorId = null;
      if (annotation.InReplyTo) {
        parentAuthorId = annotManager.getAnnotationById(annotation.InReplyTo).authorId || 'default';
      }
      server.updateAnnotation(annotation.Id, {
        authorId: authorId,
        parentAuthorId: parentAuthorId,
        xfdf: xfdf
      });
    } else if (type === 'delete') {
      server.deleteAnnotation(annotation.Id);
    }
  });
});
```

{% endcode %}
{% endtab %}
{% endtabs %}

1. Lastly, we will overwrite the client-side permission checking function using [`annotManager.setPermissionCheckCallback`](https://sdk.apryse.com/api/web/Core.AnnotationManager.html#setPermissionCheckCallback). The default is set to compare the authorName. Instead, we will compare authorId created from the server.

{% tabs %}
{% tab title="JavaScript (SDK v8.0+)" %}
{% code lineNumbers="true" %}

```js
annotationManager.setPermissionCheckCallback((author, annotation) => annotation.authorId === authorId);
```

{% endcode %}
{% endtab %}

{% tab title="JavaScript (SDK v6.0+)" %}
{% code lineNumbers="true" %}

```js
annotManager.setPermissionCheckCallback((author, annotation) => annotation.authorId === authorId);
```

{% endcode %}
{% endtab %}
{% endtabs %}


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.apryse.com/web/collaboration/realtime-collaboration-client.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
