惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园 - 司徒正美
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
腾讯CDC
WordPress大学
WordPress大学
爱范儿
爱范儿
GbyAI
GbyAI
酷 壳 – CoolShell
酷 壳 – CoolShell
博客园 - 聂微东
Google DeepMind News
Google DeepMind News
Recent Announcements
Recent Announcements
Latest news
Latest news
Last Week in AI
Last Week in AI
V2EX - 技术
V2EX - 技术
I
InfoQ
N
News | PayPal Newsroom
SecWiki News
SecWiki News
Microsoft Azure Blog
Microsoft Azure Blog
美团技术团队
T
Troy Hunt's Blog
H
Hacker News: Front Page
S
SegmentFault 最新的问题
TaoSecurity Blog
TaoSecurity Blog
V
Visual Studio Blog
Martin Fowler
Martin Fowler
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
博客园_首页
S
Security @ Cisco Blogs
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
小众软件
小众软件
L
LangChain Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
T
Tor Project blog
L
LINUX DO - 热门话题
月光博客
月光博客
S
Schneier on Security
Y
Y Combinator Blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
P
Proofpoint News Feed
Forbes - Security
Forbes - Security
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Hugging Face - Blog
Hugging Face - Blog
A
Arctic Wolf
Webroot Blog
Webroot Blog
博客园 - 叶小钗
F
Fortinet All Blogs
S
Securelist
AI
AI
B
Blog RSS Feed
Security Latest
Security Latest

Inside Nutrient

A guide to the invisible work behind documents Introducing Nutrient Documents for Salesforce: Native document generation and signing Document AI vs. traditional OCR: Choosing between OCR, AI, and hybrid pipelines PDF SDK compliance and security evaluation checklist for enterprise teams (2026) Invariant Corp replaces paper processes with Nutrient Workflow and scales without limits What is process mapping? A complete guide Nutrient vs. Conga Composer for Salesforce document generation (2026) Document routing: How to automate document distribution The CTO’s AI playbook: Why accountability architecture beats orchestration Compliance workflow automation: Why built-in compliance is table stakes Workflow diagrams: Examples, symbols, and how to build one that actually runs Digital forms: Replace paper forms with automated workflows Approval workflow software: How to automate approvals Why document-centric automation is different The CEO’s AI playbook: Why decision architecture beats model selection Nutrient SDK product updates for Q1 2026 PDF redaction verification: How to prove sensitive data is permanently removed What is a VPAT? The complete guide to accessibility conformance reports What is PDF/UA? The accessible PDF standard explained Salesforce eSignatures: Generate, sign, and track documents in one flow Online document viewer: Options, tradeoffs, and how to embed one Document viewer for web apps: React, Vue, Angular (2026) Best document viewers in 2026: A buyer’s guide How to edit a PDF in Python: Add text, images, and annotations Nutrient advances Workflow platform with agentic AI for enterprise-grade speed and consistency in document-heavy operations How to create a Salesforce quote template from opportunity data The business case for accessibility: Five ways it drives enterprise value Python PDF library comparison (2026): 7 libraries for developers Why your AI agent hallucinates PDF table data PDF.js limitations: When to upgrade to a commercial PDF SDK How Subject scaled 5× with Nutrient’s PDF SDK without rebuilding its document layer I replaced our sales training with an AI coach that runs in Slack — here’s what broke Redirecting to: https://securitybuzz.com/cybersecurity-news/why-enterprise-permissions-are-ais-most-dangerous-inheritance/ Nutrient .NET SDK vs. iText Core: Complete comparison for .NET developers DocuVieware: Support’s most frequently asked setup questions Introducing Nutrient Workflow How to convert PDF to Word in C# (.NET) When email and spreadsheets stop working: Work order approval workflows for field teams on the move Compliance with confidence: Why document-centric automation is the foundation of your mission Nutrient expands AI Assistant, automating multistep document workflows inside any application What is document generation? A developer’s guide to PDF generation Document Converter data flow and how real-time watermarks skip the queue PDF/UA compliance guide: Requirements, standards, and best practices Computers still can’t understand you How Athena Intelligence built AI agents for regulated enterprises with Nutrient’s document infrastructure How to convert HTML to PDF (2026): 4 methods from browser print to SDK How to build a document extraction pipeline with Nutrient Vision API OCR vs. intelligent document processing: Choosing the right document extraction engine Beyond OCR: How document intelligence eliminates manual processing in regulated industries Nutrient vs. IronPDF: Complete comparison for .NET developers Nutrient vs. Aspose.PDF: Complete comparison for .NET developers Redirecting to: https://fortune.com/2026/02/19/openclaw-who-is-peter-steinberger-openai-sam-altman-anthropic-moltbook/ Lufthansa Systems uses Nutrient to deliver reliable, scalable PDF rendering for pilots worldwide Nutrient vs. Syncfusion: Complete comparison for .NET developers React’s useTransition: The hook you’re probably using wrong First City Monument Bank streamlines banking processes with Nutrient Workflow Redirecting to: https://www.sdcexec.com/warehousing/automation/article/22957364/nutrient-workflow-automation-the-missing-link-in-supply-chain-efficiency The complete guide to digital signatures: PAdES, CAdES, and XAdES explained Nutrient Python SDK: Production-grade document processing for Python Introducing agentic document editing for web applications with AI Assistant Nutrient vs. QuestPDF: Complete comparison for .NET developers How we fixed the GdPicture license expiration (and what to do if you’re affected) Red team security testing with agentic AI The future of healthcare document automation Best healthcare workflow software compared Nutrient SDK product updates for Q4 2025 How Harvey scaled legal document workflows 50 percent MoM without rebuilding infrastructure HIPAA-compliant document management in hospitals How we optimized rendering performance while handling thousands of annotations in React — Part 2 Automated PII removal with Nutrient API Redirecting to: https://www.devopsdigest.com/2026-low-code-no-code-predictions Redirecting to: https://www.kmworld.com/Articles/Editorial/ViewPoints/Leaders-predict-AI-to-continue-permeating-all-aspects-of-KM-in-2026-172594.aspx What are deep agents and how do they solve complex problems? Whipping up document magic: Your easy-bake recipe for Vue and Nutrient Web SDK 🧁 What I’ve learned about product iteration planning while building SDKs Passwordless document signing: Three-layer security guide New zip folder functionality streamlines file management in Document Automation Server From experienced engineer to AI beginner: My unexpected journey AI-assisted manual testing: Handling Safari’s PDF rendering and UI quirks How to keep a 20-year-old SDK up to date How we optimized rendering performance while handling thousands of annotations in React — Part 1 Nutrient announces new executive hires to accelerate next phase of growth High performance UI using web workers Automate document conversion at scale with Python and Nutrient DCS From curiosity to PLG (and AI): My journey to understanding product-led growth Prost to progress: One year as Nutrient Pigeon usage at Nutrient: Bridging native SDKs to Flutter Modernizing CI build servers: How to migrate from Chef to Ansible Unix man pages: AI-friendly documentation since 1971 Consistent hashing for even load distribution Best AI redaction APIs: Complete comparison guide for 2025 Why AI document redaction matters for modern security From coding to coordinating: How AI transformed my workflow What is intelligent document processing (IDP)? A complete guide Enterprise PDF SDKs: Best PSPDFKit (now Nutrient) alternatives Nutrient SDK product updates for Q3 2025 GdPicture support best practices Redacting sensitive data with Nutrient AI redaction API How AI is transforming the customer experience at Nutrient: From instant answers to intelligent support How manual QA uses PR testing between releases
The keyboard shortcuts playbook: Taking control of keyboard events in Nutrient Web SDK
Omar Talaat · 2025-12-12 · via Inside Nutrient

Implementing custom keyboard shortcuts in web apps can get tricky, especially when you want to have a different way of handling things compared to the default input of Nutrient Web SDK. This guide walks through six reliable patterns for predictable, conflict-free keyboard handling that work across Shadow DOM and iframe setups.

The examples listed here have been tested with Nutrient Web SDK 1.8.0. All code examples are production-ready and available to test immediately in our Playground.

Who this is for

This article is for developers who need to know how to:

  • Stop keyboard events from firing twice.
  • Choose exactly which keys the viewer reacts to.
  • Add their own shortcuts without breaking the viewer’s standard features.

All examples work in both Shadow DOM and iframe setups. The patterns behave the same because they intercept keyboard events before implementation details matter.

Essential concepts: How events actually work

Before diving into the code, it’s important to first establish the foundation that makes these patterns work.

Event propagation: The three phases

When you press a key, the browser processes that event in three distinct phases:

  1. Capturing phase — The event propagates through the target’s ancestor nodes, starting from the window and diving all the way down to the target element.
  2. Target phase — The event reaches the actual element that triggered it.
  3. Bubbling phase — The event is propagated back up through the target’s ancestors in reverse order.

By default, addEventListener listens during the bubbling phase, meaning your handler runs after the event reaches its target. But for keyboard shortcuts, you want the capturing phase (addEventListener(..., true)). This lets your handlers run before the viewer’s handlers, giving you first dibs on every keystroke.

Integration models: Shadow DOM and iframe

Nutrient Web SDK can be embedded in two main ways, each with its own event behavior.

Shadow DOM:

  • The viewer renders inside a Shadow DOM tree attached to your page. This isolates its internal DOM from the rest of your app.
  • When keyboard events cross this shadow boundary, browsers perform retargeting; they change event.target to reference the shadow host instead of the internal element. If you need the actual internal element, use event.composedPath()[0] instead of event.target.
  • If you attach listeners both to the main document and inside the viewer’s shadow root, you may see the same key event fire twice. Keep your listener in one place to avoid duplication.

iframe:

  • The viewer runs in its own document context, completely separate from the host page.
  • Because the iframe has its own document, your keyboard listeners must be attached to instance.contentDocument (not the host page).

Choosing your listener target

The critical rule is to attach your listener to one place, never both:

// For Shadow DOM:

document.addEventListener("keydown", handler, true);

// For iframe:

instance.contentDocument.addEventListener("keydown", handler, true);

All patterns shown below work identically for both models — just swap the document reference.

The six essential patterns

Each pattern solves a specific keyboard handling challenge with its relevant code snippet, and you can try every single one right now.

Every example includes a link to the Playground that opens a live Nutrient Web SDK environment prefilled with the code. There’s no setup, no local project, and no fiddling. Just click, run, and start tinkering with the shortcuts handling code directly in your browser.

Whether you want to hijack Control-P, test Escape behavior, or block Delete keys, the Playground makes it effortless to see how each pattern behaves in a real viewer session.

1. Single-listener pattern (no duplicate events)

The problem — The same keystroke can trigger listeners multiple times as events retarget across boundaries.

The solution — Attach exactly one listener in the capturing phase.

// Your viewer configuration.

const config = {

container: "#viewer",

document: "document.pdf",

// ... your other options.

};

NutrientViewer.load(config).then((instance) => {

// For Shadow DOM: use `document`.

// For iframe: use `instance.contentDocument`.

document.addEventListener(

"keydown",

(e) => {

if ((e.ctrlKey || e.metaKey) && e.key === "1") {

e.preventDefault();

instance.setViewState((v) =>

v.set("interactionMode", NutrientViewer.InteractionMode.INK),

);

}

},

true, // Capture phase.

);

});

When to use — This is your foundation. Start here, and add more shortcuts following the same pattern (Control-2, Control-3, etc.).

Tip — If the viewer is embedded in an iframe, use instance.contentDocument for event handling. Otherwise, use document. Always ensure you only attach listeners to one of these targets to avoid duplicate events.

Try this example in our Playground.

2. Silencing default shortcuts: Override print

The problem — Your app has a custom Export PDF button, but users keep pressing Control-P and triggering the browser’s print dialog instead.

The solution — Intercept Control-P before either the browser or viewer sees it.

NutrientViewer.load(config).then(() => {

document.addEventListener(

"keydown",

(e) => {

if ((e.ctrlKey || e.metaKey) && e.key === "p") {

e.preventDefault(); // Stop browser print.

e.stopImmediatePropagation(); // Stop viewer print.

// Optional: Trigger your custom export logic here.

// handleCustomExport();

}

},

true,

);

});

When to use — When you need to replace standard shortcuts with custom workflows. Keep the logic focused by only blocking the specific keys you’re replacing.

Important — This overrides the default SDK behavior. Ensure you provide proper checks and equivalent functionality before removing standard shortcuts users expect.

Try this example in our Playground.

3. Modal priority: Make escape close your overlay first

The problem — You show a confirmation modal over the viewer, and the user presses Escape, expecting the modal to close, but the viewer’s toolbar collapses instead.

The solution — Intercept Escape when your modal is active, and restore normal behavior when it closes.

NutrientViewer.load(config).then(() => {

let modalActive = false; // Set this to `true` when your modal opens.

const handleEscape = (e) => {

if (e.key === "Escape" && modalActive) {

e.stopImmediatePropagation(); // Viewer never hears it.

console.log("Close your modal here");

// modalActive = false; // Optional depending on your UI.

}

};

document.addEventListener("keydown", handleEscape, true);

// In your UI code:

// modalActive = true; // When opening the modal.

// modalActive = false; // When closing the modal.

});

When to use — Any time you layer a user interface (UI) on top of the viewer — modals, dialogs, overlays. This ensures your UI gets priority without permanently breaking the viewer’s escape behavior.

Key detail — Make sure to only hijack Escape when modalActive is true. This conditional approach means the viewer works normally the rest of the time.

Try this example in our Playground.

4. Tool switcher: Keyboard shortcuts without focus loss

The problem — Users want to switch annotation tools via keyboard (like Photoshop’s tool shortcuts), but clicking a toolbar button steals focus from the document. Now they have to click back into the document before annotating.

The solution — Change the tool and immediately blur the active button so focus stays on the canvas.

NutrientViewer.load(config).then((instance) => {

const blurActiveBtn = () => {

instance.contentDocument

.querySelector(".NutrientViewer-Tool-Button-active")

?.blur();

};

document.addEventListener(

"keydown",

(e) => {

if (!e.ctrlKey && !e.metaKey) return;

switch (e.key) {

case "1":

e.preventDefault();

instance.setViewState((v) =>

v.set("interactionMode", NutrientViewer.InteractionMode.INK),

);

setTimeout(blurActiveBtn, 0);

break;

case "2":

e.preventDefault();

instance.setViewState((v) =>

v.set(

"interactionMode",

NutrientViewer.InteractionMode.SHAPE_RECTANGLE,

),

);

setTimeout(blurActiveBtn, 0);

break;

case "3":

e.preventDefault();

instance.setViewState((v) =>

v.set("interactionMode", NutrientViewer.InteractionMode.TEXT),

);

setTimeout(blurActiveBtn, 0);

break;

}

},

true,

);

});

When to use — For power users who annotate rapidly and want Photoshop-style tool switching. This pattern allows users to switch between annotation tools via keyboard shortcuts without losing focus on a document, streamlining workflows for reviewers who frequently cycle through tools.

Why setTimeout(..., 0)? — The viewer’s internal focus routine runs synchronously. By deferring the blur to the next event loop tick, this lets the viewer finish its focus logic first and then immediately unfocus. This is required for the pattern to work at all: The viewer runs its own synchronous focus logic, and deferring the blur with setTimeout(..., 0) ensures the viewer finishes that routine before removing focus.

Try this example in our Playground.

5. Disable delete keys without breaking the UI

The problem — When you display a modal or overlay on top of the viewer, pressing Escape often collapses the viewer’s toolbar instead of closing your modal. Both your app and the viewer listen for the same key, and the viewer wins the race because its handler fires first. This can confuse users who expect Escape to dismiss what’s visually on top.

The solution

  1. Intercept the Escape key while your modal is active so your app closes it before the event reaches the viewer.
  2. When the modal closes, restore the normal Escape behavior.
  • This approach ensures consistent, predictable keyboard interaction; your UI responds first, and the viewer remains unaffected underneath.

NutrientViewer.load(config).then((instance) => {

instance.addEventListener("annotations.willChange", (e) => {

if (e.reason === "deleteStart") e.cancelAllow();

});

document.addEventListener(

"keydown",

(e) => {

if (e.key === "Delete" || e.key === "Backspace") {

e.preventDefault();

e.stopImmediatePropagation();

}

},

true,

);

});

When to use — In workflows where deletion requires authorization (e.g. only managers can delete comments, or deletions must be logged for compliance). This pattern blocks accidental keyboard deletions while keeping your custom Delete with Approval buttons functional.

ImportantcancelAllow() blocks all deletion attempts — keyboard, toolbar, and API calls. Use it sparingly to avoid locking legitimate workflows. Moreover, to hide Delete buttons on the toolbar as well, here’s a CSS line you can add to take care of this in the Playground link below:

.PSPDFKit-Annotation-Toolbar [class*="-Toolbar-Button-Delete"] {

display: none;

}

Try this example in our Playground.

6. Pause all input during critical modals

The problem — You’re showing a permissions dialog or payment confirmation and users must make a decision before continuing. But they can still scroll the document, zoom with keyboard shortcuts, or accidentally trigger annotations behind your modal.

The solution — Temporarily block everything — keyboard, mouse, wheel, gestures — and restore it all when the modal closes.

NutrientViewer.load(config).then((instance) => {

const getRoot = () =>

// Viewer root inside Shadow DOM or iframe.

instance.contentDocument?.querySelector(".PSPDFKit-Root") ||

// Fallback for non-shadow setups.

document.querySelector(".PSPDFKit-Root") ||

document;

let paused = false;

let cleanupFns = [];

let prevTouchAction = null;

// Stop any input reaching the viewer.

const kill = (ev) => {

ev.preventDefault();

ev.stopImmediatePropagation();

ev.stopPropagation();

};

function addListener(el, type, handler, opts) {

el.addEventListener(type, handler, opts);

cleanupFns.push(() => el.removeEventListener(type, handler, opts));

}

function pauseViewer() {

if (paused) return;

const root = getRoot();

// Block keyboard, mouse, wheel, and touch inside the viewer.

const events = [

"mousedown",

"mouseup",

"click",

"dblclick",

"contextmenu",

"pointerdown",

"pointermove",

"pointerup",

"dragstart",

"keydown",

"keyup",

"keypress",

"wheel",

"touchstart",

"touchmove",

"touchend",

];

events.forEach((event) => {

addListener(root, event, kill, true); // Capture phase.

});

// Disable pinch-zoom and panning gestures.

if (root instanceof HTMLElement) {

prevTouchAction = root.style.touchAction;

root.style.touchAction = "none";

cleanupFns.push(() => {

root.style.touchAction = prevTouchAction ?? "";

prevTouchAction = null;

});

}

paused = true;

}

function resumeViewer() {

if (!paused) return;

for (let i = cleanupFns.length - 1; i >= 0; i--) {

try {

cleanupFns[i]();

} catch {}

}

cleanupFns = [];

paused = false;

}

// Optional helpers your modal can call.

let criticalModalOpen = false;

function showCriticalModal() {

criticalModalOpen = true;

pauseViewer();

}

function closeCriticalModal() {

criticalModalOpen = false;

resumeViewer();

}

// Allow Escape to close your modal if needed.

document.addEventListener(

"keydown",

(e) => {

if (e.key === "Escape" && criticalModalOpen) {

closeCriticalModal();

}

},

true,

);

// Prevent browser-level zoom (Control/Command + / - / =) while critical UI is active.

document.addEventListener(

"keydown",

(e) => {

if (!criticalModalOpen) return;

const isZoomShortcut =

(e.metaKey || e.ctrlKey) &&

(e.key === "+" || e.key === "=" || e.key === "-");

if (isZoomShortcut) {

e.preventDefault();

}

},

true,

);

});

When to use — For truly critical UIs that demand user attention, such as payment confirmations, destructive action warnings, and permission requests. This pattern is absolute: It shuts down all input. Use it sparingly.

Why this works — By attaching kill handlers at the root, document, and window levels in the capturing phase, you’re creating a three-layer shield, and nothing gets through. Setting touch-action: none disables built-in browser gestures like pinch-zoom or panning; this ensures touch and pointer events are entirely controlled by your handlers instead of the browser’s gesture recognizer.

Critical reminder — Remember to call resumeViewer() when closing your modal. Forgetting this leaves your viewer permanently frozen.

Debugging checklist

If your shortcuts aren’t working, follow this checklist.

  1. Are you using capture phase? Check for true as the third parameter in addEventListener.
  2. Did you forget preventDefault()? Default actions will still fire without it.
  3. Are you listening to both document and contentDocument? Pick one to avoid duplicates.
  4. Did you test in the same setup as production? Differences between development and production setups can affect keyboard behavior in unexpected ways.
  5. Are other handlers stealing your events? Check your browser DevTools Event Listeners panel.
  6. Are you attaching listeners after initialization? instance.contentDocument (or the shadow root) becomes available once the viewer has loaded. Attach viewer-specific shortcuts there when the intent is to affect only the viewer.

Key principle — Always clean up listeners when your component unmounts. Orphaned listeners cause memory leaks and unpredictable behavior.

Reference documentation

Wrapping up

Understanding event capture and how embedding model boundaries affect event flow gives you complete control over keyboard interactions in Nutrient Web SDK. These six patterns cover the most common scenarios, but the principles extend to any custom keyboard behavior you need.

Five key takeaways:

  1. Always use the capturing phase (true parameter) for reliable keyboard shortcuts.
  2. Choose one listener target (document or contentDocument) to avoid duplicates.
  3. The patterns work identically for both Shadow DOM and iframe implementations.
  4. Clean up listeners when components unmount to prevent leaks.
  5. Test with proper checks before overriding default SDK behavior.

Whether you’re building power user annotation workflows or custom modals, or you just need Control-1 to switch to the ink tool, these patterns provide a solid foundation that’s predictable, conflict-free, and easy to extend.

Have questions or unique keyboard handling challenges? Reach out to our Support team.