@atlaskit/editor-plugin-show-diff 10.1.10 → 10.1.11
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 +9 -0
- package/dist/cjs/pm-plugins/calculateDiff/calculateDiffDecorations.js +118 -31
- package/dist/cjs/pm-plugins/calculateDiff/diffBySteps.js +124 -1
- package/dist/cjs/pm-plugins/decorations/createGranularBlockReferenceWidget.js +125 -0
- package/dist/cjs/pm-plugins/decorations/createNodeChangedDecorationWidget.js +17 -103
- package/dist/cjs/pm-plugins/decorations/utils/wrapBlockNodeView.js +79 -1
- package/dist/es2019/pm-plugins/calculateDiff/calculateDiffDecorations.js +91 -7
- package/dist/es2019/pm-plugins/calculateDiff/diffBySteps.js +104 -0
- package/dist/es2019/pm-plugins/decorations/createGranularBlockReferenceWidget.js +120 -0
- package/dist/es2019/pm-plugins/decorations/createNodeChangedDecorationWidget.js +1 -81
- package/dist/es2019/pm-plugins/decorations/utils/wrapBlockNodeView.js +74 -2
- package/dist/esm/pm-plugins/calculateDiff/calculateDiffDecorations.js +119 -32
- package/dist/esm/pm-plugins/calculateDiff/diffBySteps.js +123 -0
- package/dist/esm/pm-plugins/decorations/createGranularBlockReferenceWidget.js +120 -0
- package/dist/esm/pm-plugins/decorations/createNodeChangedDecorationWidget.js +13 -98
- package/dist/esm/pm-plugins/decorations/utils/wrapBlockNodeView.js +79 -2
- package/dist/types/pm-plugins/calculateDiff/diffBySteps.d.ts +20 -0
- package/dist/types/pm-plugins/decorations/createGranularBlockReferenceWidget.d.ts +36 -0
- package/dist/types/pm-plugins/decorations/utils/wrapBlockNodeView.d.ts +15 -0
- package/package.json +2 -2
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
+
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
|
|
3
4
|
Object.defineProperty(exports, "__esModule", {
|
|
4
5
|
value: true
|
|
5
6
|
});
|
|
6
|
-
exports.wrapBlockNodeView = void 0;
|
|
7
|
+
exports.wrapBlockNodeView = exports.injectInnerWrapper = exports.createContentWrapper = void 0;
|
|
8
|
+
var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray"));
|
|
7
9
|
var _lazyNodeView = require("@atlaskit/editor-common/lazy-node-view");
|
|
8
10
|
var _messages = require("@atlaskit/editor-common/messages");
|
|
9
11
|
var _nodeTypeUtils = require("@atlaskit/editor-common/utils/node-type-utils");
|
|
@@ -551,4 +553,80 @@ var wrapBlockNodeView = exports.wrapBlockNodeView = function wrapBlockNodeView(_
|
|
|
551
553
|
});
|
|
552
554
|
}
|
|
553
555
|
}
|
|
556
|
+
};
|
|
557
|
+
var getDeletedContentStyleUnbounded = function getDeletedContentStyleUnbounded(colorScheme) {
|
|
558
|
+
var isActive = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
|
|
559
|
+
if (colorScheme === 'traditional' && isActive) {
|
|
560
|
+
return _traditional.deletedTraditionalContentStyleUnboundedActive;
|
|
561
|
+
}
|
|
562
|
+
return colorScheme === 'traditional' ? _traditional.deletedTraditionalContentStyleUnbounded : _standard.deletedContentStyleUnbounded;
|
|
563
|
+
};
|
|
564
|
+
var getInsertedContentStyle = function getInsertedContentStyle(colorScheme) {
|
|
565
|
+
var isActive = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
|
|
566
|
+
if (colorScheme === 'traditional') {
|
|
567
|
+
return isActive ? _traditional.traditionalInsertStyleActive : _traditional.traditionalInsertStyle;
|
|
568
|
+
}
|
|
569
|
+
if (isActive) {
|
|
570
|
+
return _standard.editingStyleActiveExtended;
|
|
571
|
+
}
|
|
572
|
+
return _standard.editingStyleExtended;
|
|
573
|
+
};
|
|
574
|
+
var getDeletedContentStyle = function getDeletedContentStyle(colorScheme) {
|
|
575
|
+
var isActive = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
|
|
576
|
+
if (colorScheme === 'traditional') {
|
|
577
|
+
return (0, _traditional.getDeletedTraditionalInlineStyle)(isActive);
|
|
578
|
+
}
|
|
579
|
+
if ((0, _expValEquals.expValEquals)('platform_editor_diff_plugin_extended', 'isEnabled', true)) {
|
|
580
|
+
return (isActive ? _standard.deletedContentStyleActive : _standard.deletedContentStyleNew) + _standard.deletedInlineContentStyleExtended;
|
|
581
|
+
}
|
|
582
|
+
return isActive ? _standard.deletedContentStyleActive : _standard.deletedContentStyleNew;
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Injects a styled inner wrapper span around the children of a block node element.
|
|
587
|
+
* CSS backgrounds don't work when applied to a wrapper around a paragraph, so
|
|
588
|
+
* the wrapper needs to be injected inside the node around the child content.
|
|
589
|
+
*/
|
|
590
|
+
var injectInnerWrapper = exports.injectInnerWrapper = function injectInnerWrapper(_ref1) {
|
|
591
|
+
var node = _ref1.node,
|
|
592
|
+
colorScheme = _ref1.colorScheme,
|
|
593
|
+
isActive = _ref1.isActive,
|
|
594
|
+
isInserted = _ref1.isInserted;
|
|
595
|
+
var wrapper = document.createElement('span');
|
|
596
|
+
wrapper.setAttribute('style', isInserted ? getInsertedContentStyle(colorScheme, isActive) : getDeletedContentStyle(colorScheme, isActive));
|
|
597
|
+
(0, _toConsumableArray2.default)(node.childNodes).forEach(function (child) {
|
|
598
|
+
var removedChild = node.removeChild(child);
|
|
599
|
+
wrapper.append(removedChild);
|
|
600
|
+
});
|
|
601
|
+
node.appendChild(wrapper);
|
|
602
|
+
return node;
|
|
603
|
+
};
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Creates a styled span wrapper for inline content within a change decoration.
|
|
607
|
+
*/
|
|
608
|
+
var createContentWrapper = exports.createContentWrapper = function createContentWrapper(colorScheme) {
|
|
609
|
+
var isActive = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
|
|
610
|
+
var isInserted = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
|
|
611
|
+
var wrapper = document.createElement('span');
|
|
612
|
+
var baseStyle = (0, _lazyNodeView.convertToInlineCss)({
|
|
613
|
+
position: 'relative',
|
|
614
|
+
width: 'fit-content'
|
|
615
|
+
});
|
|
616
|
+
if ((0, _expValEquals.expValEquals)('platform_editor_diff_plugin_extended', 'isEnabled', true)) {
|
|
617
|
+
if (isInserted) {
|
|
618
|
+
wrapper.setAttribute('style', "".concat(baseStyle).concat(getInsertedContentStyle(colorScheme, isActive)));
|
|
619
|
+
} else {
|
|
620
|
+
wrapper.setAttribute('style', "".concat(baseStyle).concat(getDeletedContentStyle(colorScheme, isActive)));
|
|
621
|
+
var strikethrough = document.createElement('span');
|
|
622
|
+
strikethrough.setAttribute('style', getDeletedContentStyleUnbounded(colorScheme, isActive));
|
|
623
|
+
wrapper.append(strikethrough);
|
|
624
|
+
}
|
|
625
|
+
} else {
|
|
626
|
+
wrapper.setAttribute('style', "".concat(baseStyle).concat(getDeletedContentStyle(colorScheme, isActive)));
|
|
627
|
+
var _strikethrough = document.createElement('span');
|
|
628
|
+
_strikethrough.setAttribute('style', getDeletedContentStyleUnbounded(colorScheme, isActive));
|
|
629
|
+
wrapper.append(_strikethrough);
|
|
630
|
+
}
|
|
631
|
+
return wrapper;
|
|
554
632
|
};
|
|
@@ -8,12 +8,13 @@ import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
|
|
|
8
8
|
import { areDocsEqualByBlockStructureAndText } from '../areDocsEqualByBlockStructureAndText';
|
|
9
9
|
import { createDocMarginAnchorWidget } from '../decorations/createAnchorDecorationWidgets';
|
|
10
10
|
import { createBlockChangedDecoration } from '../decorations/createBlockChangedDecoration';
|
|
11
|
+
import { createGranularBlockReferenceWidget } from '../decorations/createGranularBlockReferenceWidget';
|
|
11
12
|
import { createInlineChangedDecoration } from '../decorations/createInlineChangedDecoration';
|
|
12
13
|
import { createNodeChangedDecorationWidget } from '../decorations/createNodeChangedDecorationWidget';
|
|
13
14
|
import { extractDiffDescriptors } from '../decorations/decorationKeys';
|
|
14
15
|
import { getAttrChangeRanges, stepIsValidAttrChange } from '../decorations/utils/getAttrChangeRanges';
|
|
15
16
|
import { getMarkChangeRanges } from '../decorations/utils/getMarkChangeRanges';
|
|
16
|
-
import { diffBySteps } from './diffBySteps';
|
|
17
|
+
import { diffBySteps, getStepChanges } from './diffBySteps';
|
|
17
18
|
import { groupChangesByBlock } from './groupChangesByBlock';
|
|
18
19
|
import { optimizeChanges } from './optimizeChanges';
|
|
19
20
|
import { simplifySteps } from './simplifySteps';
|
|
@@ -149,12 +150,16 @@ const calculateDiffDecorationsInner = ({
|
|
|
149
150
|
if (showIndicators && expValEquals('platform_editor_diff_plugin_extended', 'isEnabled', true)) {
|
|
150
151
|
decorations.push(createDocMarginAnchorWidget());
|
|
151
152
|
}
|
|
152
|
-
|
|
153
|
+
|
|
154
|
+
// Our default operations are insertions, so it should match the opposite of isInverted.
|
|
155
|
+
const isInserted = !isInverted;
|
|
156
|
+
const createDecorationsForChange = (change, showGranularWithBlock) => {
|
|
153
157
|
const isActive = activeIndexPos && change.fromB === activeIndexPos.from && change.toB === activeIndexPos.to;
|
|
154
|
-
// Our default operations are insertions, so it should match the opposite of isInverted.
|
|
155
|
-
const isInserted = !isInverted;
|
|
156
158
|
if (change.inserted.length > 0) {
|
|
157
|
-
|
|
159
|
+
// shouldHideDeleted for block/node decorations: suppressed when isInverted + hideDeletedDiffs,
|
|
160
|
+
// or when showGranularWithBlock (block reference widget is shown instead).
|
|
161
|
+
// isInverted gates both — on an inverted diff the inserted side is visually the deleted side.
|
|
162
|
+
const shouldHideDeleted = expValEquals('platform_editor_diff_plugin_extended', 'isEnabled', true) ? isInverted && (hideDeletedDiffs || showGranularWithBlock) && change.deleted.length > 0 : false;
|
|
158
163
|
decorations.push(...createInlineChangedDecoration({
|
|
159
164
|
change,
|
|
160
165
|
doc: tr.doc,
|
|
@@ -180,7 +185,7 @@ const calculateDiffDecorationsInner = ({
|
|
|
180
185
|
}));
|
|
181
186
|
}
|
|
182
187
|
if (change.deleted.length > 0) {
|
|
183
|
-
const shouldHideDeleted = expValEquals('platform_editor_diff_plugin_extended', 'isEnabled', true) ? !isInverted && hideDeletedDiffs : false;
|
|
188
|
+
const shouldHideDeleted = expValEquals('platform_editor_diff_plugin_extended', 'isEnabled', true) ? !isInverted && (hideDeletedDiffs || showGranularWithBlock) && change.inserted.length > 0 : false;
|
|
184
189
|
if (!shouldHideDeleted) {
|
|
185
190
|
decorations.push(...createNodeChangedDecorationWidget({
|
|
186
191
|
change,
|
|
@@ -198,7 +203,86 @@ const calculateDiffDecorationsInner = ({
|
|
|
198
203
|
}));
|
|
199
204
|
}
|
|
200
205
|
}
|
|
201
|
-
}
|
|
206
|
+
};
|
|
207
|
+
if (diffType === 'step' && expValEquals('platform_editor_diff_granular_extended', 'isEnabled', true)) {
|
|
208
|
+
// Uses getStepChanges instead of getChanges so that we have per-step granularity metadata.
|
|
209
|
+
// Specifically, we need to know how many granular changes each step produced in order to
|
|
210
|
+
// apply the shouldHideDeleted suppression threshold (> 3 granular changes per step).
|
|
211
|
+
// getChanges returns a flat Change[] with no per-step grouping, making this count impossible
|
|
212
|
+
// to derive after the fact without re-introducing per-change metadata.
|
|
213
|
+
const stepChanges = getStepChanges(originalDoc, steps);
|
|
214
|
+
stepChanges.forEach(({
|
|
215
|
+
isGranular,
|
|
216
|
+
changes: stepChangeList
|
|
217
|
+
}) => {
|
|
218
|
+
const granularCount = isGranular ? stepChangeList.length : 0;
|
|
219
|
+
|
|
220
|
+
// Calculate the average ratio of changed content on both A (original) and B (new)
|
|
221
|
+
// sides of the diff. If 30% or more of the block has changed on average, we show
|
|
222
|
+
// the block reference widget even if the granular change count is below the threshold.
|
|
223
|
+
// Block length is derived from the enclosing text block boundaries rather than the
|
|
224
|
+
// first/last change positions, so unchanged words at the start/end are accounted for.
|
|
225
|
+
let avgChangedRatio = 0;
|
|
226
|
+
if (isGranular && stepChangeList.length > 0) {
|
|
227
|
+
const first = stepChangeList[0];
|
|
228
|
+
const last = stepChangeList[stepChangeList.length - 1];
|
|
229
|
+
const resolvedA = originalDoc.resolve(first.fromA);
|
|
230
|
+
const resolvedB = tr.doc.resolve(first.fromB);
|
|
231
|
+
let blockStartA = first.fromA;
|
|
232
|
+
let blockEndA = last.toA;
|
|
233
|
+
let blockStartB = first.fromB;
|
|
234
|
+
let blockEndB = last.toB;
|
|
235
|
+
for (let depth = resolvedA.depth; depth >= 0; depth--) {
|
|
236
|
+
const node = resolvedA.node(depth);
|
|
237
|
+
if (node.isTextblock) {
|
|
238
|
+
blockStartA = resolvedA.start(depth);
|
|
239
|
+
blockEndA = blockStartA + node.nodeSize - 2; // exclude open/close tokens
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
for (let depth = resolvedB.depth; depth >= 0; depth--) {
|
|
244
|
+
const node = resolvedB.node(depth);
|
|
245
|
+
if (node.isTextblock) {
|
|
246
|
+
blockStartB = resolvedB.start(depth);
|
|
247
|
+
blockEndB = blockStartB + node.nodeSize - 2; // exclude open/close tokens
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
const blockLengthA = blockEndA - blockStartA;
|
|
252
|
+
const blockLengthB = blockEndB - blockStartB;
|
|
253
|
+
const totalChangedA = stepChangeList.reduce((sum, c) => sum + (c.toA - c.fromA), 0);
|
|
254
|
+
const totalChangedB = stepChangeList.reduce((sum, c) => sum + (c.toB - c.fromB), 0);
|
|
255
|
+
const ratioA = blockLengthA > 0 ? totalChangedA / blockLengthA : 0;
|
|
256
|
+
const ratioB = blockLengthB > 0 ? totalChangedB / blockLengthB : 0;
|
|
257
|
+
avgChangedRatio = (ratioA + ratioB) / 2;
|
|
258
|
+
}
|
|
259
|
+
const showGranularWithBlock = isGranular && granularCount !== 1 && (granularCount > 3 || avgChangedRatio >= 0.3);
|
|
260
|
+
stepChangeList.forEach(change => {
|
|
261
|
+
createDecorationsForChange(change, showGranularWithBlock);
|
|
262
|
+
});
|
|
263
|
+
if (showGranularWithBlock && stepChangeList.length > 0) {
|
|
264
|
+
const lastChange = stepChangeList[stepChangeList.length - 1];
|
|
265
|
+
const granularBlockDiffId = crypto.randomUUID();
|
|
266
|
+
const blockWidgets = createGranularBlockReferenceWidget({
|
|
267
|
+
change: lastChange,
|
|
268
|
+
originalDoc,
|
|
269
|
+
newDoc: tr.doc,
|
|
270
|
+
isInverted,
|
|
271
|
+
nodeViewSerializer,
|
|
272
|
+
colorScheme,
|
|
273
|
+
intl,
|
|
274
|
+
activeIndexPos,
|
|
275
|
+
diffId: granularBlockDiffId,
|
|
276
|
+
showIndicators
|
|
277
|
+
});
|
|
278
|
+
decorations.push(...blockWidgets);
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
} else {
|
|
282
|
+
changes.forEach(change => {
|
|
283
|
+
createDecorationsForChange(change, /* showGranularWithBlock */false);
|
|
284
|
+
});
|
|
285
|
+
}
|
|
202
286
|
getMarkChangeRanges(steps).forEach(change => {
|
|
203
287
|
const isActive = activeIndexPos && change.fromB === activeIndexPos.from && change.toB === activeIndexPos.to;
|
|
204
288
|
decorations.push(...createInlineChangedDecoration({
|
|
@@ -362,4 +362,108 @@ export const diffBySteps = (originalDoc, steps) => {
|
|
|
362
362
|
});
|
|
363
363
|
}
|
|
364
364
|
return mergeOverlappingByNewDocRange(changes);
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* A fork of `diffBySteps` that returns changes grouped per step, rather than as a flat list.
|
|
369
|
+
*
|
|
370
|
+
* Why forked rather than refactoring `diffBySteps`:
|
|
371
|
+
* - `diffBySteps` returns a flat `Change[]` and is consumed by the existing decoration path.
|
|
372
|
+
* Changing its return shape would require threading per-step metadata through all callers,
|
|
373
|
+
* adding complexity to a stable code path.
|
|
374
|
+
* - The per-step grouping is only needed for the `platform_editor_diff_granular_extended` gate,
|
|
375
|
+
* where we need to know how many granular changes a single step produced in order to decide
|
|
376
|
+
* whether to suppress deleted decorations (threshold: > 3 granular changes per step).
|
|
377
|
+
* - Keeping the two functions separate means each has a clear, focused contract and neither
|
|
378
|
+
* accumulates the other's concerns. Shared logic (mapping helpers, `mergeOverlappingByNewDocRange`,
|
|
379
|
+
* `shouldCheckGranularDiff`, etc.) is already extracted and reused by both.
|
|
380
|
+
*/
|
|
381
|
+
export const getStepChanges = (originalDoc, steps) => {
|
|
382
|
+
const result = [];
|
|
383
|
+
let currentDoc = originalDoc;
|
|
384
|
+
const successfulStepMaps = [];
|
|
385
|
+
const rangedSteps = [];
|
|
386
|
+
for (const step of steps) {
|
|
387
|
+
const before = currentDoc;
|
|
388
|
+
const stepResult = step.apply(currentDoc);
|
|
389
|
+
if (stepResult.failed !== null || !stepResult.doc) {
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
const stepMap = step.getMap();
|
|
393
|
+
const rangeStep = step;
|
|
394
|
+
if (typeof rangeStep.from === 'number' && typeof rangeStep.to === 'number') {
|
|
395
|
+
rangedSteps.push({
|
|
396
|
+
before,
|
|
397
|
+
doc: stepResult.doc,
|
|
398
|
+
from: rangeStep.from,
|
|
399
|
+
to: rangeStep.to,
|
|
400
|
+
mapIndex: successfulStepMaps.length,
|
|
401
|
+
step,
|
|
402
|
+
stepMap
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
successfulStepMaps.push(stepMap);
|
|
406
|
+
currentDoc = stepResult.doc;
|
|
407
|
+
}
|
|
408
|
+
for (const rangedStep of rangedSteps) {
|
|
409
|
+
const originalToBeforeStep = createMapping(successfulStepMaps.slice(0, rangedStep.mapIndex));
|
|
410
|
+
const beforeStepToOriginal = originalToBeforeStep.invert();
|
|
411
|
+
const fromA = mapPosition(beforeStepToOriginal, rangedStep.from);
|
|
412
|
+
const toA = mapPosition(beforeStepToOriginal, rangedStep.to);
|
|
413
|
+
const fromAfterStep = rangedStep.stepMap.map(rangedStep.from, -1);
|
|
414
|
+
const toAfterStep = rangedStep.stepMap.map(rangedStep.to, 1);
|
|
415
|
+
const afterStepToFinal = createMapping(successfulStepMaps.slice(rangedStep.mapIndex + 1));
|
|
416
|
+
const fromB = mapPosition(afterStepToFinal, fromAfterStep);
|
|
417
|
+
const toB = mapPosition(afterStepToFinal, toAfterStep);
|
|
418
|
+
if (shouldCheckGranularDiff(rangedStep.step, rangedStep.before, rangedStep.from, rangedStep.to)) {
|
|
419
|
+
const granularStepChanges = ChangeSet.create(rangedStep.before).addSteps(rangedStep.doc, [rangedStep.stepMap], null);
|
|
420
|
+
const optimizedGranularStepChanges = optimizeChanges(simplifyChanges(granularStepChanges.changes, rangedStep.doc));
|
|
421
|
+
const stepChanges = [];
|
|
422
|
+
for (const granularChange of optimizedGranularStepChanges) {
|
|
423
|
+
const expandedA = expandToWordBoundaries(rangedStep.before, granularChange.fromA, granularChange.toA);
|
|
424
|
+
const expandedB = expandToWordBoundaries(rangedStep.doc, granularChange.fromB, granularChange.toB);
|
|
425
|
+
const aLeftDelta = granularChange.fromA - expandedA.from;
|
|
426
|
+
const aRightDelta = expandedA.to - granularChange.toA;
|
|
427
|
+
const bLeftDelta = granularChange.fromB - expandedB.from;
|
|
428
|
+
const bRightDelta = expandedB.to - granularChange.toB;
|
|
429
|
+
let finalA = expandedA;
|
|
430
|
+
let finalB = expandedB;
|
|
431
|
+
if (aLeftDelta > bLeftDelta || aRightDelta > bRightDelta) {
|
|
432
|
+
const extraLeft = Math.max(0, aLeftDelta - bLeftDelta);
|
|
433
|
+
const extraRight = Math.max(0, aRightDelta - bRightDelta);
|
|
434
|
+
finalB = expandToWordBoundaries(rangedStep.doc, Math.max(expandedB.from - extraLeft, 0), expandedB.to + extraRight);
|
|
435
|
+
}
|
|
436
|
+
if (bLeftDelta > aLeftDelta || bRightDelta > aRightDelta) {
|
|
437
|
+
const extraLeft = Math.max(0, bLeftDelta - aLeftDelta);
|
|
438
|
+
const extraRight = Math.max(0, bRightDelta - aRightDelta);
|
|
439
|
+
finalA = expandToWordBoundaries(rangedStep.before, Math.max(expandedA.from - extraLeft, 0), expandedA.to + extraRight);
|
|
440
|
+
}
|
|
441
|
+
stepChanges.push({
|
|
442
|
+
fromA: mapPosition(beforeStepToOriginal, finalA.from),
|
|
443
|
+
toA: mapPosition(beforeStepToOriginal, finalA.to),
|
|
444
|
+
fromB: mapPosition(afterStepToFinal, finalB.from),
|
|
445
|
+
toB: mapPosition(afterStepToFinal, finalB.to),
|
|
446
|
+
deleted: createSpans(Math.max(0, finalA.to - finalA.from)),
|
|
447
|
+
inserted: createSpans(Math.max(0, finalB.to - finalB.from))
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
result.push({
|
|
451
|
+
isGranular: true,
|
|
452
|
+
changes: mergeOverlappingByNewDocRange(stepChanges)
|
|
453
|
+
});
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
result.push({
|
|
457
|
+
isGranular: false,
|
|
458
|
+
changes: [{
|
|
459
|
+
fromA,
|
|
460
|
+
toA,
|
|
461
|
+
fromB,
|
|
462
|
+
toB,
|
|
463
|
+
deleted: createSpans(Math.max(0, toA - fromA)),
|
|
464
|
+
inserted: createSpans(Math.max(0, toB - fromB))
|
|
465
|
+
}]
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
return result;
|
|
365
469
|
};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { Decoration } from '@atlaskit/editor-prosemirror/view';
|
|
2
|
+
import { createLeftAnchorWidget } from './createAnchorDecorationWidgets';
|
|
3
|
+
import { buildDiffDecorationSpec, buildAnchorDecorationKey } from './decorationKeys';
|
|
4
|
+
import { wrapBlockNodeView, injectInnerWrapper } from './utils/wrapBlockNodeView';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Creates a single block widget that renders a reference text block content
|
|
8
|
+
* beneath a granular diff set, for reference when deleted content is hidden.
|
|
9
|
+
*
|
|
10
|
+
* Since granular diffing only applies to singular isTextBlock=true nodes, this
|
|
11
|
+
* widget always renders exactly one block node and does not need the slice/fragment
|
|
12
|
+
* complexity of createNodeChangedDecorationWidget.
|
|
13
|
+
*
|
|
14
|
+
* Resolves which doc and positions to render based on isInverted:
|
|
15
|
+
* - !isInverted: renders originalDoc at A-side positions (what was there before)
|
|
16
|
+
* - isInverted: renders newDoc at B-side positions (the current/new content)
|
|
17
|
+
*
|
|
18
|
+
* The widget is always inserted at the B-side block boundary in newDoc since
|
|
19
|
+
* ProseMirror decorations are always anchored against the live (new) document.
|
|
20
|
+
*/
|
|
21
|
+
export const createGranularBlockReferenceWidget = ({
|
|
22
|
+
change,
|
|
23
|
+
originalDoc,
|
|
24
|
+
newDoc,
|
|
25
|
+
isInverted,
|
|
26
|
+
nodeViewSerializer,
|
|
27
|
+
colorScheme,
|
|
28
|
+
intl,
|
|
29
|
+
activeIndexPos,
|
|
30
|
+
diffId,
|
|
31
|
+
showIndicators = false
|
|
32
|
+
}) => {
|
|
33
|
+
// Determine which doc and positions to use for rendering the reference block.
|
|
34
|
+
const renderDoc = isInverted ? newDoc : originalDoc;
|
|
35
|
+
const renderTo = isInverted ? change.toB : change.toA;
|
|
36
|
+
|
|
37
|
+
// The insertion point is always in newDoc — ProseMirror decorates against the live document.
|
|
38
|
+
// Walk up from toB to find the enclosing text block boundary for widget placement.
|
|
39
|
+
const insertResolvedPos = newDoc.resolve(change.toB);
|
|
40
|
+
let blockEnd = change.toB;
|
|
41
|
+
for (let depth = insertResolvedPos.depth; depth >= 0; depth--) {
|
|
42
|
+
const node = insertResolvedPos.node(depth);
|
|
43
|
+
if (node.isTextblock) {
|
|
44
|
+
blockEnd = insertResolvedPos.start(depth) + node.nodeSize - 1;
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Find the block node to render from the render doc at the render positions.
|
|
50
|
+
const renderResolvedPos = renderDoc.resolve(renderTo);
|
|
51
|
+
let renderBlockNode = null;
|
|
52
|
+
for (let depth = renderResolvedPos.depth; depth >= 0; depth--) {
|
|
53
|
+
const node = renderResolvedPos.node(depth);
|
|
54
|
+
if (node.isTextblock) {
|
|
55
|
+
renderBlockNode = node;
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (!renderBlockNode) {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
const isActive = activeIndexPos && change.fromB === activeIndexPos.from && change.toB === activeIndexPos.to;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* This will always be a block node as it's a textBlock-like node.
|
|
66
|
+
* We use div instead of span so we can add margins.
|
|
67
|
+
*/
|
|
68
|
+
const dom = document.createElement('div');
|
|
69
|
+
dom.setAttribute('data-testid', 'show-diff-granular-block-reference');
|
|
70
|
+
dom.style.setProperty('margin-top', "var(--ds-space-200, 16px)");
|
|
71
|
+
const nodeView = nodeViewSerializer.tryCreateNodeView(renderBlockNode);
|
|
72
|
+
if (nodeView) {
|
|
73
|
+
wrapBlockNodeView({
|
|
74
|
+
dom,
|
|
75
|
+
nodeView,
|
|
76
|
+
targetNode: renderBlockNode,
|
|
77
|
+
colorScheme,
|
|
78
|
+
intl,
|
|
79
|
+
isActive: !!isActive,
|
|
80
|
+
isInserted: false
|
|
81
|
+
});
|
|
82
|
+
} else {
|
|
83
|
+
const serialized = nodeViewSerializer.serializeNode(renderBlockNode);
|
|
84
|
+
if (serialized && serialized instanceof HTMLElement) {
|
|
85
|
+
injectInnerWrapper({
|
|
86
|
+
node: serialized,
|
|
87
|
+
colorScheme,
|
|
88
|
+
isActive: !!isActive,
|
|
89
|
+
isInserted: false
|
|
90
|
+
});
|
|
91
|
+
dom.append(serialized);
|
|
92
|
+
} else if (serialized) {
|
|
93
|
+
dom.append(serialized);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (dom.childNodes.length === 0) {
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
const decorations = [];
|
|
100
|
+
if (showIndicators) {
|
|
101
|
+
const leftAnchor = createLeftAnchorWidget({
|
|
102
|
+
doc: newDoc,
|
|
103
|
+
from: blockEnd,
|
|
104
|
+
diffId
|
|
105
|
+
});
|
|
106
|
+
dom.style.setProperty('anchor-name', `--${buildAnchorDecorationKey({
|
|
107
|
+
diffId
|
|
108
|
+
})}`);
|
|
109
|
+
if (leftAnchor) {
|
|
110
|
+
decorations.push(leftAnchor);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
decorations.push(Decoration.widget(blockEnd, dom, buildDiffDecorationSpec({
|
|
114
|
+
decorationType: 'widget',
|
|
115
|
+
diffId,
|
|
116
|
+
// We want it to be as close to the granular diff as possible
|
|
117
|
+
side: -999
|
|
118
|
+
})));
|
|
119
|
+
return decorations;
|
|
120
|
+
};
|
|
@@ -1,90 +1,10 @@
|
|
|
1
|
-
import { convertToInlineCss } from '@atlaskit/editor-common/lazy-node-view';
|
|
2
1
|
import { Decoration } from '@atlaskit/editor-prosemirror/view';
|
|
3
2
|
import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
|
|
4
|
-
import { editingStyle, editingStyleExtended, editingStyleActive, editingStyleActiveExtended, deletedContentStyle, deletedContentStyleActive, deletedContentStyleNew, deletedContentStyleUnbounded, deletedInlineContentStyleExtended } from './colorSchemes/standard';
|
|
5
|
-
import { traditionalInsertStyle, traditionalInsertStyleActive, getDeletedTraditionalInlineStyle, deletedTraditionalContentStyleUnbounded, deletedTraditionalContentStyleUnboundedActive } from './colorSchemes/traditional';
|
|
6
3
|
import { createLeftAnchorWidget } from './createAnchorDecorationWidgets';
|
|
7
4
|
import { createChangedRowDecorationWidgets } from './createChangedRowDecorationWidgets';
|
|
8
5
|
import { buildDiffDecorationSpec, buildAnchorDecorationKey } from './decorationKeys';
|
|
9
6
|
import { findSafeInsertPos } from './utils/findSafeInsertPos';
|
|
10
|
-
import { wrapBlockNodeView } from './utils/wrapBlockNodeView';
|
|
11
|
-
const getDeletedContentStyleUnbounded = (colorScheme, isActive = false) => {
|
|
12
|
-
if (colorScheme === 'traditional' && isActive) {
|
|
13
|
-
return deletedTraditionalContentStyleUnboundedActive;
|
|
14
|
-
}
|
|
15
|
-
return colorScheme === 'traditional' ? deletedTraditionalContentStyleUnbounded : deletedContentStyleUnbounded;
|
|
16
|
-
};
|
|
17
|
-
const getInsertedContentStyle = (colorScheme, isActive = false) => {
|
|
18
|
-
if (colorScheme === 'traditional') {
|
|
19
|
-
if (isActive) {
|
|
20
|
-
return traditionalInsertStyleActive;
|
|
21
|
-
}
|
|
22
|
-
return traditionalInsertStyle;
|
|
23
|
-
}
|
|
24
|
-
if (isActive) {
|
|
25
|
-
return expValEquals('platform_editor_diff_plugin_extended', 'isEnabled', true) ? editingStyleActiveExtended : editingStyleActive;
|
|
26
|
-
}
|
|
27
|
-
return expValEquals('platform_editor_diff_plugin_extended', 'isEnabled', true) ? editingStyleExtended : editingStyle;
|
|
28
|
-
};
|
|
29
|
-
const getDeletedContentStyle = (colorScheme, isActive = false) => {
|
|
30
|
-
if (colorScheme === 'traditional') {
|
|
31
|
-
return getDeletedTraditionalInlineStyle(isActive);
|
|
32
|
-
}
|
|
33
|
-
// Merge into existing styles when cleaning up
|
|
34
|
-
if (expValEquals('platform_editor_diff_plugin_extended', 'isEnabled', true)) {
|
|
35
|
-
if (isActive) {
|
|
36
|
-
return deletedContentStyleActive + deletedInlineContentStyleExtended;
|
|
37
|
-
}
|
|
38
|
-
return deletedContentStyleNew + deletedInlineContentStyleExtended;
|
|
39
|
-
}
|
|
40
|
-
if (isActive) {
|
|
41
|
-
return deletedContentStyleActive;
|
|
42
|
-
}
|
|
43
|
-
return expValEquals('platform_editor_enghealth_a11y_jan_fixes', 'isEnabled', true) ? deletedContentStyleNew : deletedContentStyle;
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* CSS backgrounds don't work when applied to a wrapper around a paragraph, so
|
|
48
|
-
* the wrapper needs to be injected inside the node around the child content
|
|
49
|
-
*/
|
|
50
|
-
const injectInnerWrapper = ({
|
|
51
|
-
node,
|
|
52
|
-
colorScheme,
|
|
53
|
-
isActive,
|
|
54
|
-
isInserted
|
|
55
|
-
}) => {
|
|
56
|
-
const wrapper = document.createElement('span');
|
|
57
|
-
wrapper.setAttribute('style', isInserted ? getInsertedContentStyle(colorScheme, isActive) : getDeletedContentStyle(colorScheme, isActive));
|
|
58
|
-
[...node.childNodes].forEach(child => {
|
|
59
|
-
const removedChild = node.removeChild(child);
|
|
60
|
-
wrapper.append(removedChild);
|
|
61
|
-
});
|
|
62
|
-
node.appendChild(wrapper);
|
|
63
|
-
return node;
|
|
64
|
-
};
|
|
65
|
-
const createContentWrapper = (colorScheme, isActive = false, isInserted = false) => {
|
|
66
|
-
const wrapper = document.createElement('span');
|
|
67
|
-
const baseStyle = convertToInlineCss({
|
|
68
|
-
position: 'relative',
|
|
69
|
-
width: 'fit-content'
|
|
70
|
-
});
|
|
71
|
-
if (expValEquals('platform_editor_diff_plugin_extended', 'isEnabled', true)) {
|
|
72
|
-
if (isInserted) {
|
|
73
|
-
wrapper.setAttribute('style', `${baseStyle}${getInsertedContentStyle(colorScheme, isActive)}`);
|
|
74
|
-
} else {
|
|
75
|
-
wrapper.setAttribute('style', `${baseStyle}${getDeletedContentStyle(colorScheme, isActive)}`);
|
|
76
|
-
const strikethrough = document.createElement('span');
|
|
77
|
-
strikethrough.setAttribute('style', getDeletedContentStyleUnbounded(colorScheme, isActive));
|
|
78
|
-
wrapper.append(strikethrough);
|
|
79
|
-
}
|
|
80
|
-
} else {
|
|
81
|
-
wrapper.setAttribute('style', `${baseStyle}${getDeletedContentStyle(colorScheme, isActive)}`);
|
|
82
|
-
const strikethrough = document.createElement('span');
|
|
83
|
-
strikethrough.setAttribute('style', getDeletedContentStyleUnbounded(colorScheme, isActive));
|
|
84
|
-
wrapper.append(strikethrough);
|
|
85
|
-
}
|
|
86
|
-
return wrapper;
|
|
87
|
-
};
|
|
7
|
+
import { wrapBlockNodeView, injectInnerWrapper, createContentWrapper } from './utils/wrapBlockNodeView';
|
|
88
8
|
|
|
89
9
|
/**
|
|
90
10
|
* This function is used to create a decoration widget to show content
|
|
@@ -2,8 +2,8 @@ import { convertToInlineCss } from '@atlaskit/editor-common/lazy-node-view';
|
|
|
2
2
|
import { trackChangesMessages } from '@atlaskit/editor-common/messages';
|
|
3
3
|
import { getBaseNodeTypeName } from '@atlaskit/editor-common/utils/node-type-utils';
|
|
4
4
|
import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
|
|
5
|
-
import { deletedBlockOutline, deletedBlockOutlineActive, deletedBlockOutlineRounded, deletedBlockOutlineRoundedActive, deletedContentStyle, deletedContentStyleActive, deletedContentStyleNew, deletedStyleQuoteNodeWithLozenge, deletedStyleQuoteNodeWithLozengeActive, editingContentStyleInBlockExtended, editingStyleExtended, editingStyleActiveExtended, editingStyleNode, addedCellOverlayStyle, deletedCellOverlayStyle } from '../colorSchemes/standard';
|
|
6
|
-
import { deletedTraditionalBlockOutlineActive, deletedTraditionalBlockOutlineNew, deletedTraditionalBlockOutlineRoundedActive, deletedTraditionalBlockOutlineRoundedNew, getDeletedTraditionalInlineStyle, deletedTraditionalStyleQuoteNode, deletedTraditionalStyleQuoteNodeActive, traditionalInsertStyle, traditionalInsertStyleActive, traditionalStyleNodeActive, traditionalStyleNodeNew, traditionalAddedCellOverlayStyleNew, deletedTraditionalCellOverlayStyle } from '../colorSchemes/traditional';
|
|
5
|
+
import { deletedBlockOutline, deletedBlockOutlineActive, deletedBlockOutlineRounded, deletedBlockOutlineRoundedActive, deletedContentStyle, deletedContentStyleActive, deletedContentStyleNew, deletedContentStyleUnbounded, deletedInlineContentStyleExtended, deletedStyleQuoteNodeWithLozenge, deletedStyleQuoteNodeWithLozengeActive, editingContentStyleInBlockExtended, editingStyleExtended, editingStyleActiveExtended, editingStyleNode, addedCellOverlayStyle, deletedCellOverlayStyle } from '../colorSchemes/standard';
|
|
6
|
+
import { deletedTraditionalBlockOutlineActive, deletedTraditionalBlockOutlineNew, deletedTraditionalBlockOutlineRoundedActive, deletedTraditionalBlockOutlineRoundedNew, deletedTraditionalContentStyleUnbounded, deletedTraditionalContentStyleUnboundedActive, getDeletedTraditionalInlineStyle, deletedTraditionalStyleQuoteNode, deletedTraditionalStyleQuoteNodeActive, traditionalInsertStyle, traditionalInsertStyleActive, traditionalStyleNodeActive, traditionalStyleNodeNew, traditionalAddedCellOverlayStyleNew, deletedTraditionalCellOverlayStyle } from '../colorSchemes/traditional';
|
|
7
7
|
const lozengeStyle = convertToInlineCss({
|
|
8
8
|
display: 'inline-flex',
|
|
9
9
|
boxSizing: 'border-box',
|
|
@@ -542,4 +542,76 @@ export const wrapBlockNodeView = ({
|
|
|
542
542
|
});
|
|
543
543
|
}
|
|
544
544
|
}
|
|
545
|
+
};
|
|
546
|
+
const getDeletedContentStyleUnbounded = (colorScheme, isActive = false) => {
|
|
547
|
+
if (colorScheme === 'traditional' && isActive) {
|
|
548
|
+
return deletedTraditionalContentStyleUnboundedActive;
|
|
549
|
+
}
|
|
550
|
+
return colorScheme === 'traditional' ? deletedTraditionalContentStyleUnbounded : deletedContentStyleUnbounded;
|
|
551
|
+
};
|
|
552
|
+
const getInsertedContentStyle = (colorScheme, isActive = false) => {
|
|
553
|
+
if (colorScheme === 'traditional') {
|
|
554
|
+
return isActive ? traditionalInsertStyleActive : traditionalInsertStyle;
|
|
555
|
+
}
|
|
556
|
+
if (isActive) {
|
|
557
|
+
return editingStyleActiveExtended;
|
|
558
|
+
}
|
|
559
|
+
return editingStyleExtended;
|
|
560
|
+
};
|
|
561
|
+
const getDeletedContentStyle = (colorScheme, isActive = false) => {
|
|
562
|
+
if (colorScheme === 'traditional') {
|
|
563
|
+
return getDeletedTraditionalInlineStyle(isActive);
|
|
564
|
+
}
|
|
565
|
+
if (expValEquals('platform_editor_diff_plugin_extended', 'isEnabled', true)) {
|
|
566
|
+
return (isActive ? deletedContentStyleActive : deletedContentStyleNew) + deletedInlineContentStyleExtended;
|
|
567
|
+
}
|
|
568
|
+
return isActive ? deletedContentStyleActive : deletedContentStyleNew;
|
|
569
|
+
};
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Injects a styled inner wrapper span around the children of a block node element.
|
|
573
|
+
* CSS backgrounds don't work when applied to a wrapper around a paragraph, so
|
|
574
|
+
* the wrapper needs to be injected inside the node around the child content.
|
|
575
|
+
*/
|
|
576
|
+
export const injectInnerWrapper = ({
|
|
577
|
+
node,
|
|
578
|
+
colorScheme,
|
|
579
|
+
isActive,
|
|
580
|
+
isInserted
|
|
581
|
+
}) => {
|
|
582
|
+
const wrapper = document.createElement('span');
|
|
583
|
+
wrapper.setAttribute('style', isInserted ? getInsertedContentStyle(colorScheme, isActive) : getDeletedContentStyle(colorScheme, isActive));
|
|
584
|
+
[...node.childNodes].forEach(child => {
|
|
585
|
+
const removedChild = node.removeChild(child);
|
|
586
|
+
wrapper.append(removedChild);
|
|
587
|
+
});
|
|
588
|
+
node.appendChild(wrapper);
|
|
589
|
+
return node;
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Creates a styled span wrapper for inline content within a change decoration.
|
|
594
|
+
*/
|
|
595
|
+
export const createContentWrapper = (colorScheme, isActive = false, isInserted = false) => {
|
|
596
|
+
const wrapper = document.createElement('span');
|
|
597
|
+
const baseStyle = convertToInlineCss({
|
|
598
|
+
position: 'relative',
|
|
599
|
+
width: 'fit-content'
|
|
600
|
+
});
|
|
601
|
+
if (expValEquals('platform_editor_diff_plugin_extended', 'isEnabled', true)) {
|
|
602
|
+
if (isInserted) {
|
|
603
|
+
wrapper.setAttribute('style', `${baseStyle}${getInsertedContentStyle(colorScheme, isActive)}`);
|
|
604
|
+
} else {
|
|
605
|
+
wrapper.setAttribute('style', `${baseStyle}${getDeletedContentStyle(colorScheme, isActive)}`);
|
|
606
|
+
const strikethrough = document.createElement('span');
|
|
607
|
+
strikethrough.setAttribute('style', getDeletedContentStyleUnbounded(colorScheme, isActive));
|
|
608
|
+
wrapper.append(strikethrough);
|
|
609
|
+
}
|
|
610
|
+
} else {
|
|
611
|
+
wrapper.setAttribute('style', `${baseStyle}${getDeletedContentStyle(colorScheme, isActive)}`);
|
|
612
|
+
const strikethrough = document.createElement('span');
|
|
613
|
+
strikethrough.setAttribute('style', getDeletedContentStyleUnbounded(colorScheme, isActive));
|
|
614
|
+
wrapper.append(strikethrough);
|
|
615
|
+
}
|
|
616
|
+
return wrapper;
|
|
545
617
|
};
|