UNPKG

@atlaskit/editor-plugin-show-diff

Version:

ShowDiff plugin for @atlaskit/editor-core

208 lines (198 loc) • 10.1 kB
import _defineProperty from "@babel/runtime/helpers/defineProperty"; function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } import isEqual from 'lodash/isEqual'; import omit from 'lodash/omit'; import { SetAttrsStep } from '@atlaskit/adf-schema/steps/set-attrs'; import { getBaseNodeTypeName } from '@atlaskit/editor-common/utils/node-type-utils'; import { AttrStep } from '@atlaskit/editor-prosemirror/transform'; import { fg } from '@atlaskit/platform-feature-flags/fg'; import { getRequiredIncludedDiffableAttrs, isDiffableAttr } from './diffableAttrs'; import { getComparableExcerptIncludeAttrs, getReferencedContentId, isExcerptInclude } from './excerptIncludeAttrs'; var filterUndefined = function filterUndefined(x) { return !!x; }; // Attr lists sourced from the shared `diffableAttrs` map (shared with the encoder). var mediaAttrs = getRequiredIncludedDiffableAttrs('media'); var dateAttrs = getRequiredIncludedDiffableAttrs('date'); var taskItemAttrs = getRequiredIncludedDiffableAttrs('taskItem'); var panelAttrs = getRequiredIncludedDiffableAttrs('panel'); var emojiAttrs = getRequiredIncludedDiffableAttrs('emoji'); var mentionAttrs = getRequiredIncludedDiffableAttrs('mention'); var statusAttrs = getRequiredIncludedDiffableAttrs('status'); // Map of node type name → the attrs that represent a meaningful content change for that node var inlineNodeAttrMap = { date: dateAttrs, emoji: emojiAttrs, mention: mentionAttrs, status: statusAttrs }; export var isInlineAttrChangeNodeName = function isInlineAttrChangeNodeName(nodeName) { return 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. var extensionNodeNames = ['extension', 'inlineExtension', 'bodiedExtension']; var transientExtensionAttrPaths = ['localId', 'parameters.macroParams._parentId']; /** * Removes extension metadata that can change between document versions without changing the * extension's user-visible configuration. */ var getComparableExtensionAttrs = function getComparableExtensionAttrs(node) { return omit(node.attrs, transientExtensionAttrPaths); }; var haveSameRelevantExtensionAttrs = function haveSameRelevantExtensionAttrs(beforeNode, afterNode) { if (fg('platform_editor_normalize_excerpt_diff') && isExcerptInclude(beforeNode.attrs) && isExcerptInclude(afterNode.attrs)) { var beforeReference = getReferencedContentId(beforeNode.attrs); var afterReference = getReferencedContentId(afterNode.attrs); return isEqual(getComparableExcerptIncludeAttrs(beforeNode.attrs), getComparableExcerptIncludeAttrs(afterNode.attrs)) && (beforeReference === undefined || afterReference === undefined || beforeReference === afterReference); } return isEqual(getComparableExtensionAttrs(beforeNode), getComparableExtensionAttrs(afterNode)); }; var getStepAttrs = function getStepAttrs(step) { if (step instanceof AttrStep) { return [step.attr]; } if (step.attrs) { return Object.keys(step.attrs); } return []; }; export var getAttrChangeRanges = function getAttrChangeRanges(doc, attrStepContexts, originalDoc) { return attrStepContexts.map(function (attrStepContext) { var afterNode = attrStepContext.afterNode, attributionKey = attrStepContext.attributionKey, beforeNode = attrStepContext.beforeNode, finalPos = attrStepContext.finalPos, originalPos = attrStepContext.originalPos, step = attrStepContext.step; if (!(step instanceof AttrStep) && !(step instanceof SetAttrsStep)) { return undefined; } var stepAttrs = getStepAttrs(step); // SetAttrsStep contains the complete replacement attrs, not only the attrs that changed, // so narrow it to the attrs whose values actually differ. Fall back to the full step // payload if either step-time node is unavailable. var attrsToCheck = beforeNode && afterNode ? stepAttrs.filter(function (attrName) { return !isEqual(beforeNode.attrs[attrName], afterNode.attrs[attrName]); }) : stepAttrs; var $pos = doc.resolve(finalPos); var nodeAtPos = doc.nodeAt(finalPos); var originalNodeAtPos = originalPos === undefined ? null : originalDoc.nodeAt(originalPos); var hasChangedAttr = function hasChangedAttr(watchedAttrs) { return attrsToCheck.some(function (attr) { return watchedAttrs.includes(attr) && (!fg('confluence_ncs_step_diffing_version_history') || !originalNodeAtPos || originalNodeAtPos.type !== (nodeAtPos === null || nodeAtPos === void 0 ? void 0 : nodeAtPos.type) || !isEqual(originalNodeAtPos.attrs[attr], nodeAtPos === null || nodeAtPos === void 0 ? void 0 : nodeAtPos.attrs[attr])); }); }; // The changeset path (createDecorationsForChange) handles the deletion widget via // prosemirror-changeset; we only need to add the inline insertion highlight here. if (nodeAtPos) { var nodeName = nodeAtPos.type.name; if (isInlineAttrChangeNodeName(nodeName)) { var watchedAttrs = inlineNodeAttrMap[nodeName]; if (hasChangedAttr(watchedAttrs)) { return _objectSpread(_objectSpread({}, attributionKey ? { attributionKey: attributionKey } : {}), {}, { fromB: finalPos, toB: finalPos + nodeAtPos.nodeSize, isInline: true, inlineNodeName: nodeName }, originalPos !== undefined && originalNodeAtPos && { fromA: originalPos, toA: originalPos + originalNodeAtPos.nodeSize }); } } } // taskItem node: state attribute change — highlight the taskItem node if (attrsToCheck.some(function (v) { return taskItemAttrs.includes(v); }) && (nodeAtPos === null || nodeAtPos === void 0 ? void 0 : nodeAtPos.type.name) === 'taskItem') { return _objectSpread(_objectSpread({}, attributionKey ? { attributionKey: attributionKey } : {}), {}, { fromB: finalPos, toB: finalPos + 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 (hasChangedAttr(panelAttrs) && nodeAtPos && getBaseNodeTypeName(nodeAtPos.type) === 'panel') { return _objectSpread(_objectSpread({}, attributionKey ? { attributionKey: attributionKey } : {}), {}, { fromB: finalPos, toB: finalPos + nodeAtPos.nodeSize }, originalPos !== undefined && originalNodeAtPos && { fromA: originalPos, toA: originalPos + originalNodeAtPos.nodeSize }); } // Extension nodes: highlight changes to user-visible configuration. Transient extension // metadata is ignored by the comparison below. if (nodeAtPos && extensionNodeNames.includes(nodeAtPos.type.name) && attrsToCheck.some(function (v) { return isDiffableAttr(nodeAtPos.type.name, v); })) { if (beforeNode && afterNode && beforeNode.type === afterNode.type && haveSameRelevantExtensionAttrs(beforeNode, afterNode)) { return undefined; } var isInline = nodeAtPos.type.name === 'inlineExtension'; return _objectSpread(_objectSpread({}, attributionKey ? { attributionKey: attributionKey } : {}), {}, { fromB: finalPos, toB: finalPos + nodeAtPos.nodeSize, isInline: isInline }); } // media node: id/collection/url attribute change — highlight the mediaSingle parent if (attrsToCheck.some(function (v) { return mediaAttrs.includes(v); }) && $pos.parent.type === doc.type.schema.nodes.mediaSingle) { var startPos = $pos.pos + $pos.parentOffset; return _objectSpread(_objectSpread({}, attributionKey ? { attributionKey: attributionKey } : {}), {}, { 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. .filter(function (range, i, arr) { return fg('confluence_ncs_step_diffing_version_history') ? !arr.some(function (r, nextIndex) { return nextIndex > i && r.fromB === range.fromB; }) : arr.findIndex(function (r) { return 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 var stepIsValidAttrChange = function stepIsValidAttrChange(step, beforeDoc, afterDoc) { try { if (step instanceof AttrStep || step instanceof SetAttrsStep) { var attrStepAfter = afterDoc.nodeAt(step.pos); var attrStepBefore = beforeDoc.nodeAt(step.pos); // The change affected the document if (attrStepAfter && attrStepBefore && !attrStepAfter.eq(attrStepBefore)) { return true; } } return false; } catch (_unused) { return false; } };