@atlaskit/editor-plugin-show-diff 20.0.1 → 20.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 (35) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/cjs/pm-plugins/calculateDiff/calculateDiffDecorations.js +9 -10
  3. package/dist/cjs/pm-plugins/calculateDiff/groupDeletedColumnChanges.js +92 -26
  4. package/dist/cjs/pm-plugins/calculateDiff/simplifyChangesWithAttribution.js +46 -22
  5. package/dist/cjs/pm-plugins/decorations/colorSchemes/attributions.js +4 -5
  6. package/dist/cjs/pm-plugins/decorations/createChangedRowDecorationWidgets.js +4 -1
  7. package/dist/cjs/pm-plugins/decorations/createNodeChangedDecorationWidget.js +1 -1
  8. package/dist/cjs/pm-plugins/decorations/extractContributorTags.js +3 -1
  9. package/dist/cjs/pm-plugins/getScrollableDecorations.js +1 -8
  10. package/dist/cjs/pm-plugins/main.js +5 -2
  11. package/dist/es2019/pm-plugins/calculateDiff/calculateDiffDecorations.js +9 -10
  12. package/dist/es2019/pm-plugins/calculateDiff/groupDeletedColumnChanges.js +85 -14
  13. package/dist/es2019/pm-plugins/calculateDiff/simplifyChangesWithAttribution.js +42 -22
  14. package/dist/es2019/pm-plugins/decorations/colorSchemes/attributions.js +4 -5
  15. package/dist/es2019/pm-plugins/decorations/createChangedRowDecorationWidgets.js +4 -1
  16. package/dist/es2019/pm-plugins/decorations/createNodeChangedDecorationWidget.js +1 -1
  17. package/dist/es2019/pm-plugins/decorations/extractContributorTags.js +1 -1
  18. package/dist/es2019/pm-plugins/getScrollableDecorations.js +1 -6
  19. package/dist/es2019/pm-plugins/main.js +5 -1
  20. package/dist/esm/pm-plugins/calculateDiff/calculateDiffDecorations.js +9 -10
  21. package/dist/esm/pm-plugins/calculateDiff/groupDeletedColumnChanges.js +92 -26
  22. package/dist/esm/pm-plugins/calculateDiff/simplifyChangesWithAttribution.js +46 -22
  23. package/dist/esm/pm-plugins/decorations/colorSchemes/attributions.js +4 -5
  24. package/dist/esm/pm-plugins/decorations/createChangedRowDecorationWidgets.js +4 -1
  25. package/dist/esm/pm-plugins/decorations/createNodeChangedDecorationWidget.js +1 -1
  26. package/dist/esm/pm-plugins/decorations/extractContributorTags.js +3 -1
  27. package/dist/esm/pm-plugins/getScrollableDecorations.js +1 -8
  28. package/dist/esm/pm-plugins/main.js +5 -2
  29. package/dist/types/pm-plugins/calculateDiff/calculateDiffDecorations.d.ts +6 -4
  30. package/dist/types/pm-plugins/calculateDiff/simplifyChangesWithAttribution.d.ts +2 -2
  31. package/dist/types/pm-plugins/decorations/colorSchemes/attributions.d.ts +2 -3
  32. package/dist/types/pm-plugins/decorations/createNodeChangedDecorationWidget.d.ts +1 -0
  33. package/dist/types/pm-plugins/decorations/extractContributorTags.d.ts +1 -0
  34. package/dist/types/pm-plugins/main.d.ts +1 -0
  35. package/package.json +2 -2
@@ -2,7 +2,6 @@ import { areNodesEqualIgnoreAttrs } from '@atlaskit/editor-common/utils/document
2
2
  import { Fragment, Slice } from '@atlaskit/editor-prosemirror/model';
3
3
  import { TableMap } from '@atlaskit/editor-tables/table-map';
4
4
  export const CELL_CONTENT_OFFSET = 2;
5
- const IGNORED_CELL_ATTRS = ['localId', 'colwidth'];
6
5
  const tablesIn = doc => {
7
6
  const tables = [];
8
7
  doc.descendants((node, pos) => {
@@ -47,21 +46,92 @@ const positionMatchedColumns = ({
47
46
  }
48
47
  return surviving;
49
48
  };
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;
49
+ const headerLocalIds = table => {
50
+ var _table$node$firstChil, _table$node$firstChil2;
51
+ return ((_table$node$firstChil = (_table$node$firstChil2 = table.node.firstChild) === null || _table$node$firstChil2 === void 0 ? void 0 : _table$node$firstChil2.children) !== null && _table$node$firstChil !== void 0 ? _table$node$firstChil : []).map(cell => {
52
+ var _cell$attrs$localId;
53
+ return (_cell$attrs$localId = cell.attrs.localId) !== null && _cell$attrs$localId !== void 0 ? _cell$attrs$localId : undefined;
54
+ });
55
+ };
56
+
57
+ /**
58
+ * Cells keep their `localId` when a column is deleted, so the first row's ids name each remaining
59
+ * column even when the steps replaced the whole table. Missing or repeated ids, such as in content
60
+ * pasted from elsewhere, cannot name a column.
61
+ */
62
+ const localIdMatchedColumns = (before, after) => {
63
+ const beforeIds = headerLocalIds(before);
64
+ if (beforeIds.some(id => !id) || new Set(beforeIds).size !== beforeIds.length) {
65
+ return undefined;
66
+ }
67
+ const surviving = new Set();
68
+ let previousColumn = -1;
69
+ for (const id of headerLocalIds(after)) {
70
+ const column = id ? beforeIds.indexOf(id) : -1;
71
+ if (column <= previousColumn) {
72
+ return undefined;
73
+ }
74
+ surviving.add(column);
75
+ previousColumn = column;
76
+ }
77
+ return surviving;
78
+ };
79
+ const IGNORED_CELL_ATTRS = ['localId', 'colwidth'];
80
+
81
+ /** Rows may also have been deleted, so the remaining cells only need to appear in order. */
82
+ const isColumnKept = (before, beforeColumn, after, afterColumn) => {
83
+ let beforeRow = 0;
84
+ return after.node.children.every(afterRow => {
85
+ const afterCell = afterRow.maybeChild(afterColumn);
86
+ while (beforeRow < before.node.childCount) {
87
+ const beforeCell = before.node.child(beforeRow++).maybeChild(beforeColumn);
88
+ if (afterCell && beforeCell && areNodesEqualIgnoreAttrs(beforeCell, afterCell, IGNORED_CELL_ATTRS)) {
89
+ return true;
90
+ }
91
+ }
92
+ return false;
93
+ });
94
+ };
95
+
96
+ /**
97
+ * Pairs each remaining column with an equal original column, keeping their order, and takes each
98
+ * pair from the `direction` end first. Returns the original column of each remaining column.
99
+ */
100
+ const pairColumnsInOrder = (beforeWidth, afterWidth, isMatch, direction) => {
101
+ const step = direction === 'left' ? 1 : -1;
102
+ const pairing = [];
103
+ let beforeColumn = direction === 'left' ? 0 : beforeWidth - 1;
104
+ let afterColumn = direction === 'left' ? 0 : afterWidth - 1;
105
+ for (; afterColumn >= 0 && afterColumn < afterWidth; afterColumn += step) {
106
+ while (beforeColumn >= 0 && beforeColumn < beforeWidth && !isMatch(beforeColumn, afterColumn)) {
107
+ beforeColumn += step;
108
+ }
109
+ if (beforeColumn < 0 || beforeColumn >= beforeWidth) {
110
+ return undefined;
58
111
  }
59
- surviving.add(matches[0]);
112
+ pairing.push(beforeColumn);
113
+ beforeColumn += step;
114
+ }
115
+ return direction === 'left' ? pairing : pairing.reverse();
116
+ };
117
+
118
+ /**
119
+ * For tables whose cells have no usable ids, such as content created before cells had ids. Every
120
+ * valid pairing of remaining columns to original ones lies between the leftmost and the rightmost
121
+ * pairing, so the columns are only reported when those two agree. Otherwise, such as when several
122
+ * original columns are identical, which of them were removed cannot be known.
123
+ */
124
+ const uniquelyContentMatchedColumns = (before, after, beforeWidth, afterWidth) => {
125
+ const isMatch = (beforeColumn, afterColumn) => isColumnKept(before, beforeColumn, after, afterColumn);
126
+ const leftmost = pairColumnsInOrder(beforeWidth, afterWidth, isMatch, 'left');
127
+ const rightmost = pairColumnsInOrder(beforeWidth, afterWidth, isMatch, 'right');
128
+ if (!leftmost || !rightmost || leftmost.some((column, index) => column !== rightmost[index])) {
129
+ return undefined;
60
130
  }
61
- const ordered = [...surviving];
62
- return !ordered.some((column, index) => index > 0 && column <= ordered[index - 1]);
131
+ return new Set(leftmost);
63
132
  };
64
133
  const findDeletedColumns = (before, after, afterToBefore) => {
134
+ var _localIdMatchedColumn;
65
135
  const beforeMap = TableMap.get(before.node);
66
136
  const afterMap = TableMap.get(after.node);
67
137
  // Merged or malformed tables do not have reliable column indices.
@@ -69,13 +139,14 @@ const findDeletedColumns = (before, after, afterToBefore) => {
69
139
  return [];
70
140
  }
71
141
  const beforeOffsets = columnOffsets(beforeMap, before.node);
72
- const surviving = positionMatchedColumns({
142
+ const positionMatched = positionMatchedColumns({
73
143
  before,
74
144
  after,
75
145
  afterToBefore,
76
146
  beforeOffsets
77
147
  });
78
- if (surviving.size !== afterMap.width && !addContentMatchedColumns(surviving, before, after)) {
148
+ const surviving = positionMatched.size === afterMap.width ? positionMatched : (_localIdMatchedColumn = localIdMatchedColumns(before, after)) !== null && _localIdMatchedColumn !== void 0 ? _localIdMatchedColumn : uniquelyContentMatchedColumns(before, after, beforeMap.width, afterMap.width);
149
+ if (!surviving) {
79
150
  return [];
80
151
  }
81
152
  return beforeOffsets.flatMap((_, column) => surviving.has(column) ? [] : [column]);
@@ -24,6 +24,7 @@ const getAttributionIdentity = change => {
24
24
  mergeable: true
25
25
  };
26
26
  };
27
+ const hasContributor = change => [...change.deleted, ...change.inserted].some(span => getAttributionKey(span.data) !== undefined);
27
28
  const byNewDocRange = (left, right) => left.fromB - right.fromB || left.toB - right.toB;
28
29
 
29
30
  /** Union of the given ranges, with overlapping and touching ones merged. */
@@ -74,12 +75,28 @@ const blockNodeRanges = (doc, change, ownsPosition) => {
74
75
  * whoever wrote last (EDITOR-8932).
75
76
  */
76
77
  const splitMixedIdentityChanges = (changes, doc) => changes.flatMap(change => {
77
- if (getAttributionIdentity(change).mergeable) {
78
- return [change];
78
+ var _change$inserted$;
79
+ // A replacement can carry an anonymous deletion and an authored insertion. The deleted side
80
+ // needs its own zero-width change or the insertion's actor claims the deletion too.
81
+ const deletedKeys = new Set(change.deleted.map(span => getAttributionKey(span.data)));
82
+ const firstInsertedKey = getAttributionKey((_change$inserted$ = change.inserted[0]) === null || _change$inserted$ === void 0 ? void 0 : _change$inserted$.data);
83
+ const detachDeleted = change.deleted.length > 0 && change.inserted.length > 0 && deletedKeys.size === 1 && !deletedKeys.has(firstInsertedKey) && (deletedKeys.has(undefined) || firstInsertedKey === undefined);
84
+ const deletedChange = detachDeleted ? [{
85
+ ...change,
86
+ toB: change.fromB,
87
+ inserted: []
88
+ }] : [];
89
+ const contentChange = detachDeleted ? {
90
+ ...change,
91
+ fromA: change.toA,
92
+ deleted: []
93
+ } : change;
94
+ if (getAttributionIdentity(contentChange).mergeable) {
95
+ return [...deletedChange, contentChange];
79
96
  }
80
97
  const runs = [];
81
- let offset = change.fromB;
82
- for (const span of change.inserted) {
98
+ let offset = contentChange.fromB;
99
+ for (const span of contentChange.inserted) {
83
100
  const key = getAttributionKey(span.data);
84
101
  const last = runs[runs.length - 1];
85
102
  if (last && last.key === key) {
@@ -97,31 +114,31 @@ const splitMixedIdentityChanges = (changes, doc) => changes.flatMap(change => {
97
114
  }
98
115
 
99
116
  // The offsets above are only positions if the spans partition the inserted range.
100
- if (runs.length < 2 || offset !== change.toB) {
101
- return [change];
117
+ if (runs.length < 2 || offset !== contentChange.toB) {
118
+ return [...deletedChange, contentChange];
102
119
  }
103
- return runs.map((run, index) => {
120
+ return [...deletedChange, ...runs.map((run, index) => {
104
121
  const owned = blockNodeRanges(doc, {
105
- from: change.fromB,
106
- to: change.toB
122
+ from: contentChange.fromB,
123
+ to: contentChange.toB
107
124
  }, pos => pos >= run.from && pos < run.to);
108
125
  return {
109
- ...change,
110
- fromA: index === 0 ? change.fromA : change.toA,
126
+ ...contentChange,
127
+ fromA: index === 0 ? contentChange.fromA : contentChange.toA,
111
128
  fromB: run.from,
112
129
  toB: run.to,
113
- deleted: index === 0 ? change.deleted : [],
130
+ deleted: index === 0 ? contentChange.deleted : [],
114
131
  inserted: run.spans,
115
132
  ...(owned ? {
116
133
  blockNodeRangesB: owned
117
134
  } : {})
118
135
  };
119
- });
136
+ })];
120
137
  });
121
138
 
122
139
  /**
123
- * Subtracts every other author's contained change from a change's range, so both contributions
124
- * survive. Applies when the contributors arrive as separate changes rather than as spans of one —
140
+ * Subtracts every differently attributed contained change from a change's range, including when
141
+ * one has no contributor. Applies when the changes arrive separately rather than as spans of one —
125
142
  * the `step` diff type. A contained change by the same author, and one that only partially
126
143
  * overlaps, are left for the collapse below.
127
144
  *
@@ -129,11 +146,12 @@ const splitMixedIdentityChanges = (changes, doc) => changes.flatMap(change => {
129
146
  */
130
147
  const splitContainedChanges = (changes, doc) => changes.flatMap(change => {
131
148
  const {
132
- key
149
+ key,
150
+ mergeable
133
151
  } = getAttributionIdentity(change);
134
152
  const inner = mergeRanges(changes.filter(other => {
135
- const otherKey = getAttributionIdentity(other).key;
136
- return other !== change && key !== undefined && otherKey !== undefined && otherKey !== key && isContained(change, other);
153
+ const otherIdentity = getAttributionIdentity(other);
154
+ return other !== change && mergeable && otherIdentity.mergeable && otherIdentity.key !== key && isContained(change, other);
137
155
  }).map(({
138
156
  fromB,
139
157
  toB
@@ -196,7 +214,7 @@ const splitContainedChanges = (changes, doc) => changes.flatMap(change => {
196
214
  */
197
215
  const mergeBlockNodeRanges = (left, right) => left.blockNodeRangesB && right.blockNodeRangesB ? [...left.blockNodeRangesB, ...right.blockNodeRangesB] : undefined;
198
216
 
199
- /** Unions the changes whose new-document ranges strictly overlap, leaving touching ones alone. */
217
+ /** Unions overlapping changes, except when only one side names a contributor. */
200
218
  const collapse = changes => {
201
219
  // A zero-length new-document range paints no inline decoration, so it is kept aside rather than
202
220
  // folded into a neighbouring insertion.
@@ -204,7 +222,9 @@ const collapse = changes => {
204
222
  const collapsed = [];
205
223
  for (const change of changes.filter(change => change.toB > change.fromB).sort(byNewDocRange)) {
206
224
  const current = collapsed[collapsed.length - 1];
207
- if (current && change.fromB < current.toB) {
225
+ const currentHasContributor = current && hasContributor(current);
226
+ const changeHasContributor = hasContributor(change);
227
+ if (current && change.fromB < current.toB && currentHasContributor === changeHasContributor) {
208
228
  const blockNodeRangesB = mergeBlockNodeRanges(current, change);
209
229
  collapsed[collapsed.length - 1] = {
210
230
  fromA: Math.min(current.fromA, change.fromA),
@@ -232,8 +252,8 @@ const collapse = changes => {
232
252
  * characters — which stacks two inline decorations and renders one contributor tag per copy.
233
253
  *
234
254
  * Merging keeps every span from both sides, so the latest-writer rule still decides whose tag is
235
- * shown. Ranges that merely touch are left alone, so an unattributed edit next to an attributed one
236
- * is not credited to it.
255
+ * shown among attributed edits. An unattributed change never merges with an attributed one.
256
+ * Ranges that merely touch are left alone.
237
257
  *
238
258
  * Contributors are separated first, by `splitMixedIdentityChanges` and `splitContainedChanges`.
239
259
  * Both leave ranges that only touch, so they survive the collapse.
@@ -183,13 +183,12 @@ const changesOverlap = (change, attributedChange) => {
183
183
  const getAttributionKeyForChanges = changes => getAttributionKey(getLatestAttribution(changes));
184
184
 
185
185
  /**
186
- * The attribution key owning a change. Inline and step changes retain direct attribution spans, so
187
- * resolve those first. Grouped and smart-classified changes can lose that data; for those, fall
188
- * back to the latest overlapping attributed step.
186
+ * Preserve overlap attribution outside Confluence version history. Within version history, a
187
+ * recovery step's keyless spans must not inherit an overlapping NCS step's actor.
189
188
  */
190
189
  export const getAttributionKeyForChange = (change, attributedChanges) => {
191
- var _getAttributionKeyFor;
192
- return (_getAttributionKeyFor = getAttributionKeyForChanges([change])) !== null && _getAttributionKeyFor !== void 0 ? _getAttributionKeyFor : getAttributionKeyForChanges(attributedChanges.filter(attributedChange => changesOverlap(change, attributedChange)));
190
+ const directKey = getAttributionKeyForChanges([change]);
191
+ return directKey !== null && directKey !== void 0 ? directKey : fg('confluence_ncs_step_diffing_version_history') ? undefined : getAttributionKeyForChanges(attributedChanges.filter(attributedChange => changesOverlap(change, attributedChange)));
193
192
  };
194
193
  export const getColorSchemeForChange = (change, attributedChanges, attributionColors, fallback) => {
195
194
  var _attributionColors$ge;
@@ -196,7 +196,7 @@ const createChangedRowDOM = ({
196
196
  }
197
197
  return tr;
198
198
  };
199
- const supportsAnchorPositioning = () => typeof CSS !== 'undefined' && typeof CSS.supports === 'function' && CSS.supports('top', 'anchor(--a top)');
199
+ const supportsAnchorPositioning = () => typeof CSS !== 'undefined' && typeof CSS.supports === 'function' && CSS.supports('top', 'anchor(--a top)') && CSS.supports('position-anchor', '--a');
200
200
  const createAnchoredRemovedLozengeWidget = ({
201
201
  anchorName,
202
202
  colorScheme,
@@ -207,6 +207,9 @@ const createAnchoredRemovedLozengeWidget = ({
207
207
  const lozenge = createRemovedLozenge(intl, isActive, colorScheme, true);
208
208
  const inset = lozenge.style.top;
209
209
  lozenge.style.setProperty('position', 'fixed');
210
+ // Fixed labels only follow their default anchor on scroll, and escape scroll-container clipping.
211
+ lozenge.style.setProperty('position-anchor', `--${anchorName}`);
212
+ lozenge.style.setProperty('position-visibility', 'anchors-visible');
210
213
  lozenge.style.setProperty('top', `calc(anchor(--${anchorName} top) + ${inset})`);
211
214
  lozenge.style.setProperty('left', `calc(anchor(--${anchorName} right) - ${inset})`);
212
215
  lozenge.style.setProperty('right', 'auto');
@@ -139,7 +139,7 @@ export const createNodeChangedDecorationWidget = ({
139
139
  // node renders beneath its replacement; otherwise anchor at the start (change.fromB).
140
140
  const anchorPos = placeBelow ? change.toB : change.fromB;
141
141
  const safeInsertPos = findSafeInsertPos(newDoc, anchorPos, slice);
142
- const isActive = activeIndexPos && (fg('confluence_ncs_step_diffing_version_history') ? safeInsertPos >= activeIndexPos.from && safeInsertPos <= activeIndexPos.to : safeInsertPos === activeIndexPos.from && safeInsertPos === activeIndexPos.to);
142
+ const isActive = activeIndexPos && (fg('confluence_ncs_step_diffing_version_history') ? safeInsertPos >= activeIndexPos.from && safeInsertPos <= activeIndexPos.to && (!('attributionKey' in activeIndexPos) || activeIndexPos.attributionKey === attributionKey) : safeInsertPos === activeIndexPos.from && safeInsertPos === activeIndexPos.to);
143
143
  if (deletedBlankLineCount > 0) {
144
144
  // The blocks have no content to serialize, so the deletion is shown as one struck-through
145
145
  // return glyph per line the author removed. A single widget, not one per line: the lines are
@@ -72,7 +72,7 @@ const owningStop = (target, stops) => stops.reduce((narrowest, stop) => stop.fro
72
72
  * group's union range touches, so several tags revealed over each other (EDITOR-8971). Hence
73
73
  * containment, and the leading change of what it covers.
74
74
  */
75
- const resolveActiveTarget = (surviving, activeIndexPos) => containedBy(surviving, activeIndexPos)[0];
75
+ const resolveActiveTarget = (surviving, activeIndexPos) => containedBy(surviving, activeIndexPos).find(target => !fg('confluence_ncs_step_diffing_version_history') || !('attributionKey' in activeIndexPos) || target.decoration.spec.attributionKey === activeIndexPos.attributionKey);
76
76
 
77
77
  /**
78
78
  * One model per diff decoration whose attribution resolves to a supplied contributor; anything
@@ -77,15 +77,12 @@ function groupTouchingDecorations(decorations, isInlineDecoration) {
77
77
  let clusterTo = 0;
78
78
  const sortedDecorations = [...decorations].sort((a, b) => a.from === b.from ? a.to - b.to : a.from - b.from);
79
79
  sortedDecorations.forEach(decoration => {
80
- var _openGroups$find;
81
80
  // A gap ends the run: nothing before it can be joined.
82
81
  if (decoration.from > clusterTo) {
83
82
  openGroups = [];
84
83
  }
85
84
  const contributor = contributorOf(decoration);
86
- // An unattributed decoration names nobody to keep apart, so it joins whatever is open — a
87
- // decision item's own highlight must not split the stop from the tag on its list.
88
- const openGroup = (_openGroups$find = openGroups.find(group => group.contributor === contributor)) !== null && _openGroups$find !== void 0 ? _openGroups$find : contributor === '' ? openGroups[0] : openGroups.find(group => group.contributor === '');
85
+ const openGroup = openGroups.find(group => group.contributor === contributor);
89
86
  if (openGroup === undefined) {
90
87
  const group = {
91
88
  contributor,
@@ -96,8 +93,6 @@ function groupTouchingDecorations(decorations, isInlineDecoration) {
96
93
  groups.push(group);
97
94
  openGroups = [...openGroups, group];
98
95
  } else {
99
- // The first contributor to join claims the group, so the next one still starts its own.
100
- openGroup.contributor = openGroup.contributor || contributor;
101
96
  openGroup.decorations.push(decoration);
102
97
  openGroup.to = Math.max(openGroup.to, decoration.to);
103
98
  }
@@ -4,6 +4,7 @@ import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
4
4
  import { PluginKey } from '@atlaskit/editor-prosemirror/state';
5
5
  import { Step as ProseMirrorStep } from '@atlaskit/editor-prosemirror/transform-override';
6
6
  import { DecorationSet } from '@atlaskit/editor-prosemirror/view';
7
+ import { fg } from '@atlaskit/platform-feature-flags/fg';
7
8
  import { getActiveDiffAnnouncement } from '../ui/DiffNavigation/announceActiveDiff';
8
9
  import { calculateDiffDecorations } from './calculateDiff/calculateDiffDecorations';
9
10
  import { isDiffDecoration, isDiffDecorationSpec } from './decorations/decorationKeys';
@@ -174,7 +175,10 @@ export const createPlugin = (config, getIntl, api, onEditorView) => {
174
175
  activeIndex: nextIndex,
175
176
  activeIndexPos: activeDecoration ? {
176
177
  from: activeDecoration.from,
177
- to: activeDecoration.to
178
+ to: activeDecoration.to,
179
+ ...(fg('confluence_ncs_step_diffing_version_history') ? {
180
+ attributionKey: isDiffDecoration(activeDecoration) ? activeDecoration.spec.attributionKey : undefined
181
+ } : {})
178
182
  } : undefined
179
183
  };
180
184
  // Recalculate decorations with the new active index
@@ -120,19 +120,18 @@ var leafTextblockRanges = function leafTextblockRanges(doc, from, to) {
120
120
  // once it has more leaf textblocks (~cells) than this threshold. Small tables
121
121
  // keep the precise per-cell path.
122
122
  var LARGE_TABLE_LEAF_THRESHOLD = 40;
123
-
124
123
  /**
125
- * An active scrollable change may contain several underlying diff changes. Treat ranges that
126
- * overlap or touch the active range as part of the same active customer-facing edit.
124
+ * An active scrollable change may contain several underlying diff changes. With attribution, only
125
+ * ranges belonging to the selected contributor (including the anonymous contributor) are active.
127
126
  */
128
- var isRangeActive = function isRangeActive(activeIndexPos, from, to) {
127
+ var isRangeActive = function isRangeActive(activeIndexPos, from, to, attributionKey) {
129
128
  if (activeIndexPos === undefined) {
130
129
  return undefined;
131
130
  }
132
131
  if (!fg('confluence_ncs_step_diffing_version_history')) {
133
132
  return from === activeIndexPos.from && to === activeIndexPos.to;
134
133
  }
135
- return from <= activeIndexPos.to && activeIndexPos.from <= to;
134
+ return from <= activeIndexPos.to && activeIndexPos.from <= to && (!('attributionKey' in activeIndexPos) || activeIndexPos.attributionKey === attributionKey);
136
135
  };
137
136
  var isLargeInsertedTableRange = function isLargeInsertedTableRange(doc, from, to) {
138
137
  var containsTable = false;
@@ -207,7 +206,7 @@ var calculateNodesForBlockDecoration = function calculateNodesForBlockDecoration
207
206
  if (!(node !== null && node !== void 0 && node.isBlock)) {
208
207
  continue;
209
208
  }
210
- var isActive = isRangeActive(activeIndexPos, pos, nodeEnd);
209
+ var isActive = isRangeActive(activeIndexPos, pos, nodeEnd, attributionKey);
211
210
  decorations.push.apply(decorations, _toConsumableArray(createBlockChangedDecoration({
212
211
  attributionKey: attributionKey,
213
212
  change: {
@@ -251,7 +250,7 @@ var calculateNodesForBlockDecoration = function calculateNodesForBlockDecoration
251
250
  }
252
251
  if (node.isBlock && (!requireContainedBlocks || pos >= from) && pos + node.nodeSize <= to) {
253
252
  var _nodeEnd = pos + node.nodeSize;
254
- var _isActive = isRangeActive(activeIndexPos, pos, _nodeEnd);
253
+ var _isActive = isRangeActive(activeIndexPos, pos, _nodeEnd, attributionKey);
255
254
  decorations.push.apply(decorations, _toConsumableArray(createBlockChangedDecoration({
256
255
  attributionKey: attributionKey,
257
256
  change: {
@@ -535,7 +534,7 @@ var calculateDiffDecorationsInner = function calculateDiffDecorationsInner(_ref8
535
534
  var leftAnchorId = fg('platform_editor_ai_show_diff_patch_2') ? "replacement-".concat(change.fromA, "-").concat(change.toA, "-").concat(change.fromB, "-").concat(change.toB) : undefined;
536
535
  var changeColorScheme = attributionColors ? getColorSchemeForChange(change, attributedChanges, attributionColors, colorScheme) : colorScheme;
537
536
  var attributionKey = showContributorTags ? getAttributionKeyForChange(change, attributedChanges) : undefined;
538
- var isActive = isRangeActive(activeIndexPos, change.fromB, change.toB);
537
+ var isActive = isRangeActive(activeIndexPos, change.fromB, change.toB, attributionKey);
539
538
  var deletedColumns = change.deletedColumns;
540
539
  // On an inverted diff the table that lost columns is the one in `tr.doc`, so the column
541
540
  // indices address it directly and each label anchors inside its own cell.
@@ -772,7 +771,7 @@ var calculateDiffDecorationsInner = function calculateDiffDecorationsInner(_ref8
772
771
  var _attributionColors$ge, _attributionColors;
773
772
  var changeColorScheme = change.attributionKey ? (_attributionColors$ge = (_attributionColors = attributionColors) === null || _attributionColors === void 0 ? void 0 : _attributionColors.get(change.attributionKey)) !== null && _attributionColors$ge !== void 0 ? _attributionColors$ge : colorScheme : colorScheme;
774
773
  var attributionKey = showContributorTags ? change.attributionKey : undefined;
775
- var isActive = isRangeActive(activeIndexPos, change.fromB, change.toB);
774
+ var isActive = isRangeActive(activeIndexPos, change.fromB, change.toB, attributionKey);
776
775
  decorations.push.apply(decorations, _toConsumableArray(createInlineChangedDecoration({
777
776
  attributionKey: attributionKey,
778
777
  change: change,
@@ -801,7 +800,7 @@ var calculateDiffDecorationsInner = function calculateDiffDecorationsInner(_ref8
801
800
  var attributionKey = showContributorTags ? change.attributionKey : undefined;
802
801
  if (change.isInline) {
803
802
  // Inline nodes (e.g. date, emoji, mention, status) need an inline decoration rather than a block decoration
804
- var isActive = isRangeActive(activeIndexPos, change.fromB, change.toB);
803
+ var isActive = isRangeActive(activeIndexPos, change.fromB, change.toB, attributionKey);
805
804
  decorations.push.apply(decorations, _toConsumableArray(createInlineChangedDecoration({
806
805
  attributionKey: attributionKey,
807
806
  change: change,
@@ -6,7 +6,6 @@ import { areNodesEqualIgnoreAttrs } from '@atlaskit/editor-common/utils/document
6
6
  import { Fragment, Slice } from '@atlaskit/editor-prosemirror/model';
7
7
  import { TableMap } from '@atlaskit/editor-tables/table-map';
8
8
  export var CELL_CONTENT_OFFSET = 2;
9
- var IGNORED_CELL_ATTRS = ['localId', 'colwidth'];
10
9
  var tablesIn = function tablesIn(doc) {
11
10
  var tables = [];
12
11
  doc.descendants(function (node, pos) {
@@ -56,41 +55,107 @@ var positionMatchedColumns = function positionMatchedColumns(_ref) {
56
55
  }
57
56
  return surviving;
58
57
  };
59
- var addContentMatchedColumns = function addContentMatchedColumns(surviving, before, after) {
60
- var _before$node$firstChi, _before$node$firstChi2, _after$node$firstChil, _after$node$firstChil2;
61
- var 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 : [];
62
- var 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 : [];
63
- var _iterator = _createForOfIteratorHelper(afterCells),
58
+ var headerLocalIds = function headerLocalIds(table) {
59
+ var _table$node$firstChil, _table$node$firstChil2;
60
+ return ((_table$node$firstChil = (_table$node$firstChil2 = table.node.firstChild) === null || _table$node$firstChil2 === void 0 ? void 0 : _table$node$firstChil2.children) !== null && _table$node$firstChil !== void 0 ? _table$node$firstChil : []).map(function (cell) {
61
+ var _cell$attrs$localId;
62
+ return (_cell$attrs$localId = cell.attrs.localId) !== null && _cell$attrs$localId !== void 0 ? _cell$attrs$localId : undefined;
63
+ });
64
+ };
65
+
66
+ /**
67
+ * Cells keep their `localId` when a column is deleted, so the first row's ids name each remaining
68
+ * column even when the steps replaced the whole table. Missing or repeated ids, such as in content
69
+ * pasted from elsewhere, cannot name a column.
70
+ */
71
+ var localIdMatchedColumns = function localIdMatchedColumns(before, after) {
72
+ var beforeIds = headerLocalIds(before);
73
+ if (beforeIds.some(function (id) {
74
+ return !id;
75
+ }) || new Set(beforeIds).size !== beforeIds.length) {
76
+ return undefined;
77
+ }
78
+ var surviving = new Set();
79
+ var previousColumn = -1;
80
+ var _iterator = _createForOfIteratorHelper(headerLocalIds(after)),
64
81
  _step;
65
82
  try {
66
- var _loop = function _loop() {
67
- var afterCell = _step.value;
68
- var matches = beforeCells.flatMap(function (beforeCell, column) {
69
- return areNodesEqualIgnoreAttrs(beforeCell, afterCell, IGNORED_CELL_ATTRS) ? [column] : [];
70
- });
71
- if (matches.length !== 1) {
72
- return {
73
- v: false
74
- };
75
- }
76
- surviving.add(matches[0]);
77
- },
78
- _ret;
79
83
  for (_iterator.s(); !(_step = _iterator.n()).done;) {
80
- _ret = _loop();
81
- if (_ret) return _ret.v;
84
+ var id = _step.value;
85
+ var column = id ? beforeIds.indexOf(id) : -1;
86
+ if (column <= previousColumn) {
87
+ return undefined;
88
+ }
89
+ surviving.add(column);
90
+ previousColumn = column;
82
91
  }
83
92
  } catch (err) {
84
93
  _iterator.e(err);
85
94
  } finally {
86
95
  _iterator.f();
87
96
  }
88
- var ordered = _toConsumableArray(surviving);
89
- return !ordered.some(function (column, index) {
90
- return index > 0 && column <= ordered[index - 1];
97
+ return surviving;
98
+ };
99
+ var IGNORED_CELL_ATTRS = ['localId', 'colwidth'];
100
+
101
+ /** Rows may also have been deleted, so the remaining cells only need to appear in order. */
102
+ var isColumnKept = function isColumnKept(before, beforeColumn, after, afterColumn) {
103
+ var beforeRow = 0;
104
+ return after.node.children.every(function (afterRow) {
105
+ var afterCell = afterRow.maybeChild(afterColumn);
106
+ while (beforeRow < before.node.childCount) {
107
+ var beforeCell = before.node.child(beforeRow++).maybeChild(beforeColumn);
108
+ if (afterCell && beforeCell && areNodesEqualIgnoreAttrs(beforeCell, afterCell, IGNORED_CELL_ATTRS)) {
109
+ return true;
110
+ }
111
+ }
112
+ return false;
91
113
  });
92
114
  };
115
+
116
+ /**
117
+ * Pairs each remaining column with an equal original column, keeping their order, and takes each
118
+ * pair from the `direction` end first. Returns the original column of each remaining column.
119
+ */
120
+ var pairColumnsInOrder = function pairColumnsInOrder(beforeWidth, afterWidth, isMatch, direction) {
121
+ var step = direction === 'left' ? 1 : -1;
122
+ var pairing = [];
123
+ var beforeColumn = direction === 'left' ? 0 : beforeWidth - 1;
124
+ var afterColumn = direction === 'left' ? 0 : afterWidth - 1;
125
+ for (; afterColumn >= 0 && afterColumn < afterWidth; afterColumn += step) {
126
+ while (beforeColumn >= 0 && beforeColumn < beforeWidth && !isMatch(beforeColumn, afterColumn)) {
127
+ beforeColumn += step;
128
+ }
129
+ if (beforeColumn < 0 || beforeColumn >= beforeWidth) {
130
+ return undefined;
131
+ }
132
+ pairing.push(beforeColumn);
133
+ beforeColumn += step;
134
+ }
135
+ return direction === 'left' ? pairing : pairing.reverse();
136
+ };
137
+
138
+ /**
139
+ * For tables whose cells have no usable ids, such as content created before cells had ids. Every
140
+ * valid pairing of remaining columns to original ones lies between the leftmost and the rightmost
141
+ * pairing, so the columns are only reported when those two agree. Otherwise, such as when several
142
+ * original columns are identical, which of them were removed cannot be known.
143
+ */
144
+ var uniquelyContentMatchedColumns = function uniquelyContentMatchedColumns(before, after, beforeWidth, afterWidth) {
145
+ var isMatch = function isMatch(beforeColumn, afterColumn) {
146
+ return isColumnKept(before, beforeColumn, after, afterColumn);
147
+ };
148
+ var leftmost = pairColumnsInOrder(beforeWidth, afterWidth, isMatch, 'left');
149
+ var rightmost = pairColumnsInOrder(beforeWidth, afterWidth, isMatch, 'right');
150
+ if (!leftmost || !rightmost || leftmost.some(function (column, index) {
151
+ return column !== rightmost[index];
152
+ })) {
153
+ return undefined;
154
+ }
155
+ return new Set(leftmost);
156
+ };
93
157
  var findDeletedColumns = function findDeletedColumns(before, after, afterToBefore) {
158
+ var _localIdMatchedColumn;
94
159
  var beforeMap = TableMap.get(before.node);
95
160
  var afterMap = TableMap.get(after.node);
96
161
  // Merged or malformed tables do not have reliable column indices.
@@ -98,13 +163,14 @@ var findDeletedColumns = function findDeletedColumns(before, after, afterToBefor
98
163
  return [];
99
164
  }
100
165
  var beforeOffsets = columnOffsets(beforeMap, before.node);
101
- var surviving = positionMatchedColumns({
166
+ var positionMatched = positionMatchedColumns({
102
167
  before: before,
103
168
  after: after,
104
169
  afterToBefore: afterToBefore,
105
170
  beforeOffsets: beforeOffsets
106
171
  });
107
- if (surviving.size !== afterMap.width && !addContentMatchedColumns(surviving, before, after)) {
172
+ var surviving = positionMatched.size === afterMap.width ? positionMatched : (_localIdMatchedColumn = localIdMatchedColumns(before, after)) !== null && _localIdMatchedColumn !== void 0 ? _localIdMatchedColumn : uniquelyContentMatchedColumns(before, after, beforeMap.width, afterMap.width);
173
+ if (!surviving) {
108
174
  return [];
109
175
  }
110
176
  return beforeOffsets.flatMap(function (_, column) {