UNPKG

@atlaskit/editor-plugin-show-diff

Version:

ShowDiff plugin for @atlaskit/editor-core

223 lines (222 loc) 7.99 kB
// eslint-disable-next-line @atlassian/tangerine/import/entry-points import isEqual from 'lodash/isEqual'; import memoizeOne from 'memoize-one'; import { ChangeSet, simplifyChanges } from 'prosemirror-changeset'; import { areNodesEqualIgnoreAttrs } from '@atlaskit/editor-common/utils/document'; import { DecorationSet } from '@atlaskit/editor-prosemirror/view'; import { fg } from '@atlaskit/platform-feature-flags'; import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals'; import { areDocsEqualByBlockStructureAndText } from '../areDocsEqualByBlockStructureAndText'; import { createBlockChangedDecoration } from '../decorations/createBlockChangedDecoration'; import { createInlineChangedDecoration } from '../decorations/createInlineChangedDecoration'; import { createNodeChangedDecorationWidget } from '../decorations/createNodeChangedDecorationWidget'; import { getAttrChangeRanges, stepIsValidAttrChange } from '../decorations/utils/getAttrChangeRanges'; import { getMarkChangeRanges } from '../decorations/utils/getMarkChangeRanges'; import { groupChangesByBlock } from './groupChangesByBlock'; import { optimizeChanges } from './optimizeChanges'; import { simplifySteps } from './simplifySteps'; const getChanges = ({ changeset, originalDoc, steppedDoc, diffType, tr }) => { if (diffType === 'block' && expValEquals('platform_editor_diff_plugin_extended', 'isEnabled', true)) { return groupChangesByBlock(changeset.changes, originalDoc, steppedDoc); } const changes = simplifyChanges(changeset.changes, tr.doc); return optimizeChanges(changes); }; const calculateNodesForBlockDecoration = ({ doc, from, to, colorScheme, isInserted = true, activeIndexPos }) => { const decorations = []; // Iterate over the document nodes within the range doc.nodesBetween(from, to, (node, pos) => { if (node.isBlock && (!expValEquals('platform_editor_diff_plugin_extended', 'isEnabled', true) || pos + node.nodeSize <= to)) { const nodeEnd = pos + node.nodeSize; const isActive = activeIndexPos && pos === activeIndexPos.from && nodeEnd === activeIndexPos.to; const decoration = createBlockChangedDecoration({ change: { from: pos, to: nodeEnd, name: node.type.name }, colorScheme, isInserted, isActive }); if (decoration) { decorations.push(decoration); } } }); return decorations; }; const calculateDiffDecorationsInner = ({ state, pluginState, nodeViewSerializer, colorScheme, intl, activeIndexPos, api, isInverted = false, diffType = 'inline' }) => { const { originalDoc, steps } = pluginState; if (!originalDoc || !pluginState.isDisplayingChanges) { return DecorationSet.empty; } const { tr } = state; let steppedDoc = originalDoc; const attrSteps = []; const simplifiedSteps = simplifySteps(steps, originalDoc); const stepMaps = []; for (const step of simplifiedSteps) { const result = step.apply(steppedDoc); if (result.failed === null && result.doc) { if (stepIsValidAttrChange(step, steppedDoc, result.doc)) { attrSteps.push(step); } stepMaps.push(step.getMap()); steppedDoc = result.doc; } } // Rather than using .eq() we use a custom function that only checks for structural // changes and ignores differences in attributes which don't affect decoration positions if (!areNodesEqualIgnoreAttrs(steppedDoc, tr.doc)) { const recoveredViaContentEquality = fg('platform_editor_show_diff_equality_fallback') ? areDocsEqualByBlockStructureAndText(steppedDoc, tr.doc) : undefined; if (expValEquals('platform_editor_are_nodes_equal_ignore_mark_order', 'isEnabled', true)) { var _api$analytics; api === null || api === void 0 ? void 0 : (_api$analytics = api.analytics) === null || _api$analytics === void 0 ? void 0 : _api$analytics.actions.fireAnalyticsEvent({ eventType: 'track', action: 'nodesNotEqual', actionSubject: 'showDiff', attributes: { docSizeEqual: steppedDoc.nodeSize === tr.doc.nodeSize, colorScheme, recoveredViaContentEquality } }); } if (fg('platform_editor_show_diff_equality_fallback')) { if (!recoveredViaContentEquality) { return DecorationSet.empty; } } else { return DecorationSet.empty; } } const changeset = ChangeSet.create(originalDoc).addSteps(steppedDoc, stepMaps, tr.doc); const changes = getChanges({ changeset, originalDoc, steppedDoc, diffType, tr }); const decorations = []; changes.forEach(change => { const isActive = activeIndexPos && change.fromB === activeIndexPos.from && change.toB === activeIndexPos.to; // Our default operations are insertions, so it should match the opposite of isInverted. const isInserted = !isInverted; if (change.inserted.length > 0) { decorations.push(createInlineChangedDecoration({ change, colorScheme, isActive, ...(expValEquals('platform_editor_diff_plugin_extended', 'isEnabled', true) && { isInserted }) })); decorations.push(...calculateNodesForBlockDecoration({ doc: tr.doc, from: change.fromB, to: change.toB, colorScheme, ...(expValEquals('platform_editor_diff_plugin_extended', 'isEnabled', true) && { isInserted }), activeIndexPos, intl })); } if (change.deleted.length > 0) { const decoration = createNodeChangedDecorationWidget({ change, doc: originalDoc, nodeViewSerializer, colorScheme, newDoc: tr.doc, intl, activeIndexPos, ...(expValEquals('platform_editor_diff_plugin_extended', 'isEnabled', true) && { isInserted: !isInserted }) }); if (decoration) { decorations.push(...decoration); } } }); getMarkChangeRanges(steps).forEach(change => { const isActive = activeIndexPos && change.fromB === activeIndexPos.from && change.toB === activeIndexPos.to; decorations.push(createInlineChangedDecoration({ change, colorScheme, isActive, isInserted: true })); }); getAttrChangeRanges(tr.doc, attrSteps).forEach(change => { decorations.push(...calculateNodesForBlockDecoration({ doc: tr.doc, from: change.fromB, to: change.toB, colorScheme, isInserted: true, activeIndexPos, intl })); }); return DecorationSet.empty.add(tr.doc, decorations); }; export const calculateDiffDecorations = memoizeOne(calculateDiffDecorationsInner, // Cache results unless relevant inputs change ([{ pluginState, state, colorScheme, intl, activeIndexPos, isInverted, diffType }], [{ pluginState: lastPluginState, state: lastState, colorScheme: lastColorScheme, intl: lastIntl, activeIndexPos: lastActiveIndexPos, isInverted: lastIsInverted, diffType: lastDiffType }]) => { var _ref2; const originalDocIsSame = lastPluginState.originalDoc && pluginState.originalDoc && pluginState.originalDoc.eq(lastPluginState.originalDoc); if (expValEquals('platform_editor_diff_plugin_extended', 'isEnabled', true)) { var _ref; return (_ref = colorScheme === lastColorScheme && intl.locale === lastIntl.locale && isInverted === lastIsInverted && diffType === lastDiffType && isEqual(activeIndexPos, lastActiveIndexPos) && originalDocIsSame && isEqual(pluginState.steps, lastPluginState.steps) && state.doc.eq(lastState.doc)) !== null && _ref !== void 0 ? _ref : false; } return (_ref2 = originalDocIsSame && isEqual(pluginState.steps, lastPluginState.steps) && state.doc.eq(lastState.doc) && colorScheme === lastColorScheme && intl.locale === lastIntl.locale && isEqual(activeIndexPos, lastActiveIndexPos)) !== null && _ref2 !== void 0 ? _ref2 : false; });