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

View File

@@ -104,21 +104,21 @@ export class Editor extends EventEmitter<EditorEvents> {
* An object of all registered commands.
*/
public get commands(): SingleCommands {
return this.commandManager.createCommands()
return this.commandManager.commands
}
/**
* Create a command chain to call multiple commands at once.
*/
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.
*/
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.
*/
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[] {
const { editor } = this
// With ProseMirror, first plugins within an array are executed first.
// In tiptap, we provide the ability to override plugins,
// 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 = {
name: extension.name,
options: extension.options,
editor: this.editor,
editor,
type: getSchemaTypeByName(extension.name, this.schema),
}
@@ -238,7 +240,7 @@ export default class ExtensionManager {
Object
.entries(addKeyboardShortcuts())
.map(([shortcut, method]) => {
return [shortcut, () => method({ editor: this.editor })]
return [shortcut, () => method({ editor })]
}),
)
@@ -253,7 +255,7 @@ export default class ExtensionManager {
context,
)
if (this.editor.options.enableInputRules && addInputRules) {
if (editor.options.enableInputRules && addInputRules) {
inputRules.push(...addInputRules())
}
@@ -263,7 +265,7 @@ export default class ExtensionManager {
context,
)
if (this.editor.options.enablePasteRules && addPasteRules) {
if (editor.options.enablePasteRules && addPasteRules) {
pasteRules.push(...addPasteRules())
}
@@ -284,8 +286,14 @@ export default class ExtensionManager {
.flat()
return [
inputRulesPlugin(inputRules),
pasteRulesPlugin(pasteRules),
inputRulesPlugin({
editor,
rules: inputRules,
}),
pasteRulesPlugin({
editor,
rules: pasteRules,
}),
...allPlugins,
]
}

View File

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

View File

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

View File

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