mirror of
https://github.com/ueberdosis/tiptap.git
synced 2024-11-27 14:59:27 +08:00
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:
parent
8b89b97d5b
commit
361a821245
@ -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
|
||||
},
|
||||
})
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
@ -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
|
||||
},
|
||||
})
|
||||
|
Loading…
Reference in New Issue
Block a user