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 { 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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 rule’s
|
* input that matches any of the given rules to trigger the rule’s
|
||||||
* 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',
|
||||||
|
|||||||
@@ -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 rule’s
|
* text that matches any of the given rules to trigger the rule’s
|
||||||
* 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,
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
Reference in New Issue
Block a user