@atlaskit/editor-plugin-track-changes
Version:
ShowDiff plugin for @atlaskit/editor-core
126 lines (123 loc) • 6.94 kB
JavaScript
import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
import { PluginKey } from '@atlaskit/editor-prosemirror/state';
import { ReplaceAroundStep, ReplaceStep, AddMarkStep, RemoveMarkStep, AttrStep } from '@atlaskit/editor-prosemirror/transform';
import { filterSteps } from './filterSteps';
import { InvertableStep } from './invertableStep';
import { TOGGLE_TRACK_CHANGES_ACTION as ACTION } from './types';
export const trackChangesPluginKey = new PluginKey('trackChangesPlugin');
// Exported for test purposes
export const getBaselineFromSteps = (doc, steps) => {
try {
// Filter out AttrStep's since attribute changes shouldn't affect baseline content comparison
const contentSteps = steps.filter(step => !(step.step instanceof AttrStep));
for (const step of contentSteps.slice().reverse()) {
const result = step.inverted.apply(doc);
if (result.failed === null && result.doc) {
doc = result.doc;
}
}
return doc;
} catch {
// Temporary - we need to understand how this happens - but we want to unblock issues where this crashes the editor
return undefined;
}
};
export const createTrackChangesPlugin = api => {
// Mark the state to be reset on next time the document has a meaningful change
let resetBaseline = false;
return new SafePlugin({
key: trackChangesPluginKey,
state: {
init() {
return {
steps: [],
shouldChangesBeDisplayed: false,
isShowDiffAvailable: false,
allocations: new Set()
};
},
apply(tr, state) {
var _api$history, _state$steps$at$alloc, _state$steps$at;
const metadata = tr.getMeta(trackChangesPluginKey);
if (metadata && metadata.action === ACTION.RESET_BASELINE) {
return {
...state,
steps: [],
isShowDiffAvailable: false
};
}
if (metadata && metadata.action === ACTION.TOGGLE_TRACK_CHANGES) {
resetBaseline = true;
return {
...state,
shouldChangesBeDisplayed: !state.shouldChangesBeDisplayed
};
}
const isDocChanged = tr.docChanged && tr.steps.some(step => step instanceof ReplaceStep || step instanceof ReplaceAroundStep || step instanceof AddMarkStep || step instanceof RemoveMarkStep || step instanceof AttrStep);
const isAnnotationStep = step => step instanceof AddMarkStep && step.mark.type.name === 'annotation';
if (!isDocChanged || tr.getMeta('isRemote') || tr.getMeta('replaceDocument') || tr.steps.some(isAnnotationStep)) {
// If the transaction is remote, we need to map the steps to the current document
return {
...state,
steps: state.steps.map(s => {
const newStep = s.step.map(tr.mapping);
const newInvertedStep = s.inverted.map(tr.mapping);
if (newStep && newInvertedStep) {
return new InvertableStep(newStep, newInvertedStep, s.allocation);
}
return undefined;
}).filter(s => !!s)
};
}
// If we don't have the history plugin don't limit the change tracking
const historyState = api === null || api === void 0 ? void 0 : (_api$history = api.history) === null || _api$history === void 0 ? void 0 : _api$history.sharedState.currentState();
const currentAllocation = historyState ?
// Combine both done + undone so we have the total "distance".
historyState.done.eventCount + historyState.undone.eventCount : ((_state$steps$at$alloc = (_state$steps$at = state.steps.at(-1)) === null || _state$steps$at === void 0 ? void 0 : _state$steps$at.allocation) !== null && _state$steps$at$alloc !== void 0 ? _state$steps$at$alloc : 0) + 1;
const newSteps = tr.steps.map((step, idx) => new InvertableStep(step, step.invert(tr.docs[idx]), currentAllocation));
const concatSteps = resetBaseline ? newSteps : [...state.steps, ...newSteps];
resetBaseline = false;
const {
allocations,
steps
} = filterSteps(concatSteps, state.allocations.add(currentAllocation));
// Calculate if there are actual changes by comparing current doc with baseline
const baselineDoc = getBaselineFromSteps(tr.doc, steps);
const hasChangesFromBaseline = baselineDoc ? !tr.doc.eq(baselineDoc) : false;
// Create a new ChangeSet based on document changes
return {
...state,
allocations,
steps,
shouldChangesBeDisplayed: false,
isShowDiffAvailable: hasChangesFromBaseline
};
}
},
view() {
return {
update(view, prevState) {
var _trackChangesPluginKe;
const prevShouldChangesBeDisplayed = (_trackChangesPluginKe = trackChangesPluginKey.getState(prevState)) === null || _trackChangesPluginKe === void 0 ? void 0 : _trackChangesPluginKe.shouldChangesBeDisplayed;
const currentPluginState = trackChangesPluginKey.getState(view.state);
const currentShouldChangesBeDisplayed = currentPluginState === null || currentPluginState === void 0 ? void 0 : currentPluginState.shouldChangesBeDisplayed;
if (prevShouldChangesBeDisplayed === false && currentShouldChangesBeDisplayed === true) {
var _currentPluginState$s;
const steps = (_currentPluginState$s = currentPluginState === null || currentPluginState === void 0 ? void 0 : currentPluginState.steps) !== null && _currentPluginState$s !== void 0 ? _currentPluginState$s : [];
const originalDoc = getBaselineFromSteps(view.state.doc, steps);
if (originalDoc) {
var _api$core, _api$showDiff, _api$showDiff$command;
api === null || api === void 0 ? void 0 : (_api$core = api.core) === null || _api$core === void 0 ? void 0 : _api$core.actions.execute(api === null || api === void 0 ? void 0 : (_api$showDiff = api.showDiff) === null || _api$showDiff === void 0 ? void 0 : (_api$showDiff$command = _api$showDiff.commands) === null || _api$showDiff$command === void 0 ? void 0 : _api$showDiff$command.showDiff({
originalDoc,
steps: steps.map(s => s.step)
}));
}
} else if (currentShouldChangesBeDisplayed === false && prevShouldChangesBeDisplayed === true) {
var _api$core2, _api$showDiff2, _api$showDiff2$comman;
api === null || api === void 0 ? void 0 : (_api$core2 = api.core) === null || _api$core2 === void 0 ? void 0 : _api$core2.actions.execute(api === null || api === void 0 ? void 0 : (_api$showDiff2 = api.showDiff) === null || _api$showDiff2 === void 0 ? void 0 : (_api$showDiff2$comman = _api$showDiff2.commands) === null || _api$showDiff2$comman === void 0 ? void 0 : _api$showDiff2$comman.hideDiff);
}
}
};
}
});
};