tiptap/packages/extension-link/src/link.ts
Philipp Kühn 9afadeb7fe
feat!: Replace defaultOptions with addOptions (#2088)
* add new addOptions option

* replace defaultOptions with addOptions for all extensions

* replace defaultOptions with addOptions for all demos

* replace defaultOptions with addOptions in docs

* refactoring

* refactoring

* drop object support for addOptions

* fix optional options

* fix tests
2021-10-26 18:31:13 +02:00

180 lines
3.9 KiB
TypeScript

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<string, any>,
}
declare module '@tiptap/core' {
interface Commands<ReturnType> {
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<LinkOptions>({
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
},
})