-
-
Notifications
You must be signed in to change notification settings - Fork 717
Added click handling and HTML attribute link options to the editor #2656
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,12 +1,14 @@ | ||||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||||
| AnyExtension as AnyTiptapExtension, | ||||||||||||||||||||||||||||||||
| extensions, | ||||||||||||||||||||||||||||||||
| getAttributes, | ||||||||||||||||||||||||||||||||
| Node, | ||||||||||||||||||||||||||||||||
| Extension as TiptapExtension, | ||||||||||||||||||||||||||||||||
| } from "@tiptap/core"; | ||||||||||||||||||||||||||||||||
| import { Gapcursor } from "@tiptap/extensions/gap-cursor"; | ||||||||||||||||||||||||||||||||
| import { Link } from "@tiptap/extension-link"; | ||||||||||||||||||||||||||||||||
| import { Text } from "@tiptap/extension-text"; | ||||||||||||||||||||||||||||||||
| import { Plugin, PluginKey } from "prosemirror-state"; | ||||||||||||||||||||||||||||||||
| import { createDropFileExtension } from "../../../api/clipboard/fromClipboard/fileDropExtension.js"; | ||||||||||||||||||||||||||||||||
| import { createPasteFromClipboardExtension } from "../../../api/clipboard/fromClipboard/pasteExtension.js"; | ||||||||||||||||||||||||||||||||
| import { createCopyToClipboardExtension } from "../../../api/clipboard/toClipboard/copyExtension.js"; | ||||||||||||||||||||||||||||||||
|
|
@@ -91,12 +93,94 @@ export function getDefaultTiptapExtensions( | |||||||||||||||||||||||||||||||
| delete (attrs as Record<string, unknown>).title; | ||||||||||||||||||||||||||||||||
| return attrs; | ||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||
| addProseMirrorPlugins() { | ||||||||||||||||||||||||||||||||
| const plugins = this.parent?.() || []; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| const markType = this.type; | ||||||||||||||||||||||||||||||||
| const tiptapEditor = this.editor; | ||||||||||||||||||||||||||||||||
| // Copied from @tiptap/extension-link's `clickHandler` helper, but | ||||||||||||||||||||||||||||||||
| // calls `onClick` if the editor option is defined. Otherwise, uses | ||||||||||||||||||||||||||||||||
| // default behaviour. | ||||||||||||||||||||||||||||||||
| // https://github.com/ueberdosis/tiptap/blob/main/packages/extension-link/src/helpers/clickHandler.ts | ||||||||||||||||||||||||||||||||
| plugins.push( | ||||||||||||||||||||||||||||||||
| new Plugin({ | ||||||||||||||||||||||||||||||||
| key: new PluginKey("linkClickHandler"), | ||||||||||||||||||||||||||||||||
| props: { | ||||||||||||||||||||||||||||||||
| handleClick: (view, _pos, event) => { | ||||||||||||||||||||||||||||||||
| if (event.button !== 0) { | ||||||||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| if (!view.editable) { | ||||||||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| let link: HTMLAnchorElement | null = null; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| if (event.target instanceof HTMLAnchorElement) { | ||||||||||||||||||||||||||||||||
| link = event.target; | ||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||
| const target = event.target as HTMLElement | null; | ||||||||||||||||||||||||||||||||
| if (!target) { | ||||||||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| const root = tiptapEditor.view.dom; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // Tntentionally limit the lookup to the editor root. | ||||||||||||||||||||||||||||||||
| // Using tag names like DIV as boundaries breaks with custom NodeViews, | ||||||||||||||||||||||||||||||||
|
Comment on lines
+130
to
+131
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||
| link = target.closest<HTMLAnchorElement>("a"); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| if (link && !root.contains(link)) { | ||||||||||||||||||||||||||||||||
| link = null; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| if (!link) { | ||||||||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| 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; | ||||||||||||||||||||||||||||||||
|
Comment on lines
+161
to
+163
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Opening a user-controlled URL in a new browsing context without - window.open(href, target);
+ window.open(href, linkTarget || "_blank", "noopener,noreferrer");📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
Comment on lines
+158
to
+164
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 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 extensionmanagerRepository: 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
Additionally, when Also fix the typo at line 130: "Tntentionally" → "Intentionally". Use 🐛 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| return handled; | ||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||
|
Comment on lines
+143
to
+168
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When Either return 🐛 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 |
||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| return plugins; | ||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||
| .configure({ | ||||||||||||||||||||||||||||||||
| defaultProtocol: DEFAULT_LINK_PROTOCOL, | ||||||||||||||||||||||||||||||||
| // only call this once if we have multiple editors installed. Or fix https://github.com/ueberdosis/tiptap/issues/5450 | ||||||||||||||||||||||||||||||||
| protocols: LINKIFY_INITIALIZED ? [] : VALID_LINK_PROTOCOLS, | ||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||
| defaultProtocol: DEFAULT_LINK_PROTOCOL, | ||||||||||||||||||||||||||||||||
| // only call this once if we have multiple editors installed. Or fix https://github.com/ueberdosis/tiptap/issues/5450 | ||||||||||||||||||||||||||||||||
| protocols: LINKIFY_INITIALIZED ? [] : VALID_LINK_PROTOCOLS, | ||||||||||||||||||||||||||||||||
| HTMLAttributes: options.links?.HTMLAttributes ?? {}, | ||||||||||||||||||||||||||||||||
| // Always false as we handle clicks ourselves above. | ||||||||||||||||||||||||||||||||
| openOnClick: false, | ||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||
| ...(Object.values(editor.schema.styleSpecs).map((styleSpec) => { | ||||||||||||||||||||||||||||||||
| return styleSpec.implementation.mark.configure({ | ||||||||||||||||||||||||||||||||
| editor: editor, | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Docs examples use
new BlockNoteEditor(...), but the constructor is protected.In
packages/core/src/editor/BlockNoteEditor.ts, the constructor is declaredprotectedand the public entry point is the staticBlockNoteEditor.create(...). All three snippets in this section (lines 86–98, 104–113, 119–127) invokenew BlockNoteEditor(...), which is not a valid public API and will fail type-checking for users copy-pasting these examples. Consider switching toBlockNoteEditor.create({ ... })to match the rest of the documentation.📝 Proposed fix (apply to all three examples)
📝 Committable suggestion
🤖 Prompt for AI Agents