UNPKG

@atlaskit/editor-plugin-show-diff

Version:

ShowDiff plugin for @atlaskit/editor-core

398 lines (385 loc) • 16.3 kB
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; }); };