UNPKG

@atlaskit/editor-plugin-show-diff

Version:

ShowDiff plugin for @atlaskit/editor-core

155 lines (145 loc) 7 kB
import { SetAttrsStep } from '@atlaskit/adf-schema/steps'; import { isExperimentEnabled } from '@atlaskit/editor-common/deprecated-platform-feature-experiments'; import { getBaseNodeTypeName } from '@atlaskit/editor-common/utils/node-type-utils'; import { AttrStep } from '@atlaskit/editor-prosemirror/transform'; import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals'; import { getRequiredIncludedDiffableAttrs, isDiffableAttr } from './diffableAttrs'; const filterUndefined = x => !!x; // Attr lists sourced from the shared `diffableAttrs` map (shared with the encoder). const mediaAttrs = getRequiredIncludedDiffableAttrs('media'); const dateAttrs = getRequiredIncludedDiffableAttrs('date'); const taskItemAttrs = getRequiredIncludedDiffableAttrs('taskItem'); const panelAttrs = getRequiredIncludedDiffableAttrs('panel'); const emojiAttrs = getRequiredIncludedDiffableAttrs('emoji'); const mentionAttrs = getRequiredIncludedDiffableAttrs('mention'); const statusAttrs = getRequiredIncludedDiffableAttrs('status'); // Map of node type name → the attrs that represent a meaningful content change for that node const inlineNodeAttrMap = { date: dateAttrs, emoji: emojiAttrs, mention: mentionAttrs, status: statusAttrs }; const isInlineAttrChangeNodeName = nodeName => nodeName in inlineNodeAttrMap; // Extension node type names. Their "any attr except localId" exclude rule lives // in the shared `diffableAttrs` map and is applied via `isDiffableAttr` below. const extensionNodeNames = ['extension', 'inlineExtension', 'bodiedExtension']; const getStepAttrs = step => { if (step instanceof AttrStep) { return [step.attr]; } if (step instanceof SetAttrsStep && step.attrs) { return Object.keys(step.attrs); } return []; }; export const getAttrChangeRanges = (doc, steps, originalDoc) => { return steps.map(step => { if (!(step instanceof AttrStep) && !(step instanceof SetAttrsStep)) { return undefined; } const stepAttrs = getStepAttrs(step); const $pos = doc.resolve(step.pos); const nodeAtPos = doc.nodeAt(step.pos); // date node: timestamp attribute change — highlight the date node itself (inline) if (stepAttrs.some(v => dateAttrs.includes(v)) && (nodeAtPos === null || nodeAtPos === void 0 ? void 0 : nodeAtPos.type.name) === 'date' && !isExperimentEnabled('platform_editor_improve_inline_diffs', () => expValEquals('platform_editor_improve_inline_diffs', 'isEnabled', true))) { return { fromB: step.pos, toB: step.pos + nodeAtPos.nodeSize, isInline: true, inlineNodeName: 'date' }; } // The following inline node attr changes are gated behind platform_editor_improve_inline_diffs. // The changeset path (createDecorationsForChange) handles the deletion widget via // prosemirror-changeset; we only need to add the inline insertion highlight here. if (isExperimentEnabled('platform_editor_improve_inline_diffs', () => expValEquals('platform_editor_improve_inline_diffs', 'isEnabled', true)) && nodeAtPos) { const nodeName = nodeAtPos.type.name; if (isInlineAttrChangeNodeName(nodeName)) { const watchedAttrs = inlineNodeAttrMap[nodeName]; if (stepAttrs.some(v => watchedAttrs.includes(v))) { const originalNodeAtPos = originalDoc === null || originalDoc === void 0 ? void 0 : originalDoc.nodeAt(step.pos); return { fromB: step.pos, toB: step.pos + nodeAtPos.nodeSize, isInline: true, inlineNodeName: nodeName, ...(originalNodeAtPos && { fromA: step.pos, toA: step.pos + originalNodeAtPos.nodeSize }) }; } } } // taskItem node: state attribute change — highlight the taskItem node if (stepAttrs.some(v => taskItemAttrs.includes(v)) && (nodeAtPos === null || nodeAtPos === void 0 ? void 0 : nodeAtPos.type.name) === 'taskItem') { return { fromB: step.pos, toB: step.pos + nodeAtPos.nodeSize }; } // panel node (incl. variants like panel_c1): type/colour/icon attribute change // (e.g. note -> warning) — highlight the new panel and, when the original node // is resolvable, expose its range (fromA/toA) so the caller can render the old // panel as a "deleted" widget for a before/after comparison. if (stepAttrs.some(v => panelAttrs.includes(v)) && nodeAtPos && getBaseNodeTypeName(nodeAtPos.type) === 'panel') { const originalNodeAtPos = originalDoc === null || originalDoc === void 0 ? void 0 : originalDoc.nodeAt(step.pos); return { fromB: step.pos, toB: step.pos + nodeAtPos.nodeSize, ...(originalNodeAtPos && { fromA: step.pos, toA: step.pos + originalNodeAtPos.nodeSize }) }; } // extension nodes: highlight on any diffable attr change (exclude rule in shared map) if (nodeAtPos && extensionNodeNames.includes(nodeAtPos.type.name) && stepAttrs.some(v => isDiffableAttr(nodeAtPos.type.name, v))) { const isInline = nodeAtPos.type.name === 'inlineExtension'; return { fromB: step.pos, toB: step.pos + nodeAtPos.nodeSize, isInline }; } // media node: id/collection/url attribute change — highlight the mediaSingle parent if (stepAttrs.some(v => mediaAttrs.includes(v)) && $pos.parent.type === doc.type.schema.nodes.mediaSingle) { const startPos = $pos.pos + $pos.parentOffset; return { fromB: startPos, toB: startPos + $pos.parent.nodeSize - 1 }; } return undefined; }).filter(filterUndefined) // Deduplicate by node position: multiple AttrSteps on the same node // (e.g. setNodeAttribute(pos, 'text', ...) + setNodeAttribute(pos, 'color', ...)) // should produce only one decoration, not one per step. // Gated behind the same experiment as the inline node attr changes that introduced // the possibility of multiple ranges for the same node position. .filter((range, i, arr) => !isExperimentEnabled('platform_editor_improve_inline_diffs', () => expValEquals('platform_editor_improve_inline_diffs', 'isEnabled', true)) || arr.findIndex(r => r.fromB === range.fromB) === i); }; /** * Check if the step was a valid attr change and affected the doc * * @param step Attr step to test * @param beforeDoc Doc before the step * @param afterDoc Doc after the step * @returns Boolean if the change should show a decoration */ export const stepIsValidAttrChange = (step, beforeDoc, afterDoc) => { try { if (step instanceof AttrStep || step instanceof SetAttrsStep) { const attrStepAfter = afterDoc.nodeAt(step.pos); const attrStepBefore = beforeDoc.nodeAt(step.pos); // The change affected the document if (attrStepAfter && attrStepBefore && !attrStepAfter.eq(attrStepBefore)) { return true; } } return false; } catch { return false; } };