@atlaskit/editor-plugin-show-diff 17.0.1 → 17.1.0

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 (75) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/cjs/entry-points/show-diff-plugin-type.js +12 -1
  3. package/dist/cjs/pm-plugins/NodeViewSerializer.js +1 -1
  4. package/dist/cjs/pm-plugins/calculateDiff/calculateDiffDecorations.js +128 -51
  5. package/dist/cjs/pm-plugins/calculateDiff/groupDeletedColumnChanges.js +265 -0
  6. package/dist/cjs/pm-plugins/decorations/colorSchemes/attributions.js +14 -3
  7. package/dist/cjs/pm-plugins/decorations/createBlockChangedDecoration.js +1 -1
  8. package/dist/cjs/pm-plugins/decorations/createChangedRowDecorationWidgets.js +100 -34
  9. package/dist/cjs/pm-plugins/decorations/createContributorTagWidget.js +18 -13
  10. package/dist/cjs/pm-plugins/decorations/createNodeChangedDecorationWidget.js +82 -20
  11. package/dist/cjs/pm-plugins/decorations/utils/createDeletedLineBreakWidget.js +106 -0
  12. package/dist/cjs/pm-plugins/decorations/utils/tableDiffMode.js +36 -0
  13. package/dist/cjs/pm-plugins/decorations/utils/wrapBlockNodeView.js +19 -8
  14. package/dist/cjs/pm-plugins/resolveDiffContributors.js +4 -3
  15. package/dist/cjs/pm-plugins/utils/emptyTextBlocks.js +66 -0
  16. package/dist/cjs/showDiffPluginType.js +87 -1
  17. package/dist/cjs/ui/ContributorTag/contributorAvatarRenderer.js +22 -14
  18. package/dist/cjs/ui/ContributorTag/contributorTagIcons.js +4 -0
  19. package/dist/es2019/entry-points/calculate-diff.js +1 -0
  20. package/dist/es2019/entry-points/show-diff-plugin-type.js +3 -0
  21. package/dist/es2019/entry-points/show-diff-plugin.js +1 -0
  22. package/dist/es2019/pm-plugins/NodeViewSerializer.js +1 -1
  23. package/dist/es2019/pm-plugins/calculateDiff/calculateDiffDecorations.js +63 -3
  24. package/dist/es2019/pm-plugins/calculateDiff/groupDeletedColumnChanges.js +210 -0
  25. package/dist/es2019/pm-plugins/decorations/colorSchemes/attributions.js +12 -3
  26. package/dist/es2019/pm-plugins/decorations/colorSchemes/factory.js +0 -1
  27. package/dist/es2019/pm-plugins/decorations/createBlockChangedDecoration.js +1 -1
  28. package/dist/es2019/pm-plugins/decorations/createChangedRowDecorationWidgets.js +79 -13
  29. package/dist/es2019/pm-plugins/decorations/createContributorTagWidget.js +8 -2
  30. package/dist/es2019/pm-plugins/decorations/createNodeChangedDecorationWidget.js +85 -15
  31. package/dist/es2019/pm-plugins/decorations/utils/createDeletedLineBreakWidget.js +100 -0
  32. package/dist/es2019/pm-plugins/decorations/utils/tableDiffMode.js +27 -0
  33. package/dist/es2019/pm-plugins/decorations/utils/wrapBlockNodeView.js +17 -7
  34. package/dist/es2019/pm-plugins/resolveDiffContributors.js +3 -1
  35. package/dist/es2019/pm-plugins/scrollToDiff.js +1 -0
  36. package/dist/es2019/pm-plugins/utils/emptyTextBlocks.js +58 -0
  37. package/dist/es2019/showDiffPluginType.js +85 -1
  38. package/dist/es2019/ui/ContributorTag/contributorAvatarRenderer.js +22 -14
  39. package/dist/es2019/ui/ContributorTag/contributorTagIcons.js +4 -0
  40. package/dist/esm/entry-points/calculate-diff.js +1 -0
  41. package/dist/esm/entry-points/show-diff-plugin-type.js +3 -0
  42. package/dist/esm/entry-points/show-diff-plugin.js +1 -0
  43. package/dist/esm/pm-plugins/NodeViewSerializer.js +1 -1
  44. package/dist/esm/pm-plugins/calculateDiff/calculateDiffDecorations.js +129 -52
  45. package/dist/esm/pm-plugins/calculateDiff/groupDeletedColumnChanges.js +258 -0
  46. package/dist/esm/pm-plugins/decorations/colorSchemes/attributions.js +14 -3
  47. package/dist/esm/pm-plugins/decorations/colorSchemes/factory.js +0 -1
  48. package/dist/esm/pm-plugins/decorations/createBlockChangedDecoration.js +1 -1
  49. package/dist/esm/pm-plugins/decorations/createChangedRowDecorationWidgets.js +100 -34
  50. package/dist/esm/pm-plugins/decorations/createContributorTagWidget.js +18 -13
  51. package/dist/esm/pm-plugins/decorations/createNodeChangedDecorationWidget.js +84 -22
  52. package/dist/esm/pm-plugins/decorations/utils/createDeletedLineBreakWidget.js +100 -0
  53. package/dist/esm/pm-plugins/decorations/utils/tableDiffMode.js +30 -0
  54. package/dist/esm/pm-plugins/decorations/utils/wrapBlockNodeView.js +18 -7
  55. package/dist/esm/pm-plugins/resolveDiffContributors.js +4 -3
  56. package/dist/esm/pm-plugins/scrollToDiff.js +1 -0
  57. package/dist/esm/pm-plugins/utils/emptyTextBlocks.js +60 -0
  58. package/dist/esm/showDiffPluginType.js +85 -1
  59. package/dist/esm/ui/ContributorTag/contributorAvatarRenderer.js +22 -14
  60. package/dist/esm/ui/ContributorTag/contributorTagIcons.js +4 -0
  61. package/dist/types/entry-points/show-diff-plugin-type.d.ts +2 -1
  62. package/dist/types/pm-plugins/calculateDiff/groupDeletedColumnChanges.d.ts +9 -0
  63. package/dist/types/pm-plugins/decorations/colorSchemes/attributions.d.ts +2 -2
  64. package/dist/types/pm-plugins/decorations/createChangedRowDecorationWidgets.d.ts +3 -1
  65. package/dist/types/pm-plugins/decorations/createContributorTagWidget.d.ts +5 -1
  66. package/dist/types/pm-plugins/decorations/createNodeChangedDecorationWidget.d.ts +3 -2
  67. package/dist/types/pm-plugins/decorations/utils/createDeletedLineBreakWidget.d.ts +50 -0
  68. package/dist/types/pm-plugins/decorations/utils/getAttrChangeRanges.d.ts +1 -1
  69. package/dist/types/pm-plugins/decorations/utils/getMarkChangeRanges.d.ts +1 -1
  70. package/dist/types/pm-plugins/decorations/utils/tableDiffMode.d.ts +26 -0
  71. package/dist/types/pm-plugins/decorations/utils/wrapBlockNodeView.d.ts +4 -0
  72. package/dist/types/pm-plugins/utils/emptyTextBlocks.d.ts +16 -0
  73. package/dist/types/showDiffPluginType.d.ts +6 -1
  74. package/dist/types/ui/ContributorTag/contributorTagIcons.d.ts +1 -1
  75. package/package.json +4 -4
@@ -0,0 +1,210 @@
1
+ import { areNodesEqualIgnoreAttrs } from '@atlaskit/editor-common/utils/document';
2
+ import { Fragment, Slice } from '@atlaskit/editor-prosemirror/model';
3
+ import { TableMap } from '@atlaskit/editor-tables/table-map';
4
+ export const CELL_CONTENT_OFFSET = 2;
5
+ const IGNORED_CELL_ATTRS = ['localId', 'colwidth'];
6
+ const tablesIn = doc => {
7
+ const tables = [];
8
+ doc.descendants((node, pos) => {
9
+ if (node.type.name === 'table') {
10
+ tables.push({
11
+ node,
12
+ pos
13
+ });
14
+ return false;
15
+ }
16
+ return true;
17
+ });
18
+ return tables;
19
+ };
20
+ const tableEnd = table => table.pos + table.node.nodeSize;
21
+ const documentsDifferOnlyByTable = (originalDoc, newDoc, before, after) => {
22
+ try {
23
+ return originalDoc.replace(before.pos, tableEnd(before), new Slice(Fragment.from(after.node), 0, 0)).eq(newDoc);
24
+ } catch {
25
+ return false;
26
+ }
27
+ };
28
+ const columnOffsets = (map, table) => Array.from({
29
+ length: map.width
30
+ }, (_, column) => map.positionAt(0, column, table));
31
+ const positionMatchedColumns = ({
32
+ before,
33
+ after,
34
+ afterToBefore,
35
+ beforeOffsets
36
+ }) => {
37
+ const afterMap = TableMap.get(after.node);
38
+ const surviving = new Set();
39
+ for (let column = 0; column < afterMap.width; column++) {
40
+ const offset = afterMap.positionAt(0, column, after.node);
41
+ const mapped = afterToBefore.mapResult(after.pos + CELL_CONTENT_OFFSET + offset);
42
+ const beforeColumn = beforeOffsets.indexOf(mapped.pos - before.pos - CELL_CONTENT_OFFSET);
43
+ if (mapped.deleted || beforeColumn === -1) {
44
+ return new Set();
45
+ }
46
+ surviving.add(beforeColumn);
47
+ }
48
+ return surviving;
49
+ };
50
+ const addContentMatchedColumns = (surviving, before, after) => {
51
+ var _before$node$firstChi, _before$node$firstChi2, _after$node$firstChil, _after$node$firstChil2;
52
+ const beforeCells = (_before$node$firstChi = (_before$node$firstChi2 = before.node.firstChild) === null || _before$node$firstChi2 === void 0 ? void 0 : _before$node$firstChi2.children) !== null && _before$node$firstChi !== void 0 ? _before$node$firstChi : [];
53
+ const afterCells = (_after$node$firstChil = (_after$node$firstChil2 = after.node.firstChild) === null || _after$node$firstChil2 === void 0 ? void 0 : _after$node$firstChil2.children) !== null && _after$node$firstChil !== void 0 ? _after$node$firstChil : [];
54
+ for (const afterCell of afterCells) {
55
+ const matches = beforeCells.flatMap((beforeCell, column) => areNodesEqualIgnoreAttrs(beforeCell, afterCell, IGNORED_CELL_ATTRS) ? [column] : []);
56
+ if (matches.length !== 1) {
57
+ return false;
58
+ }
59
+ surviving.add(matches[0]);
60
+ }
61
+ const ordered = [...surviving];
62
+ return !ordered.some((column, index) => index > 0 && column <= ordered[index - 1]);
63
+ };
64
+ const findDeletedColumns = (before, after, afterToBefore) => {
65
+ const beforeMap = TableMap.get(before.node);
66
+ const afterMap = TableMap.get(after.node);
67
+ // Merged or malformed tables do not have reliable column indices.
68
+ if (beforeMap.hasMergedCells() || afterMap.hasMergedCells() || beforeMap.problems || afterMap.problems) {
69
+ return [];
70
+ }
71
+ const beforeOffsets = columnOffsets(beforeMap, before.node);
72
+ const surviving = positionMatchedColumns({
73
+ before,
74
+ after,
75
+ afterToBefore,
76
+ beforeOffsets
77
+ });
78
+ if (surviving.size !== afterMap.width && !addContentMatchedColumns(surviving, before, after)) {
79
+ return [];
80
+ }
81
+ return beforeOffsets.flatMap((_, column) => surviving.has(column) ? [] : [column]);
82
+ };
83
+ const findCounterpart = ({
84
+ table,
85
+ index,
86
+ originalTables,
87
+ newTables,
88
+ mapping
89
+ }) => {
90
+ const from = mapping.map(table.pos, -1);
91
+ const to = mapping.map(tableEnd(table), 1);
92
+ const inside = mapping.mapResult(table.pos + 1);
93
+ const candidates = newTables.filter(candidate => inside.deleted ? candidate.pos >= from && tableEnd(candidate) <= to : candidate.pos < inside.pos && inside.pos < tableEnd(candidate));
94
+ if (candidates.length === 1) {
95
+ return candidates[0];
96
+ }
97
+ return originalTables.length === newTables.length ? newTables[index] : undefined;
98
+ };
99
+ const orient = ({
100
+ table,
101
+ counterpart,
102
+ mapping,
103
+ isInverted
104
+ }) => ({
105
+ before: isInverted ? counterpart : table,
106
+ after: isInverted ? table : counterpart,
107
+ afterToBefore: (isInverted ? mapping.invert() : mapping).invert()
108
+ });
109
+ const changesInTable = (changes, table) => changes.filter(change => change.fromA < tableEnd(table) && change.toA > table.pos);
110
+ const isNarrowingSafe = ({
111
+ replacedChanges,
112
+ table,
113
+ counterpart,
114
+ originalDoc,
115
+ newDoc
116
+ }) => {
117
+ const staysInside = replacedChanges.every(change => change.fromA >= table.pos && change.toA <= tableEnd(table) && change.fromB >= counterpart.pos && change.toB <= tableEnd(counterpart));
118
+ return staysInside || documentsDifferOnlyByTable(originalDoc, newDoc, table, counterpart);
119
+ };
120
+ const planTableChange = ({
121
+ table,
122
+ index,
123
+ originalTables,
124
+ newTables,
125
+ changes,
126
+ originalDoc,
127
+ newDoc,
128
+ mapping,
129
+ isInverted
130
+ }) => {
131
+ const counterpart = findCounterpart({
132
+ table,
133
+ index,
134
+ originalTables,
135
+ newTables,
136
+ mapping
137
+ });
138
+ if (!counterpart) {
139
+ return undefined;
140
+ }
141
+ const {
142
+ before,
143
+ after,
144
+ afterToBefore
145
+ } = orient({
146
+ table,
147
+ counterpart,
148
+ mapping,
149
+ isInverted
150
+ });
151
+ if (TableMap.get(before.node).width <= TableMap.get(after.node).width) {
152
+ return undefined;
153
+ }
154
+ const replacedChanges = changesInTable(changes, table);
155
+ if (!replacedChanges.length) {
156
+ return undefined;
157
+ }
158
+ if (!isNarrowingSafe({
159
+ replacedChanges,
160
+ table,
161
+ counterpart,
162
+ originalDoc,
163
+ newDoc
164
+ })) {
165
+ return undefined;
166
+ }
167
+ const spans = replacedChanges.flatMap(change => [...change.deleted, ...change.inserted]);
168
+ return {
169
+ replacedChanges,
170
+ tableChange: {
171
+ deletedColumns: findDeletedColumns(before, after, afterToBefore),
172
+ fromA: table.pos,
173
+ toA: tableEnd(table),
174
+ fromB: counterpart.pos,
175
+ toB: tableEnd(counterpart),
176
+ deleted: spans,
177
+ inserted: spans
178
+ }
179
+ };
180
+ };
181
+
182
+ /** Groups a deleted column's cell changes into one whole-table change. */
183
+ export const groupDeletedColumnChanges = (changes, originalDoc, newDoc, mapping, isInverted) => {
184
+ const originalTables = tablesIn(originalDoc);
185
+ const newTables = tablesIn(newDoc);
186
+ const plans = [];
187
+ const replaced = new Set();
188
+ originalTables.forEach((table, index) => {
189
+ const plan = planTableChange({
190
+ table,
191
+ index,
192
+ originalTables,
193
+ newTables,
194
+ changes: changes.filter(change => !replaced.has(change)),
195
+ originalDoc,
196
+ newDoc,
197
+ mapping,
198
+ isInverted
199
+ });
200
+ if (!plan) {
201
+ return;
202
+ }
203
+ plans.push(plan);
204
+ plan.replacedChanges.forEach(change => replaced.add(change));
205
+ });
206
+ if (!plans.length) {
207
+ return [...changes];
208
+ }
209
+ return [...changes.filter(change => !replaced.has(change)), ...plans.map(plan => plan.tableChange)].sort((left, right) => left.fromB - right.fromB);
210
+ };
@@ -1,6 +1,7 @@
1
1
  import { getParticipantColor } from '@atlaskit/editor-shared-styles/utils';
2
2
  import { isExperimentEnabled } from '@atlaskit/platform-feature-experiments/is-experiment-enabled';
3
3
  import { fg } from '@atlaskit/platform-feature-flags/fg';
4
+ import { DIFF_AGENT_BRANDS } from '../../../showDiffPluginType';
4
5
  import { PARTICIPANT_COLOR_SCHEMES } from './types';
5
6
 
6
7
  /** A contributor with both of its identities collapsed to internal attribution keys. */
@@ -77,6 +78,14 @@ export const getAttributionKey = attribution => {
77
78
  var _getAttributionIdenti;
78
79
  return (_getAttributionIdenti = getAttributionIdentity(attribution)) === null || _getAttributionIdenti === void 0 ? void 0 : _getAttributionIdenti.key;
79
80
  };
81
+ const isDiffAgentBrand = agentBrand => agentBrand !== undefined && DIFF_AGENT_BRANDS.has(agentBrand);
82
+
83
+ /** Prefers the resolved branded profile over the raw attribution agent type. */
84
+ const getAgentBrandForColor = (attribution, contributors) => {
85
+ var _contributors, _getAttributionKey;
86
+ const agentBrand = contributors === null || contributors === void 0 ? void 0 : (_contributors = contributors[(_getAttributionKey = getAttributionKey(attribution)) !== null && _getAttributionKey !== void 0 ? _getAttributionKey : '']) === null || _contributors === void 0 ? void 0 : _contributors.agentKind;
87
+ return isDiffAgentBrand(agentBrand) ? agentBrand : attribution === null || attribution === void 0 ? void 0 : attribution.agentType;
88
+ };
80
89
 
81
90
  /**
82
91
  * Public contributor list to internal key-addressed record. A contributor whose attribution carries
@@ -123,7 +132,7 @@ export const isAttributionColoringEnabled = stepAttributions => {
123
132
  * a tag only has to name one.
124
133
  */
125
134
  export const isContributorTagsEnabled = (stepAttributions, contributors) => areAttributionColorGatesEnabled() && Boolean(stepAttributions === null || stepAttributions === void 0 ? void 0 : stepAttributions.some(attribution => getAttributionKey(attribution) !== undefined)) && contributors !== undefined && Object.keys(contributors).length > 0;
126
- export const createAttributionColorMap = stepAttributions => {
135
+ export const createAttributionColorMap = (stepAttributions, contributors) => {
127
136
  const colors = new Map();
128
137
  const allocatedColors = new Set();
129
138
 
@@ -137,7 +146,7 @@ export const createAttributionColorMap = stepAttributions => {
137
146
  const {
138
147
  index,
139
148
  isFixed
140
- } = getParticipantColor(identity.colorSeed, attribution === null || attribution === void 0 ? void 0 : attribution.agentType);
149
+ } = getParticipantColor(identity.colorSeed, getAgentBrandForColor(attribution, contributors));
141
150
  const color = HASHED_PARTICIPANT_COLOR_SCHEMES[index];
142
151
  if (isFixed && color) {
143
152
  colors.set(identity.key, color);
@@ -151,7 +160,7 @@ export const createAttributionColorMap = stepAttributions => {
151
160
  }
152
161
  const {
153
162
  index
154
- } = getParticipantColor(identity.colorSeed, attribution === null || attribution === void 0 ? void 0 : attribution.agentType);
163
+ } = getParticipantColor(identity.colorSeed, getAgentBrandForColor(attribution, contributors));
155
164
  const hashedColor = getSlotColor(index);
156
165
  if (!hashedColor) {
157
166
  continue;
@@ -1,7 +1,6 @@
1
1
  import { agentBrandColorSchemes } from '@atlaskit/agent-color/agent-brand-color-schemes';
2
2
  import { convertToInlineCss } from '@atlaskit/editor-common/lazy-node-view';
3
3
  import { getStandardDeletedTextDecorationStyle } from './getStandardDeletedTextDecorationStyle';
4
-
5
4
  /**
6
5
  * Hover highlight for deleted text under an 'onEmphasis' scheme. Inline styles cannot express
7
6
  * `:hover`, so the resting style resolves its background from this property and
@@ -8,9 +8,9 @@ import { isTaggableBlockNode } from '../utils/taggableBlockNodes';
8
8
  import { buildAddedCellOverlayRoundedStyle, buildAddedCellOverlayStyle, buildDeletedBlockNodeStyle, buildDeletedCellOverlayRoundedStyle, buildDeletedCellOverlayStyle, buildInsertedBlockNodeStyle } from './colorSchemes/factory';
9
9
  import { colorSchemeRegistry, getLegacyColorScheme } from './colorSchemes/schemes';
10
10
  import { createBlockIndicatorAnchorWidgets } from './createAnchorDecorationWidgets';
11
+ import { getBlockNodeStyleLegacy, resolveCellOverlayStyleLegacy } from './createBlockChangedDecoration.styles.legacy';
11
12
  import { createContributorTagWidget, isContributorTagWidgetEnabled } from './createContributorTagWidget';
12
13
  import { AnchorTypeKey, buildDiffDecorationSpec, buildAnchorDecorationKey, scrollMarginTopStyle } from './decorationKeys';
13
- import { getBlockNodeStyleLegacy, resolveCellOverlayStyleLegacy } from './createBlockChangedDecoration.styles.legacy';
14
14
  const displayNoneStyle = convertToInlineCss({
15
15
  display: 'none'
16
16
  });
@@ -14,6 +14,7 @@ import { createContributorTagWidget, isContributorTagWidgetEnabled } from './cre
14
14
  import { AnchorTypeKey, buildAnchorDecorationKey, buildDiffDecorationSpec, scrollMarginTopValue } from './decorationKeys';
15
15
  import { findSafeInsertPos } from './utils/findSafeInsertPos';
16
16
  import { applyCellEdgeAttrs, getRowCellEdgeAttrs, getTableTopAndBottomCellEdgeAttrs } from './utils/tableCellEdgeAttrs';
17
+ import { createRemovedLozenge } from './utils/wrapBlockNodeView';
17
18
  /**
18
19
  * Extracts information about deleted table rows from a change
19
20
  */
@@ -144,17 +145,28 @@ const isEmptyRow = rowNode => {
144
145
  });
145
146
  return isEmpty;
146
147
  };
147
-
148
148
  /**
149
149
  * Creates a DOM representation of a deleted table row
150
150
  */
151
- const createChangedRowDOM = (rowNode, cellEdgeAttrs, nodeViewSerializer, colorScheme, isInserted, diffType) => {
151
+ const createChangedRowDOM = ({
152
+ rowNode,
153
+ cellEdgeAttrs,
154
+ nodeViewSerializer,
155
+ colorScheme,
156
+ isInserted,
157
+ diffType,
158
+ hasAnchoredRemovedLozenge,
159
+ intl,
160
+ isActive
161
+ }) => {
152
162
  const tr = document.createElement('tr');
153
163
  const colors = colorSchemeRegistry[colorScheme !== null && colorScheme !== void 0 ? colorScheme : 'standard'];
164
+ const deletedTreatment = isExperimentEnabled('platform_editor_show_diff_color_scheme_refactor') ? buildDeletedRowStyle(colors) : resolveDeletedRowStyleLegacy(getLegacyColorScheme(colorScheme));
165
+ const hostsRemovedLozenge = Boolean(intl) && isExtendedEnabled(diffType) && !isInserted && !hasAnchoredRemovedLozenge;
154
166
 
155
167
  // Inserted rows keep their natural styling; the row strikethrough is deletions only.
156
168
  if (!isExtendedEnabled(diffType) || !isInserted) {
157
- tr.setAttribute('style', isExperimentEnabled('platform_editor_show_diff_color_scheme_refactor') ? buildDeletedRowStyle(colors) : resolveDeletedRowStyleLegacy(getLegacyColorScheme(colorScheme)));
169
+ tr.setAttribute('style', deletedTreatment);
158
170
  }
159
171
  // Mirrors the strikethrough condition above: under the extended experience an `isInserted` row
160
172
  // is ADDED content, so it must not claim the "deleted" testid — page models match that as
@@ -195,8 +207,40 @@ const createChangedRowDOM = (rowNode, cellEdgeAttrs, nodeViewSerializer, colorSc
195
207
  cellIndex++;
196
208
  }
197
209
  });
210
+
211
+ // A "Removed" label on the row's last cell, so it reads at the row's top right corner. Deletions
212
+ // only: an inserted row is added content and carries no removal label.
213
+ if (intl && hostsRemovedLozenge) {
214
+ const lastCell = tr.lastElementChild;
215
+ if (lastCell instanceof HTMLElement) {
216
+ lastCell.style.position = 'relative';
217
+ // Append, never prepend. Editor CSS resets the top margin of a cell's first child, so a
218
+ // prepended label pushes the paragraph out of that reset and the row grows taller.
219
+ lastCell.append(createRemovedLozenge(intl, isActive, colorScheme, true));
220
+ }
221
+ }
198
222
  return tr;
199
223
  };
224
+ const supportsAnchorPositioning = () => typeof CSS !== 'undefined' && typeof CSS.supports === 'function' && CSS.supports('top', 'anchor(--a top)');
225
+ const createAnchoredRemovedLozengeWidget = ({
226
+ anchorName,
227
+ colorScheme,
228
+ from,
229
+ intl,
230
+ isActive
231
+ }) => {
232
+ const lozenge = createRemovedLozenge(intl, isActive, colorScheme, true);
233
+ const inset = lozenge.style.top;
234
+ lozenge.style.setProperty('position', 'fixed');
235
+ lozenge.style.setProperty('top', `calc(anchor(--${anchorName} top) + ${inset})`);
236
+ lozenge.style.setProperty('left', `calc(anchor(--${anchorName} right) - ${inset})`);
237
+ lozenge.style.setProperty('right', 'auto');
238
+ lozenge.style.setProperty('transform', 'translateX(-100%)');
239
+ return Decoration.widget(from, lozenge, {
240
+ key: `removed-lozenge-${anchorName}`,
241
+ side: -1
242
+ });
243
+ };
200
244
 
201
245
  /**
202
246
  * Expands a diff to include whole changed rows when table rows are affected
@@ -236,6 +280,7 @@ export const createChangedRowDecorationWidgets = ({
236
280
  isActive,
237
281
  isInserted = false,
238
282
  diffType,
283
+ intl,
239
284
  showIndicators = false,
240
285
  showContributorTags = false,
241
286
  tagMountContext
@@ -248,16 +293,25 @@ export const createChangedRowDecorationWidgets = ({
248
293
  diffType
249
294
  });
250
295
  return changedRows.flatMap(changedRow => {
251
- const rowDOM = createChangedRowDOM(changedRow.rowNode, changedRow.cellEdgeAttrs, nodeViewSerializer, colorScheme, isInserted, diffType);
296
+ // Keep the ID stable while contributor tags are enabled so recalculation preserves their state.
297
+ const diffId = showContributorTags ? `widget-row-${changedRow.fromA}-${changedRow.toA}` : crypto.randomUUID();
298
+ const hasAnchoredRemovedLozenge = Boolean(intl) && isExtendedEnabled(diffType) && !isInserted && supportsAnchorPositioning();
299
+ const rowDOM = createChangedRowDOM({
300
+ rowNode: changedRow.rowNode,
301
+ cellEdgeAttrs: changedRow.cellEdgeAttrs,
302
+ nodeViewSerializer,
303
+ colorScheme,
304
+ isInserted,
305
+ diffType,
306
+ hasAnchoredRemovedLozenge,
307
+ intl,
308
+ isActive
309
+ });
252
310
 
253
311
  // Find safe insertion position for the deleted row
254
312
  const safeInsertPos = findSafeInsertPos(newDoc, changedRow.fromB - 1,
255
313
  // -1 to find the first safe position from the table
256
314
  originalDoc.slice(changedRow.fromA, changedRow.toA));
257
-
258
- // Stable while contributor tags are enabled so a decoration recalculation preserves the tag's
259
- // identity and controller state.
260
- const diffId = showContributorTags ? `widget-row-${changedRow.fromA}-${changedRow.toA}` : crypto.randomUUID();
261
315
  const decorations = [];
262
316
  const rowAnchorNames = [];
263
317
 
@@ -286,6 +340,11 @@ export const createChangedRowDecorationWidgets = ({
286
340
  diffId,
287
341
  anchorType: AnchorTypeKey.tag
288
342
  }) : undefined;
343
+ const removedLozengeAnchorName = hasAnchoredRemovedLozenge ? tagAnchorName !== null && tagAnchorName !== void 0 ? tagAnchorName : buildAnchorDecorationKey({
344
+ diffId,
345
+ anchorType: AnchorTypeKey.tag
346
+ }) : undefined;
347
+ const rowLabelAnchorName = tagAnchorName !== null && tagAnchorName !== void 0 ? tagAnchorName : removedLozengeAnchorName;
289
348
  const tagWidget = tagAnchorName ? createContributorTagWidget({
290
349
  anchorAtRangeStart: true,
291
350
  anchorName: tagAnchorName,
@@ -295,11 +354,9 @@ export const createChangedRowDecorationWidgets = ({
295
354
  mountContext: tagMountContext,
296
355
  to: safeInsertPos
297
356
  }) : undefined;
298
- if (tagAnchorName) {
299
- // The tag host is a separate, table-safe widget. CSS anchor positioning aligns it to this
300
- // synthetic row without placing non-cell DOM inside the `<tr>` or inheriting its deletion
301
- // opacity and strikethrough.
302
- rowAnchorNames.push(tagAnchorName);
357
+ if (rowLabelAnchorName) {
358
+ // Labels stay outside the table row so they do not inherit its deletion treatment.
359
+ rowAnchorNames.push(rowLabelAnchorName);
303
360
  }
304
361
  if (rowAnchorNames.length > 0) {
305
362
  rowDOM.style.setProperty('anchor-name', rowAnchorNames.map(anchorName => `--${anchorName}`).join(', '));
@@ -320,6 +377,15 @@ export const createChangedRowDecorationWidgets = ({
320
377
  })
321
378
  });
322
379
  decorations.push(rowWidget);
380
+ if (intl && removedLozengeAnchorName) {
381
+ decorations.push(createAnchoredRemovedLozengeWidget({
382
+ anchorName: removedLozengeAnchorName,
383
+ colorScheme,
384
+ from: safeInsertPos,
385
+ intl,
386
+ isActive
387
+ }));
388
+ }
323
389
  if (tagWidget) {
324
390
  decorations.push(tagWidget);
325
391
  }
@@ -88,14 +88,20 @@ const supportsAnchorPositioning = () => typeof CSS !== 'undefined' && typeof CSS
88
88
  * The host for a tag on deleted content, which renders inside its own widget decoration rather than
89
89
  * needing one of its own. Unmount with `unmountContributorTag` from that widget's `destroy`.
90
90
  */
91
- export const createContributorTagHost = (diffId, mountContext) => {
91
+ export const createContributorTagHost = ({
92
+ anchorName,
93
+ diffId,
94
+ mountContext
95
+ }) => {
92
96
  if (!isContributorTagWidgetEnabled()) {
93
97
  return undefined;
94
98
  }
95
- const host = buildContributorTagHost(diffId);
99
+ const tagAnchorName = anchorName && supportsAnchorPositioning() ? anchorName : undefined;
100
+ const host = buildContributorTagHost(diffId, tagAnchorName);
96
101
  return {
97
102
  host,
98
103
  mount: mountContributorTag({
104
+ anchorName: tagAnchorName,
99
105
  diffId,
100
106
  host,
101
107
  mountContext
@@ -1,17 +1,20 @@
1
1
  import { Decoration } from '@atlaskit/editor-prosemirror/view';
2
2
  import { fg } from '@atlaskit/platform-feature-flags/fg';
3
3
  import { isExtendedEnabled } from '../isExtendedEnabled';
4
+ import { countEmptyTextBlockOnlySlice } from '../utils/emptyTextBlocks';
4
5
  import { isEmptyParagraphSlice } from '../utils/isEmptyParagraphSlice';
5
6
  import { createLeftAnchorWidget } from './createAnchorDecorationWidgets';
6
7
  import { createChangedRowDecorationWidgets } from './createChangedRowDecorationWidgets';
7
8
  import { createContributorTagHost, unmountContributorTag } from './createContributorTagWidget';
8
- import { buildDiffDecorationSpec, buildAnchorDecorationKey, scrollMarginTopValue } from './decorationKeys';
9
+ import { AnchorTypeKey, buildDiffDecorationSpec, buildAnchorDecorationKey, scrollMarginTopValue } from './decorationKeys';
9
10
  import { absorbFirstChildMarginReset } from './utils/absorbFirstChildMarginReset';
11
+ import { createDeletedLineBreakDecoration } from './utils/createDeletedLineBreakWidget';
10
12
  import { createMarginAbsorber } from './utils/createMarginAbsorber';
11
13
  import { createNodeShapedMarginSpacer } from './utils/createNodeShapedMarginSpacer';
12
14
  import { findSafeInsertPos } from './utils/findSafeInsertPos';
13
15
  import { safeResolve } from './utils/safeResolve';
14
- import { wrapBlockNodeView, injectInnerWrapper, createContentWrapper } from './utils/wrapBlockNodeView';
16
+ import { getTableDiffMode } from './utils/tableDiffMode';
17
+ import { wrapBlockNodeView, createRemovedLozenge, injectInnerWrapper, createContentWrapper } from './utils/wrapBlockNodeView';
15
18
  const isHeadingLevel = level => typeof level === 'number' && level >= 1 && level <= 6;
16
19
 
17
20
  /**
@@ -119,11 +122,20 @@ export const createNodeChangedDecorationWidget = ({
119
122
  diffType,
120
123
  hideAddedDiffsUnderline = false,
121
124
  reveal,
122
- tagMountContext
125
+ tagMountContext,
126
+ deletedColumns
123
127
  }) => {
124
128
  var _$safeInsertPos$nodeB, _slice$content$firstC, _newDoc$firstChild, _slice$content$firstC2;
125
129
  const slice = doc.slice(change.fromA, change.toA);
126
130
  const shouldSkipDeletedEmptyParagraphDecoration = !isInserted && isEmptyParagraphSlice(slice);
131
+ // How many blank lines the deletion took, when it took nothing else. Blank means an empty
132
+ // paragraph or heading — both are a line the author put there deliberately, and neither leaves
133
+ // the widget any content to serialize.
134
+ //
135
+ // Kept separate from `shouldSkipDeletedEmptyParagraphDecoration` and gated, so the ungated early
136
+ // return below still covers only the single-empty-paragraph case it always did. Everything else
137
+ // degrades to what it rendered before the gate: an empty block, serialized invisibly.
138
+ const deletedBlankLineCount = !isInserted && fg('platform_editor_ai_show_diff_patch_2') ? countEmptyTextBlockOnlySlice(slice) : 0;
127
139
  // Widget decoration used for deletions as the content is not in the document
128
140
  // and we want to display the deleted content with a style.
129
141
  // For `placeBelow`, anchor at the END of the new content (change.toB) so the deleted
@@ -131,16 +143,33 @@ export const createNodeChangedDecorationWidget = ({
131
143
  const anchorPos = placeBelow ? change.toB : change.fromB;
132
144
  const safeInsertPos = findSafeInsertPos(newDoc, anchorPos, slice);
133
145
  const isActive = activeIndexPos && (fg('confluence_ncs_step_diffing_version_history') ? safeInsertPos >= activeIndexPos.from && safeInsertPos <= activeIndexPos.to : safeInsertPos === activeIndexPos.from && safeInsertPos === activeIndexPos.to);
134
- if (slice.content.content.length === 0 || shouldSkipDeletedEmptyParagraphDecoration) {
146
+ if (deletedBlankLineCount > 0) {
147
+ // The blocks have no content to serialize, so the deletion is shown as one struck-through
148
+ // return glyph per line the author removed. A single widget, not one per line: the lines are
149
+ // gone from `newDoc`, so they all resolve to the same anchor and separate widgets would stack.
150
+ //
151
+ // No attribution: the glyph is not a tag host, matching `canTagWidget` below.
152
+ return [createDeletedLineBreakDecoration({
153
+ colorScheme,
154
+ count: deletedBlankLineCount,
155
+ diffType,
156
+ isActive,
157
+ pos: safeInsertPos,
158
+ reveal,
159
+ side: placeBelow ? 1 : -1
160
+ })];
161
+ }
162
+ if (shouldSkipDeletedEmptyParagraphDecoration) {
135
163
  return [];
136
164
  }
137
- const isTableCellContent = slice.content.content.some(() => slice.content.content.some(siblingNode => ['tableHeader', 'tableCell'].includes(siblingNode.type.name)));
138
- const isTableRowContent = slice.content.content.some(() => slice.content.content.some(siblingNode => ['tableRow'].includes(siblingNode.type.name)));
139
-
140
- // Added whole cells (e.g. "Add table headers"): render per-cell content widgets.
141
- // Deleted whole cells (delete row/col) fall through — they don't exist in `newDoc`,
142
- // so there's nowhere to anchor content and we keep the original "render nothing".
143
- if (isTableCellContent && isInserted) {
165
+ if (slice.content.content.length === 0) {
166
+ return [];
167
+ }
168
+ const tableDiffMode = getTableDiffMode({
169
+ slice,
170
+ isInserted
171
+ });
172
+ if (tableDiffMode.kind === 'cells') {
144
173
  return createTableCellContentWidgets({
145
174
  slice,
146
175
  newDoc,
@@ -151,11 +180,10 @@ export const createNodeChangedDecorationWidget = ({
151
180
  diffType
152
181
  });
153
182
  }
154
- if (isTableCellContent) {
155
- // Deleted whole cells: nothing to render (see note above).
183
+ if (tableDiffMode.kind === 'none') {
156
184
  return [];
157
185
  }
158
- if (isTableRowContent) {
186
+ if (tableDiffMode.kind === 'rows') {
159
187
  return createChangedRowDecorationWidgets({
160
188
  attributionKey,
161
189
  changes: [change],
@@ -166,6 +194,7 @@ export const createNodeChangedDecorationWidget = ({
166
194
  isActive,
167
195
  isInserted,
168
196
  diffType,
197
+ intl,
169
198
  // Needed for the row's own indicator anchor; this path returns before the
170
199
  // `anchor-name` assignment further down.
171
200
  showIndicators,
@@ -173,6 +202,9 @@ export const createNodeChangedDecorationWidget = ({
173
202
  tagMountContext
174
203
  });
175
204
  }
205
+ // `wholeTable` and `generic` both continue into the generic block widget below. Splitting the
206
+ // whole-table case into its own renderer is the next step of this refactor.
207
+
176
208
  const serializer = nodeViewSerializer;
177
209
 
178
210
  // For non-table content, use the existing span wrapper approach
@@ -368,6 +400,40 @@ export const createNodeChangedDecorationWidget = ({
368
400
  }
369
401
  }
370
402
  });
403
+ let contributorTagAnchorName;
404
+ if (!isInserted && deletedColumns !== null && deletedColumns !== void 0 && deletedColumns.length) {
405
+ const table = dom.querySelector('table');
406
+ const firstRow = table === null || table === void 0 ? void 0 : table.rows[0];
407
+ if (table && canTagWidget) {
408
+ contributorTagAnchorName = buildAnchorDecorationKey({
409
+ diffId,
410
+ anchorType: AnchorTypeKey.tag
411
+ });
412
+ // The widget host sits before the table's preserved block margin. Anchor the tag to the
413
+ // table itself so its bottom edge meets the table's top edge with no visual gap.
414
+ table.style.setProperty('anchor-name', `--${contributorTagAnchorName}`);
415
+ }
416
+ deletedColumns.forEach(column => {
417
+ var _table$rows;
418
+ Array.from((_table$rows = table === null || table === void 0 ? void 0 : table.rows) !== null && _table$rows !== void 0 ? _table$rows : []).forEach(row => {
419
+ var _cell$children;
420
+ const cell = row.cells[column];
421
+ Array.from((_cell$children = cell === null || cell === void 0 ? void 0 : cell.children) !== null && _cell$children !== void 0 ? _cell$children : []).forEach(child => {
422
+ if (child instanceof HTMLElement) {
423
+ child.style.setProperty('text-decoration-line', 'line-through');
424
+ }
425
+ });
426
+ });
427
+ const headerCell = firstRow === null || firstRow === void 0 ? void 0 : firstRow.cells[column];
428
+ if (headerCell) {
429
+ headerCell.style.position = 'relative';
430
+ // Append, never prepend. Editor CSS resets the top margin of a cell's first child, so
431
+ // a prepended label pushes the paragraph out of that reset and the cell grows.
432
+ // The label is absolutely positioned, so DOM order does not move it.
433
+ headerCell.append(createRemovedLozenge(intl, isActive, colorScheme, true));
434
+ }
435
+ });
436
+ }
371
437
  dom.setAttribute('data-testid', 'show-diff-deleted-decoration');
372
438
  if (fg('platform_editor_ai_show_diff_patch_1')) {
373
439
  dom.style.setProperty('scroll-margin-top', scrollMarginTopValue);
@@ -405,7 +471,11 @@ export const createNodeChangedDecorationWidget = ({
405
471
 
406
472
  // Hosted at the start of the deleted run rather than on `dom` itself: `dom` is inline, so once
407
473
  // the deleted content wraps its box is the union of its line fragments.
408
- const tagHost = createContributorTagHost(diffId, tagMountContext);
474
+ const tagHost = createContributorTagHost({
475
+ anchorName: contributorTagAnchorName,
476
+ diffId,
477
+ mountContext: tagMountContext
478
+ });
409
479
  if (tagHost) {
410
480
  dom.prepend(tagHost.host);
411
481
  tagMount = tagHost.mount;