Skip to content

Added click handling and HTML attribute link options to the editor#2656

Open
matthewlipski wants to merge 1 commit intomainfrom
link-options
Open

Added click handling and HTML attribute link options to the editor#2656
matthewlipski wants to merge 1 commit intomainfrom
link-options

Conversation

@matthewlipski
Copy link
Copy Markdown
Collaborator

@matthewlipski matthewlipski commented Apr 17, 2026

Summary

This PR adds a new editor option:

links: Partial<{
  HTMLAttributes: Record<string, any>;
  onClick?: (event: MouseEvent) => void;
}>

These do basically what they say - HTMLAttributes adds HTML attributes to rendered link elements and onClick replaces the default click behaviour (which opens the link in a new tab).

Closes #1539

Rationale

Some users are finding it annoying that links open a new tab on click when they're just trying to move the selection.

HTML attributes allow for slight customization for link rendering. It's the best we can do atm, but really more of a stopgap solution as consumers should ideally be able to override the default link rendering with whatever they want.

Changes

See above.

Impact

N/A

Testing

N/A (example needed?)

Screenshots/Video

N/A

Checklist

  • Code follows the project's coding standards.
  • Unit tests covering the new feature have been added.
  • All existing tests pass.
  • The documentation has been updated to reflect the new feature

Additional Notes

Summary by CodeRabbit

  • New Features

    • Introduced link customization options to add custom HTML attributes to links and override default click behavior with a custom handler.
  • Documentation

    • Added "Customizing Links" section documenting the new link configuration capabilities.

@vercel
Copy link
Copy Markdown

vercel bot commented Apr 17, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
blocknote Ready Ready Preview Apr 17, 2026 3:37pm
blocknote-website Ready Ready Preview Apr 17, 2026 3:37pm

Request Review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 17, 2026

📝 Walkthrough

Walkthrough

This PR introduces a new links configuration option to BlockNote's editor, enabling developers to customize HTML attributes on rendered link elements and override default link click behavior with custom handlers via a click event callback.

Changes

Cohort / File(s) Summary
Documentation
docs/content/docs/features/blocks/inline-content.mdx
Added "Customizing Links" section documenting the links editor option, with examples for HTMLAttributes and onClick handler configuration.
Type Definitions
packages/core/src/editor/BlockNoteEditor.ts
Extended BlockNoteEditorOptions interface with optional links property supporting HTMLAttributes (for extra HTML attributes) and onClick (for custom click handler).
Link Extension Implementation
packages/core/src/editor/managers/ExtensionManager/extensions.ts
Added ProseMirror plugin to intercept left-button clicks on links, executing custom onClick handler if provided or defaulting to window.open with link href and target attributes; disabled built-in openOnClick behavior.

Sequence Diagram

sequenceDiagram
    actor User
    participant Editor as BlockNote Editor
    participant Plugin as Link Click Plugin
    participant Handler as Custom onClick/<br/>window.open

    User->>Editor: Click on link element
    Editor->>Plugin: ProseMirror plugin intercepts click
    Plugin->>Plugin: Verify left-click & editability
    Plugin->>Plugin: Find nearest <a> element<br/>(within editor root)
    alt Custom onClick provided
        Plugin->>Handler: Invoke options.links.onClick(event)
        Handler->>Handler: Execute custom logic
    else No custom onClick
        Plugin->>Handler: window.open(href, target)
        Handler->>Handler: Open link in target window
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 Hop, hop! The links now dance and bend,
With custom clicks around the bend!
No more default ways so plain,
Handler magic fills the lane! 🔗✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Added click handling and HTML attribute link options to the editor' clearly and accurately summarizes the main changes: introduction of link click handling and HTML attribute customization options.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Description check ✅ Passed The PR description follows the required template structure with all major sections completed: Summary, Rationale, Changes, Impact, Testing, Screenshots, Checklist, and Additional Notes. All checklist items are marked as complete.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch link-options

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (4)
packages/core/src/editor/managers/ExtensionManager/extensions.ts (2)

145-152: Remove commented-out enableClickSelection block.

Dead commented-out code referring to an option that is "always disabled" is noise — if it's not coming back in this PR, prefer deleting it (it'll still be in git history). If it's a planned follow-up, a TODO referencing an issue is clearer than a paragraph of commented code.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/editor/managers/ExtensionManager/extensions.ts` around
lines 145 - 152, Remove the dead commented-out block that references
enableClickSelection and the extendMarkRange call (the commented lines
containing "if (options.enableClickSelection) {",
"tiptapEditor.commands.extendMarkRange", and "markType.name"); delete those
commented lines rather than leaving them in place, or if you want to keep a note
add a single-line TODO referencing the issue number, but do not keep the
multi-line commented code in ExtensionManager/extensions.ts.

96-174: Missing test coverage for the new click handler.

The PR checklist claims "Unit tests covering the new feature have been added", but this handler has non-trivial branching (left-button check, editable check, ancestor walk bounded to editor root, onClick override vs. window.open fallback) and several of the bugs above would be caught by straightforward tests. Consider adding unit tests that at minimum cover:

  • Non-left-button click (should be ignored).
  • Click on non-anchor descendant inside an anchor (ancestor lookup).
  • Anchor outside editor root (should not be handled).
  • onClick provided → onClick invoked and window.open not called.
  • onClick not provided → window.open called with correct href/target, including when those come from mark attrs rather than DOM attributes.

Happy to sketch out a test file for this handler if you'd like.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/editor/managers/ExtensionManager/extensions.ts` around
lines 96 - 174, Add unit tests for the click handler produced by
ExtensionManager.addProseMirrorPlugins: exercise the Plugin.props.handleClick
branching (check non-left-button returns false; clicking a non-anchor descendant
inside an anchor resolves via closest; clicks on anchors outside the
tiptapEditor.view.dom root are ignored), verify options.links.onClick is invoked
when present (and window.open is not called), and verify window.open is called
with correct href/target when onClick is absent, including when href/target are
provided via getAttributes(view.state, markType.name) rather than the DOM
anchor; target the handleClick logic by constructing a mock view
(tiptapEditor.view), stub getAttributes, and spy on window.open and
options.links.onClick to assert behavior.
docs/content/docs/features/blocks/inline-content.mdx (1)

115-127: Clarify onClick semantics in the docs.

The description states that providing onClick disables the default open-in-new-window behavior, but it does not mention a few subtleties that users are likely to hit:

  • The handler receives only the raw MouseEvent; to get href/target, the consumer has to walk event.target up to the nearest <a> themselves. Exposing at least the anchor element (or href/target) in the callback signature would make this much more usable.
  • The default (no onClick) currently opens via window.open(href, target) with no noopener/noreferrer — worth documenting so consumers know when to supply their own handler for security-sensitive contexts.
  • It would help to mention that the callback is only invoked on primary-button (left) clicks inside an editable view.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/content/docs/features/blocks/inline-content.mdx` around lines 115 - 127,
Update the onClick docs for BlockNoteEditor.links to clarify behavior: state
that links.onClick receives only the raw MouseEvent (not the anchor or
href/target) so consumers must walk event.target up to the nearest <a> to read
href/target, note the default behavior when onClick is undefined is
window.open(href, target) without noopener/noreferrer (so recommend supplying a
custom handler for security-sensitive contexts), and mention the callback is
only invoked for primary-button (left) clicks inside an editable view; reference
the onClick option and BlockNoteEditor.links in the text so readers can locate
the setting.
packages/core/src/editor/BlockNoteEditor.ts (1)

143-161: Consider letting onClick signal whether it handled the event.

The current signature (event: MouseEvent) => void forces the editor to treat every onClick invocation as fully handling the click (see extensions.ts lines 154–165), which is fine for the documented "custom routing" use case but makes it impossible for a consumer to fall through to the default open-in-new-window behavior conditionally (e.g. ignore modifier-clicks, or only intercept same-origin links). Returning void | boolean — where true means "handled, skip default" and falsy means "fall through to default" — would be a more forward-compatible API. Not blocking, just easier to evolve than widening the return type later.

-    onClick?: (event: MouseEvent) => void;
+    onClick?: (event: MouseEvent) => void | boolean;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/editor/BlockNoteEditor.ts` around lines 143 - 161, Update
the links.onClick handler to return a boolean-ish value so consumers can signal
whether they handled the event: change the documented signature from (event:
MouseEvent) => void to (event: MouseEvent) => boolean | void (or boolean |
undefined), update the implementation that invokes onClick (the caller in
extensions.ts that currently treats any invocation as handled) to only suppress
the default open-on-click behavior when onClick returns a truthy value, and
update the JSDoc comment for links.HTMLAttributes/onClick to explain that
returning true prevents the default open-in-new-window behavior while falsy
allows fallback.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/content/docs/features/blocks/inline-content.mdx`:
- Around line 86-98: The docs show using the protected constructor (new
BlockNoteEditor(...)) which is not public; update the three examples to call the
public factory instead by replacing new BlockNoteEditor({...}) with
BlockNoteEditor.create({...}) so they match the editor's public API (constructor
is protected in BlockNoteEditor.ts) and will type-check for users.

In `@packages/core/src/editor/managers/ExtensionManager/extensions.ts`:
- Around line 143-168: The handleClick branch that calls options.links?.onClick
currently leaves handled as false so the DOM click may still trigger navigation;
update the onClick branch in the handleClick implementation to prevent default
browser navigation and mark the click as handled — e.g., call
event.preventDefault() (and optionally event.stopPropagation()) and set handled
= true after invoking options.links.onClick(event) so the function returns true
and the click is consumed; keep the existing fallback behavior (using
getAttributes/href/window.open) unchanged.
- Around line 130-131: Fix the typo and complete the truncated comment in
extensions.ts: change "Tntentionally" to "Intentionally" and finish the sentence
so it reads clearly (e.g., "Intentionally limit the lookup to the editor root.
Using tag names like DIV as boundaries breaks with custom NodeViews, so we must
scope to the editor root to avoid incorrect boundary detection."). Update the
comment near the ExtensionManager or extensions.ts lookup logic (the block that
mentions editor root and NodeViews) so it is grammatically correct and conveys
the full rationale.
- Around line 161-163: The current call window.open(href, target) is vulnerable
to tabnabbing; update the logic where href/target are used (the branch that sets
handled = true) to call window.open with the feature string
"noopener,noreferrer" (e.g., window.open(href, target, "noopener,noreferrer"))
so the opened page cannot access window.opener; ensure this is applied whenever
opening external links from the editor (the code path using href, target,
handled) and keep handled = true unchanged.
- Around line 158-164: Replace uses of the DOM IDL properties so fallback values
work: read href and target via link.getAttribute('href') and
link.getAttribute('target') (e.g., const hrefAttr = link.getAttribute('href') ??
attrs.href; const linkTarget = link.getAttribute('target') ?? attrs.target)
instead of link.href / link.target; rename the local target variable to avoid
shadowing the DOM property (e.g., linkTarget) and pass that to window.open;
ensure that when options.links.onClick is called you set handled = true (or call
event.preventDefault()) so ProseMirror doesn't allow default navigation; also
correct the typo "Tntentionally" to "Intentionally" in the surrounding
comment/strings.

---

Nitpick comments:
In `@docs/content/docs/features/blocks/inline-content.mdx`:
- Around line 115-127: Update the onClick docs for BlockNoteEditor.links to
clarify behavior: state that links.onClick receives only the raw MouseEvent (not
the anchor or href/target) so consumers must walk event.target up to the nearest
<a> to read href/target, note the default behavior when onClick is undefined is
window.open(href, target) without noopener/noreferrer (so recommend supplying a
custom handler for security-sensitive contexts), and mention the callback is
only invoked for primary-button (left) clicks inside an editable view; reference
the onClick option and BlockNoteEditor.links in the text so readers can locate
the setting.

In `@packages/core/src/editor/BlockNoteEditor.ts`:
- Around line 143-161: Update the links.onClick handler to return a boolean-ish
value so consumers can signal whether they handled the event: change the
documented signature from (event: MouseEvent) => void to (event: MouseEvent) =>
boolean | void (or boolean | undefined), update the implementation that invokes
onClick (the caller in extensions.ts that currently treats any invocation as
handled) to only suppress the default open-on-click behavior when onClick
returns a truthy value, and update the JSDoc comment for
links.HTMLAttributes/onClick to explain that returning true prevents the default
open-in-new-window behavior while falsy allows fallback.

In `@packages/core/src/editor/managers/ExtensionManager/extensions.ts`:
- Around line 145-152: Remove the dead commented-out block that references
enableClickSelection and the extendMarkRange call (the commented lines
containing "if (options.enableClickSelection) {",
"tiptapEditor.commands.extendMarkRange", and "markType.name"); delete those
commented lines rather than leaving them in place, or if you want to keep a note
add a single-line TODO referencing the issue number, but do not keep the
multi-line commented code in ExtensionManager/extensions.ts.
- Around line 96-174: Add unit tests for the click handler produced by
ExtensionManager.addProseMirrorPlugins: exercise the Plugin.props.handleClick
branching (check non-left-button returns false; clicking a non-anchor descendant
inside an anchor resolves via closest; clicks on anchors outside the
tiptapEditor.view.dom root are ignored), verify options.links.onClick is invoked
when present (and window.open is not called), and verify window.open is called
with correct href/target when onClick is absent, including when href/target are
provided via getAttributes(view.state, markType.name) rather than the DOM
anchor; target the handleClick logic by constructing a mock view
(tiptapEditor.view), stub getAttributes, and spy on window.open and
options.links.onClick to assert behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c07edf04-54b9-4e24-985b-228535edc5ce

📥 Commits

Reviewing files that changed from the base of the PR and between cb51b28 and 54f4f7a.

📒 Files selected for processing (3)
  • docs/content/docs/features/blocks/inline-content.mdx
  • packages/core/src/editor/BlockNoteEditor.ts
  • packages/core/src/editor/managers/ExtensionManager/extensions.ts

Comment on lines +86 to +98
```ts
const editor = new BlockNoteEditor({
links: {
HTMLAttributes: {
class: "my-link-class",
target: "_blank",
},
onClick: (event) => {
// Custom click logic, e.g. routing without a page reload.
},
},
});
```
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Docs examples use new BlockNoteEditor(...), but the constructor is protected.

In packages/core/src/editor/BlockNoteEditor.ts, the constructor is declared protected and the public entry point is the static BlockNoteEditor.create(...). All three snippets in this section (lines 86–98, 104–113, 119–127) invoke new BlockNoteEditor(...), which is not a valid public API and will fail type-checking for users copy-pasting these examples. Consider switching to BlockNoteEditor.create({ ... }) to match the rest of the documentation.

📝 Proposed fix (apply to all three examples)
-const editor = new BlockNoteEditor({
+const editor = BlockNoteEditor.create({
   links: {
     ...
   },
 });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
```ts
const editor = new BlockNoteEditor({
links: {
HTMLAttributes: {
class: "my-link-class",
target: "_blank",
},
onClick: (event) => {
// Custom click logic, e.g. routing without a page reload.
},
},
});
```
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/content/docs/features/blocks/inline-content.mdx` around lines 86 - 98,
The docs show using the protected constructor (new BlockNoteEditor(...)) which
is not public; update the three examples to call the public factory instead by
replacing new BlockNoteEditor({...}) with BlockNoteEditor.create({...}) so they
match the editor's public API (constructor is protected in BlockNoteEditor.ts)
and will type-check for users.

Comment on lines +130 to +131
// Tntentionally limit the lookup to the editor root.
// Using tag names like DIV as boundaries breaks with custom NodeViews,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Typo: "Tntentionally" → "Intentionally", and the comment sentence is truncated ("Using tag names like DIV as boundaries breaks with custom NodeViews,").

-                    // Tntentionally limit the lookup to the editor root.
-                    // Using tag names like DIV as boundaries breaks with custom NodeViews,
+                    // Intentionally limit the lookup to the editor root.
+                    // Using tag names like DIV as boundaries breaks with custom NodeViews.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Tntentionally limit the lookup to the editor root.
// Using tag names like DIV as boundaries breaks with custom NodeViews,
// Intentionally limit the lookup to the editor root.
// Using tag names like DIV as boundaries breaks with custom NodeViews.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/editor/managers/ExtensionManager/extensions.ts` around
lines 130 - 131, Fix the typo and complete the truncated comment in
extensions.ts: change "Tntentionally" to "Intentionally" and finish the sentence
so it reads clearly (e.g., "Intentionally limit the lookup to the editor root.
Using tag names like DIV as boundaries breaks with custom NodeViews, so we must
scope to the editor root to avoid incorrect boundary detection."). Update the
comment near the ExtensionManager or extensions.ts lookup logic (the block that
mentions editor root and NodeViews) so it is grammatically correct and conveys
the full rationale.

Comment on lines +143 to +168
let handled = false;

// `enableClickSelection` is always disabled.
// if (options.enableClickSelection) {
// const commandResult =
// tiptapEditor.commands.extendMarkRange(
// markType.name,
// );
// handled = commandResult;
// }

if (options.links?.onClick) {
options.links.onClick(event);
} else {
const attrs = getAttributes(view.state, markType.name);
const href = link.href ?? attrs.href;
const target = link.target ?? attrs.target;

if (href) {
window.open(href, target);
handled = true;
}
}

return handled;
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

onClick branch returns handled = false, so the browser may still navigate.

When options.links?.onClick is provided, handled stays false, and the plugin returns false from handleClick. ProseMirror then does not treat the click as consumed, and the underlying DOM click event continues to propagate — for a real <a href="…">, the browser's default navigation will fire after your custom handler, which typically defeats the whole point of overriding onClick (e.g. SPA routing) and can cause a full page reload.

Either return true (and/or call event.preventDefault()) when onClick is invoked, or document that consumers must call preventDefault() themselves. The former matches the documented behavior ("default behavior is disabled and this function is called instead"):

🐛 Proposed fix
-                  if (options.links?.onClick) {
-                    options.links.onClick(event);
+                  if (options.links?.onClick) {
+                    options.links.onClick(event);
+                    handled = true;
                   } else {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/editor/managers/ExtensionManager/extensions.ts` around
lines 143 - 168, The handleClick branch that calls options.links?.onClick
currently leaves handled as false so the DOM click may still trigger navigation;
update the onClick branch in the handleClick implementation to prevent default
browser navigation and mark the click as handled — e.g., call
event.preventDefault() (and optionally event.stopPropagation()) and set handled
= true after invoking options.links.onClick(event) so the function returns true
and the click is consumed; keep the existing fallback behavior (using
getAttributes/href/window.open) unchanged.

Comment on lines +158 to +164
const href = link.href ?? attrs.href;
const target = link.target ?? attrs.target;

if (href) {
window.open(href, target);
handled = true;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

HTMLAnchorElement href and target IDL attribute return type when attribute absent

💡 Result:

When the href attribute is absent on an HTMLAnchorElement, the href IDL attribute returns the empty string (""), as explicitly stated in MDN documentation with examples showing a newly created <a> element returns "". This differs from setting href to an empty string, which resolves to the document's base URL. When the target attribute is absent, the target IDL attribute returns the empty string (""), as both are reflected DOMString attributes per the HTML specification's reflection rules: the getter returns the attribute's value or null/empty string if absent, and MDN confirms it "reflects the target attribute" with no default value specified.

Citations:


🏁 Script executed:

find . -type f -name "extensions.ts" | grep -i extensionmanager

Repository: TypeCellOS/BlockNote

Length of output: 130


🏁 Script executed:

cat -n ./packages/core/src/editor/managers/ExtensionManager/extensions.ts | sed -n '110,180p'

Repository: TypeCellOS/BlockNote

Length of output: 2993


🏁 Script executed:

cat -n ./packages/core/src/editor/managers/ExtensionManager/extensions.ts | sed -n '1,50p'

Repository: TypeCellOS/BlockNote

Length of output: 2019


🏁 Script executed:

cat -n ./packages/core/src/editor/managers/ExtensionManager/extensions.ts | sed -n '100,175p'

Repository: TypeCellOS/BlockNote

Length of output: 3229


link.href ?? attrs.href and link.target ?? attrs.target never fall back.

HTMLAnchorElement.href and HTMLAnchorElement.target are IDL attributes that always return a string — they are never null/undefined. For an anchor with no href attribute, link.href is "", and for no target, link.target is "". Because nullish coalescing only falls back on null/undefined, the attrs.href / attrs.target paths are effectively dead code, and when the rendered anchor has an empty target, you'll pass "" to window.open instead of the mark's configured target.

Additionally, when options.links.onClick is provided (line 154), handled is never set to true, so the handler returns false and ProseMirror doesn't claim the click, allowing default browser navigation to proceed even though the callback was fired. The callback should either set handled = true or call event.preventDefault().

Also fix the typo at line 130: "Tntentionally" → "Intentionally".

Use getAttribute(...) (which returns null when missing and round-trips through ??), rename target to avoid confusion with the DOM element property, and set handled = true when onClick is invoked:

🐛 Proposed fix
                  if (options.links?.onClick) {
                    options.links.onClick(event);
+                   handled = true;
                  } else {
                    const attrs = getAttributes(view.state, markType.name);
-                   const href = link.href ?? attrs.href;
-                   const target = link.target ?? attrs.target;
+                   const href = link.getAttribute("href") || attrs.href;
+                   const linkTarget =
+                     link.getAttribute("target") || attrs.target;

                    if (href) {
-                     window.open(href, target);
+                     window.open(href, linkTarget);
                      handled = true;
                    }
                  }

-                 // Tntentionally limit the lookup to the editor root.
+                 // Intentionally limit the lookup to the editor root.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const href = link.href ?? attrs.href;
const target = link.target ?? attrs.target;
if (href) {
window.open(href, target);
handled = true;
}
const href = link.getAttribute("href") || attrs.href;
const linkTarget =
link.getAttribute("target") || attrs.target;
if (href) {
window.open(href, linkTarget);
handled = true;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/editor/managers/ExtensionManager/extensions.ts` around
lines 158 - 164, Replace uses of the DOM IDL properties so fallback values work:
read href and target via link.getAttribute('href') and
link.getAttribute('target') (e.g., const hrefAttr = link.getAttribute('href') ??
attrs.href; const linkTarget = link.getAttribute('target') ?? attrs.target)
instead of link.href / link.target; rename the local target variable to avoid
shadowing the DOM property (e.g., linkTarget) and pass that to window.open;
ensure that when options.links.onClick is called you set handled = true (or call
event.preventDefault()) so ProseMirror doesn't allow default navigation; also
correct the typo "Tntentionally" to "Intentionally" in the surrounding
comment/strings.

Comment on lines +161 to +163
if (href) {
window.open(href, target);
handled = true;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

window.open(href, target) without noopener,noreferrer is a tabnabbing risk.

Opening a user-controlled URL in a new browsing context without noopener leaves window.opener accessible to the destination page, which can then navigate the original editor tab via window.opener.location. Since link href/target come from editor content (often user-authored), this should use "noopener,noreferrer" features by default:

-                      window.open(href, target);
+                      window.open(href, linkTarget || "_blank", "noopener,noreferrer");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (href) {
window.open(href, target);
handled = true;
if (href) {
window.open(href, target, "noopener,noreferrer");
handled = true;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/editor/managers/ExtensionManager/extensions.ts` around
lines 161 - 163, The current call window.open(href, target) is vulnerable to
tabnabbing; update the logic where href/target are used (the branch that sets
handled = true) to call window.open with the feature string
"noopener,noreferrer" (e.g., window.open(href, target, "noopener,noreferrer"))
so the opened page cannot access window.opener; ensure this is applied whenever
opening external links from the editor (the code path using href, target,
handled) and keep handled = true unchanged.

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Apr 17, 2026

Open in StackBlitz

@blocknote/ariakit

npm i https://pkg.pr.new/@blocknote/ariakit@2656

@blocknote/code-block

npm i https://pkg.pr.new/@blocknote/code-block@2656

@blocknote/core

npm i https://pkg.pr.new/@blocknote/core@2656

@blocknote/mantine

npm i https://pkg.pr.new/@blocknote/mantine@2656

@blocknote/react

npm i https://pkg.pr.new/@blocknote/react@2656

@blocknote/server-util

npm i https://pkg.pr.new/@blocknote/server-util@2656

@blocknote/shadcn

npm i https://pkg.pr.new/@blocknote/shadcn@2656

@blocknote/xl-ai

npm i https://pkg.pr.new/@blocknote/xl-ai@2656

@blocknote/xl-docx-exporter

npm i https://pkg.pr.new/@blocknote/xl-docx-exporter@2656

@blocknote/xl-email-exporter

npm i https://pkg.pr.new/@blocknote/xl-email-exporter@2656

@blocknote/xl-multi-column

npm i https://pkg.pr.new/@blocknote/xl-multi-column@2656

@blocknote/xl-odt-exporter

npm i https://pkg.pr.new/@blocknote/xl-odt-exporter@2656

@blocknote/xl-pdf-exporter

npm i https://pkg.pr.new/@blocknote/xl-pdf-exporter@2656

commit: 54f4f7a

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Click on links are not intercepted

1 participant