@atlaskit/editor-plugin-show-diff 17.0.1 → 17.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 +15 -0
- package/dist/cjs/pm-plugins/calculateDiff/calculateDiffDecorations.js +126 -49
- package/dist/cjs/pm-plugins/calculateDiff/groupDeletedColumnChanges.js +265 -0
- package/dist/cjs/pm-plugins/decorations/createChangedRowDecorationWidgets.js +100 -34
- package/dist/cjs/pm-plugins/decorations/createContributorTagWidget.js +18 -13
- package/dist/cjs/pm-plugins/decorations/createNodeChangedDecorationWidget.js +82 -20
- package/dist/cjs/pm-plugins/decorations/utils/createDeletedLineBreakWidget.js +106 -0
- package/dist/cjs/pm-plugins/decorations/utils/tableDiffMode.js +36 -0
- package/dist/cjs/pm-plugins/decorations/utils/wrapBlockNodeView.js +17 -6
- package/dist/cjs/pm-plugins/utils/emptyTextBlocks.js +66 -0
- package/dist/es2019/pm-plugins/calculateDiff/calculateDiffDecorations.js +61 -1
- package/dist/es2019/pm-plugins/calculateDiff/groupDeletedColumnChanges.js +210 -0
- package/dist/es2019/pm-plugins/decorations/createChangedRowDecorationWidgets.js +79 -13
- package/dist/es2019/pm-plugins/decorations/createContributorTagWidget.js +8 -2
- package/dist/es2019/pm-plugins/decorations/createNodeChangedDecorationWidget.js +85 -15
- package/dist/es2019/pm-plugins/decorations/utils/createDeletedLineBreakWidget.js +100 -0
- package/dist/es2019/pm-plugins/decorations/utils/tableDiffMode.js +27 -0
- package/dist/es2019/pm-plugins/decorations/utils/wrapBlockNodeView.js +15 -5
- package/dist/es2019/pm-plugins/utils/emptyTextBlocks.js +58 -0
- package/dist/esm/pm-plugins/calculateDiff/calculateDiffDecorations.js +127 -50
- package/dist/esm/pm-plugins/calculateDiff/groupDeletedColumnChanges.js +258 -0
- package/dist/esm/pm-plugins/decorations/createChangedRowDecorationWidgets.js +100 -34
- package/dist/esm/pm-plugins/decorations/createContributorTagWidget.js +18 -13
- package/dist/esm/pm-plugins/decorations/createNodeChangedDecorationWidget.js +84 -22
- package/dist/esm/pm-plugins/decorations/utils/createDeletedLineBreakWidget.js +100 -0
- package/dist/esm/pm-plugins/decorations/utils/tableDiffMode.js +30 -0
- package/dist/esm/pm-plugins/decorations/utils/wrapBlockNodeView.js +16 -5
- package/dist/esm/pm-plugins/utils/emptyTextBlocks.js +60 -0
- package/dist/types/pm-plugins/calculateDiff/groupDeletedColumnChanges.d.ts +9 -0
- package/dist/types/pm-plugins/decorations/createChangedRowDecorationWidgets.d.ts +3 -1
- package/dist/types/pm-plugins/decorations/createContributorTagWidget.d.ts +5 -1
- package/dist/types/pm-plugins/decorations/createNodeChangedDecorationWidget.d.ts +2 -1
- package/dist/types/pm-plugins/decorations/utils/createDeletedLineBreakWidget.d.ts +50 -0
- package/dist/types/pm-plugins/decorations/utils/tableDiffMode.d.ts +26 -0
- package/dist/types/pm-plugins/decorations/utils/wrapBlockNodeView.d.ts +4 -0
- package/dist/types/pm-plugins/utils/emptyTextBlocks.d.ts +16 -0
- package/package.json +2 -2
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.emptyTextBlockContentPositions = exports.countEmptyTextBlockOnlySlice = void 0;
|
|
7
|
+
/**
|
|
8
|
+
* The textblock types whose empty form is a blank line the author put there deliberately, and so
|
|
9
|
+
* worth marking when a change removes it.
|
|
10
|
+
*
|
|
11
|
+
* Deliberately not every textblock: an empty `codeBlock` is an empty code block, which renders as a
|
|
12
|
+
* visible box of its own and needs no stand-in.
|
|
13
|
+
*/
|
|
14
|
+
var BLANK_LINE_NODE_NAMES = new Set(['paragraph', 'heading']);
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Structurally empty: no children at all, not merely no measurable content. The two coincide for a
|
|
18
|
+
* real node, and testing both keeps a node whose children report no size from passing as blank.
|
|
19
|
+
*/
|
|
20
|
+
var isEmptyTextBlock = function isEmptyTextBlock(node) {
|
|
21
|
+
return BLANK_LINE_NODE_NAMES.has(node.type.name) && node.content.childCount === 0 && node.content.size === 0;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* How many empty textblocks a slice holds, or 0 unless that is *all* it holds.
|
|
26
|
+
*
|
|
27
|
+
* Counted rather than tested for one, because a run of adjacent blank lines is reported as a single
|
|
28
|
+
* change covering all of them — the case that made a single-block test miss.
|
|
29
|
+
*/
|
|
30
|
+
var countEmptyTextBlockOnlySlice = exports.countEmptyTextBlockOnlySlice = function countEmptyTextBlockOnlySlice(slice) {
|
|
31
|
+
var _slice$content$childC;
|
|
32
|
+
var childCount = (_slice$content$childC = slice === null || slice === void 0 ? void 0 : slice.content.childCount) !== null && _slice$content$childC !== void 0 ? _slice$content$childC : 0;
|
|
33
|
+
if (!slice || childCount === 0) {
|
|
34
|
+
return 0;
|
|
35
|
+
}
|
|
36
|
+
for (var index = 0; index < childCount; index++) {
|
|
37
|
+
if (!isEmptyTextBlock(slice.content.child(index))) {
|
|
38
|
+
return 0;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return childCount;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The content position of every empty textblock lying wholly within `[from, to]`.
|
|
46
|
+
*
|
|
47
|
+
* Used to place a marker on each blank line a change removes. Partially covered blocks are skipped:
|
|
48
|
+
* the change only took part of one, so it still has text of its own, which the inline decoration
|
|
49
|
+
* already marks.
|
|
50
|
+
*/
|
|
51
|
+
var emptyTextBlockContentPositions = exports.emptyTextBlockContentPositions = function emptyTextBlockContentPositions(doc, from, to) {
|
|
52
|
+
var positions = [];
|
|
53
|
+
doc.nodesBetween(from, to, function (node, pos) {
|
|
54
|
+
if (node.isTextblock) {
|
|
55
|
+
if (isEmptyTextBlock(node) && pos >= from && pos + node.nodeSize <= to) {
|
|
56
|
+
// +1 steps over the block's own open token, into its (empty) content.
|
|
57
|
+
positions.push(pos + 1);
|
|
58
|
+
}
|
|
59
|
+
// A textblock holds inline content only, so there is nothing below it to visit.
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
// Keep descending: blank lines nest inside cells, columns and panels.
|
|
63
|
+
return true;
|
|
64
|
+
});
|
|
65
|
+
return positions;
|
|
66
|
+
};
|
|
@@ -4,7 +4,8 @@ import { ChangeSet, simplifyChanges } from 'prosemirror-changeset';
|
|
|
4
4
|
import { areDocsEqualByBlockStructureAndText } from '@atlaskit/editor-common/utils/areDocsEqualByBlockStructureAndText';
|
|
5
5
|
import { areNodesEqualIgnoreAttrs } from '@atlaskit/editor-common/utils/document';
|
|
6
6
|
import { Mapping } from '@atlaskit/editor-prosemirror/transform';
|
|
7
|
-
import { DecorationSet } from '@atlaskit/editor-prosemirror/view';
|
|
7
|
+
import { Decoration, DecorationSet } from '@atlaskit/editor-prosemirror/view';
|
|
8
|
+
import { TableMap } from '@atlaskit/editor-tables/table-map';
|
|
8
9
|
import { UNSAFE_expValNoExposure } from '@atlaskit/platform-feature-experiments/unsafe-exp-val-no-exposure';
|
|
9
10
|
import { fg } from '@atlaskit/platform-feature-flags/fg';
|
|
10
11
|
import { expValEqualsNoExposure } from '@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure';
|
|
@@ -15,15 +16,19 @@ import { createNodeChangedDecorationWidget } from '../decorations/createNodeChan
|
|
|
15
16
|
import { createAttributionColorMap, getAttributionKey, getAttributionKeyForChange, getColorSchemeForChange, isAttributionColoringEnabled, isContributorTagsEnabled } from '../decorations/colorSchemes/attributions';
|
|
16
17
|
import { extractDiffDescriptors } from '../decorations/decorationKeys';
|
|
17
18
|
import { extractContributorTags } from '../decorations/extractContributorTags';
|
|
19
|
+
import { createDeletedLineBreakDecoration } from '../decorations/utils/createDeletedLineBreakWidget';
|
|
18
20
|
import { getAttrChangeRanges, stepIsValidAttrChange } from '../decorations/utils/getAttrChangeRanges';
|
|
21
|
+
import { createRemovedLozenge } from '../decorations/utils/wrapBlockNodeView';
|
|
19
22
|
import { getMarkChangeRanges } from '../decorations/utils/getMarkChangeRanges';
|
|
20
23
|
import { getScrollableDecorations } from '../getScrollableDecorations';
|
|
21
24
|
import { getDefaultDiffType, isExtendedEnabled } from '../isExtendedEnabled';
|
|
25
|
+
import { emptyTextBlockContentPositions } from '../utils/emptyTextBlocks';
|
|
22
26
|
import { diffBySteps } from './diffBySteps';
|
|
23
27
|
import { groupChangesByBlock } from './groupChangesByBlock';
|
|
24
28
|
import { isMarkOnlyChange } from './isMarkOnlyChange';
|
|
25
29
|
import { isOpenTokenOnlyChange } from './isOpenTokenOnlyChange';
|
|
26
30
|
import { optimizeChanges } from './optimizeChanges';
|
|
31
|
+
import { CELL_CONTENT_OFFSET, groupDeletedColumnChanges } from './groupDeletedColumnChanges';
|
|
27
32
|
import { selectTokenEncoder } from './selectTokenEncoder';
|
|
28
33
|
import { collapseOverlappingChanges, simplifyChangesWithAttribution } from './simplifyChangesWithAttribution';
|
|
29
34
|
import { simplifySteps, simplifyStepsWithAttribution } from './simplifySteps';
|
|
@@ -485,10 +490,32 @@ const calculateDiffDecorationsInner = ({
|
|
|
485
490
|
|
|
486
491
|
// Our default operations are insertions, so it should match the opposite of isInverted.
|
|
487
492
|
const isInserted = !isInverted;
|
|
493
|
+
if (isExtendedEnabled(diffType)) {
|
|
494
|
+
changes = groupDeletedColumnChanges(changes, originalDoc, tr.doc, new Mapping(stepMaps), isInverted);
|
|
495
|
+
}
|
|
496
|
+
// A whole-table change owns every mark and attribute range inside its bounds, so those ranges
|
|
497
|
+
// do not also get their own decoration.
|
|
498
|
+
const tableChanges = changes.filter(change => change.deletedColumns !== undefined);
|
|
499
|
+
const isHandledByTable = (from, to) => tableChanges.some(change => from >= change.fromB && to <= change.toB);
|
|
488
500
|
const createDecorationsForChange = change => {
|
|
489
501
|
const changeColorScheme = attributionColors ? getColorSchemeForChange(change, attributedChanges, attributionColors, colorScheme) : colorScheme;
|
|
490
502
|
const attributionKey = showContributorTags ? getAttributionKeyForChange(change, attributedChanges) : undefined;
|
|
491
503
|
const isActive = isRangeActive(activeIndexPos, change.fromB, change.toB);
|
|
504
|
+
const {
|
|
505
|
+
deletedColumns
|
|
506
|
+
} = change;
|
|
507
|
+
// On an inverted diff the table that lost columns is the one in `tr.doc`, so the column
|
|
508
|
+
// indices address it directly and each label anchors inside its own cell.
|
|
509
|
+
const table = tr.doc.nodeAt(change.fromB);
|
|
510
|
+
if (isInverted && !hideDeletedDiffs && deletedColumns !== null && deletedColumns !== void 0 && deletedColumns.length && (table === null || table === void 0 ? void 0 : table.type.name) === 'table') {
|
|
511
|
+
const tableMap = TableMap.get(table);
|
|
512
|
+
deletedColumns.forEach(column => {
|
|
513
|
+
const offset = tableMap.positionAt(0, column, table);
|
|
514
|
+
decorations.push(Decoration.widget(change.fromB + CELL_CONTENT_OFFSET + offset, () => createRemovedLozenge(intl, isActive, changeColorScheme, true), {
|
|
515
|
+
side: -1
|
|
516
|
+
}));
|
|
517
|
+
});
|
|
518
|
+
}
|
|
492
519
|
|
|
493
520
|
// The deleted side of a mark-only change is a byte-identical copy of the new text, so it is
|
|
494
521
|
// suppressed in the clean view. `hideDeletedDiffs` is load-bearing: when false the reviewer
|
|
@@ -522,6 +549,32 @@ const calculateDiffDecorationsInner = ({
|
|
|
522
549
|
// On an inverted diff the inserted side is visually the deleted side.
|
|
523
550
|
const shouldHideDeleted = isExtendedEnabled(diffType) ? isInverted && hideDeletedDiffs : false;
|
|
524
551
|
|
|
552
|
+
// A removed blank line — an empty paragraph or heading — has nothing to mark up. On an
|
|
553
|
+
// inverted diff, the shape AI suggested edits renders, the block is still in the displayed
|
|
554
|
+
// document, so the removal lands here on the inserted side rather than on the deleted side,
|
|
555
|
+
// and both the inline and the block decoration paint nothing: an empty textblock has no
|
|
556
|
+
// inline content to cover, and block decorations return no style for a paragraph or heading.
|
|
557
|
+
// The reviewer sees an unmarked blank line. Stand the struck-through return glyph in for each
|
|
558
|
+
// one, anchored inside the block so it renders on the blank line itself.
|
|
559
|
+
//
|
|
560
|
+
// One decoration per blank line rather than one per change: a run of adjacent blank lines is
|
|
561
|
+
// reported as a single change spanning all of them.
|
|
562
|
+
//
|
|
563
|
+
// Withheld in the clean view, where removals are hidden rather than struck through.
|
|
564
|
+
if (fg('platform_editor_ai_show_diff_patch_2') && !isInserted && !shouldHideDeleted) {
|
|
565
|
+
for (const pos of emptyTextBlockContentPositions(tr.doc, change.fromB, change.toB)) {
|
|
566
|
+
decorations.push(createDeletedLineBreakDecoration({
|
|
567
|
+
attributionKey,
|
|
568
|
+
colorScheme: changeColorScheme,
|
|
569
|
+
diffType,
|
|
570
|
+
isActive,
|
|
571
|
+
pos,
|
|
572
|
+
reveal,
|
|
573
|
+
side: -1
|
|
574
|
+
}));
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
525
578
|
// For `smart` NODE-level promotions the change range spans a whole container
|
|
526
579
|
// (e.g. an entire list/table/layout, using outer node bounds). Applying a SINGLE
|
|
527
580
|
// inline decoration across that whole range would paint the inserted style across
|
|
@@ -630,6 +683,7 @@ const calculateDiffDecorationsInner = ({
|
|
|
630
683
|
});
|
|
631
684
|
if (!shouldHideDeleted) {
|
|
632
685
|
decorations.push(...createNodeChangedDecorationWidget({
|
|
686
|
+
deletedColumns,
|
|
633
687
|
// The deleted side has no inline decoration, so the widget carries the actor.
|
|
634
688
|
attributionKey,
|
|
635
689
|
// Extended pipeline only, matching the inline path: the non-extended styles have no
|
|
@@ -665,6 +719,9 @@ const calculateDiffDecorationsInner = ({
|
|
|
665
719
|
getMarkChangeRanges(simplifiedSteps, simplifiedStepAttributions, fg('confluence_ncs_step_diffing_version_history') ? stepMaps : undefined).filter(({
|
|
666
720
|
fromB,
|
|
667
721
|
toB
|
|
722
|
+
}) => !isHandledByTable(fromB, toB)).filter(({
|
|
723
|
+
fromB,
|
|
724
|
+
toB
|
|
668
725
|
}) => !fg('confluence_ncs_step_diffing_version_history') || !changes.some(change => change.fromB === fromB && change.toB === toB)).forEach(change => {
|
|
669
726
|
var _attributionColors$ge, _attributionColors;
|
|
670
727
|
const 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;
|
|
@@ -686,6 +743,9 @@ const calculateDiffDecorationsInner = ({
|
|
|
686
743
|
getAttrChangeRanges(tr.doc, attrStepContexts, originalDoc).filter(({
|
|
687
744
|
fromB,
|
|
688
745
|
toB
|
|
746
|
+
}) => !isHandledByTable(fromB, toB)).filter(({
|
|
747
|
+
fromB,
|
|
748
|
+
toB
|
|
689
749
|
}) => !fg('confluence_ncs_step_diffing_version_history') || !changes.some(change => change.fromB === fromB && change.toB === toB)).forEach(change => {
|
|
690
750
|
var _attributionColors$ge2, _attributionColors2;
|
|
691
751
|
const changeColorScheme = change.attributionKey ? (_attributionColors$ge2 = (_attributionColors2 = attributionColors) === null || _attributionColors2 === void 0 ? void 0 : _attributionColors2.get(change.attributionKey)) !== null && _attributionColors$ge2 !== void 0 ? _attributionColors$ge2 : colorScheme : colorScheme;
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { areNodesEqualIgnoreAttrs } from '@atlaskit/editor-common/utils/document';
|
|
2
|
+
import { Fragment, Slice } from '@atlaskit/editor-prosemirror/model';
|
|
3
|
+
import { TableMap } from '@atlaskit/editor-tables/table-map';
|
|
4
|
+
export const CELL_CONTENT_OFFSET = 2;
|
|
5
|
+
const IGNORED_CELL_ATTRS = ['localId', 'colwidth'];
|
|
6
|
+
const tablesIn = doc => {
|
|
7
|
+
const tables = [];
|
|
8
|
+
doc.descendants((node, pos) => {
|
|
9
|
+
if (node.type.name === 'table') {
|
|
10
|
+
tables.push({
|
|
11
|
+
node,
|
|
12
|
+
pos
|
|
13
|
+
});
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
return true;
|
|
17
|
+
});
|
|
18
|
+
return tables;
|
|
19
|
+
};
|
|
20
|
+
const tableEnd = table => table.pos + table.node.nodeSize;
|
|
21
|
+
const documentsDifferOnlyByTable = (originalDoc, newDoc, before, after) => {
|
|
22
|
+
try {
|
|
23
|
+
return originalDoc.replace(before.pos, tableEnd(before), new Slice(Fragment.from(after.node), 0, 0)).eq(newDoc);
|
|
24
|
+
} catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
const columnOffsets = (map, table) => Array.from({
|
|
29
|
+
length: map.width
|
|
30
|
+
}, (_, column) => map.positionAt(0, column, table));
|
|
31
|
+
const positionMatchedColumns = ({
|
|
32
|
+
before,
|
|
33
|
+
after,
|
|
34
|
+
afterToBefore,
|
|
35
|
+
beforeOffsets
|
|
36
|
+
}) => {
|
|
37
|
+
const afterMap = TableMap.get(after.node);
|
|
38
|
+
const surviving = new Set();
|
|
39
|
+
for (let column = 0; column < afterMap.width; column++) {
|
|
40
|
+
const offset = afterMap.positionAt(0, column, after.node);
|
|
41
|
+
const mapped = afterToBefore.mapResult(after.pos + CELL_CONTENT_OFFSET + offset);
|
|
42
|
+
const beforeColumn = beforeOffsets.indexOf(mapped.pos - before.pos - CELL_CONTENT_OFFSET);
|
|
43
|
+
if (mapped.deleted || beforeColumn === -1) {
|
|
44
|
+
return new Set();
|
|
45
|
+
}
|
|
46
|
+
surviving.add(beforeColumn);
|
|
47
|
+
}
|
|
48
|
+
return surviving;
|
|
49
|
+
};
|
|
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;
|
|
58
|
+
}
|
|
59
|
+
surviving.add(matches[0]);
|
|
60
|
+
}
|
|
61
|
+
const ordered = [...surviving];
|
|
62
|
+
return !ordered.some((column, index) => index > 0 && column <= ordered[index - 1]);
|
|
63
|
+
};
|
|
64
|
+
const findDeletedColumns = (before, after, afterToBefore) => {
|
|
65
|
+
const beforeMap = TableMap.get(before.node);
|
|
66
|
+
const afterMap = TableMap.get(after.node);
|
|
67
|
+
// Merged or malformed tables do not have reliable column indices.
|
|
68
|
+
if (beforeMap.hasMergedCells() || afterMap.hasMergedCells() || beforeMap.problems || afterMap.problems) {
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
const beforeOffsets = columnOffsets(beforeMap, before.node);
|
|
72
|
+
const surviving = positionMatchedColumns({
|
|
73
|
+
before,
|
|
74
|
+
after,
|
|
75
|
+
afterToBefore,
|
|
76
|
+
beforeOffsets
|
|
77
|
+
});
|
|
78
|
+
if (surviving.size !== afterMap.width && !addContentMatchedColumns(surviving, before, after)) {
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
return beforeOffsets.flatMap((_, column) => surviving.has(column) ? [] : [column]);
|
|
82
|
+
};
|
|
83
|
+
const findCounterpart = ({
|
|
84
|
+
table,
|
|
85
|
+
index,
|
|
86
|
+
originalTables,
|
|
87
|
+
newTables,
|
|
88
|
+
mapping
|
|
89
|
+
}) => {
|
|
90
|
+
const from = mapping.map(table.pos, -1);
|
|
91
|
+
const to = mapping.map(tableEnd(table), 1);
|
|
92
|
+
const inside = mapping.mapResult(table.pos + 1);
|
|
93
|
+
const candidates = newTables.filter(candidate => inside.deleted ? candidate.pos >= from && tableEnd(candidate) <= to : candidate.pos < inside.pos && inside.pos < tableEnd(candidate));
|
|
94
|
+
if (candidates.length === 1) {
|
|
95
|
+
return candidates[0];
|
|
96
|
+
}
|
|
97
|
+
return originalTables.length === newTables.length ? newTables[index] : undefined;
|
|
98
|
+
};
|
|
99
|
+
const orient = ({
|
|
100
|
+
table,
|
|
101
|
+
counterpart,
|
|
102
|
+
mapping,
|
|
103
|
+
isInverted
|
|
104
|
+
}) => ({
|
|
105
|
+
before: isInverted ? counterpart : table,
|
|
106
|
+
after: isInverted ? table : counterpart,
|
|
107
|
+
afterToBefore: (isInverted ? mapping.invert() : mapping).invert()
|
|
108
|
+
});
|
|
109
|
+
const changesInTable = (changes, table) => changes.filter(change => change.fromA < tableEnd(table) && change.toA > table.pos);
|
|
110
|
+
const isNarrowingSafe = ({
|
|
111
|
+
replacedChanges,
|
|
112
|
+
table,
|
|
113
|
+
counterpart,
|
|
114
|
+
originalDoc,
|
|
115
|
+
newDoc
|
|
116
|
+
}) => {
|
|
117
|
+
const staysInside = replacedChanges.every(change => change.fromA >= table.pos && change.toA <= tableEnd(table) && change.fromB >= counterpart.pos && change.toB <= tableEnd(counterpart));
|
|
118
|
+
return staysInside || documentsDifferOnlyByTable(originalDoc, newDoc, table, counterpart);
|
|
119
|
+
};
|
|
120
|
+
const planTableChange = ({
|
|
121
|
+
table,
|
|
122
|
+
index,
|
|
123
|
+
originalTables,
|
|
124
|
+
newTables,
|
|
125
|
+
changes,
|
|
126
|
+
originalDoc,
|
|
127
|
+
newDoc,
|
|
128
|
+
mapping,
|
|
129
|
+
isInverted
|
|
130
|
+
}) => {
|
|
131
|
+
const counterpart = findCounterpart({
|
|
132
|
+
table,
|
|
133
|
+
index,
|
|
134
|
+
originalTables,
|
|
135
|
+
newTables,
|
|
136
|
+
mapping
|
|
137
|
+
});
|
|
138
|
+
if (!counterpart) {
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
const {
|
|
142
|
+
before,
|
|
143
|
+
after,
|
|
144
|
+
afterToBefore
|
|
145
|
+
} = orient({
|
|
146
|
+
table,
|
|
147
|
+
counterpart,
|
|
148
|
+
mapping,
|
|
149
|
+
isInverted
|
|
150
|
+
});
|
|
151
|
+
if (TableMap.get(before.node).width <= TableMap.get(after.node).width) {
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
154
|
+
const replacedChanges = changesInTable(changes, table);
|
|
155
|
+
if (!replacedChanges.length) {
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
if (!isNarrowingSafe({
|
|
159
|
+
replacedChanges,
|
|
160
|
+
table,
|
|
161
|
+
counterpart,
|
|
162
|
+
originalDoc,
|
|
163
|
+
newDoc
|
|
164
|
+
})) {
|
|
165
|
+
return undefined;
|
|
166
|
+
}
|
|
167
|
+
const spans = replacedChanges.flatMap(change => [...change.deleted, ...change.inserted]);
|
|
168
|
+
return {
|
|
169
|
+
replacedChanges,
|
|
170
|
+
tableChange: {
|
|
171
|
+
deletedColumns: findDeletedColumns(before, after, afterToBefore),
|
|
172
|
+
fromA: table.pos,
|
|
173
|
+
toA: tableEnd(table),
|
|
174
|
+
fromB: counterpart.pos,
|
|
175
|
+
toB: tableEnd(counterpart),
|
|
176
|
+
deleted: spans,
|
|
177
|
+
inserted: spans
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
/** Groups a deleted column's cell changes into one whole-table change. */
|
|
183
|
+
export const groupDeletedColumnChanges = (changes, originalDoc, newDoc, mapping, isInverted) => {
|
|
184
|
+
const originalTables = tablesIn(originalDoc);
|
|
185
|
+
const newTables = tablesIn(newDoc);
|
|
186
|
+
const plans = [];
|
|
187
|
+
const replaced = new Set();
|
|
188
|
+
originalTables.forEach((table, index) => {
|
|
189
|
+
const plan = planTableChange({
|
|
190
|
+
table,
|
|
191
|
+
index,
|
|
192
|
+
originalTables,
|
|
193
|
+
newTables,
|
|
194
|
+
changes: changes.filter(change => !replaced.has(change)),
|
|
195
|
+
originalDoc,
|
|
196
|
+
newDoc,
|
|
197
|
+
mapping,
|
|
198
|
+
isInverted
|
|
199
|
+
});
|
|
200
|
+
if (!plan) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
plans.push(plan);
|
|
204
|
+
plan.replacedChanges.forEach(change => replaced.add(change));
|
|
205
|
+
});
|
|
206
|
+
if (!plans.length) {
|
|
207
|
+
return [...changes];
|
|
208
|
+
}
|
|
209
|
+
return [...changes.filter(change => !replaced.has(change)), ...plans.map(plan => plan.tableChange)].sort((left, right) => left.fromB - right.fromB);
|
|
210
|
+
};
|
|
@@ -13,6 +13,7 @@ import { resolveCellOverlayStyleLegacy, resolveDeletedRowStyleLegacy } from './c
|
|
|
13
13
|
import { createContributorTagWidget, isContributorTagWidgetEnabled } from './createContributorTagWidget';
|
|
14
14
|
import { AnchorTypeKey, buildAnchorDecorationKey, buildDiffDecorationSpec, scrollMarginTopValue } from './decorationKeys';
|
|
15
15
|
import { findSafeInsertPos } from './utils/findSafeInsertPos';
|
|
16
|
+
import { createRemovedLozenge } from './utils/wrapBlockNodeView';
|
|
16
17
|
import { applyCellEdgeAttrs, getRowCellEdgeAttrs, getTableTopAndBottomCellEdgeAttrs } from './utils/tableCellEdgeAttrs';
|
|
17
18
|
/**
|
|
18
19
|
* Extracts information about deleted table rows from a change
|
|
@@ -144,17 +145,28 @@ const isEmptyRow = rowNode => {
|
|
|
144
145
|
});
|
|
145
146
|
return isEmpty;
|
|
146
147
|
};
|
|
147
|
-
|
|
148
148
|
/**
|
|
149
149
|
* Creates a DOM representation of a deleted table row
|
|
150
150
|
*/
|
|
151
|
-
const createChangedRowDOM = (
|
|
151
|
+
const createChangedRowDOM = ({
|
|
152
|
+
rowNode,
|
|
153
|
+
cellEdgeAttrs,
|
|
154
|
+
nodeViewSerializer,
|
|
155
|
+
colorScheme,
|
|
156
|
+
isInserted,
|
|
157
|
+
diffType,
|
|
158
|
+
hasAnchoredRemovedLozenge,
|
|
159
|
+
intl,
|
|
160
|
+
isActive
|
|
161
|
+
}) => {
|
|
152
162
|
const tr = document.createElement('tr');
|
|
153
163
|
const colors = colorSchemeRegistry[colorScheme !== null && colorScheme !== void 0 ? colorScheme : 'standard'];
|
|
164
|
+
const deletedTreatment = isExperimentEnabled('platform_editor_show_diff_color_scheme_refactor') ? buildDeletedRowStyle(colors) : resolveDeletedRowStyleLegacy(getLegacyColorScheme(colorScheme));
|
|
165
|
+
const hostsRemovedLozenge = Boolean(intl) && isExtendedEnabled(diffType) && !isInserted && !hasAnchoredRemovedLozenge;
|
|
154
166
|
|
|
155
167
|
// Inserted rows keep their natural styling; the row strikethrough is deletions only.
|
|
156
168
|
if (!isExtendedEnabled(diffType) || !isInserted) {
|
|
157
|
-
tr.setAttribute('style',
|
|
169
|
+
tr.setAttribute('style', deletedTreatment);
|
|
158
170
|
}
|
|
159
171
|
// Mirrors the strikethrough condition above: under the extended experience an `isInserted` row
|
|
160
172
|
// is ADDED content, so it must not claim the "deleted" testid — page models match that as
|
|
@@ -195,8 +207,40 @@ const createChangedRowDOM = (rowNode, cellEdgeAttrs, nodeViewSerializer, colorSc
|
|
|
195
207
|
cellIndex++;
|
|
196
208
|
}
|
|
197
209
|
});
|
|
210
|
+
|
|
211
|
+
// A "Removed" label on the row's last cell, so it reads at the row's top right corner. Deletions
|
|
212
|
+
// only: an inserted row is added content and carries no removal label.
|
|
213
|
+
if (intl && hostsRemovedLozenge) {
|
|
214
|
+
const lastCell = tr.lastElementChild;
|
|
215
|
+
if (lastCell instanceof HTMLElement) {
|
|
216
|
+
lastCell.style.position = 'relative';
|
|
217
|
+
// Append, never prepend. Editor CSS resets the top margin of a cell's first child, so a
|
|
218
|
+
// prepended label pushes the paragraph out of that reset and the row grows taller.
|
|
219
|
+
lastCell.append(createRemovedLozenge(intl, isActive, colorScheme, true));
|
|
220
|
+
}
|
|
221
|
+
}
|
|
198
222
|
return tr;
|
|
199
223
|
};
|
|
224
|
+
const supportsAnchorPositioning = () => typeof CSS !== 'undefined' && typeof CSS.supports === 'function' && CSS.supports('top', 'anchor(--a top)');
|
|
225
|
+
const createAnchoredRemovedLozengeWidget = ({
|
|
226
|
+
anchorName,
|
|
227
|
+
colorScheme,
|
|
228
|
+
from,
|
|
229
|
+
intl,
|
|
230
|
+
isActive
|
|
231
|
+
}) => {
|
|
232
|
+
const lozenge = createRemovedLozenge(intl, isActive, colorScheme, true);
|
|
233
|
+
const inset = lozenge.style.top;
|
|
234
|
+
lozenge.style.setProperty('position', 'fixed');
|
|
235
|
+
lozenge.style.setProperty('top', `calc(anchor(--${anchorName} top) + ${inset})`);
|
|
236
|
+
lozenge.style.setProperty('left', `calc(anchor(--${anchorName} right) - ${inset})`);
|
|
237
|
+
lozenge.style.setProperty('right', 'auto');
|
|
238
|
+
lozenge.style.setProperty('transform', 'translateX(-100%)');
|
|
239
|
+
return Decoration.widget(from, lozenge, {
|
|
240
|
+
key: `removed-lozenge-${anchorName}`,
|
|
241
|
+
side: -1
|
|
242
|
+
});
|
|
243
|
+
};
|
|
200
244
|
|
|
201
245
|
/**
|
|
202
246
|
* Expands a diff to include whole changed rows when table rows are affected
|
|
@@ -236,6 +280,7 @@ export const createChangedRowDecorationWidgets = ({
|
|
|
236
280
|
isActive,
|
|
237
281
|
isInserted = false,
|
|
238
282
|
diffType,
|
|
283
|
+
intl,
|
|
239
284
|
showIndicators = false,
|
|
240
285
|
showContributorTags = false,
|
|
241
286
|
tagMountContext
|
|
@@ -248,16 +293,25 @@ export const createChangedRowDecorationWidgets = ({
|
|
|
248
293
|
diffType
|
|
249
294
|
});
|
|
250
295
|
return changedRows.flatMap(changedRow => {
|
|
251
|
-
|
|
296
|
+
// Keep the ID stable while contributor tags are enabled so recalculation preserves their state.
|
|
297
|
+
const diffId = showContributorTags ? `widget-row-${changedRow.fromA}-${changedRow.toA}` : crypto.randomUUID();
|
|
298
|
+
const hasAnchoredRemovedLozenge = Boolean(intl) && isExtendedEnabled(diffType) && !isInserted && supportsAnchorPositioning();
|
|
299
|
+
const rowDOM = createChangedRowDOM({
|
|
300
|
+
rowNode: changedRow.rowNode,
|
|
301
|
+
cellEdgeAttrs: changedRow.cellEdgeAttrs,
|
|
302
|
+
nodeViewSerializer,
|
|
303
|
+
colorScheme,
|
|
304
|
+
isInserted,
|
|
305
|
+
diffType,
|
|
306
|
+
hasAnchoredRemovedLozenge,
|
|
307
|
+
intl,
|
|
308
|
+
isActive
|
|
309
|
+
});
|
|
252
310
|
|
|
253
311
|
// Find safe insertion position for the deleted row
|
|
254
312
|
const safeInsertPos = findSafeInsertPos(newDoc, changedRow.fromB - 1,
|
|
255
313
|
// -1 to find the first safe position from the table
|
|
256
314
|
originalDoc.slice(changedRow.fromA, changedRow.toA));
|
|
257
|
-
|
|
258
|
-
// Stable while contributor tags are enabled so a decoration recalculation preserves the tag's
|
|
259
|
-
// identity and controller state.
|
|
260
|
-
const diffId = showContributorTags ? `widget-row-${changedRow.fromA}-${changedRow.toA}` : crypto.randomUUID();
|
|
261
315
|
const decorations = [];
|
|
262
316
|
const rowAnchorNames = [];
|
|
263
317
|
|
|
@@ -286,6 +340,11 @@ export const createChangedRowDecorationWidgets = ({
|
|
|
286
340
|
diffId,
|
|
287
341
|
anchorType: AnchorTypeKey.tag
|
|
288
342
|
}) : undefined;
|
|
343
|
+
const removedLozengeAnchorName = hasAnchoredRemovedLozenge ? tagAnchorName !== null && tagAnchorName !== void 0 ? tagAnchorName : buildAnchorDecorationKey({
|
|
344
|
+
diffId,
|
|
345
|
+
anchorType: AnchorTypeKey.tag
|
|
346
|
+
}) : undefined;
|
|
347
|
+
const rowLabelAnchorName = tagAnchorName !== null && tagAnchorName !== void 0 ? tagAnchorName : removedLozengeAnchorName;
|
|
289
348
|
const tagWidget = tagAnchorName ? createContributorTagWidget({
|
|
290
349
|
anchorAtRangeStart: true,
|
|
291
350
|
anchorName: tagAnchorName,
|
|
@@ -295,11 +354,9 @@ export const createChangedRowDecorationWidgets = ({
|
|
|
295
354
|
mountContext: tagMountContext,
|
|
296
355
|
to: safeInsertPos
|
|
297
356
|
}) : undefined;
|
|
298
|
-
if (
|
|
299
|
-
//
|
|
300
|
-
|
|
301
|
-
// opacity and strikethrough.
|
|
302
|
-
rowAnchorNames.push(tagAnchorName);
|
|
357
|
+
if (rowLabelAnchorName) {
|
|
358
|
+
// Labels stay outside the table row so they do not inherit its deletion treatment.
|
|
359
|
+
rowAnchorNames.push(rowLabelAnchorName);
|
|
303
360
|
}
|
|
304
361
|
if (rowAnchorNames.length > 0) {
|
|
305
362
|
rowDOM.style.setProperty('anchor-name', rowAnchorNames.map(anchorName => `--${anchorName}`).join(', '));
|
|
@@ -320,6 +377,15 @@ export const createChangedRowDecorationWidgets = ({
|
|
|
320
377
|
})
|
|
321
378
|
});
|
|
322
379
|
decorations.push(rowWidget);
|
|
380
|
+
if (intl && removedLozengeAnchorName) {
|
|
381
|
+
decorations.push(createAnchoredRemovedLozengeWidget({
|
|
382
|
+
anchorName: removedLozengeAnchorName,
|
|
383
|
+
colorScheme,
|
|
384
|
+
from: safeInsertPos,
|
|
385
|
+
intl,
|
|
386
|
+
isActive
|
|
387
|
+
}));
|
|
388
|
+
}
|
|
323
389
|
if (tagWidget) {
|
|
324
390
|
decorations.push(tagWidget);
|
|
325
391
|
}
|
|
@@ -88,14 +88,20 @@ const supportsAnchorPositioning = () => typeof CSS !== 'undefined' && typeof CSS
|
|
|
88
88
|
* The host for a tag on deleted content, which renders inside its own widget decoration rather than
|
|
89
89
|
* needing one of its own. Unmount with `unmountContributorTag` from that widget's `destroy`.
|
|
90
90
|
*/
|
|
91
|
-
export const createContributorTagHost = (
|
|
91
|
+
export const createContributorTagHost = ({
|
|
92
|
+
anchorName,
|
|
93
|
+
diffId,
|
|
94
|
+
mountContext
|
|
95
|
+
}) => {
|
|
92
96
|
if (!isContributorTagWidgetEnabled()) {
|
|
93
97
|
return undefined;
|
|
94
98
|
}
|
|
95
|
-
const
|
|
99
|
+
const tagAnchorName = anchorName && supportsAnchorPositioning() ? anchorName : undefined;
|
|
100
|
+
const host = buildContributorTagHost(diffId, tagAnchorName);
|
|
96
101
|
return {
|
|
97
102
|
host,
|
|
98
103
|
mount: mountContributorTag({
|
|
104
|
+
anchorName: tagAnchorName,
|
|
99
105
|
diffId,
|
|
100
106
|
host,
|
|
101
107
|
mountContext
|