2023-02-03 00:37:33 +08:00
|
|
|
|
import { EditorState, Plugin } from '@tiptap/pm/state'
|
2022-06-08 20:10:25 +08:00
|
|
|
|
|
2021-12-06 19:00:09 +08:00
|
|
|
|
import { CommandManager } from './CommandManager'
|
2022-06-08 20:10:25 +08:00
|
|
|
|
import { Editor } from './Editor'
|
2021-12-06 19:00:09 +08:00
|
|
|
|
import { createChainableState } from './helpers/createChainableState'
|
2021-10-14 17:56:40 +08:00
|
|
|
|
import {
|
2022-06-08 20:10:25 +08:00
|
|
|
|
CanCommands,
|
|
|
|
|
ChainedCommands,
|
2021-10-14 17:56:40 +08:00
|
|
|
|
ExtendedRegExpMatchArray,
|
2022-06-08 20:10:25 +08:00
|
|
|
|
Range,
|
2021-10-14 17:56:40 +08:00
|
|
|
|
SingleCommands,
|
|
|
|
|
} from './types'
|
2022-06-08 20:10:25 +08:00
|
|
|
|
import { isNumber } from './utilities/isNumber'
|
|
|
|
|
import { isRegExp } from './utilities/isRegExp'
|
2021-10-08 21:02:09 +08:00
|
|
|
|
|
|
|
|
|
export type PasteRuleMatch = {
|
2023-02-03 00:37:33 +08:00
|
|
|
|
index: number
|
|
|
|
|
text: string
|
|
|
|
|
replaceWith?: string
|
|
|
|
|
match?: RegExpMatchArray
|
|
|
|
|
data?: Record<string, any>
|
2021-10-08 21:02:09 +08:00
|
|
|
|
}
|
|
|
|
|
|
2023-02-03 00:37:33 +08:00
|
|
|
|
export type PasteRuleFinder = RegExp | ((text: string) => PasteRuleMatch[] | null | undefined)
|
2021-10-08 21:02:09 +08:00
|
|
|
|
|
|
|
|
|
export class PasteRule {
|
|
|
|
|
find: PasteRuleFinder
|
|
|
|
|
|
|
|
|
|
handler: (props: {
|
2023-02-03 00:37:33 +08:00
|
|
|
|
state: EditorState
|
|
|
|
|
range: Range
|
|
|
|
|
match: ExtendedRegExpMatchArray
|
|
|
|
|
commands: SingleCommands
|
|
|
|
|
chain: () => ChainedCommands
|
|
|
|
|
can: () => CanCommands
|
2022-01-10 21:55:53 +08:00
|
|
|
|
}) => void | null
|
2021-10-08 21:02:09 +08:00
|
|
|
|
|
|
|
|
|
constructor(config: {
|
2023-02-03 00:37:33 +08:00
|
|
|
|
find: PasteRuleFinder
|
2021-10-08 21:02:09 +08:00
|
|
|
|
handler: (props: {
|
2023-02-03 00:37:33 +08:00
|
|
|
|
state: EditorState
|
|
|
|
|
range: Range
|
|
|
|
|
match: ExtendedRegExpMatchArray
|
|
|
|
|
commands: SingleCommands
|
|
|
|
|
chain: () => ChainedCommands
|
|
|
|
|
can: () => CanCommands
|
|
|
|
|
}) => void | null
|
2021-10-08 21:02:09 +08:00
|
|
|
|
}) {
|
|
|
|
|
this.find = config.find
|
|
|
|
|
this.handler = config.handler
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-03 00:37:33 +08:00
|
|
|
|
const pasteRuleMatcherHandler = (
|
|
|
|
|
text: string,
|
|
|
|
|
find: PasteRuleFinder,
|
|
|
|
|
): ExtendedRegExpMatchArray[] => {
|
2021-10-08 21:02:09 +08:00
|
|
|
|
if (isRegExp(find)) {
|
|
|
|
|
return [...text.matchAll(find)]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const matches = find(text)
|
|
|
|
|
|
|
|
|
|
if (!matches) {
|
|
|
|
|
return []
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return matches.map(pasteRuleMatch => {
|
2023-01-25 17:19:51 +08:00
|
|
|
|
const result: ExtendedRegExpMatchArray = [pasteRuleMatch.text]
|
2021-10-08 21:02:09 +08:00
|
|
|
|
|
|
|
|
|
result.index = pasteRuleMatch.index
|
|
|
|
|
result.input = text
|
|
|
|
|
result.data = pasteRuleMatch.data
|
|
|
|
|
|
|
|
|
|
if (pasteRuleMatch.replaceWith) {
|
|
|
|
|
if (!pasteRuleMatch.text.includes(pasteRuleMatch.replaceWith)) {
|
2023-02-03 00:37:33 +08:00
|
|
|
|
console.warn(
|
|
|
|
|
'[tiptap warn]: "pasteRuleMatch.replaceWith" must be part of "pasteRuleMatch.text".',
|
|
|
|
|
)
|
2021-10-08 21:02:09 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
result.push(pasteRuleMatch.replaceWith)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function run(config: {
|
2023-02-03 00:37:33 +08:00
|
|
|
|
editor: Editor
|
|
|
|
|
state: EditorState
|
|
|
|
|
from: number
|
|
|
|
|
to: number
|
|
|
|
|
rule: PasteRule
|
2022-01-10 21:43:56 +08:00
|
|
|
|
}): boolean {
|
2021-10-08 21:02:09 +08:00
|
|
|
|
const {
|
2023-02-03 00:37:33 +08:00
|
|
|
|
editor, state, from, to, rule,
|
2021-10-08 21:02:09 +08:00
|
|
|
|
} = config
|
|
|
|
|
|
2021-10-14 17:56:40 +08:00
|
|
|
|
const { commands, chain, can } = new CommandManager({
|
|
|
|
|
editor,
|
|
|
|
|
state,
|
|
|
|
|
})
|
|
|
|
|
|
2022-01-10 21:55:53 +08:00
|
|
|
|
const handlers: (void | null)[] = []
|
2022-01-10 21:43:56 +08:00
|
|
|
|
|
2021-10-08 21:02:09 +08:00
|
|
|
|
state.doc.nodesBetween(from, to, (node, pos) => {
|
|
|
|
|
if (!node.isTextblock || node.type.spec.code) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const resolvedFrom = Math.max(from, pos)
|
|
|
|
|
const resolvedTo = Math.min(to, pos + node.content.size)
|
2023-02-03 00:37:33 +08:00
|
|
|
|
const textToMatch = node.textBetween(resolvedFrom - pos, resolvedTo - pos, undefined, '\ufffc')
|
2021-10-08 21:02:09 +08:00
|
|
|
|
|
2022-01-10 21:43:56 +08:00
|
|
|
|
const matches = pasteRuleMatcherHandler(textToMatch, rule.find)
|
2021-10-08 21:02:09 +08:00
|
|
|
|
|
2022-01-10 21:43:56 +08:00
|
|
|
|
matches.forEach(match => {
|
|
|
|
|
if (match.index === undefined) {
|
|
|
|
|
return
|
|
|
|
|
}
|
2021-10-08 21:02:09 +08:00
|
|
|
|
|
2022-01-10 21:43:56 +08:00
|
|
|
|
const start = resolvedFrom + match.index + 1
|
|
|
|
|
const end = start + match[0].length
|
|
|
|
|
const range = {
|
|
|
|
|
from: state.tr.mapping.map(start),
|
|
|
|
|
to: state.tr.mapping.map(end),
|
|
|
|
|
}
|
2021-10-08 21:02:09 +08:00
|
|
|
|
|
2022-01-10 21:43:56 +08:00
|
|
|
|
const handler = rule.handler({
|
|
|
|
|
state,
|
|
|
|
|
range,
|
|
|
|
|
match,
|
|
|
|
|
commands,
|
|
|
|
|
chain,
|
|
|
|
|
can,
|
2021-10-08 21:02:09 +08:00
|
|
|
|
})
|
2022-01-10 21:43:56 +08:00
|
|
|
|
|
|
|
|
|
handlers.push(handler)
|
2021-10-08 21:02:09 +08:00
|
|
|
|
})
|
2021-10-09 04:40:06 +08:00
|
|
|
|
})
|
2022-01-10 21:43:56 +08:00
|
|
|
|
|
|
|
|
|
const success = handlers.every(handler => handler !== null)
|
|
|
|
|
|
|
|
|
|
return success
|
2021-10-08 21:02:09 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Create an paste rules plugin. When enabled, it will cause pasted
|
|
|
|
|
* text that matches any of the given rules to trigger the rule’s
|
|
|
|
|
* action.
|
|
|
|
|
*/
|
2023-02-03 00:37:33 +08:00
|
|
|
|
export function pasteRulesPlugin(props: { editor: Editor; rules: PasteRule[] }): Plugin[] {
|
2021-10-14 17:56:40 +08:00
|
|
|
|
const { editor, rules } = props
|
2022-01-20 22:04:34 +08:00
|
|
|
|
let dragSourceElement: Element | null = null
|
|
|
|
|
let isPastedFromProseMirror = false
|
|
|
|
|
let isDroppedFromProseMirror = false
|
2021-10-11 01:25:47 +08:00
|
|
|
|
|
2022-01-10 21:43:56 +08:00
|
|
|
|
const plugins = rules.map(rule => {
|
|
|
|
|
return new Plugin({
|
2022-01-20 22:04:34 +08:00
|
|
|
|
// we register a global drag handler to track the current drag source element
|
|
|
|
|
view(view) {
|
|
|
|
|
const handleDragstart = (event: DragEvent) => {
|
|
|
|
|
dragSourceElement = view.dom.parentElement?.contains(event.target as Element)
|
|
|
|
|
? view.dom.parentElement
|
|
|
|
|
: null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
window.addEventListener('dragstart', handleDragstart)
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
destroy() {
|
|
|
|
|
window.removeEventListener('dragstart', handleDragstart)
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
2022-01-10 21:43:56 +08:00
|
|
|
|
props: {
|
2022-01-20 22:04:34 +08:00
|
|
|
|
handleDOMEvents: {
|
2022-11-05 01:21:12 +08:00
|
|
|
|
drop: view => {
|
2022-01-20 22:04:34 +08:00
|
|
|
|
isDroppedFromProseMirror = dragSourceElement === view.dom.parentElement
|
|
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
},
|
2021-10-11 01:25:47 +08:00
|
|
|
|
|
2022-06-21 06:17:10 +08:00
|
|
|
|
paste: (view, event: Event) => {
|
2022-06-20 17:45:37 +08:00
|
|
|
|
const html = (event as ClipboardEvent).clipboardData?.getData('text/html')
|
2021-10-11 01:25:47 +08:00
|
|
|
|
|
2022-01-20 22:04:34 +08:00
|
|
|
|
isPastedFromProseMirror = !!html?.includes('data-pm-slice')
|
|
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
},
|
2022-01-10 21:43:56 +08:00
|
|
|
|
},
|
2021-10-11 01:25:47 +08:00
|
|
|
|
},
|
2022-01-20 22:04:34 +08:00
|
|
|
|
|
2022-01-10 21:43:56 +08:00
|
|
|
|
appendTransaction: (transactions, oldState, state) => {
|
|
|
|
|
const transaction = transactions[0]
|
2022-01-20 22:04:34 +08:00
|
|
|
|
const isPaste = transaction.getMeta('uiEvent') === 'paste' && !isPastedFromProseMirror
|
|
|
|
|
const isDrop = transaction.getMeta('uiEvent') === 'drop' && !isDroppedFromProseMirror
|
2021-10-08 21:02:09 +08:00
|
|
|
|
|
2022-01-20 22:04:34 +08:00
|
|
|
|
if (!isPaste && !isDrop) {
|
2022-01-10 21:43:56 +08:00
|
|
|
|
return
|
|
|
|
|
}
|
2021-10-08 21:02:09 +08:00
|
|
|
|
|
2022-01-10 21:43:56 +08:00
|
|
|
|
// stop if there is no changed range
|
|
|
|
|
const from = oldState.doc.content.findDiffStart(state.doc.content)
|
|
|
|
|
const to = oldState.doc.content.findDiffEnd(state.doc.content)
|
2021-10-08 21:02:09 +08:00
|
|
|
|
|
2022-01-10 21:43:56 +08:00
|
|
|
|
if (!isNumber(from) || !to || from === to.b) {
|
|
|
|
|
return
|
|
|
|
|
}
|
2021-10-08 21:02:09 +08:00
|
|
|
|
|
2022-01-10 21:43:56 +08:00
|
|
|
|
// build a chainable state
|
|
|
|
|
// so we can use a single transaction for all paste rules
|
|
|
|
|
const tr = state.tr
|
|
|
|
|
const chainableState = createChainableState({
|
|
|
|
|
state,
|
|
|
|
|
transaction: tr,
|
|
|
|
|
})
|
2021-10-08 21:02:09 +08:00
|
|
|
|
|
2022-01-10 21:43:56 +08:00
|
|
|
|
const handler = run({
|
|
|
|
|
editor,
|
|
|
|
|
state: chainableState,
|
|
|
|
|
from: Math.max(from - 1, 0),
|
2022-08-22 19:26:26 +08:00
|
|
|
|
to: to.b - 1,
|
2022-01-10 21:43:56 +08:00
|
|
|
|
rule,
|
|
|
|
|
})
|
2021-10-08 21:02:09 +08:00
|
|
|
|
|
2022-01-10 21:43:56 +08:00
|
|
|
|
// stop if there are no changes
|
|
|
|
|
if (!handler || !tr.steps.length) {
|
|
|
|
|
return
|
|
|
|
|
}
|
2021-10-08 21:02:09 +08:00
|
|
|
|
|
2022-01-10 21:43:56 +08:00
|
|
|
|
return tr
|
|
|
|
|
},
|
|
|
|
|
})
|
2021-10-08 21:02:09 +08:00
|
|
|
|
})
|
|
|
|
|
|
2022-01-10 21:43:56 +08:00
|
|
|
|
return plugins
|
2021-10-08 21:02:09 +08:00
|
|
|
|
}
|