@atlaskit/editor-plugin-show-diff
Version:
ShowDiff plugin for @atlaskit/editor-core
653 lines (635 loc) • 33.5 kB
JavaScript
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 { Decoration } from '@atlaskit/editor-prosemirror/view';
import { fg } from '@atlaskit/platform-feature-flags/fg';
import { countEmptyTextBlockOnlySlice } from '../utils/emptyTextBlocks';
import { isEmptyParagraphSlice } from '../utils/isEmptyParagraphSlice';
import { clampAnchorPosIntoCell, createAnchorNameSpan, createLeftAnchorWidget } from './createAnchorDecorationWidgets';
import { createChangedRowDecorationWidgets } from './createChangedRowDecorationWidgets';
import { createContributorTagHost, resolveHoistedCodeBlockAnchor, unmountContributorTag } from './createContributorTagWidget';
import { AnchorTypeKey, buildContributorTagDecorationSpec, buildDiffDecorationSpec, buildAnchorDecorationKey, scrollMarginTopValue } from './decorationKeys';
import { absorbFirstChildMarginReset } from './utils/absorbFirstChildMarginReset';
import { createDeletedLineBreakDecoration } from './utils/createDeletedLineBreakWidget';
import { createMarginAbsorber } from './utils/createMarginAbsorber';
import { createNodeShapedMarginSpacer } from './utils/createNodeShapedMarginSpacer';
import { findSafeInsertPos } from './utils/findSafeInsertPos';
import { safeResolve } from './utils/safeResolve';
import { getTableDiffMode } from './utils/tableDiffMode';
import { wrapBlockNodeView, createRemovedLozenge, injectInnerWrapper, createContentWrapper } from './utils/wrapBlockNodeView';
/**
* Where a slice's `index`th child sits in `doc`: the children belong to the ancestor `openStart`
* levels above the change. `null` if out of range — `posAtIndex` throws past the last child.
*/
var getSourcePos = function getSourcePos(doc, $changeFrom, openStart, index) {
var depth = $changeFrom ? $changeFrom.depth - openStart : -1;
if (!$changeFrom || depth < 0) {
return null;
}
var childIndex = $changeFrom.index(depth) + index;
if (childIndex >= $changeFrom.node(depth).childCount) {
return null;
}
return safeResolve(doc, $changeFrom.posAtIndex(childIndex, depth));
};
var isHeadingLevel = function isHeadingLevel(level) {
return typeof level === 'number' && level >= 1 && level <= 6;
};
/**
* Render one content widget per table cell, positioned inside the matching cell in
* `newDoc`, so a whole-cell replacement shows its added content in-place instead of
* being dropped (EDITOR-8442).
*/
var createTableCellContentWidgets = function createTableCellContentWidgets(_ref) {
var slice = _ref.slice,
newDoc = _ref.newDoc,
change = _ref.change,
nodeViewSerializer = _ref.nodeViewSerializer,
colorScheme = _ref.colorScheme,
isInserted = _ref.isInserted,
leftAnchorId = _ref.leftAnchorId;
// Collect the inner content position of each cell in the new document within
// the change range. Whole-cell replacement preserves cell count/order, so the
// nth new-doc cell corresponds to the nth cell in the slice.
var newCellInnerPositions = [];
newDoc.nodesBetween(change.fromB, change.toB, function (node, pos) {
if (node.type.name === 'tableHeader' || node.type.name === 'tableCell') {
// Skip malformed cells with no block child: `pos + 2` would be out of bounds.
if (node.childCount === 0) {
return false;
}
// +1 into the cell, +1 into its first child (the paragraph) = inner content.
newCellInnerPositions.push(pos + 2);
return false;
}
return true;
});
var decorations = [];
slice.content.forEach(function (cellNode, _offset, index) {
var targetPos = newCellInnerPositions[index];
if (targetPos === undefined || cellNode.content.size === 0) {
return;
}
var dom = document.createElement('span');
// A cell contains block content (paragraphs); serialize the cell's content
// so every block inside the cell is rendered.
cellNode.content.forEach(function (blockNode) {
var nodeView = nodeViewSerializer.tryCreateNodeView(blockNode, 0, undefined, isInserted, colorScheme);
var wrapper = createContentWrapper(colorScheme, false, isInserted);
if (nodeView) {
wrapper.append(nodeView);
} else {
var serialized = blockNode.type.inlineContent ? nodeViewSerializer.serializeFragment(blockNode.content) : nodeViewSerializer.serializeNode(blockNode);
if (serialized) {
wrapper.append(serialized);
}
}
dom.append(wrapper);
});
if (dom.childNodes.length === 0) {
return;
}
// This helper only runs for added content, so use the "changed" testid — not
// "deleted", which page models match as removed content.
dom.setAttribute('data-testid', 'show-diff-changed-decoration');
if (fg('platform_editor_ai_show_diff_patch_1')) {
dom.style.setProperty('scroll-margin-top', scrollMarginTopValue);
}
// Skip findSafeInsertPos: it walks forward to a schema-valid cell-insert point
// and pushes the widget into the next cell. `targetPos` is the correct spot.
decorations.push(Decoration.widget(targetPos, dom, _objectSpread({}, buildDiffDecorationSpec({
colorScheme: colorScheme,
decorationType: 'widget',
diffId: crypto.randomUUID(),
leftAnchorId: leftAnchorId,
isInserted: isInserted,
side: -1
}))));
});
return decorations;
};
/**
* This function is used to create a decoration widget to show content
* that is not in the current document.
*/
export var createNodeChangedDecorationWidget = function createNodeChangedDecorationWidget(_ref2) {
var _$safeInsertPos$nodeB, _slice$content$firstC, _newDoc$firstChild, _slice$content$firstC2;
var attributionKey = _ref2.attributionKey,
change = _ref2.change,
doc = _ref2.doc,
nodeViewSerializer = _ref2.nodeViewSerializer,
colorScheme = _ref2.colorScheme,
newDoc = _ref2.newDoc,
intl = _ref2.intl,
activeIndexPos = _ref2.activeIndexPos,
_ref2$isInserted = _ref2.isInserted,
isInserted = _ref2$isInserted === void 0 ? false : _ref2$isInserted,
leftAnchorId = _ref2.leftAnchorId,
_ref2$showContributor = _ref2.showContributorTags,
showContributorTags = _ref2$showContributor === void 0 ? false : _ref2$showContributor,
_ref2$showIndicators = _ref2.showIndicators,
showIndicators = _ref2$showIndicators === void 0 ? false : _ref2$showIndicators,
_ref2$placeBelow = _ref2.placeBelow,
placeBelow = _ref2$placeBelow === void 0 ? false : _ref2$placeBelow,
_ref2$hideAddedDiffsU = _ref2.hideAddedDiffsUnderline,
hideAddedDiffsUnderline = _ref2$hideAddedDiffsU === void 0 ? false : _ref2$hideAddedDiffsU,
reveal = _ref2.reveal,
tagMountContext = _ref2.tagMountContext,
deletedColumns = _ref2.deletedColumns;
var slice = doc.slice(change.fromA, change.toA);
var shouldSkipDeletedEmptyParagraphDecoration = !isInserted && isEmptyParagraphSlice(slice);
// How many blank lines the deletion took, when it took nothing else. Blank means an empty
// paragraph or heading — both are a line the author put there deliberately, and neither leaves
// the widget any content to serialize.
//
// Kept separate from `shouldSkipDeletedEmptyParagraphDecoration` and gated, so the ungated early
// return below still covers only the single-empty-paragraph case it always did. Everything else
// degrades to what it rendered before the gate: an empty block, serialized invisibly.
var deletedBlankLineCount = !isInserted && fg('platform_editor_ai_show_diff_patch_2') ? countEmptyTextBlockOnlySlice(slice) : 0;
// Widget decoration used for deletions as the content is not in the document
// and we want to display the deleted content with a style.
// For `placeBelow`, anchor at the END of the new content (change.toB) so the deleted
// node renders beneath its replacement; otherwise anchor at the start (change.fromB).
var anchorPos = placeBelow ? change.toB : change.fromB;
var safeInsertPos = findSafeInsertPos(newDoc, anchorPos, slice);
var isActive = activeIndexPos && (fg('confluence_ncs_step_diffing_version_history') ? safeInsertPos >= activeIndexPos.from && safeInsertPos <= activeIndexPos.to && (!('attributionKey' in activeIndexPos) || activeIndexPos.attributionKey === attributionKey) : safeInsertPos === activeIndexPos.from && safeInsertPos === activeIndexPos.to);
if (deletedBlankLineCount > 0) {
// The blocks have no content to serialize, so the deletion is shown as one struck-through
// return glyph per line the author removed. A single widget, not one per line: the lines are
// gone from `newDoc`, so they all resolve to the same anchor and separate widgets would stack.
//
// No attribution: the glyph is not a tag host, matching `canTagWidget` below.
return [createDeletedLineBreakDecoration({
colorScheme: colorScheme,
count: deletedBlankLineCount,
isActive: isActive,
pos: safeInsertPos,
reveal: reveal,
side: placeBelow ? 1 : -1
})];
}
if (shouldSkipDeletedEmptyParagraphDecoration) {
return [];
}
if (slice.content.content.length === 0) {
return [];
}
var tableDiffMode = getTableDiffMode({
slice: slice,
isInserted: isInserted
});
if (tableDiffMode.kind === 'cells') {
return createTableCellContentWidgets({
slice: slice,
newDoc: newDoc,
change: change,
nodeViewSerializer: nodeViewSerializer,
colorScheme: colorScheme,
isInserted: isInserted,
leftAnchorId: leftAnchorId
});
}
if (tableDiffMode.kind === 'none') {
return [];
}
if (tableDiffMode.kind === 'rows') {
return createChangedRowDecorationWidgets({
attributionKey: attributionKey,
changes: [change],
originalDoc: doc,
newDoc: newDoc,
nodeViewSerializer: nodeViewSerializer,
colorScheme: colorScheme,
isActive: isActive,
isInserted: isInserted,
leftAnchorId: leftAnchorId,
intl: intl,
// Needed for the row's own indicator anchor; this path returns before the
// `anchor-name` assignment further down.
showIndicators: showIndicators,
showContributorTags: showContributorTags,
tagMountContext: tagMountContext
});
}
// `wholeTable` and `generic` both continue into the generic block widget below. Splitting the
// whole-table case into its own renderer is the next step of this refactor.
var serializer = nodeViewSerializer;
// For non-table content, use the existing span wrapper approach
var dom = document.createElement('span');
var $safeInsertPos = newDoc.resolve(safeInsertPos);
// Whether the widget renders above the very start of its parent's content — the document, a
// layout column, a table cell, a panel.
//
// `parentOffset === 0` is the whole test on the position side: prosemirror-view paints every
// widget at a position before the node starting there, so a leading widget at the parent's start
// always renders above its content.
var isVisuallyFirstInParent = !placeBelow && $safeInsertPos.parentOffset === 0;
var isTopLevelInsert = $safeInsertPos.depth === 0;
var hasPreviousBlock = ((_$safeInsertPos$nodeB = $safeInsertPos.nodeBefore) === null || _$safeInsertPos$nodeB === void 0 ? void 0 : _$safeInsertPos$nodeB.isBlock) === true;
var isFirstDocHeadingReplacement = !placeBelow && change.fromB === 0 && ((_slice$content$firstC = slice.content.firstChild) === null || _slice$content$firstC === void 0 ? void 0 : _slice$content$firstC.type.name) === 'heading' && ((_newDoc$firstChild = newDoc.firstChild) === null || _newDoc$firstChild === void 0 ? void 0 : _newDoc$firstChild.type.name) === 'heading';
if (isTopLevelInsert && hasPreviousBlock && !isFirstDocHeadingReplacement) {
// Editor CSS removes the top margin from a block when it is its parent's first child.
// Keep the serialized block as a subsequent child so each block type retains its own margin.
dom.append(dom.ownerDocument.createElement('div'));
}
if (isFirstDocHeadingReplacement) {
// The replacement widget is the visual first block, while the real first document node has
// its top margin reset. Add a compact gap after the widget so the two diff blocks do not overlap.
dom.style.display = 'block';
dom.style.marginBottom = "var(--ds-space-200, 16px)";
}
// When mediaSingle nodes are rendered inside a widget decoration (e.g. as part of
// a replaced panel), the centering CSS (margin-left: 50%; transform: translateX(-50%))
// incorrectly applies because isNestedNode resolves to false (getPos returns 0).
// Observe DOM mutations and override the transform on .rich-media-item elements
// after React mounts to prevent the image from shifting outside its parent container.
var constrainMediaObserver = new MutationObserver(function () {
var richMediaItems = dom.querySelectorAll('.rich-media-item');
richMediaItems.forEach(function (el) {
if (el instanceof HTMLElement) {
el.style.transform = 'none';
el.style.marginLeft = '0';
el.style.maxWidth = '100%';
}
});
if (richMediaItems.length > 0) {
constrainMediaObserver.disconnect();
}
});
constrainMediaObserver.observe(dom, {
childList: true,
subtree: true
});
// Derived from the deleted range so it survives a recalculation — see the same reasoning in
// `createInlineChangedDecoration`.
var diffId = showContributorTags ? "widget-".concat(change.fromA, "-").concat(change.toA) : crypto.randomUUID();
// Match the rendering predicate above: an empty paragraph has no widget to tag, while whitespace
// text in a rendered widget remains eligible.
var canTagWidget = showContributorTags && !shouldSkipDeletedEmptyParagraphDecoration;
var decorations = [];
var replacementNode = newDoc.nodeAt(change.fromB);
var firstReplacedNode = slice.content.firstChild;
var lastReplacedNode = slice.content.lastChild;
var showDiffPatch1 = fg('platform_editor_ai_show_diff_patch_1');
// Whether the slice's outermost textblocks still hold all of their original content.
//
// `openStart`/`openEnd` say whether the cut landed inside a block, but not whether it took
// any text with it — a deletion stopping exactly at a block's content boundary is still
// reported as open. Resolving the change's own ends separates the two, so a block whose text
// survived intact can render as a block even though its boundary was open.
//
// Both resolves sit behind the gate that consumes them, so this adds no position arithmetic to
// the ungated path, and an unresolvable position degrades to the `openStart`/`openEnd` test on
// its own — the block is then treated as partial, which is what it rendered as before the gate.
var $changeFromA = fg('platform_editor_ai_show_diff_patch_2') ? safeResolve(doc, change.fromA) : null;
var $changeToA = fg('platform_editor_ai_show_diff_patch_2') ? safeResolve(doc, change.toA) : null;
var isFirstNodeContentComplete = slice.openStart === 0 || ($changeFromA === null || $changeFromA === void 0 ? void 0 : $changeFromA.parentOffset) === 0;
var isLastNodeContentComplete = slice.openEnd === 0 || $changeToA !== null && $changeToA.parentOffset === $changeToA.parent.content.size;
var isCompleteSameTypeReplacement = slice.content.childCount === 1 && firstReplacedNode !== null && replacementNode !== null && firstReplacedNode.type === replacementNode.type && replacementNode.nodeSize === change.toB - change.fromB;
/*
* The thinking is we separate out the fragment we got from doc.slice
* and if it's the first or last content, we go in however many the sliced Open
* or sliced End depth is and match only the entire node.
*/
slice.content.forEach(function (node, _offset, index) {
var isFirst = firstReplacedNode === node;
var isLast = lastReplacedNode === node;
var isOpenAtSliceBoundary = isFirst && slice.openStart > 0 || isLast && slice.openEnd > 0;
var shouldPreserveCompleteMultiInlineBlock = showDiffPatch1 && node.isBlock && node.type.inlineContent && node.content.childCount > 1 && !isOpenAtSliceBoundary && isCompleteSameTypeReplacement;
// A textblock is otherwise serialized as its inline content only, with no `<p>`/`<h2>` wrapper.
// That is right when the diff cuts into an existing block — the deleted text belongs on the same
// line as the text that replaced it — but when the whole block went away it drops the very
// element that carries the block's margin, so the deleted block renders flush against its
// neighbours. Only the first and last children can be partial; middle ones are always complete.
var shouldRenderAsBlockNode = fg('platform_editor_ai_show_diff_patch_2') && node.isTextblock && (!isFirst || isFirstNodeContentComplete) && (!isLast || isLastNodeContentComplete);
// Helper function to handle multiple child nodes
var handleMultipleChildNodes = function handleMultipleChildNodes(node) {
if (!shouldPreserveCompleteMultiInlineBlock && !shouldRenderAsBlockNode && node.content.childCount > 1 && node.type.inlineContent) {
node.content.forEach(function (childNode) {
var childNodeView = serializer.tryCreateNodeView(childNode, 0, undefined, isInserted, colorScheme);
if (childNodeView) {
var lineBreak = document.createElement('br');
dom.append(lineBreak);
var wrapper = createContentWrapper(colorScheme, isActive, isInserted);
wrapper.append(childNodeView);
dom.append(wrapper);
} else {
// Fallback to serializing the individual child node
var serializedChild = serializer.serializeNode(childNode);
if (serializedChild) {
var _wrapper = createContentWrapper(colorScheme, isActive, isInserted);
_wrapper.append(serializedChild);
dom.append(_wrapper);
}
}
});
return true; // Indicates we handled multiple children
}
return false; // Indicates single child, continue with normal logic
};
// Determine which node to use and how to serialize
var hasInlineContent = node.content.childCount > 0 && node.type.inlineContent === true;
var fallbackSerialization;
if (handleMultipleChildNodes(node)) {
return;
}
if (shouldPreserveCompleteMultiInlineBlock || shouldRenderAsBlockNode) {
fallbackSerialization = function fallbackSerialization() {
return serializer.serializeNode(node);
};
} else if ((isFirst || isLast && slice.content.childCount > 2) && hasInlineContent) {
fallbackSerialization = function fallbackSerialization() {
return serializer.serializeFragment(node.content);
};
} else if (isLast && slice.content.childCount === 2) {
fallbackSerialization = function fallbackSerialization() {
if (node.type.name === 'text') {
return document.createTextNode(node.text || '');
}
if (node.type.name === 'paragraph') {
var lineBreak = document.createElement('br');
dom.append(lineBreak);
return serializer.serializeFragment(node.content);
}
return serializer.serializeFragment(node.content);
};
} else {
fallbackSerialization = function fallbackSerialization() {
return serializer.serializeNode(node);
};
}
// Try to create node view, fallback to serialization
var $sourcePos = fg('platform_editor_ai_show_diff_patch_2') ? getSourcePos(doc, $changeFromA, slice.openStart, index) : null;
var nodeView = serializer.tryCreateNodeView(node, 0, $sourcePos !== null && $sourcePos !== void 0 ? $sourcePos : undefined, isInserted, colorScheme);
if (nodeView) {
if (node.isInline) {
var wrapper = createContentWrapper(colorScheme, isActive, isInserted, reveal);
wrapper.append(nodeView);
dom.append(wrapper);
} else {
// Handle all block nodes with unified function
wrapBlockNodeView({
dom: dom,
nodeView: nodeView,
targetNode: node,
colorScheme: colorScheme,
intl: intl,
isActive: isActive,
isInserted: isInserted,
hideAddedDiffsUnderline: hideAddedDiffsUnderline,
highlightInlineLeafNodes: shouldPreserveCompleteMultiInlineBlock
});
}
} else if (nodeViewSerializer.getFilteredNodeViewBlocklist(['paragraph', 'tableRow']).has(node.type.name)) {
// Skip the case where the node is a paragraph or table row that way it can still be rendered and delete the entire table
return;
} else {
var fallbackNode = fallbackSerialization();
if (fallbackNode) {
if (fallbackNode instanceof HTMLElement) {
var injectedNode = injectInnerWrapper({
node: fallbackNode,
colorScheme: colorScheme,
isActive: isActive,
isInserted: isInserted,
reveal: reveal
});
dom.append(injectedNode);
} else {
var _wrapper2 = createContentWrapper(colorScheme, isActive, isInserted, reveal);
_wrapper2.append(fallbackNode);
dom.append(_wrapper2);
}
}
}
});
// A change inside a code block hoists its tag out of the block (EDITOR-9045).
var hoistedCodeBlockAnchor = canTagWidget ? resolveHoistedCodeBlockAnchor(newDoc, safeInsertPos, diffId) : undefined;
if (hoistedCodeBlockAnchor) {
decorations.push(hoistedCodeBlockAnchor.marker);
}
var contributorTagAnchorName = hoistedCodeBlockAnchor === null || hoistedCodeBlockAnchor === void 0 ? void 0 : hoistedCodeBlockAnchor.anchorName;
if (!isInserted && deletedColumns !== null && deletedColumns !== void 0 && deletedColumns.length) {
var table = dom.querySelector('table');
var firstRow = table === null || table === void 0 ? void 0 : table.rows[0];
if (table && canTagWidget) {
contributorTagAnchorName = buildAnchorDecorationKey({
diffId: diffId,
anchorType: AnchorTypeKey.tag
});
// The widget host sits before the table's preserved block margin. Anchor the tag to the
// table itself so its bottom edge meets the table's top edge with no visual gap.
table.style.setProperty('anchor-name', "--".concat(contributorTagAnchorName));
}
deletedColumns.forEach(function (column) {
var _table$rows;
Array.from((_table$rows = table === null || table === void 0 ? void 0 : table.rows) !== null && _table$rows !== void 0 ? _table$rows : []).forEach(function (row) {
var _cell$children;
var cell = row.cells[column];
Array.from((_cell$children = cell === null || cell === void 0 ? void 0 : cell.children) !== null && _cell$children !== void 0 ? _cell$children : []).forEach(function (child) {
if (child instanceof HTMLElement) {
child.style.setProperty('text-decoration-line', 'line-through');
}
});
});
var headerCell = firstRow === null || firstRow === void 0 ? void 0 : firstRow.cells[column];
if (headerCell) {
headerCell.style.position = 'relative';
// Append, never prepend. Editor CSS resets the top margin of a cell's first child, so
// a prepended label pushes the paragraph out of that reset and the cell grows.
// The label is absolutely positioned, so DOM order does not move it.
headerCell.append(createRemovedLozenge(intl, isActive, colorScheme, true));
}
});
}
dom.setAttribute('data-testid', 'show-diff-deleted-decoration');
if (fg('platform_editor_ai_show_diff_patch_1')) {
dom.style.setProperty('scroll-margin-top', scrollMarginTopValue);
}
// A block node serialized into the widget is the widget's first child, and the editor's
// first-child reset is written against the parent element rather than the document position — so
// it zeroes the margin the block wrapper was kept for in the first place.
//
// Only away from the parent's start. There the reset is the correct outcome: the widget is the
// first thing in the document, column, cell or panel, and a leading gap above it would be wrong.
// The margin that matters at that position belongs to the node *below* the widget, which the
// shaped spacer pair supplies instead.
if (fg('platform_editor_ai_show_diff_patch_2') && !isVisuallyFirstInParent) {
absorbFirstChildMarginReset({
dom: dom,
testId: 'show-diff-widget-margin-absorber'
});
}
// Needed even with the indicator bar off, since it anchors its top/bottom to the widget.
if (showIndicators || showContributorTags) {
dom.style.setProperty('anchor-name', "--".concat(buildAnchorDecorationKey({
diffId: diffId
})));
// `dom` is inline, so its box fragments across a line wrap and `anchor()` resolves to the
// wrong edge. The tag anchors to its own point marker instead (EDITOR-9286).
if (fg('confluence_ncs_step_diffing_version_history') && !contributorTagAnchorName) {
var tagPointAnchorName = buildAnchorDecorationKey({
diffId: diffId,
anchorType: AnchorTypeKey.tag
});
contributorTagAnchorName = tagPointAnchorName;
decorations.push(Decoration.widget(safeInsertPos, function () {
return createAnchorNameSpan(tagPointAnchorName);
}, {
key: "contributor-tag-anchor-".concat(diffId),
side: -1,
marks: [],
ignoreSelection: true
}));
}
}
// Taken down in the widget's `destroy` below: `dom` is rebuilt on every recalculation, so the tag
// mounted into it must not outlive the copy ProseMirror is drawing.
var tagMount;
if (canTagWidget) {
// Lets the contributor tag find the deleted content on hover.
dom.setAttribute('data-diff-id', diffId);
if (hoistedCodeBlockAnchor) {
// Own widget just before the code block, anchored to the marker above, not `dom` (EDITOR-9045).
var hoistedTagMount;
decorations.push(Decoration.widget(clampAnchorPosIntoCell(newDoc, hoistedCodeBlockAnchor.codeBlockStart, 1), function () {
var _tagHost$host;
var tagHost = createContributorTagHost({
anchorName: contributorTagAnchorName,
diffId: diffId,
mountContext: tagMountContext
});
hoistedTagMount = tagHost === null || tagHost === void 0 ? void 0 : tagHost.mount;
return (_tagHost$host = tagHost === null || tagHost === void 0 ? void 0 : tagHost.host) !== null && _tagHost$host !== void 0 ? _tagHost$host : document.createElement('span');
}, _objectSpread(_objectSpread({}, buildContributorTagDecorationSpec(diffId)), {}, {
side: 1,
marks: [],
ignoreSelection: true,
stopEvent: function stopEvent() {
return true;
},
destroy: function destroy() {
unmountContributorTag(hoistedTagMount);
hoistedTagMount = undefined;
}
})));
} else {
// Anchored to the point marker pushed above, not to `dom` (EDITOR-9286).
var tagHost = createContributorTagHost({
anchorName: contributorTagAnchorName,
diffId: diffId,
mountContext: tagMountContext
});
if (tagHost) {
dom.prepend(tagHost.host);
tagMount = tagHost.mount;
}
}
}
if (showIndicators) {
var leftAnchor = createLeftAnchorWidget({
doc: newDoc,
from: safeInsertPos,
diffId: diffId,
leftAnchorId: leftAnchorId,
measureElement: fg('platform_editor_ai_show_diff_patch_2') ? dom : undefined,
sliceOverride: fg('platform_editor_ai_show_diff_patch_2') ? slice : undefined,
underlyingRange: fg('platform_editor_ai_show_diff_patch_2') ? {
from: change.fromB,
to: change.toB
} : undefined
});
if (leftAnchor) {
decorations.push(leftAnchor);
}
}
decorations.push(Decoration.widget(safeInsertPos, dom, _objectSpread(_objectSpread({}, buildDiffDecorationSpec({
attributionKey: canTagWidget ? attributionKey : undefined,
colorScheme: colorScheme,
decorationType: 'widget',
diffId: diffId,
leftAnchorId: leftAnchorId,
isActive: isActive,
isInserted: isInserted,
// placeBelow anchors at the end of the new content, so render on the
// trailing side (1); otherwise render before the new content (-1).
side: placeBelow ? 1 : -1
})), {}, {
// Without an explicit mark set, prosemirror-view wraps the widget in the marks of the
// adjacent text in the NEW document. `dom` already carries the original marks, so a
// formatting change would render the deleted side wrapped in the very mark the edit
// introduced — unbolded text shown struck through but still bold.
marks: [],
destroy: function destroy() {
constrainMediaObserver.disconnect();
unmountContributorTag(tagMount);
tagMount = undefined;
}
})));
// The first document node keeps its margin-top reset even when an inverted diff renders a visible
// block before it. Add an empty widget matching the following heading so the editor's own heading
// styles supply the appropriate spacing. For other node types, retain the existing generic spacer.
var isPureDeletion = change.fromB === change.toB;
var isSingleBlock = slice.content.childCount === 1 && ((_slice$content$firstC2 = slice.content.firstChild) === null || _slice$content$firstC2 === void 0 ? void 0 : _slice$content$firstC2.isBlock);
var isDiffWidgetAtStartOfDoc = $safeInsertPos.depth === 0 && $safeInsertPos.index(0) === 0;
// A node at the start of its parent has its top margin reset, so a diff widget rendered above
// it sits flush against it. This applies wherever that parent is — the document, a layout
// column, a table cell — so it is decided from the anchor rather than from the document root
// (see `isVisuallyFirstInParent` above).
//
// Away from the parent's start the node below keeps its own margin, and it is the block *inside*
// the widget that loses one — handled by `absorbFirstChildMarginReset` above, not here.
var nodeAfterWidget = $safeInsertPos.nodeAfter;
if (fg('platform_editor_ai_show_diff_patch_2') && isVisuallyFirstInParent && nodeAfterWidget) {
var shapedSpacer = createNodeShapedMarginSpacer({
node: nodeAfterWidget,
serializer: serializer,
testId: 'show-diff-shaped-margin-spacer'
});
if (shapedSpacer) {
// Two elements, because the two families of reset have to be handled differently. The
// absorber takes the document-level adjacent-sibling reset for a leading widget, whose
// `!important` would otherwise zero the shaped spacer's margin; the shaped spacer, now one
// position further along, is out of that rule's reach and keeps the margin it supplies.
// Inside containers the counting selectors skip both, so only the shaped one has an effect.
//
// Sides order the widgets against each other only — both still paint before the real node.
decorations.push(Decoration.widget(safeInsertPos, createMarginAbsorber({
testId: 'show-diff-margin-absorber'
}), {
side: 0
}));
decorations.push(Decoration.widget(safeInsertPos, shapedSpacer, {
side: 1
}));
}
}
if (!fg('platform_editor_ai_show_diff_patch_2') && isDiffWidgetAtStartOfDoc && isSingleBlock && isPureDeletion) {
var followingNode = $safeInsertPos.nodeAfter;
var headingLevel = (followingNode === null || followingNode === void 0 ? void 0 : followingNode.type.name) === 'heading' ? followingNode.attrs.level : undefined;
if (isHeadingLevel(headingLevel)) {
var nodeLikeSpacer = dom.ownerDocument.createElement("h".concat(headingLevel));
nodeLikeSpacer.dataset.testid = 'show-diff-node-like-margin-spacer';
nodeLikeSpacer.setAttribute('aria-hidden', 'true');
nodeLikeSpacer.contentEditable = 'false';
decorations.push(Decoration.widget(safeInsertPos, nodeLikeSpacer, {
side: 0
}));
} else if (followingNode) {
var defaultSpacer = dom.ownerDocument.createElement('span');
defaultSpacer.dataset.testid = 'show-diff-default-margin-spacer';
defaultSpacer.style.display = 'block';
defaultSpacer.style.marginTop = "var(--ds-space-100, 8px)";
if (fg('platform_editor_ai_show_diff_patch_1')) {
defaultSpacer.style.setProperty('scroll-margin-top', scrollMarginTopValue);
}
decorations.push(Decoration.widget(safeInsertPos, defaultSpacer, _objectSpread({}, buildDiffDecorationSpec({
colorScheme: colorScheme,
decorationType: 'widget',
diffId: crypto.randomUUID(),
isInserted: isInserted
}))));
}
}
return decorations;
};