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;