tiptap/packages/core/src/CommandManager.ts
Cameron Hessler 26610cdff3
fix(core) Nested chain not preserving dispatch state (#4152)
* Fix nested chain not preserving dispatch state

* Change test to read as sentence
2023-07-07 11:20:29 +02:00

150 lines
3.8 KiB
TypeScript

import { EditorState, Transaction } from '@tiptap/pm/state'
import { Editor } from './Editor.js'
import { createChainableState } from './helpers/createChainableState.js'
import {
AnyCommands, CanCommands, ChainedCommands, CommandProps, SingleCommands,
} from './types.js'
export class CommandManager {
editor: Editor
rawCommands: AnyCommands
customState?: EditorState
constructor(props: { editor: Editor; state?: EditorState }) {
this.editor = props.editor
this.rawCommands = this.editor.extensionManager.commands
this.customState = props.state
}
get hasCustomState(): boolean {
return !!this.customState
}
get state(): EditorState {
return this.customState || this.editor.state
}
get commands(): SingleCommands {
const { rawCommands, editor, state } = this
const { view } = editor
const { tr } = state
const props = this.buildProps(tr)
return Object.fromEntries(
Object.entries(rawCommands).map(([name, command]) => {
const method = (...args: any[]) => {
const callback = command(...args)(props)
if (!tr.getMeta('preventDispatch') && !this.hasCustomState) {
view.dispatch(tr)
}
return callback
}
return [name, method]
}),
) as unknown as SingleCommands
}
get chain(): () => ChainedCommands {
return () => this.createChain()
}
get can(): () => CanCommands {
return () => this.createCan()
}
public createChain(startTr?: Transaction, shouldDispatch = true): ChainedCommands {
const { rawCommands, editor, state } = this
const { view } = editor
const callbacks: boolean[] = []
const hasStartTransaction = !!startTr
const tr = startTr || state.tr
const run = () => {
if (
!hasStartTransaction
&& shouldDispatch
&& !tr.getMeta('preventDispatch')
&& !this.hasCustomState
) {
view.dispatch(tr)
}
return callbacks.every(callback => callback === true)
}
const chain = {
...Object.fromEntries(
Object.entries(rawCommands).map(([name, command]) => {
const chainedCommand = (...args: never[]) => {
const props = this.buildProps(tr, shouldDispatch)
const callback = command(...args)(props)
callbacks.push(callback)
return chain
}
return [name, chainedCommand]
}),
),
run,
} as unknown as ChainedCommands
return chain
}
public createCan(startTr?: Transaction): CanCommands {
const { rawCommands, state } = this
const dispatch = false
const tr = startTr || state.tr
const props = this.buildProps(tr, dispatch)
const formattedCommands = Object.fromEntries(
Object.entries(rawCommands).map(([name, command]) => {
return [name, (...args: never[]) => command(...args)({ ...props, dispatch: undefined })]
}),
) as unknown as SingleCommands
return {
...formattedCommands,
chain: () => this.createChain(tr, dispatch),
} as CanCommands
}
public buildProps(tr: Transaction, shouldDispatch = true): CommandProps {
const { rawCommands, editor, state } = this
const { view } = editor
if (state.storedMarks) {
tr.setStoredMarks(state.storedMarks)
}
const props: CommandProps = {
tr,
editor,
view,
state: createChainableState({
state,
transaction: tr,
}),
dispatch: shouldDispatch ? () => undefined : undefined,
chain: () => this.createChain(tr, shouldDispatch),
can: () => this.createCan(tr),
get commands() {
return Object.fromEntries(
Object.entries(rawCommands).map(([name, command]) => {
return [name, (...args: never[]) => command(...args)(props)]
}),
) as unknown as SingleCommands
},
}
return props
}
}