Skip to content

Commit

Permalink
fix: properly transform paste/input rules (#5545)
Browse files Browse the repository at this point in the history
  • Loading branch information
nperez0111 authored Oct 25, 2024
1 parent f95b13e commit 466a5a9
Show file tree
Hide file tree
Showing 2 changed files with 111 additions and 71 deletions.
75 changes: 45 additions & 30 deletions packages/core/src/InputRule.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { Fragment, Node as ProseMirrorNode } from '@tiptap/pm/model'
import { EditorState, Plugin, TextSelection } from '@tiptap/pm/state'

import { CommandManager } from './CommandManager.js'
import { Editor } from './Editor.js'
import { createChainableState } from './helpers/createChainableState.js'
import { getHTMLFromFragment } from './helpers/getHTMLFromFragment.js'
import { getTextContentFromNodes } from './helpers/getTextContentFromNodes.js'
import {
CanCommands,
Expand All @@ -14,37 +16,37 @@ import {
import { isRegExp } from './utilities/isRegExp.js'

export type InputRuleMatch = {
index: number
text: string
replaceWith?: string
match?: RegExpMatchArray
data?: Record<string, any>
}
index: number;
text: string;
replaceWith?: string;
match?: RegExpMatchArray;
data?: Record<string, any>;
};

export type InputRuleFinder = RegExp | ((text: string) => InputRuleMatch | null)
export type InputRuleFinder = RegExp | ((text: string) => InputRuleMatch | null);

export class InputRule {
find: InputRuleFinder

handler: (props: {
state: EditorState
range: Range
match: ExtendedRegExpMatchArray
commands: SingleCommands
chain: () => ChainedCommands
can: () => CanCommands
state: EditorState;
range: Range;
match: ExtendedRegExpMatchArray;
commands: SingleCommands;
chain: () => ChainedCommands;
can: () => CanCommands;
}) => void | null

constructor(config: {
find: InputRuleFinder
find: InputRuleFinder;
handler: (props: {
state: EditorState
range: Range
match: ExtendedRegExpMatchArray
commands: SingleCommands
chain: () => ChainedCommands
can: () => CanCommands
}) => void | null
state: EditorState;
range: Range;
match: ExtendedRegExpMatchArray;
commands: SingleCommands;
chain: () => ChainedCommands;
can: () => CanCommands;
}) => void | null;
}) {
this.find = config.find
this.handler = config.handler
Expand Down Expand Up @@ -85,12 +87,12 @@ const inputRuleMatcherHandler = (
}

function run(config: {
editor: Editor
from: number
to: number
text: string
rules: InputRule[]
plugin: Plugin
editor: Editor;
from: number;
to: number;
text: string;
rules: InputRule[];
plugin: Plugin;
}): boolean {
const {
editor, from, to, text, rules, plugin,
Expand Down Expand Up @@ -184,20 +186,33 @@ export function inputRulesPlugin(props: { editor: Editor; rules: InputRule[] }):
init() {
return null
},
apply(tr, prev) {
apply(tr, prev, state) {
const stored = tr.getMeta(plugin)

if (stored) {
return stored
}

// if InputRule is triggered by insertContent()
const simulatedInputMeta = tr.getMeta('applyInputRules')
const simulatedInputMeta = tr.getMeta('applyInputRules') as
| undefined
| {
from: number;
text: string | ProseMirrorNode | Fragment;
}
const isSimulatedInput = !!simulatedInputMeta

if (isSimulatedInput) {
setTimeout(() => {
const { from, text } = simulatedInputMeta
let { text } = simulatedInputMeta

if (typeof text === 'string') {
text = text as string
} else {
text = getHTMLFromFragment(Fragment.from(text), state.schema)
}

const { from } = simulatedInputMeta
const to = from + text.length

run({
Expand Down
107 changes: 66 additions & 41 deletions packages/core/src/PasteRule.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { Fragment, Node as ProseMirrorNode } from '@tiptap/pm/model'
import { EditorState, Plugin } from '@tiptap/pm/state'

import { CommandManager } from './CommandManager.js'
import { Editor } from './Editor.js'
import { createChainableState } from './helpers/createChainableState.js'
import { getHTMLFromFragment } from './helpers/getHTMLFromFragment.js'
import {
CanCommands,
ChainedCommands,
Expand All @@ -14,14 +16,16 @@ import { isNumber } from './utilities/isNumber.js'
import { isRegExp } from './utilities/isRegExp.js'

export type PasteRuleMatch = {
index: number
text: string
replaceWith?: string
match?: RegExpMatchArray
data?: Record<string, any>
}
index: number;
text: string;
replaceWith?: string;
match?: RegExpMatchArray;
data?: Record<string, any>;
};

export type PasteRuleFinder = RegExp | ((text: string, event?: ClipboardEvent | null) => PasteRuleMatch[] | null | undefined)
export type PasteRuleFinder =
| RegExp
| ((text: string, event?: ClipboardEvent | null) => PasteRuleMatch[] | null | undefined);

/**
* Paste rules are used to react to pasted content.
Expand All @@ -31,28 +35,28 @@ export class PasteRule {
find: PasteRuleFinder

handler: (props: {
state: EditorState
range: Range
match: ExtendedRegExpMatchArray
commands: SingleCommands
chain: () => ChainedCommands
can: () => CanCommands
pasteEvent: ClipboardEvent | null
dropEvent: DragEvent | null
state: EditorState;
range: Range;
match: ExtendedRegExpMatchArray;
commands: SingleCommands;
chain: () => ChainedCommands;
can: () => CanCommands;
pasteEvent: ClipboardEvent | null;
dropEvent: DragEvent | null;
}) => void | null

constructor(config: {
find: PasteRuleFinder
find: PasteRuleFinder;
handler: (props: {
can: () => CanCommands
chain: () => ChainedCommands
commands: SingleCommands
dropEvent: DragEvent | null
match: ExtendedRegExpMatchArray
pasteEvent: ClipboardEvent | null
range: Range
state: EditorState
}) => void | null
can: () => CanCommands;
chain: () => ChainedCommands;
commands: SingleCommands;
dropEvent: DragEvent | null;
match: ExtendedRegExpMatchArray;
pasteEvent: ClipboardEvent | null;
range: Range;
state: EditorState;
}) => void | null;
}) {
this.find = config.find
this.handler = config.handler
Expand Down Expand Up @@ -96,13 +100,13 @@ const pasteRuleMatcherHandler = (
}

function run(config: {
editor: Editor
state: EditorState
from: number
to: number
rule: PasteRule
pasteEvent: ClipboardEvent | null
dropEvent: DragEvent | null
editor: Editor;
state: EditorState;
from: number;
to: number;
rule: PasteRule;
pasteEvent: ClipboardEvent | null;
dropEvent: DragEvent | null;
}): boolean {
const {
editor, state, from, to, rule, pasteEvent, dropEvent,
Expand Down Expand Up @@ -179,7 +183,13 @@ export function pasteRulesPlugin(props: { editor: Editor; rules: PasteRule[] }):
let isPastedFromProseMirror = false
let isDroppedFromProseMirror = false
let pasteEvent = typeof ClipboardEvent !== 'undefined' ? new ClipboardEvent('paste') : null
let dropEvent = typeof DragEvent !== 'undefined' ? new DragEvent('drop') : null
let dropEvent: DragEvent | null

try {
dropEvent = typeof DragEvent !== 'undefined' ? new DragEvent('drop') : null
} catch (e) {
dropEvent = null
}

const processEvent = ({
state,
Expand All @@ -188,11 +198,11 @@ export function pasteRulesPlugin(props: { editor: Editor; rules: PasteRule[] }):
rule,
pasteEvt,
}: {
state: EditorState
from: number
to: { b: number }
rule: PasteRule
pasteEvt: ClipboardEvent | null
state: EditorState;
from: number;
to: { b: number };
rule: PasteRule;
pasteEvt: ClipboardEvent | null;
}) => {
const tr = state.tr
const chainableState = createChainableState({
Expand All @@ -214,7 +224,11 @@ export function pasteRulesPlugin(props: { editor: Editor; rules: PasteRule[] }):
return
}

dropEvent = typeof DragEvent !== 'undefined' ? new DragEvent('drop') : null
try {
dropEvent = typeof DragEvent !== 'undefined' ? new DragEvent('drop') : null
} catch (e) {
dropEvent = null
}
pasteEvent = typeof ClipboardEvent !== 'undefined' ? new ClipboardEvent('paste') : null

return tr
Expand Down Expand Up @@ -266,7 +280,9 @@ export function pasteRulesPlugin(props: { editor: Editor; rules: PasteRule[] }):
const isDrop = transaction.getMeta('uiEvent') === 'drop' && !isDroppedFromProseMirror

// if PasteRule is triggered by insertContent()
const simulatedPasteMeta = transaction.getMeta('applyPasteRules')
const simulatedPasteMeta = transaction.getMeta('applyPasteRules') as
| undefined
| { from: number; text: string | ProseMirrorNode | Fragment }
const isSimulatedPaste = !!simulatedPasteMeta

if (!isPaste && !isDrop && !isSimulatedPaste) {
Expand All @@ -275,8 +291,17 @@ export function pasteRulesPlugin(props: { editor: Editor; rules: PasteRule[] }):

// Handle simulated paste
if (isSimulatedPaste) {
const { from, text } = simulatedPasteMeta
let { text } = simulatedPasteMeta

if (typeof text === 'string') {
text = text as string
} else {
text = getHTMLFromFragment(Fragment.from(text), state.schema)
}

const { from } = simulatedPasteMeta
const to = from + text.length

const pasteEvt = createClipboardPasteEvent(text)

return processEvent({
Expand Down

0 comments on commit 466a5a9

Please sign in to comment.