feat: Allow to use commands within InputRule and PasteRule (#2035)

* add optional state prop to commandmanager

* add commands, chain and can getter to commandmanager

* use custom CommandManager for input rules and paste rules

* export commandmanager
This commit is contained in:
Philipp Kühn
2021-10-14 11:56:40 +02:00
committed by GitHub
parent 8c32dab55c
commit 4303637a78
6 changed files with 126 additions and 40 deletions

View File

@@ -1,4 +1,4 @@
import { Transaction } from 'prosemirror-state' import { EditorState, Transaction } from 'prosemirror-state'
import { Editor } from './Editor' import { Editor } from './Editor'
import createChainableState from './helpers/createChainableState' import createChainableState from './helpers/createChainableState'
import { import {
@@ -13,26 +13,40 @@ export default class CommandManager {
editor: Editor editor: Editor
commands: AnyCommands rawCommands: AnyCommands
constructor(editor: Editor, commands: AnyCommands) { customState?: EditorState
this.editor = editor
this.commands = commands constructor(props: {
editor: Editor,
state?: EditorState,
}) {
this.editor = props.editor
this.rawCommands = this.editor.extensionManager.commands
this.customState = props.state
} }
public createCommands(): SingleCommands { get hasCustomState(): boolean {
const { commands, editor } = this return !!this.customState
const { state, view } = editor }
get state(): EditorState {
return this.customState || this.editor.state
}
get commands(): SingleCommands {
const { rawCommands, editor, state } = this
const { view } = editor
const { tr } = state const { tr } = state
const props = this.buildProps(tr) const props = this.buildProps(tr)
return Object.fromEntries(Object return Object.fromEntries(Object
.entries(commands) .entries(rawCommands)
.map(([name, command]) => { .map(([name, command]) => {
const method = (...args: any[]) => { const method = (...args: any[]) => {
const callback = command(...args)(props) const callback = command(...args)(props)
if (!tr.getMeta('preventDispatch')) { if (!tr.getMeta('preventDispatch') && !this.hasCustomState) {
view.dispatch(tr) view.dispatch(tr)
} }
@@ -43,15 +57,28 @@ export default class CommandManager {
})) as unknown as SingleCommands })) as unknown as SingleCommands
} }
get chain(): () => ChainedCommands {
return () => this.createChain()
}
get can(): () => CanCommands {
return () => this.createCan()
}
public createChain(startTr?: Transaction, shouldDispatch = true): ChainedCommands { public createChain(startTr?: Transaction, shouldDispatch = true): ChainedCommands {
const { commands, editor } = this const { rawCommands, editor, state } = this
const { state, view } = editor const { view } = editor
const callbacks: boolean[] = [] const callbacks: boolean[] = []
const hasStartTransaction = !!startTr const hasStartTransaction = !!startTr
const tr = startTr || state.tr const tr = startTr || state.tr
const run = () => { const run = () => {
if (!hasStartTransaction && shouldDispatch && !tr.getMeta('preventDispatch')) { if (
!hasStartTransaction
&& shouldDispatch
&& !tr.getMeta('preventDispatch')
&& !this.hasCustomState
) {
view.dispatch(tr) view.dispatch(tr)
} }
@@ -59,7 +86,7 @@ export default class CommandManager {
} }
const chain = { const chain = {
...Object.fromEntries(Object.entries(commands).map(([name, command]) => { ...Object.fromEntries(Object.entries(rawCommands).map(([name, command]) => {
const chainedCommand = (...args: never[]) => { const chainedCommand = (...args: never[]) => {
const props = this.buildProps(tr, shouldDispatch) const props = this.buildProps(tr, shouldDispatch)
const callback = command(...args)(props) const callback = command(...args)(props)
@@ -78,13 +105,12 @@ export default class CommandManager {
} }
public createCan(startTr?: Transaction): CanCommands { public createCan(startTr?: Transaction): CanCommands {
const { commands, editor } = this const { rawCommands, state } = this
const { state } = editor
const dispatch = undefined const dispatch = undefined
const tr = startTr || state.tr const tr = startTr || state.tr
const props = this.buildProps(tr, dispatch) const props = this.buildProps(tr, dispatch)
const formattedCommands = Object.fromEntries(Object const formattedCommands = Object.fromEntries(Object
.entries(commands) .entries(rawCommands)
.map(([name, command]) => { .map(([name, command]) => {
return [name, (...args: never[]) => command(...args)({ ...props, dispatch })] return [name, (...args: never[]) => command(...args)({ ...props, dispatch })]
})) as unknown as SingleCommands })) as unknown as SingleCommands
@@ -96,8 +122,8 @@ export default class CommandManager {
} }
public buildProps(tr: Transaction, shouldDispatch = true): CommandProps { public buildProps(tr: Transaction, shouldDispatch = true): CommandProps {
const { editor, commands } = this const { rawCommands, editor, state } = this
const { state, view } = editor const { view } = editor
if (state.storedMarks) { if (state.storedMarks) {
tr.setStoredMarks(state.storedMarks) tr.setStoredMarks(state.storedMarks)
@@ -118,7 +144,7 @@ export default class CommandManager {
can: () => this.createCan(tr), can: () => this.createCan(tr),
get commands() { get commands() {
return Object.fromEntries(Object return Object.fromEntries(Object
.entries(commands) .entries(rawCommands)
.map(([name, command]) => { .map(([name, command]) => {
return [name, (...args: never[]) => command(...args)(props)] return [name, (...args: never[]) => command(...args)(props)]
})) as unknown as SingleCommands })) as unknown as SingleCommands

View File

@@ -104,21 +104,21 @@ export class Editor extends EventEmitter<EditorEvents> {
* An object of all registered commands. * An object of all registered commands.
*/ */
public get commands(): SingleCommands { public get commands(): SingleCommands {
return this.commandManager.createCommands() return this.commandManager.commands
} }
/** /**
* Create a command chain to call multiple commands at once. * Create a command chain to call multiple commands at once.
*/ */
public chain(): ChainedCommands { public chain(): ChainedCommands {
return this.commandManager.createChain() return this.commandManager.chain()
} }
/** /**
* Check if a command or a command chain can be executed. Without executing it. * Check if a command or a command chain can be executed. Without executing it.
*/ */
public can(): CanCommands { public can(): CanCommands {
return this.commandManager.createCan() return this.commandManager.can()
} }
/** /**
@@ -235,7 +235,9 @@ export class Editor extends EventEmitter<EditorEvents> {
* Creates an command manager. * Creates an command manager.
*/ */
private createCommandManager(): void { private createCommandManager(): void {
this.commandManager = new CommandManager(this, this.extensionManager.commands) this.commandManager = new CommandManager({
editor: this,
})
} }
/** /**

View File

@@ -206,6 +206,8 @@ export default class ExtensionManager {
} }
get plugins(): Plugin[] { get plugins(): Plugin[] {
const { editor } = this
// With ProseMirror, first plugins within an array are executed first. // With ProseMirror, first plugins within an array are executed first.
// In tiptap, we provide the ability to override plugins, // In tiptap, we provide the ability to override plugins,
// so it feels more natural to run plugins at the end of an array first. // so it feels more natural to run plugins at the end of an array first.
@@ -221,7 +223,7 @@ export default class ExtensionManager {
const context = { const context = {
name: extension.name, name: extension.name,
options: extension.options, options: extension.options,
editor: this.editor, editor,
type: getSchemaTypeByName(extension.name, this.schema), type: getSchemaTypeByName(extension.name, this.schema),
} }
@@ -238,7 +240,7 @@ export default class ExtensionManager {
Object Object
.entries(addKeyboardShortcuts()) .entries(addKeyboardShortcuts())
.map(([shortcut, method]) => { .map(([shortcut, method]) => {
return [shortcut, () => method({ editor: this.editor })] return [shortcut, () => method({ editor })]
}), }),
) )
@@ -253,7 +255,7 @@ export default class ExtensionManager {
context, context,
) )
if (this.editor.options.enableInputRules && addInputRules) { if (editor.options.enableInputRules && addInputRules) {
inputRules.push(...addInputRules()) inputRules.push(...addInputRules())
} }
@@ -263,7 +265,7 @@ export default class ExtensionManager {
context, context,
) )
if (this.editor.options.enablePasteRules && addPasteRules) { if (editor.options.enablePasteRules && addPasteRules) {
pasteRules.push(...addPasteRules()) pasteRules.push(...addPasteRules())
} }
@@ -284,8 +286,14 @@ export default class ExtensionManager {
.flat() .flat()
return [ return [
inputRulesPlugin(inputRules), inputRulesPlugin({
pasteRulesPlugin(pasteRules), editor,
rules: inputRules,
}),
pasteRulesPlugin({
editor,
rules: pasteRules,
}),
...allPlugins, ...allPlugins,
] ]
} }

View File

@@ -1,8 +1,15 @@
import { EditorView } from 'prosemirror-view'
import { EditorState, Plugin, TextSelection } from 'prosemirror-state' import { EditorState, Plugin, TextSelection } from 'prosemirror-state'
import { Editor } from './Editor'
import CommandManager from './CommandManager'
import createChainableState from './helpers/createChainableState' import createChainableState from './helpers/createChainableState'
import isRegExp from './utilities/isRegExp' import isRegExp from './utilities/isRegExp'
import { Range, ExtendedRegExpMatchArray } from './types' import {
Range,
ExtendedRegExpMatchArray,
SingleCommands,
ChainedCommands,
CanCommands,
} from './types'
export type InputRuleMatch = { export type InputRuleMatch = {
index: number, index: number,
@@ -23,6 +30,9 @@ export class InputRule {
state: EditorState, state: EditorState,
range: Range, range: Range,
match: ExtendedRegExpMatchArray, match: ExtendedRegExpMatchArray,
commands: SingleCommands,
chain: () => ChainedCommands,
can: () => CanCommands,
}) => void }) => void
constructor(config: { constructor(config: {
@@ -31,6 +41,9 @@ export class InputRule {
state: EditorState, state: EditorState,
range: Range, range: Range,
match: ExtendedRegExpMatchArray, match: ExtendedRegExpMatchArray,
commands: SingleCommands,
chain: () => ChainedCommands,
can: () => CanCommands,
}) => void, }) => void,
}) { }) {
this.find = config.find this.find = config.find
@@ -68,7 +81,7 @@ const inputRuleMatcherHandler = (text: string, find: InputRuleFinder): ExtendedR
} }
function run(config: { function run(config: {
view: EditorView, editor: Editor,
from: number, from: number,
to: number, to: number,
text: string, text: string,
@@ -76,13 +89,14 @@ function run(config: {
plugin: Plugin, plugin: Plugin,
}): any { }): any {
const { const {
view, editor,
from, from,
to, to,
text, text,
rules, rules,
plugin, plugin,
} = config } = config
const { view } = editor
if (view.composing) { if (view.composing) {
return false return false
@@ -129,10 +143,18 @@ function run(config: {
to, to,
} }
const { commands, chain, can } = new CommandManager({
editor,
state,
})
rule.handler({ rule.handler({
state, state,
range, range,
match, match,
commands,
chain,
can,
}) })
// stop if there are no changes // stop if there are no changes
@@ -161,7 +183,8 @@ function run(config: {
* input that matches any of the given rules to trigger the rules * input that matches any of the given rules to trigger the rules
* action. * action.
*/ */
export function inputRulesPlugin(rules: InputRule[]): Plugin { export function inputRulesPlugin(props: { editor: Editor, rules: InputRule[] }): Plugin {
const { editor, rules } = props
const plugin = new Plugin({ const plugin = new Plugin({
state: { state: {
init() { init() {
@@ -183,7 +206,7 @@ export function inputRulesPlugin(rules: InputRule[]): Plugin {
props: { props: {
handleTextInput(view, from, to, text) { handleTextInput(view, from, to, text) {
return run({ return run({
view, editor,
from, from,
to, to,
text, text,
@@ -199,7 +222,7 @@ export function inputRulesPlugin(rules: InputRule[]): Plugin {
if ($cursor) { if ($cursor) {
run({ run({
view, editor,
from: $cursor.pos, from: $cursor.pos,
to: $cursor.pos, to: $cursor.pos,
text: '', text: '',
@@ -224,7 +247,7 @@ export function inputRulesPlugin(rules: InputRule[]): Plugin {
if ($cursor) { if ($cursor) {
return run({ return run({
view, editor,
from: $cursor.pos, from: $cursor.pos,
to: $cursor.pos, to: $cursor.pos,
text: '\n', text: '\n',

View File

@@ -1,7 +1,15 @@
import { EditorState, Plugin } from 'prosemirror-state' import { EditorState, Plugin } from 'prosemirror-state'
import { Editor } from './Editor'
import CommandManager from './CommandManager'
import createChainableState from './helpers/createChainableState' import createChainableState from './helpers/createChainableState'
import isRegExp from './utilities/isRegExp' import isRegExp from './utilities/isRegExp'
import { Range, ExtendedRegExpMatchArray } from './types' import {
Range,
ExtendedRegExpMatchArray,
SingleCommands,
ChainedCommands,
CanCommands,
} from './types'
export type PasteRuleMatch = { export type PasteRuleMatch = {
index: number, index: number,
@@ -22,6 +30,9 @@ export class PasteRule {
state: EditorState, state: EditorState,
range: Range, range: Range,
match: ExtendedRegExpMatchArray, match: ExtendedRegExpMatchArray,
commands: SingleCommands,
chain: () => ChainedCommands,
can: () => CanCommands,
}) => void }) => void
constructor(config: { constructor(config: {
@@ -30,6 +41,9 @@ export class PasteRule {
state: EditorState, state: EditorState,
range: Range, range: Range,
match: ExtendedRegExpMatchArray, match: ExtendedRegExpMatchArray,
commands: SingleCommands,
chain: () => ChainedCommands,
can: () => CanCommands,
}) => void, }) => void,
}) { }) {
this.find = config.find this.find = config.find
@@ -69,6 +83,7 @@ const pasteRuleMatcherHandler = (text: string, find: PasteRuleFinder): ExtendedR
} }
function run(config: { function run(config: {
editor: Editor,
state: EditorState, state: EditorState,
from: number, from: number,
to: number, to: number,
@@ -76,12 +91,18 @@ function run(config: {
plugin: Plugin, plugin: Plugin,
}): any { }): any {
const { const {
editor,
state, state,
from, from,
to, to,
rules, rules,
} = config } = config
const { commands, chain, can } = new CommandManager({
editor,
state,
})
state.doc.nodesBetween(from, to, (node, pos) => { state.doc.nodesBetween(from, to, (node, pos) => {
if (!node.isTextblock || node.type.spec.code) { if (!node.isTextblock || node.type.spec.code) {
return return
@@ -115,6 +136,9 @@ function run(config: {
state, state,
range, range,
match, match,
commands,
chain,
can,
}) })
}) })
}) })
@@ -126,7 +150,8 @@ function run(config: {
* text that matches any of the given rules to trigger the rules * text that matches any of the given rules to trigger the rules
* action. * action.
*/ */
export function pasteRulesPlugin(rules: PasteRule[]): Plugin { export function pasteRulesPlugin(props: { editor: Editor, rules: PasteRule[] }): Plugin {
const { editor, rules } = props
let isProseMirrorHTML = false let isProseMirrorHTML = false
const plugin = new Plugin({ const plugin = new Plugin({
@@ -165,6 +190,7 @@ export function pasteRulesPlugin(rules: PasteRule[]): Plugin {
}) })
run({ run({
editor,
state: chainableState, state: chainableState,
from: Math.max(from - 1, 0), from: Math.max(from - 1, 0),
to: to.b, to: to.b,

View File

@@ -9,6 +9,7 @@ export * from './NodeView'
export * from './Tracker' export * from './Tracker'
export * from './InputRule' export * from './InputRule'
export * from './PasteRule' export * from './PasteRule'
export * from './CommandManager'
export * from './types' export * from './types'
export { default as nodeInputRule } from './inputRules/nodeInputRule' export { default as nodeInputRule } from './inputRules/nodeInputRule'