Skip to content

Commit f6717b3

Browse files
fix: Clicking comment overlapping link opens link (BLO-1091) (#2696)
* Made first click of overlapping comment/link only show comment and added test * Refactored extensions to both be BlockNote extensions, use `runsBefore` instead of `priority` * Removed log * Implemented PR feedback * Updated AI test snapshot
1 parent c48abc8 commit f6717b3

6 files changed

Lines changed: 117 additions & 25 deletions

File tree

packages/core/src/comments/extension.ts

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,8 @@ export const CommentsExtension = createExtension(
158158
return {
159159
key: "comments",
160160
store,
161+
runsBefore: ["link"],
162+
tiptapExtensions: [CommentMark],
161163
prosemirrorPlugins: [
162164
new Plugin<CommentsPluginState>({
163165
key: PLUGIN_KEY,
@@ -224,7 +226,7 @@ export const CommentsExtension = createExtension(
224226
},
225227
handleClick: (view, pos, event) => {
226228
if (event.button !== 0) {
227-
return;
229+
return false;
228230
}
229231

230232
const node = view.state.doc.nodeAt(pos);
@@ -235,23 +237,41 @@ export const CommentsExtension = createExtension(
235237
...prev,
236238
selectedThreadId: undefined,
237239
}));
238-
return;
240+
return false;
239241
}
240242

241243
const commentMark = node.marks.find(
242244
(mark) =>
243245
mark.type.name === markType && mark.attrs.orphan !== true,
244246
);
245247

246-
const threadId = commentMark?.attrs.threadId as
247-
| string
248-
| undefined;
249-
if (threadId !== store.state.selectedThreadId) {
250-
store.setState((prev) => ({
251-
...prev,
252-
selectedThreadId: threadId,
253-
}));
248+
if (!commentMark) {
249+
// Clicked outside any comment thread. Deselect if needed but
250+
// don't consume the event so other handlers (e.g. link
251+
// navigation) can process it.
252+
if (store.state.selectedThreadId !== undefined) {
253+
store.setState((prev) => ({
254+
...prev,
255+
selectedThreadId: undefined,
256+
}));
257+
}
258+
return false;
259+
}
260+
261+
const threadId = commentMark.attrs.threadId as string;
262+
263+
// If the clicked thread is already selected, do nothing and let
264+
// other handlers process the event (e.g. navigating a link).
265+
if (threadId === store.state.selectedThreadId) {
266+
return false;
254267
}
268+
269+
store.setState((prev) => ({
270+
...prev,
271+
selectedThreadId: threadId,
272+
}));
273+
274+
return true;
255275
},
256276
},
257277
}),
@@ -356,7 +376,6 @@ export const CommentsExtension = createExtension(
356376
},
357377
userStore,
358378
commentEditorSchema,
359-
tiptapExtensions: [CommentMark],
360379
} as const;
361380
},
362381
);

packages/core/src/editor/managers/ExtensionManager/extensions.ts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
Extension as TiptapExtension,
66
} from "@tiptap/core";
77
import { Gapcursor } from "@tiptap/extensions/gap-cursor";
8-
import { Link } from "../../../extensions/tiptap-extensions/Link/link.js";
8+
import { LinkExtension } from "../../../extensions/tiptap-extensions/Link/link.js";
99
import { Text } from "@tiptap/extension-text";
1010
import { createDropFileExtension } from "../../../api/clipboard/fromClipboard/fileDropExtension.js";
1111
import { createPasteFromClipboardExtension } from "../../../api/clipboard/fromClipboard/pasteExtension.js";
@@ -73,14 +73,6 @@ export function getDefaultTiptapExtensions(
7373
SuggestionAddMark,
7474
SuggestionDeleteMark,
7575
SuggestionModificationMark,
76-
Link.configure({
77-
HTMLAttributes: options.links?.HTMLAttributes ?? {},
78-
editor,
79-
onClick: options.links?.onClick,
80-
...(options.links?.isValidLink
81-
? { isValidLink: options.links.isValidLink }
82-
: {}),
83-
}),
8476
...(Object.values(editor.schema.styleSpecs).map((styleSpec) => {
8577
return styleSpec.implementation.mark.configure({
8678
editor: editor,
@@ -169,6 +161,13 @@ export function getDefaultExtensions(
169161
DropCursorExtension(options),
170162
FilePanelExtension(options),
171163
FormattingToolbarExtension(options),
164+
LinkExtension({
165+
HTMLAttributes: options.links?.HTMLAttributes ?? {},
166+
onClick: options.links?.onClick,
167+
...(options.links?.isValidLink
168+
? { isValidLink: options.links.isValidLink }
169+
: {}),
170+
}),
172171
LinkToolbarExtension(options),
173172
NodeSelectionKeyboardExtension(),
174173
PlaceholderExtension(options),
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
export { Link } from "./link.js";
1+
export { Link, LinkExtension } from "./link.js";
22
export { isAllowedUri } from "./link.js";

packages/core/src/extensions/tiptap-extensions/Link/link.ts

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { PasteRuleMatch } from "@tiptap/core";
22
import { Mark, markPasteRule, mergeAttributes } from "@tiptap/core";
33
import type { Plugin } from "@tiptap/pm/state";
44
import type { BlockNoteEditor } from "../../../editor/BlockNoteEditor.js";
5+
import { createExtension } from "../../../editor/BlockNoteExtension.js";
56
import { autolink } from "./helpers/autolink.js";
67
import { findLinks } from "./helpers/linkDetector.js";
78
import { clickHandler } from "./helpers/clickHandler.js";
@@ -67,8 +68,6 @@ export type LinkOptions = {
6768
export const Link = Mark.create<LinkOptions>({
6869
name: "link",
6970

70-
priority: 1000,
71-
7271
keepOnSplit: false,
7372

7473
exitable: true,
@@ -207,3 +206,33 @@ export const Link = Mark.create<LinkOptions>({
207206
return plugins;
208207
},
209208
});
209+
210+
type LinkExtensionOptions = {
211+
HTMLAttributes?: Record<string, any>;
212+
onClick?: (
213+
event: MouseEvent,
214+
editor: BlockNoteEditor<any, any, any>,
215+
) => boolean | void;
216+
isValidLink?: (href: string) => boolean;
217+
};
218+
219+
/**
220+
* BlockNote extension wrapping the {@link Link} TipTap mark. Wrapping the mark
221+
* lets other extensions order their click handlers relative to the link click
222+
* handler via `runsBefore: ["link"]`.
223+
*/
224+
export const LinkExtension = createExtension<any, LinkExtensionOptions>(
225+
({ editor, options }) => {
226+
return {
227+
key: "link",
228+
tiptapExtensions: [
229+
Link.configure({
230+
HTMLAttributes: options.HTMLAttributes ?? {},
231+
editor,
232+
onClick: options.onClick,
233+
...(options.isValidLink ? { isValidLink: options.isValidLink } : {}),
234+
}),
235+
],
236+
} as const;
237+
},
238+
);

0 commit comments

Comments
 (0)