@atlaskit/editor-plugin-show-diff 10.1.19 → 10.1.21

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.
@@ -1,83 +1,85 @@
1
+ import { findParentNodeClosestToPos } from '@atlaskit/editor-prosemirror/utils';
1
2
  import { Decoration } from '@atlaskit/editor-prosemirror/view';
2
3
  import { buildAnchorDecorationKey, buildAnchorDecorationSpec, AnchorDocMarginKey, AnchorTypeKey } from './decorationKeys';
3
- const SIDE = 1;
4
- const findPosAfterLastChild = (node, nodeStart) => {
4
+
5
+ /**
6
+ * Resolves the doc-level block node (table/expand/layout) for `from`, along with
7
+ * the position right before it (`beforePos`). Falls back to `$from.nodeAfter`
8
+ * when there is no depth-1 ancestor (e.g. `from` sits just before the block).
9
+ */
10
+ const resolveDocLevelNode = (doc, from) => {
11
+ var _$from$node;
12
+ const $from = doc.resolve(from);
13
+ const node = (_$from$node = $from.node(1)) !== null && _$from$node !== void 0 ? _$from$node : $from.nodeAfter;
5
14
  if (!node) {
6
15
  return undefined;
7
16
  }
8
- const lastChild = node.lastChild;
9
- if (!lastChild) {
10
- return undefined;
11
- }
12
- const lastChildStart = nodeStart + node.content.size - lastChild.nodeSize;
13
- return lastChildStart + lastChild.nodeSize;
17
+
18
+ // Content start of the block. For the depth-1 case this is `$from.start(1)`;
19
+ // for the `nodeAfter` fallback, `from` is the position just before the block
20
+ // node, so its content starts at `from + 1`.
21
+ const nodeStart = $from.node(1) ? $from.start(1) : from + 1;
22
+ return {
23
+ node,
24
+ nodeStart,
25
+ // Position of the block node itself (one before its content start).
26
+ beforePos: nodeStart - 1
27
+ };
14
28
  };
15
29
 
16
30
  /**
17
31
  * Handles edge cases for block nodes whose inline content can exceed the doc
18
- * margin (tables, layouts, expands). Returns an adjusted position to use as
19
- * the left anchor, or `undefined` when no adjustment is needed.
32
+ * margin (tables, layouts, expands). Returns the position whose DOM should be
33
+ * measured to size the left anchor, or `undefined` when the diff is not inside
34
+ * such a node.
20
35
  */
21
36
  const edgeCases = (doc, from) => {
22
- const $from = doc.resolve(from);
23
- const docLevelNode = $from.node(1);
24
- if (!docLevelNode) {
37
+ const resolved = resolveDocLevelNode(doc, from);
38
+ if (!resolved) {
25
39
  return undefined;
26
40
  }
27
- switch (docLevelNode.type.name) {
41
+ const {
42
+ node,
43
+ nodeStart,
44
+ beforePos
45
+ } = resolved;
46
+
47
+ /**
48
+ * All resizable nodes will need dynamic calculations of the block indicator left anchor
49
+ */
50
+ if (node.marks.some(mark => mark.type.name === 'breakout')) {
51
+ /**
52
+ * Layouts and Expands have extra padding around the container
53
+ */
54
+ return {
55
+ measurePos: beforePos
56
+ };
57
+ }
58
+ switch (node.type.name) {
28
59
  /**
29
60
  * For resizable blocks, inline content can exceed the doc margin.
30
- * The widgets need to be placed inside the block as resizing the block will
31
- * exceed the block container :')
61
+ * The widget is placed before the block; the anchor is sized against the
62
+ * block's DOM so it doesn't get clipped when the block is resized :')
32
63
  */
33
64
  case 'table':
34
65
  {
35
- const lastRow = docLevelNode.lastChild;
36
- if (!lastRow) {
37
- return undefined;
38
- }
39
- const lastRowStart = $from.start(1) + docLevelNode.content.size - lastRow.nodeSize;
40
- const firstCellOfLastRow = lastRow.firstChild;
41
- const tableAnchorPos = findPosAfterLastChild(firstCellOfLastRow, lastRowStart + 1);
42
- if (tableAnchorPos === undefined) {
43
- return undefined;
44
- }
45
- return {
46
- /**
47
- * We use the last row because the first row could be a sticky header -
48
- * when it becomes sticky the anchor won't be defined since it will be rendered
49
- * outside of the context
50
- */
51
- pos: tableAnchorPos,
52
- leftMargin: "var(--ds-space-negative-025, -2px)",
53
- side: -1
54
- };
55
- }
56
- case 'expand':
57
- {
58
- const expandAnchorPos = findPosAfterLastChild(docLevelNode, $from.start(1));
59
- if (expandAnchorPos === undefined) {
66
+ // A table with no rows has nothing to measure.
67
+ if (!node.firstChild) {
60
68
  return undefined;
61
69
  }
70
+
71
+ // Measure the first row (`nodeStart` is just inside the table, i.e. the
72
+ // position of the first row): its width matches the table's.
62
73
  return {
63
- pos: expandAnchorPos,
64
- leftMargin: "var(--ds-space-0, 0px)",
65
- side: -1
74
+ measurePos: nodeStart
66
75
  };
67
76
  }
68
77
  case 'layoutSection':
69
- {
70
- const firstLayoutColumn = docLevelNode.firstChild;
71
- const layoutAnchorPos = findPosAfterLastChild(firstLayoutColumn, $from.start(1) + 1);
72
- if (layoutAnchorPos === undefined) {
73
- return undefined;
74
- }
75
- return {
76
- pos: layoutAnchorPos,
77
- leftMargin: "var(--ds-space-negative-025, -2px)",
78
- side: 1
79
- };
80
- }
78
+ case 'expand':
79
+ // Measure the block itself (the widget is rendered outside the block).
80
+ return {
81
+ leftOffset: 12
82
+ };
81
83
  default:
82
84
  return undefined;
83
85
  }
@@ -93,9 +95,11 @@ export const createDocMarginAnchorWidget = () => {
93
95
  const span = document.createElement('span');
94
96
  span.style.setProperty('anchor-name', `--${AnchorDocMarginKey}`);
95
97
  return span;
96
- }, buildAnchorDecorationSpec({
98
+ },
99
+ // set the side to -999 so that it is always rendered before any other anchors
100
+ buildAnchorDecorationSpec({
97
101
  anchorType: AnchorTypeKey.docMargin,
98
- side: SIDE
102
+ side: -999
99
103
  }));
100
104
  };
101
105
 
@@ -118,20 +122,56 @@ export const createLeftAnchorWidget = ({
118
122
  if (edgeCase === undefined) {
119
123
  return undefined;
120
124
  }
125
+
126
+ // Render the widget right before the doc-level node so it lives outside the
127
+ // resizable block container.
128
+ const resolved = resolveDocLevelNode(doc, from);
129
+ if (!resolved) {
130
+ return undefined;
131
+ }
132
+ const {
133
+ beforePos
134
+ } = resolved;
121
135
  const leftAnchorKey = buildAnchorDecorationKey({
122
136
  diffId,
123
137
  anchorType: AnchorTypeKey.left
124
138
  });
125
- return Decoration.widget(edgeCase.pos, () => {
126
- const span = document.createElement('span');
127
- span.style.setProperty('anchor-name', `--${leftAnchorKey}`);
128
- span.style.setProperty('position', 'absolute');
129
- span.style.setProperty('left', edgeCase.leftMargin);
130
- return span;
139
+ return Decoration.widget(beforePos, (view, getPos) => {
140
+ // Outer span stays in the flow but takes up no space.
141
+ const wrapper = document.createElement('div');
142
+ wrapper.style.setProperty('position', 'relative');
143
+ wrapper.style.setProperty('width', '100%');
144
+
145
+ // Inner span is absolutely positioned in the doc margin; it carries the
146
+ // `anchor-name` the IndicatorBar aligns its left edge against.
147
+ const anchor = document.createElement('div');
148
+ anchor.style.setProperty('anchor-name', `--${leftAnchorKey}`);
149
+ anchor.style.setProperty('position', 'absolute');
150
+ anchor.style.setProperty('left', `calc(50% - ${(edgeCase === null || edgeCase === void 0 ? void 0 : edgeCase.leftOffset) || 0}px)`);
151
+ anchor.style.setProperty('transform', 'translateX(-50%)');
152
+ wrapper.appendChild(anchor);
153
+
154
+ // The block DOM may not be settled synchronously (e.g. after a
155
+ // transaction), so defer the measurement like the gap cursor does.
156
+ requestAnimationFrame(() => {
157
+ // The widget may have been unmounted; bail if its position is gone.
158
+ // We still measure the stable block node position, not the widget's
159
+ // own position.
160
+ if (getPos() === undefined || edgeCase.measurePos === undefined) {
161
+ return;
162
+ }
163
+ const dom = view.nodeDOM(edgeCase.measurePos);
164
+ if (dom instanceof HTMLElement) {
165
+ // The left anchor only needs the container width so the
166
+ // IndicatorBar can align against the block's horizontal extent.
167
+ anchor.style.setProperty('width', `${dom.offsetWidth}px`);
168
+ }
169
+ });
170
+ return wrapper;
131
171
  }, buildAnchorDecorationSpec({
132
172
  diffId,
133
173
  anchorType: AnchorTypeKey.left,
134
- side: edgeCase.side * SIDE
174
+ side: -999
135
175
  }));
136
176
  };
137
177
  const isFullNodeRange = (doc, from, to) => {
@@ -145,6 +185,111 @@ const isFullNodeRange = (doc, from, to) => {
145
185
  });
146
186
  return matchesFullNodeRange;
147
187
  };
188
+ /**
189
+ * Creates invisible anchor widgets for a single block-changed diff so that the
190
+ * `IndicatorBar` can use CSS anchor positioning to align itself with the diff.
191
+ *
192
+ * The interface mirrors `createInlineIndicatorAnchorWidgets`:
193
+ * - A `from` anchor is placed at the start of the node range (top of the bar).
194
+ * - A `to` anchor is placed at the end of the node range (bottom of the bar).
195
+ * - An optional `left` anchor is placed inside a resizable container (table,
196
+ * layout, expand) so the bar aligns within the container boundary.
197
+ *
198
+ * Unlike the inline variant there is no `isFullNodeRange` guard — block diffs
199
+ * always span exactly one block node so the anchors are always needed.
200
+ */
201
+ export const createBlockIndicatorAnchorWidgets = ({
202
+ doc,
203
+ from,
204
+ to,
205
+ diffId
206
+ }) => {
207
+ const leftAnchor = createLeftAnchorWidget({
208
+ doc,
209
+ from,
210
+ diffId
211
+ });
212
+ const maybeLeftAnchor = leftAnchor ? [leftAnchor] : [];
213
+
214
+ /**
215
+ * A single anchor widget spans the full height of the block node, mimicking
216
+ * the gap cursor placement logic (see `place-gap-cursor.ts`): an element
217
+ * whose height is measured from the block's DOM so its box covers the block.
218
+ *
219
+ * Because the anchor rect covers the whole block, the `IndicatorBar` can
220
+ * resolve `top`, `bottom` and `left` against this one anchor (keyed by
221
+ * `diffId` with no `anchorType`) instead of separate `from`/`to` anchors.
222
+ */
223
+ const blockAnchorKey = buildAnchorDecorationKey({
224
+ diffId
225
+ });
226
+
227
+ /**
228
+ * If `from` lands inside a table cell/header or a table row, the widget must
229
+ * still be rendered *outside* the table (widgets placed inside a table are
230
+ * clipped/mis-laid-out), but we want the anchor to be sized against the
231
+ * actual cell/row DOM. So we split into two positions:
232
+ * - `widgetPos`: where the widget DOM is rendered (outside the table).
233
+ * - `measurePos`: the closest cell/row whose DOM we measure for the height.
234
+ */
235
+ const $from = doc.resolve(from);
236
+ const parentTable = findParentNodeClosestToPos($from, ancestor => ancestor.type.name === 'table');
237
+ const parentCellOrRow = findParentNodeClosestToPos($from, ancestor => ['tableCell', 'tableHeader', 'tableRow'].includes(ancestor.type.name));
238
+
239
+ // Render outside the table when inside one; otherwise keep the original pos.
240
+ const widgetPos = parentTable ? parentTable.pos : from;
241
+ // Measure the actual cell/row DOM when inside one; otherwise measure the
242
+ // widget's own position.
243
+ const measurePos = parentCellOrRow ? parentCellOrRow.pos : from;
244
+ const blockWidget = Decoration.widget(widgetPos, (view, getPos) => {
245
+ // Outer span stays in the flow but takes up no space.
246
+ const wrapper = document.createElement('span');
247
+ wrapper.style.setProperty('position', 'relative');
248
+
249
+ // Inner span is absolutely positioned and sized to the block height;
250
+ // it carries the `anchor-name` the IndicatorBar aligns against.
251
+ const anchor = document.createElement('span');
252
+ anchor.style.setProperty('position', 'absolute');
253
+ anchor.style.setProperty('anchor-name', `--${blockAnchorKey}`);
254
+ wrapper.appendChild(anchor);
255
+
256
+ // The block DOM may not be settled synchronously (e.g. after a
257
+ // transaction), so defer the measurement like the gap cursor does.
258
+ requestAnimationFrame(() => {
259
+ // The widget may have been unmounted; bail if its position is gone.
260
+ // We still measure the stable cell/row node position, not the
261
+ // widget's own position.
262
+ if (getPos() === undefined) {
263
+ return;
264
+ }
265
+ const dom = view.nodeDOM(measurePos);
266
+ if (dom instanceof HTMLElement) {
267
+ anchor.style.setProperty('height', `${dom.offsetHeight}px`);
268
+
269
+ // The wrapper renders outside the table, so there is a vertical
270
+ // gap between it and the cell/row we're anchoring to. Measure
271
+ // that delta and offset the (absolutely positioned) anchor by it
272
+ // so its box lines up with the cell/row.
273
+ const wrapperTop = wrapper.getBoundingClientRect().top;
274
+ const domTop = dom.getBoundingClientRect().top;
275
+ const verticalOffset = domTop - wrapperTop;
276
+ anchor.style.setProperty('top', `${verticalOffset}px`);
277
+ // The offset already accounts for the cell/row's position, so the
278
+ // margin-top must not be double-applied.
279
+ anchor.style.setProperty('margin-top', '0px');
280
+ }
281
+ });
282
+ return wrapper;
283
+ }, buildAnchorDecorationSpec({
284
+ diffId,
285
+ // Reuse the `from` anchor type slot; the generated key intentionally
286
+ // omits the anchor type so the single element backs top/bottom/left.
287
+ anchorType: AnchorTypeKey.from,
288
+ side: -1
289
+ }));
290
+ return [blockWidget, ...maybeLeftAnchor];
291
+ };
292
+
148
293
  /**
149
294
  * Creates invisible anchor widgets for a single inline diff range so that the
150
295
  * `IndicatorBar` can use CSS anchor positioning to align itself with the diff.
@@ -172,8 +317,8 @@ export const createInlineIndicatorAnchorWidgets = ({
172
317
 
173
318
  /**
174
319
  * Two widgets mark the start and end of the inline range so the
175
- * IndicatorBar can determine top/bottom even when the decoration
176
- * spans multiple blocks.
320
+ * IndicatorBar can determine top/bottom even if
321
+ * the inline decoration is broken up by marks / between blocks.
177
322
  */
178
323
  const fromAnchorKey = buildAnchorDecorationKey({
179
324
  diffId,
@@ -3,6 +3,7 @@ import { Decoration } from '@atlaskit/editor-prosemirror/view';
3
3
  import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
4
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
+ import { createBlockIndicatorAnchorWidgets } from './createAnchorDecorationWidgets';
6
7
  import { buildDiffDecorationSpec } from './decorationKeys';
7
8
  const displayNoneStyle = convertToInlineCss({
8
9
  display: 'none'
@@ -120,7 +121,9 @@ export const createBlockChangedDecoration = ({
120
121
  colorScheme,
121
122
  isInserted = true,
122
123
  isActive = false,
123
- shouldHideDeleted = false
124
+ shouldHideDeleted = false,
125
+ showIndicators = false,
126
+ doc
124
127
  }) => {
125
128
  const decorations = [];
126
129
  const diffId = crypto.randomUUID();
@@ -174,5 +177,13 @@ export const createBlockChangedDecoration = ({
174
177
  nodeName: change.name
175
178
  })));
176
179
  }
180
+ if (decorations.length > 0 && showIndicators && doc && expValEquals('platform_editor_diff_plugin_extended', 'isEnabled', true)) {
181
+ decorations.push(...createBlockIndicatorAnchorWidgets({
182
+ doc,
183
+ from: change.from,
184
+ to: change.to,
185
+ diffId
186
+ }));
187
+ }
177
188
  return decorations;
178
189
  };
@@ -24,6 +24,24 @@ const renderIndicatorBar = descriptor => {
24
24
  })
25
25
  });
26
26
  }
27
+ case 'block':
28
+ {
29
+ // The block anchor is a single element (keyed by diffId with no
30
+ // anchorType) sized to the full height of the block node, so top,
31
+ // bottom and left all resolve against it.
32
+ const blockAnchor = buildAnchorDecorationKey({
33
+ diffId: descriptor.id
34
+ });
35
+ return /*#__PURE__*/React.createElement(IndicatorBar, {
36
+ key: descriptor.id,
37
+ anchorTop: blockAnchor,
38
+ anchorBottom: blockAnchor,
39
+ anchorLeft: buildAnchorDecorationKey({
40
+ diffId: descriptor.id,
41
+ anchorType: AnchorTypeKey.left
42
+ })
43
+ });
44
+ }
27
45
  case 'widget':
28
46
  {
29
47
  return /*#__PURE__*/React.createElement(IndicatorBar, {
@@ -43,10 +61,6 @@ const renderIndicatorBar = descriptor => {
43
61
  })
44
62
  });
45
63
  }
46
- /**
47
- * TODO EDITOR-7711:
48
- * Support IndicatorBar for block-changed type diffs.
49
- */
50
64
  default:
51
65
  return null;
52
66
  }
@@ -55,7 +55,9 @@ var calculateNodesForBlockDecoration = function calculateNodesForBlockDecoration
55
55
  isInserted = _ref2$isInserted === void 0 ? true : _ref2$isInserted,
56
56
  activeIndexPos = _ref2.activeIndexPos,
57
57
  _ref2$shouldHideDelet = _ref2.shouldHideDeleted,
58
- shouldHideDeleted = _ref2$shouldHideDelet === void 0 ? false : _ref2$shouldHideDelet;
58
+ shouldHideDeleted = _ref2$shouldHideDelet === void 0 ? false : _ref2$shouldHideDelet,
59
+ _ref2$showIndicators = _ref2.showIndicators,
60
+ showIndicators = _ref2$showIndicators === void 0 ? false : _ref2$showIndicators;
59
61
  var decorations = [];
60
62
  // Iterate over the document nodes within the range
61
63
  doc.nodesBetween(from, to, function (node, pos) {
@@ -71,7 +73,9 @@ var calculateNodesForBlockDecoration = function calculateNodesForBlockDecoration
71
73
  colorScheme: colorScheme,
72
74
  isInserted: isInserted,
73
75
  isActive: isActive,
74
- shouldHideDeleted: shouldHideDeleted
76
+ shouldHideDeleted: shouldHideDeleted,
77
+ showIndicators: showIndicators,
78
+ doc: doc
75
79
  })));
76
80
  }
77
81
  });
@@ -193,7 +197,8 @@ var calculateDiffDecorationsInner = function calculateDiffDecorationsInner(_ref3
193
197
  colorScheme: colorScheme
194
198
  }, expValEquals('platform_editor_diff_plugin_extended', 'isEnabled', true) && {
195
199
  isInserted: isInserted,
196
- shouldHideDeleted: shouldHideDeleted
200
+ shouldHideDeleted: shouldHideDeleted,
201
+ showIndicators: showIndicators
197
202
  }), {}, {
198
203
  activeIndexPos: activeIndexPos,
199
204
  intl: intl
@@ -328,7 +333,8 @@ var calculateDiffDecorationsInner = function calculateDiffDecorationsInner(_ref3
328
333
  colorScheme: colorScheme,
329
334
  isInserted: true,
330
335
  activeIndexPos: activeIndexPos,
331
- intl: intl
336
+ intl: intl,
337
+ showIndicators: showIndicators
332
338
  })));
333
339
  }
334
340
  });