@atlaskit/editor-plugin-show-diff 17.0.0 → 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 +21 -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 +4 -4
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
import { Decoration } from '@atlaskit/editor-prosemirror/view';
|
|
2
2
|
import { fg } from '@atlaskit/platform-feature-flags/fg';
|
|
3
3
|
import { isExtendedEnabled } from '../isExtendedEnabled';
|
|
4
|
+
import { countEmptyTextBlockOnlySlice } from '../utils/emptyTextBlocks';
|
|
4
5
|
import { isEmptyParagraphSlice } from '../utils/isEmptyParagraphSlice';
|
|
5
6
|
import { createLeftAnchorWidget } from './createAnchorDecorationWidgets';
|
|
6
7
|
import { createChangedRowDecorationWidgets } from './createChangedRowDecorationWidgets';
|
|
7
8
|
import { createContributorTagHost, unmountContributorTag } from './createContributorTagWidget';
|
|
8
|
-
import { buildDiffDecorationSpec, buildAnchorDecorationKey, scrollMarginTopValue } from './decorationKeys';
|
|
9
|
+
import { AnchorTypeKey, buildDiffDecorationSpec, buildAnchorDecorationKey, scrollMarginTopValue } from './decorationKeys';
|
|
9
10
|
import { absorbFirstChildMarginReset } from './utils/absorbFirstChildMarginReset';
|
|
11
|
+
import { createDeletedLineBreakDecoration } from './utils/createDeletedLineBreakWidget';
|
|
10
12
|
import { createMarginAbsorber } from './utils/createMarginAbsorber';
|
|
11
13
|
import { createNodeShapedMarginSpacer } from './utils/createNodeShapedMarginSpacer';
|
|
12
14
|
import { findSafeInsertPos } from './utils/findSafeInsertPos';
|
|
13
15
|
import { safeResolve } from './utils/safeResolve';
|
|
14
|
-
import {
|
|
16
|
+
import { getTableDiffMode } from './utils/tableDiffMode';
|
|
17
|
+
import { wrapBlockNodeView, createRemovedLozenge, injectInnerWrapper, createContentWrapper } from './utils/wrapBlockNodeView';
|
|
15
18
|
const isHeadingLevel = level => typeof level === 'number' && level >= 1 && level <= 6;
|
|
16
19
|
|
|
17
20
|
/**
|
|
@@ -119,11 +122,20 @@ export const createNodeChangedDecorationWidget = ({
|
|
|
119
122
|
diffType,
|
|
120
123
|
hideAddedDiffsUnderline = false,
|
|
121
124
|
reveal,
|
|
122
|
-
tagMountContext
|
|
125
|
+
tagMountContext,
|
|
126
|
+
deletedColumns
|
|
123
127
|
}) => {
|
|
124
128
|
var _$safeInsertPos$nodeB, _slice$content$firstC, _newDoc$firstChild, _slice$content$firstC2;
|
|
125
129
|
const slice = doc.slice(change.fromA, change.toA);
|
|
126
130
|
const shouldSkipDeletedEmptyParagraphDecoration = !isInserted && isEmptyParagraphSlice(slice);
|
|
131
|
+
// How many blank lines the deletion took, when it took nothing else. Blank means an empty
|
|
132
|
+
// paragraph or heading — both are a line the author put there deliberately, and neither leaves
|
|
133
|
+
// the widget any content to serialize.
|
|
134
|
+
//
|
|
135
|
+
// Kept separate from `shouldSkipDeletedEmptyParagraphDecoration` and gated, so the ungated early
|
|
136
|
+
// return below still covers only the single-empty-paragraph case it always did. Everything else
|
|
137
|
+
// degrades to what it rendered before the gate: an empty block, serialized invisibly.
|
|
138
|
+
const deletedBlankLineCount = !isInserted && fg('platform_editor_ai_show_diff_patch_2') ? countEmptyTextBlockOnlySlice(slice) : 0;
|
|
127
139
|
// Widget decoration used for deletions as the content is not in the document
|
|
128
140
|
// and we want to display the deleted content with a style.
|
|
129
141
|
// For `placeBelow`, anchor at the END of the new content (change.toB) so the deleted
|
|
@@ -131,16 +143,33 @@ export const createNodeChangedDecorationWidget = ({
|
|
|
131
143
|
const anchorPos = placeBelow ? change.toB : change.fromB;
|
|
132
144
|
const safeInsertPos = findSafeInsertPos(newDoc, anchorPos, slice);
|
|
133
145
|
const isActive = activeIndexPos && (fg('confluence_ncs_step_diffing_version_history') ? safeInsertPos >= activeIndexPos.from && safeInsertPos <= activeIndexPos.to : safeInsertPos === activeIndexPos.from && safeInsertPos === activeIndexPos.to);
|
|
134
|
-
if (
|
|
146
|
+
if (deletedBlankLineCount > 0) {
|
|
147
|
+
// The blocks have no content to serialize, so the deletion is shown as one struck-through
|
|
148
|
+
// return glyph per line the author removed. A single widget, not one per line: the lines are
|
|
149
|
+
// gone from `newDoc`, so they all resolve to the same anchor and separate widgets would stack.
|
|
150
|
+
//
|
|
151
|
+
// No attribution: the glyph is not a tag host, matching `canTagWidget` below.
|
|
152
|
+
return [createDeletedLineBreakDecoration({
|
|
153
|
+
colorScheme,
|
|
154
|
+
count: deletedBlankLineCount,
|
|
155
|
+
diffType,
|
|
156
|
+
isActive,
|
|
157
|
+
pos: safeInsertPos,
|
|
158
|
+
reveal,
|
|
159
|
+
side: placeBelow ? 1 : -1
|
|
160
|
+
})];
|
|
161
|
+
}
|
|
162
|
+
if (shouldSkipDeletedEmptyParagraphDecoration) {
|
|
135
163
|
return [];
|
|
136
164
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
165
|
+
if (slice.content.content.length === 0) {
|
|
166
|
+
return [];
|
|
167
|
+
}
|
|
168
|
+
const tableDiffMode = getTableDiffMode({
|
|
169
|
+
slice,
|
|
170
|
+
isInserted
|
|
171
|
+
});
|
|
172
|
+
if (tableDiffMode.kind === 'cells') {
|
|
144
173
|
return createTableCellContentWidgets({
|
|
145
174
|
slice,
|
|
146
175
|
newDoc,
|
|
@@ -151,11 +180,10 @@ export const createNodeChangedDecorationWidget = ({
|
|
|
151
180
|
diffType
|
|
152
181
|
});
|
|
153
182
|
}
|
|
154
|
-
if (
|
|
155
|
-
// Deleted whole cells: nothing to render (see note above).
|
|
183
|
+
if (tableDiffMode.kind === 'none') {
|
|
156
184
|
return [];
|
|
157
185
|
}
|
|
158
|
-
if (
|
|
186
|
+
if (tableDiffMode.kind === 'rows') {
|
|
159
187
|
return createChangedRowDecorationWidgets({
|
|
160
188
|
attributionKey,
|
|
161
189
|
changes: [change],
|
|
@@ -166,6 +194,7 @@ export const createNodeChangedDecorationWidget = ({
|
|
|
166
194
|
isActive,
|
|
167
195
|
isInserted,
|
|
168
196
|
diffType,
|
|
197
|
+
intl,
|
|
169
198
|
// Needed for the row's own indicator anchor; this path returns before the
|
|
170
199
|
// `anchor-name` assignment further down.
|
|
171
200
|
showIndicators,
|
|
@@ -173,6 +202,9 @@ export const createNodeChangedDecorationWidget = ({
|
|
|
173
202
|
tagMountContext
|
|
174
203
|
});
|
|
175
204
|
}
|
|
205
|
+
// `wholeTable` and `generic` both continue into the generic block widget below. Splitting the
|
|
206
|
+
// whole-table case into its own renderer is the next step of this refactor.
|
|
207
|
+
|
|
176
208
|
const serializer = nodeViewSerializer;
|
|
177
209
|
|
|
178
210
|
// For non-table content, use the existing span wrapper approach
|
|
@@ -368,6 +400,40 @@ export const createNodeChangedDecorationWidget = ({
|
|
|
368
400
|
}
|
|
369
401
|
}
|
|
370
402
|
});
|
|
403
|
+
let contributorTagAnchorName;
|
|
404
|
+
if (!isInserted && deletedColumns !== null && deletedColumns !== void 0 && deletedColumns.length) {
|
|
405
|
+
const table = dom.querySelector('table');
|
|
406
|
+
const firstRow = table === null || table === void 0 ? void 0 : table.rows[0];
|
|
407
|
+
if (table && canTagWidget) {
|
|
408
|
+
contributorTagAnchorName = buildAnchorDecorationKey({
|
|
409
|
+
diffId,
|
|
410
|
+
anchorType: AnchorTypeKey.tag
|
|
411
|
+
});
|
|
412
|
+
// The widget host sits before the table's preserved block margin. Anchor the tag to the
|
|
413
|
+
// table itself so its bottom edge meets the table's top edge with no visual gap.
|
|
414
|
+
table.style.setProperty('anchor-name', `--${contributorTagAnchorName}`);
|
|
415
|
+
}
|
|
416
|
+
deletedColumns.forEach(column => {
|
|
417
|
+
var _table$rows;
|
|
418
|
+
Array.from((_table$rows = table === null || table === void 0 ? void 0 : table.rows) !== null && _table$rows !== void 0 ? _table$rows : []).forEach(row => {
|
|
419
|
+
var _cell$children;
|
|
420
|
+
const cell = row.cells[column];
|
|
421
|
+
Array.from((_cell$children = cell === null || cell === void 0 ? void 0 : cell.children) !== null && _cell$children !== void 0 ? _cell$children : []).forEach(child => {
|
|
422
|
+
if (child instanceof HTMLElement) {
|
|
423
|
+
child.style.setProperty('text-decoration-line', 'line-through');
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
});
|
|
427
|
+
const headerCell = firstRow === null || firstRow === void 0 ? void 0 : firstRow.cells[column];
|
|
428
|
+
if (headerCell) {
|
|
429
|
+
headerCell.style.position = 'relative';
|
|
430
|
+
// Append, never prepend. Editor CSS resets the top margin of a cell's first child, so
|
|
431
|
+
// a prepended label pushes the paragraph out of that reset and the cell grows.
|
|
432
|
+
// The label is absolutely positioned, so DOM order does not move it.
|
|
433
|
+
headerCell.append(createRemovedLozenge(intl, isActive, colorScheme, true));
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
}
|
|
371
437
|
dom.setAttribute('data-testid', 'show-diff-deleted-decoration');
|
|
372
438
|
if (fg('platform_editor_ai_show_diff_patch_1')) {
|
|
373
439
|
dom.style.setProperty('scroll-margin-top', scrollMarginTopValue);
|
|
@@ -405,7 +471,11 @@ export const createNodeChangedDecorationWidget = ({
|
|
|
405
471
|
|
|
406
472
|
// Hosted at the start of the deleted run rather than on `dom` itself: `dom` is inline, so once
|
|
407
473
|
// the deleted content wraps its box is the union of its line fragments.
|
|
408
|
-
const tagHost = createContributorTagHost(
|
|
474
|
+
const tagHost = createContributorTagHost({
|
|
475
|
+
anchorName: contributorTagAnchorName,
|
|
476
|
+
diffId,
|
|
477
|
+
mountContext: tagMountContext
|
|
478
|
+
});
|
|
409
479
|
if (tagHost) {
|
|
410
480
|
dom.prepend(tagHost.host);
|
|
411
481
|
tagMount = tagHost.mount;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { Decoration } from '@atlaskit/editor-prosemirror/view';
|
|
2
|
+
import { fg } from '@atlaskit/platform-feature-flags/fg';
|
|
3
|
+
import { isExtendedEnabled } from '../../isExtendedEnabled';
|
|
4
|
+
import { buildDiffDecorationSpec, scrollMarginTopValue } from '../decorationKeys';
|
|
5
|
+
import { createContentWrapper } from './wrapBlockNodeView';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* U+2936 ARROW POINTING DOWNWARDS THEN CURVING LEFTWARDS — the return-key glyph.
|
|
9
|
+
*
|
|
10
|
+
* Exported so tests assert against the glyph actually rendered rather than a copy of it.
|
|
11
|
+
*/
|
|
12
|
+
export const RETURN_GLYPH = '⤶';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The DOM for a removed blank line: the return glyph, struck through in the deleted colours.
|
|
16
|
+
*
|
|
17
|
+
* An empty paragraph or heading is a line the author deliberately added, but it has no content for
|
|
18
|
+
* the deleted-content widget to serialize, so a deletion of one used to render nothing at all — the
|
|
19
|
+
* reviewer saw the surrounding text close up with no sign of what was removed. Standing the glyph
|
|
20
|
+
* in for the absent content keeps the deletion visible without inventing content that was not there.
|
|
21
|
+
*
|
|
22
|
+
* `createContentWrapper` supplies the highlight and the strikethrough line, so the glyph is styled
|
|
23
|
+
* exactly like any other run of deleted inline content, including the active and reveal states.
|
|
24
|
+
*
|
|
25
|
+
* Hidden from assistive technology: the glyph is a picture of a key, and read out it is announced as
|
|
26
|
+
* its Unicode name rather than as a removed line.
|
|
27
|
+
*/
|
|
28
|
+
export const createDeletedLineBreakWidget = ({
|
|
29
|
+
colorScheme,
|
|
30
|
+
count = 1,
|
|
31
|
+
diffType,
|
|
32
|
+
isActive = false,
|
|
33
|
+
reveal
|
|
34
|
+
}) => {
|
|
35
|
+
const dom = document.createElement('span');
|
|
36
|
+
// The testid deleted content is matched by, so the glyph is found wherever deleted content is.
|
|
37
|
+
dom.setAttribute('data-testid', 'show-diff-deleted-decoration');
|
|
38
|
+
dom.setAttribute('aria-hidden', 'true');
|
|
39
|
+
dom.contentEditable = 'false';
|
|
40
|
+
// Scroll navigation aligns on the widget itself, as it does for every other deleted widget.
|
|
41
|
+
dom.style.setProperty('scroll-margin-top', scrollMarginTopValue);
|
|
42
|
+
for (let index = 0; index < Math.max(count, 1); index++) {
|
|
43
|
+
if (index > 0) {
|
|
44
|
+
// Each removed line gets its own line in the widget, so a run of blank lines reads as the
|
|
45
|
+
// number of lines it was. `br` rather than a block wrapper: the widget is inline, and the
|
|
46
|
+
// deleted-content wrapper is styled as an inline run.
|
|
47
|
+
dom.append(dom.ownerDocument.createElement('br'));
|
|
48
|
+
}
|
|
49
|
+
const wrapper = createContentWrapper(colorScheme, isActive, false, diffType, reveal);
|
|
50
|
+
wrapper.append(dom.ownerDocument.createTextNode(RETURN_GLYPH));
|
|
51
|
+
dom.append(wrapper);
|
|
52
|
+
}
|
|
53
|
+
return dom;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The glyph as a widget decoration anchored at `pos`.
|
|
58
|
+
*
|
|
59
|
+
* Shared by the two shapes a blank-line removal arrives in. On a forward diff the block is gone from
|
|
60
|
+
* the new document, so the change lands on the deleted side and the glyph is anchored where the
|
|
61
|
+
* block used to be. On an inverted diff — what AI suggested edits renders — the block is still in
|
|
62
|
+
* the displayed document and the change lands on the *inserted* side, so the glyph is anchored
|
|
63
|
+
* inside the surviving block, on the blank line itself.
|
|
64
|
+
*/
|
|
65
|
+
export const createDeletedLineBreakDecoration = ({
|
|
66
|
+
attributionKey,
|
|
67
|
+
colorScheme,
|
|
68
|
+
count,
|
|
69
|
+
diffType,
|
|
70
|
+
isActive = false,
|
|
71
|
+
pos,
|
|
72
|
+
reveal,
|
|
73
|
+
side
|
|
74
|
+
}) => Decoration.widget(pos, createDeletedLineBreakWidget({
|
|
75
|
+
colorScheme,
|
|
76
|
+
count,
|
|
77
|
+
diffType,
|
|
78
|
+
isActive,
|
|
79
|
+
reveal
|
|
80
|
+
}), {
|
|
81
|
+
...buildDiffDecorationSpec({
|
|
82
|
+
attributionKey,
|
|
83
|
+
colorScheme,
|
|
84
|
+
decorationType: 'widget',
|
|
85
|
+
diffId: crypto.randomUUID(),
|
|
86
|
+
isActive,
|
|
87
|
+
// The glyph always stands in for removed content, whichever side of the changeset the
|
|
88
|
+
// change itself landed on.
|
|
89
|
+
isInserted: false,
|
|
90
|
+
diffType,
|
|
91
|
+
...(isExtendedEnabled(diffType) && side !== undefined && {
|
|
92
|
+
side
|
|
93
|
+
})
|
|
94
|
+
}),
|
|
95
|
+
// Without an explicit mark set, prosemirror-view wraps the widget in the marks of the
|
|
96
|
+
// adjacent text. The glyph carries its own deleted styling and must not inherit them.
|
|
97
|
+
...(fg('platform_editor_diff_inline_mark_changes') && {
|
|
98
|
+
marks: []
|
|
99
|
+
})
|
|
100
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const CELL_TYPE_NAMES = ['tableCell', 'tableHeader'];
|
|
2
|
+
export const getTableDiffMode = ({
|
|
3
|
+
slice,
|
|
4
|
+
isInserted
|
|
5
|
+
}) => {
|
|
6
|
+
const topLevelNodes = slice.content.content;
|
|
7
|
+
if (topLevelNodes.some(node => CELL_TYPE_NAMES.includes(node.type.name))) {
|
|
8
|
+
return isInserted ? {
|
|
9
|
+
kind: 'cells'
|
|
10
|
+
} : {
|
|
11
|
+
kind: 'none'
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
if (topLevelNodes.some(node => node.type.name === 'tableRow')) {
|
|
15
|
+
return {
|
|
16
|
+
kind: 'rows'
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
if (topLevelNodes.length === 1 && topLevelNodes[0].type.name === 'table' && slice.openStart === 0 && slice.openEnd === 0) {
|
|
20
|
+
return {
|
|
21
|
+
kind: 'wholeTable'
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
kind: 'generic'
|
|
26
|
+
};
|
|
27
|
+
};
|
|
@@ -139,24 +139,34 @@ const applyCellOverlayStyles = ({
|
|
|
139
139
|
/**
|
|
140
140
|
* Creates a "Removed" lozenge to be displayed at the top right corner of deleted block nodes
|
|
141
141
|
*/
|
|
142
|
-
const createRemovedLozenge = (intl, isActive = false, colorScheme) => {
|
|
142
|
+
export const createRemovedLozenge = (intl, isActive = false, colorScheme, inCell = false) => {
|
|
143
143
|
const container = document.createElement('span');
|
|
144
144
|
const containerStyle = convertToInlineCss({
|
|
145
145
|
position: 'absolute',
|
|
146
146
|
top: "var(--ds-space-075, 6px)",
|
|
147
147
|
right: "var(--ds-space-075, 6px)",
|
|
148
|
-
|
|
148
|
+
// Rounded table-cell overlays use z-index 2 and are appended after the cell content.
|
|
149
|
+
// Keep the lozenge above them so the overlay cannot tint or obscure it.
|
|
150
|
+
zIndex: 3,
|
|
149
151
|
pointerEvents: 'none',
|
|
150
152
|
display: 'flex'
|
|
151
153
|
});
|
|
152
|
-
container.setAttribute('style', containerStyle);
|
|
153
|
-
container.setAttribute('data-testid', 'show-diff-removed-lozenge');
|
|
154
154
|
|
|
155
155
|
// Create vanilla HTML lozenge element with Atlaskit Lozenge styling (visual refresh)
|
|
156
156
|
const lozengeElement = document.createElement('span');
|
|
157
|
-
|
|
157
|
+
|
|
158
|
+
// A column label sits over an already-grey deleted cell, so use the red Removed treatment.
|
|
159
|
+
const lozengeInnerStyle = resolveRemovedLozengeStyle(colorScheme, inCell || isActive);
|
|
158
160
|
lozengeElement.setAttribute('style', lozengeInnerStyle);
|
|
159
161
|
lozengeElement.textContent = intl.formatMessage(trackChangesMessages.removed).toUpperCase();
|
|
162
|
+
if (inCell) {
|
|
163
|
+
lozengeElement.setAttribute('style', `${lozengeInnerStyle};${containerStyle}`);
|
|
164
|
+
lozengeElement.setAttribute('data-testid', 'show-diff-removed-lozenge');
|
|
165
|
+
lozengeElement.contentEditable = 'false';
|
|
166
|
+
return lozengeElement;
|
|
167
|
+
}
|
|
168
|
+
container.setAttribute('style', containerStyle);
|
|
169
|
+
container.setAttribute('data-testid', 'show-diff-removed-lozenge');
|
|
160
170
|
container.appendChild(lozengeElement);
|
|
161
171
|
return container;
|
|
162
172
|
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The textblock types whose empty form is a blank line the author put there deliberately, and so
|
|
3
|
+
* worth marking when a change removes it.
|
|
4
|
+
*
|
|
5
|
+
* Deliberately not every textblock: an empty `codeBlock` is an empty code block, which renders as a
|
|
6
|
+
* visible box of its own and needs no stand-in.
|
|
7
|
+
*/
|
|
8
|
+
const BLANK_LINE_NODE_NAMES = new Set(['paragraph', 'heading']);
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Structurally empty: no children at all, not merely no measurable content. The two coincide for a
|
|
12
|
+
* real node, and testing both keeps a node whose children report no size from passing as blank.
|
|
13
|
+
*/
|
|
14
|
+
const isEmptyTextBlock = node => BLANK_LINE_NODE_NAMES.has(node.type.name) && node.content.childCount === 0 && node.content.size === 0;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* How many empty textblocks a slice holds, or 0 unless that is *all* it holds.
|
|
18
|
+
*
|
|
19
|
+
* Counted rather than tested for one, because a run of adjacent blank lines is reported as a single
|
|
20
|
+
* change covering all of them — the case that made a single-block test miss.
|
|
21
|
+
*/
|
|
22
|
+
export const countEmptyTextBlockOnlySlice = slice => {
|
|
23
|
+
var _slice$content$childC;
|
|
24
|
+
const childCount = (_slice$content$childC = slice === null || slice === void 0 ? void 0 : slice.content.childCount) !== null && _slice$content$childC !== void 0 ? _slice$content$childC : 0;
|
|
25
|
+
if (!slice || childCount === 0) {
|
|
26
|
+
return 0;
|
|
27
|
+
}
|
|
28
|
+
for (let index = 0; index < childCount; index++) {
|
|
29
|
+
if (!isEmptyTextBlock(slice.content.child(index))) {
|
|
30
|
+
return 0;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return childCount;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The content position of every empty textblock lying wholly within `[from, to]`.
|
|
38
|
+
*
|
|
39
|
+
* Used to place a marker on each blank line a change removes. Partially covered blocks are skipped:
|
|
40
|
+
* the change only took part of one, so it still has text of its own, which the inline decoration
|
|
41
|
+
* already marks.
|
|
42
|
+
*/
|
|
43
|
+
export const emptyTextBlockContentPositions = (doc, from, to) => {
|
|
44
|
+
const positions = [];
|
|
45
|
+
doc.nodesBetween(from, to, (node, pos) => {
|
|
46
|
+
if (node.isTextblock) {
|
|
47
|
+
if (isEmptyTextBlock(node) && pos >= from && pos + node.nodeSize <= to) {
|
|
48
|
+
// +1 steps over the block's own open token, into its (empty) content.
|
|
49
|
+
positions.push(pos + 1);
|
|
50
|
+
}
|
|
51
|
+
// A textblock holds inline content only, so there is nothing below it to visit.
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
// Keep descending: blank lines nest inside cells, columns and panels.
|
|
55
|
+
return true;
|
|
56
|
+
});
|
|
57
|
+
return positions;
|
|
58
|
+
};
|