feature(core): pass through paste event to pasteHandler getAttributes (#4354)

* add pass through of paste event for paste handlers

* remove unused pasteHandler.ts

* remove link extension from youtube demo

* added missing prop for handler
This commit is contained in:
bdbch 2023-09-20 17:23:43 +02:00 committed by GitHub
parent 8b89b97d5b
commit 361a821245
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 76 additions and 139 deletions

View File

@ -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
},
})

View File

@ -14,14 +14,16 @@ export function markPasteRule(config: {
type: MarkType
getAttributes?:
| Record<string, any>
| ((match: ExtendedRegExpMatchArray) => Record<string, any>)
| ((match: ExtendedRegExpMatchArray, event: ClipboardEvent) => Record<string, any>)
| 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

View File

@ -13,14 +13,16 @@ export function nodePasteRule(config: {
type: NodeType
getAttributes?:
| Record<string, any>
| ((match: ExtendedRegExpMatchArray) => Record<string, any>)
| ((match: ExtendedRegExpMatchArray, event: ClipboardEvent) => Record<string, any>)
| 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

View File

@ -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(`<a href="${link.href}">${link.href}</a>`)
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
},
},
})
}

View File

@ -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<LinkOptions>({
}
},
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<LinkOptions>({
)
}
plugins.push(
pasteHandler({
editor: this.editor,
type: this.type,
linkOnPaste: this.options.linkOnPaste,
}),
)
return plugins
},
})