diff --git a/packages/core/src/PasteRule.ts b/packages/core/src/PasteRule.ts index acc24faa3e7..100eaa5032b 100644 --- a/packages/core/src/PasteRule.ts +++ b/packages/core/src/PasteRule.ts @@ -33,17 +33,21 @@ export class PasteRule { commands: SingleCommands chain: () => ChainedCommands can: () => CanCommands + pasteEvent: ClipboardEvent + dropEvent: DragEvent }) => void | null constructor(config: { find: PasteRuleFinder handler: (props: { - state: EditorState - range: Range - match: ExtendedRegExpMatchArray - commands: SingleCommands - chain: () => ChainedCommands can: () => CanCommands + chain: () => ChainedCommands + commands: SingleCommands + dropEvent: DragEvent + match: ExtendedRegExpMatchArray + pasteEvent: ClipboardEvent + range: Range + state: EditorState }) => void | null }) { this.find = config.find @@ -92,9 +96,11 @@ function run(config: { from: number to: number rule: PasteRule + pasteEvent: ClipboardEvent + dropEvent: DragEvent }): boolean { const { - editor, state, from, to, rule, + editor, state, from, to, rule, pasteEvent, dropEvent, } = config const { commands, chain, can } = new CommandManager({ @@ -134,6 +140,8 @@ function run(config: { commands, chain, can, + pasteEvent, + dropEvent, }) handlers.push(handler) @@ -155,6 +163,8 @@ export function pasteRulesPlugin(props: { editor: Editor; rules: PasteRule[] }): let dragSourceElement: Element | null = null let isPastedFromProseMirror = false let isDroppedFromProseMirror = false + let pasteEvent = new ClipboardEvent('paste') + let dropEvent = new DragEvent('drop') const plugins = rules.map(rule => { return new Plugin({ @@ -177,15 +187,18 @@ export function pasteRulesPlugin(props: { editor: Editor; rules: PasteRule[] }): props: { handleDOMEvents: { - drop: view => { + drop: (view, event: Event) => { isDroppedFromProseMirror = dragSourceElement === view.dom.parentElement + dropEvent = event as DragEvent return false }, - paste: (view, event: Event) => { + paste: (_view, event: Event) => { const html = (event as ClipboardEvent).clipboardData?.getData('text/html') + pasteEvent = event as ClipboardEvent + isPastedFromProseMirror = !!html?.includes('data-pm-slice') return false @@ -224,6 +237,8 @@ export function pasteRulesPlugin(props: { editor: Editor; rules: PasteRule[] }): from: Math.max(from - 1, 0), to: to.b - 1, rule, + pasteEvent, + dropEvent, }) // stop if there are no changes @@ -231,6 +246,9 @@ export function pasteRulesPlugin(props: { editor: Editor; rules: PasteRule[] }): return } + dropEvent = new DragEvent('drop') + pasteEvent = new ClipboardEvent('paste') + return tr }, }) diff --git a/packages/core/src/pasteRules/markPasteRule.ts b/packages/core/src/pasteRules/markPasteRule.ts index 906bd7d9a60..a95c437c6ce 100644 --- a/packages/core/src/pasteRules/markPasteRule.ts +++ b/packages/core/src/pasteRules/markPasteRule.ts @@ -14,14 +14,16 @@ export function markPasteRule(config: { type: MarkType getAttributes?: | Record - | ((match: ExtendedRegExpMatchArray) => Record) + | ((match: ExtendedRegExpMatchArray, event: ClipboardEvent) => Record) | false | null }) { return new PasteRule({ find: config.find, - handler: ({ state, range, match }) => { - const attributes = callOrReturn(config.getAttributes, undefined, match) + handler: ({ + state, range, match, pasteEvent, + }) => { + const attributes = callOrReturn(config.getAttributes, undefined, match, pasteEvent) if (attributes === false || attributes === null) { return null diff --git a/packages/core/src/pasteRules/nodePasteRule.ts b/packages/core/src/pasteRules/nodePasteRule.ts index b7a0965edf0..4618a1a1ee7 100644 --- a/packages/core/src/pasteRules/nodePasteRule.ts +++ b/packages/core/src/pasteRules/nodePasteRule.ts @@ -13,14 +13,16 @@ export function nodePasteRule(config: { type: NodeType getAttributes?: | Record - | ((match: ExtendedRegExpMatchArray) => Record) + | ((match: ExtendedRegExpMatchArray, event: ClipboardEvent) => Record) | false | null }) { return new PasteRule({ find: config.find, - handler({ match, chain, range }) { - const attributes = callOrReturn(config.getAttributes, undefined, match) + handler({ + match, chain, range, pasteEvent, + }) { + const attributes = callOrReturn(config.getAttributes, undefined, match, pasteEvent) if (attributes === false || attributes === null) { return null diff --git a/packages/extension-link/src/helpers/pasteHandler.ts b/packages/extension-link/src/helpers/pasteHandler.ts deleted file mode 100644 index b85dc59432f..00000000000 --- a/packages/extension-link/src/helpers/pasteHandler.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { Editor } from '@tiptap/core' -import { MarkType } from '@tiptap/pm/model' -import { Plugin, PluginKey } from '@tiptap/pm/state' -import { find } from 'linkifyjs' - -type PasteHandlerOptions = { - editor: Editor - type: MarkType - linkOnPaste?: boolean -} - -export function pasteHandler(options: PasteHandlerOptions): Plugin { - return new Plugin({ - key: new PluginKey('handlePasteLink'), - props: { - handlePaste: (view, event, slice) => { - const { state } = view - const { selection } = state - - // Do not proceed if in code block. - if (state.doc.resolve(selection.from).parent.type.spec.code) { - return false - } - - let textContent = '' - - slice.content.forEach(node => { - textContent += node.textContent - }) - - let isAlreadyLink = false - - slice.content.descendants(node => { - if (node.marks.some(mark => mark.type.name === options.type.name)) { - isAlreadyLink = true - } - }) - - if (isAlreadyLink) { - return - } - - const link = find(textContent).find(item => item.isLink && item.value === textContent) - - if (!selection.empty && options.linkOnPaste) { - const pastedLink = link?.href || null - - if (pastedLink) { - options.editor.commands.setMark(options.type, { href: pastedLink }) - - return true - } - } - - const firstChildIsText = slice.content.firstChild?.type.name === 'text' - const firstChildContainsLinkMark = slice.content.firstChild?.marks.some(mark => mark.type.name === options.type.name) - - if ((firstChildIsText && firstChildContainsLinkMark) || !options.linkOnPaste) { - return false - } - - if (link && selection.empty) { - options.editor.commands.insertContent(`${link.href}`) - - return true - } - - const { tr } = state - let deleteOnly = false - - if (!selection.empty) { - deleteOnly = true - - tr.delete(selection.from, selection.to) - } - - let currentPos = selection.from - let fragmentLinks = [] - - slice.content.forEach(node => { - fragmentLinks = find(node.textContent) - - tr.insert(currentPos - 1, node) - - if (fragmentLinks.length > 0) { - deleteOnly = false - - fragmentLinks.forEach(fragmentLink => { - const linkStart = currentPos + fragmentLink.start - const linkEnd = currentPos + fragmentLink.end - const hasMark = tr.doc.rangeHasMark(linkStart, linkEnd, options.type) - - if (!hasMark) { - tr.addMark(linkStart, linkEnd, options.type.create({ href: fragmentLink.href })) - } - }) - - } - currentPos += node.nodeSize - }) - - const hasFragmentLinks = fragmentLinks.length > 0 - - if (tr.docChanged && !deleteOnly && hasFragmentLinks) { - options.editor.view.dispatch(tr) - - return true - } - - return false - }, - }, - }) -} diff --git a/packages/extension-link/src/link.ts b/packages/extension-link/src/link.ts index 57afc058353..3dee3fb2ae4 100644 --- a/packages/extension-link/src/link.ts +++ b/packages/extension-link/src/link.ts @@ -1,10 +1,9 @@ -import { Mark, mergeAttributes } from '@tiptap/core' +import { Mark, markPasteRule, mergeAttributes } from '@tiptap/core' import { Plugin } from '@tiptap/pm/state' -import { registerCustomProtocol, reset } from 'linkifyjs' +import { find, registerCustomProtocol, reset } from 'linkifyjs' import { autolink } from './helpers/autolink.js' import { clickHandler } from './helpers/clickHandler.js' -import { pasteHandler } from './helpers/pasteHandler.js' export interface LinkProtocolOptions { scheme: string; @@ -149,6 +148,44 @@ export const Link = Mark.create({ } }, + addPasteRules() { + return [ + markPasteRule({ + find: text => find(text) + .filter(link => { + if (this.options.validate) { + return this.options.validate(link.value) + } + + return true + }) + .filter(link => link.isLink) + .map(link => ({ + text: link.value, + index: link.start, + data: link, + })), + type: this.type, + getAttributes: (match, pasteEvent) => { + const html = pasteEvent.clipboardData?.getData('text/html') + const hrefRegex = /href="([^"]*)"/ + + const existingLink = html?.match(hrefRegex) + + if (existingLink) { + return { + href: existingLink[1], + } + } + + return { + href: match.data?.href, + } + }, + }), + ] + }, + addProseMirrorPlugins() { const plugins: Plugin[] = [] @@ -169,14 +206,6 @@ export const Link = Mark.create({ ) } - plugins.push( - pasteHandler({ - editor: this.editor, - type: this.type, - linkOnPaste: this.options.linkOnPaste, - }), - ) - return plugins }, })