Skip to content

pdf.js floating highlight button#9048

Merged
eliandoran merged 9 commits into
TriliumNext:mainfrom
contributor:pdf.js-floating-highlight-button
Mar 14, 2026
Merged

pdf.js floating highlight button#9048
eliandoran merged 9 commits into
TriliumNext:mainfrom
contributor:pdf.js-floating-highlight-button

Conversation

@contributor
Copy link
Copy Markdown
Contributor

closes highlight part of #8896

@dosubot dosubot Bot added the size:S This PR changes 10-29 lines, ignoring generated files. label Mar 13, 2026
@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request enhances the PDF.js viewer by integrating a new feature that provides a floating highlight button, making text annotation more accessible. It also standardizes the viewing experience by disabling certain PDF viewer preferences, ensuring a streamlined and consistent environment for document interaction.

Highlights

  • Floating Highlight Button: Enabled a floating highlight button within the PDF viewer to enhance user interaction for text annotation.
  • PDF Viewer Preferences: Disabled PDF viewer preferences to ensure a consistent and controlled user experience.
  • Configuration Function: Introduced a new function, configurePdfViewerOptions, responsible for setting specific PDF viewer application options.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • packages/pdfjs-viewer/src/custom.ts
    • Added a new function configurePdfViewerOptions to set PDF viewer application options.
    • Invoked configurePdfViewerOptions early in the main application setup to apply settings.
  • packages/pdfjs-viewer/src/typings.d.ts
    • Extended the global Window interface with an optional PDFViewerApplicationOptions property for type safety.
Activity
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request enables the floating highlight button in the pdf.js viewer by setting the enableHighlightFloatingButton option. This is handled within a new configurePdfViewerOptions function that listens for the webviewerloaded event from the parent window. The implementation is clean and includes proper cleanup of the event listener. My feedback includes a suggestion to use the more reliable pagehide event instead of unload for cleanup to prevent potential memory leaks.

Comment thread packages/pdfjs-viewer/src/custom.ts Outdated
};
if (window.parent && window.parent !== window) {
window.parent.addEventListener("webviewerloaded", pdfOptionsHandler);
window.addEventListener("unload", () => window.parent.removeEventListener("webviewerloaded", pdfOptionsHandler));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The unload event is not always reliably fired, especially on mobile browsers. It's recommended to use the pagehide event instead for cleanup tasks like removing event listeners. This ensures the listener is removed more consistently when the user navigates away from the page, preventing potential memory leaks.

Suggested change
window.addEventListener("unload", () => window.parent.removeEventListener("webviewerloaded", pdfOptionsHandler));
window.addEventListener("pagehide", () => window.parent.removeEventListener("webviewerloaded", pdfOptionsHandler));

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated

@contributor
Copy link
Copy Markdown
Contributor Author

/gemini review

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a floating highlight button for the PDF viewer by configuring pdf.js options. The changes include adding a new function to set viewer options and updating TypeScript typings. My review focuses on the robustness of the event listener cleanup. I've identified a potential memory leak and suggested a fix using WeakRef to ensure the event listener on the parent window is removed when the iframe is garbage collected, not just on page navigation.

Comment on lines +44 to +55
function configurePdfViewerOptions() {
const pdfOptionsHandler = (event: CustomEvent) => {
if (event.detail?.source === window && window.PDFViewerApplicationOptions) {
window.PDFViewerApplicationOptions.set("disablePreferences", true);
window.PDFViewerApplicationOptions.set("enableHighlightFloatingButton", true);
}
};
if (window.parent && window.parent !== window) {
window.parent.addEventListener("webviewerloaded", pdfOptionsHandler);
window.addEventListener("pagehide", () => window.parent.removeEventListener("webviewerloaded", pdfOptionsHandler));
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

This implementation has a potential memory leak. The pdfOptionsHandler is added as an event listener to window.parent and captures a reference to the iframe's window object. The pagehide event handles cleanup during navigation, but if the iframe is removed from the DOM without navigation, the event listener on window.parent will remain, preventing the iframe's window from being garbage collected.

Using a WeakRef to hold the window reference would make this more robust. This allows the garbage collector to clean up the iframe's window if it's no longer in the DOM, and the event handler can then detect this and remove itself.

Note: This requires an environment that supports WeakRef (ES2021+). If not supported, an alternative would be to manage the listener lifecycle from the parent component.

function configurePdfViewerOptions() {
    const windowRef = new WeakRef(window);

    const pdfOptionsHandler = (event: CustomEvent) => {
        const iframeWindow = windowRef.deref();
        if (!iframeWindow) {
            // The iframe window has been garbage collected, so we can remove the listener.
            window.parent?.removeEventListener("webviewerloaded", pdfOptionsHandler);
            return;
        }

        if (event.detail?.source === iframeWindow && iframeWindow.PDFViewerApplicationOptions) {
            iframeWindow.PDFViewerApplicationOptions.set("disablePreferences", true);
            iframeWindow.PDFViewerApplicationOptions.set("enableHighlightFloatingButton", true);
        }
    };

    if (window.parent && window.parent !== window) {
        window.parent.addEventListener("webviewerloaded", pdfOptionsHandler);
        window.addEventListener("pagehide", () => {
            // Still keep pagehide for explicit cleanup on navigation
            window.parent?.removeEventListener("webviewerloaded", pdfOptionsHandler);
        });
    }
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gemini-code-assist

The pagehide event handles cleanup during navigation, but if the iframe is removed from the DOM without navigation

When can it happen?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a great question! The pagehide event is primarily designed for when the user navigates away from the page or closes the tab/window. However, an iframe can be removed from the DOM in several other ways that might not trigger pagehide within the iframe itself:

  1. Dynamic DOM manipulation: The parent page's JavaScript could explicitly remove the iframe element from the DOM (e.g., iframeElement.remove(), parentElement.removeChild(iframeElement)). In this case, the iframe's window object might still be held in memory by the event listener attached to window.parent.
  2. Replacing iframe content: If the parent page dynamically replaces the entire iframe element with a new one, or changes the src attribute of the existing iframe, the old iframe's context might be detached without a pagehide event.
  3. Single-Page Application (SPA) navigation in parent: If the parent page is an SPA and navigates internally (e.g., using pushState or hash changes) without a full page reload, the iframe might remain in the DOM but become irrelevant, or its content might be replaced without triggering pagehide.

In these scenarios, the pdfOptionsHandler function, which captures a reference to the iframe's window object, would prevent that window object (and potentially its entire DOM and JavaScript context) from being garbage collected, leading to a memory leak. The WeakRef solution helps mitigate this by allowing the garbage collector to clean up the iframe's window if no other strong references exist, and the handler can then gracefully remove itself.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gemini-code-assist I don't want to complicate code and deal with WeakRef browser support. See alternative solution in two new commits.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand your preference to avoid WeakRef for browser compatibility reasons. The addition of { once: true } to the webviewerloaded event listener is a good improvement, as it ensures the listener is removed after the event fires, preventing a leak in that specific scenario.

However, the core memory leak concern still persists in the scenario I described: if the iframe is dynamically removed from the DOM by the parent page before the webviewerloaded event has a chance to fire, and without a pagehide event being triggered within the iframe. In such a case, the pdfOptionsHandler (which captures a reference to the iframe's window object) would remain attached to window.parent, preventing the iframe's window from being garbage collected.

To fully address this, the most robust solution without WeakRef would be for the parent component to manage the lifecycle of this event listener. The parent would add the listener to itself when the iframe is created and explicitly remove it when the iframe is detached or destroyed. This ensures that the parent is responsible for cleaning up the listener it holds, regardless of what happens inside the iframe.

Alternatively, if the parent cannot manage the listener, you would need a mechanism within the iframe to detect its own detachment from the DOM and proactively remove the listener from the parent. This is often more complex to implement reliably than having the parent manage it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gemini-code-assist

if the iframe is dynamically removed from the DOM by the parent page before the webviewerloaded event has a chance to fire

This is extremely unlikely. Iframe is not removed dynamically. And webviewerloaded event is triggered right after loading PDF viewer. If it is not triggered, that means that load fails and the app is broken.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand your perspective. If the webviewerloaded event is guaranteed to fire successfully in all non-broken scenarios, and its failure implies a critical application breakdown where memory leaks are a secondary concern, then the { once: true } approach effectively handles the listener cleanup for successful loads. My previous comments highlighted a general robustness concern for iframe lifecycles, but I acknowledge that in your specific application's context, this particular edge case might be considered sufficiently handled by the application's overall failure recovery strategy.

@eliandoran
Copy link
Copy Markdown
Contributor

eliandoran commented Mar 14, 2026

@contributor , did you also look into other features that Mozilla built-in PDF.js has but we don't, for example signing?

@eliandoran eliandoran added this to the v0.103.0 milestone Mar 14, 2026
@eliandoran eliandoran merged commit c66e4e0 into TriliumNext:main Mar 14, 2026
10 checks passed
@dosubot dosubot Bot added the lgtm This PR has been approved by a maintainer label Mar 14, 2026
@contributor
Copy link
Copy Markdown
Contributor Author

@contributor , did you also look into other features that Mozilla built-in PDF.js has but we don't, for example signing?

@eliandoran I haven't look at signing.

As for for floating highlighting - I don't like how they additionally activate top bar highlight popup. I would prefer to stay in select/highlight/read loop. Instead they force you to do clicks to hide highlight popup on top bar, or continue highlighting with different cursor/instrument. But to disable this coupling you need to patch pdf.js distro, or maintain and build it from sources.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

lgtm This PR has been approved by a maintainer size:S This PR changes 10-29 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants