tiptap/packages/core/src/CommandManager.ts

183 lines
4.4 KiB
TypeScript
Raw Normal View History

2020-09-24 06:29:05 +08:00
import { EditorState, Transaction } from 'prosemirror-state'
2020-11-17 04:42:35 +08:00
import { Editor } from './Editor'
2020-11-03 00:18:12 +08:00
import {
2020-11-16 22:56:44 +08:00
SingleCommands,
ChainedCommands,
2020-11-17 00:25:55 +08:00
CanCommands,
CommandSpec,
2020-11-17 04:42:35 +08:00
} from './types'
2020-11-30 16:42:53 +08:00
import getAllMethodNames from './utilities/getAllMethodNames'
2020-09-23 03:25:32 +08:00
export default class CommandManager {
editor: Editor
2020-09-24 06:29:05 +08:00
commands: { [key: string]: any } = {}
2020-09-23 03:25:32 +08:00
2020-09-24 15:49:46 +08:00
methodNames: string[] = []
2020-09-23 14:59:21 +08:00
constructor(editor: Editor) {
2020-09-23 03:25:32 +08:00
this.editor = editor
2020-09-24 15:49:46 +08:00
this.methodNames = getAllMethodNames(this.editor)
2020-09-23 14:59:21 +08:00
}
/**
* Register a command.
*
* @param name The name of your command
* @param callback The method of your command
*/
public registerCommand(name: string, callback: CommandSpec): Editor {
if (this.commands[name]) {
2020-09-23 14:59:21 +08:00
throw new Error(`tiptap: command '${name}' is already defined.`)
}
2020-09-24 15:49:46 +08:00
if (this.methodNames.includes(name)) {
2020-09-23 14:59:21 +08:00
throw new Error(`tiptap: '${name}' is a protected name.`)
}
this.commands[name] = callback
2020-09-23 14:59:21 +08:00
return this.editor
2020-09-23 03:25:32 +08:00
}
public createCommands() {
const { commands, editor } = this
2020-11-13 16:58:30 +08:00
const { state, view } = editor
2020-12-01 19:45:30 +08:00
const { tr } = state
const props = this.buildProps(tr)
2020-11-13 16:58:30 +08:00
return Object.fromEntries(Object
.entries(commands)
2020-11-13 16:58:30 +08:00
.map(([name, command]) => {
const method = (...args: any) => {
const callback = command(...args)(props)
2020-12-02 16:28:55 +08:00
if (tr.steps.length) {
view.dispatch(tr)
}
2020-11-13 16:58:30 +08:00
return callback
}
return [name, method]
2020-11-16 22:56:44 +08:00
})) as SingleCommands
2020-11-13 16:58:30 +08:00
}
2020-11-02 21:29:58 +08:00
public createChain(startTr?: Transaction, shouldDispatch = true) {
const { commands, editor } = this
2020-09-23 03:25:32 +08:00
const { state, view } = editor
const callbacks: boolean[] = []
const hasStartTransaction = !!startTr
2020-11-13 16:58:30 +08:00
const tr = startTr || state.tr
2020-09-23 03:25:32 +08:00
return new Proxy({}, {
2020-09-23 03:35:02 +08:00
get: (_, name: string, proxy) => {
2020-09-23 03:25:32 +08:00
if (name === 'run') {
2020-12-02 16:28:55 +08:00
if (!hasStartTransaction && shouldDispatch && tr.steps.length) {
view.dispatch(tr)
}
2020-09-23 03:25:32 +08:00
return () => callbacks.every(callback => callback === true)
}
const command = commands[name]
2020-09-23 03:25:32 +08:00
if (!command) {
throw new Error(`tiptap: command '${name}' not found.`)
}
return (...args: any) => {
2020-12-02 06:32:39 +08:00
const props = this.buildProps(tr, shouldDispatch)
2020-09-23 03:25:32 +08:00
const callback = command(...args)(props)
2020-12-02 06:32:39 +08:00
2020-09-23 03:25:32 +08:00
callbacks.push(callback)
return proxy
}
2020-09-24 06:29:05 +08:00
},
2020-11-16 22:56:44 +08:00
}) as ChainedCommands
2020-09-23 03:25:32 +08:00
}
2020-11-03 00:18:12 +08:00
public createCan(startTr?: Transaction) {
const { commands, editor } = this
2020-11-03 00:18:12 +08:00
const { state } = editor
const dispatch = false
2020-11-13 16:58:30 +08:00
const tr = startTr || state.tr
2020-11-03 00:18:12 +08:00
const props = this.buildProps(tr, dispatch)
const formattedCommands = Object.fromEntries(Object
.entries(commands)
2020-11-03 00:18:12 +08:00
.map(([name, command]) => {
return [name, (...args: any[]) => command(...args)({ ...props, dispatch })]
2020-11-16 22:56:44 +08:00
})) as SingleCommands
2020-11-03 00:18:12 +08:00
return {
...formattedCommands,
chain: () => this.createChain(tr, dispatch),
2020-11-17 00:25:55 +08:00
} as CanCommands
2020-11-03 00:18:12 +08:00
}
2020-11-02 21:29:58 +08:00
public buildProps(tr: Transaction, shouldDispatch = true) {
const { editor, commands } = this
2020-09-23 03:35:02 +08:00
const { state, view } = editor
2020-12-02 06:32:39 +08:00
if (state.storedMarks) {
tr.setStoredMarks(state.storedMarks)
}
2020-09-23 03:35:02 +08:00
const props = {
tr,
2020-09-23 03:35:02 +08:00
editor,
view,
state: this.chainableState(tr, state),
2020-11-02 21:29:58 +08:00
dispatch: shouldDispatch
? () => undefined
2020-11-02 21:29:58 +08:00
: undefined,
chain: () => this.createChain(tr),
2020-11-03 00:18:12 +08:00
can: () => this.createCan(tr),
2020-09-23 03:35:02 +08:00
get commands() {
return Object.fromEntries(Object
.entries(commands)
2020-09-23 03:35:02 +08:00
.map(([name, command]) => {
return [name, (...args: any[]) => command(...args)(props)]
}))
2020-09-24 06:29:05 +08:00
},
2020-09-23 03:35:02 +08:00
}
return props
}
public chainableState(tr: Transaction, state: EditorState): EditorState {
2020-12-02 06:32:39 +08:00
let { selection } = tr
let { doc } = tr
let { storedMarks } = tr
2020-09-23 03:25:32 +08:00
return {
...state,
schema: state.schema,
plugins: state.plugins,
apply: state.apply.bind(state),
applyTransaction: state.applyTransaction.bind(state),
reconfigure: state.reconfigure.bind(state),
toJSON: state.toJSON.bind(state),
get storedMarks() {
2020-12-02 06:32:39 +08:00
return storedMarks
2020-09-23 03:25:32 +08:00
},
get selection() {
2020-12-02 06:32:39 +08:00
return selection
2020-09-23 03:25:32 +08:00
},
get doc() {
2020-12-02 06:32:39 +08:00
return doc
2020-09-23 03:25:32 +08:00
},
get tr() {
2020-12-02 06:32:39 +08:00
selection = tr.selection
doc = tr.doc
storedMarks = tr.storedMarks
2020-09-23 03:25:32 +08:00
return tr
},
2020-09-24 06:29:05 +08:00
}
2020-09-23 03:25:32 +08:00
}
2020-09-24 06:29:05 +08:00
}