@atlaskit/editor-plugin-show-diff 16.0.17 → 16.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/afm-products/tsconfig.json +3 -0
  3. package/dist/cjs/pm-plugins/decorations/createNodeChangedDecorationWidget.js +83 -3
  4. package/dist/cjs/pm-plugins/decorations/utils/absorbFirstChildMarginReset.js +33 -0
  5. package/dist/cjs/pm-plugins/decorations/utils/createMarginAbsorber.js +43 -0
  6. package/dist/cjs/pm-plugins/decorations/utils/createNodeShapedMarginSpacer.js +76 -0
  7. package/dist/cjs/pm-plugins/decorations/utils/safeResolve.js +24 -0
  8. package/dist/cjs/pm-plugins/main.js +20 -18
  9. package/dist/cjs/ui/ContributorTag/contributorTagController.js +4 -4
  10. package/dist/es2019/pm-plugins/decorations/createNodeChangedDecorationWidget.js +83 -3
  11. package/dist/es2019/pm-plugins/decorations/utils/absorbFirstChildMarginReset.js +29 -0
  12. package/dist/es2019/pm-plugins/decorations/utils/createMarginAbsorber.js +39 -0
  13. package/dist/es2019/pm-plugins/decorations/utils/createNodeShapedMarginSpacer.js +69 -0
  14. package/dist/es2019/pm-plugins/decorations/utils/safeResolve.js +18 -0
  15. package/dist/es2019/pm-plugins/main.js +19 -17
  16. package/dist/es2019/ui/ContributorTag/contributorTagController.js +4 -4
  17. package/dist/esm/pm-plugins/decorations/createNodeChangedDecorationWidget.js +83 -3
  18. package/dist/esm/pm-plugins/decorations/utils/absorbFirstChildMarginReset.js +28 -0
  19. package/dist/esm/pm-plugins/decorations/utils/createMarginAbsorber.js +37 -0
  20. package/dist/esm/pm-plugins/decorations/utils/createNodeShapedMarginSpacer.js +70 -0
  21. package/dist/esm/pm-plugins/decorations/utils/safeResolve.js +18 -0
  22. package/dist/esm/pm-plugins/main.js +20 -18
  23. package/dist/esm/ui/ContributorTag/contributorTagController.js +4 -4
  24. package/dist/types/entry-points/show-diff-plugin-type.d.ts +1 -1
  25. package/dist/types/pm-plugins/decorations/colorSchemes/attributions.d.ts +2 -2
  26. package/dist/types/pm-plugins/decorations/utils/absorbFirstChildMarginReset.d.ts +23 -0
  27. package/dist/types/pm-plugins/decorations/utils/createMarginAbsorber.d.ts +23 -0
  28. package/dist/types/pm-plugins/decorations/utils/createNodeShapedMarginSpacer.d.ts +26 -0
  29. package/dist/types/pm-plugins/decorations/utils/safeResolve.d.ts +13 -0
  30. package/dist/types/pm-plugins/main.d.ts +7 -1
  31. package/dist/types/showDiffPluginType.d.ts +17 -2
  32. package/package.json +14 -6
package/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # @atlaskit/editor-plugin-show-diff
2
2
 
3
+ ## 16.1.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [`285905eda333a`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/285905eda333a) -
8
+ Show agent avatars before user avatars in connected contributor tags.
9
+ - [`2adef2b5610ff`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/2adef2b5610ff) -
10
+ [ux] Supply the top margin above a diff widget that lands at the start of its parent, so deleted
11
+ content no longer renders flush against the node below it. A node at the start of its parent has
12
+ its top margin reset; a spacer shaped like that node now carries the margin instead, in the
13
+ document and inside layout columns, expands, sync blocks and table cells. A complete deleted
14
+ textblock also keeps its own block wrapper so it retains its margin, including when that block
15
+ carries an alignment or indentation mark. Behind the platform_editor_ai_show_diff_patch_2 feature
16
+ gate.
17
+ - Updated dependencies
18
+
19
+ ## 16.1.0
20
+
21
+ ### Minor Changes
22
+
23
+ - [`8c4837792f18f`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/8c4837792f18f) -
24
+ Add a per-call colorScheme override to the showDiff command, so callers can paint a diff in a
25
+ specific colour scheme without changing the plugin's configured default.
26
+
27
+ ### Patch Changes
28
+
29
+ - Updated dependencies
30
+
3
31
  ## 16.0.17
4
32
 
5
33
  ### Patch Changes
@@ -32,6 +32,9 @@
32
32
  {
33
33
  "path": "../../editor-plugin-analytics/afm-products/tsconfig.json"
34
34
  },
35
+ {
36
+ "path": "../../editor-plugin-limited-mode/afm-products/tsconfig.json"
37
+ },
35
38
  {
36
39
  "path": "../../editor-plugin-user-intent/afm-products/tsconfig.json"
37
40
  },
@@ -14,7 +14,11 @@ var _createAnchorDecorationWidgets = require("./createAnchorDecorationWidgets");
14
14
  var _createChangedRowDecorationWidgets = require("./createChangedRowDecorationWidgets");
15
15
  var _createContributorTagWidget = require("./createContributorTagWidget");
16
16
  var _decorationKeys = require("./decorationKeys");
17
+ var _absorbFirstChildMarginReset = require("./utils/absorbFirstChildMarginReset");
18
+ var _createMarginAbsorber = require("./utils/createMarginAbsorber");
19
+ var _createNodeShapedMarginSpacer = require("./utils/createNodeShapedMarginSpacer");
17
20
  var _findSafeInsertPos = require("./utils/findSafeInsertPos");
21
+ var _safeResolve = require("./utils/safeResolve");
18
22
  var _wrapBlockNodeView = require("./utils/wrapBlockNodeView");
19
23
  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; }
20
24
  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; }
@@ -190,6 +194,14 @@ var createNodeChangedDecorationWidget = exports.createNodeChangedDecorationWidge
190
194
  // For non-table content, use the existing span wrapper approach
191
195
  var dom = document.createElement('span');
192
196
  var $safeInsertPos = newDoc.resolve(safeInsertPos);
197
+
198
+ // Whether the widget renders above the very start of its parent's content — the document, a
199
+ // layout column, a table cell, a panel.
200
+ //
201
+ // `parentOffset === 0` is the whole test on the position side: prosemirror-view paints every
202
+ // widget at a position before the node starting there, so a leading widget at the parent's start
203
+ // always renders above its content.
204
+ var isVisuallyFirstInParent = !placeBelow && $safeInsertPos.parentOffset === 0;
193
205
  var isTopLevelInsert = $safeInsertPos.depth === 0;
194
206
  var hasPreviousBlock = ((_$safeInsertPos$nodeB = $safeInsertPos.nodeBefore) === null || _$safeInsertPos$nodeB === void 0 ? void 0 : _$safeInsertPos$nodeB.isBlock) === true;
195
207
  var isFirstDocHeadingReplacement = (0, _isExtendedEnabled.isExtendedEnabled)(diffType) && !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';
@@ -241,6 +253,20 @@ var createNodeChangedDecorationWidget = exports.createNodeChangedDecorationWidge
241
253
  var firstReplacedNode = slice.content.firstChild;
242
254
  var lastReplacedNode = slice.content.lastChild;
243
255
  var showDiffPatch1 = (0, _fg.fg)('platform_editor_ai_show_diff_patch_1');
256
+ // Whether the slice's outermost textblocks still hold all of their original content.
257
+ //
258
+ // `openStart`/`openEnd` say whether the cut landed inside a block, but not whether it took
259
+ // any text with it — a deletion stopping exactly at a block's content boundary is still
260
+ // reported as open. Resolving the change's own ends separates the two, so a block whose text
261
+ // survived intact can render as a block even though its boundary was open.
262
+ //
263
+ // Both resolves sit behind the gate that consumes them, so this adds no position arithmetic to
264
+ // the ungated path, and an unresolvable position degrades to the `openStart`/`openEnd` test on
265
+ // its own — the block is then treated as partial, which is what it rendered as before the gate.
266
+ var $changeFromA = (0, _fg.fg)('platform_editor_ai_show_diff_patch_2') ? (0, _safeResolve.safeResolve)(doc, change.fromA) : null;
267
+ var $changeToA = (0, _fg.fg)('platform_editor_ai_show_diff_patch_2') ? (0, _safeResolve.safeResolve)(doc, change.toA) : null;
268
+ var isFirstNodeContentComplete = slice.openStart === 0 || ($changeFromA === null || $changeFromA === void 0 ? void 0 : $changeFromA.parentOffset) === 0;
269
+ var isLastNodeContentComplete = slice.openEnd === 0 || $changeToA !== null && $changeToA.parentOffset === $changeToA.parent.content.size;
244
270
  var isCompleteSameTypeReplacement = slice.content.childCount === 1 && firstReplacedNode !== null && replacementNode !== null && firstReplacedNode.type === replacementNode.type && replacementNode.nodeSize === change.toB - change.fromB;
245
271
 
246
272
  /*
@@ -253,10 +279,16 @@ var createNodeChangedDecorationWidget = exports.createNodeChangedDecorationWidge
253
279
  var isLast = lastReplacedNode === node;
254
280
  var isOpenAtSliceBoundary = isFirst && slice.openStart > 0 || isLast && slice.openEnd > 0;
255
281
  var shouldPreserveCompleteMultiInlineBlock = showDiffPatch1 && node.isBlock && node.type.inlineContent && node.content.childCount > 1 && !isOpenAtSliceBoundary && isCompleteSameTypeReplacement;
282
+ // A textblock is otherwise serialized as its inline content only, with no `<p>`/`<h2>` wrapper.
283
+ // That is right when the diff cuts into an existing block — the deleted text belongs on the same
284
+ // line as the text that replaced it — but when the whole block went away it drops the very
285
+ // element that carries the block's margin, so the deleted block renders flush against its
286
+ // neighbours. Only the first and last children can be partial; middle ones are always complete.
287
+ var shouldRenderAsBlockNode = (0, _fg.fg)('platform_editor_ai_show_diff_patch_2') && node.isTextblock && (!isFirst || isFirstNodeContentComplete) && (!isLast || isLastNodeContentComplete);
256
288
 
257
289
  // Helper function to handle multiple child nodes
258
290
  var handleMultipleChildNodes = function handleMultipleChildNodes(node) {
259
- if (!shouldPreserveCompleteMultiInlineBlock && node.content.childCount > 1 && node.type.inlineContent) {
291
+ if (!shouldPreserveCompleteMultiInlineBlock && !shouldRenderAsBlockNode && node.content.childCount > 1 && node.type.inlineContent) {
260
292
  node.content.forEach(function (childNode) {
261
293
  var childNodeView = serializer.tryCreateNodeView(childNode);
262
294
  if (childNodeView) {
@@ -286,7 +318,7 @@ var createNodeChangedDecorationWidget = exports.createNodeChangedDecorationWidge
286
318
  if (handleMultipleChildNodes(node)) {
287
319
  return;
288
320
  }
289
- if (shouldPreserveCompleteMultiInlineBlock) {
321
+ if (shouldPreserveCompleteMultiInlineBlock || shouldRenderAsBlockNode) {
290
322
  fallbackSerialization = function fallbackSerialization() {
291
323
  return serializer.serializeNode(node);
292
324
  };
@@ -363,6 +395,21 @@ var createNodeChangedDecorationWidget = exports.createNodeChangedDecorationWidge
363
395
  dom.style.setProperty('scroll-margin-top', _decorationKeys.scrollMarginTopValue);
364
396
  }
365
397
 
398
+ // A block node serialized into the widget is the widget's first child, and the editor's
399
+ // first-child reset is written against the parent element rather than the document position — so
400
+ // it zeroes the margin the block wrapper was kept for in the first place.
401
+ //
402
+ // Only away from the parent's start. There the reset is the correct outcome: the widget is the
403
+ // first thing in the document, column, cell or panel, and a leading gap above it would be wrong.
404
+ // The margin that matters at that position belongs to the node *below* the widget, which the
405
+ // shaped spacer pair supplies instead.
406
+ if ((0, _fg.fg)('platform_editor_ai_show_diff_patch_2') && !isVisuallyFirstInParent) {
407
+ (0, _absorbFirstChildMarginReset.absorbFirstChildMarginReset)({
408
+ dom: dom,
409
+ testId: 'show-diff-widget-margin-absorber'
410
+ });
411
+ }
412
+
366
413
  // Needed even when the indicator bar is off, because a contributor tag also anchors against the
367
414
  // widget.
368
415
  if ((showIndicators || showContributorTags) && (0, _isExtendedEnabled.isExtendedEnabled)(diffType)) {
@@ -425,7 +472,40 @@ var createNodeChangedDecorationWidget = exports.createNodeChangedDecorationWidge
425
472
  var isPureDeletion = change.fromB === change.toB;
426
473
  var isSingleBlock = slice.content.childCount === 1 && ((_slice$content$firstC2 = slice.content.firstChild) === null || _slice$content$firstC2 === void 0 ? void 0 : _slice$content$firstC2.isBlock);
427
474
  var isDiffWidgetAtStartOfDoc = $safeInsertPos.depth === 0 && $safeInsertPos.index(0) === 0;
428
- if (isDiffWidgetAtStartOfDoc && isSingleBlock && isPureDeletion && (0, _isExtendedEnabled.isExtendedEnabled)(diffType)) {
475
+
476
+ // A node at the start of its parent has its top margin reset, so a diff widget rendered above
477
+ // it sits flush against it. This applies wherever that parent is — the document, a layout
478
+ // column, a table cell — so it is decided from the anchor rather than from the document root
479
+ // (see `isVisuallyFirstInParent` above).
480
+ //
481
+ // Away from the parent's start the node below keeps its own margin, and it is the block *inside*
482
+ // the widget that loses one — handled by `absorbFirstChildMarginReset` above, not here.
483
+ var nodeAfterWidget = $safeInsertPos.nodeAfter;
484
+ if ((0, _fg.fg)('platform_editor_ai_show_diff_patch_2') && (0, _isExtendedEnabled.isExtendedEnabled)(diffType) && isVisuallyFirstInParent && nodeAfterWidget) {
485
+ var shapedSpacer = (0, _createNodeShapedMarginSpacer.createNodeShapedMarginSpacer)({
486
+ node: nodeAfterWidget,
487
+ serializer: serializer,
488
+ testId: 'show-diff-shaped-margin-spacer'
489
+ });
490
+ if (shapedSpacer) {
491
+ // Two elements, because the two families of reset have to be handled differently. The
492
+ // absorber takes the document-level adjacent-sibling reset for a leading widget, whose
493
+ // `!important` would otherwise zero the shaped spacer's margin; the shaped spacer, now one
494
+ // position further along, is out of that rule's reach and keeps the margin it supplies.
495
+ // Inside containers the counting selectors skip both, so only the shaped one has an effect.
496
+ //
497
+ // Sides order the widgets against each other only — both still paint before the real node.
498
+ decorations.push(_view.Decoration.widget(safeInsertPos, (0, _createMarginAbsorber.createMarginAbsorber)({
499
+ testId: 'show-diff-margin-absorber'
500
+ }), {
501
+ side: 0
502
+ }));
503
+ decorations.push(_view.Decoration.widget(safeInsertPos, shapedSpacer, {
504
+ side: 1
505
+ }));
506
+ }
507
+ }
508
+ if (!(0, _fg.fg)('platform_editor_ai_show_diff_patch_2') && isDiffWidgetAtStartOfDoc && isSingleBlock && isPureDeletion && (0, _isExtendedEnabled.isExtendedEnabled)(diffType)) {
429
509
  var followingNode = $safeInsertPos.nodeAfter;
430
510
  var headingLevel = (followingNode === null || followingNode === void 0 ? void 0 : followingNode.type.name) === 'heading' ? followingNode.attrs.level : undefined;
431
511
  if (isHeadingLevel(headingLevel)) {
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.absorbFirstChildMarginReset = void 0;
7
+ var _createMarginAbsorber = require("./createMarginAbsorber");
8
+ /**
9
+ * Keeps a block node rendered inside a diff widget from losing its own top margin.
10
+ *
11
+ * The editor's leading-block margin reset is written against the parent element, not the document
12
+ * position, so it fires wherever a text block is a first child — including inside the widget's own
13
+ * `span`, which is what a whole-block deletion renders into. The widget then has no margin of its
14
+ * own, and the block sits flush against whatever is above it, regardless of where in the document
15
+ * the widget landed.
16
+ *
17
+ * The margin is not re-supplied; it is never taken away. Prepending an element the reset does not
18
+ * select moves the real block off the leading position, so the ordinary `.ProseMirror p`,
19
+ * `.ProseMirror h2` and similar rules go on applying to it untouched. That is why this needs no
20
+ * knowledge of which margin the block should have — unlike `createNodeShapedMarginSpacer`, which
21
+ * replicates a margin that has already been zeroed and cannot be read back.
22
+ *
23
+ * Unconditional, because the absorber generates no box: it costs nothing in a widget holding
24
+ * inline content, which the reset was never going to match anyway. Deciding here instead would mean
25
+ * restating the reset's selector list, and a copy of another package's CSS drifts silently.
26
+ */
27
+ var absorbFirstChildMarginReset = exports.absorbFirstChildMarginReset = function absorbFirstChildMarginReset(_ref) {
28
+ var dom = _ref.dom,
29
+ testId = _ref.testId;
30
+ dom.prepend((0, _createMarginAbsorber.createBoxlessMarginAbsorber)({
31
+ testId: testId
32
+ }));
33
+ };
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.createMarginAbsorber = exports.createBoxlessMarginAbsorber = void 0;
7
+ /**
8
+ * An empty, invisible element whose only job is to occupy a position in the DOM.
9
+ *
10
+ * At the top of the document `firstBlockNodeStyles` zeroes the top margin of whatever element
11
+ * follows a leading `.ProseMirror-widget`, and it does so with `!important` — which no inline style
12
+ * can outrank. This takes that hit so the shaped spacer after it, one position further along, keeps
13
+ * the margin it is there to supply.
14
+ *
15
+ * It contributes no height (no content, border or padding) and margins collapse through it, so it
16
+ * cannot affect layout beyond the selector it absorbs.
17
+ */
18
+ var createMarginAbsorber = exports.createMarginAbsorber = function createMarginAbsorber(_ref) {
19
+ var testId = _ref.testId;
20
+ // Block-level: an empty inline element takes the match just as well, but collapses to a
21
+ // zero-height line box, and the following margin then lands in the wrong place.
22
+ var absorber = document.createElement('div');
23
+ absorber.dataset.testid = testId;
24
+ absorber.setAttribute('aria-hidden', 'true');
25
+ absorber.contentEditable = 'false';
26
+ return absorber;
27
+ };
28
+
29
+ /**
30
+ * An absorber that occupies a DOM position without generating a box.
31
+ *
32
+ * Not interchangeable with `createMarginAbsorber`: a rule that selects the *rendered* sibling of a
33
+ * leading widget needs an element that generates a box, and this one does not.
34
+ */
35
+ var createBoxlessMarginAbsorber = exports.createBoxlessMarginAbsorber = function createBoxlessMarginAbsorber(_ref2) {
36
+ var testId = _ref2.testId;
37
+ var absorber = document.createElement('div');
38
+ absorber.dataset.testid = testId;
39
+ absorber.setAttribute('aria-hidden', 'true');
40
+ absorber.contentEditable = 'false';
41
+ absorber.style.display = 'contents';
42
+ return absorber;
43
+ };
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.createNodeShapedMarginSpacer = void 0;
7
+ /**
8
+ * Node types that can safely be reduced to an empty shell.
9
+ *
10
+ * A shell only replicates a margin if it collapses to nothing itself, which needs zero height and
11
+ * no vertical border or padding. Textblocks and lists qualify; nodes with intrinsic sizing (tables
12
+ * are `display: table`, media has a measured height) or a painted box (a zero-height panel is a
13
+ * visible stripe of background) do not, and are left alone.
14
+ */
15
+ var isShapeableNode = function isShapeableNode(node) {
16
+ return node.isTextblock || ['bulletList', 'orderedList'].includes(node.type.name);
17
+ };
18
+
19
+ /**
20
+ * An invisible element shaped like `node`, used to supply the top margin `node` no longer gets.
21
+ *
22
+ * A node at the start of its parent has its top margin reset, so a diff widget rendered above it
23
+ * sits flush against it. The reset cannot simply be taken off the node: the container variants skip
24
+ * widgets when counting — `nth-child(1 of :not(style, .ProseMirror-gapcursor, .ProseMirror-widget,
25
+ * span))` in layout columns, expands, sync blocks and table cells — so no amount of extra elements
26
+ * moves the match. That exclusion is what makes this work instead: the spacer is a
27
+ * `.ProseMirror-widget`, so it is never the counted first child and keeps its own margin, while the
28
+ * real node goes on taking the reset.
29
+ *
30
+ * The margin is not measured. Reading it off the real node is impossible — by then the reset has
31
+ * set it to `0`, and both `firstBlockNodeStyles` (`!important`) and block controls' `firstNodeDec`
32
+ * (an inline style) make it unrecoverable. Instead the spacer carries the node's own tag, classes
33
+ * and attributes, so the same rules that would have given the node its margin match the spacer
34
+ * (`.ProseMirror p`, `.ProseMirror h2`, the root-list rule, and so on).
35
+ *
36
+ * Returns `null` when the node is not safely shapeable or serialization fails.
37
+ */
38
+ var createNodeShapedMarginSpacer = exports.createNodeShapedMarginSpacer = function createNodeShapedMarginSpacer(_ref) {
39
+ var _node$type$createAndF;
40
+ var node = _ref.node,
41
+ serializer = _ref.serializer,
42
+ testId = _ref.testId;
43
+ if (!isShapeableNode(node)) {
44
+ return null;
45
+ }
46
+
47
+ // An empty node of the same type serializes to the same shell without walking real content.
48
+ var shellNode = (_node$type$createAndF = node.type.createAndFill(node.attrs)) !== null && _node$type$createAndF !== void 0 ? _node$type$createAndF : node;
49
+ var serialized = serializer.serializeNode(shellNode);
50
+ if (!(serialized instanceof HTMLElement)) {
51
+ return null;
52
+ }
53
+
54
+ // Keep the tag, classes and attributes the margin rules select on; drop anything that could
55
+ // occupy a line box.
56
+ serialized.replaceChildren();
57
+ serialized.dataset.testid = testId;
58
+ serialized.setAttribute('aria-hidden', 'true');
59
+ serialized.contentEditable = 'false';
60
+
61
+ // Zero height with no vertical border or padding keeps the spacer self-collapsing: its margins
62
+ // stay adjoining, so they collapse with the neighbours into a single margin equal to the largest
63
+ // rather than adding to them. That is what makes this safe in containers that never reset the
64
+ // first child — the spacer cannot double the gap there. Deliberately no `overflow: hidden`, which
65
+ // would open a block formatting context and stop that collapsing.
66
+ serialized.style.height = '0';
67
+ serialized.style.minHeight = '0';
68
+ serialized.style.paddingTop = '0';
69
+ serialized.style.paddingBottom = '0';
70
+ serialized.style.borderTopWidth = '0';
71
+ serialized.style.borderBottomWidth = '0';
72
+ // Only the top margin is being replicated. Left as-is, a larger bottom margin would win the
73
+ // self-collapse and overshoot the gap.
74
+ serialized.style.marginBottom = '0';
75
+ return serialized;
76
+ };
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.safeResolve = void 0;
7
+ /**
8
+ * Resolves a document position, or returns `null` if it cannot be resolved.
9
+ *
10
+ * `Node.resolve` throws a `RangeError` for a position outside the document. Diff decorations are
11
+ * built from change ranges computed against a document that may since have moved on, so a position
12
+ * arriving out of range is a normal outcome rather than a defect — but an exception escaping here
13
+ * takes down the whole decoration set, leaving the diff unrendered.
14
+ *
15
+ * Callers are expected to treat `null` as "cannot tell" and fall back to behaviour that does not
16
+ * need the resolved position.
17
+ */
18
+ var safeResolve = exports.safeResolve = function safeResolve(doc, pos) {
19
+ try {
20
+ return doc.resolve(pos);
21
+ } catch (_unused) {
22
+ return null;
23
+ }
24
+ };
@@ -94,7 +94,7 @@ var createPlugin = exports.createPlugin = function createPlugin(config, getIntl,
94
94
  var newPluginState = currentPluginState;
95
95
  if (meta) {
96
96
  if ((meta === null || meta === void 0 ? void 0 : meta.action) === 'SHOW_DIFF' || (meta === null || meta === void 0 ? void 0 : meta.action) === 'REVEAL_COMPLETE') {
97
- var _newPluginState, _newPluginState2, _newPluginState3, _newPluginState4, _newPluginState5, _newPluginState6, _newPluginState7, _newPluginState8, _newPluginState9, _newPluginState0, _newPluginState1;
97
+ var _newPluginState$color, _newPluginState, _newPluginState2, _newPluginState3, _newPluginState4, _newPluginState5, _newPluginState6, _newPluginState7, _newPluginState8, _newPluginState9, _newPluginState0, _newPluginState1, _newPluginState10;
98
98
  // REVEAL_COMPLETE repaints with the reveal dropped, so the decorations render their
99
99
  // ordinary resting style. Without it the reveal stays in state indefinitely and the
100
100
  // next unrelated repaint re-emits the hidden, zero-width highlight with no animation
@@ -114,23 +114,23 @@ var createPlugin = exports.createPlugin = function createPlugin(config, getIntl,
114
114
  state: newState,
115
115
  pluginState: newPluginState,
116
116
  nodeViewSerializer: nodeViewSerializer,
117
- colorScheme: config === null || config === void 0 ? void 0 : config.colorScheme,
117
+ colorScheme: (_newPluginState$color = (_newPluginState = newPluginState) === null || _newPluginState === void 0 ? void 0 : _newPluginState.colorScheme) !== null && _newPluginState$color !== void 0 ? _newPluginState$color : config === null || config === void 0 ? void 0 : config.colorScheme,
118
118
  intl: getIntl(),
119
119
  activeIndexPos: newPluginState.activeIndexPos,
120
120
  api: api,
121
121
  tagMountContext: tagMountContext
122
- }, (0, _isExtendedEnabled.isExtendedEnabled)((_newPluginState = newPluginState) === null || _newPluginState === void 0 ? void 0 : _newPluginState.diffType) ? {
123
- isInverted: (_newPluginState2 = newPluginState) === null || _newPluginState2 === void 0 ? void 0 : _newPluginState2.isInverted,
124
- diffType: (_newPluginState3 = newPluginState) === null || _newPluginState3 === void 0 ? void 0 : _newPluginState3.diffType,
125
- hideDeletedDiffs: (_newPluginState4 = newPluginState) === null || _newPluginState4 === void 0 ? void 0 : _newPluginState4.hideDeletedDiffs,
126
- hideAddedDiffsUnderline: (_newPluginState5 = newPluginState) === null || _newPluginState5 === void 0 ? void 0 : _newPluginState5.hideAddedDiffsUnderline,
127
- showIndicators: (_newPluginState6 = newPluginState) === null || _newPluginState6 === void 0 ? void 0 : _newPluginState6.showIndicators,
128
- smartThresholds: (_newPluginState7 = newPluginState) === null || _newPluginState7 === void 0 ? void 0 : _newPluginState7.smartThresholds,
129
- deletedDiffPlacement: (_newPluginState8 = newPluginState) === null || _newPluginState8 === void 0 ? void 0 : _newPluginState8.deletedDiffPlacement,
130
- inlineDeletedDiffPlacement: (_newPluginState9 = newPluginState) === null || _newPluginState9 === void 0 ? void 0 : _newPluginState9.inlineDeletedDiffPlacement,
122
+ }, (0, _isExtendedEnabled.isExtendedEnabled)((_newPluginState2 = newPluginState) === null || _newPluginState2 === void 0 ? void 0 : _newPluginState2.diffType) ? {
123
+ isInverted: (_newPluginState3 = newPluginState) === null || _newPluginState3 === void 0 ? void 0 : _newPluginState3.isInverted,
124
+ diffType: (_newPluginState4 = newPluginState) === null || _newPluginState4 === void 0 ? void 0 : _newPluginState4.diffType,
125
+ hideDeletedDiffs: (_newPluginState5 = newPluginState) === null || _newPluginState5 === void 0 ? void 0 : _newPluginState5.hideDeletedDiffs,
126
+ hideAddedDiffsUnderline: (_newPluginState6 = newPluginState) === null || _newPluginState6 === void 0 ? void 0 : _newPluginState6.hideAddedDiffsUnderline,
127
+ showIndicators: (_newPluginState7 = newPluginState) === null || _newPluginState7 === void 0 ? void 0 : _newPluginState7.showIndicators,
128
+ smartThresholds: (_newPluginState8 = newPluginState) === null || _newPluginState8 === void 0 ? void 0 : _newPluginState8.smartThresholds,
129
+ deletedDiffPlacement: (_newPluginState9 = newPluginState) === null || _newPluginState9 === void 0 ? void 0 : _newPluginState9.deletedDiffPlacement,
130
+ inlineDeletedDiffPlacement: (_newPluginState0 = newPluginState) === null || _newPluginState0 === void 0 ? void 0 : _newPluginState0.inlineDeletedDiffPlacement,
131
131
  // SHOW_DIFF only. The scroll-to-next recalculation further down deliberately
132
132
  // omits this so stepping through changes cannot replay the choreography.
133
- reveal: (_newPluginState0 = newPluginState) === null || _newPluginState0 === void 0 ? void 0 : _newPluginState0.reveal
133
+ reveal: (_newPluginState1 = newPluginState) === null || _newPluginState1 === void 0 ? void 0 : _newPluginState1.reveal
134
134
  } : {})),
135
135
  contributorTags = _calculateDiffDecorat.contributorTags,
136
136
  decorations = _calculateDiffDecorat.decorations,
@@ -138,7 +138,7 @@ var createPlugin = exports.createPlugin = function createPlugin(config, getIntl,
138
138
  // Update the decorations and their ids
139
139
  newPluginState.decorations = decorations;
140
140
  newPluginState.contributorTags = contributorTags;
141
- if ((0, _isExtendedEnabled.isExtendedEnabled)((_newPluginState1 = newPluginState) === null || _newPluginState1 === void 0 ? void 0 : _newPluginState1.diffType)) {
141
+ if ((0, _isExtendedEnabled.isExtendedEnabled)((_newPluginState10 = newPluginState) === null || _newPluginState10 === void 0 ? void 0 : _newPluginState10.diffType)) {
142
142
  newPluginState.diffDescriptors = diffDescriptors;
143
143
  }
144
144
  } else if ((meta === null || meta === void 0 ? void 0 : meta.action) === 'HIDE_DIFF') {
@@ -147,7 +147,9 @@ var createPlugin = exports.createPlugin = function createPlugin(config, getIntl,
147
147
  isDisplayingChanges: false,
148
148
  activeIndex: undefined,
149
149
  contributorTags: [],
150
- reveal: undefined
150
+ reveal: undefined,
151
+ // Per-call override — do not let it leak into the next, unrelated `showDiff` call.
152
+ colorScheme: undefined
151
153
  }, (0, _isExtendedEnabled.isExtendedEnabled)(currentPluginState.diffType) ? {
152
154
  isInverted: false,
153
155
  diffType: (0, _isExtendedEnabled.getDefaultDiffType)(),
@@ -156,11 +158,11 @@ var createPlugin = exports.createPlugin = function createPlugin(config, getIntl,
156
158
  diffDescriptors: []
157
159
  } : {});
158
160
  } else if ((meta === null || meta === void 0 ? void 0 : meta.action) === 'SCROLL_TO_NEXT' || (meta === null || meta === void 0 ? void 0 : meta.action) === 'SCROLL_TO_PREVIOUS') {
159
- var _newPluginState10;
161
+ var _newPluginState11;
160
162
  // Update the active index in plugin state and recalculate decorations
161
- var _decorations = (0, _getScrollableDecorations.getScrollableDecorations)(currentPluginState.decorations, newState.doc, (_newPluginState10 = newPluginState) === null || _newPluginState10 === void 0 ? void 0 : _newPluginState10.diffType);
163
+ var _decorations = (0, _getScrollableDecorations.getScrollableDecorations)(currentPluginState.decorations, newState.doc, (_newPluginState11 = newPluginState) === null || _newPluginState11 === void 0 ? void 0 : _newPluginState11.diffType);
162
164
  if (_decorations.length > 0) {
163
- var _currentPluginState$a;
165
+ var _currentPluginState$a, _newPluginState$color2, _newPluginState12;
164
166
  // Initialize to -1 if undefined so that the first "next" scroll takes us to index 0 (first change).
165
167
  // This allows the UI to start with no selection and only highlight on first user interaction.
166
168
  var nextIndex = (_currentPluginState$a = currentPluginState.activeIndex) !== null && _currentPluginState$a !== void 0 ? _currentPluginState$a : -1;
@@ -188,7 +190,7 @@ var createPlugin = exports.createPlugin = function createPlugin(config, getIntl,
188
190
  state: newState,
189
191
  pluginState: newPluginState,
190
192
  nodeViewSerializer: nodeViewSerializer,
191
- colorScheme: config === null || config === void 0 ? void 0 : config.colorScheme,
193
+ colorScheme: (_newPluginState$color2 = (_newPluginState12 = newPluginState) === null || _newPluginState12 === void 0 ? void 0 : _newPluginState12.colorScheme) !== null && _newPluginState$color2 !== void 0 ? _newPluginState$color2 : config === null || config === void 0 ? void 0 : config.colorScheme,
192
194
  intl: getIntl(),
193
195
  activeIndexPos: newPluginState.activeIndexPos,
194
196
  api: api,
@@ -332,11 +332,11 @@ var ContributorTagController = exports.ContributorTagController = /*#__PURE__*/f
332
332
 
333
333
  // Hand-rolled avatar stack: `@atlaskit/avatar-group` cannot render below 24px and the tag uses
334
334
  // 16px avatars.
335
- var stack = model.connectedContributor ? [].concat((0, _toConsumableArray2.default)(user ? [{
336
- contributor: user,
337
- stackIndex: 1
338
- }] : []), (0, _toConsumableArray2.default)(agent ? [{
335
+ var stack = model.connectedContributor ? [].concat((0, _toConsumableArray2.default)(agent ? [{
339
336
  contributor: agent,
337
+ stackIndex: 1
338
+ }] : []), (0, _toConsumableArray2.default)(user ? [{
339
+ contributor: user,
340
340
  stackIndex: 0
341
341
  }] : [])) : [{
342
342
  contributor: model.contributor
@@ -6,7 +6,11 @@ import { createLeftAnchorWidget } from './createAnchorDecorationWidgets';
6
6
  import { createChangedRowDecorationWidgets } from './createChangedRowDecorationWidgets';
7
7
  import { createContributorTagHost, unmountContributorTag } from './createContributorTagWidget';
8
8
  import { buildDiffDecorationSpec, buildAnchorDecorationKey, scrollMarginTopValue } from './decorationKeys';
9
+ import { absorbFirstChildMarginReset } from './utils/absorbFirstChildMarginReset';
10
+ import { createMarginAbsorber } from './utils/createMarginAbsorber';
11
+ import { createNodeShapedMarginSpacer } from './utils/createNodeShapedMarginSpacer';
9
12
  import { findSafeInsertPos } from './utils/findSafeInsertPos';
13
+ import { safeResolve } from './utils/safeResolve';
10
14
  import { wrapBlockNodeView, injectInnerWrapper, createContentWrapper } from './utils/wrapBlockNodeView';
11
15
  const isHeadingLevel = level => typeof level === 'number' && level >= 1 && level <= 6;
12
16
 
@@ -174,6 +178,14 @@ export const createNodeChangedDecorationWidget = ({
174
178
  // For non-table content, use the existing span wrapper approach
175
179
  const dom = document.createElement('span');
176
180
  const $safeInsertPos = newDoc.resolve(safeInsertPos);
181
+
182
+ // Whether the widget renders above the very start of its parent's content — the document, a
183
+ // layout column, a table cell, a panel.
184
+ //
185
+ // `parentOffset === 0` is the whole test on the position side: prosemirror-view paints every
186
+ // widget at a position before the node starting there, so a leading widget at the parent's start
187
+ // always renders above its content.
188
+ const isVisuallyFirstInParent = !placeBelow && $safeInsertPos.parentOffset === 0;
177
189
  const isTopLevelInsert = $safeInsertPos.depth === 0;
178
190
  const hasPreviousBlock = ((_$safeInsertPos$nodeB = $safeInsertPos.nodeBefore) === null || _$safeInsertPos$nodeB === void 0 ? void 0 : _$safeInsertPos$nodeB.isBlock) === true;
179
191
  const isFirstDocHeadingReplacement = isExtendedEnabled(diffType) && !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';
@@ -225,6 +237,20 @@ export const createNodeChangedDecorationWidget = ({
225
237
  const firstReplacedNode = slice.content.firstChild;
226
238
  const lastReplacedNode = slice.content.lastChild;
227
239
  const showDiffPatch1 = fg('platform_editor_ai_show_diff_patch_1');
240
+ // Whether the slice's outermost textblocks still hold all of their original content.
241
+ //
242
+ // `openStart`/`openEnd` say whether the cut landed inside a block, but not whether it took
243
+ // any text with it — a deletion stopping exactly at a block's content boundary is still
244
+ // reported as open. Resolving the change's own ends separates the two, so a block whose text
245
+ // survived intact can render as a block even though its boundary was open.
246
+ //
247
+ // Both resolves sit behind the gate that consumes them, so this adds no position arithmetic to
248
+ // the ungated path, and an unresolvable position degrades to the `openStart`/`openEnd` test on
249
+ // its own — the block is then treated as partial, which is what it rendered as before the gate.
250
+ const $changeFromA = fg('platform_editor_ai_show_diff_patch_2') ? safeResolve(doc, change.fromA) : null;
251
+ const $changeToA = fg('platform_editor_ai_show_diff_patch_2') ? safeResolve(doc, change.toA) : null;
252
+ const isFirstNodeContentComplete = slice.openStart === 0 || ($changeFromA === null || $changeFromA === void 0 ? void 0 : $changeFromA.parentOffset) === 0;
253
+ const isLastNodeContentComplete = slice.openEnd === 0 || $changeToA !== null && $changeToA.parentOffset === $changeToA.parent.content.size;
228
254
  const isCompleteSameTypeReplacement = slice.content.childCount === 1 && firstReplacedNode !== null && replacementNode !== null && firstReplacedNode.type === replacementNode.type && replacementNode.nodeSize === change.toB - change.fromB;
229
255
 
230
256
  /*
@@ -237,10 +263,16 @@ export const createNodeChangedDecorationWidget = ({
237
263
  const isLast = lastReplacedNode === node;
238
264
  const isOpenAtSliceBoundary = isFirst && slice.openStart > 0 || isLast && slice.openEnd > 0;
239
265
  const shouldPreserveCompleteMultiInlineBlock = showDiffPatch1 && node.isBlock && node.type.inlineContent && node.content.childCount > 1 && !isOpenAtSliceBoundary && isCompleteSameTypeReplacement;
266
+ // A textblock is otherwise serialized as its inline content only, with no `<p>`/`<h2>` wrapper.
267
+ // That is right when the diff cuts into an existing block — the deleted text belongs on the same
268
+ // line as the text that replaced it — but when the whole block went away it drops the very
269
+ // element that carries the block's margin, so the deleted block renders flush against its
270
+ // neighbours. Only the first and last children can be partial; middle ones are always complete.
271
+ const shouldRenderAsBlockNode = fg('platform_editor_ai_show_diff_patch_2') && node.isTextblock && (!isFirst || isFirstNodeContentComplete) && (!isLast || isLastNodeContentComplete);
240
272
 
241
273
  // Helper function to handle multiple child nodes
242
274
  const handleMultipleChildNodes = node => {
243
- if (!shouldPreserveCompleteMultiInlineBlock && node.content.childCount > 1 && node.type.inlineContent) {
275
+ if (!shouldPreserveCompleteMultiInlineBlock && !shouldRenderAsBlockNode && node.content.childCount > 1 && node.type.inlineContent) {
244
276
  node.content.forEach(childNode => {
245
277
  const childNodeView = serializer.tryCreateNodeView(childNode);
246
278
  if (childNodeView) {
@@ -270,7 +302,7 @@ export const createNodeChangedDecorationWidget = ({
270
302
  if (handleMultipleChildNodes(node)) {
271
303
  return;
272
304
  }
273
- if (shouldPreserveCompleteMultiInlineBlock) {
305
+ if (shouldPreserveCompleteMultiInlineBlock || shouldRenderAsBlockNode) {
274
306
  fallbackSerialization = () => serializer.serializeNode(node);
275
307
  } else if ((isFirst || isLast && slice.content.childCount > 2) && hasInlineContent) {
276
308
  fallbackSerialization = () => serializer.serializeFragment(node.content);
@@ -341,6 +373,21 @@ export const createNodeChangedDecorationWidget = ({
341
373
  dom.style.setProperty('scroll-margin-top', scrollMarginTopValue);
342
374
  }
343
375
 
376
+ // A block node serialized into the widget is the widget's first child, and the editor's
377
+ // first-child reset is written against the parent element rather than the document position — so
378
+ // it zeroes the margin the block wrapper was kept for in the first place.
379
+ //
380
+ // Only away from the parent's start. There the reset is the correct outcome: the widget is the
381
+ // first thing in the document, column, cell or panel, and a leading gap above it would be wrong.
382
+ // The margin that matters at that position belongs to the node *below* the widget, which the
383
+ // shaped spacer pair supplies instead.
384
+ if (fg('platform_editor_ai_show_diff_patch_2') && !isVisuallyFirstInParent) {
385
+ absorbFirstChildMarginReset({
386
+ dom,
387
+ testId: 'show-diff-widget-margin-absorber'
388
+ });
389
+ }
390
+
344
391
  // Needed even when the indicator bar is off, because a contributor tag also anchors against the
345
392
  // widget.
346
393
  if ((showIndicators || showContributorTags) && isExtendedEnabled(diffType)) {
@@ -410,7 +457,40 @@ export const createNodeChangedDecorationWidget = ({
410
457
  const isPureDeletion = change.fromB === change.toB;
411
458
  const isSingleBlock = slice.content.childCount === 1 && ((_slice$content$firstC2 = slice.content.firstChild) === null || _slice$content$firstC2 === void 0 ? void 0 : _slice$content$firstC2.isBlock);
412
459
  const isDiffWidgetAtStartOfDoc = $safeInsertPos.depth === 0 && $safeInsertPos.index(0) === 0;
413
- if (isDiffWidgetAtStartOfDoc && isSingleBlock && isPureDeletion && isExtendedEnabled(diffType)) {
460
+
461
+ // A node at the start of its parent has its top margin reset, so a diff widget rendered above
462
+ // it sits flush against it. This applies wherever that parent is — the document, a layout
463
+ // column, a table cell — so it is decided from the anchor rather than from the document root
464
+ // (see `isVisuallyFirstInParent` above).
465
+ //
466
+ // Away from the parent's start the node below keeps its own margin, and it is the block *inside*
467
+ // the widget that loses one — handled by `absorbFirstChildMarginReset` above, not here.
468
+ const nodeAfterWidget = $safeInsertPos.nodeAfter;
469
+ if (fg('platform_editor_ai_show_diff_patch_2') && isExtendedEnabled(diffType) && isVisuallyFirstInParent && nodeAfterWidget) {
470
+ const shapedSpacer = createNodeShapedMarginSpacer({
471
+ node: nodeAfterWidget,
472
+ serializer,
473
+ testId: 'show-diff-shaped-margin-spacer'
474
+ });
475
+ if (shapedSpacer) {
476
+ // Two elements, because the two families of reset have to be handled differently. The
477
+ // absorber takes the document-level adjacent-sibling reset for a leading widget, whose
478
+ // `!important` would otherwise zero the shaped spacer's margin; the shaped spacer, now one
479
+ // position further along, is out of that rule's reach and keeps the margin it supplies.
480
+ // Inside containers the counting selectors skip both, so only the shaped one has an effect.
481
+ //
482
+ // Sides order the widgets against each other only — both still paint before the real node.
483
+ decorations.push(Decoration.widget(safeInsertPos, createMarginAbsorber({
484
+ testId: 'show-diff-margin-absorber'
485
+ }), {
486
+ side: 0
487
+ }));
488
+ decorations.push(Decoration.widget(safeInsertPos, shapedSpacer, {
489
+ side: 1
490
+ }));
491
+ }
492
+ }
493
+ if (!fg('platform_editor_ai_show_diff_patch_2') && isDiffWidgetAtStartOfDoc && isSingleBlock && isPureDeletion && isExtendedEnabled(diffType)) {
414
494
  const followingNode = $safeInsertPos.nodeAfter;
415
495
  const headingLevel = (followingNode === null || followingNode === void 0 ? void 0 : followingNode.type.name) === 'heading' ? followingNode.attrs.level : undefined;
416
496
  if (isHeadingLevel(headingLevel)) {