Skip to content

Instantly share code, notes, and snippets.

@bradleyayers
Created October 7, 2018 20:28
Show Gist options
  • Save bradleyayers/1d2da8d375517ab82bf27471276cd6ac to your computer and use it in GitHub Desktop.
Save bradleyayers/1d2da8d375517ab82bf27471276cd6ac to your computer and use it in GitHub Desktop.
// 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<S extends Schema = any> {
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<S extends Schema = any> extends Plugin<S> {
spec: Pick<PluginSpec<S>, "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 };
}
};
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment