@atlaskit/editor-plugin-show-diff 20.0.0 → 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.
- package/CHANGELOG.md +23 -0
- package/dist/cjs/pm-plugins/calculateDiff/calculateDiffDecorations.js +9 -10
- package/dist/cjs/pm-plugins/calculateDiff/groupDeletedColumnChanges.js +92 -26
- package/dist/cjs/pm-plugins/calculateDiff/simplifyChangesWithAttribution.js +46 -22
- package/dist/cjs/pm-plugins/decorations/colorSchemes/attributions.js +4 -5
- package/dist/cjs/pm-plugins/decorations/createChangedRowDecorationWidgets.js +4 -1
- package/dist/cjs/pm-plugins/decorations/createNodeChangedDecorationWidget.js +1 -1
- package/dist/cjs/pm-plugins/decorations/extractContributorTags.js +3 -1
- package/dist/cjs/pm-plugins/getScrollableDecorations.js +1 -8
- package/dist/cjs/pm-plugins/main.js +5 -2
- package/dist/es2019/pm-plugins/calculateDiff/calculateDiffDecorations.js +9 -10
- package/dist/es2019/pm-plugins/calculateDiff/groupDeletedColumnChanges.js +85 -14
- package/dist/es2019/pm-plugins/calculateDiff/simplifyChangesWithAttribution.js +42 -22
- package/dist/es2019/pm-plugins/decorations/colorSchemes/attributions.js +4 -5
- package/dist/es2019/pm-plugins/decorations/createChangedRowDecorationWidgets.js +4 -1
- package/dist/es2019/pm-plugins/decorations/createNodeChangedDecorationWidget.js +1 -1
- package/dist/es2019/pm-plugins/decorations/extractContributorTags.js +1 -1
- package/dist/es2019/pm-plugins/getScrollableDecorations.js +1 -6
- package/dist/es2019/pm-plugins/main.js +5 -1
- package/dist/esm/pm-plugins/calculateDiff/calculateDiffDecorations.js +9 -10
- package/dist/esm/pm-plugins/calculateDiff/groupDeletedColumnChanges.js +92 -26
- package/dist/esm/pm-plugins/calculateDiff/simplifyChangesWithAttribution.js +46 -22
- package/dist/esm/pm-plugins/decorations/colorSchemes/attributions.js +4 -5
- package/dist/esm/pm-plugins/decorations/createChangedRowDecorationWidgets.js +4 -1
- package/dist/esm/pm-plugins/decorations/createNodeChangedDecorationWidget.js +1 -1
- package/dist/esm/pm-plugins/decorations/extractContributorTags.js +3 -1
- package/dist/esm/pm-plugins/getScrollableDecorations.js +1 -8
- package/dist/esm/pm-plugins/main.js +5 -2
- package/dist/types/pm-plugins/calculateDiff/calculateDiffDecorations.d.ts +6 -4
- package/dist/types/pm-plugins/calculateDiff/simplifyChangesWithAttribution.d.ts +2 -2
- package/dist/types/pm-plugins/decorations/colorSchemes/attributions.d.ts +2 -3
- package/dist/types/pm-plugins/decorations/createNodeChangedDecorationWidget.d.ts +1 -0
- package/dist/types/pm-plugins/decorations/extractContributorTags.d.ts +1 -0
- package/dist/types/pm-plugins/main.d.ts +1 -0
- package/package.json +4 -4
|
@@ -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
|
|
51
|
-
var
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
142
|
+
const positionMatched = positionMatchedColumns({
|
|
73
143
|
before,
|
|
74
144
|
after,
|
|
75
145
|
afterToBefore,
|
|
76
146
|
beforeOffsets
|
|
77
147
|
});
|
|
78
|
-
|
|
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
|
-
|
|
78
|
-
|
|
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 =
|
|
82
|
-
for (const span of
|
|
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 !==
|
|
101
|
-
return [
|
|
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:
|
|
106
|
-
to:
|
|
122
|
+
from: contentChange.fromB,
|
|
123
|
+
to: contentChange.toB
|
|
107
124
|
}, pos => pos >= run.from && pos < run.to);
|
|
108
125
|
return {
|
|
109
|
-
...
|
|
110
|
-
fromA: index === 0 ?
|
|
126
|
+
...contentChange,
|
|
127
|
+
fromA: index === 0 ? contentChange.fromA : contentChange.toA,
|
|
111
128
|
fromB: run.from,
|
|
112
129
|
toB: run.to,
|
|
113
|
-
deleted: index === 0 ?
|
|
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
|
|
124
|
-
*
|
|
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
|
|
136
|
-
return other !== change &&
|
|
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
|
|
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
|
-
|
|
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
|
|
236
|
-
*
|
|
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
|
-
*
|
|
187
|
-
*
|
|
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
|
-
|
|
192
|
-
return
|
|
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)
|
|
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
|
-
|
|
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.
|
|
126
|
-
*
|
|
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
|
|
60
|
-
var
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
81
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
|
166
|
+
var positionMatched = positionMatchedColumns({
|
|
102
167
|
before: before,
|
|
103
168
|
after: after,
|
|
104
169
|
afterToBefore: afterToBefore,
|
|
105
170
|
beforeOffsets: beforeOffsets
|
|
106
171
|
});
|
|
107
|
-
|
|
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) {
|