UNPKG

@atlaskit/editor-plugin-show-diff

Version:

ShowDiff plugin for @atlaskit/editor-core

659 lines (641 loc) • 34.7 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.createNodeChangedDecorationWidget = void 0; var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); var _view = require("@atlaskit/editor-prosemirror/view"); var _fg = require("@atlaskit/platform-feature-flags/fg"); var _emptyTextBlocks = require("../utils/emptyTextBlocks"); var _isEmptyParagraphSlice = require("../utils/isEmptyParagraphSlice"); var _createAnchorDecorationWidgets = require("./createAnchorDecorationWidgets"); var _createChangedRowDecorationWidgets = require("./createChangedRowDecorationWidgets"); var _createContributorTagWidget = require("./createContributorTagWidget"); var _decorationKeys = require("./decorationKeys"); var _absorbFirstChildMarginReset = require("./utils/absorbFirstChildMarginReset"); var _createDeletedLineBreakWidget = require("./utils/createDeletedLineBreakWidget"); var _createMarginAbsorber = require("./utils/createMarginAbsorber"); var _createNodeShapedMarginSpacer = require("./utils/createNodeShapedMarginSpacer"); var _findSafeInsertPos = require("./utils/findSafeInsertPos"); var _safeResolve = require("./utils/safeResolve"); var _tableDiffMode = require("./utils/tableDiffMode"); var _wrapBlockNodeView = require("./utils/wrapBlockNodeView"); 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; } /** * 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 (0, _safeResolve.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 = (0, _wrapBlockNodeView.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 ((0, _fg.fg)('platform_editor_ai_show_diff_patch_1')) { dom.style.setProperty('scroll-margin-top', _decorationKeys.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(_view.Decoration.widget(targetPos, dom, _objectSpread({}, (0, _decorationKeys.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. */ var createNodeChangedDecorationWidget = exports.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 && (0, _isEmptyParagraphSlice.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 && (0, _fg.fg)('platform_editor_ai_show_diff_patch_2') ? (0, _emptyTextBlocks.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 = (0, _findSafeInsertPos.findSafeInsertPos)(newDoc, anchorPos, slice); var isActive = activeIndexPos && ((0, _fg.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 [(0, _createDeletedLineBreakWidget.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 = (0, _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 (0, _createChangedRowDecorationWidgets.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 = (0, _fg.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 = (0, _fg.fg)('platform_editor_ai_show_diff_patch_2') ? (0, _safeResolve.safeResolve)(doc, change.fromA) : null; var $changeToA = (0, _fg.fg)('platform_editor_ai_show_diff_patch_2') ? (0, _safeResolve.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 = (0, _fg.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 = (0, _wrapBlockNodeView.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 = (0, _wrapBlockNodeView.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 = (0, _fg.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 = (0, _wrapBlockNodeView.createContentWrapper)(colorScheme, isActive, isInserted, reveal); wrapper.append(nodeView); dom.append(wrapper); } else { // Handle all block nodes with unified function (0, _wrapBlockNodeView.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 = (0, _wrapBlockNodeView.injectInnerWrapper)({ node: fallbackNode, colorScheme: colorScheme, isActive: isActive, isInserted: isInserted, reveal: reveal }); dom.append(injectedNode); } else { var _wrapper2 = (0, _wrapBlockNodeView.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 ? (0, _createContributorTagWidget.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 = (0, _decorationKeys.buildAnchorDecorationKey)({ diffId: diffId, anchorType: _decorationKeys.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((0, _wrapBlockNodeView.createRemovedLozenge)(intl, isActive, colorScheme, true)); } }); } dom.setAttribute('data-testid', 'show-diff-deleted-decoration'); if ((0, _fg.fg)('platform_editor_ai_show_diff_patch_1')) { dom.style.setProperty('scroll-margin-top', _decorationKeys.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 ((0, _fg.fg)('platform_editor_ai_show_diff_patch_2') && !isVisuallyFirstInParent) { (0, _absorbFirstChildMarginReset.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((0, _decorationKeys.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 ((0, _fg.fg)('confluence_ncs_step_diffing_version_history') && !contributorTagAnchorName) { var tagPointAnchorName = (0, _decorationKeys.buildAnchorDecorationKey)({ diffId: diffId, anchorType: _decorationKeys.AnchorTypeKey.tag }); contributorTagAnchorName = tagPointAnchorName; decorations.push(_view.Decoration.widget(safeInsertPos, function () { return (0, _createAnchorDecorationWidgets.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(_view.Decoration.widget((0, _createAnchorDecorationWidgets.clampAnchorPosIntoCell)(newDoc, hoistedCodeBlockAnchor.codeBlockStart, 1), function () { var _tagHost$host; var tagHost = (0, _createContributorTagWidget.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({}, (0, _decorationKeys.buildContributorTagDecorationSpec)(diffId)), {}, { side: 1, marks: [], ignoreSelection: true, stopEvent: function stopEvent() { return true; }, destroy: function destroy() { (0, _createContributorTagWidget.unmountContributorTag)(hoistedTagMount); hoistedTagMount = undefined; } }))); } else { // Anchored to the point marker pushed above, not to `dom` (EDITOR-9286). var tagHost = (0, _createContributorTagWidget.createContributorTagHost)({ anchorName: contributorTagAnchorName, diffId: diffId, mountContext: tagMountContext }); if (tagHost) { dom.prepend(tagHost.host); tagMount = tagHost.mount; } } } if (showIndicators) { var leftAnchor = (0, _createAnchorDecorationWidgets.createLeftAnchorWidget)({ doc: newDoc, from: safeInsertPos, diffId: diffId, leftAnchorId: leftAnchorId, measureElement: (0, _fg.fg)('platform_editor_ai_show_diff_patch_2') ? dom : undefined, sliceOverride: (0, _fg.fg)('platform_editor_ai_show_diff_patch_2') ? slice : undefined, underlyingRange: (0, _fg.fg)('platform_editor_ai_show_diff_patch_2') ? { from: change.fromB, to: change.toB } : undefined }); if (leftAnchor) { decorations.push(leftAnchor); } } decorations.push(_view.Decoration.widget(safeInsertPos, dom, _objectSpread(_objectSpread({}, (0, _decorationKeys.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(); (0, _createContributorTagWidget.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 ((0, _fg.fg)('platform_editor_ai_show_diff_patch_2') && isVisuallyFirstInParent && nodeAfterWidget) { var shapedSpacer = (0, _createNodeShapedMarginSpacer.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(_view.Decoration.widget(safeInsertPos, (0, _createMarginAbsorber.createMarginAbsorber)({ testId: 'show-diff-margin-absorber' }), { side: 0 })); decorations.push(_view.Decoration.widget(safeInsertPos, shapedSpacer, { side: 1 })); } } if (!(0, _fg.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(_view.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 ((0, _fg.fg)('platform_editor_ai_show_diff_patch_1')) { defaultSpacer.style.setProperty('scroll-margin-top', _decorationKeys.scrollMarginTopValue); } decorations.push(_view.Decoration.widget(safeInsertPos, defaultSpacer, _objectSpread({}, (0, _decorationKeys.buildDiffDecorationSpec)({ colorScheme: colorScheme, decorationType: 'widget', diffId: crypto.randomUUID(), isInserted: isInserted })))); } } return decorations; };