@atlaskit/editor-plugin-show-diff 16.0.2 → 16.0.4

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 (37) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/dist/cjs/pm-plugins/calculateDiff/calculateDiffDecorations.js +17 -2
  3. package/dist/cjs/pm-plugins/calculateDiff/isOpenTokenOnlyChange.js +32 -0
  4. package/dist/cjs/pm-plugins/decorations/createAnchorDecorationWidgets.js +13 -1
  5. package/dist/cjs/pm-plugins/decorations/createContributorTagWidget.js +20 -1
  6. package/dist/cjs/pm-plugins/decorations/extractContributorTags.js +121 -21
  7. package/dist/cjs/pm-plugins/decorations/utils/wrapBlockNodeView.js +6 -6
  8. package/dist/cjs/pm-plugins/decorations/utils/wrapBlockNodeViewStyles.js +8 -10
  9. package/dist/cjs/pm-plugins/decorations/utils/wrapBlockNodeViewStyles.legacy.js +2 -1
  10. package/dist/cjs/ui/ContributorTag/buildContributorTagDom.js +25 -1
  11. package/dist/cjs/ui/ContributorTag/contributorTagController.js +31 -9
  12. package/dist/es2019/pm-plugins/calculateDiff/calculateDiffDecorations.js +17 -2
  13. package/dist/es2019/pm-plugins/calculateDiff/isOpenTokenOnlyChange.js +27 -0
  14. package/dist/es2019/pm-plugins/decorations/createAnchorDecorationWidgets.js +13 -1
  15. package/dist/es2019/pm-plugins/decorations/createContributorTagWidget.js +18 -1
  16. package/dist/es2019/pm-plugins/decorations/extractContributorTags.js +70 -10
  17. package/dist/es2019/pm-plugins/decorations/utils/wrapBlockNodeView.js +6 -6
  18. package/dist/es2019/pm-plugins/decorations/utils/wrapBlockNodeViewStyles.js +6 -8
  19. package/dist/es2019/pm-plugins/decorations/utils/wrapBlockNodeViewStyles.legacy.js +2 -1
  20. package/dist/es2019/ui/ContributorTag/buildContributorTagDom.js +24 -0
  21. package/dist/es2019/ui/ContributorTag/contributorTagController.js +30 -10
  22. package/dist/esm/pm-plugins/calculateDiff/calculateDiffDecorations.js +17 -2
  23. package/dist/esm/pm-plugins/calculateDiff/isOpenTokenOnlyChange.js +26 -0
  24. package/dist/esm/pm-plugins/decorations/createAnchorDecorationWidgets.js +13 -1
  25. package/dist/esm/pm-plugins/decorations/createContributorTagWidget.js +20 -1
  26. package/dist/esm/pm-plugins/decorations/extractContributorTags.js +121 -21
  27. package/dist/esm/pm-plugins/decorations/utils/wrapBlockNodeView.js +6 -6
  28. package/dist/esm/pm-plugins/decorations/utils/wrapBlockNodeViewStyles.js +8 -10
  29. package/dist/esm/pm-plugins/decorations/utils/wrapBlockNodeViewStyles.legacy.js +2 -1
  30. package/dist/esm/ui/ContributorTag/buildContributorTagDom.js +24 -0
  31. package/dist/esm/ui/ContributorTag/contributorTagController.js +32 -10
  32. package/dist/types/pm-plugins/calculateDiff/isOpenTokenOnlyChange.d.ts +22 -0
  33. package/dist/types/pm-plugins/decorations/extractContributorTags.d.ts +14 -1
  34. package/dist/types/pm-plugins/decorations/utils/wrapBlockNodeViewStyles.d.ts +2 -3
  35. package/dist/types/ui/ContributorTag/buildContributorTagDom.d.ts +7 -0
  36. package/dist/types/ui/ContributorTag/contributorTagController.d.ts +11 -0
  37. package/package.json +5 -5
@@ -17,10 +17,12 @@ import { extractDiffDescriptors } from '../decorations/decorationKeys';
17
17
  import { extractContributorTags } from '../decorations/extractContributorTags';
18
18
  import { getAttrChangeRanges, stepIsValidAttrChange } from '../decorations/utils/getAttrChangeRanges';
19
19
  import { getMarkChangeRanges } from '../decorations/utils/getMarkChangeRanges';
20
+ import { getScrollableDecorations } from '../getScrollableDecorations';
20
21
  import { getDefaultDiffType, isExtendedEnabled } from '../isExtendedEnabled';
21
22
  import { diffBySteps } from './diffBySteps';
22
23
  import { groupChangesByBlock } from './groupChangesByBlock';
23
24
  import { isMarkOnlyChange } from './isMarkOnlyChange';
25
+ import { isOpenTokenOnlyChange } from './isOpenTokenOnlyChange';
24
26
  import { optimizeChanges } from './optimizeChanges';
25
27
  import { selectTokenEncoder } from './selectTokenEncoder';
26
28
  import { collapseOverlappingChanges, simplifyChangesWithAttribution } from './simplifyChangesWithAttribution';
@@ -463,6 +465,16 @@ const calculateDiffDecorationsInner = ({
463
465
  newDoc: tr.doc
464
466
  });
465
467
 
468
+ // The deleted side of a change over a node's open token is an attribute state rather than
469
+ // content, so there is nothing for the deleted-content widget to draw. Decided here, with
470
+ // the other deleted-side suppressions, so the widget is never asked for a slice it can only
471
+ // render as an empty copy of the block (EDITOR-8912). Its inserted side is untouched, and
472
+ // the node's content change is a change of its own.
473
+ const hasNoDeletedContent = fg('platform_editor_ai_show_diff_patch_1') && change.deleted.length > 0 && isOpenTokenOnlyChange({
474
+ change,
475
+ originalDoc
476
+ });
477
+
466
478
  // Hoisted because it decides BOTH where the deleted widget is anchored and — since the
467
479
  // widget pins whichever end of the range it sits at — how the indicator anchors below are
468
480
  // allowed to move.
@@ -572,7 +584,7 @@ const calculateDiffDecorationsInner = ({
572
584
  coarseTableCellsOnly: useCoarseTableDecoration
573
585
  }));
574
586
  }
575
- if (change.deleted.length > 0 && !isMarkOnly) {
587
+ if (change.deleted.length > 0 && !isMarkOnly && !hasNoDeletedContent) {
576
588
  const shouldHideDeleted = shouldHideDeletedSide({
577
589
  change,
578
590
  diffType,
@@ -732,7 +744,10 @@ const calculateDiffDecorationsInner = ({
732
744
  });
733
745
  const decorationSet = DecorationSet.empty.add(tr.doc, decorations);
734
746
  return {
735
- contributorTags: showContributorTags ? extractContributorTags(decorationSet, contributors) : [],
747
+ contributorTags: showContributorTags ? extractContributorTags(decorationSet, contributors, activeIndexPos,
748
+ // The stops the step buttons walk, so a stop cannot hold a second tag for one
749
+ // contributor that navigation can never reach.
750
+ getScrollableDecorations(decorationSet, tr.doc, diffType)) : [],
736
751
  decorations: decorationSet,
737
752
  diffDescriptors: extractDiffDescriptors(decorationSet)
738
753
  };
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Whether a change spans nothing but a block node's open token, so its deleted side carries no
3
+ * content.
4
+ *
5
+ * The attribute-aware token encoder folds a node's diffable attributes into its open token, so a
6
+ * same-type replacement — an AI suggestion rewriting a code block — is reported as two changes: one
7
+ * over the single position of that token, carrying the attribute change, and one for the content.
8
+ * Slicing the first yields the node with none of its content, which leaves the deleted-content
9
+ * widget nothing to draw but an empty copy of the block (EDITOR-8912). The content change is a
10
+ * change of its own and is decorated in place.
11
+ *
12
+ * A whole node is not matched: its range covers its close token too. Neither is an empty leaf (a
13
+ * `rule`, a `blockCard`), which is one position but slices closed rather than open.
14
+ */
15
+ export const isOpenTokenOnlyChange = ({
16
+ change,
17
+ originalDoc
18
+ }) => {
19
+ if (change.toA - change.fromA !== 1) {
20
+ return false;
21
+ }
22
+ const slice = originalDoc.slice(change.fromA, change.toA);
23
+ // `childCount === 1` already means `firstChild` is there; the optional chain is for the type,
24
+ // which has it nullable.
25
+ const node = slice.content.firstChild;
26
+ return (slice.openStart > 0 || slice.openEnd > 0) && slice.content.childCount === 1 && (node === null || node === void 0 ? void 0 : node.isBlock) === true && node.content.size === 0;
27
+ };
@@ -44,6 +44,16 @@ const edgeCases = (doc, from) => {
44
44
  nodeStart,
45
45
  beforePos
46
46
  } = resolved;
47
+ if (node.type.name === 'layoutSection' && fg('platform_editor_ai_show_diff_patch_1')) {
48
+ // Columns extend past the node-view wrapper via negative margins (12px or 20px).
49
+ // Measure their container so the indicator stays outside the diff outline, including
50
+ // breakout layouts and after responsive resizing, without changing the column geometry.
51
+ return {
52
+ beforePos,
53
+ measurePos: beforePos,
54
+ measureSelector: '[data-layout-section]'
55
+ };
56
+ }
47
57
 
48
58
  /**
49
59
  * All resizable nodes will need dynamic calculations of the block indicator left anchor
@@ -152,10 +162,12 @@ export const createLeftAnchorWidget = ({
152
162
  anchor.style.setProperty('transform', 'translateX(-50%)');
153
163
  wrapper.appendChild(anchor);
154
164
  const measureWidth = () => {
165
+ var _nodeDOM$querySelecto;
155
166
  if (getPos() === undefined || edgeCase.measurePos === undefined) {
156
167
  return;
157
168
  }
158
- const dom = view.nodeDOM(edgeCase.measurePos);
169
+ const nodeDOM = view.nodeDOM(edgeCase.measurePos);
170
+ const dom = edgeCase.measureSelector && nodeDOM instanceof HTMLElement ? (_nodeDOM$querySelecto = nodeDOM.querySelector(edgeCase.measureSelector)) !== null && _nodeDOM$querySelecto !== void 0 ? _nodeDOM$querySelecto : nodeDOM : nodeDOM;
159
171
  if (dom instanceof HTMLElement) {
160
172
  // The left anchor only needs the container width so the
161
173
  // IndicatorBar can align against the block's horizontal extent.
@@ -39,7 +39,24 @@ export const mountContributorTag = ({
39
39
  diffId
40
40
  });
41
41
  controller.mount(host);
42
- return controller;
42
+
43
+ /**
44
+ * Kept alive when ProseMirror only rebuilt the widget desc around this same host: a document
45
+ * change remaps the decoration and fires `destroy`, but an element `toDOM` is reused verbatim
46
+ * and runs no mount callback, so tearing down here left an active change with no tag
47
+ * (EDITOR-8971).
48
+ *
49
+ * Deferred, because the host is still attached while that update is in flight. A host outside a
50
+ * live editor root is a real removal — including the view being destroyed, which drops the root.
51
+ */
52
+ return {
53
+ destroy: () => queueMicrotask(() => {
54
+ var _mountContext$getEdit;
55
+ if (!host.isConnected || !((_mountContext$getEdit = mountContext.getEditorRoot()) !== null && _mountContext$getEdit !== void 0 && _mountContext$getEdit.contains(host))) {
56
+ controller.destroy();
57
+ }
58
+ })
59
+ };
43
60
  };
44
61
  export const unmountContributorTag = mount => {
45
62
  mount === null || mount === void 0 ? void 0 : mount.destroy();
@@ -34,12 +34,44 @@ const leadsReplacement = widget => fg('confluence_ncs_step_diffing_version_histo
34
34
  */
35
35
  const isSameBlockChange = (inner, block) => inner.spec.attributionKey === block.spec.attributionKey && (block.from <= inner.from && inner.to <= block.to || inner.from <= block.from && block.to <= inner.to);
36
36
 
37
+ /**
38
+ * Which of two changes in the same range leads, and so is the one a tag captions. Position first,
39
+ * then `side`, since deleted content shares its `from` with the content that replaced it and paints
40
+ * above; then the narrower range, so a change nested in another captions itself.
41
+ */
42
+ const byLeadingPosition = (a, b) => {
43
+ var _a$decoration$spec$si, _b$decoration$spec$si;
44
+ return a.decoration.from - b.decoration.from || ((_a$decoration$spec$si = a.decoration.spec.side) !== null && _a$decoration$spec$si !== void 0 ? _a$decoration$spec$si : 0) - ((_b$decoration$spec$si = b.decoration.spec.side) !== null && _b$decoration$spec$si !== void 0 ? _b$decoration$spec$si : 0) || a.decoration.to - a.decoration.from - (b.decoration.to - b.decoration.from);
45
+ };
46
+
47
+ /** The targets a navigation range wholly covers, leader first. */
48
+ const containedBy = (targets, {
49
+ from,
50
+ to
51
+ }) => targets.filter(({
52
+ decoration
53
+ }) => from <= decoration.from && decoration.to <= to).sort(byLeadingPosition);
54
+
55
+ /**
56
+ * The one tag a navigation step reveals. `spec.isActive` is no use here: it covers everything the
57
+ * group's union range touches, so several tags revealed over each other (EDITOR-8971). Hence
58
+ * containment, and the leading change of what it covers.
59
+ */
60
+ const resolveActiveTarget = (surviving, activeIndexPos) => containedBy(surviving, activeIndexPos)[0];
61
+
37
62
  /**
38
63
  * One model per diff decoration whose attribution resolves to a supplied contributor; anything
39
64
  * unattributed, untaggable or unresolved is skipped. A replacement's two decorations are collapsed
40
65
  * into a single tag, on whichever half renders first — see `isSameChange` and `leadsReplacement`.
66
+ *
67
+ * `activeIndexPos`, when given, reveals exactly one of the tags — see `resolveActiveTarget`.
68
+ * Absent, each tag keeps the active state its own decoration was drawn with.
69
+ *
70
+ * `stops`, when given, is the navigation stop list the step buttons walk
71
+ * (`getScrollableDecorations`). One contributor cannot hold two tags in one stop, since only the
72
+ * leading one would ever be reachable. Absent, every decoration keeps its own tag.
41
73
  */
42
- export const extractContributorTags = (decorations, contributors) => {
74
+ export const extractContributorTags = (decorations, contributors, activeIndexPos, stops) => {
43
75
  if (!contributors) {
44
76
  return [];
45
77
  }
@@ -77,10 +109,15 @@ export const extractContributorTags = (decorations, contributors) => {
77
109
  const linkedDiffIds = new Map();
78
110
  const folded = new Set();
79
111
  const fold = (host, target) => {
80
- var _linkedDiffIds$get;
112
+ var _linkedDiffIds$get, _linkedDiffIds$get2;
81
113
  folded.add(target);
82
114
  const hostDiffId = host.decoration.spec.diffId;
83
- linkedDiffIds.set(hostDiffId, [...((_linkedDiffIds$get = linkedDiffIds.get(hostDiffId)) !== null && _linkedDiffIds$get !== void 0 ? _linkedDiffIds$get : []), target.decoration.spec.diffId]);
115
+ const targetDiffId = target.decoration.spec.diffId;
116
+ // A target that already hosts folded tags hands them up, so no hover target is orphaned when
117
+ // one host folds into another.
118
+ const inherited = (_linkedDiffIds$get = linkedDiffIds.get(targetDiffId)) !== null && _linkedDiffIds$get !== void 0 ? _linkedDiffIds$get : [];
119
+ linkedDiffIds.delete(targetDiffId);
120
+ linkedDiffIds.set(hostDiffId, [...((_linkedDiffIds$get2 = linkedDiffIds.get(hostDiffId)) !== null && _linkedDiffIds$get2 !== void 0 ? _linkedDiffIds$get2 : []), targetDiffId, ...inherited]);
84
121
  };
85
122
  for (const target of targets) {
86
123
  const {
@@ -120,15 +157,38 @@ export const extractContributorTags = (decorations, contributors) => {
120
157
  fold(host, target);
121
158
  }
122
159
  }
123
- return targets.filter(target => !folded.has(target)).map(({
124
- contributor,
125
- decoration: {
126
- spec
160
+
161
+ // One tag per contributor per navigation stop. The folds above only catch a replacement's two
162
+ // halves and a block's own highlights; two of one contributor's changes that merely touch are a
163
+ // single stop (`groupTouchingDecorations`) yet kept a tag each, so the stop's trailing tag was
164
+ // drawn but could never be stepped to (EDITOR-8971). It folds into the leading tag of its
165
+ // contributor, staying a hover target. Contributors are kept apart: a stop spanning two of them
166
+ // still captions each, rather than crediting one for the other's change.
167
+ for (const stop of stops !== null && stops !== void 0 ? stops : []) {
168
+ const byContributor = new Map();
169
+ for (const target of containedBy(targets.filter(target => !folded.has(target)), stop)) {
170
+ var _target$decoration$sp, _byContributor$get;
171
+ const key = (_target$decoration$sp = target.decoration.spec.attributionKey) !== null && _target$decoration$sp !== void 0 ? _target$decoration$sp : '';
172
+ byContributor.set(key, [...((_byContributor$get = byContributor.get(key)) !== null && _byContributor$get !== void 0 ? _byContributor$get : []), target]);
127
173
  }
128
- }) => {
174
+ for (const [leader, ...trailing] of byContributor.values()) {
175
+ trailing.forEach(target => fold(leader, target));
176
+ }
177
+ }
178
+ const surviving = targets.filter(target => !folded.has(target));
179
+ const activeTarget = activeIndexPos === undefined ? undefined : resolveActiveTarget(surviving, activeIndexPos);
180
+ return surviving.map(target => {
181
+ const {
182
+ contributor,
183
+ decoration: {
184
+ spec
185
+ }
186
+ } = target;
129
187
  const connected = contributor.connectedToKey ? contributors[contributor.connectedToKey] : undefined;
130
188
  const connectedContributor = connected ? toTagContributor(connected) : undefined;
131
189
  const linked = linkedDiffIds.get(spec.diffId);
190
+ // Only the navigated tag reveals; with no active range, the decoration's own state stands.
191
+ const isActive = activeIndexPos === undefined ? spec.isActive : target === activeTarget;
132
192
  return {
133
193
  contributor: toTagContributor(contributor),
134
194
  diffId: spec.diffId,
@@ -138,8 +198,8 @@ export const extractContributorTags = (decorations, contributors) => {
138
198
  ...(spec.colorScheme ? {
139
199
  colorScheme: spec.colorScheme
140
200
  } : {}),
141
- ...(spec.isActive !== undefined ? {
142
- isActive: spec.isActive
201
+ ...(isActive !== undefined ? {
202
+ isActive
143
203
  } : {}),
144
204
  ...(spec.isInserted !== undefined ? {
145
205
  isInserted: spec.isInserted
@@ -206,25 +206,25 @@ const applyMultiContainerLikeStyles = ({
206
206
  const targetNodeName = expValEquals('platform_editor_nest_table_in_panel', 'isEnabled', true) ? getBaseNodeTypeName(targetNode.type) : targetNode.type.name;
207
207
  const nodeSpecificStyle = getChangedNodeStyle(targetNodeName, colorScheme, isInserted, isActive, diffType, hideAddedDiffsUnderline) || '';
208
208
  if (targetNode.type.name === 'decisionList') {
209
- const nestedInsertedNodeStyle = resolveNestedInsertedNodeStyle();
209
+ const nestedInsertedNodeStyle = resolveNestedInsertedNodeStyle(colorScheme);
210
210
  element.querySelectorAll('li').forEach(listItem => {
211
211
  const currentListItemStyle = listItem.getAttribute('style') || '';
212
- listItem.setAttribute('style', `${currentListItemStyle}${nestedInsertedNodeStyle}`);
212
+ listItem.setAttribute('style', fg('platform_editor_ai_show_diff_patch_1') ? combineStyles(currentListItemStyle, nestedInsertedNodeStyle) : `${currentListItemStyle}${nestedInsertedNodeStyle}`);
213
213
  });
214
214
  } else if (targetNode.type.name === 'layoutSection') {
215
- const nestedInsertedNodeStyle = resolveNestedInsertedNodeStyle();
215
+ const nestedInsertedNodeStyle = resolveNestedInsertedNodeStyle(colorScheme);
216
216
  element.querySelectorAll('[data-layout-column="true"]').forEach(section => {
217
217
  const currentSectionStyle = section.getAttribute('style') || '';
218
- section.setAttribute('style', `${currentSectionStyle}${nestedInsertedNodeStyle}`);
218
+ section.setAttribute('style', fg('platform_editor_ai_show_diff_patch_1') ? combineStyles(currentSectionStyle, nestedInsertedNodeStyle) : `${currentSectionStyle}${nestedInsertedNodeStyle}`);
219
219
  });
220
220
  } else if (targetNode.type.name === 'taskList') {
221
- const nestedInsertedNodeStyle = resolveNestedInsertedNodeStyle();
221
+ const nestedInsertedNodeStyle = resolveNestedInsertedNodeStyle(colorScheme);
222
222
  element.querySelectorAll('li').forEach(listItem => {
223
223
  const currentListItemStyle = listItem.getAttribute('style') || '';
224
224
  listItem.setAttribute('style', `${currentListItemStyle}${nestedInsertedNodeStyle}`);
225
225
  });
226
226
  }
227
- element.setAttribute('style', `${currentStyle};${nodeSpecificStyle}`);
227
+ element.setAttribute('style', fg('platform_editor_ai_show_diff_patch_1') ? combineStyles(currentStyle, nodeSpecificStyle) : `${currentStyle};${nodeSpecificStyle}`);
228
228
  };
229
229
  const combineStyles = (currentStyle, appendedStyle) => {
230
230
  const separator = currentStyle && appendedStyle && !currentStyle.trimEnd().endsWith(';') ? '; ' : '';
@@ -11,6 +11,7 @@
11
11
  * keeping the `*Next` bodies.
12
12
  */
13
13
  import { isExperimentEnabled } from '@atlaskit/platform-feature-experiments/is-experiment-enabled';
14
+ import { fg } from '@atlaskit/platform-feature-flags/fg';
14
15
  import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
15
16
  import { isExtendedEnabled } from '../../isExtendedEnabled';
16
17
  import { buildAddedCellOverlayRoundedStyle, buildAddedCellOverlayStyle, buildAddedCellOverlayStyleNew, buildDeletedCellOverlayRoundedStyle, buildDeletedCellOverlayStyle, buildDeletedInlineContentStyle, buildDeletedInlineContentStyleExtended, buildDeletedLozengeActiveStyle, buildDeletedLozengeStyle, buildDeletedNodeCSSVariables, buildDeletedQuoteNodeWithLozengeStyle, buildDeletedStrikethroughLine, buildDeletedWrappedBlockOutline, buildInsertedBlockNodeStyle, buildInsertedInlineStyle, buildInsertStyleInBlockExtended, buildInsertStyleInBlockExtendedNoUnderline, buildInsertStyleNode } from '../colorSchemes/factory';
@@ -19,9 +20,7 @@ import { getChangedContentStyleLegacy, getChangedNodeStyleLegacy, getDeletedCont
19
20
  const getColorScheme = colorScheme => colorSchemeRegistry[colorScheme !== null && colorScheme !== void 0 ? colorScheme : 'standard'];
20
21
 
21
22
  /**
22
- * Inserted content inside a multi-container or list node always uses the standard scheme, so a
23
- * traditional diff shows purple here. Pre-existing: these call sites predate traditional.
24
- * Preserved as-is; see EDITOR-8281.
23
+ * Preserve the historical purple nested outlines when the layout/decision diff patch is off.
25
24
  */
26
25
  const nestedContentScheme = standardScheme;
27
26
 
@@ -49,7 +48,7 @@ const getChangedNodeStyleNext = (nodeName, colorScheme, isInserted = false, isAc
49
48
  const colors = getColorScheme(colorScheme);
50
49
  if (isExtendedEnabled(diffType) && isInserted) {
51
50
  if (isMultiContainerBlockNode(nodeName)) {
52
- return hideAddedDiffsUnderline ? buildInsertStyleInBlockExtendedNoUnderline(nestedContentScheme) : buildInsertStyleInBlockExtended(nestedContentScheme);
51
+ return hideAddedDiffsUnderline || fg('platform_editor_ai_show_diff_patch_1') ? buildInsertStyleInBlockExtendedNoUnderline(nestedContentScheme) : buildInsertStyleInBlockExtended(nestedContentScheme);
53
52
  }
54
53
  if (isTextLikeBlockNode(nodeName)) {
55
54
  return undefined;
@@ -101,7 +100,7 @@ const resolveRemovedLozengeStyleNext = (colorScheme, isActive) => {
101
100
  const colors = getColorScheme(colorScheme);
102
101
  return isActive ? buildDeletedLozengeActiveStyle(colors) : buildDeletedLozengeStyle(colors);
103
102
  };
104
- const resolveNestedInsertedNodeStyleNext = () => buildInsertStyleNode(nestedContentScheme);
103
+ const resolveNestedInsertedNodeStyleNext = colorScheme => buildInsertStyleNode(fg('platform_editor_ai_show_diff_patch_1') ? getColorScheme(colorScheme) : nestedContentScheme);
105
104
 
106
105
  // Only 'stateful' schemes have a resting ring.
107
106
  const hasRestingDeletedRingNext = colorScheme => getColorScheme(colorScheme).deletedNodeEmphasis === 'stateful';
@@ -138,10 +137,9 @@ export const resolveRemovedLozengeStyle = (colorScheme, isActive) => isExperimen
138
137
 
139
138
  /**
140
139
  * Style for inserted content nested inside a multi-container or list node — the `decisionList` and
141
- * `taskList` `li`s and the `layoutSection` columns. Scheme-independent: see
142
- * `nestedContentScheme` above.
140
+ * `taskList` `li`s and the `layoutSection` columns.
143
141
  */
144
- export const resolveNestedInsertedNodeStyle = () => isExperimentEnabled('platform_editor_show_diff_color_scheme_refactor') ? resolveNestedInsertedNodeStyleNext() : resolveNestedInsertedNodeStyleLegacy();
142
+ export const resolveNestedInsertedNodeStyle = colorScheme => isExperimentEnabled('platform_editor_show_diff_color_scheme_refactor') ? resolveNestedInsertedNodeStyleNext(colorScheme) : resolveNestedInsertedNodeStyleLegacy();
145
143
 
146
144
  /**
147
145
  * Whether the colour scheme draws a resting (non-active) ring on a deleted media/embed node, which
@@ -9,6 +9,7 @@
9
9
  * `colorSchemes/standard.ts`, `colorSchemes/traditional.ts` (EDITOR-8281).
10
10
  */
11
11
  import { convertToInlineCss } from '@atlaskit/editor-common/lazy-node-view';
12
+ import { fg } from '@atlaskit/platform-feature-flags/fg';
12
13
  import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
13
14
  import { isExtendedEnabled } from '../../isExtendedEnabled';
14
15
  import { deletedBlockOutline, deletedBlockOutlineActive, deletedBlockOutlineRounded, deletedBlockOutlineRoundedActive, deletedContentStyleUnbounded, deletedInlineContentStyleExtended, deletedStyleQuoteNodeWithLozenge, deletedStyleQuoteNodeWithLozengeActive, editingContentStyleInBlockExtended, editingContentStyleInBlockExtendedNoUnderline, editingStyleExtended, editingStyleExtendedNoUnderline, editingStyleActiveExtended, editingStyleActiveExtendedNoUnderline, editingStyleNode, getStandardDeletedContentStyle, getStandardDeletedContentStyleActive, getStandardDeletedContentStyleNew, addedCellOverlayStyle, addedCellOverlayRoundedStyle, deletedCellOverlayStyle, deletedCellOverlayRoundedStyle } from '../colorSchemes/standard';
@@ -97,7 +98,7 @@ export const getChangedNodeStyleLegacy = (nodeName, colorScheme, isInserted = fa
97
98
  const isTraditional = colorScheme === 'traditional';
98
99
  if (isExtendedEnabled(diffType) && isInserted) {
99
100
  if (isMultiContainerBlockNode(nodeName)) {
100
- return hideAddedDiffsUnderline ? editingContentStyleInBlockExtendedNoUnderline : editingContentStyleInBlockExtended;
101
+ return hideAddedDiffsUnderline || fg('platform_editor_ai_show_diff_patch_1') ? editingContentStyleInBlockExtendedNoUnderline : editingContentStyleInBlockExtended;
101
102
  }
102
103
  if (isTextLikeBlockNode(nodeName)) {
103
104
  return undefined;
@@ -41,6 +41,30 @@ export const CONTRIBUTOR_TAG_REVEALED_ATTRIBUTE = 'data-revealed';
41
41
  */
42
42
  export const TAG_EXIT_FALLBACK_MS = 1200;
43
43
 
44
+ /**
45
+ * The tooltip's look, inline rather than through `VANILLA_TOOLTIP_DEFAULT_CLASS`: this tooltip is
46
+ * hoisted out of the tag, and that class's rule is scoped under `.ProseMirror` — see
47
+ * `resolveTooltipContainer`. Otherwise `vanillaTooltipDefaultStyles` in editor-core, which is the
48
+ * look every other vanilla tooltip has; keep the two in step.
49
+ */
50
+ export const CONTRIBUTOR_TAG_TOOLTIP_STYLES = {
51
+ boxSizing: 'border-box',
52
+ maxWidth: '240px',
53
+ backgroundColor: "var(--ds-background-neutral-bold, #292A2E)",
54
+ // A `popover` is given one by the UA stylesheet.
55
+ border: 'none',
56
+ borderRadius: "var(--ds-radius-small, 3px)",
57
+ color: "var(--ds-text-inverse, #FFFFFF)",
58
+ font: "var(--ds-font-body-small, normal 400 12px/16px \"Atlassian Sans\", ui-sans-serif, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Ubuntu, \"Helvetica Neue\", sans-serif)",
59
+ fontFamily: "var(--ds-font-family-body, \"Atlassian Sans\", ui-sans-serif, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Ubuntu, \"Helvetica Neue\", sans-serif)",
60
+ overflowWrap: 'break-word',
61
+ paddingBlock: "var(--ds-space-050, 4px)",
62
+ paddingInline: "var(--ds-space-075, 6px)",
63
+ whiteSpace: 'normal',
64
+ // A tooltip is never a hit target — it hangs over the document's own content.
65
+ pointerEvents: 'none'
66
+ };
67
+
44
68
  // Positioned against the host widget its decoration renders on the change's first character:
45
69
  // `bottom: 100%` lifts the tag onto the line above, `inset-inline-start` starts it on that
46
70
  // character.
@@ -3,7 +3,7 @@ import { bind, bindAll } from 'bind-event-listener';
3
3
  import { VanillaTooltip } from '@atlaskit/editor-common/vanilla-tooltip';
4
4
  import { getAccentTokens } from '../../pm-plugins/decorations/colorSchemes/factory';
5
5
  import { colorSchemeRegistry } from '../../pm-plugins/decorations/colorSchemes/schemes';
6
- import { buildContributorTagDom, CONTRIBUTOR_TAG_REVEALED_ATTRIBUTE, TAG_EXIT_FALLBACK_MS } from './buildContributorTagDom';
6
+ import { buildContributorTagDom, CONTRIBUTOR_TAG_REVEALED_ATTRIBUTE, CONTRIBUTOR_TAG_TOOLTIP_STYLES, TAG_EXIT_FALLBACK_MS } from './buildContributorTagDom';
7
7
  import { contributorAvatarRenderer } from './contributorAvatarRenderer';
8
8
  import { formatContributorLabel } from './contributorLabel';
9
9
  /**
@@ -357,7 +357,7 @@ export class ContributorTagController {
357
357
  * shape of `syncVanillaDisabledTooltip` in `mentionNodeView`.
358
358
  */
359
359
  syncTooltip() {
360
- var _this$tooltip2, _this$dom$tag$querySe;
360
+ var _this$tooltip2;
361
361
  const content = this.dom ? this.fullLabel : undefined;
362
362
  if (content === this.tooltipContent) {
363
363
  return;
@@ -368,17 +368,37 @@ export class ContributorTagController {
368
368
  if (!content || !this.dom) {
369
369
  return;
370
370
  }
371
- this.tooltip = new VanillaTooltip(this.dom.tag, content, undefined,
372
- // `ak-editor-vanilla-tooltip-default` opts into the shared `VanillaTooltip` look, defined as
373
- // `vanillaTooltipDefaultStyles` in both EditorContentContainer stylesheets — keep in sync when
374
- // renaming. Those styles are scoped under `.ProseMirror`, and the tag renders inside it.
375
- 'ak-editor-vanilla-tooltip-default');
371
+ this.tooltip = new VanillaTooltip(this.dom.tag, content,
372
+ // Generated id.
373
+ undefined,
374
+ // No class: the look is inline, because a hoisted tooltip is outside the `.ProseMirror`
375
+ // scope `VANILLA_TOOLTIP_DEFAULT_CLASS` is keyed on — see `CONTRIBUTOR_TAG_TOOLTIP_STYLES`.
376
+ '',
377
+ // Default delay, no `onShow`, and the default `top` placement.
378
+ undefined, CONTRIBUTOR_TAG_TOOLTIP_STYLES, undefined, undefined, this.resolveTooltipContainer());
376
379
  // `VanillaTooltip` sets `aria-describedby` on its trigger, which would announce the label a
377
380
  // second time — the hidden child already announces it, and names the tag with it.
378
381
  this.dom.tag.removeAttribute('aria-describedby');
379
- // The popover is a child of the trigger, so its text would be a third copy of the same
380
- // sentence. It stays visible for sighted users, who need the part of the name the tag clipped.
381
- (_this$dom$tag$querySe = this.dom.tag.querySelector(':scope > [role="tooltip"]')) === null || _this$dom$tag$querySe === void 0 ? void 0 : _this$dom$tag$querySe.setAttribute('aria-hidden', 'true');
382
+ // Out of the tag but still in the document, so its text would otherwise be a third copy of the
383
+ // same sentence. It stays visible for sighted users, who need the part of the name the tag
384
+ // clipped.
385
+ this.tooltip.element.setAttribute('aria-hidden', 'true');
386
+ }
387
+
388
+ /**
389
+ * Where the tooltip is appended, rather than inside the tag.
390
+ *
391
+ * Popper and the browser only agree on a top-layer popover's origin when nothing above it is
392
+ * transformed, and the tag hangs under the editor's own transformed nodes — inside a wide image
393
+ * that is `.rich-media-item`, whose `translateX(-50%)` threw the tooltip off screen
394
+ * (EDITOR-8971). The content area is the nearest ancestor with none of that above it, and keeps
395
+ * the tooltip inside the editor it belongs to; the document body covers an appearance that has
396
+ * no content area.
397
+ */
398
+ resolveTooltipContainer() {
399
+ var _editorRoot$closest;
400
+ const editorRoot = this.options.getEditorRoot();
401
+ return (_editorRoot$closest = editorRoot === null || editorRoot === void 0 ? void 0 : editorRoot.closest('.ak-editor-content-area')) !== null && _editorRoot$closest !== void 0 ? _editorRoot$closest : editorRoot === null || editorRoot === void 0 ? void 0 : editorRoot.ownerDocument.body;
382
402
  }
383
403
 
384
404
  /**
@@ -25,10 +25,12 @@ import { extractDiffDescriptors } from '../decorations/decorationKeys';
25
25
  import { extractContributorTags } from '../decorations/extractContributorTags';
26
26
  import { getAttrChangeRanges, stepIsValidAttrChange } from '../decorations/utils/getAttrChangeRanges';
27
27
  import { getMarkChangeRanges } from '../decorations/utils/getMarkChangeRanges';
28
+ import { getScrollableDecorations } from '../getScrollableDecorations';
28
29
  import { getDefaultDiffType, isExtendedEnabled } from '../isExtendedEnabled';
29
30
  import { diffBySteps } from './diffBySteps';
30
31
  import { groupChangesByBlock } from './groupChangesByBlock';
31
32
  import { isMarkOnlyChange } from './isMarkOnlyChange';
33
+ import { isOpenTokenOnlyChange } from './isOpenTokenOnlyChange';
32
34
  import { optimizeChanges } from './optimizeChanges';
33
35
  import { selectTokenEncoder } from './selectTokenEncoder';
34
36
  import { collapseOverlappingChanges, simplifyChangesWithAttribution } from './simplifyChangesWithAttribution';
@@ -497,6 +499,16 @@ var calculateDiffDecorationsInner = function calculateDiffDecorationsInner(_ref8
497
499
  newDoc: tr.doc
498
500
  });
499
501
 
502
+ // The deleted side of a change over a node's open token is an attribute state rather than
503
+ // content, so there is nothing for the deleted-content widget to draw. Decided here, with
504
+ // the other deleted-side suppressions, so the widget is never asked for a slice it can only
505
+ // render as an empty copy of the block (EDITOR-8912). Its inserted side is untouched, and
506
+ // the node's content change is a change of its own.
507
+ var hasNoDeletedContent = fg('platform_editor_ai_show_diff_patch_1') && change.deleted.length > 0 && isOpenTokenOnlyChange({
508
+ change: change,
509
+ originalDoc: originalDoc
510
+ });
511
+
500
512
  // Hoisted because it decides BOTH where the deleted widget is anchored and — since the
501
513
  // widget pins whichever end of the range it sits at — how the indicator anchors below are
502
514
  // allowed to move.
@@ -615,7 +627,7 @@ var calculateDiffDecorationsInner = function calculateDiffDecorationsInner(_ref8
615
627
  coarseTableCellsOnly: useCoarseTableDecoration
616
628
  }))));
617
629
  }
618
- if (change.deleted.length > 0 && !isMarkOnly) {
630
+ if (change.deleted.length > 0 && !isMarkOnly && !hasNoDeletedContent) {
619
631
  var _shouldHideDeleted = shouldHideDeletedSide({
620
632
  change: change,
621
633
  diffType: diffType,
@@ -781,7 +793,10 @@ var calculateDiffDecorationsInner = function calculateDiffDecorationsInner(_ref8
781
793
  });
782
794
  var decorationSet = DecorationSet.empty.add(tr.doc, decorations);
783
795
  return {
784
- contributorTags: showContributorTags ? extractContributorTags(decorationSet, contributors) : [],
796
+ contributorTags: showContributorTags ? extractContributorTags(decorationSet, contributors, activeIndexPos,
797
+ // The stops the step buttons walk, so a stop cannot hold a second tag for one
798
+ // contributor that navigation can never reach.
799
+ getScrollableDecorations(decorationSet, tr.doc, diffType)) : [],
785
800
  decorations: decorationSet,
786
801
  diffDescriptors: extractDiffDescriptors(decorationSet)
787
802
  };
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Whether a change spans nothing but a block node's open token, so its deleted side carries no
3
+ * content.
4
+ *
5
+ * The attribute-aware token encoder folds a node's diffable attributes into its open token, so a
6
+ * same-type replacement — an AI suggestion rewriting a code block — is reported as two changes: one
7
+ * over the single position of that token, carrying the attribute change, and one for the content.
8
+ * Slicing the first yields the node with none of its content, which leaves the deleted-content
9
+ * widget nothing to draw but an empty copy of the block (EDITOR-8912). The content change is a
10
+ * change of its own and is decorated in place.
11
+ *
12
+ * A whole node is not matched: its range covers its close token too. Neither is an empty leaf (a
13
+ * `rule`, a `blockCard`), which is one position but slices closed rather than open.
14
+ */
15
+ export var isOpenTokenOnlyChange = function isOpenTokenOnlyChange(_ref) {
16
+ var change = _ref.change,
17
+ originalDoc = _ref.originalDoc;
18
+ if (change.toA - change.fromA !== 1) {
19
+ return false;
20
+ }
21
+ var slice = originalDoc.slice(change.fromA, change.toA);
22
+ // `childCount === 1` already means `firstChild` is there; the optional chain is for the type,
23
+ // which has it nullable.
24
+ var node = slice.content.firstChild;
25
+ return (slice.openStart > 0 || slice.openEnd > 0) && slice.content.childCount === 1 && (node === null || node === void 0 ? void 0 : node.isBlock) === true && node.content.size === 0;
26
+ };
@@ -45,6 +45,16 @@ var edgeCases = function edgeCases(doc, from) {
45
45
  var node = resolved.node,
46
46
  nodeStart = resolved.nodeStart,
47
47
  beforePos = resolved.beforePos;
48
+ if (node.type.name === 'layoutSection' && fg('platform_editor_ai_show_diff_patch_1')) {
49
+ // Columns extend past the node-view wrapper via negative margins (12px or 20px).
50
+ // Measure their container so the indicator stays outside the diff outline, including
51
+ // breakout layouts and after responsive resizing, without changing the column geometry.
52
+ return {
53
+ beforePos: beforePos,
54
+ measurePos: beforePos,
55
+ measureSelector: '[data-layout-section]'
56
+ };
57
+ }
48
58
 
49
59
  /**
50
60
  * All resizable nodes will need dynamic calculations of the block indicator left anchor
@@ -152,10 +162,12 @@ export var createLeftAnchorWidget = function createLeftAnchorWidget(_ref) {
152
162
  anchor.style.setProperty('transform', 'translateX(-50%)');
153
163
  wrapper.appendChild(anchor);
154
164
  var measureWidth = function measureWidth() {
165
+ var _nodeDOM$querySelecto;
155
166
  if (getPos() === undefined || edgeCase.measurePos === undefined) {
156
167
  return;
157
168
  }
158
- var dom = view.nodeDOM(edgeCase.measurePos);
169
+ var nodeDOM = view.nodeDOM(edgeCase.measurePos);
170
+ var dom = edgeCase.measureSelector && nodeDOM instanceof HTMLElement ? (_nodeDOM$querySelecto = nodeDOM.querySelector(edgeCase.measureSelector)) !== null && _nodeDOM$querySelecto !== void 0 ? _nodeDOM$querySelecto : nodeDOM : nodeDOM;
159
171
  if (dom instanceof HTMLElement) {
160
172
  // The left anchor only needs the container width so the
161
173
  // IndicatorBar can align against the block's horizontal extent.
@@ -42,7 +42,26 @@ export var mountContributorTag = function mountContributorTag(_ref) {
42
42
  diffId: diffId
43
43
  }));
44
44
  controller.mount(host);
45
- return controller;
45
+
46
+ /**
47
+ * Kept alive when ProseMirror only rebuilt the widget desc around this same host: a document
48
+ * change remaps the decoration and fires `destroy`, but an element `toDOM` is reused verbatim
49
+ * and runs no mount callback, so tearing down here left an active change with no tag
50
+ * (EDITOR-8971).
51
+ *
52
+ * Deferred, because the host is still attached while that update is in flight. A host outside a
53
+ * live editor root is a real removal — including the view being destroyed, which drops the root.
54
+ */
55
+ return {
56
+ destroy: function destroy() {
57
+ return queueMicrotask(function () {
58
+ var _mountContext$getEdit;
59
+ if (!host.isConnected || !((_mountContext$getEdit = mountContext.getEditorRoot()) !== null && _mountContext$getEdit !== void 0 && _mountContext$getEdit.contains(host))) {
60
+ controller.destroy();
61
+ }
62
+ });
63
+ }
64
+ };
46
65
  };
47
66
  export var unmountContributorTag = function unmountContributorTag(mount) {
48
67
  mount === null || mount === void 0 || mount.destroy();