import { mergeAttributes } from '@tiptap/core'; import TiptapLink from '@tiptap/extension-link'; import type { EditorView } from '@tiptap/pm/view'; import { getMarkRange } from '@tiptap/core'; import { Plugin, TextSelection } from '@tiptap/pm/state'; export const Link = TiptapLink.extend({ /* * Determines whether typing next to a link automatically becomes part of the link. * In this case, we don't want any characters to be included as part of the link. */ inclusive: false, /* * Match all elements that have an href attribute, except for: * - elements with a data-type attribute set to button * - elements with an href attribute that contains 'javascript:' */ parseHTML() { return [{ tag: 'a[href]:not([data-type="button"]):not([href *= "javascript:" i])' }]; }, renderHTML({ HTMLAttributes }) { return ['a', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]; }, addOptions() { return { ...this.parent?.(), openOnClick: false, HTMLAttributes: { class: 'link', }, }; }, addProseMirrorPlugins() { const { editor } = this; return [ ...(this.parent?.() || []), new Plugin({ props: { handleKeyDown: (_: EditorView, event: KeyboardEvent) => { const { selection } = editor.state; /* * Handles the 'Escape' key press when there's a selection within the link. * This will move the cursor to the end of the link. */ if (event.key === 'Escape' && selection.empty !== true) { editor.commands.focus(selection.to, { scrollIntoView: false }); } return false; }, handleClick(view, pos) { /* * Marks the entire link when the user clicks on it. */ const { schema, doc, tr } = view.state; const range = getMarkRange(doc.resolve(pos), schema.marks.link); if (!range) { return; } const { from, to } = range; const start = Math.min(from, to); const end = Math.max(from, to); if (pos < start || pos > end) { return; } const $start = doc.resolve(start); const $end = doc.resolve(end); const transaction = tr.setSelection(new TextSelection($start, $end)); view.dispatch(transaction); }, }, }), ]; }, }); export default Link;