@atlaskit/editor-plugin-show-diff 16.1.0 → 16.1.1
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 +16 -0
- package/dist/cjs/pm-plugins/decorations/createNodeChangedDecorationWidget.js +83 -3
- package/dist/cjs/pm-plugins/decorations/utils/absorbFirstChildMarginReset.js +33 -0
- package/dist/cjs/pm-plugins/decorations/utils/createMarginAbsorber.js +43 -0
- package/dist/cjs/pm-plugins/decorations/utils/createNodeShapedMarginSpacer.js +76 -0
- package/dist/cjs/pm-plugins/decorations/utils/safeResolve.js +24 -0
- package/dist/cjs/ui/ContributorTag/contributorTagController.js +4 -4
- package/dist/es2019/pm-plugins/decorations/createNodeChangedDecorationWidget.js +83 -3
- package/dist/es2019/pm-plugins/decorations/utils/absorbFirstChildMarginReset.js +29 -0
- package/dist/es2019/pm-plugins/decorations/utils/createMarginAbsorber.js +39 -0
- package/dist/es2019/pm-plugins/decorations/utils/createNodeShapedMarginSpacer.js +69 -0
- package/dist/es2019/pm-plugins/decorations/utils/safeResolve.js +18 -0
- package/dist/es2019/ui/ContributorTag/contributorTagController.js +4 -4
- package/dist/esm/pm-plugins/decorations/createNodeChangedDecorationWidget.js +83 -3
- package/dist/esm/pm-plugins/decorations/utils/absorbFirstChildMarginReset.js +28 -0
- package/dist/esm/pm-plugins/decorations/utils/createMarginAbsorber.js +37 -0
- package/dist/esm/pm-plugins/decorations/utils/createNodeShapedMarginSpacer.js +70 -0
- package/dist/esm/pm-plugins/decorations/utils/safeResolve.js +18 -0
- package/dist/esm/ui/ContributorTag/contributorTagController.js +4 -4
- package/dist/types/pm-plugins/decorations/utils/absorbFirstChildMarginReset.d.ts +23 -0
- package/dist/types/pm-plugins/decorations/utils/createMarginAbsorber.d.ts +23 -0
- package/dist/types/pm-plugins/decorations/utils/createNodeShapedMarginSpacer.d.ts +26 -0
- package/dist/types/pm-plugins/decorations/utils/safeResolve.d.ts +13 -0
- package/package.json +12 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @atlaskit/editor-plugin-show-diff
|
|
2
2
|
|
|
3
|
+
## 16.1.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [`285905eda333a`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/285905eda333a) -
|
|
8
|
+
Show agent avatars before user avatars in connected contributor tags.
|
|
9
|
+
- [`2adef2b5610ff`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/2adef2b5610ff) -
|
|
10
|
+
[ux] Supply the top margin above a diff widget that lands at the start of its parent, so deleted
|
|
11
|
+
content no longer renders flush against the node below it. A node at the start of its parent has
|
|
12
|
+
its top margin reset; a spacer shaped like that node now carries the margin instead, in the
|
|
13
|
+
document and inside layout columns, expands, sync blocks and table cells. A complete deleted
|
|
14
|
+
textblock also keeps its own block wrapper so it retains its margin, including when that block
|
|
15
|
+
carries an alignment or indentation mark. Behind the platform_editor_ai_show_diff_patch_2 feature
|
|
16
|
+
gate.
|
|
17
|
+
- Updated dependencies
|
|
18
|
+
|
|
3
19
|
## 16.1.0
|
|
4
20
|
|
|
5
21
|
### Minor Changes
|
|
@@ -14,7 +14,11 @@ var _createAnchorDecorationWidgets = require("./createAnchorDecorationWidgets");
|
|
|
14
14
|
var _createChangedRowDecorationWidgets = require("./createChangedRowDecorationWidgets");
|
|
15
15
|
var _createContributorTagWidget = require("./createContributorTagWidget");
|
|
16
16
|
var _decorationKeys = require("./decorationKeys");
|
|
17
|
+
var _absorbFirstChildMarginReset = require("./utils/absorbFirstChildMarginReset");
|
|
18
|
+
var _createMarginAbsorber = require("./utils/createMarginAbsorber");
|
|
19
|
+
var _createNodeShapedMarginSpacer = require("./utils/createNodeShapedMarginSpacer");
|
|
17
20
|
var _findSafeInsertPos = require("./utils/findSafeInsertPos");
|
|
21
|
+
var _safeResolve = require("./utils/safeResolve");
|
|
18
22
|
var _wrapBlockNodeView = require("./utils/wrapBlockNodeView");
|
|
19
23
|
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; }
|
|
20
24
|
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; }
|
|
@@ -190,6 +194,14 @@ var createNodeChangedDecorationWidget = exports.createNodeChangedDecorationWidge
|
|
|
190
194
|
// For non-table content, use the existing span wrapper approach
|
|
191
195
|
var dom = document.createElement('span');
|
|
192
196
|
var $safeInsertPos = newDoc.resolve(safeInsertPos);
|
|
197
|
+
|
|
198
|
+
// Whether the widget renders above the very start of its parent's content — the document, a
|
|
199
|
+
// layout column, a table cell, a panel.
|
|
200
|
+
//
|
|
201
|
+
// `parentOffset === 0` is the whole test on the position side: prosemirror-view paints every
|
|
202
|
+
// widget at a position before the node starting there, so a leading widget at the parent's start
|
|
203
|
+
// always renders above its content.
|
|
204
|
+
var isVisuallyFirstInParent = !placeBelow && $safeInsertPos.parentOffset === 0;
|
|
193
205
|
var isTopLevelInsert = $safeInsertPos.depth === 0;
|
|
194
206
|
var hasPreviousBlock = ((_$safeInsertPos$nodeB = $safeInsertPos.nodeBefore) === null || _$safeInsertPos$nodeB === void 0 ? void 0 : _$safeInsertPos$nodeB.isBlock) === true;
|
|
195
207
|
var isFirstDocHeadingReplacement = (0, _isExtendedEnabled.isExtendedEnabled)(diffType) && !placeBelow && change.fromB === 0 && ((_slice$content$firstC = slice.content.firstChild) === null || _slice$content$firstC === void 0 ? void 0 : _slice$content$firstC.type.name) === 'heading' && ((_newDoc$firstChild = newDoc.firstChild) === null || _newDoc$firstChild === void 0 ? void 0 : _newDoc$firstChild.type.name) === 'heading';
|
|
@@ -241,6 +253,20 @@ var createNodeChangedDecorationWidget = exports.createNodeChangedDecorationWidge
|
|
|
241
253
|
var firstReplacedNode = slice.content.firstChild;
|
|
242
254
|
var lastReplacedNode = slice.content.lastChild;
|
|
243
255
|
var showDiffPatch1 = (0, _fg.fg)('platform_editor_ai_show_diff_patch_1');
|
|
256
|
+
// Whether the slice's outermost textblocks still hold all of their original content.
|
|
257
|
+
//
|
|
258
|
+
// `openStart`/`openEnd` say whether the cut landed inside a block, but not whether it took
|
|
259
|
+
// any text with it — a deletion stopping exactly at a block's content boundary is still
|
|
260
|
+
// reported as open. Resolving the change's own ends separates the two, so a block whose text
|
|
261
|
+
// survived intact can render as a block even though its boundary was open.
|
|
262
|
+
//
|
|
263
|
+
// Both resolves sit behind the gate that consumes them, so this adds no position arithmetic to
|
|
264
|
+
// the ungated path, and an unresolvable position degrades to the `openStart`/`openEnd` test on
|
|
265
|
+
// its own — the block is then treated as partial, which is what it rendered as before the gate.
|
|
266
|
+
var $changeFromA = (0, _fg.fg)('platform_editor_ai_show_diff_patch_2') ? (0, _safeResolve.safeResolve)(doc, change.fromA) : null;
|
|
267
|
+
var $changeToA = (0, _fg.fg)('platform_editor_ai_show_diff_patch_2') ? (0, _safeResolve.safeResolve)(doc, change.toA) : null;
|
|
268
|
+
var isFirstNodeContentComplete = slice.openStart === 0 || ($changeFromA === null || $changeFromA === void 0 ? void 0 : $changeFromA.parentOffset) === 0;
|
|
269
|
+
var isLastNodeContentComplete = slice.openEnd === 0 || $changeToA !== null && $changeToA.parentOffset === $changeToA.parent.content.size;
|
|
244
270
|
var isCompleteSameTypeReplacement = slice.content.childCount === 1 && firstReplacedNode !== null && replacementNode !== null && firstReplacedNode.type === replacementNode.type && replacementNode.nodeSize === change.toB - change.fromB;
|
|
245
271
|
|
|
246
272
|
/*
|
|
@@ -253,10 +279,16 @@ var createNodeChangedDecorationWidget = exports.createNodeChangedDecorationWidge
|
|
|
253
279
|
var isLast = lastReplacedNode === node;
|
|
254
280
|
var isOpenAtSliceBoundary = isFirst && slice.openStart > 0 || isLast && slice.openEnd > 0;
|
|
255
281
|
var shouldPreserveCompleteMultiInlineBlock = showDiffPatch1 && node.isBlock && node.type.inlineContent && node.content.childCount > 1 && !isOpenAtSliceBoundary && isCompleteSameTypeReplacement;
|
|
282
|
+
// A textblock is otherwise serialized as its inline content only, with no `<p>`/`<h2>` wrapper.
|
|
283
|
+
// That is right when the diff cuts into an existing block — the deleted text belongs on the same
|
|
284
|
+
// line as the text that replaced it — but when the whole block went away it drops the very
|
|
285
|
+
// element that carries the block's margin, so the deleted block renders flush against its
|
|
286
|
+
// neighbours. Only the first and last children can be partial; middle ones are always complete.
|
|
287
|
+
var shouldRenderAsBlockNode = (0, _fg.fg)('platform_editor_ai_show_diff_patch_2') && node.isTextblock && (!isFirst || isFirstNodeContentComplete) && (!isLast || isLastNodeContentComplete);
|
|
256
288
|
|
|
257
289
|
// Helper function to handle multiple child nodes
|
|
258
290
|
var handleMultipleChildNodes = function handleMultipleChildNodes(node) {
|
|
259
|
-
if (!shouldPreserveCompleteMultiInlineBlock && node.content.childCount > 1 && node.type.inlineContent) {
|
|
291
|
+
if (!shouldPreserveCompleteMultiInlineBlock && !shouldRenderAsBlockNode && node.content.childCount > 1 && node.type.inlineContent) {
|
|
260
292
|
node.content.forEach(function (childNode) {
|
|
261
293
|
var childNodeView = serializer.tryCreateNodeView(childNode);
|
|
262
294
|
if (childNodeView) {
|
|
@@ -286,7 +318,7 @@ var createNodeChangedDecorationWidget = exports.createNodeChangedDecorationWidge
|
|
|
286
318
|
if (handleMultipleChildNodes(node)) {
|
|
287
319
|
return;
|
|
288
320
|
}
|
|
289
|
-
if (shouldPreserveCompleteMultiInlineBlock) {
|
|
321
|
+
if (shouldPreserveCompleteMultiInlineBlock || shouldRenderAsBlockNode) {
|
|
290
322
|
fallbackSerialization = function fallbackSerialization() {
|
|
291
323
|
return serializer.serializeNode(node);
|
|
292
324
|
};
|
|
@@ -363,6 +395,21 @@ var createNodeChangedDecorationWidget = exports.createNodeChangedDecorationWidge
|
|
|
363
395
|
dom.style.setProperty('scroll-margin-top', _decorationKeys.scrollMarginTopValue);
|
|
364
396
|
}
|
|
365
397
|
|
|
398
|
+
// A block node serialized into the widget is the widget's first child, and the editor's
|
|
399
|
+
// first-child reset is written against the parent element rather than the document position — so
|
|
400
|
+
// it zeroes the margin the block wrapper was kept for in the first place.
|
|
401
|
+
//
|
|
402
|
+
// Only away from the parent's start. There the reset is the correct outcome: the widget is the
|
|
403
|
+
// first thing in the document, column, cell or panel, and a leading gap above it would be wrong.
|
|
404
|
+
// The margin that matters at that position belongs to the node *below* the widget, which the
|
|
405
|
+
// shaped spacer pair supplies instead.
|
|
406
|
+
if ((0, _fg.fg)('platform_editor_ai_show_diff_patch_2') && !isVisuallyFirstInParent) {
|
|
407
|
+
(0, _absorbFirstChildMarginReset.absorbFirstChildMarginReset)({
|
|
408
|
+
dom: dom,
|
|
409
|
+
testId: 'show-diff-widget-margin-absorber'
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
|
|
366
413
|
// Needed even when the indicator bar is off, because a contributor tag also anchors against the
|
|
367
414
|
// widget.
|
|
368
415
|
if ((showIndicators || showContributorTags) && (0, _isExtendedEnabled.isExtendedEnabled)(diffType)) {
|
|
@@ -425,7 +472,40 @@ var createNodeChangedDecorationWidget = exports.createNodeChangedDecorationWidge
|
|
|
425
472
|
var isPureDeletion = change.fromB === change.toB;
|
|
426
473
|
var isSingleBlock = slice.content.childCount === 1 && ((_slice$content$firstC2 = slice.content.firstChild) === null || _slice$content$firstC2 === void 0 ? void 0 : _slice$content$firstC2.isBlock);
|
|
427
474
|
var isDiffWidgetAtStartOfDoc = $safeInsertPos.depth === 0 && $safeInsertPos.index(0) === 0;
|
|
428
|
-
|
|
475
|
+
|
|
476
|
+
// A node at the start of its parent has its top margin reset, so a diff widget rendered above
|
|
477
|
+
// it sits flush against it. This applies wherever that parent is — the document, a layout
|
|
478
|
+
// column, a table cell — so it is decided from the anchor rather than from the document root
|
|
479
|
+
// (see `isVisuallyFirstInParent` above).
|
|
480
|
+
//
|
|
481
|
+
// Away from the parent's start the node below keeps its own margin, and it is the block *inside*
|
|
482
|
+
// the widget that loses one — handled by `absorbFirstChildMarginReset` above, not here.
|
|
483
|
+
var nodeAfterWidget = $safeInsertPos.nodeAfter;
|
|
484
|
+
if ((0, _fg.fg)('platform_editor_ai_show_diff_patch_2') && (0, _isExtendedEnabled.isExtendedEnabled)(diffType) && isVisuallyFirstInParent && nodeAfterWidget) {
|
|
485
|
+
var shapedSpacer = (0, _createNodeShapedMarginSpacer.createNodeShapedMarginSpacer)({
|
|
486
|
+
node: nodeAfterWidget,
|
|
487
|
+
serializer: serializer,
|
|
488
|
+
testId: 'show-diff-shaped-margin-spacer'
|
|
489
|
+
});
|
|
490
|
+
if (shapedSpacer) {
|
|
491
|
+
// Two elements, because the two families of reset have to be handled differently. The
|
|
492
|
+
// absorber takes the document-level adjacent-sibling reset for a leading widget, whose
|
|
493
|
+
// `!important` would otherwise zero the shaped spacer's margin; the shaped spacer, now one
|
|
494
|
+
// position further along, is out of that rule's reach and keeps the margin it supplies.
|
|
495
|
+
// Inside containers the counting selectors skip both, so only the shaped one has an effect.
|
|
496
|
+
//
|
|
497
|
+
// Sides order the widgets against each other only — both still paint before the real node.
|
|
498
|
+
decorations.push(_view.Decoration.widget(safeInsertPos, (0, _createMarginAbsorber.createMarginAbsorber)({
|
|
499
|
+
testId: 'show-diff-margin-absorber'
|
|
500
|
+
}), {
|
|
501
|
+
side: 0
|
|
502
|
+
}));
|
|
503
|
+
decorations.push(_view.Decoration.widget(safeInsertPos, shapedSpacer, {
|
|
504
|
+
side: 1
|
|
505
|
+
}));
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
if (!(0, _fg.fg)('platform_editor_ai_show_diff_patch_2') && isDiffWidgetAtStartOfDoc && isSingleBlock && isPureDeletion && (0, _isExtendedEnabled.isExtendedEnabled)(diffType)) {
|
|
429
509
|
var followingNode = $safeInsertPos.nodeAfter;
|
|
430
510
|
var headingLevel = (followingNode === null || followingNode === void 0 ? void 0 : followingNode.type.name) === 'heading' ? followingNode.attrs.level : undefined;
|
|
431
511
|
if (isHeadingLevel(headingLevel)) {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.absorbFirstChildMarginReset = void 0;
|
|
7
|
+
var _createMarginAbsorber = require("./createMarginAbsorber");
|
|
8
|
+
/**
|
|
9
|
+
* Keeps a block node rendered inside a diff widget from losing its own top margin.
|
|
10
|
+
*
|
|
11
|
+
* The editor's leading-block margin reset is written against the parent element, not the document
|
|
12
|
+
* position, so it fires wherever a text block is a first child — including inside the widget's own
|
|
13
|
+
* `span`, which is what a whole-block deletion renders into. The widget then has no margin of its
|
|
14
|
+
* own, and the block sits flush against whatever is above it, regardless of where in the document
|
|
15
|
+
* the widget landed.
|
|
16
|
+
*
|
|
17
|
+
* The margin is not re-supplied; it is never taken away. Prepending an element the reset does not
|
|
18
|
+
* select moves the real block off the leading position, so the ordinary `.ProseMirror p`,
|
|
19
|
+
* `.ProseMirror h2` and similar rules go on applying to it untouched. That is why this needs no
|
|
20
|
+
* knowledge of which margin the block should have — unlike `createNodeShapedMarginSpacer`, which
|
|
21
|
+
* replicates a margin that has already been zeroed and cannot be read back.
|
|
22
|
+
*
|
|
23
|
+
* Unconditional, because the absorber generates no box: it costs nothing in a widget holding
|
|
24
|
+
* inline content, which the reset was never going to match anyway. Deciding here instead would mean
|
|
25
|
+
* restating the reset's selector list, and a copy of another package's CSS drifts silently.
|
|
26
|
+
*/
|
|
27
|
+
var absorbFirstChildMarginReset = exports.absorbFirstChildMarginReset = function absorbFirstChildMarginReset(_ref) {
|
|
28
|
+
var dom = _ref.dom,
|
|
29
|
+
testId = _ref.testId;
|
|
30
|
+
dom.prepend((0, _createMarginAbsorber.createBoxlessMarginAbsorber)({
|
|
31
|
+
testId: testId
|
|
32
|
+
}));
|
|
33
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.createMarginAbsorber = exports.createBoxlessMarginAbsorber = void 0;
|
|
7
|
+
/**
|
|
8
|
+
* An empty, invisible element whose only job is to occupy a position in the DOM.
|
|
9
|
+
*
|
|
10
|
+
* At the top of the document `firstBlockNodeStyles` zeroes the top margin of whatever element
|
|
11
|
+
* follows a leading `.ProseMirror-widget`, and it does so with `!important` — which no inline style
|
|
12
|
+
* can outrank. This takes that hit so the shaped spacer after it, one position further along, keeps
|
|
13
|
+
* the margin it is there to supply.
|
|
14
|
+
*
|
|
15
|
+
* It contributes no height (no content, border or padding) and margins collapse through it, so it
|
|
16
|
+
* cannot affect layout beyond the selector it absorbs.
|
|
17
|
+
*/
|
|
18
|
+
var createMarginAbsorber = exports.createMarginAbsorber = function createMarginAbsorber(_ref) {
|
|
19
|
+
var testId = _ref.testId;
|
|
20
|
+
// Block-level: an empty inline element takes the match just as well, but collapses to a
|
|
21
|
+
// zero-height line box, and the following margin then lands in the wrong place.
|
|
22
|
+
var absorber = document.createElement('div');
|
|
23
|
+
absorber.dataset.testid = testId;
|
|
24
|
+
absorber.setAttribute('aria-hidden', 'true');
|
|
25
|
+
absorber.contentEditable = 'false';
|
|
26
|
+
return absorber;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* An absorber that occupies a DOM position without generating a box.
|
|
31
|
+
*
|
|
32
|
+
* Not interchangeable with `createMarginAbsorber`: a rule that selects the *rendered* sibling of a
|
|
33
|
+
* leading widget needs an element that generates a box, and this one does not.
|
|
34
|
+
*/
|
|
35
|
+
var createBoxlessMarginAbsorber = exports.createBoxlessMarginAbsorber = function createBoxlessMarginAbsorber(_ref2) {
|
|
36
|
+
var testId = _ref2.testId;
|
|
37
|
+
var absorber = document.createElement('div');
|
|
38
|
+
absorber.dataset.testid = testId;
|
|
39
|
+
absorber.setAttribute('aria-hidden', 'true');
|
|
40
|
+
absorber.contentEditable = 'false';
|
|
41
|
+
absorber.style.display = 'contents';
|
|
42
|
+
return absorber;
|
|
43
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.createNodeShapedMarginSpacer = void 0;
|
|
7
|
+
/**
|
|
8
|
+
* Node types that can safely be reduced to an empty shell.
|
|
9
|
+
*
|
|
10
|
+
* A shell only replicates a margin if it collapses to nothing itself, which needs zero height and
|
|
11
|
+
* no vertical border or padding. Textblocks and lists qualify; nodes with intrinsic sizing (tables
|
|
12
|
+
* are `display: table`, media has a measured height) or a painted box (a zero-height panel is a
|
|
13
|
+
* visible stripe of background) do not, and are left alone.
|
|
14
|
+
*/
|
|
15
|
+
var isShapeableNode = function isShapeableNode(node) {
|
|
16
|
+
return node.isTextblock || ['bulletList', 'orderedList'].includes(node.type.name);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* An invisible element shaped like `node`, used to supply the top margin `node` no longer gets.
|
|
21
|
+
*
|
|
22
|
+
* A node at the start of its parent has its top margin reset, so a diff widget rendered above it
|
|
23
|
+
* sits flush against it. The reset cannot simply be taken off the node: the container variants skip
|
|
24
|
+
* widgets when counting — `nth-child(1 of :not(style, .ProseMirror-gapcursor, .ProseMirror-widget,
|
|
25
|
+
* span))` in layout columns, expands, sync blocks and table cells — so no amount of extra elements
|
|
26
|
+
* moves the match. That exclusion is what makes this work instead: the spacer is a
|
|
27
|
+
* `.ProseMirror-widget`, so it is never the counted first child and keeps its own margin, while the
|
|
28
|
+
* real node goes on taking the reset.
|
|
29
|
+
*
|
|
30
|
+
* The margin is not measured. Reading it off the real node is impossible — by then the reset has
|
|
31
|
+
* set it to `0`, and both `firstBlockNodeStyles` (`!important`) and block controls' `firstNodeDec`
|
|
32
|
+
* (an inline style) make it unrecoverable. Instead the spacer carries the node's own tag, classes
|
|
33
|
+
* and attributes, so the same rules that would have given the node its margin match the spacer
|
|
34
|
+
* (`.ProseMirror p`, `.ProseMirror h2`, the root-list rule, and so on).
|
|
35
|
+
*
|
|
36
|
+
* Returns `null` when the node is not safely shapeable or serialization fails.
|
|
37
|
+
*/
|
|
38
|
+
var createNodeShapedMarginSpacer = exports.createNodeShapedMarginSpacer = function createNodeShapedMarginSpacer(_ref) {
|
|
39
|
+
var _node$type$createAndF;
|
|
40
|
+
var node = _ref.node,
|
|
41
|
+
serializer = _ref.serializer,
|
|
42
|
+
testId = _ref.testId;
|
|
43
|
+
if (!isShapeableNode(node)) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// An empty node of the same type serializes to the same shell without walking real content.
|
|
48
|
+
var shellNode = (_node$type$createAndF = node.type.createAndFill(node.attrs)) !== null && _node$type$createAndF !== void 0 ? _node$type$createAndF : node;
|
|
49
|
+
var serialized = serializer.serializeNode(shellNode);
|
|
50
|
+
if (!(serialized instanceof HTMLElement)) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Keep the tag, classes and attributes the margin rules select on; drop anything that could
|
|
55
|
+
// occupy a line box.
|
|
56
|
+
serialized.replaceChildren();
|
|
57
|
+
serialized.dataset.testid = testId;
|
|
58
|
+
serialized.setAttribute('aria-hidden', 'true');
|
|
59
|
+
serialized.contentEditable = 'false';
|
|
60
|
+
|
|
61
|
+
// Zero height with no vertical border or padding keeps the spacer self-collapsing: its margins
|
|
62
|
+
// stay adjoining, so they collapse with the neighbours into a single margin equal to the largest
|
|
63
|
+
// rather than adding to them. That is what makes this safe in containers that never reset the
|
|
64
|
+
// first child — the spacer cannot double the gap there. Deliberately no `overflow: hidden`, which
|
|
65
|
+
// would open a block formatting context and stop that collapsing.
|
|
66
|
+
serialized.style.height = '0';
|
|
67
|
+
serialized.style.minHeight = '0';
|
|
68
|
+
serialized.style.paddingTop = '0';
|
|
69
|
+
serialized.style.paddingBottom = '0';
|
|
70
|
+
serialized.style.borderTopWidth = '0';
|
|
71
|
+
serialized.style.borderBottomWidth = '0';
|
|
72
|
+
// Only the top margin is being replicated. Left as-is, a larger bottom margin would win the
|
|
73
|
+
// self-collapse and overshoot the gap.
|
|
74
|
+
serialized.style.marginBottom = '0';
|
|
75
|
+
return serialized;
|
|
76
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.safeResolve = void 0;
|
|
7
|
+
/**
|
|
8
|
+
* Resolves a document position, or returns `null` if it cannot be resolved.
|
|
9
|
+
*
|
|
10
|
+
* `Node.resolve` throws a `RangeError` for a position outside the document. Diff decorations are
|
|
11
|
+
* built from change ranges computed against a document that may since have moved on, so a position
|
|
12
|
+
* arriving out of range is a normal outcome rather than a defect — but an exception escaping here
|
|
13
|
+
* takes down the whole decoration set, leaving the diff unrendered.
|
|
14
|
+
*
|
|
15
|
+
* Callers are expected to treat `null` as "cannot tell" and fall back to behaviour that does not
|
|
16
|
+
* need the resolved position.
|
|
17
|
+
*/
|
|
18
|
+
var safeResolve = exports.safeResolve = function safeResolve(doc, pos) {
|
|
19
|
+
try {
|
|
20
|
+
return doc.resolve(pos);
|
|
21
|
+
} catch (_unused) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
@@ -332,11 +332,11 @@ var ContributorTagController = exports.ContributorTagController = /*#__PURE__*/f
|
|
|
332
332
|
|
|
333
333
|
// Hand-rolled avatar stack: `@atlaskit/avatar-group` cannot render below 24px and the tag uses
|
|
334
334
|
// 16px avatars.
|
|
335
|
-
var stack = model.connectedContributor ? [].concat((0, _toConsumableArray2.default)(
|
|
336
|
-
contributor: user,
|
|
337
|
-
stackIndex: 1
|
|
338
|
-
}] : []), (0, _toConsumableArray2.default)(agent ? [{
|
|
335
|
+
var stack = model.connectedContributor ? [].concat((0, _toConsumableArray2.default)(agent ? [{
|
|
339
336
|
contributor: agent,
|
|
337
|
+
stackIndex: 1
|
|
338
|
+
}] : []), (0, _toConsumableArray2.default)(user ? [{
|
|
339
|
+
contributor: user,
|
|
340
340
|
stackIndex: 0
|
|
341
341
|
}] : [])) : [{
|
|
342
342
|
contributor: model.contributor
|
|
@@ -6,7 +6,11 @@ import { createLeftAnchorWidget } from './createAnchorDecorationWidgets';
|
|
|
6
6
|
import { createChangedRowDecorationWidgets } from './createChangedRowDecorationWidgets';
|
|
7
7
|
import { createContributorTagHost, unmountContributorTag } from './createContributorTagWidget';
|
|
8
8
|
import { buildDiffDecorationSpec, buildAnchorDecorationKey, scrollMarginTopValue } from './decorationKeys';
|
|
9
|
+
import { absorbFirstChildMarginReset } from './utils/absorbFirstChildMarginReset';
|
|
10
|
+
import { createMarginAbsorber } from './utils/createMarginAbsorber';
|
|
11
|
+
import { createNodeShapedMarginSpacer } from './utils/createNodeShapedMarginSpacer';
|
|
9
12
|
import { findSafeInsertPos } from './utils/findSafeInsertPos';
|
|
13
|
+
import { safeResolve } from './utils/safeResolve';
|
|
10
14
|
import { wrapBlockNodeView, injectInnerWrapper, createContentWrapper } from './utils/wrapBlockNodeView';
|
|
11
15
|
const isHeadingLevel = level => typeof level === 'number' && level >= 1 && level <= 6;
|
|
12
16
|
|
|
@@ -174,6 +178,14 @@ export const createNodeChangedDecorationWidget = ({
|
|
|
174
178
|
// For non-table content, use the existing span wrapper approach
|
|
175
179
|
const dom = document.createElement('span');
|
|
176
180
|
const $safeInsertPos = newDoc.resolve(safeInsertPos);
|
|
181
|
+
|
|
182
|
+
// Whether the widget renders above the very start of its parent's content — the document, a
|
|
183
|
+
// layout column, a table cell, a panel.
|
|
184
|
+
//
|
|
185
|
+
// `parentOffset === 0` is the whole test on the position side: prosemirror-view paints every
|
|
186
|
+
// widget at a position before the node starting there, so a leading widget at the parent's start
|
|
187
|
+
// always renders above its content.
|
|
188
|
+
const isVisuallyFirstInParent = !placeBelow && $safeInsertPos.parentOffset === 0;
|
|
177
189
|
const isTopLevelInsert = $safeInsertPos.depth === 0;
|
|
178
190
|
const hasPreviousBlock = ((_$safeInsertPos$nodeB = $safeInsertPos.nodeBefore) === null || _$safeInsertPos$nodeB === void 0 ? void 0 : _$safeInsertPos$nodeB.isBlock) === true;
|
|
179
191
|
const isFirstDocHeadingReplacement = isExtendedEnabled(diffType) && !placeBelow && change.fromB === 0 && ((_slice$content$firstC = slice.content.firstChild) === null || _slice$content$firstC === void 0 ? void 0 : _slice$content$firstC.type.name) === 'heading' && ((_newDoc$firstChild = newDoc.firstChild) === null || _newDoc$firstChild === void 0 ? void 0 : _newDoc$firstChild.type.name) === 'heading';
|
|
@@ -225,6 +237,20 @@ export const createNodeChangedDecorationWidget = ({
|
|
|
225
237
|
const firstReplacedNode = slice.content.firstChild;
|
|
226
238
|
const lastReplacedNode = slice.content.lastChild;
|
|
227
239
|
const showDiffPatch1 = fg('platform_editor_ai_show_diff_patch_1');
|
|
240
|
+
// Whether the slice's outermost textblocks still hold all of their original content.
|
|
241
|
+
//
|
|
242
|
+
// `openStart`/`openEnd` say whether the cut landed inside a block, but not whether it took
|
|
243
|
+
// any text with it — a deletion stopping exactly at a block's content boundary is still
|
|
244
|
+
// reported as open. Resolving the change's own ends separates the two, so a block whose text
|
|
245
|
+
// survived intact can render as a block even though its boundary was open.
|
|
246
|
+
//
|
|
247
|
+
// Both resolves sit behind the gate that consumes them, so this adds no position arithmetic to
|
|
248
|
+
// the ungated path, and an unresolvable position degrades to the `openStart`/`openEnd` test on
|
|
249
|
+
// its own — the block is then treated as partial, which is what it rendered as before the gate.
|
|
250
|
+
const $changeFromA = fg('platform_editor_ai_show_diff_patch_2') ? safeResolve(doc, change.fromA) : null;
|
|
251
|
+
const $changeToA = fg('platform_editor_ai_show_diff_patch_2') ? safeResolve(doc, change.toA) : null;
|
|
252
|
+
const isFirstNodeContentComplete = slice.openStart === 0 || ($changeFromA === null || $changeFromA === void 0 ? void 0 : $changeFromA.parentOffset) === 0;
|
|
253
|
+
const isLastNodeContentComplete = slice.openEnd === 0 || $changeToA !== null && $changeToA.parentOffset === $changeToA.parent.content.size;
|
|
228
254
|
const isCompleteSameTypeReplacement = slice.content.childCount === 1 && firstReplacedNode !== null && replacementNode !== null && firstReplacedNode.type === replacementNode.type && replacementNode.nodeSize === change.toB - change.fromB;
|
|
229
255
|
|
|
230
256
|
/*
|
|
@@ -237,10 +263,16 @@ export const createNodeChangedDecorationWidget = ({
|
|
|
237
263
|
const isLast = lastReplacedNode === node;
|
|
238
264
|
const isOpenAtSliceBoundary = isFirst && slice.openStart > 0 || isLast && slice.openEnd > 0;
|
|
239
265
|
const shouldPreserveCompleteMultiInlineBlock = showDiffPatch1 && node.isBlock && node.type.inlineContent && node.content.childCount > 1 && !isOpenAtSliceBoundary && isCompleteSameTypeReplacement;
|
|
266
|
+
// A textblock is otherwise serialized as its inline content only, with no `<p>`/`<h2>` wrapper.
|
|
267
|
+
// That is right when the diff cuts into an existing block — the deleted text belongs on the same
|
|
268
|
+
// line as the text that replaced it — but when the whole block went away it drops the very
|
|
269
|
+
// element that carries the block's margin, so the deleted block renders flush against its
|
|
270
|
+
// neighbours. Only the first and last children can be partial; middle ones are always complete.
|
|
271
|
+
const shouldRenderAsBlockNode = fg('platform_editor_ai_show_diff_patch_2') && node.isTextblock && (!isFirst || isFirstNodeContentComplete) && (!isLast || isLastNodeContentComplete);
|
|
240
272
|
|
|
241
273
|
// Helper function to handle multiple child nodes
|
|
242
274
|
const handleMultipleChildNodes = node => {
|
|
243
|
-
if (!shouldPreserveCompleteMultiInlineBlock && node.content.childCount > 1 && node.type.inlineContent) {
|
|
275
|
+
if (!shouldPreserveCompleteMultiInlineBlock && !shouldRenderAsBlockNode && node.content.childCount > 1 && node.type.inlineContent) {
|
|
244
276
|
node.content.forEach(childNode => {
|
|
245
277
|
const childNodeView = serializer.tryCreateNodeView(childNode);
|
|
246
278
|
if (childNodeView) {
|
|
@@ -270,7 +302,7 @@ export const createNodeChangedDecorationWidget = ({
|
|
|
270
302
|
if (handleMultipleChildNodes(node)) {
|
|
271
303
|
return;
|
|
272
304
|
}
|
|
273
|
-
if (shouldPreserveCompleteMultiInlineBlock) {
|
|
305
|
+
if (shouldPreserveCompleteMultiInlineBlock || shouldRenderAsBlockNode) {
|
|
274
306
|
fallbackSerialization = () => serializer.serializeNode(node);
|
|
275
307
|
} else if ((isFirst || isLast && slice.content.childCount > 2) && hasInlineContent) {
|
|
276
308
|
fallbackSerialization = () => serializer.serializeFragment(node.content);
|
|
@@ -341,6 +373,21 @@ export const createNodeChangedDecorationWidget = ({
|
|
|
341
373
|
dom.style.setProperty('scroll-margin-top', scrollMarginTopValue);
|
|
342
374
|
}
|
|
343
375
|
|
|
376
|
+
// A block node serialized into the widget is the widget's first child, and the editor's
|
|
377
|
+
// first-child reset is written against the parent element rather than the document position — so
|
|
378
|
+
// it zeroes the margin the block wrapper was kept for in the first place.
|
|
379
|
+
//
|
|
380
|
+
// Only away from the parent's start. There the reset is the correct outcome: the widget is the
|
|
381
|
+
// first thing in the document, column, cell or panel, and a leading gap above it would be wrong.
|
|
382
|
+
// The margin that matters at that position belongs to the node *below* the widget, which the
|
|
383
|
+
// shaped spacer pair supplies instead.
|
|
384
|
+
if (fg('platform_editor_ai_show_diff_patch_2') && !isVisuallyFirstInParent) {
|
|
385
|
+
absorbFirstChildMarginReset({
|
|
386
|
+
dom,
|
|
387
|
+
testId: 'show-diff-widget-margin-absorber'
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
344
391
|
// Needed even when the indicator bar is off, because a contributor tag also anchors against the
|
|
345
392
|
// widget.
|
|
346
393
|
if ((showIndicators || showContributorTags) && isExtendedEnabled(diffType)) {
|
|
@@ -410,7 +457,40 @@ export const createNodeChangedDecorationWidget = ({
|
|
|
410
457
|
const isPureDeletion = change.fromB === change.toB;
|
|
411
458
|
const isSingleBlock = slice.content.childCount === 1 && ((_slice$content$firstC2 = slice.content.firstChild) === null || _slice$content$firstC2 === void 0 ? void 0 : _slice$content$firstC2.isBlock);
|
|
412
459
|
const isDiffWidgetAtStartOfDoc = $safeInsertPos.depth === 0 && $safeInsertPos.index(0) === 0;
|
|
413
|
-
|
|
460
|
+
|
|
461
|
+
// A node at the start of its parent has its top margin reset, so a diff widget rendered above
|
|
462
|
+
// it sits flush against it. This applies wherever that parent is — the document, a layout
|
|
463
|
+
// column, a table cell — so it is decided from the anchor rather than from the document root
|
|
464
|
+
// (see `isVisuallyFirstInParent` above).
|
|
465
|
+
//
|
|
466
|
+
// Away from the parent's start the node below keeps its own margin, and it is the block *inside*
|
|
467
|
+
// the widget that loses one — handled by `absorbFirstChildMarginReset` above, not here.
|
|
468
|
+
const nodeAfterWidget = $safeInsertPos.nodeAfter;
|
|
469
|
+
if (fg('platform_editor_ai_show_diff_patch_2') && isExtendedEnabled(diffType) && isVisuallyFirstInParent && nodeAfterWidget) {
|
|
470
|
+
const shapedSpacer = createNodeShapedMarginSpacer({
|
|
471
|
+
node: nodeAfterWidget,
|
|
472
|
+
serializer,
|
|
473
|
+
testId: 'show-diff-shaped-margin-spacer'
|
|
474
|
+
});
|
|
475
|
+
if (shapedSpacer) {
|
|
476
|
+
// Two elements, because the two families of reset have to be handled differently. The
|
|
477
|
+
// absorber takes the document-level adjacent-sibling reset for a leading widget, whose
|
|
478
|
+
// `!important` would otherwise zero the shaped spacer's margin; the shaped spacer, now one
|
|
479
|
+
// position further along, is out of that rule's reach and keeps the margin it supplies.
|
|
480
|
+
// Inside containers the counting selectors skip both, so only the shaped one has an effect.
|
|
481
|
+
//
|
|
482
|
+
// Sides order the widgets against each other only — both still paint before the real node.
|
|
483
|
+
decorations.push(Decoration.widget(safeInsertPos, createMarginAbsorber({
|
|
484
|
+
testId: 'show-diff-margin-absorber'
|
|
485
|
+
}), {
|
|
486
|
+
side: 0
|
|
487
|
+
}));
|
|
488
|
+
decorations.push(Decoration.widget(safeInsertPos, shapedSpacer, {
|
|
489
|
+
side: 1
|
|
490
|
+
}));
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
if (!fg('platform_editor_ai_show_diff_patch_2') && isDiffWidgetAtStartOfDoc && isSingleBlock && isPureDeletion && isExtendedEnabled(diffType)) {
|
|
414
494
|
const followingNode = $safeInsertPos.nodeAfter;
|
|
415
495
|
const headingLevel = (followingNode === null || followingNode === void 0 ? void 0 : followingNode.type.name) === 'heading' ? followingNode.attrs.level : undefined;
|
|
416
496
|
if (isHeadingLevel(headingLevel)) {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { createBoxlessMarginAbsorber } from './createMarginAbsorber';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Keeps a block node rendered inside a diff widget from losing its own top margin.
|
|
5
|
+
*
|
|
6
|
+
* The editor's leading-block margin reset is written against the parent element, not the document
|
|
7
|
+
* position, so it fires wherever a text block is a first child — including inside the widget's own
|
|
8
|
+
* `span`, which is what a whole-block deletion renders into. The widget then has no margin of its
|
|
9
|
+
* own, and the block sits flush against whatever is above it, regardless of where in the document
|
|
10
|
+
* the widget landed.
|
|
11
|
+
*
|
|
12
|
+
* The margin is not re-supplied; it is never taken away. Prepending an element the reset does not
|
|
13
|
+
* select moves the real block off the leading position, so the ordinary `.ProseMirror p`,
|
|
14
|
+
* `.ProseMirror h2` and similar rules go on applying to it untouched. That is why this needs no
|
|
15
|
+
* knowledge of which margin the block should have — unlike `createNodeShapedMarginSpacer`, which
|
|
16
|
+
* replicates a margin that has already been zeroed and cannot be read back.
|
|
17
|
+
*
|
|
18
|
+
* Unconditional, because the absorber generates no box: it costs nothing in a widget holding
|
|
19
|
+
* inline content, which the reset was never going to match anyway. Deciding here instead would mean
|
|
20
|
+
* restating the reset's selector list, and a copy of another package's CSS drifts silently.
|
|
21
|
+
*/
|
|
22
|
+
export const absorbFirstChildMarginReset = ({
|
|
23
|
+
dom,
|
|
24
|
+
testId
|
|
25
|
+
}) => {
|
|
26
|
+
dom.prepend(createBoxlessMarginAbsorber({
|
|
27
|
+
testId
|
|
28
|
+
}));
|
|
29
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An empty, invisible element whose only job is to occupy a position in the DOM.
|
|
3
|
+
*
|
|
4
|
+
* At the top of the document `firstBlockNodeStyles` zeroes the top margin of whatever element
|
|
5
|
+
* follows a leading `.ProseMirror-widget`, and it does so with `!important` — which no inline style
|
|
6
|
+
* can outrank. This takes that hit so the shaped spacer after it, one position further along, keeps
|
|
7
|
+
* the margin it is there to supply.
|
|
8
|
+
*
|
|
9
|
+
* It contributes no height (no content, border or padding) and margins collapse through it, so it
|
|
10
|
+
* cannot affect layout beyond the selector it absorbs.
|
|
11
|
+
*/
|
|
12
|
+
export const createMarginAbsorber = ({
|
|
13
|
+
testId
|
|
14
|
+
}) => {
|
|
15
|
+
// Block-level: an empty inline element takes the match just as well, but collapses to a
|
|
16
|
+
// zero-height line box, and the following margin then lands in the wrong place.
|
|
17
|
+
const absorber = document.createElement('div');
|
|
18
|
+
absorber.dataset.testid = testId;
|
|
19
|
+
absorber.setAttribute('aria-hidden', 'true');
|
|
20
|
+
absorber.contentEditable = 'false';
|
|
21
|
+
return absorber;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* An absorber that occupies a DOM position without generating a box.
|
|
26
|
+
*
|
|
27
|
+
* Not interchangeable with `createMarginAbsorber`: a rule that selects the *rendered* sibling of a
|
|
28
|
+
* leading widget needs an element that generates a box, and this one does not.
|
|
29
|
+
*/
|
|
30
|
+
export const createBoxlessMarginAbsorber = ({
|
|
31
|
+
testId
|
|
32
|
+
}) => {
|
|
33
|
+
const absorber = document.createElement('div');
|
|
34
|
+
absorber.dataset.testid = testId;
|
|
35
|
+
absorber.setAttribute('aria-hidden', 'true');
|
|
36
|
+
absorber.contentEditable = 'false';
|
|
37
|
+
absorber.style.display = 'contents';
|
|
38
|
+
return absorber;
|
|
39
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node types that can safely be reduced to an empty shell.
|
|
3
|
+
*
|
|
4
|
+
* A shell only replicates a margin if it collapses to nothing itself, which needs zero height and
|
|
5
|
+
* no vertical border or padding. Textblocks and lists qualify; nodes with intrinsic sizing (tables
|
|
6
|
+
* are `display: table`, media has a measured height) or a painted box (a zero-height panel is a
|
|
7
|
+
* visible stripe of background) do not, and are left alone.
|
|
8
|
+
*/
|
|
9
|
+
const isShapeableNode = node => node.isTextblock || ['bulletList', 'orderedList'].includes(node.type.name);
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* An invisible element shaped like `node`, used to supply the top margin `node` no longer gets.
|
|
13
|
+
*
|
|
14
|
+
* A node at the start of its parent has its top margin reset, so a diff widget rendered above it
|
|
15
|
+
* sits flush against it. The reset cannot simply be taken off the node: the container variants skip
|
|
16
|
+
* widgets when counting — `nth-child(1 of :not(style, .ProseMirror-gapcursor, .ProseMirror-widget,
|
|
17
|
+
* span))` in layout columns, expands, sync blocks and table cells — so no amount of extra elements
|
|
18
|
+
* moves the match. That exclusion is what makes this work instead: the spacer is a
|
|
19
|
+
* `.ProseMirror-widget`, so it is never the counted first child and keeps its own margin, while the
|
|
20
|
+
* real node goes on taking the reset.
|
|
21
|
+
*
|
|
22
|
+
* The margin is not measured. Reading it off the real node is impossible — by then the reset has
|
|
23
|
+
* set it to `0`, and both `firstBlockNodeStyles` (`!important`) and block controls' `firstNodeDec`
|
|
24
|
+
* (an inline style) make it unrecoverable. Instead the spacer carries the node's own tag, classes
|
|
25
|
+
* and attributes, so the same rules that would have given the node its margin match the spacer
|
|
26
|
+
* (`.ProseMirror p`, `.ProseMirror h2`, the root-list rule, and so on).
|
|
27
|
+
*
|
|
28
|
+
* Returns `null` when the node is not safely shapeable or serialization fails.
|
|
29
|
+
*/
|
|
30
|
+
export const createNodeShapedMarginSpacer = ({
|
|
31
|
+
node,
|
|
32
|
+
serializer,
|
|
33
|
+
testId
|
|
34
|
+
}) => {
|
|
35
|
+
var _node$type$createAndF;
|
|
36
|
+
if (!isShapeableNode(node)) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// An empty node of the same type serializes to the same shell without walking real content.
|
|
41
|
+
const shellNode = (_node$type$createAndF = node.type.createAndFill(node.attrs)) !== null && _node$type$createAndF !== void 0 ? _node$type$createAndF : node;
|
|
42
|
+
const serialized = serializer.serializeNode(shellNode);
|
|
43
|
+
if (!(serialized instanceof HTMLElement)) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Keep the tag, classes and attributes the margin rules select on; drop anything that could
|
|
48
|
+
// occupy a line box.
|
|
49
|
+
serialized.replaceChildren();
|
|
50
|
+
serialized.dataset.testid = testId;
|
|
51
|
+
serialized.setAttribute('aria-hidden', 'true');
|
|
52
|
+
serialized.contentEditable = 'false';
|
|
53
|
+
|
|
54
|
+
// Zero height with no vertical border or padding keeps the spacer self-collapsing: its margins
|
|
55
|
+
// stay adjoining, so they collapse with the neighbours into a single margin equal to the largest
|
|
56
|
+
// rather than adding to them. That is what makes this safe in containers that never reset the
|
|
57
|
+
// first child — the spacer cannot double the gap there. Deliberately no `overflow: hidden`, which
|
|
58
|
+
// would open a block formatting context and stop that collapsing.
|
|
59
|
+
serialized.style.height = '0';
|
|
60
|
+
serialized.style.minHeight = '0';
|
|
61
|
+
serialized.style.paddingTop = '0';
|
|
62
|
+
serialized.style.paddingBottom = '0';
|
|
63
|
+
serialized.style.borderTopWidth = '0';
|
|
64
|
+
serialized.style.borderBottomWidth = '0';
|
|
65
|
+
// Only the top margin is being replicated. Left as-is, a larger bottom margin would win the
|
|
66
|
+
// self-collapse and overshoot the gap.
|
|
67
|
+
serialized.style.marginBottom = '0';
|
|
68
|
+
return serialized;
|
|
69
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves a document position, or returns `null` if it cannot be resolved.
|
|
3
|
+
*
|
|
4
|
+
* `Node.resolve` throws a `RangeError` for a position outside the document. Diff decorations are
|
|
5
|
+
* built from change ranges computed against a document that may since have moved on, so a position
|
|
6
|
+
* arriving out of range is a normal outcome rather than a defect — but an exception escaping here
|
|
7
|
+
* takes down the whole decoration set, leaving the diff unrendered.
|
|
8
|
+
*
|
|
9
|
+
* Callers are expected to treat `null` as "cannot tell" and fall back to behaviour that does not
|
|
10
|
+
* need the resolved position.
|
|
11
|
+
*/
|
|
12
|
+
export const safeResolve = (doc, pos) => {
|
|
13
|
+
try {
|
|
14
|
+
return doc.resolve(pos);
|
|
15
|
+
} catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
@@ -290,11 +290,11 @@ export class ContributorTagController {
|
|
|
290
290
|
|
|
291
291
|
// Hand-rolled avatar stack: `@atlaskit/avatar-group` cannot render below 24px and the tag uses
|
|
292
292
|
// 16px avatars.
|
|
293
|
-
const stack = model.connectedContributor ? [...(
|
|
294
|
-
contributor: user,
|
|
295
|
-
stackIndex: 1
|
|
296
|
-
}] : []), ...(agent ? [{
|
|
293
|
+
const stack = model.connectedContributor ? [...(agent ? [{
|
|
297
294
|
contributor: agent,
|
|
295
|
+
stackIndex: 1
|
|
296
|
+
}] : []), ...(user ? [{
|
|
297
|
+
contributor: user,
|
|
298
298
|
stackIndex: 0
|
|
299
299
|
}] : [])] : [{
|
|
300
300
|
contributor: model.contributor
|
|
@@ -9,7 +9,11 @@ import { createLeftAnchorWidget } from './createAnchorDecorationWidgets';
|
|
|
9
9
|
import { createChangedRowDecorationWidgets } from './createChangedRowDecorationWidgets';
|
|
10
10
|
import { createContributorTagHost, unmountContributorTag } from './createContributorTagWidget';
|
|
11
11
|
import { buildDiffDecorationSpec, buildAnchorDecorationKey, scrollMarginTopValue } from './decorationKeys';
|
|
12
|
+
import { absorbFirstChildMarginReset } from './utils/absorbFirstChildMarginReset';
|
|
13
|
+
import { createMarginAbsorber } from './utils/createMarginAbsorber';
|
|
14
|
+
import { createNodeShapedMarginSpacer } from './utils/createNodeShapedMarginSpacer';
|
|
12
15
|
import { findSafeInsertPos } from './utils/findSafeInsertPos';
|
|
16
|
+
import { safeResolve } from './utils/safeResolve';
|
|
13
17
|
import { wrapBlockNodeView, injectInnerWrapper, createContentWrapper } from './utils/wrapBlockNodeView';
|
|
14
18
|
var isHeadingLevel = function isHeadingLevel(level) {
|
|
15
19
|
return typeof level === 'number' && level >= 1 && level <= 6;
|
|
@@ -183,6 +187,14 @@ export var createNodeChangedDecorationWidget = function createNodeChangedDecorat
|
|
|
183
187
|
// For non-table content, use the existing span wrapper approach
|
|
184
188
|
var dom = document.createElement('span');
|
|
185
189
|
var $safeInsertPos = newDoc.resolve(safeInsertPos);
|
|
190
|
+
|
|
191
|
+
// Whether the widget renders above the very start of its parent's content — the document, a
|
|
192
|
+
// layout column, a table cell, a panel.
|
|
193
|
+
//
|
|
194
|
+
// `parentOffset === 0` is the whole test on the position side: prosemirror-view paints every
|
|
195
|
+
// widget at a position before the node starting there, so a leading widget at the parent's start
|
|
196
|
+
// always renders above its content.
|
|
197
|
+
var isVisuallyFirstInParent = !placeBelow && $safeInsertPos.parentOffset === 0;
|
|
186
198
|
var isTopLevelInsert = $safeInsertPos.depth === 0;
|
|
187
199
|
var hasPreviousBlock = ((_$safeInsertPos$nodeB = $safeInsertPos.nodeBefore) === null || _$safeInsertPos$nodeB === void 0 ? void 0 : _$safeInsertPos$nodeB.isBlock) === true;
|
|
188
200
|
var isFirstDocHeadingReplacement = isExtendedEnabled(diffType) && !placeBelow && change.fromB === 0 && ((_slice$content$firstC = slice.content.firstChild) === null || _slice$content$firstC === void 0 ? void 0 : _slice$content$firstC.type.name) === 'heading' && ((_newDoc$firstChild = newDoc.firstChild) === null || _newDoc$firstChild === void 0 ? void 0 : _newDoc$firstChild.type.name) === 'heading';
|
|
@@ -234,6 +246,20 @@ export var createNodeChangedDecorationWidget = function createNodeChangedDecorat
|
|
|
234
246
|
var firstReplacedNode = slice.content.firstChild;
|
|
235
247
|
var lastReplacedNode = slice.content.lastChild;
|
|
236
248
|
var showDiffPatch1 = fg('platform_editor_ai_show_diff_patch_1');
|
|
249
|
+
// Whether the slice's outermost textblocks still hold all of their original content.
|
|
250
|
+
//
|
|
251
|
+
// `openStart`/`openEnd` say whether the cut landed inside a block, but not whether it took
|
|
252
|
+
// any text with it — a deletion stopping exactly at a block's content boundary is still
|
|
253
|
+
// reported as open. Resolving the change's own ends separates the two, so a block whose text
|
|
254
|
+
// survived intact can render as a block even though its boundary was open.
|
|
255
|
+
//
|
|
256
|
+
// Both resolves sit behind the gate that consumes them, so this adds no position arithmetic to
|
|
257
|
+
// the ungated path, and an unresolvable position degrades to the `openStart`/`openEnd` test on
|
|
258
|
+
// its own — the block is then treated as partial, which is what it rendered as before the gate.
|
|
259
|
+
var $changeFromA = fg('platform_editor_ai_show_diff_patch_2') ? safeResolve(doc, change.fromA) : null;
|
|
260
|
+
var $changeToA = fg('platform_editor_ai_show_diff_patch_2') ? safeResolve(doc, change.toA) : null;
|
|
261
|
+
var isFirstNodeContentComplete = slice.openStart === 0 || ($changeFromA === null || $changeFromA === void 0 ? void 0 : $changeFromA.parentOffset) === 0;
|
|
262
|
+
var isLastNodeContentComplete = slice.openEnd === 0 || $changeToA !== null && $changeToA.parentOffset === $changeToA.parent.content.size;
|
|
237
263
|
var isCompleteSameTypeReplacement = slice.content.childCount === 1 && firstReplacedNode !== null && replacementNode !== null && firstReplacedNode.type === replacementNode.type && replacementNode.nodeSize === change.toB - change.fromB;
|
|
238
264
|
|
|
239
265
|
/*
|
|
@@ -246,10 +272,16 @@ export var createNodeChangedDecorationWidget = function createNodeChangedDecorat
|
|
|
246
272
|
var isLast = lastReplacedNode === node;
|
|
247
273
|
var isOpenAtSliceBoundary = isFirst && slice.openStart > 0 || isLast && slice.openEnd > 0;
|
|
248
274
|
var shouldPreserveCompleteMultiInlineBlock = showDiffPatch1 && node.isBlock && node.type.inlineContent && node.content.childCount > 1 && !isOpenAtSliceBoundary && isCompleteSameTypeReplacement;
|
|
275
|
+
// A textblock is otherwise serialized as its inline content only, with no `<p>`/`<h2>` wrapper.
|
|
276
|
+
// That is right when the diff cuts into an existing block — the deleted text belongs on the same
|
|
277
|
+
// line as the text that replaced it — but when the whole block went away it drops the very
|
|
278
|
+
// element that carries the block's margin, so the deleted block renders flush against its
|
|
279
|
+
// neighbours. Only the first and last children can be partial; middle ones are always complete.
|
|
280
|
+
var shouldRenderAsBlockNode = fg('platform_editor_ai_show_diff_patch_2') && node.isTextblock && (!isFirst || isFirstNodeContentComplete) && (!isLast || isLastNodeContentComplete);
|
|
249
281
|
|
|
250
282
|
// Helper function to handle multiple child nodes
|
|
251
283
|
var handleMultipleChildNodes = function handleMultipleChildNodes(node) {
|
|
252
|
-
if (!shouldPreserveCompleteMultiInlineBlock && node.content.childCount > 1 && node.type.inlineContent) {
|
|
284
|
+
if (!shouldPreserveCompleteMultiInlineBlock && !shouldRenderAsBlockNode && node.content.childCount > 1 && node.type.inlineContent) {
|
|
253
285
|
node.content.forEach(function (childNode) {
|
|
254
286
|
var childNodeView = serializer.tryCreateNodeView(childNode);
|
|
255
287
|
if (childNodeView) {
|
|
@@ -279,7 +311,7 @@ export var createNodeChangedDecorationWidget = function createNodeChangedDecorat
|
|
|
279
311
|
if (handleMultipleChildNodes(node)) {
|
|
280
312
|
return;
|
|
281
313
|
}
|
|
282
|
-
if (shouldPreserveCompleteMultiInlineBlock) {
|
|
314
|
+
if (shouldPreserveCompleteMultiInlineBlock || shouldRenderAsBlockNode) {
|
|
283
315
|
fallbackSerialization = function fallbackSerialization() {
|
|
284
316
|
return serializer.serializeNode(node);
|
|
285
317
|
};
|
|
@@ -356,6 +388,21 @@ export var createNodeChangedDecorationWidget = function createNodeChangedDecorat
|
|
|
356
388
|
dom.style.setProperty('scroll-margin-top', scrollMarginTopValue);
|
|
357
389
|
}
|
|
358
390
|
|
|
391
|
+
// A block node serialized into the widget is the widget's first child, and the editor's
|
|
392
|
+
// first-child reset is written against the parent element rather than the document position — so
|
|
393
|
+
// it zeroes the margin the block wrapper was kept for in the first place.
|
|
394
|
+
//
|
|
395
|
+
// Only away from the parent's start. There the reset is the correct outcome: the widget is the
|
|
396
|
+
// first thing in the document, column, cell or panel, and a leading gap above it would be wrong.
|
|
397
|
+
// The margin that matters at that position belongs to the node *below* the widget, which the
|
|
398
|
+
// shaped spacer pair supplies instead.
|
|
399
|
+
if (fg('platform_editor_ai_show_diff_patch_2') && !isVisuallyFirstInParent) {
|
|
400
|
+
absorbFirstChildMarginReset({
|
|
401
|
+
dom: dom,
|
|
402
|
+
testId: 'show-diff-widget-margin-absorber'
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
|
|
359
406
|
// Needed even when the indicator bar is off, because a contributor tag also anchors against the
|
|
360
407
|
// widget.
|
|
361
408
|
if ((showIndicators || showContributorTags) && isExtendedEnabled(diffType)) {
|
|
@@ -418,7 +465,40 @@ export var createNodeChangedDecorationWidget = function createNodeChangedDecorat
|
|
|
418
465
|
var isPureDeletion = change.fromB === change.toB;
|
|
419
466
|
var isSingleBlock = slice.content.childCount === 1 && ((_slice$content$firstC2 = slice.content.firstChild) === null || _slice$content$firstC2 === void 0 ? void 0 : _slice$content$firstC2.isBlock);
|
|
420
467
|
var isDiffWidgetAtStartOfDoc = $safeInsertPos.depth === 0 && $safeInsertPos.index(0) === 0;
|
|
421
|
-
|
|
468
|
+
|
|
469
|
+
// A node at the start of its parent has its top margin reset, so a diff widget rendered above
|
|
470
|
+
// it sits flush against it. This applies wherever that parent is — the document, a layout
|
|
471
|
+
// column, a table cell — so it is decided from the anchor rather than from the document root
|
|
472
|
+
// (see `isVisuallyFirstInParent` above).
|
|
473
|
+
//
|
|
474
|
+
// Away from the parent's start the node below keeps its own margin, and it is the block *inside*
|
|
475
|
+
// the widget that loses one — handled by `absorbFirstChildMarginReset` above, not here.
|
|
476
|
+
var nodeAfterWidget = $safeInsertPos.nodeAfter;
|
|
477
|
+
if (fg('platform_editor_ai_show_diff_patch_2') && isExtendedEnabled(diffType) && isVisuallyFirstInParent && nodeAfterWidget) {
|
|
478
|
+
var shapedSpacer = createNodeShapedMarginSpacer({
|
|
479
|
+
node: nodeAfterWidget,
|
|
480
|
+
serializer: serializer,
|
|
481
|
+
testId: 'show-diff-shaped-margin-spacer'
|
|
482
|
+
});
|
|
483
|
+
if (shapedSpacer) {
|
|
484
|
+
// Two elements, because the two families of reset have to be handled differently. The
|
|
485
|
+
// absorber takes the document-level adjacent-sibling reset for a leading widget, whose
|
|
486
|
+
// `!important` would otherwise zero the shaped spacer's margin; the shaped spacer, now one
|
|
487
|
+
// position further along, is out of that rule's reach and keeps the margin it supplies.
|
|
488
|
+
// Inside containers the counting selectors skip both, so only the shaped one has an effect.
|
|
489
|
+
//
|
|
490
|
+
// Sides order the widgets against each other only — both still paint before the real node.
|
|
491
|
+
decorations.push(Decoration.widget(safeInsertPos, createMarginAbsorber({
|
|
492
|
+
testId: 'show-diff-margin-absorber'
|
|
493
|
+
}), {
|
|
494
|
+
side: 0
|
|
495
|
+
}));
|
|
496
|
+
decorations.push(Decoration.widget(safeInsertPos, shapedSpacer, {
|
|
497
|
+
side: 1
|
|
498
|
+
}));
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
if (!fg('platform_editor_ai_show_diff_patch_2') && isDiffWidgetAtStartOfDoc && isSingleBlock && isPureDeletion && isExtendedEnabled(diffType)) {
|
|
422
502
|
var followingNode = $safeInsertPos.nodeAfter;
|
|
423
503
|
var headingLevel = (followingNode === null || followingNode === void 0 ? void 0 : followingNode.type.name) === 'heading' ? followingNode.attrs.level : undefined;
|
|
424
504
|
if (isHeadingLevel(headingLevel)) {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { createBoxlessMarginAbsorber } from './createMarginAbsorber';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Keeps a block node rendered inside a diff widget from losing its own top margin.
|
|
5
|
+
*
|
|
6
|
+
* The editor's leading-block margin reset is written against the parent element, not the document
|
|
7
|
+
* position, so it fires wherever a text block is a first child — including inside the widget's own
|
|
8
|
+
* `span`, which is what a whole-block deletion renders into. The widget then has no margin of its
|
|
9
|
+
* own, and the block sits flush against whatever is above it, regardless of where in the document
|
|
10
|
+
* the widget landed.
|
|
11
|
+
*
|
|
12
|
+
* The margin is not re-supplied; it is never taken away. Prepending an element the reset does not
|
|
13
|
+
* select moves the real block off the leading position, so the ordinary `.ProseMirror p`,
|
|
14
|
+
* `.ProseMirror h2` and similar rules go on applying to it untouched. That is why this needs no
|
|
15
|
+
* knowledge of which margin the block should have — unlike `createNodeShapedMarginSpacer`, which
|
|
16
|
+
* replicates a margin that has already been zeroed and cannot be read back.
|
|
17
|
+
*
|
|
18
|
+
* Unconditional, because the absorber generates no box: it costs nothing in a widget holding
|
|
19
|
+
* inline content, which the reset was never going to match anyway. Deciding here instead would mean
|
|
20
|
+
* restating the reset's selector list, and a copy of another package's CSS drifts silently.
|
|
21
|
+
*/
|
|
22
|
+
export var absorbFirstChildMarginReset = function absorbFirstChildMarginReset(_ref) {
|
|
23
|
+
var dom = _ref.dom,
|
|
24
|
+
testId = _ref.testId;
|
|
25
|
+
dom.prepend(createBoxlessMarginAbsorber({
|
|
26
|
+
testId: testId
|
|
27
|
+
}));
|
|
28
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An empty, invisible element whose only job is to occupy a position in the DOM.
|
|
3
|
+
*
|
|
4
|
+
* At the top of the document `firstBlockNodeStyles` zeroes the top margin of whatever element
|
|
5
|
+
* follows a leading `.ProseMirror-widget`, and it does so with `!important` — which no inline style
|
|
6
|
+
* can outrank. This takes that hit so the shaped spacer after it, one position further along, keeps
|
|
7
|
+
* the margin it is there to supply.
|
|
8
|
+
*
|
|
9
|
+
* It contributes no height (no content, border or padding) and margins collapse through it, so it
|
|
10
|
+
* cannot affect layout beyond the selector it absorbs.
|
|
11
|
+
*/
|
|
12
|
+
export var createMarginAbsorber = function createMarginAbsorber(_ref) {
|
|
13
|
+
var testId = _ref.testId;
|
|
14
|
+
// Block-level: an empty inline element takes the match just as well, but collapses to a
|
|
15
|
+
// zero-height line box, and the following margin then lands in the wrong place.
|
|
16
|
+
var absorber = document.createElement('div');
|
|
17
|
+
absorber.dataset.testid = testId;
|
|
18
|
+
absorber.setAttribute('aria-hidden', 'true');
|
|
19
|
+
absorber.contentEditable = 'false';
|
|
20
|
+
return absorber;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* An absorber that occupies a DOM position without generating a box.
|
|
25
|
+
*
|
|
26
|
+
* Not interchangeable with `createMarginAbsorber`: a rule that selects the *rendered* sibling of a
|
|
27
|
+
* leading widget needs an element that generates a box, and this one does not.
|
|
28
|
+
*/
|
|
29
|
+
export var createBoxlessMarginAbsorber = function createBoxlessMarginAbsorber(_ref2) {
|
|
30
|
+
var testId = _ref2.testId;
|
|
31
|
+
var absorber = document.createElement('div');
|
|
32
|
+
absorber.dataset.testid = testId;
|
|
33
|
+
absorber.setAttribute('aria-hidden', 'true');
|
|
34
|
+
absorber.contentEditable = 'false';
|
|
35
|
+
absorber.style.display = 'contents';
|
|
36
|
+
return absorber;
|
|
37
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node types that can safely be reduced to an empty shell.
|
|
3
|
+
*
|
|
4
|
+
* A shell only replicates a margin if it collapses to nothing itself, which needs zero height and
|
|
5
|
+
* no vertical border or padding. Textblocks and lists qualify; nodes with intrinsic sizing (tables
|
|
6
|
+
* are `display: table`, media has a measured height) or a painted box (a zero-height panel is a
|
|
7
|
+
* visible stripe of background) do not, and are left alone.
|
|
8
|
+
*/
|
|
9
|
+
var isShapeableNode = function isShapeableNode(node) {
|
|
10
|
+
return node.isTextblock || ['bulletList', 'orderedList'].includes(node.type.name);
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* An invisible element shaped like `node`, used to supply the top margin `node` no longer gets.
|
|
15
|
+
*
|
|
16
|
+
* A node at the start of its parent has its top margin reset, so a diff widget rendered above it
|
|
17
|
+
* sits flush against it. The reset cannot simply be taken off the node: the container variants skip
|
|
18
|
+
* widgets when counting — `nth-child(1 of :not(style, .ProseMirror-gapcursor, .ProseMirror-widget,
|
|
19
|
+
* span))` in layout columns, expands, sync blocks and table cells — so no amount of extra elements
|
|
20
|
+
* moves the match. That exclusion is what makes this work instead: the spacer is a
|
|
21
|
+
* `.ProseMirror-widget`, so it is never the counted first child and keeps its own margin, while the
|
|
22
|
+
* real node goes on taking the reset.
|
|
23
|
+
*
|
|
24
|
+
* The margin is not measured. Reading it off the real node is impossible — by then the reset has
|
|
25
|
+
* set it to `0`, and both `firstBlockNodeStyles` (`!important`) and block controls' `firstNodeDec`
|
|
26
|
+
* (an inline style) make it unrecoverable. Instead the spacer carries the node's own tag, classes
|
|
27
|
+
* and attributes, so the same rules that would have given the node its margin match the spacer
|
|
28
|
+
* (`.ProseMirror p`, `.ProseMirror h2`, the root-list rule, and so on).
|
|
29
|
+
*
|
|
30
|
+
* Returns `null` when the node is not safely shapeable or serialization fails.
|
|
31
|
+
*/
|
|
32
|
+
export var createNodeShapedMarginSpacer = function createNodeShapedMarginSpacer(_ref) {
|
|
33
|
+
var _node$type$createAndF;
|
|
34
|
+
var node = _ref.node,
|
|
35
|
+
serializer = _ref.serializer,
|
|
36
|
+
testId = _ref.testId;
|
|
37
|
+
if (!isShapeableNode(node)) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// An empty node of the same type serializes to the same shell without walking real content.
|
|
42
|
+
var shellNode = (_node$type$createAndF = node.type.createAndFill(node.attrs)) !== null && _node$type$createAndF !== void 0 ? _node$type$createAndF : node;
|
|
43
|
+
var serialized = serializer.serializeNode(shellNode);
|
|
44
|
+
if (!(serialized instanceof HTMLElement)) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Keep the tag, classes and attributes the margin rules select on; drop anything that could
|
|
49
|
+
// occupy a line box.
|
|
50
|
+
serialized.replaceChildren();
|
|
51
|
+
serialized.dataset.testid = testId;
|
|
52
|
+
serialized.setAttribute('aria-hidden', 'true');
|
|
53
|
+
serialized.contentEditable = 'false';
|
|
54
|
+
|
|
55
|
+
// Zero height with no vertical border or padding keeps the spacer self-collapsing: its margins
|
|
56
|
+
// stay adjoining, so they collapse with the neighbours into a single margin equal to the largest
|
|
57
|
+
// rather than adding to them. That is what makes this safe in containers that never reset the
|
|
58
|
+
// first child — the spacer cannot double the gap there. Deliberately no `overflow: hidden`, which
|
|
59
|
+
// would open a block formatting context and stop that collapsing.
|
|
60
|
+
serialized.style.height = '0';
|
|
61
|
+
serialized.style.minHeight = '0';
|
|
62
|
+
serialized.style.paddingTop = '0';
|
|
63
|
+
serialized.style.paddingBottom = '0';
|
|
64
|
+
serialized.style.borderTopWidth = '0';
|
|
65
|
+
serialized.style.borderBottomWidth = '0';
|
|
66
|
+
// Only the top margin is being replicated. Left as-is, a larger bottom margin would win the
|
|
67
|
+
// self-collapse and overshoot the gap.
|
|
68
|
+
serialized.style.marginBottom = '0';
|
|
69
|
+
return serialized;
|
|
70
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves a document position, or returns `null` if it cannot be resolved.
|
|
3
|
+
*
|
|
4
|
+
* `Node.resolve` throws a `RangeError` for a position outside the document. Diff decorations are
|
|
5
|
+
* built from change ranges computed against a document that may since have moved on, so a position
|
|
6
|
+
* arriving out of range is a normal outcome rather than a defect — but an exception escaping here
|
|
7
|
+
* takes down the whole decoration set, leaving the diff unrendered.
|
|
8
|
+
*
|
|
9
|
+
* Callers are expected to treat `null` as "cannot tell" and fall back to behaviour that does not
|
|
10
|
+
* need the resolved position.
|
|
11
|
+
*/
|
|
12
|
+
export var safeResolve = function safeResolve(doc, pos) {
|
|
13
|
+
try {
|
|
14
|
+
return doc.resolve(pos);
|
|
15
|
+
} catch (_unused) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
@@ -325,11 +325,11 @@ export var ContributorTagController = /*#__PURE__*/function () {
|
|
|
325
325
|
|
|
326
326
|
// Hand-rolled avatar stack: `@atlaskit/avatar-group` cannot render below 24px and the tag uses
|
|
327
327
|
// 16px avatars.
|
|
328
|
-
var stack = model.connectedContributor ? [].concat(_toConsumableArray(
|
|
329
|
-
contributor: user,
|
|
330
|
-
stackIndex: 1
|
|
331
|
-
}] : []), _toConsumableArray(agent ? [{
|
|
328
|
+
var stack = model.connectedContributor ? [].concat(_toConsumableArray(agent ? [{
|
|
332
329
|
contributor: agent,
|
|
330
|
+
stackIndex: 1
|
|
331
|
+
}] : []), _toConsumableArray(user ? [{
|
|
332
|
+
contributor: user,
|
|
333
333
|
stackIndex: 0
|
|
334
334
|
}] : [])) : [{
|
|
335
335
|
contributor: model.contributor
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keeps a block node rendered inside a diff widget from losing its own top margin.
|
|
3
|
+
*
|
|
4
|
+
* The editor's leading-block margin reset is written against the parent element, not the document
|
|
5
|
+
* position, so it fires wherever a text block is a first child — including inside the widget's own
|
|
6
|
+
* `span`, which is what a whole-block deletion renders into. The widget then has no margin of its
|
|
7
|
+
* own, and the block sits flush against whatever is above it, regardless of where in the document
|
|
8
|
+
* the widget landed.
|
|
9
|
+
*
|
|
10
|
+
* The margin is not re-supplied; it is never taken away. Prepending an element the reset does not
|
|
11
|
+
* select moves the real block off the leading position, so the ordinary `.ProseMirror p`,
|
|
12
|
+
* `.ProseMirror h2` and similar rules go on applying to it untouched. That is why this needs no
|
|
13
|
+
* knowledge of which margin the block should have — unlike `createNodeShapedMarginSpacer`, which
|
|
14
|
+
* replicates a margin that has already been zeroed and cannot be read back.
|
|
15
|
+
*
|
|
16
|
+
* Unconditional, because the absorber generates no box: it costs nothing in a widget holding
|
|
17
|
+
* inline content, which the reset was never going to match anyway. Deciding here instead would mean
|
|
18
|
+
* restating the reset's selector list, and a copy of another package's CSS drifts silently.
|
|
19
|
+
*/
|
|
20
|
+
export declare const absorbFirstChildMarginReset: ({ dom, testId, }: {
|
|
21
|
+
dom: HTMLElement;
|
|
22
|
+
testId: string;
|
|
23
|
+
}) => void;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An empty, invisible element whose only job is to occupy a position in the DOM.
|
|
3
|
+
*
|
|
4
|
+
* At the top of the document `firstBlockNodeStyles` zeroes the top margin of whatever element
|
|
5
|
+
* follows a leading `.ProseMirror-widget`, and it does so with `!important` — which no inline style
|
|
6
|
+
* can outrank. This takes that hit so the shaped spacer after it, one position further along, keeps
|
|
7
|
+
* the margin it is there to supply.
|
|
8
|
+
*
|
|
9
|
+
* It contributes no height (no content, border or padding) and margins collapse through it, so it
|
|
10
|
+
* cannot affect layout beyond the selector it absorbs.
|
|
11
|
+
*/
|
|
12
|
+
export declare const createMarginAbsorber: ({ testId }: {
|
|
13
|
+
testId: string;
|
|
14
|
+
}) => HTMLElement;
|
|
15
|
+
/**
|
|
16
|
+
* An absorber that occupies a DOM position without generating a box.
|
|
17
|
+
*
|
|
18
|
+
* Not interchangeable with `createMarginAbsorber`: a rule that selects the *rendered* sibling of a
|
|
19
|
+
* leading widget needs an element that generates a box, and this one does not.
|
|
20
|
+
*/
|
|
21
|
+
export declare const createBoxlessMarginAbsorber: ({ testId }: {
|
|
22
|
+
testId: string;
|
|
23
|
+
}) => HTMLElement;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Node as PMNode } from '@atlaskit/editor-prosemirror/model';
|
|
2
|
+
import type { NodeViewSerializer } from '../../NodeViewSerializer';
|
|
3
|
+
/**
|
|
4
|
+
* An invisible element shaped like `node`, used to supply the top margin `node` no longer gets.
|
|
5
|
+
*
|
|
6
|
+
* A node at the start of its parent has its top margin reset, so a diff widget rendered above it
|
|
7
|
+
* sits flush against it. The reset cannot simply be taken off the node: the container variants skip
|
|
8
|
+
* widgets when counting — `nth-child(1 of :not(style, .ProseMirror-gapcursor, .ProseMirror-widget,
|
|
9
|
+
* span))` in layout columns, expands, sync blocks and table cells — so no amount of extra elements
|
|
10
|
+
* moves the match. That exclusion is what makes this work instead: the spacer is a
|
|
11
|
+
* `.ProseMirror-widget`, so it is never the counted first child and keeps its own margin, while the
|
|
12
|
+
* real node goes on taking the reset.
|
|
13
|
+
*
|
|
14
|
+
* The margin is not measured. Reading it off the real node is impossible — by then the reset has
|
|
15
|
+
* set it to `0`, and both `firstBlockNodeStyles` (`!important`) and block controls' `firstNodeDec`
|
|
16
|
+
* (an inline style) make it unrecoverable. Instead the spacer carries the node's own tag, classes
|
|
17
|
+
* and attributes, so the same rules that would have given the node its margin match the spacer
|
|
18
|
+
* (`.ProseMirror p`, `.ProseMirror h2`, the root-list rule, and so on).
|
|
19
|
+
*
|
|
20
|
+
* Returns `null` when the node is not safely shapeable or serialization fails.
|
|
21
|
+
*/
|
|
22
|
+
export declare const createNodeShapedMarginSpacer: ({ node, serializer, testId, }: {
|
|
23
|
+
node: PMNode;
|
|
24
|
+
serializer: NodeViewSerializer;
|
|
25
|
+
testId: string;
|
|
26
|
+
}) => HTMLElement | null;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Node as PMNode, ResolvedPos } from '@atlaskit/editor-prosemirror/model';
|
|
2
|
+
/**
|
|
3
|
+
* Resolves a document position, or returns `null` if it cannot be resolved.
|
|
4
|
+
*
|
|
5
|
+
* `Node.resolve` throws a `RangeError` for a position outside the document. Diff decorations are
|
|
6
|
+
* built from change ranges computed against a document that may since have moved on, so a position
|
|
7
|
+
* arriving out of range is a normal outcome rather than a defect — but an exception escaping here
|
|
8
|
+
* takes down the whole decoration set, leaving the diff unrendered.
|
|
9
|
+
*
|
|
10
|
+
* Callers are expected to treat `null` as "cannot tell" and fall back to behaviour that does not
|
|
11
|
+
* need the resolved position.
|
|
12
|
+
*/
|
|
13
|
+
export declare const safeResolve: (doc: PMNode, pos: number) => ResolvedPos | null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atlaskit/editor-plugin-show-diff",
|
|
3
|
-
"version": "16.1.
|
|
3
|
+
"version": "16.1.1",
|
|
4
4
|
"description": "ShowDiff plugin for @atlaskit/editor-core",
|
|
5
5
|
"author": "Atlassian Pty Ltd",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -8,6 +8,11 @@
|
|
|
8
8
|
"registry": "https://registry.npmjs.org/"
|
|
9
9
|
},
|
|
10
10
|
"atlassian": {
|
|
11
|
+
"website": {
|
|
12
|
+
"tags": [
|
|
13
|
+
"platform-labs"
|
|
14
|
+
]
|
|
15
|
+
},
|
|
11
16
|
"team": "Editor: Lego",
|
|
12
17
|
"releaseModel": "continuous",
|
|
13
18
|
"singleton": true
|
|
@@ -33,8 +38,8 @@
|
|
|
33
38
|
"@atlaskit/platform-feature-experiments": "^0.3.0",
|
|
34
39
|
"@atlaskit/platform-feature-flags": "^2.2.0",
|
|
35
40
|
"@atlaskit/primitives": "^22.5.0",
|
|
36
|
-
"@atlaskit/tmp-editor-statsig": "^
|
|
37
|
-
"@atlaskit/tokens": "^
|
|
41
|
+
"@atlaskit/tmp-editor-statsig": "^199.0.0",
|
|
42
|
+
"@atlaskit/tokens": "^18.0.0",
|
|
38
43
|
"@babel/runtime": "^7.0.0",
|
|
39
44
|
"@compiled/react": "^1.0.2",
|
|
40
45
|
"bind-event-listener": "^3.0.0",
|
|
@@ -65,7 +70,7 @@
|
|
|
65
70
|
"react-intl": "^7.0.0"
|
|
66
71
|
},
|
|
67
72
|
"peerDependencies": {
|
|
68
|
-
"@atlaskit/editor-common": "^122.
|
|
73
|
+
"@atlaskit/editor-common": "^122.11.0",
|
|
69
74
|
"react": "^18.2.0 || ^19.2.0",
|
|
70
75
|
"react-dom": "^18.2.0 || ^19.2.0",
|
|
71
76
|
"react-intl": "^5.25.1 || ^6.0.0 || ^7.0.0"
|
|
@@ -80,6 +85,9 @@
|
|
|
80
85
|
"platform_editor_ai_show_diff_patch_1": {
|
|
81
86
|
"type": "boolean"
|
|
82
87
|
},
|
|
88
|
+
"platform_editor_ai_show_diff_patch_2": {
|
|
89
|
+
"type": "boolean"
|
|
90
|
+
},
|
|
83
91
|
"platform_editor_ai_smart_diff": {
|
|
84
92
|
"type": "boolean"
|
|
85
93
|
},
|