@atlaskit/editor-plugin-show-diff 16.0.13 → 16.0.16

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 (31) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/cjs/pm-plugins/calculateDiff/calculateDiffDecorations.js +1 -1
  3. package/dist/cjs/pm-plugins/decorations/createChangedRowDecorationWidgets.js +91 -28
  4. package/dist/cjs/pm-plugins/decorations/createInlineChangedDecoration.js +4 -6
  5. package/dist/cjs/pm-plugins/decorations/createNodeChangedDecorationWidget.js +11 -8
  6. package/dist/cjs/pm-plugins/utils/isEmptyParagraphSlice.js +11 -0
  7. package/dist/cjs/ui/ContributorTag/buildContributorTagDom.js +5 -4
  8. package/dist/es2019/pm-plugins/calculateDiff/calculateDiffDecorations.js +1 -1
  9. package/dist/es2019/pm-plugins/decorations/createChangedRowDecorationWidgets.js +70 -13
  10. package/dist/es2019/pm-plugins/decorations/createInlineChangedDecoration.js +4 -6
  11. package/dist/es2019/pm-plugins/decorations/createNodeChangedDecorationWidget.js +11 -8
  12. package/dist/es2019/pm-plugins/utils/isEmptyParagraphSlice.js +5 -0
  13. package/dist/es2019/ui/ContributorTag/buildContributorTagDom.js +5 -4
  14. package/dist/esm/pm-plugins/calculateDiff/calculateDiffDecorations.js +1 -1
  15. package/dist/esm/pm-plugins/decorations/createChangedRowDecorationWidgets.js +92 -29
  16. package/dist/esm/pm-plugins/decorations/createInlineChangedDecoration.js +4 -6
  17. package/dist/esm/pm-plugins/decorations/createNodeChangedDecorationWidget.js +11 -8
  18. package/dist/esm/pm-plugins/utils/isEmptyParagraphSlice.js +5 -0
  19. package/dist/esm/ui/ContributorTag/buildContributorTagDom.js +5 -4
  20. package/dist/types/pm-plugins/decorations/createChangedRowDecorationWidgets.d.ts +6 -1
  21. package/dist/types/pm-plugins/utils/isEmptyParagraphSlice.d.ts +3 -0
  22. package/dist/types/ui/ContributorTag/buildContributorTagDom.d.ts +4 -4
  23. package/package.json +2 -2
  24. package/dist/cjs/pm-plugins/areDocsEqualByBlockStructureAndText.js +0 -55
  25. package/dist/cjs/pm-plugins/utils/hasVisibleContent.js +0 -37
  26. package/dist/es2019/pm-plugins/areDocsEqualByBlockStructureAndText.js +0 -50
  27. package/dist/es2019/pm-plugins/utils/hasVisibleContent.js +0 -28
  28. package/dist/esm/pm-plugins/areDocsEqualByBlockStructureAndText.js +0 -50
  29. package/dist/esm/pm-plugins/utils/hasVisibleContent.js +0 -32
  30. package/dist/types/pm-plugins/areDocsEqualByBlockStructureAndText.d.ts +0 -12
  31. package/dist/types/pm-plugins/utils/hasVisibleContent.d.ts +0 -13
package/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # @atlaskit/editor-plugin-show-diff
2
2
 
3
+ ## 16.0.16
4
+
5
+ ### Patch Changes
6
+
7
+ - [`b56b3a7dbfb5a`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/b56b3a7dbfb5a) -
8
+ [ux] Show contributor tags at the top-left of removed table rows and above table interaction
9
+ overlays and sticky header rows. Behind `confluence_ncs_step_diffing_version_history`.
10
+ - Updated dependencies
11
+
12
+ ## 16.0.15
13
+
14
+ ### Patch Changes
15
+
16
+ - [`442abbe2a88e0`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/442abbe2a88e0) -
17
+ Normalize documents before relaxed show-diff comparisons when Confluence version-history diffing
18
+ is enabled.
19
+ - Updated dependencies
20
+
21
+ ## 16.0.14
22
+
23
+ ### Patch Changes
24
+
25
+ - [`6667b6ae8e77c`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/6667b6ae8e77c) -
26
+ Ensure contributor tags are rendered for shown attributed diff content, including whitespace-only
27
+ changes and deletion widgets, without tagging structurally empty paragraphs.
28
+
3
29
  ## 16.0.13
4
30
 
5
31
  ### Patch Changes
@@ -11,13 +11,13 @@ var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/sli
11
11
  var _isEqual = _interopRequireDefault(require("lodash/isEqual"));
12
12
  var _memoizeOne = _interopRequireDefault(require("memoize-one"));
13
13
  var _prosemirrorChangeset = require("prosemirror-changeset");
14
+ var _areDocsEqualByBlockStructureAndText = require("@atlaskit/editor-common/utils/areDocsEqualByBlockStructureAndText");
14
15
  var _document = require("@atlaskit/editor-common/utils/document");
15
16
  var _transform = require("@atlaskit/editor-prosemirror/transform");
16
17
  var _view = require("@atlaskit/editor-prosemirror/view");
17
18
  var _unsafeExpValNoExposure = require("@atlaskit/platform-feature-experiments/unsafe-exp-val-no-exposure");
18
19
  var _fg = require("@atlaskit/platform-feature-flags/fg");
19
20
  var _expValEqualsNoExposure = require("@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure");
20
- var _areDocsEqualByBlockStructureAndText = require("../areDocsEqualByBlockStructureAndText");
21
21
  var _createAnchorDecorationWidgets = require("../decorations/createAnchorDecorationWidgets");
22
22
  var _createBlockChangedDecoration = require("../decorations/createBlockChangedDecoration");
23
23
  var _createInlineChangedDecoration = require("../decorations/createInlineChangedDecoration");
@@ -19,6 +19,7 @@ var _factory = require("./colorSchemes/factory");
19
19
  var _schemes = require("./colorSchemes/schemes");
20
20
  var _createAnchorDecorationWidgets = require("./createAnchorDecorationWidgets");
21
21
  var _createChangedRowDecorationWidgetsStyles = require("./createChangedRowDecorationWidgets.styles.legacy");
22
+ var _createContributorTagWidget = require("./createContributorTagWidget");
22
23
  var _decorationKeys = require("./decorationKeys");
23
24
  var _findSafeInsertPos = require("./utils/findSafeInsertPos");
24
25
  var _tableCellEdgeAttrs = require("./utils/tableCellEdgeAttrs");
@@ -105,20 +106,40 @@ var extractChangedRows = function extractChangedRows(_ref) {
105
106
  }
106
107
  });
107
108
 
108
- // Filter changes that never truly got deleted
109
+ // Filter changes that never truly got deleted. Compare the number of equivalent rows rather
110
+ // than only checking whether one still exists: deleting two of three identical empty rows should
111
+ // retain two changed rows even though the third remains in the new table.
112
+ var equivalentRowDeletionCounts = [];
109
113
  return changedRows.filter(function (changedRow) {
110
114
  // A replacement leaves every sibling row in place, so asking "does this row still exist
111
115
  // ANYWHERE in the new table?" can drop a genuinely rewritten row that happens to match a
112
- // sibling. Compare it against the row at the same index instead. Deletions must keep the
113
- // whole-table `some()`: their indices shift, so an index comparison would let every
114
- // surviving row through and draw a widget for each one.
116
+ // sibling. Compare it against the row at the same index instead.
115
117
  if (handleRowReplacement) {
116
118
  var counterpart = tableNew.node.maybeChild(changedRow.rowIndex);
117
119
  return !counterpart || !(0, _document.areNodesEqualIgnoreAttrs)(counterpart, changedRow.rowNode);
118
120
  }
119
- return !tableNew.node.children.some(function (newRow) {
120
- return (0, _document.areNodesEqualIgnoreAttrs)(newRow, changedRow.rowNode);
121
+ var deletionCount = equivalentRowDeletionCounts.find(function (_ref2) {
122
+ var rowNode = _ref2.rowNode;
123
+ return (0, _document.areNodesEqualIgnoreAttrs)(rowNode, changedRow.rowNode);
121
124
  });
125
+ if (!deletionCount) {
126
+ var oldRowCount = tableOld.node.children.filter(function (oldRow) {
127
+ return (0, _document.areNodesEqualIgnoreAttrs)(oldRow, changedRow.rowNode);
128
+ }).length;
129
+ var newRowCount = tableNew.node.children.filter(function (newRow) {
130
+ return (0, _document.areNodesEqualIgnoreAttrs)(newRow, changedRow.rowNode);
131
+ }).length;
132
+ deletionCount = {
133
+ remaining: Math.max(0, oldRowCount - newRowCount),
134
+ rowNode: changedRow.rowNode
135
+ };
136
+ equivalentRowDeletionCounts.push(deletionCount);
137
+ }
138
+ if (deletionCount.remaining === 0) {
139
+ return false;
140
+ }
141
+ deletionCount.remaining--;
142
+ return true;
122
143
  });
123
144
  };
124
145
 
@@ -201,11 +222,11 @@ var createChangedRowDOM = function createChangedRowDOM(rowNode, cellEdgeAttrs, n
201
222
  /**
202
223
  * Expands a diff to include whole changed rows when table rows are affected
203
224
  */
204
- var expandDiffForChangedRows = function expandDiffForChangedRows(_ref2) {
205
- var changes = _ref2.changes,
206
- originalDoc = _ref2.originalDoc,
207
- newDoc = _ref2.newDoc,
208
- diffType = _ref2.diffType;
225
+ var expandDiffForChangedRows = function expandDiffForChangedRows(_ref3) {
226
+ var changes = _ref3.changes,
227
+ originalDoc = _ref3.originalDoc,
228
+ newDoc = _ref3.newDoc,
229
+ diffType = _ref3.diffType;
209
230
  var rowInfo = [];
210
231
  var _iterator = _createForOfIteratorHelper(changes),
211
232
  _step;
@@ -234,17 +255,22 @@ var expandDiffForChangedRows = function expandDiffForChangedRows(_ref2) {
234
255
  /**
235
256
  * Main function to handle deleted rows - computes diff and creates decorations
236
257
  */
237
- var createChangedRowDecorationWidgets = exports.createChangedRowDecorationWidgets = function createChangedRowDecorationWidgets(_ref3) {
238
- var changes = _ref3.changes,
239
- originalDoc = _ref3.originalDoc,
240
- newDoc = _ref3.newDoc,
241
- nodeViewSerializer = _ref3.nodeViewSerializer,
242
- colorScheme = _ref3.colorScheme,
243
- _ref3$isInserted = _ref3.isInserted,
244
- isInserted = _ref3$isInserted === void 0 ? false : _ref3$isInserted,
245
- diffType = _ref3.diffType,
246
- _ref3$showIndicators = _ref3.showIndicators,
247
- showIndicators = _ref3$showIndicators === void 0 ? false : _ref3$showIndicators;
258
+ var createChangedRowDecorationWidgets = exports.createChangedRowDecorationWidgets = function createChangedRowDecorationWidgets(_ref4) {
259
+ var attributionKey = _ref4.attributionKey,
260
+ changes = _ref4.changes,
261
+ originalDoc = _ref4.originalDoc,
262
+ newDoc = _ref4.newDoc,
263
+ nodeViewSerializer = _ref4.nodeViewSerializer,
264
+ colorScheme = _ref4.colorScheme,
265
+ isActive = _ref4.isActive,
266
+ _ref4$isInserted = _ref4.isInserted,
267
+ isInserted = _ref4$isInserted === void 0 ? false : _ref4$isInserted,
268
+ diffType = _ref4.diffType,
269
+ _ref4$showIndicators = _ref4.showIndicators,
270
+ showIndicators = _ref4$showIndicators === void 0 ? false : _ref4$showIndicators,
271
+ _ref4$showContributor = _ref4.showContributorTags,
272
+ showContributorTags = _ref4$showContributor === void 0 ? false : _ref4$showContributor,
273
+ tagMountContext = _ref4.tagMountContext;
248
274
  // First, expand the changes to include complete deleted rows
249
275
  var changedRows = expandDiffForChangedRows({
250
276
  changes: changes.filter(function (change) {
@@ -261,8 +287,12 @@ var createChangedRowDecorationWidgets = exports.createChangedRowDecorationWidget
261
287
  var safeInsertPos = (0, _findSafeInsertPos.findSafeInsertPos)(newDoc, changedRow.fromB - 1,
262
288
  // -1 to find the first safe position from the table
263
289
  originalDoc.slice(changedRow.fromA, changedRow.toA));
264
- var diffId = crypto.randomUUID();
290
+
291
+ // Stable while contributor tags are enabled so a decoration recalculation preserves the tag's
292
+ // identity and controller state.
293
+ var diffId = showContributorTags ? "widget-row-".concat(changedRow.fromA, "-").concat(changedRow.toA) : crypto.randomUUID();
265
294
  var decorations = [];
295
+ var rowAnchorNames = [];
266
296
 
267
297
  // `IndicatorBarContentComponent` renders a bar for every widget descriptor and anchors its
268
298
  // top and bottom to `anchor-<diffId>`. Without an element carrying that anchor name the bar
@@ -270,9 +300,9 @@ var createChangedRowDecorationWidgets = exports.createChangedRowDecorationWidget
270
300
  // bar covers only the row being changed. `createNodeChangedDecorationWidget` sets the same
271
301
  // property on its own widget DOM for non-table content; this path returns before reaching it.
272
302
  if (showIndicators && (0, _isExtendedEnabled.isExtendedEnabled)(diffType) && (0, _fg.fg)('platform_editor_ai_show_diff_patch_1')) {
273
- rowDOM.style.setProperty('anchor-name', "--".concat((0, _decorationKeys.buildAnchorDecorationKey)({
303
+ rowAnchorNames.push((0, _decorationKeys.buildAnchorDecorationKey)({
274
304
  diffId: diffId
275
- })));
305
+ }));
276
306
 
277
307
  // A table's content can extend past the doc margin, so the bar also needs a left anchor
278
308
  // measured against the table itself or it is clipped once the table is resized.
@@ -285,14 +315,47 @@ var createChangedRowDecorationWidgets = exports.createChangedRowDecorationWidget
285
315
  decorations.push(leftAnchor);
286
316
  }
287
317
  }
288
- decorations.push(_view.Decoration.widget(safeInsertPos, rowDOM, _objectSpread({}, (0, _decorationKeys.buildDiffDecorationSpec)({
318
+ var tagAnchorName = showContributorTags && (0, _createContributorTagWidget.isContributorTagWidgetEnabled)() ? (0, _decorationKeys.buildAnchorDecorationKey)({
319
+ diffId: diffId,
320
+ anchorType: _decorationKeys.AnchorTypeKey.tag
321
+ }) : undefined;
322
+ var tagWidget = tagAnchorName ? (0, _createContributorTagWidget.createContributorTagWidget)({
323
+ anchorAtRangeStart: true,
324
+ anchorName: tagAnchorName,
325
+ diffId: diffId,
326
+ doc: newDoc,
327
+ from: safeInsertPos,
328
+ mountContext: tagMountContext,
329
+ to: safeInsertPos
330
+ }) : undefined;
331
+ if (tagAnchorName) {
332
+ // The tag host is a separate, table-safe widget. CSS anchor positioning aligns it to this
333
+ // synthetic row without placing non-cell DOM inside the `<tr>` or inheriting its deletion
334
+ // opacity and strikethrough.
335
+ rowAnchorNames.push(tagAnchorName);
336
+ }
337
+ if (rowAnchorNames.length > 0) {
338
+ rowDOM.style.setProperty('anchor-name', rowAnchorNames.map(function (anchorName) {
339
+ return "--".concat(anchorName);
340
+ }).join(', '));
341
+ }
342
+ if (tagWidget) {
343
+ // Lets the contributor tag reveal when any part of the deleted row is hovered.
344
+ rowDOM.setAttribute('data-diff-id', diffId);
345
+ }
346
+ var rowWidget = _view.Decoration.widget(safeInsertPos, rowDOM, _objectSpread({}, (0, _decorationKeys.buildDiffDecorationSpec)({
347
+ attributionKey: tagWidget ? attributionKey : undefined,
289
348
  colorScheme: colorScheme,
290
349
  decorationType: 'widget',
291
350
  diffId: diffId,
292
- isActive: false,
351
+ isActive: isActive,
293
352
  isInserted: isInserted,
294
353
  diffType: diffType
295
- }))));
354
+ })));
355
+ decorations.push(rowWidget);
356
+ if (tagWidget) {
357
+ decorations.push(tagWidget);
358
+ }
296
359
  return decorations;
297
360
  });
298
361
  };
@@ -12,7 +12,7 @@ var _view = require("@atlaskit/editor-prosemirror/view");
12
12
  var _isExperimentEnabled = require("@atlaskit/platform-feature-experiments/is-experiment-enabled");
13
13
  var _buildContributorTagDom = require("../../ui/ContributorTag/buildContributorTagDom");
14
14
  var _isExtendedEnabled = require("../isExtendedEnabled");
15
- var _hasVisibleContent = require("../utils/hasVisibleContent");
15
+ var _isEmptyParagraphSlice = require("../utils/isEmptyParagraphSlice");
16
16
  var _factory = require("./colorSchemes/factory");
17
17
  var _schemes = require("./colorSchemes/schemes");
18
18
  var _createAnchorDecorationWidgets = require("./createAnchorDecorationWidgets");
@@ -163,11 +163,9 @@ var createInlineChangedDecoration = exports.createInlineChangedDecoration = func
163
163
  // tag host's key, remounting every tag mid-step. Random ids are kept when tags are off.
164
164
  var diffId = showContributorTags ? "inline-".concat(change.fromB, "-").concat(change.toB) : crypto.randomUUID();
165
165
 
166
- // Whether this highlight hosts a tag. A range of nothing but whitespace has nothing to caption,
167
- // so it stays unattributed: the spec is the only route attribution takes to
168
- // `extractContributorTags`, so dropping the key there is what keeps a tag from being resolved for
169
- // a host that is never emitted (EDITOR-8855).
170
- var canTagChange = showContributorTags && !!doc && (0, _isExtendedEnabled.isExtendedEnabled)(diffType) && (0, _createContributorTagWidget.isContributorTagWidgetEnabled)() && (0, _hasVisibleContent.hasVisibleContent)(doc.slice(change.fromB, change.toB).content);
166
+ // Match the rendered shape: whitespace text is eligible, while an empty paragraph is structural
167
+ // content with no tag host.
168
+ var canTagChange = showContributorTags && !!doc && (0, _isExtendedEnabled.isExtendedEnabled)(diffType) && (0, _createContributorTagWidget.isContributorTagWidgetEnabled)() && !(0, _isEmptyParagraphSlice.isEmptyParagraphSlice)(doc.slice(change.fromB, change.toB));
171
169
  if (shouldHideDeleted) {
172
170
  return [_view.Decoration.inline(change.fromB, change.toB, {
173
171
  style: displayNoneStyle
@@ -9,7 +9,7 @@ var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/de
9
9
  var _view = require("@atlaskit/editor-prosemirror/view");
10
10
  var _fg = require("@atlaskit/platform-feature-flags/fg");
11
11
  var _isExtendedEnabled = require("../isExtendedEnabled");
12
- var _hasVisibleContent = require("../utils/hasVisibleContent");
12
+ var _isEmptyParagraphSlice = require("../utils/isEmptyParagraphSlice");
13
13
  var _createAnchorDecorationWidgets = require("./createAnchorDecorationWidgets");
14
14
  var _createChangedRowDecorationWidgets = require("./createChangedRowDecorationWidgets");
15
15
  var _createContributorTagWidget = require("./createContributorTagWidget");
@@ -101,7 +101,7 @@ var createTableCellContentWidgets = function createTableCellContentWidgets(_ref)
101
101
  * that is not in the current document.
102
102
  */
103
103
  var createNodeChangedDecorationWidget = exports.createNodeChangedDecorationWidget = function createNodeChangedDecorationWidget(_ref2) {
104
- var _slice$content, _slice$content2, _slice$content3, _$safeInsertPos$nodeB, _slice$content$firstC, _newDoc$firstChild, _slice$content$firstC2;
104
+ var _$safeInsertPos$nodeB, _slice$content$firstC, _newDoc$firstChild, _slice$content$firstC2;
105
105
  var attributionKey = _ref2.attributionKey,
106
106
  change = _ref2.change,
107
107
  doc = _ref2.doc,
@@ -124,7 +124,7 @@ var createNodeChangedDecorationWidget = exports.createNodeChangedDecorationWidge
124
124
  reveal = _ref2.reveal,
125
125
  tagMountContext = _ref2.tagMountContext;
126
126
  var slice = doc.slice(change.fromA, change.toA);
127
- var shouldSkipDeletedEmptyParagraphDecoration = !isInserted && (slice === null || slice === void 0 || (_slice$content = slice.content) === null || _slice$content === void 0 ? void 0 : _slice$content.childCount) === 1 && (slice === null || slice === void 0 || (_slice$content2 = slice.content) === null || _slice$content2 === void 0 || (_slice$content2 = _slice$content2.firstChild) === null || _slice$content2 === void 0 ? void 0 : _slice$content2.type.name) === 'paragraph' && (slice === null || slice === void 0 || (_slice$content3 = slice.content) === null || _slice$content3 === void 0 || (_slice$content3 = _slice$content3.firstChild) === null || _slice$content3 === void 0 ? void 0 : _slice$content3.content.size) === 0;
127
+ var shouldSkipDeletedEmptyParagraphDecoration = !isInserted && (0, _isEmptyParagraphSlice.isEmptyParagraphSlice)(slice);
128
128
  // Widget decoration used for deletions as the content is not in the document
129
129
  // and we want to display the deleted content with a style.
130
130
  // For `placeBelow`, anchor at the END of the new content (change.toB) so the deleted
@@ -166,16 +166,20 @@ var createNodeChangedDecorationWidget = exports.createNodeChangedDecorationWidge
166
166
  }
167
167
  if (isTableRowContent) {
168
168
  return (0, _createChangedRowDecorationWidgets.createChangedRowDecorationWidgets)({
169
+ attributionKey: attributionKey,
169
170
  changes: [change],
170
171
  originalDoc: doc,
171
172
  newDoc: newDoc,
172
173
  nodeViewSerializer: nodeViewSerializer,
173
174
  colorScheme: colorScheme,
175
+ isActive: isActive,
174
176
  isInserted: isInserted,
175
177
  diffType: diffType,
176
178
  // Needed for the row's own indicator anchor; this path returns before the
177
179
  // `anchor-name` assignment further down.
178
- showIndicators: showIndicators
180
+ showIndicators: showIndicators,
181
+ showContributorTags: showContributorTags,
182
+ tagMountContext: tagMountContext
179
183
  });
180
184
  }
181
185
  var serializer = nodeViewSerializer;
@@ -226,10 +230,9 @@ var createNodeChangedDecorationWidget = exports.createNodeChangedDecorationWidge
226
230
  // Derived from the deleted range so it survives a recalculation — see the same reasoning in
227
231
  // `createInlineChangedDecoration`.
228
232
  var diffId = showContributorTags ? "widget-".concat(change.fromA, "-").concat(change.toA) : crypto.randomUUID();
229
- // Whether this widget hosts a tag. Removing nothing but whitespace leaves no visible deleted
230
- // content to caption, so the change stays unattributed — matching the inline half's rule in
231
- // `createInlineChangedDecoration` (EDITOR-8855).
232
- var canTagWidget = showContributorTags && (0, _hasVisibleContent.hasVisibleContent)(slice.content);
233
+ // Match the rendering predicate above: an empty paragraph has no widget to tag, while whitespace
234
+ // text in a rendered widget remains eligible.
235
+ var canTagWidget = showContributorTags && !shouldSkipDeletedEmptyParagraphDecoration;
233
236
  var decorations = [];
234
237
  var replacementNode = newDoc.nodeAt(change.fromB);
235
238
  var firstReplacedNode = slice.content.firstChild;
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.isEmptyParagraphSlice = void 0;
7
+ /** Whether a slice contains exactly one structurally empty paragraph. */
8
+ var isEmptyParagraphSlice = exports.isEmptyParagraphSlice = function isEmptyParagraphSlice(slice) {
9
+ var _slice$content$firstC;
10
+ return (slice === null || slice === void 0 ? void 0 : slice.content.childCount) === 1 && ((_slice$content$firstC = slice.content.firstChild) === null || _slice$content$firstC === void 0 ? void 0 : _slice$content$firstC.type.name) === 'paragraph' && slice.content.firstChild.content.size === 0;
11
+ };
@@ -7,6 +7,7 @@ Object.defineProperty(exports, "__esModule", {
7
7
  exports.buildContributorTagDom = exports.TAG_EXIT_FALLBACK_MS = exports.MAX_TAG_WIDTH = exports.CONTRIBUTOR_TAG_Z_INDEX = exports.CONTRIBUTOR_TAG_TOOLTIP_STYLES = exports.CONTRIBUTOR_TAG_TESTID = exports.CONTRIBUTOR_TAG_REVEALED_ATTRIBUTE = exports.CONTRIBUTOR_TAG_NAME_TESTID = exports.CONTRIBUTOR_TAG_CLASS = void 0;
8
8
  var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));
9
9
  var _lazyNodeView = require("@atlaskit/editor-common/lazy-node-view");
10
+ var _constants = require("@atlaskit/editor-shared-styles/constants");
10
11
  var CONTRIBUTOR_TAG_TESTID = exports.CONTRIBUTOR_TAG_TESTID = 'diff-contributor-tag';
11
12
  var CONTRIBUTOR_TAG_NAME_TESTID = exports.CONTRIBUTOR_TAG_NAME_TESTID = 'diff-contributor-tag-name';
12
13
 
@@ -20,15 +21,15 @@ var MAX_TAG_WIDTH = exports.MAX_TAG_WIDTH = '200px';
20
21
  /**
21
22
  * The tag sits on the line above the change, so it has to paint over that line to be readable —
22
23
  * including any diff highlight already on it, which would otherwise slice straight through the tag.
23
- * Hence one level above `akEditorUnitZIndex`, where the highlights sit; exported because the
24
- * change's own highlight is stacked one level below it — see `stackBelowContributorTagStyle` in
25
- * `createInlineChangedDecoration`.
24
+ * It also has to paint above table interaction overlays and sticky-capable header rows, which use
25
+ * `akEditorSmallZIndex`. The change's own highlight is stacked one level below it — see
26
+ * `stackBelowContributorTagStyle` in `createInlineChangedDecoration`.
26
27
  *
27
28
  * Painting above its own highlight is only safe because the tag can never reach it: `bottom: 100%`
28
29
  * puts the root's bottom edge exactly on the highlight's top edge, and the root's `overflow: clip`
29
30
  * keeps every pixel the tag paints inside it.
30
31
  */
31
- var CONTRIBUTOR_TAG_Z_INDEX = exports.CONTRIBUTOR_TAG_Z_INDEX = 2;
32
+ var CONTRIBUTOR_TAG_Z_INDEX = exports.CONTRIBUTOR_TAG_Z_INDEX = _constants.akEditorSmallZIndex + 1;
32
33
 
33
34
  /**
34
35
  * The plugin's whole side of the tag's motion: the fade and the hidden state are
@@ -1,13 +1,13 @@
1
1
  import isEqual from 'lodash/isEqual';
2
2
  import memoizeOne from 'memoize-one';
3
3
  import { ChangeSet, simplifyChanges } from 'prosemirror-changeset';
4
+ import { areDocsEqualByBlockStructureAndText } from '@atlaskit/editor-common/utils/areDocsEqualByBlockStructureAndText';
4
5
  import { areNodesEqualIgnoreAttrs } from '@atlaskit/editor-common/utils/document';
5
6
  import { Mapping } from '@atlaskit/editor-prosemirror/transform';
6
7
  import { DecorationSet } from '@atlaskit/editor-prosemirror/view';
7
8
  import { UNSAFE_expValNoExposure } from '@atlaskit/platform-feature-experiments/unsafe-exp-val-no-exposure';
8
9
  import { fg } from '@atlaskit/platform-feature-flags/fg';
9
10
  import { expValEqualsNoExposure } from '@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure';
10
- import { areDocsEqualByBlockStructureAndText } from '../areDocsEqualByBlockStructureAndText';
11
11
  import { createDocMarginAnchorWidget } from '../decorations/createAnchorDecorationWidgets';
12
12
  import { createBlockChangedDecoration } from '../decorations/createBlockChangedDecoration';
13
13
  import { createInlineChangedDecoration } from '../decorations/createInlineChangedDecoration';
@@ -10,7 +10,8 @@ import { buildAddedCellOverlayRoundedStyle, buildAddedCellOverlayStyle, buildDel
10
10
  import { colorSchemeRegistry, getLegacyColorScheme } from './colorSchemes/schemes';
11
11
  import { createLeftAnchorWidget } from './createAnchorDecorationWidgets';
12
12
  import { resolveCellOverlayStyleLegacy, resolveDeletedRowStyleLegacy } from './createChangedRowDecorationWidgets.styles.legacy';
13
- import { buildAnchorDecorationKey, buildDiffDecorationSpec } from './decorationKeys';
13
+ import { createContributorTagWidget, isContributorTagWidgetEnabled } from './createContributorTagWidget';
14
+ import { AnchorTypeKey, buildAnchorDecorationKey, buildDiffDecorationSpec } from './decorationKeys';
14
15
  import { findSafeInsertPos } from './utils/findSafeInsertPos';
15
16
  import { applyCellEdgeAttrs, getRowCellEdgeAttrs, getTableTopAndBottomCellEdgeAttrs } from './utils/tableCellEdgeAttrs';
16
17
  /**
@@ -86,18 +87,35 @@ const extractChangedRows = ({
86
87
  }
87
88
  });
88
89
 
89
- // Filter changes that never truly got deleted
90
+ // Filter changes that never truly got deleted. Compare the number of equivalent rows rather
91
+ // than only checking whether one still exists: deleting two of three identical empty rows should
92
+ // retain two changed rows even though the third remains in the new table.
93
+ const equivalentRowDeletionCounts = [];
90
94
  return changedRows.filter(changedRow => {
91
95
  // A replacement leaves every sibling row in place, so asking "does this row still exist
92
96
  // ANYWHERE in the new table?" can drop a genuinely rewritten row that happens to match a
93
- // sibling. Compare it against the row at the same index instead. Deletions must keep the
94
- // whole-table `some()`: their indices shift, so an index comparison would let every
95
- // surviving row through and draw a widget for each one.
97
+ // sibling. Compare it against the row at the same index instead.
96
98
  if (handleRowReplacement) {
97
99
  const counterpart = tableNew.node.maybeChild(changedRow.rowIndex);
98
100
  return !counterpart || !areNodesEqualIgnoreAttrs(counterpart, changedRow.rowNode);
99
101
  }
100
- return !tableNew.node.children.some(newRow => areNodesEqualIgnoreAttrs(newRow, changedRow.rowNode));
102
+ let deletionCount = equivalentRowDeletionCounts.find(({
103
+ rowNode
104
+ }) => areNodesEqualIgnoreAttrs(rowNode, changedRow.rowNode));
105
+ if (!deletionCount) {
106
+ const oldRowCount = tableOld.node.children.filter(oldRow => areNodesEqualIgnoreAttrs(oldRow, changedRow.rowNode)).length;
107
+ const newRowCount = tableNew.node.children.filter(newRow => areNodesEqualIgnoreAttrs(newRow, changedRow.rowNode)).length;
108
+ deletionCount = {
109
+ remaining: Math.max(0, oldRowCount - newRowCount),
110
+ rowNode: changedRow.rowNode
111
+ };
112
+ equivalentRowDeletionCounts.push(deletionCount);
113
+ }
114
+ if (deletionCount.remaining === 0) {
115
+ return false;
116
+ }
117
+ deletionCount.remaining--;
118
+ return true;
101
119
  });
102
120
  };
103
121
 
@@ -206,14 +224,18 @@ const expandDiffForChangedRows = ({
206
224
  * Main function to handle deleted rows - computes diff and creates decorations
207
225
  */
208
226
  export const createChangedRowDecorationWidgets = ({
227
+ attributionKey,
209
228
  changes,
210
229
  originalDoc,
211
230
  newDoc,
212
231
  nodeViewSerializer,
213
232
  colorScheme,
233
+ isActive,
214
234
  isInserted = false,
215
235
  diffType,
216
- showIndicators = false
236
+ showIndicators = false,
237
+ showContributorTags = false,
238
+ tagMountContext
217
239
  }) => {
218
240
  // First, expand the changes to include complete deleted rows
219
241
  const changedRows = expandDiffForChangedRows({
@@ -229,8 +251,12 @@ export const createChangedRowDecorationWidgets = ({
229
251
  const safeInsertPos = findSafeInsertPos(newDoc, changedRow.fromB - 1,
230
252
  // -1 to find the first safe position from the table
231
253
  originalDoc.slice(changedRow.fromA, changedRow.toA));
232
- const diffId = crypto.randomUUID();
254
+
255
+ // Stable while contributor tags are enabled so a decoration recalculation preserves the tag's
256
+ // identity and controller state.
257
+ const diffId = showContributorTags ? `widget-row-${changedRow.fromA}-${changedRow.toA}` : crypto.randomUUID();
233
258
  const decorations = [];
259
+ const rowAnchorNames = [];
234
260
 
235
261
  // `IndicatorBarContentComponent` renders a bar for every widget descriptor and anchors its
236
262
  // top and bottom to `anchor-<diffId>`. Without an element carrying that anchor name the bar
@@ -238,9 +264,9 @@ export const createChangedRowDecorationWidgets = ({
238
264
  // bar covers only the row being changed. `createNodeChangedDecorationWidget` sets the same
239
265
  // property on its own widget DOM for non-table content; this path returns before reaching it.
240
266
  if (showIndicators && isExtendedEnabled(diffType) && fg('platform_editor_ai_show_diff_patch_1')) {
241
- rowDOM.style.setProperty('anchor-name', `--${buildAnchorDecorationKey({
267
+ rowAnchorNames.push(buildAnchorDecorationKey({
242
268
  diffId
243
- })}`);
269
+ }));
244
270
 
245
271
  // A table's content can extend past the doc margin, so the bar also needs a left anchor
246
272
  // measured against the table itself or it is clipped once the table is resized.
@@ -253,16 +279,47 @@ export const createChangedRowDecorationWidgets = ({
253
279
  decorations.push(leftAnchor);
254
280
  }
255
281
  }
256
- decorations.push(Decoration.widget(safeInsertPos, rowDOM, {
282
+ const tagAnchorName = showContributorTags && isContributorTagWidgetEnabled() ? buildAnchorDecorationKey({
283
+ diffId,
284
+ anchorType: AnchorTypeKey.tag
285
+ }) : undefined;
286
+ const tagWidget = tagAnchorName ? createContributorTagWidget({
287
+ anchorAtRangeStart: true,
288
+ anchorName: tagAnchorName,
289
+ diffId,
290
+ doc: newDoc,
291
+ from: safeInsertPos,
292
+ mountContext: tagMountContext,
293
+ to: safeInsertPos
294
+ }) : undefined;
295
+ if (tagAnchorName) {
296
+ // The tag host is a separate, table-safe widget. CSS anchor positioning aligns it to this
297
+ // synthetic row without placing non-cell DOM inside the `<tr>` or inheriting its deletion
298
+ // opacity and strikethrough.
299
+ rowAnchorNames.push(tagAnchorName);
300
+ }
301
+ if (rowAnchorNames.length > 0) {
302
+ rowDOM.style.setProperty('anchor-name', rowAnchorNames.map(anchorName => `--${anchorName}`).join(', '));
303
+ }
304
+ if (tagWidget) {
305
+ // Lets the contributor tag reveal when any part of the deleted row is hovered.
306
+ rowDOM.setAttribute('data-diff-id', diffId);
307
+ }
308
+ const rowWidget = Decoration.widget(safeInsertPos, rowDOM, {
257
309
  ...buildDiffDecorationSpec({
310
+ attributionKey: tagWidget ? attributionKey : undefined,
258
311
  colorScheme,
259
312
  decorationType: 'widget',
260
313
  diffId,
261
- isActive: false,
314
+ isActive,
262
315
  isInserted,
263
316
  diffType
264
317
  })
265
- }));
318
+ });
319
+ decorations.push(rowWidget);
320
+ if (tagWidget) {
321
+ decorations.push(tagWidget);
322
+ }
266
323
  return decorations;
267
324
  });
268
325
  };
@@ -3,7 +3,7 @@ import { Decoration } from '@atlaskit/editor-prosemirror/view';
3
3
  import { isExperimentEnabled } from '@atlaskit/platform-feature-experiments/is-experiment-enabled';
4
4
  import { CONTRIBUTOR_TAG_Z_INDEX } from '../../ui/ContributorTag/buildContributorTagDom';
5
5
  import { isExtendedEnabled } from '../isExtendedEnabled';
6
- import { hasVisibleContent } from '../utils/hasVisibleContent';
6
+ import { isEmptyParagraphSlice } from '../utils/isEmptyParagraphSlice';
7
7
  import { buildAtomicInlineChangedCSSVariables, buildDeletedInlineContentStyleExtended, buildDeletedInlineStyle, buildDeletedInlineStyleStandard, buildInsertStyle, buildInsertStyleActive, buildInsertStyleExtended, buildInsertStyleExtendedActive, buildInsertStyleExtendedNoUnderline, buildInsertStyleExtendedNoUnderlineActive } from './colorSchemes/factory';
8
8
  import { colorSchemeRegistry, getLegacyColorScheme } from './colorSchemes/schemes';
9
9
  import { createInlineIndicatorAnchorWidgets } from './createAnchorDecorationWidgets';
@@ -138,11 +138,9 @@ export const createInlineChangedDecoration = ({
138
138
  // tag host's key, remounting every tag mid-step. Random ids are kept when tags are off.
139
139
  const diffId = showContributorTags ? `inline-${change.fromB}-${change.toB}` : crypto.randomUUID();
140
140
 
141
- // Whether this highlight hosts a tag. A range of nothing but whitespace has nothing to caption,
142
- // so it stays unattributed: the spec is the only route attribution takes to
143
- // `extractContributorTags`, so dropping the key there is what keeps a tag from being resolved for
144
- // a host that is never emitted (EDITOR-8855).
145
- const canTagChange = showContributorTags && !!doc && isExtendedEnabled(diffType) && isContributorTagWidgetEnabled() && hasVisibleContent(doc.slice(change.fromB, change.toB).content);
141
+ // Match the rendered shape: whitespace text is eligible, while an empty paragraph is structural
142
+ // content with no tag host.
143
+ const canTagChange = showContributorTags && !!doc && isExtendedEnabled(diffType) && isContributorTagWidgetEnabled() && !isEmptyParagraphSlice(doc.slice(change.fromB, change.toB));
146
144
  if (shouldHideDeleted) {
147
145
  return [Decoration.inline(change.fromB, change.toB, {
148
146
  style: displayNoneStyle
@@ -1,7 +1,7 @@
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 { hasVisibleContent } from '../utils/hasVisibleContent';
4
+ import { isEmptyParagraphSlice } from '../utils/isEmptyParagraphSlice';
5
5
  import { createLeftAnchorWidget } from './createAnchorDecorationWidgets';
6
6
  import { createChangedRowDecorationWidgets } from './createChangedRowDecorationWidgets';
7
7
  import { createContributorTagHost, unmountContributorTag } from './createContributorTagWidget';
@@ -114,9 +114,9 @@ export const createNodeChangedDecorationWidget = ({
114
114
  reveal,
115
115
  tagMountContext
116
116
  }) => {
117
- var _slice$content, _slice$content2, _slice$content2$first, _slice$content3, _slice$content3$first, _$safeInsertPos$nodeB, _slice$content$firstC, _newDoc$firstChild, _slice$content$firstC2;
117
+ var _$safeInsertPos$nodeB, _slice$content$firstC, _newDoc$firstChild, _slice$content$firstC2;
118
118
  const slice = doc.slice(change.fromA, change.toA);
119
- const shouldSkipDeletedEmptyParagraphDecoration = !isInserted && (slice === null || slice === void 0 ? void 0 : (_slice$content = slice.content) === null || _slice$content === void 0 ? void 0 : _slice$content.childCount) === 1 && (slice === null || slice === void 0 ? void 0 : (_slice$content2 = slice.content) === null || _slice$content2 === void 0 ? void 0 : (_slice$content2$first = _slice$content2.firstChild) === null || _slice$content2$first === void 0 ? void 0 : _slice$content2$first.type.name) === 'paragraph' && (slice === null || slice === void 0 ? void 0 : (_slice$content3 = slice.content) === null || _slice$content3 === void 0 ? void 0 : (_slice$content3$first = _slice$content3.firstChild) === null || _slice$content3$first === void 0 ? void 0 : _slice$content3$first.content.size) === 0;
119
+ const shouldSkipDeletedEmptyParagraphDecoration = !isInserted && isEmptyParagraphSlice(slice);
120
120
  // Widget decoration used for deletions as the content is not in the document
121
121
  // and we want to display the deleted content with a style.
122
122
  // For `placeBelow`, anchor at the END of the new content (change.toB) so the deleted
@@ -150,16 +150,20 @@ export const createNodeChangedDecorationWidget = ({
150
150
  }
151
151
  if (isTableRowContent) {
152
152
  return createChangedRowDecorationWidgets({
153
+ attributionKey,
153
154
  changes: [change],
154
155
  originalDoc: doc,
155
156
  newDoc,
156
157
  nodeViewSerializer,
157
158
  colorScheme,
159
+ isActive,
158
160
  isInserted,
159
161
  diffType,
160
162
  // Needed for the row's own indicator anchor; this path returns before the
161
163
  // `anchor-name` assignment further down.
162
- showIndicators
164
+ showIndicators,
165
+ showContributorTags,
166
+ tagMountContext
163
167
  });
164
168
  }
165
169
  const serializer = nodeViewSerializer;
@@ -210,10 +214,9 @@ export const createNodeChangedDecorationWidget = ({
210
214
  // Derived from the deleted range so it survives a recalculation — see the same reasoning in
211
215
  // `createInlineChangedDecoration`.
212
216
  const diffId = showContributorTags ? `widget-${change.fromA}-${change.toA}` : crypto.randomUUID();
213
- // Whether this widget hosts a tag. Removing nothing but whitespace leaves no visible deleted
214
- // content to caption, so the change stays unattributed — matching the inline half's rule in
215
- // `createInlineChangedDecoration` (EDITOR-8855).
216
- const canTagWidget = showContributorTags && hasVisibleContent(slice.content);
217
+ // Match the rendering predicate above: an empty paragraph has no widget to tag, while whitespace
218
+ // text in a rendered widget remains eligible.
219
+ const canTagWidget = showContributorTags && !shouldSkipDeletedEmptyParagraphDecoration;
217
220
  const decorations = [];
218
221
  const replacementNode = newDoc.nodeAt(change.fromB);
219
222
  const firstReplacedNode = slice.content.firstChild;
@@ -0,0 +1,5 @@
1
+ /** Whether a slice contains exactly one structurally empty paragraph. */
2
+ export const isEmptyParagraphSlice = slice => {
3
+ var _slice$content$firstC;
4
+ return (slice === null || slice === void 0 ? void 0 : slice.content.childCount) === 1 && ((_slice$content$firstC = slice.content.firstChild) === null || _slice$content$firstC === void 0 ? void 0 : _slice$content$firstC.type.name) === 'paragraph' && slice.content.firstChild.content.size === 0;
5
+ };