import { Mark, markPasteRule, mergeAttributes, } from '@tiptap/core' import { Plugin, PluginKey } from 'prosemirror-state' import { find } from 'linkifyjs' export interface LinkOptions { /** * If enabled, links will be opened on click. */ openOnClick: boolean, /** * Adds a link to the current selection if the pasted content only contains an url. */ linkOnPaste: boolean, /** * A list of HTML attributes to be rendered. */ HTMLAttributes: Record, } declare module '@tiptap/core' { interface Commands { link: { /** * Set a link mark */ setLink: (attributes: { href: string, target?: string }) => ReturnType, /** * Toggle a link mark */ toggleLink: (attributes: { href: string, target?: string }) => ReturnType, /** * Unset a link mark */ unsetLink: () => ReturnType, } } } export const Link = Mark.create({ name: 'link', priority: 1000, inclusive: false, addOptions() { return { openOnClick: true, linkOnPaste: true, HTMLAttributes: { target: '_blank', rel: 'noopener noreferrer nofollow', }, } }, addAttributes() { return { href: { default: null, }, target: { default: this.options.HTMLAttributes.target, }, } }, parseHTML() { return [ { tag: 'a[href]' }, ] }, renderHTML({ HTMLAttributes }) { return ['a', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0] }, addCommands() { return { setLink: attributes => ({ commands }) => { return commands.setMark('link', attributes) }, toggleLink: attributes => ({ commands }) => { return commands.toggleMark('link', attributes, { extendEmptyMarkRange: true }) }, unsetLink: () => ({ commands }) => { return commands.unsetMark('link', { extendEmptyMarkRange: true }) }, } }, addPasteRules() { return [ markPasteRule({ find: text => find(text) .filter(link => link.isLink) .map(link => ({ text: link.value, index: link.start, data: link, })), type: this.type, getAttributes: match => ({ href: match.data?.href, }), }), ] }, addProseMirrorPlugins() { const plugins = [] if (this.options.openOnClick) { plugins.push( new Plugin({ key: new PluginKey('handleClickLink'), props: { handleClick: (view, pos, event) => { const attrs = this.editor.getAttributes('link') const link = (event.target as HTMLElement)?.closest('a') if (link && attrs.href) { window.open(attrs.href, attrs.target) return true } return false }, }, }), ) } if (this.options.linkOnPaste) { plugins.push( new Plugin({ key: new PluginKey('handlePasteLink'), props: { handlePaste: (view, event, slice) => { const { state } = view const { selection } = state const { empty } = selection if (empty) { return false } let textContent = '' slice.content.forEach(node => { textContent += node.textContent }) const link = find(textContent) .find(item => item.isLink && item.value === textContent) if (!textContent || !link) { return false } this.editor.commands.setMark(this.type, { href: link.href, }) return true }, }, }), ) } return plugins }, })