Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow to use commands within InputRule and PasteRule #2035

Merged
merged 4 commits into from
Oct 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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