tiptap/packages/extension-link/src/link.ts

172 lines
3.8 KiB
TypeScript
Raw Normal View History

2020-11-25 16:50:54 +08:00
import {
Mark,
markPasteRule,
mergeAttributes,
} from '@tiptap/core'
2020-09-25 04:26:23 +08:00
import { Plugin, PluginKey } from 'prosemirror-state'
export interface LinkOptions {
/**
* If enabled, links will be opened on click.
*/
2020-09-25 04:26:23 +08:00
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.
*/
2021-04-21 15:43:31 +08:00
HTMLAttributes: Record<string, any>,
2020-09-25 04:26:23 +08:00
}
2021-02-10 16:59:35 +08:00
declare module '@tiptap/core' {
2021-06-05 03:56:29 +08:00
interface Commands<ReturnType> {
2021-02-16 18:27:58 +08:00
link: {
/**
* Set a link mark
*/
2021-06-05 03:56:29 +08:00
setLink: (attributes: { href: string, target?: string }) => ReturnType,
2021-02-16 18:27:58 +08:00
/**
* Toggle a link mark
*/
2021-06-05 03:56:29 +08:00
toggleLink: (attributes: { href: string, target?: string }) => ReturnType,
2021-02-16 18:27:58 +08:00
/**
* Unset a link mark
*/
2021-06-05 03:56:29 +08:00
unsetLink: () => ReturnType,
2021-02-16 18:27:58 +08:00
}
2021-02-10 16:59:35 +08:00
}
}
/**
* A regex that matches any string that contains a link
*/
2020-11-25 00:29:43 +08:00
export const pasteRegex = /https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z]{2,}\b(?:[-a-zA-Z0-9@:%._+~#=?!&/]*)(?:[-a-zA-Z0-9@:%._+~#=?!&/]*)/gi
/**
* A regex that matches an url
*/
export const pasteRegexExact = /^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z]{2,}\b(?:[-a-zA-Z0-9@:%._+~#=?!&/]*)(?:[-a-zA-Z0-9@:%._+~#=?!&/]*)$/gi
2020-09-25 04:26:23 +08:00
2021-02-11 01:25:08 +08:00
export const Link = Mark.create<LinkOptions>({
2020-10-22 18:34:49 +08:00
name: 'link',
priority: 1000,
2020-10-22 18:34:49 +08:00
inclusive: false,
2021-02-11 01:25:08 +08:00
defaultOptions: {
2020-09-25 04:26:23 +08:00
openOnClick: true,
linkOnPaste: true,
2020-11-13 23:44:22 +08:00
HTMLAttributes: {
target: '_blank',
rel: 'noopener noreferrer nofollow',
},
2020-10-22 18:34:49 +08:00
},
addAttributes() {
return {
2020-09-25 04:26:23 +08:00
href: {
default: null,
},
target: {
2020-11-13 23:44:22 +08:00
default: this.options.HTMLAttributes.target,
2020-09-25 04:26:23 +08:00
},
2020-10-22 18:34:49 +08:00
}
},
parseHTML() {
2020-10-27 19:59:18 +08:00
return [
{ tag: 'a[href]' },
]
2020-10-22 18:34:49 +08:00
},
2020-11-13 23:07:20 +08:00
renderHTML({ HTMLAttributes }) {
2020-11-25 16:50:54 +08:00
return ['a', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
2020-10-22 18:34:49 +08:00
},
addCommands() {
return {
2021-02-10 16:59:35 +08:00
setLink: attributes => ({ commands }) => {
2020-11-19 00:36:00 +08:00
return commands.setMark('link', attributes)
2020-11-18 18:10:06 +08:00
},
2021-02-10 16:59:35 +08:00
toggleLink: attributes => ({ commands }) => {
2020-11-18 18:10:06 +08:00
return commands.toggleMark('link', attributes)
},
2021-02-10 16:59:35 +08:00
unsetLink: () => ({ commands }) => {
2020-11-19 00:38:16 +08:00
return commands.unsetMark('link')
2020-10-22 18:34:49 +08:00
},
}
},
addPasteRules() {
return [
markPasteRule(pasteRegex, this.type, match => ({ href: match[0] })),
2020-10-22 18:34:49 +08:00
]
},
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
},
},
}),
)
2020-09-25 04:26:23 +08:00
}
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
2020-09-25 04:26:23 +08:00
if (empty) {
return false
}
2020-09-25 04:26:23 +08:00
let textContent = ''
2020-09-25 04:26:23 +08:00
slice.content.forEach(node => {
textContent += node.textContent
})
if (!textContent || !textContent.match(pasteRegexExact)) {
return false
}
this.editor.commands.setMark(this.type, {
href: textContent,
})
return true
},
2020-09-25 04:26:23 +08:00
},
}),
)
}
return plugins
2020-10-22 18:34:49 +08:00
},
})