// tslint:disable:no-invalid-this import memoizeOne from "memoize-one"; import { EditorState, Plugin, Transaction, PluginSpec } from "prosemirror-state"; import { Schema } from "prosemirror-model"; declare module "prosemirror-state" { // tslint:disable-next-line:no-any interface PluginSpec { replaceTransaction?: | ((tr: Transaction, oldState: EditorState, newState: EditorState) => undefined | null | false | Transaction) | null; } } /** * Installs a monkey patch for `replaceTransaction`. This avoids forking * prosemirror-state (which would be tricky for distribution), and serves as * reference implementation for https://github.com/ProseMirror/rfcs/pull/10. */ export const installReplaceTransactionMonkeyPatch = memoizeOne(() => { // tslint:disable-next-line:no-any interface PluginWithReplaceTransaction extends Plugin { spec: Pick, "replaceTransaction">; } interface EditorStateWithPrivate extends EditorState { filterTransaction(tr: Transaction, ignore?: number): boolean; applyInner(tr: Transaction): EditorState; config: { plugins: PluginWithReplaceTransaction[]; }; } function replaceTransaction( state: EditorState, tr: Transaction, ignorePlugin = -1 ): false | { tr: Transaction; state: EditorState } { if (!(state as EditorStateWithPrivate).filterTransaction(tr, ignorePlugin)) { return false; } let newState = (state as EditorStateWithPrivate).applyInner(tr); const { plugins } = (state as EditorStateWithPrivate).config; for (let i = 0; i < plugins.length; i++) { if (i !== ignorePlugin) { const plugin = plugins[i]; if (plugin.spec.replaceTransaction != null) { const replaceResult = plugin.spec.replaceTransaction.call(plugin, tr, state, newState); if (replaceResult === false) { // Equivalent effect of `filterTransaction` return false; } else if (replaceResult != null) { tr = replaceResult; newState = (state as EditorStateWithPrivate).applyInner(tr); } } } } return { tr, state: newState }; } EditorState.prototype.applyTransaction = function(tr) { const replaceResult = replaceTransaction(this, tr); if (replaceResult === false) { return { state: this, transactions: [] }; } // tslint:disable-next-line:no-any const { plugins } = (this as any).config; const trs = [replaceResult.tr]; let newState = replaceResult.state; let seen = null; // This loop repeatedly gives plugins a chance to respond to // transactions as new transactions are added, making sure to only // pass the transactions the plugin did not see before. for (;;) { let haveNew = false; for (let i = 0; i < plugins.length; i++) { const plugin = plugins[i]; if (plugin.spec.appendTransaction) { const n = seen !== null ? seen[i].n : 0; const oldState = seen !== null ? seen[i].state : this; const trAppended = n < trs.length && plugin.spec.appendTransaction.call(plugin, n > 0 ? trs.slice(n) : trs, oldState, newState); if (trAppended) { const replaceResult = replaceTransaction(newState, trAppended, i); if (replaceResult !== false) { replaceResult.tr.setMeta("appendedTransaction", tr); if (seen === null) { seen = []; for (let j = 0; j < plugins.length; j++) { seen.push(j < i ? { state: newState, n: trs.length } : { state: this, n: 0 }); } } trs.push(replaceResult.tr); newState = replaceResult.state; haveNew = true; } } if (seen !== null) seen[i] = { state: newState, n: trs.length }; } } if (!haveNew) return { state: newState, transactions: trs }; } }; });