@atlaskit/editor-plugin-show-diff
Version:
ShowDiff plugin for @atlaskit/editor-core
234 lines (222 loc) • 11.7 kB
JavaScript
;
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.getScrollableDecorations = void 0;
exports.isInlineDiffDecorationRenderableInDoc = isInlineDiffDecorationRenderableInDoc;
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray"));
var _view = require("@atlaskit/editor-prosemirror/view");
var _fg = require("@atlaskit/platform-feature-flags/fg");
var _decorationKeys = require("./decorations/decorationKeys");
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) { (0, _defineProperty2.default)(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; }
/**
* True if `fragment` contains at least one inline node (text, hardBreak, emoji, mention, etc.).
* Block-only subtrees (e.g. empty paragraphs, block cards with no inline children) return false.
*/
function fragmentContainsInlineContent(fragment) {
for (var i = 0; i < fragment.childCount; i++) {
var node = fragment.child(i);
if (node.isInline) {
return true;
}
if (node.content.size > 0 && fragmentContainsInlineContent(node.content)) {
return true;
}
}
return false;
}
/**
* Returns true when an inline decoration's [from, to) range can actually show in the document:
* positions are valid, and the slice contains at least one inline node ProseMirror would paint
* (not only empty block wrappers or block-only structure).
*/
function isInlineDiffDecorationRenderableInDoc(doc, from, to) {
try {
var slice = doc.slice(from, to);
return fragmentContainsInlineContent(slice.content);
} catch (_unused) {
return false;
}
}
function isRangeFullyInside(range1Start, range1End, range2Start, range2End) {
return range2Start <= range1Start && range1End <= range2End;
}
/**
* Where a decoration paints relative to others at the same position: negative before the node at
* that position, positive after it. Only deleted-content widgets carry one — everything else is
* ranged, and so paints where its range starts.
*/
function decorationSide(decoration) {
return (0, _decorationKeys.isDiffDecoration)(decoration) && decoration.spec.side || 0;
}
/**
* Whose change a decoration is part of, or `''` on an unattributed diff — where every decoration
* reads as one contributor and the grouping below is unchanged.
*
* Contributors are kept apart so one stop never covers two of them, since only the tag on the
* change stepped to reveals (EDITOR-8932).
*/
function contributorOf(decoration) {
return (0, _decorationKeys.isDiffDecoration)(decoration) && decoration.spec.attributionKey || '';
}
/**
* Collapses decorations that represent one continuous customer-facing edit.
*
* Decorations are kept separate in the DecorationSet so each one can retain its own visual
* styling. This list is only used for change counting and navigation, where an insertion and a
* deletion at the same location should be treated as one replacement. Zero-width decorations
* (normally deleted-content widgets) can join a group, but cannot extend its range and therefore
* cannot bridge two otherwise separate edits.
*
* One group per contributor within a run of touching ranges — see `contributorOf`.
*/
function groupTouchingDecorations(decorations, isInlineDecoration) {
if (decorations.length < 2) {
return decorations;
}
var groups = [];
// The groups a following decoration can still join — one per contributor, dropped at every gap.
var openGroups = [];
var clusterTo = 0;
var sortedDecorations = (0, _toConsumableArray2.default)(decorations).sort(function (a, b) {
return a.from === b.from ? a.to - b.to : a.from - b.from;
});
sortedDecorations.forEach(function (decoration) {
// A gap ends the run: nothing before it can be joined.
if (decoration.from > clusterTo) {
openGroups = [];
}
var contributor = contributorOf(decoration);
var openGroup = openGroups.find(function (group) {
return group.contributor === contributor;
});
if (openGroup === undefined) {
var group = {
contributor: contributor,
decorations: [decoration],
from: decoration.from,
to: decoration.to
};
groups.push(group);
openGroups = [].concat((0, _toConsumableArray2.default)(openGroups), [group]);
} else {
openGroup.decorations.push(decoration);
openGroup.to = Math.max(openGroup.to, decoration.to);
}
// A zero-width decoration must not extend the run and bridge a gap.
clusterTo = Math.max(clusterTo, decoration.to);
});
return groups.map(function (_ref) {
var _ref2, _group$find;
var group = _ref.decorations,
from = _ref.from,
to = _ref.to;
var representative = (_ref2 = (_group$find = group.find(isInlineDecoration)) !== null && _group$find !== void 0 ? _group$find : group.find(function (decoration) {
return decoration.from !== decoration.to;
})) !== null && _ref2 !== void 0 ? _ref2 : group[0];
if (!representative) {
return representative;
}
// Deleted content is a widget at the start of the added content that replaced it, on a more
// negative side so it paints above. That makes it the visual start of the edit, reachable
// only through its own DOM — resolving the group's start position lands on the added content
// painted after it. The group keeps its range, which navigation and the active-range
// calculation both need, and reports the widget as what to scroll to.
var scrollTarget = (0, _fg.fg)('platform_editor_ai_show_diff_patch_1') ? group.find(function (decoration) {
return decoration.from === from && decorationSide(decoration) < decorationSide(representative);
}) : undefined;
if (!scrollTarget && representative.from === from && representative.to === to) {
return representative;
}
// This decoration is only used for navigation and active-range calculation. The actual
// visual decorations remain in the DecorationSet with their original ranges and styles —
// including their spec, hence a copy to add `scrollTarget` for `scrollToDiff` to read.
var spec = _objectSpread(_objectSpread({}, representative.spec), {}, {
scrollTarget: scrollTarget
});
return isInlineDecoration(representative) ? _view.Decoration.inline(from, to, {}, spec) : _view.Decoration.node(from, to, {}, spec);
});
}
/**
* Gets scrollable decorations from a DecorationSet, filtering out overlapping decorations
* and applying various rules for diff visualization.
*
* Rules:
* 1. Only includes diff-inline, diff-widget-* and diff-block decorations
* 2. Excludes listItem diff-block decorations (never scrollable)
* 3. Deduplicates diff-block decorations with same from, to and nodeName
* 4. When `doc` is passed: excludes diff-inline decorations whose range has no inline content
* (invalid positions, or block-only slices with no text/atoms — e.g. empty blocks)
* 5. When `confluence_ncs_step_diffing_version_history` is enabled, groups overlapping or
* directly touching ranges across decoration types into one result per contributor, using the
* union of all grouped ranges
* (zero-width widgets can join a group without extending it). Under
* `platform_editor_ai_show_diff_patch_1`, a group that starts with content painting above
* the content that replaced it — reports that widget as its `scrollTarget` spec,
* so scrolling reaches the visual start of the edit
* 6. Results are sorted by from position, then by to position
*
* @param set - The DecorationSet to extract scrollable decorations from
* @param doc - Current document; when set, diff-inline ranges are validated against this doc
* @returns Array of scrollable decorations, sorted and deduplicated
*/
var getScrollableDecorations = exports.getScrollableDecorations = function getScrollableDecorations(set, doc) {
if (!set) {
return [];
}
var isBlockDecoration = function isBlockDecoration(decoration) {
return (0, _decorationKeys.isDiffDecoration)(decoration) && decoration.spec.decorationType === 'block';
};
var isInlineDecoration = function isInlineDecoration(decoration) {
return (0, _decorationKeys.isDiffDecoration)(decoration) && decoration.spec.decorationType === 'inline';
};
var isWidgetDecoration = function isWidgetDecoration(decoration) {
return (0, _decorationKeys.isDiffDecoration)(decoration) && decoration.spec.decorationType === 'widget';
};
var seenBlockKeys = new Set();
var allDecorations = set.find(undefined, undefined, _decorationKeys.isDiffDecorationSpec);
// First pass: filter out listItem blocks and deduplicates blocks
var filtered = allDecorations.filter(function (dec) {
if (!isBlockDecoration(dec) && !isInlineDecoration(dec) && !isWidgetDecoration(dec)) {
return false;
}
if (isBlockDecoration(dec)) {
var _dec$spec, _dec$spec$nodeName;
// Skip listItem blocks as they are not scrollable
if (((_dec$spec = dec.spec) === null || _dec$spec === void 0 ? void 0 : _dec$spec.nodeName) === 'listItem') return false;
var key = "".concat(dec.from, "-").concat(dec.to, "-").concat((_dec$spec$nodeName = dec.spec.nodeName) !== null && _dec$spec$nodeName !== void 0 ? _dec$spec$nodeName : '');
// Skip blocks that have already been seen
if (seenBlockKeys.has(key)) return false;
seenBlockKeys.add(key);
}
return true;
});
// Separate decorations by type for easier processing
var blocks = filtered.filter(isBlockDecoration);
var rawInlines = filtered.filter(isInlineDecoration);
var inlines = doc !== undefined ? rawInlines.filter(function (d) {
return isInlineDiffDecorationRenderableInDoc(doc, d.from, d.to);
}) : rawInlines;
var widgets = filtered.filter(isWidgetDecoration);
var result;
if ((0, _fg.fg)('confluence_ncs_step_diffing_version_history')) {
// Group overlapping or directly touching ranges into one customer-facing edit.
result = groupTouchingDecorations([].concat((0, _toConsumableArray2.default)(blocks), (0, _toConsumableArray2.default)(inlines), (0, _toConsumableArray2.default)(widgets)), isInlineDecoration);
} else {
// Legacy behavior: exclude blocks that contain or are contained by an inline decoration.
var nonOverlappingBlocks = blocks.filter(function (block) {
return !inlines.some(function (inline) {
return isRangeFullyInside(block.from, block.to, inline.from, inline.to) || isRangeFullyInside(inline.from, inline.to, block.from, block.to);
});
});
result = [].concat((0, _toConsumableArray2.default)(nonOverlappingBlocks), (0, _toConsumableArray2.default)(inlines), (0, _toConsumableArray2.default)(widgets));
}
// Sort by from position, then by to position
result.sort(function (a, b) {
return a.from === b.from ? a.to - b.to : a.from - b.from;
});
return result;
};