diff --git a/packages/core/src/CommandManager.ts b/packages/core/src/CommandManager.ts index cff6c00a..68a90843 100644 --- a/packages/core/src/CommandManager.ts +++ b/packages/core/src/CommandManager.ts @@ -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 diff --git a/packages/core/src/Editor.ts b/packages/core/src/Editor.ts index 1af42795..8ad0bf69 100644 --- a/packages/core/src/Editor.ts +++ b/packages/core/src/Editor.ts @@ -104,21 +104,21 @@ export class Editor extends EventEmitter { * 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 { * Creates an command manager. */ private createCommandManager(): void { - this.commandManager = new CommandManager(this, this.extensionManager.commands) + this.commandManager = new CommandManager({ + editor: this, + }) } /** diff --git a/packages/core/src/ExtensionManager.ts b/packages/core/src/ExtensionManager.ts index 66c20c4e..0056179d 100644 --- a/packages/core/src/ExtensionManager.ts +++ b/packages/core/src/ExtensionManager.ts @@ -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, ] } diff --git a/packages/core/src/InputRule.ts b/packages/core/src/InputRule.ts index 0f6933e0..01ae4942 100644 --- a/packages/core/src/InputRule.ts +++ b/packages/core/src/InputRule.ts @@ -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', diff --git a/packages/core/src/PasteRule.ts b/packages/core/src/PasteRule.ts index 981a8ab7..7d4e9c96 100644 --- a/packages/core/src/PasteRule.ts +++ b/packages/core/src/PasteRule.ts @@ -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, diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 424a28b2..241ceff0 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -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'