@atlaskit/editor-plugin-show-diff 10.1.10 → 10.1.12

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.
Files changed (27) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/cjs/pm-plugins/calculateDiff/calculateDiffDecorations.js +118 -31
  3. package/dist/cjs/pm-plugins/calculateDiff/diffBySteps.js +124 -1
  4. package/dist/cjs/pm-plugins/decorations/colorSchemes/standard.js +6 -1
  5. package/dist/cjs/pm-plugins/decorations/createBlockChangedDecoration.js +17 -0
  6. package/dist/cjs/pm-plugins/decorations/createGranularBlockReferenceWidget.js +125 -0
  7. package/dist/cjs/pm-plugins/decorations/createNodeChangedDecorationWidget.js +50 -105
  8. package/dist/cjs/pm-plugins/decorations/utils/wrapBlockNodeView.js +79 -1
  9. package/dist/es2019/pm-plugins/calculateDiff/calculateDiffDecorations.js +91 -7
  10. package/dist/es2019/pm-plugins/calculateDiff/diffBySteps.js +104 -0
  11. package/dist/es2019/pm-plugins/decorations/colorSchemes/standard.js +5 -0
  12. package/dist/es2019/pm-plugins/decorations/createBlockChangedDecoration.js +18 -1
  13. package/dist/es2019/pm-plugins/decorations/createGranularBlockReferenceWidget.js +120 -0
  14. package/dist/es2019/pm-plugins/decorations/createNodeChangedDecorationWidget.js +41 -89
  15. package/dist/es2019/pm-plugins/decorations/utils/wrapBlockNodeView.js +74 -2
  16. package/dist/esm/pm-plugins/calculateDiff/calculateDiffDecorations.js +119 -32
  17. package/dist/esm/pm-plugins/calculateDiff/diffBySteps.js +123 -0
  18. package/dist/esm/pm-plugins/decorations/colorSchemes/standard.js +5 -0
  19. package/dist/esm/pm-plugins/decorations/createBlockChangedDecoration.js +18 -1
  20. package/dist/esm/pm-plugins/decorations/createGranularBlockReferenceWidget.js +120 -0
  21. package/dist/esm/pm-plugins/decorations/createNodeChangedDecorationWidget.js +46 -100
  22. package/dist/esm/pm-plugins/decorations/utils/wrapBlockNodeView.js +79 -2
  23. package/dist/types/pm-plugins/calculateDiff/diffBySteps.d.ts +20 -0
  24. package/dist/types/pm-plugins/decorations/colorSchemes/standard.d.ts +1 -0
  25. package/dist/types/pm-plugins/decorations/createGranularBlockReferenceWidget.d.ts +36 -0
  26. package/dist/types/pm-plugins/decorations/utils/wrapBlockNodeView.d.ts +15 -0
  27. package/package.json +2 -2
@@ -6,12 +6,8 @@ Object.defineProperty(exports, "__esModule", {
6
6
  });
7
7
  exports.createNodeChangedDecorationWidget = void 0;
8
8
  var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
9
- var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray"));
10
- var _lazyNodeView = require("@atlaskit/editor-common/lazy-node-view");
11
9
  var _view = require("@atlaskit/editor-prosemirror/view");
12
10
  var _expValEquals = require("@atlaskit/tmp-editor-statsig/exp-val-equals");
13
- var _standard = require("./colorSchemes/standard");
14
- var _traditional = require("./colorSchemes/traditional");
15
11
  var _createAnchorDecorationWidgets = require("./createAnchorDecorationWidgets");
16
12
  var _createChangedRowDecorationWidgets = require("./createChangedRowDecorationWidgets");
17
13
  var _decorationKeys = require("./decorationKeys");
@@ -19,105 +15,23 @@ var _findSafeInsertPos = require("./utils/findSafeInsertPos");
19
15
  var _wrapBlockNodeView = require("./utils/wrapBlockNodeView");
20
16
  function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
21
17
  function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { (0, _defineProperty2.default)(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
22
- var getDeletedContentStyleUnbounded = function getDeletedContentStyleUnbounded(colorScheme) {
23
- var isActive = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
24
- if (colorScheme === 'traditional' && isActive) {
25
- return _traditional.deletedTraditionalContentStyleUnboundedActive;
26
- }
27
- return colorScheme === 'traditional' ? _traditional.deletedTraditionalContentStyleUnbounded : _standard.deletedContentStyleUnbounded;
28
- };
29
- var getInsertedContentStyle = function getInsertedContentStyle(colorScheme) {
30
- var isActive = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
31
- if (colorScheme === 'traditional') {
32
- if (isActive) {
33
- return _traditional.traditionalInsertStyleActive;
34
- }
35
- return _traditional.traditionalInsertStyle;
36
- }
37
- if (isActive) {
38
- return (0, _expValEquals.expValEquals)('platform_editor_diff_plugin_extended', 'isEnabled', true) ? _standard.editingStyleActiveExtended : _standard.editingStyleActive;
39
- }
40
- return (0, _expValEquals.expValEquals)('platform_editor_diff_plugin_extended', 'isEnabled', true) ? _standard.editingStyleExtended : _standard.editingStyle;
41
- };
42
- var getDeletedContentStyle = function getDeletedContentStyle(colorScheme) {
43
- var isActive = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
44
- if (colorScheme === 'traditional') {
45
- return (0, _traditional.getDeletedTraditionalInlineStyle)(isActive);
46
- }
47
- // Merge into existing styles when cleaning up
48
- if ((0, _expValEquals.expValEquals)('platform_editor_diff_plugin_extended', 'isEnabled', true)) {
49
- if (isActive) {
50
- return _standard.deletedContentStyleActive + _standard.deletedInlineContentStyleExtended;
51
- }
52
- return _standard.deletedContentStyleNew + _standard.deletedInlineContentStyleExtended;
53
- }
54
- if (isActive) {
55
- return _standard.deletedContentStyleActive;
56
- }
57
- return (0, _expValEquals.expValEquals)('platform_editor_enghealth_a11y_jan_fixes', 'isEnabled', true) ? _standard.deletedContentStyleNew : _standard.deletedContentStyle;
58
- };
59
-
60
- /**
61
- * CSS backgrounds don't work when applied to a wrapper around a paragraph, so
62
- * the wrapper needs to be injected inside the node around the child content
63
- */
64
- var injectInnerWrapper = function injectInnerWrapper(_ref) {
65
- var node = _ref.node,
66
- colorScheme = _ref.colorScheme,
67
- isActive = _ref.isActive,
68
- isInserted = _ref.isInserted;
69
- var wrapper = document.createElement('span');
70
- wrapper.setAttribute('style', isInserted ? getInsertedContentStyle(colorScheme, isActive) : getDeletedContentStyle(colorScheme, isActive));
71
- (0, _toConsumableArray2.default)(node.childNodes).forEach(function (child) {
72
- var removedChild = node.removeChild(child);
73
- wrapper.append(removedChild);
74
- });
75
- node.appendChild(wrapper);
76
- return node;
77
- };
78
- var createContentWrapper = function createContentWrapper(colorScheme) {
79
- var isActive = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
80
- var isInserted = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
81
- var wrapper = document.createElement('span');
82
- var baseStyle = (0, _lazyNodeView.convertToInlineCss)({
83
- position: 'relative',
84
- width: 'fit-content'
85
- });
86
- if ((0, _expValEquals.expValEquals)('platform_editor_diff_plugin_extended', 'isEnabled', true)) {
87
- if (isInserted) {
88
- wrapper.setAttribute('style', "".concat(baseStyle).concat(getInsertedContentStyle(colorScheme, isActive)));
89
- } else {
90
- wrapper.setAttribute('style', "".concat(baseStyle).concat(getDeletedContentStyle(colorScheme, isActive)));
91
- var strikethrough = document.createElement('span');
92
- strikethrough.setAttribute('style', getDeletedContentStyleUnbounded(colorScheme, isActive));
93
- wrapper.append(strikethrough);
94
- }
95
- } else {
96
- wrapper.setAttribute('style', "".concat(baseStyle).concat(getDeletedContentStyle(colorScheme, isActive)));
97
- var _strikethrough = document.createElement('span');
98
- _strikethrough.setAttribute('style', getDeletedContentStyleUnbounded(colorScheme, isActive));
99
- wrapper.append(_strikethrough);
100
- }
101
- return wrapper;
102
- };
103
-
104
18
  /**
105
19
  * This function is used to create a decoration widget to show content
106
20
  * that is not in the current document.
107
21
  */
108
- var createNodeChangedDecorationWidget = exports.createNodeChangedDecorationWidget = function createNodeChangedDecorationWidget(_ref2) {
22
+ var createNodeChangedDecorationWidget = exports.createNodeChangedDecorationWidget = function createNodeChangedDecorationWidget(_ref) {
109
23
  var _slice$content, _slice$content2, _slice$content3, _slice$content$firstC;
110
- var change = _ref2.change,
111
- doc = _ref2.doc,
112
- nodeViewSerializer = _ref2.nodeViewSerializer,
113
- colorScheme = _ref2.colorScheme,
114
- newDoc = _ref2.newDoc,
115
- intl = _ref2.intl,
116
- activeIndexPos = _ref2.activeIndexPos,
117
- _ref2$isInserted = _ref2.isInserted,
118
- isInserted = _ref2$isInserted === void 0 ? false : _ref2$isInserted,
119
- _ref2$showIndicators = _ref2.showIndicators,
120
- showIndicators = _ref2$showIndicators === void 0 ? false : _ref2$showIndicators;
24
+ var change = _ref.change,
25
+ doc = _ref.doc,
26
+ nodeViewSerializer = _ref.nodeViewSerializer,
27
+ colorScheme = _ref.colorScheme,
28
+ newDoc = _ref.newDoc,
29
+ intl = _ref.intl,
30
+ activeIndexPos = _ref.activeIndexPos,
31
+ _ref$isInserted = _ref.isInserted,
32
+ isInserted = _ref$isInserted === void 0 ? false : _ref$isInserted,
33
+ _ref$showIndicators = _ref.showIndicators,
34
+ showIndicators = _ref$showIndicators === void 0 ? false : _ref$showIndicators;
121
35
  var slice = doc.slice(change.fromA, change.toA);
122
36
  var shouldSkipDeletedEmptyParagraphDecoration = !isInserted && (slice === null || slice === void 0 || (_slice$content = slice.content) === null || _slice$content === void 0 ? void 0 : _slice$content.childCount) === 1 && (slice === null || slice === void 0 || (_slice$content2 = slice.content) === null || _slice$content2 === void 0 || (_slice$content2 = _slice$content2.firstChild) === null || _slice$content2 === void 0 ? void 0 : _slice$content2.type.name) === 'paragraph' && (slice === null || slice === void 0 || (_slice$content3 = slice.content) === null || _slice$content3 === void 0 || (_slice$content3 = _slice$content3.firstChild) === null || _slice$content3 === void 0 ? void 0 : _slice$content3.content.size) === 0;
123
37
  // Widget decoration used for deletions as the content is not in the document
@@ -154,6 +68,32 @@ var createNodeChangedDecorationWidget = exports.createNodeChangedDecorationWidge
154
68
 
155
69
  // For non-table content, use the existing span wrapper approach
156
70
  var dom = document.createElement('span');
71
+ // When mediaSingle nodes are rendered inside a widget decoration (e.g. as part of
72
+ // a replaced panel), the centering CSS (margin-left: 50%; transform: translateX(-50%))
73
+ // incorrectly applies because isNestedNode resolves to false (getPos returns 0).
74
+ // Observe DOM mutations and override the transform on .rich-media-item elements
75
+ // after React mounts to prevent the image from shifting outside its parent container.
76
+ var constrainMediaObserver;
77
+ if ((0, _expValEquals.expValEquals)('platform_editor_diff_plugin_extended', 'isEnabled', true)) {
78
+ constrainMediaObserver = new MutationObserver(function () {
79
+ var richMediaItems = dom.querySelectorAll('.rich-media-item');
80
+ richMediaItems.forEach(function (el) {
81
+ if (el instanceof HTMLElement) {
82
+ el.style.transform = 'none';
83
+ el.style.marginLeft = '0';
84
+ el.style.maxWidth = '100%';
85
+ }
86
+ });
87
+ if (richMediaItems.length > 0) {
88
+ var _constrainMediaObserv;
89
+ (_constrainMediaObserv = constrainMediaObserver) === null || _constrainMediaObserv === void 0 || _constrainMediaObserv.disconnect();
90
+ }
91
+ });
92
+ constrainMediaObserver.observe(dom, {
93
+ childList: true,
94
+ subtree: true
95
+ });
96
+ }
157
97
  var diffId = crypto.randomUUID();
158
98
  var decorations = [];
159
99
 
@@ -171,14 +111,14 @@ var createNodeChangedDecorationWidget = exports.createNodeChangedDecorationWidge
171
111
  if (childNodeView) {
172
112
  var lineBreak = document.createElement('br');
173
113
  dom.append(lineBreak);
174
- var wrapper = createContentWrapper(colorScheme, isActive, isInserted);
114
+ var wrapper = (0, _wrapBlockNodeView.createContentWrapper)(colorScheme, isActive, isInserted);
175
115
  wrapper.append(childNodeView);
176
116
  dom.append(wrapper);
177
117
  } else {
178
118
  // Fallback to serializing the individual child node
179
119
  var serializedChild = serializer.serializeNode(childNode);
180
120
  if (serializedChild) {
181
- var _wrapper = createContentWrapper(colorScheme, isActive, isInserted);
121
+ var _wrapper = (0, _wrapBlockNodeView.createContentWrapper)(colorScheme, isActive, isInserted);
182
122
  _wrapper.append(serializedChild);
183
123
  dom.append(_wrapper);
184
124
  }
@@ -223,7 +163,7 @@ var createNodeChangedDecorationWidget = exports.createNodeChangedDecorationWidge
223
163
  var nodeView = serializer.tryCreateNodeView(node);
224
164
  if (nodeView) {
225
165
  if (node.isInline) {
226
- var wrapper = createContentWrapper(colorScheme, isActive, isInserted);
166
+ var wrapper = (0, _wrapBlockNodeView.createContentWrapper)(colorScheme, isActive, isInserted);
227
167
  wrapper.append(nodeView);
228
168
  dom.append(wrapper);
229
169
  } else {
@@ -245,7 +185,7 @@ var createNodeChangedDecorationWidget = exports.createNodeChangedDecorationWidge
245
185
  var fallbackNode = fallbackSerialization();
246
186
  if (fallbackNode) {
247
187
  if (fallbackNode instanceof HTMLElement) {
248
- var injectedNode = injectInnerWrapper({
188
+ var injectedNode = (0, _wrapBlockNodeView.injectInnerWrapper)({
249
189
  node: fallbackNode,
250
190
  colorScheme: colorScheme,
251
191
  isActive: isActive,
@@ -253,7 +193,7 @@ var createNodeChangedDecorationWidget = exports.createNodeChangedDecorationWidge
253
193
  });
254
194
  dom.append(injectedNode);
255
195
  } else {
256
- var _wrapper2 = createContentWrapper(colorScheme, isActive, isInserted);
196
+ var _wrapper2 = (0, _wrapBlockNodeView.createContentWrapper)(colorScheme, isActive, isInserted);
257
197
  _wrapper2.append(fallbackNode);
258
198
  dom.append(_wrapper2);
259
199
  }
@@ -274,13 +214,18 @@ var createNodeChangedDecorationWidget = exports.createNodeChangedDecorationWidge
274
214
  decorations.push(leftAnchor);
275
215
  }
276
216
  }
277
- decorations.push(_view.Decoration.widget(safeInsertPos, dom, (0, _decorationKeys.buildDiffDecorationSpec)(_objectSpread({
217
+ decorations.push(_view.Decoration.widget(safeInsertPos, dom, _objectSpread(_objectSpread({}, (0, _decorationKeys.buildDiffDecorationSpec)(_objectSpread({
278
218
  decorationType: 'widget',
279
219
  diffId: diffId,
280
220
  isActive: isActive
281
221
  }, (0, _expValEquals.expValEquals)('platform_editor_diff_plugin_extended', 'isEnabled', true) && {
282
222
  side: -1
283
- }))));
223
+ }))), {}, {
224
+ destroy: function destroy() {
225
+ var _constrainMediaObserv2;
226
+ return (_constrainMediaObserv2 = constrainMediaObserver) === null || _constrainMediaObserv2 === void 0 ? void 0 : _constrainMediaObserv2.disconnect();
227
+ }
228
+ })));
284
229
 
285
230
  // When a single block node is purely deleted at the very start of the doc (first child),
286
231
  // the deleted widget decoration overlaps with the existing first child's decoration.
@@ -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
- changes.forEach(change => {
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
- const shouldHideDeleted = expValEquals('platform_editor_diff_plugin_extended', 'isEnabled', true) ? isInverted && hideDeletedDiffs : false;
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
  };
@@ -136,6 +136,11 @@ export const standardDecorationMarkerVariable = convertToInlineCss({
136
136
  '--diff-decoration-marker-color': "var(--ds-border-accent-purple, #AF59E1)",
137
137
  '--diff-decoration-marker-ring-width': '1px'
138
138
  });
139
+ export const deletedDecorationMarkerVariable = convertToInlineCss({
140
+ '--diff-decoration-marker-color': "var(--ds-border-accent-gray, #7D818A)",
141
+ '--diff-decoration-marker-ring-width': '1px',
142
+ opacity: 0.8
143
+ });
139
144
  export const addedCellOverlayStyle = convertToInlineCss({
140
145
  position: 'absolute',
141
146
  top: 0,
@@ -1,7 +1,7 @@
1
1
  import { convertToInlineCss } from '@atlaskit/editor-common/lazy-node-view';
2
2
  import { Decoration } from '@atlaskit/editor-prosemirror/view';
3
3
  import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
4
- import { standardDecorationMarkerVariable, editingStyleQuoteNode, editingStyleRuleNode, editingStyleCardBlockNode, editingStyleNode, deletedContentStyleNew, deletedStyleQuoteNode, addedCellOverlayStyle, deletedCellOverlayStyle } from './colorSchemes/standard';
4
+ import { standardDecorationMarkerVariable, deletedDecorationMarkerVariable, editingStyleQuoteNode, editingStyleRuleNode, editingStyleCardBlockNode, editingStyleNode, deletedContentStyleNew, deletedStyleQuoteNode, addedCellOverlayStyle, deletedCellOverlayStyle } from './colorSchemes/standard';
5
5
  import { traditionalDecorationMarkerVariableActive, traditionalDecorationMarkerVariableNew, traditionalDeletedDecorationMarkerVariableActive, traditionalDeletedDecorationMarkerVariableNew, traditionalStyleQuoteNodeActive, traditionalStyleQuoteNodeNew, traditionalStyleRuleNodeActive, traditionalStyleRuleNodeNew, traditionalStyleCardBlockNodeActive, traditionalStyleCardBlockNodeNew, traditionalStyleNodeActive, traditionalStyleNodeNew, getDeletedTraditionalInlineStyle, deletedTraditionalStyleQuoteNode, traditionalAddedCellOverlayStyle, deletedTraditionalCellOverlayStyle } from './colorSchemes/traditional';
6
6
  import { buildDiffDecorationSpec } from './decorationKeys';
7
7
  const displayNoneStyle = convertToInlineCss({
@@ -30,6 +30,14 @@ const getBlockNodeStyle = ({
30
30
  // Layout nodes do not need special styling
31
31
  return undefined;
32
32
  }
33
+ // Media nodes inside mediaSingle should not get position:relative
34
+ // as it shifts the image outside its parent container (e.g. panel)
35
+ if (nodeName === 'media') {
36
+ if (!isInserted && expValEquals('platform_editor_diff_plugin_extended', 'isEnabled', true)) {
37
+ return isTraditional ? getDeletedTraditionalInlineStyle(false) : deletedDecorationMarkerVariable;
38
+ }
39
+ return isTraditional ? isActive ? traditionalStyleNodeActive : traditionalStyleNodeNew : editingStyleNode;
40
+ }
33
41
  if (['tableCell', 'tableHeader'].includes(nodeName)) {
34
42
  if (expValEquals('platform_editor_diff_plugin_extended', 'isEnabled', true)) {
35
43
  // This is used for positioning the cell overlay widget decorations
@@ -40,11 +48,20 @@ const getBlockNodeStyle = ({
40
48
  // When gate is off, it should return undefined as above
41
49
  return undefined;
42
50
  }
51
+ if (nodeName === 'panel') {
52
+ if (!isInserted && expValEquals('platform_editor_diff_plugin_extended', 'isEnabled', true)) {
53
+ return isTraditional ? getDeletedTraditionalInlineStyle(false) : deletedDecorationMarkerVariable;
54
+ }
55
+ return isTraditional ? isActive ? traditionalStyleNodeActive : traditionalStyleNodeNew : editingStyleNode;
56
+ }
43
57
  if (['extension', 'embedCard', 'listItem'].includes(nodeName)) {
44
58
  if (expValEquals('platform_editor_diff_plugin_extended', 'isEnabled', true)) {
45
59
  if (isInserted) {
46
60
  return isTraditional && isActive ? traditionalDecorationMarkerVariableActive : isTraditional ? traditionalDecorationMarkerVariableNew : standardDecorationMarkerVariable;
47
61
  } else {
62
+ if (nodeName === 'listItem') {
63
+ return isTraditional && isActive ? traditionalDeletedDecorationMarkerVariableActive : isTraditional ? traditionalDeletedDecorationMarkerVariableNew : deletedDecorationMarkerVariable;
64
+ }
48
65
  return isTraditional && isActive ? traditionalDeletedDecorationMarkerVariableActive : isTraditional ? traditionalDeletedDecorationMarkerVariableNew : deletedContentStyleNew;
49
66
  }
50
67
  }