From 0e26efee1d2db08a87aabb0f9a2cb3080af469d7 Mon Sep 17 00:00:00 2001 From: Dominik Biedebach Date: Fri, 18 Aug 2023 21:17:03 -0700 Subject: [PATCH 1/4] add pass through of paste event for paste handlers --- demos/src/Nodes/Youtube/React/index.jsx | 2 + packages/core/src/PasteRule.ts | 23 +++++++-- packages/core/src/pasteRules/markPasteRule.ts | 8 +-- packages/core/src/pasteRules/nodePasteRule.ts | 8 +-- packages/extension-link/src/link.ts | 51 +++++++++++++++---- 5 files changed, 72 insertions(+), 20 deletions(-) diff --git a/demos/src/Nodes/Youtube/React/index.jsx b/demos/src/Nodes/Youtube/React/index.jsx index b0f664647a..68d07ede38 100644 --- a/demos/src/Nodes/Youtube/React/index.jsx +++ b/demos/src/Nodes/Youtube/React/index.jsx @@ -1,5 +1,6 @@ import './styles.scss' +import Link from '@tiptap/extension-link' import Youtube from '@tiptap/extension-youtube' import { EditorContent, useEditor } from '@tiptap/react' import StarterKit from '@tiptap/starter-kit' @@ -45,6 +46,7 @@ export default () => { const editor = useEditor({ extensions: [ StarterKit, + Link, Youtube.configure({ controls: false, }), diff --git a/packages/core/src/PasteRule.ts b/packages/core/src/PasteRule.ts index acc24faa3e..1c41bd3ca6 100644 --- a/packages/core/src/PasteRule.ts +++ b/packages/core/src/PasteRule.ts @@ -33,6 +33,8 @@ export class PasteRule { commands: SingleCommands chain: () => ChainedCommands can: () => CanCommands + pasteEvent: ClipboardEvent + dropEvent: DragEvent }) => void | null constructor(config: { @@ -44,6 +46,7 @@ export class PasteRule { commands: SingleCommands chain: () => ChainedCommands can: () => CanCommands + pasteEvent: ClipboardEvent }) => void | null }) { this.find = config.find @@ -92,9 +95,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 +139,8 @@ function run(config: { commands, chain, can, + pasteEvent, + dropEvent, }) handlers.push(handler) @@ -155,6 +162,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 +186,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 +236,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 +245,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 906bd7d9a6..a95c437c6c 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 b7a0965edf..4618a1a1ee 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/link.ts b/packages/extension-link/src/link.ts index 57afc05835..3dee3fb2ae 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 }, }) From d9f6f840ad39051d1a38fbf046a699046ed6a205 Mon Sep 17 00:00:00 2001 From: Dominik Biedebach Date: Fri, 18 Aug 2023 21:17:21 -0700 Subject: [PATCH 2/4] remove unused pasteHandler.ts --- .../src/helpers/pasteHandler.ts | 114 ------------------ 1 file changed, 114 deletions(-) delete mode 100644 packages/extension-link/src/helpers/pasteHandler.ts diff --git a/packages/extension-link/src/helpers/pasteHandler.ts b/packages/extension-link/src/helpers/pasteHandler.ts deleted file mode 100644 index b85dc59432..0000000000 --- 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 - }, - }, - }) -} From e6f778c56a47c4b6e2e08d75784b7674f942df28 Mon Sep 17 00:00:00 2001 From: Dominik Biedebach Date: Fri, 18 Aug 2023 21:17:57 -0700 Subject: [PATCH 3/4] remove link extension from youtube demo --- demos/src/Nodes/Youtube/React/index.jsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/demos/src/Nodes/Youtube/React/index.jsx b/demos/src/Nodes/Youtube/React/index.jsx index 68d07ede38..b0f664647a 100644 --- a/demos/src/Nodes/Youtube/React/index.jsx +++ b/demos/src/Nodes/Youtube/React/index.jsx @@ -1,6 +1,5 @@ import './styles.scss' -import Link from '@tiptap/extension-link' import Youtube from '@tiptap/extension-youtube' import { EditorContent, useEditor } from '@tiptap/react' import StarterKit from '@tiptap/starter-kit' @@ -46,7 +45,6 @@ export default () => { const editor = useEditor({ extensions: [ StarterKit, - Link, Youtube.configure({ controls: false, }), From 896266df6514d387c05a83a1175dedf834baefac Mon Sep 17 00:00:00 2001 From: Dominik Biedebach Date: Fri, 18 Aug 2023 21:37:36 -0700 Subject: [PATCH 4/4] added missing prop for handler --- packages/core/src/PasteRule.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/core/src/PasteRule.ts b/packages/core/src/PasteRule.ts index 1c41bd3ca6..100eaa5032 100644 --- a/packages/core/src/PasteRule.ts +++ b/packages/core/src/PasteRule.ts @@ -40,13 +40,14 @@ export class PasteRule { 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