Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: properly transform paste/input rules #5545

Merged
merged 3 commits into from
Oct 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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