Skip to content

Commit

Permalink
feat: Allow to use commands within InputRule and PasteRule (#2035)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
philippkuehn authored Oct 14, 2021
1 parent 8c32dab commit 4303637
Show file tree
Hide file tree
Showing 6 changed files with 126 additions and 40 deletions.
66 changes: 46 additions & 20 deletions packages/core/src/CommandManager.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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
}

get hasCustomState(): boolean {
return !!this.customState
}

get state(): EditorState {
return this.customState || this.editor.state
}

public createCommands(): SingleCommands {
const { commands, editor } = this
const { state, view } = editor
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)
}

Expand All @@ -43,23 +57,36 @@ 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)
}

return callbacks.every(callback => callback === true)
}

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)
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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
Expand Down
10 changes: 6 additions & 4 deletions packages/core/src/Editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}

/**
Expand Down Expand Up @@ -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,
})
}

/**
Expand Down
20 changes: 14 additions & 6 deletions packages/core/src/ExtensionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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),
}

Expand All @@ -238,7 +240,7 @@ export default class ExtensionManager {
Object
.entries(addKeyboardShortcuts())
.map(([shortcut, method]) => {
return [shortcut, () => method({ editor: this.editor })]
return [shortcut, () => method({ editor })]
}),
)

Expand All @@ -253,7 +255,7 @@ export default class ExtensionManager {
context,
)

if (this.editor.options.enableInputRules && addInputRules) {
if (editor.options.enableInputRules && addInputRules) {
inputRules.push(...addInputRules())
}

Expand All @@ -263,7 +265,7 @@ export default class ExtensionManager {
context,
)

if (this.editor.options.enablePasteRules && addPasteRules) {
if (editor.options.enablePasteRules && addPasteRules) {
pasteRules.push(...addPasteRules())
}

Expand All @@ -284,8 +286,14 @@ export default class ExtensionManager {
.flat()

return [
inputRulesPlugin(inputRules),
pasteRulesPlugin(pasteRules),
inputRulesPlugin({
editor,
rules: inputRules,
}),
pasteRulesPlugin({
editor,
rules: pasteRules,
}),
...allPlugins,
]
}
Expand Down
39 changes: 31 additions & 8 deletions packages/core/src/InputRule.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -23,6 +30,9 @@ export class InputRule {
state: EditorState,
range: Range,
match: ExtendedRegExpMatchArray,
commands: SingleCommands,
chain: () => ChainedCommands,
can: () => CanCommands,
}) => void

constructor(config: {
Expand All @@ -31,6 +41,9 @@ export class InputRule {
state: EditorState,
range: Range,
match: ExtendedRegExpMatchArray,
commands: SingleCommands,
chain: () => ChainedCommands,
can: () => CanCommands,
}) => void,
}) {
this.find = config.find
Expand Down Expand Up @@ -68,21 +81,22 @@ const inputRuleMatcherHandler = (text: string, find: InputRuleFinder): ExtendedR
}

function run(config: {
view: EditorView,
editor: Editor,
from: number,
to: number,
text: string,
rules: InputRule[],
plugin: Plugin,
}): any {
const {
view,
editor,
from,
to,
text,
rules,
plugin,
} = config
const { view } = editor

if (view.composing) {
return false
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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() {
Expand All @@ -183,7 +206,7 @@ export function inputRulesPlugin(rules: InputRule[]): Plugin {
props: {
handleTextInput(view, from, to, text) {
return run({
view,
editor,
from,
to,
text,
Expand All @@ -199,7 +222,7 @@ export function inputRulesPlugin(rules: InputRule[]): Plugin {

if ($cursor) {
run({
view,
editor,
from: $cursor.pos,
to: $cursor.pos,
text: '',
Expand All @@ -224,7 +247,7 @@ export function inputRulesPlugin(rules: InputRule[]): Plugin {

if ($cursor) {
return run({
view,
editor,
from: $cursor.pos,
to: $cursor.pos,
text: '\n',
Expand Down
Loading

0 comments on commit 4303637

Please sign in to comment.