Skip to content

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

Open
matthewlipski wants to merge 2 commits intomainfrom
link-options
Open

Added click handling and HTML attribute link options to the editor#2656
matthewlipski wants to merge 2 commits 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

    • Added link customization: specify HTML attributes for rendered links and provide a custom click handler to override default click behavior.
  • Documentation

    • Added a "Customizing Links" section that explains using the new link options, including how attributes are applied and how providing a handler changes click behavior (otherwise links retain their default open behavior).

@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

Adds a links option to the editor API and updates the Link extension to inject a ProseMirror plugin that intercepts left-clicks on anchor elements; supports links.HTMLAttributes for rendered anchor attributes and links.onClick to handle clicks instead of default navigation.

Changes

Cohort / File(s) Summary
Documentation
docs/content/docs/features/blocks/inline-content.mdx
Added "Customizing Links" section documenting links.HTMLAttributes and links.onClick usage.
Type Definitions
packages/core/src/editor/BlockNoteEditor.ts
Extended exported BlockNoteEditorOptions with optional links?: { HTMLAttributes?: Record<string, any>; onClick?: (event: MouseEvent) => void }.
Link Extension Implementation
packages/core/src/editor/managers/ExtensionManager/extensions.ts
Link extension now configures HTMLAttributes from options.links?.HTMLAttributes ?? {} and openOnClick: false; adds addProseMirrorPlugins() to register a plugin with handleClick that intercepts left-clicks within the editor root, resolves the nearest <a> element, calls options.links?.onClick(event) if provided (marking the event handled), or performs default navigation using href/target from the element or mark attributes.

Sequence Diagram

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

    User->>Editor: click on link
    Editor->>Plugin: dispatch click to plugin
    Plugin->>Plugin: check left-button & editable
    Plugin->>Plugin: find nearest <a> within editor root
    alt options.links.onClick provided
        Plugin->>Handler: invoke options.links.onClick(event)
        Handler->>Plugin: handler handles event
    else no onClick
        Plugin->>Handler: extract href/target and call window.open(href, target)
        Handler->>Plugin: navigation performed
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐇 I hopped through docs and code tonight,

links now listen when you click them right.
HTML bits and handlers too,
I guard the editor — that's what I do. ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main changes: adding click handling and HTML attribute customization options for links in the editor.
Description check ✅ Passed The description covers key sections (Summary, Rationale, Changes) and includes a checklist indicating testing and documentation updates, though some sections lack detailed content.
Linked Issues check ✅ Passed The PR implements click interception via the onClick option [#1539] and provides HTML attribute customization, directly addressing the requirement to prevent accidental navigation.
Out of Scope Changes check ✅ Passed All changes are directly scoped to implementing the links editor option (click handling and HTML attributes) as required by issue #1539; documentation update is in scope.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ 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 thread docs/content/docs/features/blocks/inline-content.mdx
Comment thread packages/core/src/editor/managers/ExtensionManager/extensions.ts Outdated
Comment thread packages/core/src/editor/managers/ExtensionManager/extensions.ts
Comment thread packages/core/src/editor/managers/ExtensionManager/extensions.ts
Comment thread packages/core/src/editor/managers/ExtensionManager/extensions.ts
@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: 7ec964f

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.

♻️ Duplicate comments (1)
packages/core/src/editor/managers/ExtensionManager/extensions.ts (1)

158-164: ⚠️ Potential issue | 🟠 Major

Fix the default-open path before relying on fallbacks.

link.href ?? attrs.href and link.target ?? attrs.target still won’t fall back when the DOM properties return "", and window.open(href, target) should include noopener,noreferrer for user-authored links.

🐛 Proposed fix
-                    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 || "_blank",
+                        "noopener,noreferrer",
+                      );
                       handled = true;
                     }
Verify HTMLAnchorElement.href and HTMLAnchorElement.target behavior when the corresponding attributes are absent, and verify that window.open with "noopener,noreferrer" prevents opener access.
🤖 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, The current fallback uses link.href ?? attrs.href which treats
empty string as valid — change to use a truthy check so href = (link.href ?? "")
|| attrs.href and target = (link.target ?? "") || attrs.target, then only call
window.open if href is non-empty; when opening user links include the
noopener,noreferrer feature string in the third argument to window.open to
prevent opener access (use the same conditional branch that currently calls
window.open and reference getAttributes, markType.name, link.href/target,
attrs.href/target, and window.open).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@packages/core/src/editor/managers/ExtensionManager/extensions.ts`:
- Around line 158-164: The current fallback uses link.href ?? attrs.href which
treats empty string as valid — change to use a truthy check so href = (link.href
?? "") || attrs.href and target = (link.target ?? "") || attrs.target, then only
call window.open if href is non-empty; when opening user links include the
noopener,noreferrer feature string in the third argument to window.open to
prevent opener access (use the same conditional branch that currently calls
window.open and reference getAttributes, markType.name, link.href/target,
attrs.href/target, and window.open).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f62267d0-cfad-4274-b9e0-3adbea9769d5

📥 Commits

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

📒 Files selected for processing (2)
  • docs/content/docs/features/blocks/inline-content.mdx
  • packages/core/src/editor/managers/ExtensionManager/extensions.ts
✅ Files skipped from review due to trivial changes (1)
  • docs/content/docs/features/blocks/inline-content.mdx

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