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:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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 rule’s
|
||||
* 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',
|
||||
|
||||
@@ -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 rule’s
|
||||
* 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,
|
||||
|
||||
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user