@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.
- package/CHANGELOG.md +20 -0
- package/dist/cjs/pm-plugins/calculateDiff/calculateDiffDecorations.js +36 -3
- package/dist/cjs/pm-plugins/calculateDiff/diffBySteps.js +14 -2
- package/dist/cjs/pm-plugins/decorations/createNodeChangedDecorationWidget.js +14 -0
- package/dist/cjs/pm-plugins/decorations/createReplacedCellDecorations.js +350 -0
- package/dist/cjs/pm-plugins/decorations/utils/cellAttrs.js +30 -0
- package/dist/cjs/pm-plugins/scrollToDiff.js +41 -32
- package/dist/es2019/pm-plugins/calculateDiff/calculateDiffDecorations.js +37 -4
- package/dist/es2019/pm-plugins/calculateDiff/diffBySteps.js +12 -2
- package/dist/es2019/pm-plugins/decorations/createNodeChangedDecorationWidget.js +14 -0
- package/dist/es2019/pm-plugins/decorations/createReplacedCellDecorations.js +338 -0
- package/dist/es2019/pm-plugins/decorations/utils/cellAttrs.js +19 -0
- package/dist/es2019/pm-plugins/scrollToDiff.js +39 -30
- package/dist/esm/pm-plugins/calculateDiff/calculateDiffDecorations.js +36 -3
- package/dist/esm/pm-plugins/calculateDiff/diffBySteps.js +14 -2
- package/dist/esm/pm-plugins/decorations/createNodeChangedDecorationWidget.js +14 -0
- package/dist/esm/pm-plugins/decorations/createReplacedCellDecorations.js +343 -0
- package/dist/esm/pm-plugins/decorations/utils/cellAttrs.js +25 -0
- package/dist/esm/pm-plugins/scrollToDiff.js +41 -32
- package/dist/types/pm-plugins/decorations/createReplacedCellDecorations.d.ts +67 -0
- package/dist/types/pm-plugins/decorations/utils/cellAttrs.d.ts +13 -0
- package/dist/types/pm-plugins/markViews.d.ts +1 -1
- package/package.json +5 -1
|
@@ -14,6 +14,7 @@ import { createDocMarginAnchorWidget } from '../decorations/createAnchorDecorati
|
|
|
14
14
|
import { createBlockChangedDecoration } from '../decorations/createBlockChangedDecoration';
|
|
15
15
|
import { createInlineChangedDecoration } from '../decorations/createInlineChangedDecoration';
|
|
16
16
|
import { createNodeChangedDecorationWidget } from '../decorations/createNodeChangedDecorationWidget';
|
|
17
|
+
import { createReplacedCellDecorations, getReplacedCells } from '../decorations/createReplacedCellDecorations';
|
|
17
18
|
import { extractDiffDescriptors } from '../decorations/decorationKeys';
|
|
18
19
|
import { extractContributorTags } from '../decorations/extractContributorTags';
|
|
19
20
|
import { createDeletedLineBreakDecoration } from '../decorations/utils/createDeletedLineBreakWidget';
|
|
@@ -548,7 +549,11 @@ const calculateDiffDecorationsInner = ({
|
|
|
548
549
|
// suppressed in the clean view. `hideDeletedDiffs` is load-bearing: when false the reviewer
|
|
549
550
|
// asked to compare against the original, and the deleted side is the only place the
|
|
550
551
|
// previous formatting is visible.
|
|
551
|
-
|
|
552
|
+
//
|
|
553
|
+
// Not on an inverted diff: there the widget's side is the suggested content, which the clean
|
|
554
|
+
// view keeps. Suppressing it hides the suggestion along with the displayed side it replaces,
|
|
555
|
+
// e.g. a cell whose only change is its background, as the signature ignores attrs.
|
|
556
|
+
const isMarkOnly = shouldHideMarkOnlyDeletions && hideDeletedDiffs && !(isInverted && fg('platform_editor_ai_show_diff_patch_2')) && change.deleted.length > 0 && isMarkOnlyChange({
|
|
552
557
|
change,
|
|
553
558
|
originalDoc,
|
|
554
559
|
newDoc: tr.doc
|
|
@@ -625,7 +630,35 @@ const calculateDiffDecorationsInner = ({
|
|
|
625
630
|
hideDeletedDiffs,
|
|
626
631
|
isInverted
|
|
627
632
|
});
|
|
628
|
-
|
|
633
|
+
|
|
634
|
+
// A whole-cell replacement keeps its cells in place, so it takes its own decorations
|
|
635
|
+
// instead of the generic inline and block ones.
|
|
636
|
+
const replacedCells = fg('platform_editor_ai_show_diff_patch_2') ? getReplacedCells({
|
|
637
|
+
slice: originalDoc.slice(change.fromA, change.toA),
|
|
638
|
+
doc: tr.doc,
|
|
639
|
+
fromB: change.fromB,
|
|
640
|
+
toB: change.toB
|
|
641
|
+
}) : [];
|
|
642
|
+
if (replacedCells.length > 0) {
|
|
643
|
+
decorations.push(...createReplacedCellDecorations({
|
|
644
|
+
replacedCells,
|
|
645
|
+
change,
|
|
646
|
+
doc: tr.doc,
|
|
647
|
+
attributionKey,
|
|
648
|
+
colorScheme: changeColorScheme,
|
|
649
|
+
hasDeletedWidget: willRenderDeletedWidget,
|
|
650
|
+
hideAddedDiffsUnderline,
|
|
651
|
+
isActive,
|
|
652
|
+
isDeletedWidgetBelow,
|
|
653
|
+
isInserted,
|
|
654
|
+
leftAnchorId,
|
|
655
|
+
reveal,
|
|
656
|
+
shouldHideDeleted,
|
|
657
|
+
showContributorTags,
|
|
658
|
+
showIndicators,
|
|
659
|
+
tagMountContext
|
|
660
|
+
}));
|
|
661
|
+
} else if (isSmartNodeLevel) {
|
|
629
662
|
// For a large inserted table, skip the O(cells) inline decorations inside the
|
|
630
663
|
// table (the cell overlays added below provide the highlight without the
|
|
631
664
|
// expensive content re-render); leaf blocks outside the table still get theirs.
|
|
@@ -674,7 +707,7 @@ const calculateDiffDecorationsInner = ({
|
|
|
674
707
|
tagMountContext
|
|
675
708
|
}));
|
|
676
709
|
}
|
|
677
|
-
decorations.push(...calculateNodesForBlockDecoration({
|
|
710
|
+
decorations.push(...(replacedCells.length > 0 ? [] : calculateNodesForBlockDecoration({
|
|
678
711
|
// A deleted widget placed below anchors at the change's end, inside the very block
|
|
679
712
|
// this would let the decoration claim.
|
|
680
713
|
allowTrailingCloseTokens: !isDeletedWidgetBelow,
|
|
@@ -694,7 +727,7 @@ const calculateDiffDecorationsInner = ({
|
|
|
694
727
|
intl,
|
|
695
728
|
coarseTableCellsOnly: useCoarseTableDecoration,
|
|
696
729
|
leftAnchorId
|
|
697
|
-
}));
|
|
730
|
+
})));
|
|
698
731
|
}
|
|
699
732
|
if (change.deleted.length > 0 && !isMarkOnly && !hasNoDeletedContent) {
|
|
700
733
|
const shouldHideDeleted = shouldHideDeletedSide({
|
|
@@ -3,6 +3,7 @@ import { Mark } from '@atlaskit/editor-prosemirror/model';
|
|
|
3
3
|
import { Mapping, ReplaceStep } from '@atlaskit/editor-prosemirror/transform';
|
|
4
4
|
import { fg } from '@atlaskit/platform-feature-flags/fg';
|
|
5
5
|
import { getAttributionKey } from '../decorations/colorSchemes/attributions';
|
|
6
|
+
import { isCellAttrChanged } from '../decorations/utils/cellAttrs';
|
|
6
7
|
import { buildCharsByOffset } from '../utils/charsByOffset';
|
|
7
8
|
import { attrAwareTokenEncoder } from './attrAwareTokenEncoder';
|
|
8
9
|
import { optimizeChanges } from './optimizeChanges';
|
|
@@ -17,6 +18,14 @@ const WHITESPACE_REGEX = /\s/u;
|
|
|
17
18
|
* excluded: `groupDeletedColumnChanges` relies on a replaced table being one whole-table change.
|
|
18
19
|
*/
|
|
19
20
|
const isGranularIsolatingNode = node => !!node.type.spec.isolating && node.type.spec.tableRole !== 'table';
|
|
21
|
+
const CELL_TYPE_NAMES = ['tableCell', 'tableHeader'];
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Whether a table cell's replacement changes one of its diffable attributes, such as its background.
|
|
25
|
+
* Such a cell is diffed whole: it renders as a cell style plus a copy of its row, which already shows
|
|
26
|
+
* the cell's new content, so inline marks inside it would only repeat that.
|
|
27
|
+
*/
|
|
28
|
+
const isCellAttrChange = (replacedNode, replacingNode) => CELL_TYPE_NAMES.includes(replacedNode.type.name) && isCellAttrChanged(replacedNode, replacingNode);
|
|
20
29
|
const mapPosition = (mapping, pos) => mapping.map(pos);
|
|
21
30
|
|
|
22
31
|
/**
|
|
@@ -206,7 +215,8 @@ const mergeOverlappingByNewDocRange = changes => {
|
|
|
206
215
|
* - The replaced slice has only one child
|
|
207
216
|
* - The replacing slice has only one child
|
|
208
217
|
* - Both children are the same textblock type, or (behind `platform_editor_ai_show_diff_patch_2`)
|
|
209
|
-
* the same isolating, non-table node type whose text content differs
|
|
218
|
+
* the same isolating, non-table node type whose text content differs (for a table cell, only when
|
|
219
|
+
* its diffable attributes are unchanged)
|
|
210
220
|
* - The replaced slice and replacing slice have the same text content
|
|
211
221
|
* - The replaced slice and replacing slice have the same child marks (if text content is equal)
|
|
212
222
|
*/
|
|
@@ -244,7 +254,7 @@ const shouldCheckGranularDiff = (step, before, from, to) => {
|
|
|
244
254
|
// `hasSameChildMarks` only compares an isolating node's direct (block) children, so it cannot
|
|
245
255
|
// vouch for one with unchanged text. Keep the existing whole-node fallback for that case.
|
|
246
256
|
if (isIsolatingNode) {
|
|
247
|
-
return !isTextContentEqual;
|
|
257
|
+
return !isTextContentEqual && !(replacingNode && isCellAttrChange(replacedNode, replacingNode));
|
|
248
258
|
}
|
|
249
259
|
return !isTextContentEqual || isTextContentEqual && hasSameChildMarks(replacedNode, replacingNode);
|
|
250
260
|
};
|
|
@@ -10,6 +10,7 @@ import { clampAnchorPosIntoCell, createAnchorNameSpan, createLeftAnchorWidget }
|
|
|
10
10
|
import { createChangedRowDecorationWidgets } from './createChangedRowDecorationWidgets';
|
|
11
11
|
import { createContributorTagHost, resolveHoistedCodeBlockAnchor, unmountContributorTag } from './createContributorTagWidget';
|
|
12
12
|
import { getAtomicInlineChangedAttrs } from './createInlineChangedDecoration';
|
|
13
|
+
import { createReplacedCellWidgets } from './createReplacedCellDecorations';
|
|
13
14
|
import { AnchorTypeKey, buildContributorTagDecorationSpec, buildDiffDecorationSpec, buildAnchorDecorationKey, scrollMarginTopValue } from './decorationKeys';
|
|
14
15
|
import { absorbFirstChildMarginReset } from './utils/absorbFirstChildMarginReset';
|
|
15
16
|
import { createDeletedLineBreakDecoration } from './utils/createDeletedLineBreakWidget';
|
|
@@ -214,6 +215,19 @@ export const createNodeChangedDecorationWidget = ({
|
|
|
214
215
|
isInserted
|
|
215
216
|
});
|
|
216
217
|
if (tableDiffMode.kind === 'cells') {
|
|
218
|
+
if (fg('platform_editor_ai_show_diff_patch_2')) {
|
|
219
|
+
return createReplacedCellWidgets({
|
|
220
|
+
slice,
|
|
221
|
+
newDoc,
|
|
222
|
+
change,
|
|
223
|
+
nodeViewSerializer,
|
|
224
|
+
colorScheme,
|
|
225
|
+
intl,
|
|
226
|
+
isActive,
|
|
227
|
+
isInserted,
|
|
228
|
+
leftAnchorId
|
|
229
|
+
});
|
|
230
|
+
}
|
|
217
231
|
return createTableCellContentWidgets({
|
|
218
232
|
slice,
|
|
219
233
|
newDoc,
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import { getDarkModeLCHColor } from '@atlaskit/adf-schema/get-dark-mode-lch-color';
|
|
2
|
+
import { isRgb } from '@atlaskit/adf-schema/is-rgb';
|
|
3
|
+
import { rgbToHex } from '@atlaskit/adf-schema/rgb-to-hex';
|
|
4
|
+
import { getCellDomAttrs } from '@atlaskit/adf-schema/tableNodes';
|
|
5
|
+
import { convertToInlineCss } from '@atlaskit/editor-common/css-helper';
|
|
6
|
+
import { areNodesEqualIgnoreAttrs } from '@atlaskit/editor-common/utils/document';
|
|
7
|
+
import { Decoration } from '@atlaskit/editor-prosemirror/view';
|
|
8
|
+
import { createBlockChangedDecoration } from './createBlockChangedDecoration';
|
|
9
|
+
import { createInlineChangedDecoration } from './createInlineChangedDecoration';
|
|
10
|
+
import { buildDiffDecorationSpec } from './decorationKeys';
|
|
11
|
+
import { isCellAttrChanged, isCellBackgroundChanged, normalizeCellBackground } from './utils/cellAttrs';
|
|
12
|
+
import { injectInnerWrapper, wrapBlockNodeView } from './utils/wrapBlockNodeView';
|
|
13
|
+
const CELL_TYPE_NAMES = ['tableCell', 'tableHeader'];
|
|
14
|
+
/**
|
|
15
|
+
* Pairs each cell of a whole-cell replacement with the live cell it replaces, or `[]` when the slice
|
|
16
|
+
* is not a run of whole cells matching the live ones. Whole-cell replacement preserves cell count and
|
|
17
|
+
* order, so the nth live cell in the range matches the nth cell in the slice.
|
|
18
|
+
*/
|
|
19
|
+
export const getReplacedCells = ({
|
|
20
|
+
slice,
|
|
21
|
+
doc,
|
|
22
|
+
fromB,
|
|
23
|
+
toB
|
|
24
|
+
}) => {
|
|
25
|
+
const otherCells = slice.content.content;
|
|
26
|
+
if (slice.openStart !== 0 || slice.openEnd !== 0 || otherCells.length === 0 || !otherCells.every(node => CELL_TYPE_NAMES.includes(node.type.name))) {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
const liveCells = [];
|
|
30
|
+
doc.nodesBetween(fromB, toB, (node, pos) => {
|
|
31
|
+
if (CELL_TYPE_NAMES.includes(node.type.name)) {
|
|
32
|
+
liveCells.push({
|
|
33
|
+
node,
|
|
34
|
+
pos
|
|
35
|
+
});
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
return true;
|
|
39
|
+
});
|
|
40
|
+
if (liveCells.length !== otherCells.length) {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
return otherCells.map((otherCell, index) => ({
|
|
44
|
+
otherCell,
|
|
45
|
+
liveCell: liveCells[index].node,
|
|
46
|
+
liveCellPos: liveCells[index].pos
|
|
47
|
+
}));
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Whether two cells hold the same content, ignoring the cells' own attributes and `localId`s, which
|
|
52
|
+
* a suggestion can regenerate. Such a replacement only changed the cell (e.g. its background), so
|
|
53
|
+
* there is no content to diff.
|
|
54
|
+
*/
|
|
55
|
+
const isCellContentEqual = (left, right) => {
|
|
56
|
+
if (left.childCount !== right.childCount) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
for (let index = 0; index < left.childCount; index++) {
|
|
60
|
+
if (!areNodesEqualIgnoreAttrs(left.child(index), right.child(index), ['localId'])) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return true;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Whether the cell itself changed — its type, or an attribute that represents a change such as its
|
|
69
|
+
* background or span — as opposed to only its content.
|
|
70
|
+
*/
|
|
71
|
+
const isCellChanged = (left, right) => left.type !== right.type || isCellAttrChanged(left, right);
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* A custom (non-palette) cell colour as the schema renders it: inverted in dark mode so text stays
|
|
75
|
+
* readable, except for design tokens, which are already themed.
|
|
76
|
+
*/
|
|
77
|
+
const getCustomCellColor = background => {
|
|
78
|
+
const color = isRgb(background) && rgbToHex(background) || background;
|
|
79
|
+
if (color.startsWith('var(') || document.documentElement.getAttribute('data-color-mode') !== 'dark') {
|
|
80
|
+
return background;
|
|
81
|
+
}
|
|
82
|
+
return getDarkModeLCHColor(color);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The colour `cell` renders with. A palette colour is the editor's own themed variable for its colour
|
|
87
|
+
* name (e.g. for dark mode), the one `td[colorname]` cells use; a custom colour is the schema's own
|
|
88
|
+
* treatment of it; no colour is the editor's default.
|
|
89
|
+
*/
|
|
90
|
+
const getCellBackgroundColor = cell => {
|
|
91
|
+
const background = normalizeCellBackground(cell.attrs.background);
|
|
92
|
+
if (!background) {
|
|
93
|
+
return cell.type.name === 'tableHeader' ? "var(--ds-background-accent-gray-subtlest, #F0F1F2)" : "var(--ds-surface, #FFFFFF)";
|
|
94
|
+
}
|
|
95
|
+
const {
|
|
96
|
+
colorname
|
|
97
|
+
} = getCellDomAttrs(cell);
|
|
98
|
+
return colorname ? `var(--ak-editor-table-cell-background-${colorname.toLowerCase().split(' ').join('-')})` : getCustomCellColor(background);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* A layer painting `color` over the cell's own background and under its content, so the cell reads
|
|
103
|
+
* as having the new background without any of the cell's own attributes being overridden: removing
|
|
104
|
+
* a decoration removes the attributes it set rather than restoring the ones they replaced.
|
|
105
|
+
*/
|
|
106
|
+
const createCellBackgroundLayer = color => {
|
|
107
|
+
const layer = document.createElement('div');
|
|
108
|
+
layer.setAttribute('aria-hidden', 'true');
|
|
109
|
+
layer.contentEditable = 'false';
|
|
110
|
+
layer.dataset.testid = 'show-diff-new-cell-background';
|
|
111
|
+
layer.setAttribute('style', convertToInlineCss({
|
|
112
|
+
position: 'absolute',
|
|
113
|
+
inset: 0,
|
|
114
|
+
zIndex: -1,
|
|
115
|
+
borderRadius: 'inherit',
|
|
116
|
+
pointerEvents: 'none',
|
|
117
|
+
backgroundColor: color
|
|
118
|
+
}));
|
|
119
|
+
return layer;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/** The other side's cell content, as blocks, for the top of the live cell. */
|
|
123
|
+
const createCellContentWidget = ({
|
|
124
|
+
cell,
|
|
125
|
+
pos,
|
|
126
|
+
nodeViewSerializer,
|
|
127
|
+
colorScheme,
|
|
128
|
+
intl,
|
|
129
|
+
isActive,
|
|
130
|
+
isInserted,
|
|
131
|
+
leftAnchorId
|
|
132
|
+
}) => {
|
|
133
|
+
const dom = document.createElement('div');
|
|
134
|
+
cell.content.forEach(block => {
|
|
135
|
+
const nodeView = nodeViewSerializer.tryCreateNodeView(block, 0, undefined, isInserted, colorScheme);
|
|
136
|
+
if (nodeView) {
|
|
137
|
+
wrapBlockNodeView({
|
|
138
|
+
dom,
|
|
139
|
+
nodeView,
|
|
140
|
+
targetNode: block,
|
|
141
|
+
colorScheme,
|
|
142
|
+
intl,
|
|
143
|
+
isActive,
|
|
144
|
+
isInserted
|
|
145
|
+
});
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const serialized = nodeViewSerializer.serializeNode(block);
|
|
149
|
+
if (serialized instanceof HTMLElement) {
|
|
150
|
+
dom.append(injectInnerWrapper({
|
|
151
|
+
node: serialized,
|
|
152
|
+
colorScheme,
|
|
153
|
+
isActive,
|
|
154
|
+
isInserted
|
|
155
|
+
}));
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
if (dom.childNodes.length === 0) {
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
161
|
+
dom.setAttribute('data-testid', isInserted ? 'show-diff-changed-decoration' : 'show-diff-deleted-decoration');
|
|
162
|
+
|
|
163
|
+
// Before the cell's own blocks, as a block of its own, so it never lands inside a paragraph the
|
|
164
|
+
// displayed side may hide.
|
|
165
|
+
return Decoration.widget(pos + 1, dom, {
|
|
166
|
+
...buildDiffDecorationSpec({
|
|
167
|
+
colorScheme,
|
|
168
|
+
decorationType: 'widget',
|
|
169
|
+
diffId: crypto.randomUUID(),
|
|
170
|
+
leftAnchorId,
|
|
171
|
+
isActive,
|
|
172
|
+
isInserted,
|
|
173
|
+
side: -1
|
|
174
|
+
}),
|
|
175
|
+
marks: []
|
|
176
|
+
});
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* The other side of a whole-cell replacement, drawn inside the live cells so the table keeps its
|
|
181
|
+
* shape: each live cell takes the other cell's background when it differs, and its content when that
|
|
182
|
+
* differs. A cell whose content did not change has nothing to diff.
|
|
183
|
+
*/
|
|
184
|
+
export const createReplacedCellWidgets = ({
|
|
185
|
+
slice,
|
|
186
|
+
newDoc,
|
|
187
|
+
change,
|
|
188
|
+
nodeViewSerializer,
|
|
189
|
+
colorScheme,
|
|
190
|
+
intl,
|
|
191
|
+
isActive,
|
|
192
|
+
isInserted,
|
|
193
|
+
leftAnchorId
|
|
194
|
+
}) => getReplacedCells({
|
|
195
|
+
slice,
|
|
196
|
+
doc: newDoc,
|
|
197
|
+
fromB: change.fromB,
|
|
198
|
+
toB: change.toB
|
|
199
|
+
}).flatMap(({
|
|
200
|
+
otherCell,
|
|
201
|
+
liveCell,
|
|
202
|
+
liveCellPos
|
|
203
|
+
}) => {
|
|
204
|
+
const decorations = [];
|
|
205
|
+
if (isInserted && isCellBackgroundChanged(otherCell, liveCell)) {
|
|
206
|
+
const cellEnd = liveCellPos + liveCell.nodeSize;
|
|
207
|
+
decorations.push(
|
|
208
|
+
// The cell becomes the layer's stacking context, so the layer sits above the cell's own
|
|
209
|
+
// background rather than behind it. Through `z-index` at the cell overlay's own level, not
|
|
210
|
+
// `isolation`: an isolated cell would trap its overlay below later cells, which then cover
|
|
211
|
+
// the overlay's outline where it overlaps their border. Properties the cell never sets, so
|
|
212
|
+
// removing them takes nothing of its own.
|
|
213
|
+
Decoration.node(liveCellPos, cellEnd, {
|
|
214
|
+
style: convertToInlineCss({
|
|
215
|
+
position: 'relative',
|
|
216
|
+
// The highest z-index a cell diff overlay takes (the rounded-corner variant).
|
|
217
|
+
zIndex: 2
|
|
218
|
+
})
|
|
219
|
+
}, {
|
|
220
|
+
key: `show-diff-new-cell-background-host-${liveCellPos}`
|
|
221
|
+
}),
|
|
222
|
+
// At the cell's end, beside its diff overlay, so the cell's first block stays its first child.
|
|
223
|
+
Decoration.widget(cellEnd - 1, createCellBackgroundLayer(getCellBackgroundColor(otherCell)), {
|
|
224
|
+
key: `show-diff-new-cell-background-${liveCellPos}`,
|
|
225
|
+
side: -1,
|
|
226
|
+
marks: [],
|
|
227
|
+
ignoreSelection: true
|
|
228
|
+
}));
|
|
229
|
+
}
|
|
230
|
+
if (!isCellContentEqual(otherCell, liveCell)) {
|
|
231
|
+
const contentWidget = createCellContentWidget({
|
|
232
|
+
cell: otherCell,
|
|
233
|
+
pos: liveCellPos,
|
|
234
|
+
nodeViewSerializer,
|
|
235
|
+
colorScheme,
|
|
236
|
+
intl,
|
|
237
|
+
isActive,
|
|
238
|
+
isInserted,
|
|
239
|
+
leftAnchorId
|
|
240
|
+
});
|
|
241
|
+
if (contentWidget) {
|
|
242
|
+
decorations.push(contentWidget);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return decorations;
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* The displayed side of a whole-cell replacement. A table cell is never removed on its own, only
|
|
250
|
+
* replaced in place, so a live cell is never hidden — hiding it would leave the row a cell short. It
|
|
251
|
+
* takes the changed-cell treatment only when the cell itself changed; its content is diffed, and
|
|
252
|
+
* hidden in the clean view, only when that changed. A cell whose content stayed the same keeps
|
|
253
|
+
* showing it.
|
|
254
|
+
*/
|
|
255
|
+
export const createReplacedCellDecorations = ({
|
|
256
|
+
replacedCells,
|
|
257
|
+
change,
|
|
258
|
+
doc,
|
|
259
|
+
attributionKey,
|
|
260
|
+
colorScheme,
|
|
261
|
+
hasDeletedWidget,
|
|
262
|
+
hideAddedDiffsUnderline,
|
|
263
|
+
isActive,
|
|
264
|
+
isDeletedWidgetBelow,
|
|
265
|
+
isInserted,
|
|
266
|
+
leftAnchorId,
|
|
267
|
+
reveal,
|
|
268
|
+
shouldHideDeleted,
|
|
269
|
+
showContributorTags,
|
|
270
|
+
showIndicators,
|
|
271
|
+
tagMountContext
|
|
272
|
+
}) => replacedCells.flatMap(({
|
|
273
|
+
otherCell,
|
|
274
|
+
liveCell,
|
|
275
|
+
liveCellPos
|
|
276
|
+
}) => {
|
|
277
|
+
const cellEnd = liveCellPos + liveCell.nodeSize;
|
|
278
|
+
const decorations = isCellChanged(otherCell, liveCell) ? createBlockChangedDecoration({
|
|
279
|
+
attributionKey,
|
|
280
|
+
change: {
|
|
281
|
+
from: liveCellPos,
|
|
282
|
+
to: cellEnd,
|
|
283
|
+
name: liveCell.type.name
|
|
284
|
+
},
|
|
285
|
+
colorScheme,
|
|
286
|
+
doc,
|
|
287
|
+
isActive,
|
|
288
|
+
// The cell stays either way, so it reads as changed, not removed.
|
|
289
|
+
isInserted: true,
|
|
290
|
+
leftAnchorId,
|
|
291
|
+
showContributorTags,
|
|
292
|
+
showIndicators,
|
|
293
|
+
tagMountContext
|
|
294
|
+
}) : [];
|
|
295
|
+
if (isCellContentEqual(otherCell, liveCell)) {
|
|
296
|
+
return decorations;
|
|
297
|
+
}
|
|
298
|
+
decorations.push(...createInlineChangedDecoration({
|
|
299
|
+
attributionKey,
|
|
300
|
+
change: {
|
|
301
|
+
...change,
|
|
302
|
+
fromB: liveCellPos,
|
|
303
|
+
toB: cellEnd
|
|
304
|
+
},
|
|
305
|
+
colorScheme,
|
|
306
|
+
doc,
|
|
307
|
+
hasDeletedWidget,
|
|
308
|
+
hideAddedDiffsUnderline,
|
|
309
|
+
isActive,
|
|
310
|
+
isDeletedWidgetBelow,
|
|
311
|
+
isInserted,
|
|
312
|
+
leftAnchorId,
|
|
313
|
+
reveal,
|
|
314
|
+
shouldHideDeleted,
|
|
315
|
+
showContributorTags,
|
|
316
|
+
showIndicators,
|
|
317
|
+
tagMountContext
|
|
318
|
+
}));
|
|
319
|
+
if (shouldHideDeleted) {
|
|
320
|
+
liveCell.forEach((block, offset) => {
|
|
321
|
+
const blockPos = liveCellPos + 1 + offset;
|
|
322
|
+
decorations.push(...createBlockChangedDecoration({
|
|
323
|
+
change: {
|
|
324
|
+
from: blockPos,
|
|
325
|
+
to: blockPos + block.nodeSize,
|
|
326
|
+
name: block.type.name
|
|
327
|
+
},
|
|
328
|
+
colorScheme,
|
|
329
|
+
doc,
|
|
330
|
+
isActive,
|
|
331
|
+
isInserted,
|
|
332
|
+
leftAnchorId,
|
|
333
|
+
shouldHideDeleted
|
|
334
|
+
}));
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
return decorations;
|
|
338
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
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 const normalizeCellBackground = background => typeof background === 'string' && background !== '' && background !== 'null' ? background : null;
|
|
8
|
+
|
|
9
|
+
/** Whether two cells render different backgrounds. */
|
|
10
|
+
export const isCellBackgroundChanged = (left, right) => normalizeCellBackground(left.attrs.background) !== normalizeCellBackground(right.attrs.background);
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Whether two cells differ by an attribute that represents a change, such as their background or
|
|
14
|
+
* span, as opposed to only their content.
|
|
15
|
+
*/
|
|
16
|
+
export const isCellAttrChanged = (left, right) => {
|
|
17
|
+
var _getDiffableAttrNames;
|
|
18
|
+
return ((_getDiffableAttrNames = getDiffableAttrNames(left.type.name, left.attrs)) !== null && _getDiffableAttrNames !== void 0 ? _getDiffableAttrNames : []).some(name => name === 'background' ? isCellBackgroundChanged(left, right) : left.attrs[name] !== right.attrs[name]);
|
|
19
|
+
};
|
|
@@ -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
|
const tableHeaderHeight = isLimitedModeEnabled ? undefined : getStickyHeaderHeight(view, targetPos);
|
|
20
21
|
view.dom.style.setProperty(SCROLL_TARGET_MARGIN_CSS_PROPERTY, `${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
|
-
|
|
24
|
+
// ProseMirror may return a text node; scroll its parent element instead.
|
|
25
|
+
const 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
|
+
const 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,23 @@ function scrollToSelection(node) {
|
|
|
41
42
|
const previousScrollMarginTop = element.style.scrollMarginTop;
|
|
42
43
|
element.style.scrollMarginTop = `${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(element => element.dataset.diffId === target.spec.diffId)) !== null && _Array$from$find !== void 0 ? _Array$from$find : view.nodeDOM(target.from);
|
|
58
|
+
}
|
|
59
|
+
const targetNode = view.nodeDOM(target.from);
|
|
60
|
+
return targetNode instanceof Element ? targetNode : (_view$domAtPos = view.domAtPos(target.from)) === null || _view$domAtPos === void 0 ? void 0 : _view$domAtPos.node;
|
|
61
|
+
}
|
|
52
62
|
|
|
53
63
|
/**
|
|
54
64
|
* Schedules scrolling to the decoration at the given index after the next frame. Defaults to the
|
|
@@ -66,28 +76,27 @@ export const scrollToDecoration = (view, decorations, activeIndex = 0, api) => {
|
|
|
66
76
|
// deleted-content widget rather than at the group's `from` position — see `scrollTarget` in
|
|
67
77
|
// `getScrollableDecorations`. Only set under `platform_editor_ai_show_diff_patch_1`.
|
|
68
78
|
const target = isDiffDecoration(decoration) && decoration.spec.scrollTarget || decoration;
|
|
69
|
-
let rafId =
|
|
70
|
-
|
|
79
|
+
let rafId = null;
|
|
80
|
+
let cancelled = false;
|
|
81
|
+
const scrollToTarget = () => {
|
|
82
|
+
if (view.isDestroyed) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
71
85
|
if (fg('platform_editor_ai_show_diff_patch_1')) {
|
|
72
86
|
updateScrollTargetMargin(view, target.from, api);
|
|
73
87
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
scrollToSelection(targetNode);
|
|
83
|
-
} else {
|
|
84
|
-
var _view$domAtPos;
|
|
85
|
-
const targetNode = view.nodeDOM(target === null || target === void 0 ? void 0 : target.from);
|
|
86
|
-
const 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;
|
|
87
|
-
scrollToSelection(node);
|
|
88
|
+
scrollToSelection(resolveScrollTarget(view, target));
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// Called from the plugin's post-DOM update hook; defer measurement until layout can settle.
|
|
92
|
+
rafId = requestAnimationFrame(() => {
|
|
93
|
+
rafId = null;
|
|
94
|
+
if (!cancelled) {
|
|
95
|
+
scrollToTarget();
|
|
88
96
|
}
|
|
89
97
|
});
|
|
90
98
|
return () => {
|
|
99
|
+
cancelled = true;
|
|
91
100
|
if (rafId !== null) {
|
|
92
101
|
cancelAnimationFrame(rafId);
|
|
93
102
|
rafId = null;
|
|
@@ -22,6 +22,7 @@ import { createDocMarginAnchorWidget } from '../decorations/createAnchorDecorati
|
|
|
22
22
|
import { createBlockChangedDecoration } from '../decorations/createBlockChangedDecoration';
|
|
23
23
|
import { createInlineChangedDecoration } from '../decorations/createInlineChangedDecoration';
|
|
24
24
|
import { createNodeChangedDecorationWidget } from '../decorations/createNodeChangedDecorationWidget';
|
|
25
|
+
import { createReplacedCellDecorations, getReplacedCells } from '../decorations/createReplacedCellDecorations';
|
|
25
26
|
import { extractDiffDescriptors } from '../decorations/decorationKeys';
|
|
26
27
|
import { extractContributorTags } from '../decorations/extractContributorTags';
|
|
27
28
|
import { createDeletedLineBreakDecoration } from '../decorations/utils/createDeletedLineBreakWidget';
|
|
@@ -596,7 +597,11 @@ var calculateDiffDecorationsInner = function calculateDiffDecorationsInner(_ref8
|
|
|
596
597
|
// suppressed in the clean view. `hideDeletedDiffs` is load-bearing: when false the reviewer
|
|
597
598
|
// asked to compare against the original, and the deleted side is the only place the
|
|
598
599
|
// previous formatting is visible.
|
|
599
|
-
|
|
600
|
+
//
|
|
601
|
+
// Not on an inverted diff: there the widget's side is the suggested content, which the clean
|
|
602
|
+
// view keeps. Suppressing it hides the suggestion along with the displayed side it replaces,
|
|
603
|
+
// e.g. a cell whose only change is its background, as the signature ignores attrs.
|
|
604
|
+
var isMarkOnly = shouldHideMarkOnlyDeletions && hideDeletedDiffs && !(isInverted && fg('platform_editor_ai_show_diff_patch_2')) && change.deleted.length > 0 && isMarkOnlyChange({
|
|
600
605
|
change: change,
|
|
601
606
|
originalDoc: originalDoc,
|
|
602
607
|
newDoc: tr.doc
|
|
@@ -682,7 +687,35 @@ var calculateDiffDecorationsInner = function calculateDiffDecorationsInner(_ref8
|
|
|
682
687
|
hideDeletedDiffs: hideDeletedDiffs,
|
|
683
688
|
isInverted: isInverted
|
|
684
689
|
});
|
|
685
|
-
|
|
690
|
+
|
|
691
|
+
// A whole-cell replacement keeps its cells in place, so it takes its own decorations
|
|
692
|
+
// instead of the generic inline and block ones.
|
|
693
|
+
var replacedCells = fg('platform_editor_ai_show_diff_patch_2') ? getReplacedCells({
|
|
694
|
+
slice: originalDoc.slice(change.fromA, change.toA),
|
|
695
|
+
doc: tr.doc,
|
|
696
|
+
fromB: change.fromB,
|
|
697
|
+
toB: change.toB
|
|
698
|
+
}) : [];
|
|
699
|
+
if (replacedCells.length > 0) {
|
|
700
|
+
decorations.push.apply(decorations, _toConsumableArray(createReplacedCellDecorations({
|
|
701
|
+
replacedCells: replacedCells,
|
|
702
|
+
change: change,
|
|
703
|
+
doc: tr.doc,
|
|
704
|
+
attributionKey: attributionKey,
|
|
705
|
+
colorScheme: changeColorScheme,
|
|
706
|
+
hasDeletedWidget: willRenderDeletedWidget,
|
|
707
|
+
hideAddedDiffsUnderline: hideAddedDiffsUnderline,
|
|
708
|
+
isActive: isActive,
|
|
709
|
+
isDeletedWidgetBelow: isDeletedWidgetBelow,
|
|
710
|
+
isInserted: isInserted,
|
|
711
|
+
leftAnchorId: leftAnchorId,
|
|
712
|
+
reveal: reveal,
|
|
713
|
+
shouldHideDeleted: shouldHideDeleted,
|
|
714
|
+
showContributorTags: showContributorTags,
|
|
715
|
+
showIndicators: showIndicators,
|
|
716
|
+
tagMountContext: tagMountContext
|
|
717
|
+
})));
|
|
718
|
+
} else if (isSmartNodeLevel) {
|
|
686
719
|
// For a large inserted table, skip the O(cells) inline decorations inside the
|
|
687
720
|
// table (the cell overlays added below provide the highlight without the
|
|
688
721
|
// expensive content re-render); leaf blocks outside the table still get theirs.
|
|
@@ -742,7 +775,7 @@ var calculateDiffDecorationsInner = function calculateDiffDecorationsInner(_ref8
|
|
|
742
775
|
tagMountContext: tagMountContext
|
|
743
776
|
})));
|
|
744
777
|
}
|
|
745
|
-
decorations.push.apply(decorations, _toConsumableArray(calculateNodesForBlockDecoration({
|
|
778
|
+
decorations.push.apply(decorations, _toConsumableArray(replacedCells.length > 0 ? [] : calculateNodesForBlockDecoration({
|
|
746
779
|
// A deleted widget placed below anchors at the change's end, inside the very block
|
|
747
780
|
// this would let the decoration claim.
|
|
748
781
|
allowTrailingCloseTokens: !isDeletedWidgetBelow,
|