ReactJS | Customizing user interace

Which product are you using?
PDFJS Express Plus

PDF.js Express Version
8.7.0

Detailed description of issue
Hello, am currently assessing the PDFJS web standalone kit for PDF Viewing and annotations. If the features and functionalities are in line with our requirements we can go ahead and pay the annual license fees.

Now onto the issue -


Refer to the above attached file.
(1) Where I need to disable the default toolbar of the component and instead,
(2) I need to define my own toolbar with limited capabilities. On clicking a button in the custom toolbar, I need the behavior to be similar to a button click on the default toolbar. For example, on click of the Zoom In button on the custom toolbar, I need the viewer to zoom in on the document.
Similarly on click of a custom button of the annotation I need the component to be ready to drop the corresponding annotation (wherever user drops the annotation)
(3) Need a way to disable the default commenting functionality of the component. Instead what we will need is - when the user drops an annotation, a popup (we will build the HTML-based popup) where the user enters the comments and then saves. Upon save we will go back and insert the comment into the annotation.

To summarize the above section we need

  • Disable the toolbar
  • Information about setting the desired action on click of a button.
  • Disable the default commenting functionality.

Thank you for posting the incident to our forum. We will provide you with an update as soon as possible.

Hello there,

You can disable elements in the UI using the disableElements API.

https://pdfjs.express/api/UI.html#.disableElements

You can disable the comments panel button like so:

instance.UI.disableElements(['toggleNotesButton']);

In your image, the group separator is still there and it cannot be disabled with this API. Instead, you can the css option to remove it.

    WebViewer(
      {
        initialDoc: '../../files/demo.pdf',
        path: '../../../lib',
        licenseKey: '',
        css: './style.css', //add css line
      },

I think to meet your requirement you can disable the toolsHeader and just disable the buttons in the header. You can then add custom buttons like so:


      instance.UI.setHeaderItems(header => {
        const items = header.getItems();
        items.splice(6, 1,
          {
            type: 'actionButton',
            img: 'icon-header-page-manipulation-page-rotation-clockwise-line',
            title: 'Page Rotation Clockwise',
            onClick: () => {
              documentViewer.rotateClockwise();
            },
          }
        );
        header.update(items);
      });

Best Regards,
Darian

WebViewer(
  {
    path: '/webviewer/lib',
    disabledElements: [
      'toggleNotesButton',
      'leftPanelButton'
    ],
    css: './style.css'
  },
  pdfViewer.current,
)

Hello,
I had already set the above 'toggleNotesButton' property. However, the comments toolbar opens as soon as I drop a note annotation.
Recording.zip (152.1 KB)

Is there any way that I can, through an event listener detect new added annotations and open a custom HTML popup to record notes/ comments and prevent further propogation?

Hi there,

You can prevent the comments toolbar from opening by using the following code:

instance.UI.disableElements(['notesPanel']);

You can hide anything in the UI as long as it has a data-element attribute. You can inspect the element in the console to find the data elements.

To detect when an annotation is added you can use the annotationChanged event. For the popup, you could try using a custom modal to achieve this.

Sample Code

      const { annotationManager } = instance.Core;
      // Creating HTML DOM elements
      let divInput1 = document.createElement('input');
      divInput1.type = 'text';
      divInput1.id = 'unique_id_1';
      divInput1.style = 'height: 28px; margin-top: 10px; margin-right: 20px';

      let divInput2 = document.createElement('input');
      divInput2.type = 'text';
      divInput2.id = 'unique_id_2';
      divInput2.style = 'height: 28px; margin-top: 10px;';

      // Custom modal parameters
      const modalOptions = {
        dataElement: 'myCustomModal',
        header: {
          title: 'Header',
          className: 'myCustomModal-header',
        },
        body: {
          className: 'myCustomModal-body',
          style: {},
          children: [divInput1, divInput2],
        },
        footer: {
          className: 'myCustomModal-footer footer',
          style: {},
          children: [
            {
              title: 'Cancel',
              button: true,
              style: {},
              className: 'modal-button cancel-form-field-button',
              onClick: (e) => { console.log('Cancel button') }
            },
            {
              title: 'OK',
              button: true,
              style: {},
              className: 'modal-button confirm ok-btn',
              onClick: (e) => { console.log('OK button') }
            },
          ]
        }
      }

      instance.UI.addCustomModal(modalOptions);

      // Listen for when an annotation is added
      annotationManager.addEventListener('annotationChanged', (annotations, action, { imported }) => {
        // Check if the action is 'add' and not from importing annotations
        if (action === 'add' && !imported) {
          instance.UI.openElements([modalOptions.dataElement]);
        }
      });

Hello many thanks for your reply,

The list is never-ending, I guess.

Annotation items are deletable, and movable even when the XFDF imported says Locked = true.

<?xml version="1.0" encoding="utf-16"?>
<xfdf xml:space="preserve" xmlns="http://ns.adobe.com/xfdf/">
  <pdf-info version="2" import-version="4" xmlns="http://www.pdftron.com/pdfinfo">
    <fields />
    <annots>
<text page="0" rect="95.390,581.670,126.390,612.670" color="#FFCD45" flags="print,nozoom,norotate" name="1798cc75-8a1d-f662-c142-fb2c8e7b6d23" title="Kshitij Thube" subject="Comment" date="D:20231209010946+05'30'" creationdate="D:20231209010942+05'30'" icon="Comment" statemodel="Review" Locked="true">
  <contents>note comment test</contents>
</text>
<highlight page="0" rect="93.600,711.890,152.713,723.284" color="#00CC63" flags="print" name="3f2c71a2-c642-8164-9966-b2c2e7aef177" title="Kshitij Thube" subject="Highlight" date="D:20231209010928+05'30'" creationdate="D:20231209010920+05'30'" coords="93.6,723.2843,152.71282065,723.2843,93.6,711.8905,152.71282065,711.8905" Locked="true">
  <trn-custom-data bytes="{&quot;trn-annot-preview&quot;:&quot;THIS IS A TEST&quot;}" />
  <contents>Test for showing highlight saving</contents>
</highlight>
<square page="0" rect="304.730,708.820,456.710,747.390" color="#E44234" flags="print" name="e0ea163d-8203-3157-2493-d1fccce45afa" title="Kshitij Thube" subject="Rectangle" date="D:20231209011440+05'30'" creationdate="D:20231209011435+05'30'" Locked="true">
  <contents>2nd square box</contents>
</square>
<square page="0" rect="276.630,655.910,427.980,731.590" color="#E44234" flags="print" name="607b9d23-e401-f5b0-d634-ec1f559b9e7d" title="Kshitij Thube" subject="Rectangle" date="D:20231209010954+05'30'" creationdate="D:20231209010949+05'30'" Locked="true">
  <contents>square box test</contents>
</square>
</annots>
    <pages>
      <defmtx matrix="1,0,0,-1,0,792" />
    </pages>
  </pdf-info>
</xfdf>

Above is the sample of the XFDF I am importing from the server. The idea is that users should not be able to edit/ delete any existing annotation since there may be multiple users commenting on it. Only “unsaved” annotations should be editable.
Therefore - need a way to prevent the editing/deletion of all annotations saved in the DB. Only current session annotations can be editable.

Hi there,

I tried importing that XFDF on our demo and it seems to work fine: PDF.js Viewer Demo | PDF.js Express
The two rectangles are not movable or deletable.

We also have a NoDelete property you can set to true.

How are you importing the XFDF? Are you using the importAnnotations API?

await instance.Core.annotationManager.importAnnotations(xfdf) // xfdf is the string you provided

You should also make sure you are not in admin mode. User admin will allow anyone to move the annotations even if they are locked.

You can check like so:

instance.Core.annotationManager.isUserAdmin() // should return false

To exit user admin mode, you can use the following:

instance.Core.annotationManager.demoteUserFromAdmin()

Best Regards,
Darian

Okay, I found out the root cause. The reason why I was able to edit those annotations was because the creator of those annotations (author) and the current user are the same - not because the user is in admin mode.

However, I need to prevent this as well. Once an annotation is updated to the server, should not be editable/movable/deletable. I have tried passing ReadOnly, Locked, and NoDelete however an author with the same name can manipulate the data - which needs to be avoided.

Also, I am defining my toolbar like below:

For the toolbar I have the following queries:

  1. Apart from CSS Classes and style manipulation is there a way to place items at the end of the toolbar? Right to Left? The idea is to keep the page-related options on the left and the annotation options to the right so that the two groups are easily distinguishable.

  2. With defining a custom toolbar, I have no set my annotation styles i.e. color of annotation, border width etc. Any way this can be achieved?

//Button definition
{
  type: 'actionButton',
  text: 'Text Highlight Annotation',
  onClick: () => {
    documentViewer.setToolMode(new Tools.TextHighlightCreateTool(documentViewer));
  },
}

//Setting style
documentViewer.getTool('AnnotationCreateTextHighlight').setStyles({
  StrokeColor: new Annotations.Color(255, 0, 0)
});

But for some reason, the above doesn’t work in my case. The color I set here does not get reflected when I place the annotation. Is there any way I can set global default styles for all annotations? I am mostly looking at setting styles for Tools.RectangleCreateTool, Tools.EllipseCreateTool, Tools.StickyCreateTool

Hi there,

We do have an API that could potentially help this situation. This will prevent the same user from editing the annotation. You would also need to include logic to check whether the annotation is imported.

 const { documentViewer, Annotations, annotationManager } = instance.Core;


      // Function to check if the current user is the author of the annotation
      const isAuthor = (author) => {
        return author === annotationManager.getCurrentUser();
      };

      // Custom permission check callback
      annotationManager.setPermissionCheckCallback((author, annotation) => {
        console.log(annotation);
        // For imported annotations, check if the current user is the author
        if (isAuthor(author)) { // include logic for imported annotations
          // If not the author, prevent deletion or modification
          return false;
        }

        // Allow other actions or if the user is the author
        return true;
      });

For the second issue, I think you may have to try using CSS to achieve this. We also have a way to customize the UI: Advanced Customization with PDF.js Express Viewer | Documentation

For the third issue, could you provide a sample project (GitHub) or if the code is just a single file, could you provide that?

For now I am doing as below:

annotationManager.addEventListener('annotationChanged', (annotations, action, {imported}) => {
  if (action === 'add' && !imported) {
    switch (annotations[0]['Subject']) {
      case 'Ellipse':
        annotations[0].Color = new Annotations.Color(255, 0, 0);
        break;
      case 'Comment':
        annotations[0].Color = new Annotations.Color(255, 255, 0);
        break;
      case 'Rectangle':
        annotations[0].Color = new Annotations.Color(255, 0, 0);
        break;
      case 'Polygon':
        annotations[0].Color = new Annotations.Color(255, 0, 0);
        break;
    }
  }
});

But the above works only after the annotation is saved. There is a 2-3 second gap where the annotation is created in black color since I guess it’s the default color and then the color that I have defined get’s set. Is there a way that I can set different color and/ or styling for each type of annotation? Rather then setting it in the annotationChanged event?

Also, (I guess) the below code will not work for me since I am not using the default icons of the tool

documentViewer.getTool('AnnotationCreateTextHighlight').setStyles({
  StrokeColor: new Annotations.Color(255, 0, 0)
});

Instead I am defining the toolbar as follows:

instance.UI.setHeaderItems(header => {
  const items = header.getItems();
items.push(
  {
    type: 'actionButton',
    img: 'icon-tool-comment-fill',
    title: 'Callout Annotation',
    onClick: () => {
      documentViewer.setToolMode(new Tools.StickyCreateTool(documentViewer));
    },
  }, {
    type: 'actionButton',
    img: 'ic_annotation_circle_black_24px',
    title: 'Circle Annotation',
    onClick: () => {
      documentViewer.setToolMode(new Tools.EllipseCreateTool(documentViewer));
    },
  }, {
    type: 'actionButton',
    img: 'ic_annotation_square_black_24px',
    title: 'Rectangle Annotation',
    onClick: () => {
      documentViewer.setToolMode(new Tools.RectangleCreateTool(documentViewer));
    },
  },
  {
    type: 'actionButton',
    img: 'icon-tool-highlight',
    text: 'Text Highlight Annotation',
    onClick: () => {
      documentViewer.setToolMode(new Tools.TextHighlightCreateTool(documentViewer));
    },
  },
  {
    type: 'actionButton',
    img: 'icon-tool-shape-cloud',
    text: 'Cloud Annotation',
    onClick: () => {
      documentViewer.setToolMode(new Tools.PolygonCloudCreateTool(documentViewer));
    },
  }
);
header.update(items);
});

awaiting further samples for above reply

Hello,

Apologies for the delay.

I think it would be easier to take the current tools we have and put them in the header.

For example, we can take the rectangle tool and put it in the header like so:

      instance.UI.setHeaderItems(header => {
        const rectangleToolButton = {
          type: 'toolButton',
          toolName: 'AnnotationCreateRectangle',
          dataElement: 'rectangleToolButton', 
          title: 'Rectangle Tool',
          img: 'icon-header-rectangle',
        };
      
        header.getHeader('default').push(rectangleToolButton);
      });

This should allow you to set the tool style.

You could also consider making your own custom tool and registering it. I found this other forum post where you can use the code provided to create a custom tool. Here is the post: Help with Custom Rectangle tool button

    const newRectangleTool = new Tools.RectangleCreateTool(documentViewer);

    newRectangleTool.setStyles({
      StrokeColor: new Annotations.Color(255, 255, 0),
      //StrokeThickness: "30pt",
    });

    instance.UI.registerTool({
      toolName: "RedactTool",
      buttonName: "RedactTool",
      toolObject: newRectangleTool,
      showColor: "always",
      tooltip: "Redact",
      buttonImage: "https://www.pdftron.com/favicon-32x32.png",
    });

    instance.UI.setHeaderItems((header) => {
      const items = header.getItems();
      items[items.length - 1] = {
        type: "toolButton",
        toolName: "RedactTool",
      };
      header.update(items);
    });

Hello,
Is there a way to set a different color for highlighted annotations?
2024-02-09_11-49-37

In the below screenshot, the annotation is an ellipsis annotation when highlighted a rectangle is drawn around it. So distinguishing between highlighted annotations becomes difficult. Is there any way we can change color of the highlighted items?

Hello,

You can use the setStyles API to set a default color.
Here is another post you can reference: Want to set the default color of the reactangle tool - #2 by darian.chen

Best Regards,
Darian

Hi,
I donot wish to permanently change the color. Just that when the annotation is currently selected I need to set a different color. Once deselected it can go back to the original color.

Hi there,

You can achieve this with the annotationSelected and annotationDeselected events.

https://pdfjs.express/api/Core.AnnotationManager.html#event:annotationDeselected

You will need to store the original color, maybe as custom data, and then reapply it to the annotation when deselected.

Best Regards,
Darian

Hi,
By this method the annotation stroke color gets updated but the outer highlight is still the old default color. Can I change the highlight color as well?

image

As you can see the inner ellipsis color has been changed on selection but the outer “highlight” color is still the same. The idea is on selection the user should be able to identify the selected annotation.

Hi there,

Could you provide the code to create this specific annotation with the highlight? Is the square part you are referring to the selection model?

Best Regards,
Darian

Yes, the square part is the highlight. I am not writing any custom code to generate the square part.

The annotation tool is defined as a custom tool - following is the code

const items = header.getItems();

const ellipseCreateTool = new Tools.EllipseCreateTool(documentViewer);
ellipseCreateTool.setStyles({
  StrokeColor: new Annotations.Color(255, 0, 0)
});
instance.registerTool({
  toolName: 'EllipseCreateTool',
  buttonName: 'EllipseCreateTool',
  toolObject: ellipseCreateTool,
  showColor: 'never',
  tooltip: 'Circle Create Tool',
  buttonImage: 'ic_annotation_circle_black_24px',
});
items.push({
  type: 'toolButton',
  toolName: 'EllipseCreateTool',
});
header.update(items);

Hi there,

It looks like that annotation in your image is locked or set to read only. You can change the styling by using the following code:

const { Annotations } = instance;
Annotations.SelectionModel.defaultNoPermissionSelectionOutlineColor = new Annotations.Color(0, 255, 0, 1);

This will change the red border to green.

image

Best Regards,
Darian