@atlaskit/editor-plugin-show-diff 26.0.0 → 26.0.2

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 (23) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/cjs/pm-plugins/calculateDiff/calculateDiffDecorations.js +36 -3
  3. package/dist/cjs/pm-plugins/calculateDiff/diffBySteps.js +14 -2
  4. package/dist/cjs/pm-plugins/decorations/createNodeChangedDecorationWidget.js +14 -0
  5. package/dist/cjs/pm-plugins/decorations/createReplacedCellDecorations.js +350 -0
  6. package/dist/cjs/pm-plugins/decorations/utils/cellAttrs.js +30 -0
  7. package/dist/cjs/pm-plugins/scrollToDiff.js +41 -32
  8. package/dist/es2019/pm-plugins/calculateDiff/calculateDiffDecorations.js +37 -4
  9. package/dist/es2019/pm-plugins/calculateDiff/diffBySteps.js +12 -2
  10. package/dist/es2019/pm-plugins/decorations/createNodeChangedDecorationWidget.js +14 -0
  11. package/dist/es2019/pm-plugins/decorations/createReplacedCellDecorations.js +338 -0
  12. package/dist/es2019/pm-plugins/decorations/utils/cellAttrs.js +19 -0
  13. package/dist/es2019/pm-plugins/scrollToDiff.js +39 -30
  14. package/dist/esm/pm-plugins/calculateDiff/calculateDiffDecorations.js +36 -3
  15. package/dist/esm/pm-plugins/calculateDiff/diffBySteps.js +14 -2
  16. package/dist/esm/pm-plugins/decorations/createNodeChangedDecorationWidget.js +14 -0
  17. package/dist/esm/pm-plugins/decorations/createReplacedCellDecorations.js +343 -0
  18. package/dist/esm/pm-plugins/decorations/utils/cellAttrs.js +25 -0
  19. package/dist/esm/pm-plugins/scrollToDiff.js +41 -32
  20. package/dist/types/pm-plugins/decorations/createReplacedCellDecorations.d.ts +67 -0
  21. package/dist/types/pm-plugins/decorations/utils/cellAttrs.d.ts +13 -0
  22. package/dist/types/pm-plugins/markViews.d.ts +1 -1
  23. package/package.json +5 -1
@@ -11,6 +11,7 @@ import { Mark } from '@atlaskit/editor-prosemirror/model';
11
11
  import { Mapping, ReplaceStep } from '@atlaskit/editor-prosemirror/transform';
12
12
  import { fg } from '@atlaskit/platform-feature-flags/fg';
13
13
  import { getAttributionKey } from '../decorations/colorSchemes/attributions';
14
+ import { isCellAttrChanged } from '../decorations/utils/cellAttrs';
14
15
  import { buildCharsByOffset } from '../utils/charsByOffset';
15
16
  import { attrAwareTokenEncoder } from './attrAwareTokenEncoder';
16
17
  import { optimizeChanges } from './optimizeChanges';
@@ -27,6 +28,16 @@ var WHITESPACE_REGEX = /[\t-\r \xA0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u
27
28
  var isGranularIsolatingNode = function isGranularIsolatingNode(node) {
28
29
  return !!node.type.spec.isolating && node.type.spec.tableRole !== 'table';
29
30
  };
31
+ var CELL_TYPE_NAMES = ['tableCell', 'tableHeader'];
32
+
33
+ /**
34
+ * Whether a table cell's replacement changes one of its diffable attributes, such as its background.
35
+ * Such a cell is diffed whole: it renders as a cell style plus a copy of its row, which already shows
36
+ * the cell's new content, so inline marks inside it would only repeat that.
37
+ */
38
+ var isCellAttrChange = function isCellAttrChange(replacedNode, replacingNode) {
39
+ return CELL_TYPE_NAMES.includes(replacedNode.type.name) && isCellAttrChanged(replacedNode, replacingNode);
40
+ };
30
41
  var mapPosition = function mapPosition(mapping, pos) {
31
42
  return mapping.map(pos);
32
43
  };
@@ -230,7 +241,8 @@ var mergeOverlappingByNewDocRange = function mergeOverlappingByNewDocRange(chang
230
241
  * - The replaced slice has only one child
231
242
  * - The replacing slice has only one child
232
243
  * - Both children are the same textblock type, or (behind `platform_editor_ai_show_diff_patch_2`)
233
- * the same isolating, non-table node type whose text content differs
244
+ * the same isolating, non-table node type whose text content differs (for a table cell, only when
245
+ * its diffable attributes are unchanged)
234
246
  * - The replaced slice and replacing slice have the same text content
235
247
  * - The replaced slice and replacing slice have the same child marks (if text content is equal)
236
248
  */
@@ -268,7 +280,7 @@ var shouldCheckGranularDiff = function shouldCheckGranularDiff(step, before, fro
268
280
  // `hasSameChildMarks` only compares an isolating node's direct (block) children, so it cannot
269
281
  // vouch for one with unchanged text. Keep the existing whole-node fallback for that case.
270
282
  if (isIsolatingNode) {
271
- return !isTextContentEqual;
283
+ return !isTextContentEqual && !(replacingNode && isCellAttrChange(replacedNode, replacingNode));
272
284
  }
273
285
  return !isTextContentEqual || isTextContentEqual && hasSameChildMarks(replacedNode, replacingNode);
274
286
  };
@@ -13,6 +13,7 @@ import { clampAnchorPosIntoCell, createAnchorNameSpan, createLeftAnchorWidget }
13
13
  import { createChangedRowDecorationWidgets } from './createChangedRowDecorationWidgets';
14
14
  import { createContributorTagHost, resolveHoistedCodeBlockAnchor, unmountContributorTag } from './createContributorTagWidget';
15
15
  import { getAtomicInlineChangedAttrs } from './createInlineChangedDecoration';
16
+ import { createReplacedCellWidgets } from './createReplacedCellDecorations';
16
17
  import { AnchorTypeKey, buildContributorTagDecorationSpec, buildDiffDecorationSpec, buildAnchorDecorationKey, scrollMarginTopValue } from './decorationKeys';
17
18
  import { absorbFirstChildMarginReset } from './utils/absorbFirstChildMarginReset';
18
19
  import { createDeletedLineBreakDecoration } from './utils/createDeletedLineBreakWidget';
@@ -216,6 +217,19 @@ export var createNodeChangedDecorationWidget = function createNodeChangedDecorat
216
217
  isInserted: isInserted
217
218
  });
218
219
  if (tableDiffMode.kind === 'cells') {
220
+ if (fg('platform_editor_ai_show_diff_patch_2')) {
221
+ return createReplacedCellWidgets({
222
+ slice: slice,
223
+ newDoc: newDoc,
224
+ change: change,
225
+ nodeViewSerializer: nodeViewSerializer,
226
+ colorScheme: colorScheme,
227
+ intl: intl,
228
+ isActive: isActive,
229
+ isInserted: isInserted,
230
+ leftAnchorId: leftAnchorId
231
+ });
232
+ }
219
233
  return createTableCellContentWidgets({
220
234
  slice: slice,
221
235
  newDoc: newDoc,
@@ -0,0 +1,343 @@
1
+ import _toConsumableArray from "@babel/runtime/helpers/toConsumableArray";
2
+ import _defineProperty from "@babel/runtime/helpers/defineProperty";
3
+ function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
4
+ function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
5
+ import { getDarkModeLCHColor } from '@atlaskit/adf-schema/get-dark-mode-lch-color';
6
+ import { isRgb } from '@atlaskit/adf-schema/is-rgb';
7
+ import { rgbToHex } from '@atlaskit/adf-schema/rgb-to-hex';
8
+ import { getCellDomAttrs } from '@atlaskit/adf-schema/tableNodes';
9
+ import { convertToInlineCss } from '@atlaskit/editor-common/css-helper';
10
+ import { areNodesEqualIgnoreAttrs } from '@atlaskit/editor-common/utils/document';
11
+ import { Decoration } from '@atlaskit/editor-prosemirror/view';
12
+ import { createBlockChangedDecoration } from './createBlockChangedDecoration';
13
+ import { createInlineChangedDecoration } from './createInlineChangedDecoration';
14
+ import { buildDiffDecorationSpec } from './decorationKeys';
15
+ import { isCellAttrChanged, isCellBackgroundChanged, normalizeCellBackground } from './utils/cellAttrs';
16
+ import { injectInnerWrapper, wrapBlockNodeView } from './utils/wrapBlockNodeView';
17
+ var CELL_TYPE_NAMES = ['tableCell', 'tableHeader'];
18
+ /**
19
+ * Pairs each cell of a whole-cell replacement with the live cell it replaces, or `[]` when the slice
20
+ * is not a run of whole cells matching the live ones. Whole-cell replacement preserves cell count and
21
+ * order, so the nth live cell in the range matches the nth cell in the slice.
22
+ */
23
+ export var getReplacedCells = function getReplacedCells(_ref) {
24
+ var slice = _ref.slice,
25
+ doc = _ref.doc,
26
+ fromB = _ref.fromB,
27
+ toB = _ref.toB;
28
+ var otherCells = slice.content.content;
29
+ if (slice.openStart !== 0 || slice.openEnd !== 0 || otherCells.length === 0 || !otherCells.every(function (node) {
30
+ return CELL_TYPE_NAMES.includes(node.type.name);
31
+ })) {
32
+ return [];
33
+ }
34
+ var liveCells = [];
35
+ doc.nodesBetween(fromB, toB, function (node, pos) {
36
+ if (CELL_TYPE_NAMES.includes(node.type.name)) {
37
+ liveCells.push({
38
+ node: node,
39
+ pos: pos
40
+ });
41
+ return false;
42
+ }
43
+ return true;
44
+ });
45
+ if (liveCells.length !== otherCells.length) {
46
+ return [];
47
+ }
48
+ return otherCells.map(function (otherCell, index) {
49
+ return {
50
+ otherCell: otherCell,
51
+ liveCell: liveCells[index].node,
52
+ liveCellPos: liveCells[index].pos
53
+ };
54
+ });
55
+ };
56
+
57
+ /**
58
+ * Whether two cells hold the same content, ignoring the cells' own attributes and `localId`s, which
59
+ * a suggestion can regenerate. Such a replacement only changed the cell (e.g. its background), so
60
+ * there is no content to diff.
61
+ */
62
+ var isCellContentEqual = function isCellContentEqual(left, right) {
63
+ if (left.childCount !== right.childCount) {
64
+ return false;
65
+ }
66
+ for (var index = 0; index < left.childCount; index++) {
67
+ if (!areNodesEqualIgnoreAttrs(left.child(index), right.child(index), ['localId'])) {
68
+ return false;
69
+ }
70
+ }
71
+ return true;
72
+ };
73
+
74
+ /**
75
+ * Whether the cell itself changed — its type, or an attribute that represents a change such as its
76
+ * background or span — as opposed to only its content.
77
+ */
78
+ var isCellChanged = function isCellChanged(left, right) {
79
+ return left.type !== right.type || isCellAttrChanged(left, right);
80
+ };
81
+
82
+ /**
83
+ * A custom (non-palette) cell colour as the schema renders it: inverted in dark mode so text stays
84
+ * readable, except for design tokens, which are already themed.
85
+ */
86
+ var getCustomCellColor = function getCustomCellColor(background) {
87
+ var color = isRgb(background) && rgbToHex(background) || background;
88
+ if (color.startsWith('var(') || document.documentElement.getAttribute('data-color-mode') !== 'dark') {
89
+ return background;
90
+ }
91
+ return getDarkModeLCHColor(color);
92
+ };
93
+
94
+ /**
95
+ * The colour `cell` renders with. A palette colour is the editor's own themed variable for its colour
96
+ * name (e.g. for dark mode), the one `td[colorname]` cells use; a custom colour is the schema's own
97
+ * treatment of it; no colour is the editor's default.
98
+ */
99
+ var getCellBackgroundColor = function getCellBackgroundColor(cell) {
100
+ var background = normalizeCellBackground(cell.attrs.background);
101
+ if (!background) {
102
+ return cell.type.name === 'tableHeader' ? "var(--ds-background-accent-gray-subtlest, #F0F1F2)" : "var(--ds-surface, #FFFFFF)";
103
+ }
104
+ var _getCellDomAttrs = getCellDomAttrs(cell),
105
+ colorname = _getCellDomAttrs.colorname;
106
+ return colorname ? "var(--ak-editor-table-cell-background-".concat(colorname.toLowerCase().split(' ').join('-'), ")") : getCustomCellColor(background);
107
+ };
108
+
109
+ /**
110
+ * A layer painting `color` over the cell's own background and under its content, so the cell reads
111
+ * as having the new background without any of the cell's own attributes being overridden: removing
112
+ * a decoration removes the attributes it set rather than restoring the ones they replaced.
113
+ */
114
+ var createCellBackgroundLayer = function createCellBackgroundLayer(color) {
115
+ var layer = document.createElement('div');
116
+ layer.setAttribute('aria-hidden', 'true');
117
+ layer.contentEditable = 'false';
118
+ layer.dataset.testid = 'show-diff-new-cell-background';
119
+ layer.setAttribute('style', convertToInlineCss({
120
+ position: 'absolute',
121
+ inset: 0,
122
+ zIndex: -1,
123
+ borderRadius: 'inherit',
124
+ pointerEvents: 'none',
125
+ backgroundColor: color
126
+ }));
127
+ return layer;
128
+ };
129
+
130
+ /** The other side's cell content, as blocks, for the top of the live cell. */
131
+ var createCellContentWidget = function createCellContentWidget(_ref2) {
132
+ var cell = _ref2.cell,
133
+ pos = _ref2.pos,
134
+ nodeViewSerializer = _ref2.nodeViewSerializer,
135
+ colorScheme = _ref2.colorScheme,
136
+ intl = _ref2.intl,
137
+ isActive = _ref2.isActive,
138
+ isInserted = _ref2.isInserted,
139
+ leftAnchorId = _ref2.leftAnchorId;
140
+ var dom = document.createElement('div');
141
+ cell.content.forEach(function (block) {
142
+ var nodeView = nodeViewSerializer.tryCreateNodeView(block, 0, undefined, isInserted, colorScheme);
143
+ if (nodeView) {
144
+ wrapBlockNodeView({
145
+ dom: dom,
146
+ nodeView: nodeView,
147
+ targetNode: block,
148
+ colorScheme: colorScheme,
149
+ intl: intl,
150
+ isActive: isActive,
151
+ isInserted: isInserted
152
+ });
153
+ return;
154
+ }
155
+ var serialized = nodeViewSerializer.serializeNode(block);
156
+ if (serialized instanceof HTMLElement) {
157
+ dom.append(injectInnerWrapper({
158
+ node: serialized,
159
+ colorScheme: colorScheme,
160
+ isActive: isActive,
161
+ isInserted: isInserted
162
+ }));
163
+ }
164
+ });
165
+ if (dom.childNodes.length === 0) {
166
+ return undefined;
167
+ }
168
+ dom.setAttribute('data-testid', isInserted ? 'show-diff-changed-decoration' : 'show-diff-deleted-decoration');
169
+
170
+ // Before the cell's own blocks, as a block of its own, so it never lands inside a paragraph the
171
+ // displayed side may hide.
172
+ return Decoration.widget(pos + 1, dom, _objectSpread(_objectSpread({}, buildDiffDecorationSpec({
173
+ colorScheme: colorScheme,
174
+ decorationType: 'widget',
175
+ diffId: crypto.randomUUID(),
176
+ leftAnchorId: leftAnchorId,
177
+ isActive: isActive,
178
+ isInserted: isInserted,
179
+ side: -1
180
+ })), {}, {
181
+ marks: []
182
+ }));
183
+ };
184
+
185
+ /**
186
+ * The other side of a whole-cell replacement, drawn inside the live cells so the table keeps its
187
+ * shape: each live cell takes the other cell's background when it differs, and its content when that
188
+ * differs. A cell whose content did not change has nothing to diff.
189
+ */
190
+ export var createReplacedCellWidgets = function createReplacedCellWidgets(_ref3) {
191
+ var slice = _ref3.slice,
192
+ newDoc = _ref3.newDoc,
193
+ change = _ref3.change,
194
+ nodeViewSerializer = _ref3.nodeViewSerializer,
195
+ colorScheme = _ref3.colorScheme,
196
+ intl = _ref3.intl,
197
+ isActive = _ref3.isActive,
198
+ isInserted = _ref3.isInserted,
199
+ leftAnchorId = _ref3.leftAnchorId;
200
+ return getReplacedCells({
201
+ slice: slice,
202
+ doc: newDoc,
203
+ fromB: change.fromB,
204
+ toB: change.toB
205
+ }).flatMap(function (_ref4) {
206
+ var otherCell = _ref4.otherCell,
207
+ liveCell = _ref4.liveCell,
208
+ liveCellPos = _ref4.liveCellPos;
209
+ var decorations = [];
210
+ if (isInserted && isCellBackgroundChanged(otherCell, liveCell)) {
211
+ var cellEnd = liveCellPos + liveCell.nodeSize;
212
+ decorations.push(
213
+ // The cell becomes the layer's stacking context, so the layer sits above the cell's own
214
+ // background rather than behind it. Through `z-index` at the cell overlay's own level, not
215
+ // `isolation`: an isolated cell would trap its overlay below later cells, which then cover
216
+ // the overlay's outline where it overlaps their border. Properties the cell never sets, so
217
+ // removing them takes nothing of its own.
218
+ Decoration.node(liveCellPos, cellEnd, {
219
+ style: convertToInlineCss({
220
+ position: 'relative',
221
+ // The highest z-index a cell diff overlay takes (the rounded-corner variant).
222
+ zIndex: 2
223
+ })
224
+ }, {
225
+ key: "show-diff-new-cell-background-host-".concat(liveCellPos)
226
+ }),
227
+ // At the cell's end, beside its diff overlay, so the cell's first block stays its first child.
228
+ Decoration.widget(cellEnd - 1, createCellBackgroundLayer(getCellBackgroundColor(otherCell)), {
229
+ key: "show-diff-new-cell-background-".concat(liveCellPos),
230
+ side: -1,
231
+ marks: [],
232
+ ignoreSelection: true
233
+ }));
234
+ }
235
+ if (!isCellContentEqual(otherCell, liveCell)) {
236
+ var contentWidget = createCellContentWidget({
237
+ cell: otherCell,
238
+ pos: liveCellPos,
239
+ nodeViewSerializer: nodeViewSerializer,
240
+ colorScheme: colorScheme,
241
+ intl: intl,
242
+ isActive: isActive,
243
+ isInserted: isInserted,
244
+ leftAnchorId: leftAnchorId
245
+ });
246
+ if (contentWidget) {
247
+ decorations.push(contentWidget);
248
+ }
249
+ }
250
+ return decorations;
251
+ });
252
+ };
253
+
254
+ /**
255
+ * The displayed side of a whole-cell replacement. A table cell is never removed on its own, only
256
+ * replaced in place, so a live cell is never hidden — hiding it would leave the row a cell short. It
257
+ * takes the changed-cell treatment only when the cell itself changed; its content is diffed, and
258
+ * hidden in the clean view, only when that changed. A cell whose content stayed the same keeps
259
+ * showing it.
260
+ */
261
+ export var createReplacedCellDecorations = function createReplacedCellDecorations(_ref5) {
262
+ var replacedCells = _ref5.replacedCells,
263
+ change = _ref5.change,
264
+ doc = _ref5.doc,
265
+ attributionKey = _ref5.attributionKey,
266
+ colorScheme = _ref5.colorScheme,
267
+ hasDeletedWidget = _ref5.hasDeletedWidget,
268
+ hideAddedDiffsUnderline = _ref5.hideAddedDiffsUnderline,
269
+ isActive = _ref5.isActive,
270
+ isDeletedWidgetBelow = _ref5.isDeletedWidgetBelow,
271
+ isInserted = _ref5.isInserted,
272
+ leftAnchorId = _ref5.leftAnchorId,
273
+ reveal = _ref5.reveal,
274
+ shouldHideDeleted = _ref5.shouldHideDeleted,
275
+ showContributorTags = _ref5.showContributorTags,
276
+ showIndicators = _ref5.showIndicators,
277
+ tagMountContext = _ref5.tagMountContext;
278
+ return replacedCells.flatMap(function (_ref6) {
279
+ var otherCell = _ref6.otherCell,
280
+ liveCell = _ref6.liveCell,
281
+ liveCellPos = _ref6.liveCellPos;
282
+ var cellEnd = liveCellPos + liveCell.nodeSize;
283
+ var decorations = isCellChanged(otherCell, liveCell) ? createBlockChangedDecoration({
284
+ attributionKey: attributionKey,
285
+ change: {
286
+ from: liveCellPos,
287
+ to: cellEnd,
288
+ name: liveCell.type.name
289
+ },
290
+ colorScheme: colorScheme,
291
+ doc: doc,
292
+ isActive: isActive,
293
+ // The cell stays either way, so it reads as changed, not removed.
294
+ isInserted: true,
295
+ leftAnchorId: leftAnchorId,
296
+ showContributorTags: showContributorTags,
297
+ showIndicators: showIndicators,
298
+ tagMountContext: tagMountContext
299
+ }) : [];
300
+ if (isCellContentEqual(otherCell, liveCell)) {
301
+ return decorations;
302
+ }
303
+ decorations.push.apply(decorations, _toConsumableArray(createInlineChangedDecoration({
304
+ attributionKey: attributionKey,
305
+ change: _objectSpread(_objectSpread({}, change), {}, {
306
+ fromB: liveCellPos,
307
+ toB: cellEnd
308
+ }),
309
+ colorScheme: colorScheme,
310
+ doc: doc,
311
+ hasDeletedWidget: hasDeletedWidget,
312
+ hideAddedDiffsUnderline: hideAddedDiffsUnderline,
313
+ isActive: isActive,
314
+ isDeletedWidgetBelow: isDeletedWidgetBelow,
315
+ isInserted: isInserted,
316
+ leftAnchorId: leftAnchorId,
317
+ reveal: reveal,
318
+ shouldHideDeleted: shouldHideDeleted,
319
+ showContributorTags: showContributorTags,
320
+ showIndicators: showIndicators,
321
+ tagMountContext: tagMountContext
322
+ })));
323
+ if (shouldHideDeleted) {
324
+ liveCell.forEach(function (block, offset) {
325
+ var blockPos = liveCellPos + 1 + offset;
326
+ decorations.push.apply(decorations, _toConsumableArray(createBlockChangedDecoration({
327
+ change: {
328
+ from: blockPos,
329
+ to: blockPos + block.nodeSize,
330
+ name: block.type.name
331
+ },
332
+ colorScheme: colorScheme,
333
+ doc: doc,
334
+ isActive: isActive,
335
+ isInserted: isInserted,
336
+ leftAnchorId: leftAnchorId,
337
+ shouldHideDeleted: shouldHideDeleted
338
+ })));
339
+ });
340
+ }
341
+ return decorations;
342
+ });
343
+ };
@@ -0,0 +1,25 @@
1
+ import { getDiffableAttrNames } from './diffableAttrs';
2
+
3
+ /**
4
+ * A cell background as it renders: absent, empty and `"null"` — the string agent payloads send for
5
+ * no background — all mean the cell has none.
6
+ */
7
+ export var normalizeCellBackground = function normalizeCellBackground(background) {
8
+ return typeof background === 'string' && background !== '' && background !== 'null' ? background : null;
9
+ };
10
+
11
+ /** Whether two cells render different backgrounds. */
12
+ export var isCellBackgroundChanged = function isCellBackgroundChanged(left, right) {
13
+ return normalizeCellBackground(left.attrs.background) !== normalizeCellBackground(right.attrs.background);
14
+ };
15
+
16
+ /**
17
+ * Whether two cells differ by an attribute that represents a change, such as their background or
18
+ * span, as opposed to only their content.
19
+ */
20
+ export var isCellAttrChanged = function isCellAttrChanged(left, right) {
21
+ var _getDiffableAttrNames;
22
+ return ((_getDiffableAttrNames = getDiffableAttrNames(left.type.name, left.attrs)) !== null && _getDiffableAttrNames !== void 0 ? _getDiffableAttrNames : []).some(function (name) {
23
+ return name === 'background' ? isCellBackgroundChanged(left, right) : left.attrs[name] !== right.attrs[name];
24
+ });
25
+ };
@@ -2,6 +2,7 @@ import { tableMarginTop } from '@atlaskit/editor-common/styles/table';
2
2
  import { getStickyHeaderHeight } from '@atlaskit/editor-common/table/get-sticky-header-height';
3
3
  import { fg } from '@atlaskit/platform-feature-flags/fg';
4
4
  import { isDiffDecoration, SCROLL_TARGET_MARGIN_CSS_PROPERTY } from './decorations/decorationKeys';
5
+ import { getWidgetElement } from './decorations/utils/getWidgetElement';
5
6
 
6
7
  /**
7
8
  * Extra space above the scrolled-to element so it does not sit flush under the
@@ -19,21 +20,21 @@ function updateScrollTargetMargin(view, targetPos, api) {
19
20
  var tableHeaderHeight = isLimitedModeEnabled ? undefined : getStickyHeaderHeight(view, targetPos);
20
21
  view.dom.style.setProperty(SCROLL_TARGET_MARGIN_CSS_PROPERTY, "".concat(tableHeaderHeight ? tableHeaderHeight + tableMarginTop : 0, "px"));
21
22
  }
22
-
23
- /**
24
- * Returns the resolved HTMLElement for a given DOM node, walking up to the
25
- * parent element if the node itself is not an Element (e.g. a text node).
26
- */
27
23
  function scrollToSelection(node) {
28
- var element = node instanceof Element ? node : (node === null || node === void 0 ? void 0 : node.parentElement) instanceof Element ? node.parentElement : null;
24
+ // ProseMirror may return a text node; scroll its parent element instead.
25
+ var element = node instanceof Element ? node : node === null || node === void 0 ? void 0 : node.parentElement;
29
26
  if (!(element instanceof HTMLElement)) {
30
27
  return;
31
28
  }
29
+ var scrollOptions = {
30
+ behavior: 'smooth',
31
+ block: 'start'
32
+ };
33
+ if (fg('platform_editor_ai_suggestion_table_scroll_fix')) {
34
+ scrollOptions.inline = 'center';
35
+ }
32
36
  if (fg('platform_editor_ai_show_diff_patch_1')) {
33
- element.scrollIntoView({
34
- behavior: 'smooth',
35
- block: 'start'
36
- });
37
+ element.scrollIntoView(scrollOptions);
37
38
  return;
38
39
  }
39
40
 
@@ -41,14 +42,25 @@ function scrollToSelection(node) {
41
42
  var previousScrollMarginTop = element.style.scrollMarginTop;
42
43
  element.style.scrollMarginTop = "".concat(SCROLL_TOP_MARGIN_PX, "px");
43
44
  try {
44
- element.scrollIntoView({
45
- behavior: 'smooth',
46
- block: 'start'
47
- });
45
+ element.scrollIntoView(scrollOptions);
48
46
  } finally {
49
47
  element.style.scrollMarginTop = previousScrollMarginTop;
50
48
  }
51
49
  }
50
+ function resolveScrollTarget(view, target) {
51
+ var _view$domAtPos;
52
+ if (isDiffDecoration(target) && target.spec.decorationType === 'widget') {
53
+ return getWidgetElement(target);
54
+ }
55
+ if (isDiffDecoration(target) && fg('platform_editor_ai_show_diff_patch_2')) {
56
+ var _Array$from$find;
57
+ return (_Array$from$find = Array.from(view.dom.querySelectorAll('[data-diff-id]')).find(function (element) {
58
+ return element.dataset.diffId === target.spec.diffId;
59
+ })) !== null && _Array$from$find !== void 0 ? _Array$from$find : view.nodeDOM(target.from);
60
+ }
61
+ var targetNode = view.nodeDOM(target.from);
62
+ return targetNode instanceof Element ? targetNode : (_view$domAtPos = view.domAtPos(target.from)) === null || _view$domAtPos === void 0 ? void 0 : _view$domAtPos.node;
63
+ }
52
64
 
53
65
  /**
54
66
  * Schedules scrolling to the decoration at the given index after the next frame. Defaults to the
@@ -68,30 +80,27 @@ export var scrollToDecoration = function scrollToDecoration(view, decorations) {
68
80
  // deleted-content widget rather than at the group's `from` position — see `scrollTarget` in
69
81
  // `getScrollableDecorations`. Only set under `platform_editor_ai_show_diff_patch_1`.
70
82
  var target = isDiffDecoration(decoration) && decoration.spec.scrollTarget || decoration;
71
- var rafId = requestAnimationFrame(function () {
72
- rafId = null;
83
+ var rafId = null;
84
+ var cancelled = false;
85
+ var scrollToTarget = function scrollToTarget() {
86
+ if (view.isDestroyed) {
87
+ return;
88
+ }
73
89
  if (fg('platform_editor_ai_show_diff_patch_1')) {
74
90
  updateScrollTargetMargin(view, target.from, api);
75
91
  }
76
- if (isDiffDecoration(target) && target.spec.decorationType === 'widget') {
77
- var _target$type;
78
- // @ts-expect-error - decoration.type is not typed public API
79
- var widgetDom = target === null || target === void 0 || (_target$type = target.type) === null || _target$type === void 0 ? void 0 : _target$type.toDOM;
80
- scrollToSelection(widgetDom);
81
- } else if (isDiffDecoration(target) && fg('platform_editor_ai_show_diff_patch_2')) {
82
- var _Array$from$find;
83
- var targetNode = (_Array$from$find = Array.from(view.dom.querySelectorAll('[data-diff-id]')).find(function (element) {
84
- return element.dataset.diffId === target.spec.diffId;
85
- })) !== null && _Array$from$find !== void 0 ? _Array$from$find : view.nodeDOM(target === null || target === void 0 ? void 0 : target.from);
86
- scrollToSelection(targetNode);
87
- } else {
88
- var _view$domAtPos;
89
- var _targetNode = view.nodeDOM(target === null || target === void 0 ? void 0 : target.from);
90
- var node = _targetNode instanceof Element ? _targetNode : (_view$domAtPos = view.domAtPos(target === null || target === void 0 ? void 0 : target.from)) === null || _view$domAtPos === void 0 ? void 0 : _view$domAtPos.node;
91
- scrollToSelection(node);
92
+ scrollToSelection(resolveScrollTarget(view, target));
93
+ };
94
+
95
+ // Called from the plugin's post-DOM update hook; defer measurement until layout can settle.
96
+ rafId = requestAnimationFrame(function () {
97
+ rafId = null;
98
+ if (!cancelled) {
99
+ scrollToTarget();
92
100
  }
93
101
  });
94
102
  return function () {
103
+ cancelled = true;
95
104
  if (rafId !== null) {
96
105
  cancelAnimationFrame(rafId);
97
106
  rafId = null;
@@ -0,0 +1,67 @@
1
+ import type { Change } from 'prosemirror-changeset';
2
+ import type { IntlShape } from 'react-intl';
3
+ import type { Node as PMNode, Slice } from '@atlaskit/editor-prosemirror/model';
4
+ import { Decoration } from '@atlaskit/editor-prosemirror/view';
5
+ import type { RevealOptions } from '../../showDiffPluginType';
6
+ import type { NodeViewSerializer } from '../NodeViewSerializer';
7
+ import type { ColorScheme } from './colorSchemes/types';
8
+ import type { ContributorTagMountContext } from './createContributorTagWidget';
9
+ export type ReplacedCell = {
10
+ /** The live cell in the displayed document. */
11
+ liveCell: PMNode;
12
+ liveCellPos: number;
13
+ /** The other side's cell, from the replaced slice. */
14
+ otherCell: PMNode;
15
+ };
16
+ /**
17
+ * Pairs each cell of a whole-cell replacement with the live cell it replaces, or `[]` when the slice
18
+ * is not a run of whole cells matching the live ones. Whole-cell replacement preserves cell count and
19
+ * order, so the nth live cell in the range matches the nth cell in the slice.
20
+ */
21
+ export declare const getReplacedCells: ({ slice, doc, fromB, toB, }: {
22
+ doc: PMNode;
23
+ fromB: number;
24
+ slice: Slice;
25
+ toB: number;
26
+ }) => ReplacedCell[];
27
+ /**
28
+ * The other side of a whole-cell replacement, drawn inside the live cells so the table keeps its
29
+ * shape: each live cell takes the other cell's background when it differs, and its content when that
30
+ * differs. A cell whose content did not change has nothing to diff.
31
+ */
32
+ export declare const createReplacedCellWidgets: ({ slice, newDoc, change, nodeViewSerializer, colorScheme, intl, isActive, isInserted, leftAnchorId, }: {
33
+ change: Pick<Change, 'fromB' | 'toB'>;
34
+ colorScheme?: ColorScheme;
35
+ intl: IntlShape;
36
+ isActive?: boolean;
37
+ isInserted: boolean;
38
+ leftAnchorId?: string;
39
+ newDoc: PMNode;
40
+ nodeViewSerializer: NodeViewSerializer;
41
+ slice: Slice;
42
+ }) => Decoration[];
43
+ /**
44
+ * The displayed side of a whole-cell replacement. A table cell is never removed on its own, only
45
+ * replaced in place, so a live cell is never hidden — hiding it would leave the row a cell short. It
46
+ * takes the changed-cell treatment only when the cell itself changed; its content is diffed, and
47
+ * hidden in the clean view, only when that changed. A cell whose content stayed the same keeps
48
+ * showing it.
49
+ */
50
+ export declare const createReplacedCellDecorations: ({ replacedCells, change, doc, attributionKey, colorScheme, hasDeletedWidget, hideAddedDiffsUnderline, isActive, isDeletedWidgetBelow, isInserted, leftAnchorId, reveal, shouldHideDeleted, showContributorTags, showIndicators, tagMountContext, }: {
51
+ attributionKey?: string;
52
+ change: Pick<Change, 'fromA' | 'toA' | 'fromB' | 'toB'>;
53
+ colorScheme?: ColorScheme;
54
+ doc: PMNode;
55
+ hasDeletedWidget?: boolean;
56
+ hideAddedDiffsUnderline?: boolean;
57
+ isActive?: boolean;
58
+ isDeletedWidgetBelow?: boolean;
59
+ isInserted: boolean;
60
+ leftAnchorId?: string;
61
+ replacedCells: ReplacedCell[];
62
+ reveal?: RevealOptions;
63
+ shouldHideDeleted?: boolean;
64
+ showContributorTags?: boolean;
65
+ showIndicators?: boolean;
66
+ tagMountContext?: ContributorTagMountContext;
67
+ }) => Decoration[];
@@ -0,0 +1,13 @@
1
+ import type { Node as PMNode } from '@atlaskit/editor-prosemirror/model';
2
+ /**
3
+ * A cell background as it renders: absent, empty and `"null"` — the string agent payloads send for
4
+ * no background — all mean the cell has none.
5
+ */
6
+ export declare const normalizeCellBackground: (background: unknown) => string | null;
7
+ /** Whether two cells render different backgrounds. */
8
+ export declare const isCellBackgroundChanged: (left: PMNode, right: PMNode) => boolean;
9
+ /**
10
+ * Whether two cells differ by an attribute that represents a change, such as their background or
11
+ * span, as opposed to only their content.
12
+ */
13
+ export declare const isCellAttrChanged: (left: PMNode, right: PMNode) => boolean;
@@ -1,4 +1,4 @@
1
- import type { NodeViewConstructor } from '@atlaskit/editor-common/lazy-node-view';
1
+ import type { NodeViewConstructor } from '@atlaskit/editor-common/lazy-node-view/types';
2
2
  import type { Node as PMNode } from '@atlaskit/editor-prosemirror/model';
3
3
  import type { EditorView } from '@atlaskit/editor-prosemirror/view';
4
4
  /** Where to look up a mark's renderer, and the view to render it against. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaskit/editor-plugin-show-diff",
3
- "version": "26.0.0",
3
+ "version": "26.0.2",
4
4
  "description": "ShowDiff plugin for @atlaskit/editor-core",
5
5
  "author": "Atlassian Pty Ltd",
6
6
  "license": "Apache-2.0",
@@ -77,6 +77,10 @@
77
77
  "react-intl": "^5.25.1 || ^6.0.0 || ^7.0.0"
78
78
  },
79
79
  "platform-feature-flags": {
80
+ "platform_editor_ai_suggestion_table_scroll_fix": {
81
+ "type": "boolean",
82
+ "referenceOnly": true
83
+ },
80
84
  "platform_editor_diff_reveal_animation": {
81
85
  "type": "boolean"
82
86
  },