@atlaskit/editor-plugin-show-diff
Version:
ShowDiff plugin for @atlaskit/editor-core
398 lines (385 loc) • 16.3 kB
JavaScript
import _toConsumableArray from "@babel/runtime/helpers/toConsumableArray";
import _defineProperty from "@babel/runtime/helpers/defineProperty";
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; }
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) { _defineProperty(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; }
import { getDarkModeLCHColor } from '@atlaskit/adf-schema/get-dark-mode-lch-color';
import { isRgb } from '@atlaskit/adf-schema/is-rgb';
import { rgbToHex } from '@atlaskit/adf-schema/rgb-to-hex';
import { getCellDomAttrs } from '@atlaskit/adf-schema/tableNodes';
import { convertToInlineCss } from '@atlaskit/editor-common/css-helper';
import { areNodesEqualIgnoreAttrs } from '@atlaskit/editor-common/utils/document';
import { Decoration } from '@atlaskit/editor-prosemirror/view';
import { fg } from '@atlaskit/platform-feature-flags/fg';
import { createLeftAnchorWidget } from './createAnchorDecorationWidgets';
import { createBlockChangedDecoration } from './createBlockChangedDecoration';
import { createInlineChangedDecoration } from './createInlineChangedDecoration';
import { buildAnchorDecorationKey, buildDiffDecorationSpec, scrollMarginTopValue } from './decorationKeys';
import { isCellAttrChanged, isCellBackgroundChanged, normalizeCellBackground } from './utils/cellAttrs';
import { injectInnerWrapper, wrapBlockNodeView } from './utils/wrapBlockNodeView';
var CELL_TYPE_NAMES = ['tableCell', 'tableHeader'];
/**
* Whether only close tokens follow `pos` in its cell: it is at the end of its parent, and so is each
* ancestor up to the cell.
*/
var isAtEndOfCell = function isAtEndOfCell(doc, pos) {
var $pos = doc.resolve(pos);
if ($pos.parentOffset !== $pos.parent.content.size) {
return false;
}
for (var depth = $pos.depth; depth > 0; depth--) {
if (CELL_TYPE_NAMES.includes($pos.node(depth).type.name)) {
return true;
}
if ($pos.indexAfter(depth - 1) !== $pos.node(depth - 1).childCount) {
return false;
}
}
return false;
};
/**
* Pairs each cell of a whole-cell replacement with the live cell it replaces, or `[]` when the slice
* is not a run of whole cells matching the live ones. Whole-cell replacement preserves cell count and
* order, so the nth live cell in the range matches the nth cell in the slice.
*/
export var getReplacedCells = function getReplacedCells(_ref) {
var slice = _ref.slice,
doc = _ref.doc,
fromB = _ref.fromB,
toB = _ref.toB;
var otherCells = slice.content.content;
if (slice.openStart !== 0 || (!fg('platform_editor_ai_show_diff_patch_2_fix') || !isAtEndOfCell(doc, toB)) && slice.openEnd !== 0 || otherCells.length === 0 || !otherCells.every(function (node) {
return CELL_TYPE_NAMES.includes(node.type.name);
})) {
return [];
}
var liveCells = [];
doc.nodesBetween(fromB, toB, function (node, pos) {
if (CELL_TYPE_NAMES.includes(node.type.name)) {
liveCells.push({
node: node,
pos: pos
});
return false;
}
return true;
});
if (liveCells.length !== otherCells.length) {
return [];
}
return otherCells.map(function (otherCell, index) {
return {
otherCell: otherCell,
liveCell: liveCells[index].node,
liveCellPos: liveCells[index].pos
};
});
};
/**
* Whether two cells hold the same content, ignoring the cells' own attributes and `localId`s, which
* a suggestion can regenerate. Such a replacement only changed the cell (e.g. its background), so
* there is no content to diff.
*/
var isCellContentEqual = function isCellContentEqual(left, right) {
if (left.childCount !== right.childCount) {
return false;
}
for (var index = 0; index < left.childCount; index++) {
if (!areNodesEqualIgnoreAttrs(left.child(index), right.child(index), ['localId'])) {
return false;
}
}
return true;
};
/**
* Whether the cell itself changed — its type, or an attribute that represents a change such as its
* background or span — as opposed to only its content.
*/
var isCellChanged = function isCellChanged(left, right) {
return left.type !== right.type || isCellAttrChanged(left, right);
};
/**
* A custom (non-palette) cell colour as the schema renders it: inverted in dark mode so text stays
* readable, except for design tokens, which are already themed.
*/
var getCustomCellColor = function getCustomCellColor(background) {
var color = isRgb(background) && rgbToHex(background) || background;
if (color.startsWith('var(') || document.documentElement.getAttribute('data-color-mode') !== 'dark') {
return background;
}
return getDarkModeLCHColor(color);
};
/**
* The colour `cell` renders with. A palette colour is the editor's own themed variable for its colour
* name (e.g. for dark mode), the one `td[colorname]` cells use; a custom colour is the schema's own
* treatment of it; no colour is the editor's default.
*/
var getCellBackgroundColor = function getCellBackgroundColor(cell) {
var background = normalizeCellBackground(cell.attrs.background);
if (!background) {
return cell.type.name === 'tableHeader' ? "var(--ds-background-accent-gray-subtlest, #F0F1F2)" : "var(--ds-surface, #FFFFFF)";
}
var _getCellDomAttrs = getCellDomAttrs(cell),
colorname = _getCellDomAttrs.colorname;
return colorname ? "var(--ak-editor-table-cell-background-".concat(colorname.toLowerCase().split(' ').join('-'), ")") : getCustomCellColor(background);
};
/**
* A layer painting `color` over the cell's own background and under its content, so the cell reads
* as having the new background without any of the cell's own attributes being overridden: removing
* a decoration removes the attributes it set rather than restoring the ones they replaced.
*/
var createCellBackgroundLayer = function createCellBackgroundLayer(color) {
var layer = document.createElement('div');
layer.setAttribute('aria-hidden', 'true');
layer.contentEditable = 'false';
layer.dataset.testid = 'show-diff-new-cell-background';
layer.setAttribute('style', convertToInlineCss({
position: 'absolute',
inset: 0,
zIndex: -1,
borderRadius: 'inherit',
pointerEvents: 'none',
backgroundColor: color
}));
return layer;
};
/** The other side's cell content, as blocks, for the top of the live cell, with its indicator anchor. */
var createCellContentWidgets = function createCellContentWidgets(_ref2) {
var cell = _ref2.cell,
doc = _ref2.doc,
pos = _ref2.pos,
nodeViewSerializer = _ref2.nodeViewSerializer,
colorScheme = _ref2.colorScheme,
intl = _ref2.intl,
isActive = _ref2.isActive,
isInserted = _ref2.isInserted,
leftAnchorId = _ref2.leftAnchorId,
showIndicators = _ref2.showIndicators;
var dom = document.createElement('div');
cell.content.forEach(function (block) {
var nodeView = nodeViewSerializer.tryCreateNodeView(block, 0, undefined, isInserted, colorScheme);
if (nodeView) {
wrapBlockNodeView({
dom: dom,
nodeView: nodeView,
targetNode: block,
colorScheme: colorScheme,
intl: intl,
isActive: isActive,
isInserted: isInserted
});
return;
}
var serialized = nodeViewSerializer.serializeNode(block);
if (serialized instanceof HTMLElement) {
dom.append(injectInnerWrapper({
node: serialized,
colorScheme: colorScheme,
isActive: isActive,
isInserted: isInserted
}));
}
});
if (dom.childNodes.length === 0) {
return [];
}
dom.setAttribute('data-testid', isInserted ? 'show-diff-changed-decoration' : 'show-diff-deleted-decoration');
if (fg('platform_editor_ai_show_diff_patch_2_fix')) {
dom.style.setProperty('scroll-margin-top', scrollMarginTopValue);
}
var diffId = crypto.randomUUID();
if (showIndicators && fg('platform_editor_ai_show_diff_patch_2_fix')) {
// The indicator bar of a widget anchors its top and bottom to the widget itself. Without this
// only the live cell's own bar resolves, which starts below the suggested content.
dom.style.setProperty('anchor-name', "--".concat(buildAnchorDecorationKey({
diffId: diffId
})));
}
// Before the cell's own blocks, as a block of its own, so it never lands inside a paragraph the
// displayed side may hide.
var widget = Decoration.widget(pos + 1, dom, _objectSpread(_objectSpread({}, buildDiffDecorationSpec({
colorScheme: colorScheme,
decorationType: 'widget',
diffId: diffId,
leftAnchorId: leftAnchorId,
isActive: isActive,
isInserted: isInserted,
side: -1
})), {}, {
marks: []
}));
// The bar aligns its left edge to this anchor. The live cell's inline decoration makes one too,
// but not when the clean view hides it, and the widget is still showing then.
var leftAnchor = showIndicators && fg('platform_editor_ai_show_diff_patch_2_fix') ? createLeftAnchorWidget({
doc: doc,
from: pos,
diffId: diffId,
leftAnchorId: leftAnchorId
}) : undefined;
return leftAnchor ? [widget, leftAnchor] : [widget];
};
/**
* The other side of a whole-cell replacement, drawn inside the live cells so the table keeps its
* shape: each live cell takes the other cell's background when it differs, and its content when that
* differs. A cell whose content did not change has nothing to diff.
*/
export var createReplacedCellWidgets = function createReplacedCellWidgets(_ref3) {
var slice = _ref3.slice,
newDoc = _ref3.newDoc,
change = _ref3.change,
nodeViewSerializer = _ref3.nodeViewSerializer,
colorScheme = _ref3.colorScheme,
intl = _ref3.intl,
isActive = _ref3.isActive,
isInserted = _ref3.isInserted,
leftAnchorId = _ref3.leftAnchorId,
showIndicators = _ref3.showIndicators;
return getReplacedCells({
slice: slice,
doc: newDoc,
fromB: change.fromB,
toB: change.toB
}).flatMap(function (_ref4) {
var otherCell = _ref4.otherCell,
liveCell = _ref4.liveCell,
liveCellPos = _ref4.liveCellPos;
var decorations = [];
if (isInserted && isCellBackgroundChanged(otherCell, liveCell)) {
var cellEnd = liveCellPos + liveCell.nodeSize;
decorations.push(
// The cell becomes the layer's stacking context, so the layer sits above the cell's own
// background rather than behind it. Through `z-index` at the cell overlay's own level, not
// `isolation`: an isolated cell would trap its overlay below later cells, which then cover
// the overlay's outline where it overlaps their border. Properties the cell never sets, so
// removing them takes nothing of its own.
Decoration.node(liveCellPos, cellEnd, {
style: convertToInlineCss({
position: 'relative',
// The highest z-index a cell diff overlay takes (the rounded-corner variant).
zIndex: 2
})
}, {
key: "show-diff-new-cell-background-host-".concat(liveCellPos)
}),
// At the cell's end, beside its diff overlay, so the cell's first block stays its first child.
Decoration.widget(cellEnd - 1, createCellBackgroundLayer(getCellBackgroundColor(otherCell)), {
key: "show-diff-new-cell-background-".concat(liveCellPos),
side: -1,
marks: [],
ignoreSelection: true
}));
}
if (!isCellContentEqual(otherCell, liveCell)) {
var contentWidgets = createCellContentWidgets({
cell: otherCell,
doc: newDoc,
pos: liveCellPos,
nodeViewSerializer: nodeViewSerializer,
colorScheme: colorScheme,
intl: intl,
isActive: isActive,
isInserted: isInserted,
leftAnchorId: leftAnchorId,
showIndicators: showIndicators
});
decorations.push.apply(decorations, _toConsumableArray(contentWidgets));
}
return decorations;
});
};
/** Whether a cell holds only blank lines: empty paragraphs or headings, with nothing to show. */
var isBlankCell = function isBlankCell(cell) {
return cell.childCount > 0 && cell.children.every(function (block) {
return block.isTextblock && block.content.size === 0;
});
};
/**
* The displayed side of a whole-cell replacement. A table cell is never removed on its own, only
* replaced in place, so a live cell is never hidden — hiding it would leave the row a cell short. It
* takes the changed-cell treatment only when the cell itself changed; its content is diffed, and
* hidden in the clean view, only when that changed. A cell whose content stayed the same keeps
* showing it.
*/
export var createReplacedCellDecorations = function createReplacedCellDecorations(_ref5) {
var replacedCells = _ref5.replacedCells,
change = _ref5.change,
doc = _ref5.doc,
attributionKey = _ref5.attributionKey,
colorScheme = _ref5.colorScheme,
hasDeletedWidget = _ref5.hasDeletedWidget,
hideAddedDiffsUnderline = _ref5.hideAddedDiffsUnderline,
isActive = _ref5.isActive,
isDeletedWidgetBelow = _ref5.isDeletedWidgetBelow,
isInserted = _ref5.isInserted,
leftAnchorId = _ref5.leftAnchorId,
shouldHideDeleted = _ref5.shouldHideDeleted,
showContributorTags = _ref5.showContributorTags,
showIndicators = _ref5.showIndicators,
tagMountContext = _ref5.tagMountContext;
return replacedCells.flatMap(function (_ref6) {
var otherCell = _ref6.otherCell,
liveCell = _ref6.liveCell,
liveCellPos = _ref6.liveCellPos;
var cellEnd = liveCellPos + liveCell.nodeSize;
var decorations = isCellChanged(otherCell, liveCell) ? createBlockChangedDecoration({
attributionKey: attributionKey,
change: {
from: liveCellPos,
to: cellEnd,
name: liveCell.type.name
},
colorScheme: colorScheme,
doc: doc,
isActive: isActive,
// The cell stays either way, so it reads as changed, not removed.
isInserted: true,
leftAnchorId: leftAnchorId,
showContributorTags: showContributorTags,
showIndicators: showIndicators,
tagMountContext: tagMountContext
}) : [];
if (isCellContentEqual(otherCell, liveCell)) {
return decorations;
}
// A blank live cell has no content to strike through, so the suggestion takes its place, as
// in the clean view, rather than leaving a blank line below it.
var hidesLiveCell = shouldHideDeleted || !isInserted && isBlankCell(liveCell) && fg('platform_editor_ai_show_diff_patch_2_fix');
decorations.push.apply(decorations, _toConsumableArray(createInlineChangedDecoration({
attributionKey: attributionKey,
change: _objectSpread(_objectSpread({}, change), {}, {
fromB: liveCellPos,
toB: cellEnd
}),
colorScheme: colorScheme,
doc: doc,
hasDeletedWidget: hasDeletedWidget,
hideAddedDiffsUnderline: hideAddedDiffsUnderline,
isActive: isActive,
isDeletedWidgetBelow: isDeletedWidgetBelow,
isInserted: isInserted,
leftAnchorId: leftAnchorId,
shouldHideDeleted: hidesLiveCell,
showContributorTags: showContributorTags,
showIndicators: showIndicators,
tagMountContext: tagMountContext
})));
if (hidesLiveCell) {
liveCell.forEach(function (block, offset) {
var blockPos = liveCellPos + 1 + offset;
decorations.push.apply(decorations, _toConsumableArray(createBlockChangedDecoration({
change: {
from: blockPos,
to: blockPos + block.nodeSize,
name: block.type.name
},
colorScheme: colorScheme,
doc: doc,
isActive: isActive,
isInserted: isInserted,
leftAnchorId: leftAnchorId,
shouldHideDeleted: hidesLiveCell
})));
});
}
return decorations;
});
};