@atlaskit/editor-plugin-show-diff
Version:
ShowDiff plugin for @atlaskit/editor-core
303 lines (293 loc) • 14 kB
JavaScript
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.createBlockChangedDecoration = void 0;
var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray"));
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
var _lazyNodeView = require("@atlaskit/editor-common/lazy-node-view");
var _utils = require("@atlaskit/editor-prosemirror/utils");
var _view = require("@atlaskit/editor-prosemirror/view");
var _isExperimentEnabled = require("@atlaskit/platform-feature-experiments/is-experiment-enabled");
var _fg = require("@atlaskit/platform-feature-flags/fg");
var _taggableBlockNodes = require("../utils/taggableBlockNodes");
var _factory = require("./colorSchemes/factory");
var _schemes = require("./colorSchemes/schemes");
var _createAnchorDecorationWidgets = require("./createAnchorDecorationWidgets");
var _createBlockChangedDecorationStyles = require("./createBlockChangedDecoration.styles.legacy");
var _createContributorTagWidget = require("./createContributorTagWidget");
var _decorationKeys = require("./decorationKeys");
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) { (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; }
var displayNoneStyle = (0, _lazyNodeView.convertToInlineCss)({
display: 'none'
});
var DEFAULT_COLOR_SCHEME = 'standard';
/** Nodes that carry no decoration styling of their own — layout, lists, text blocks, media groups. */
var UNSTYLED_NODES = ['mediaSingle', 'mediaGroup', 'table',
// Handle table separately to avoid border issues
'tableRow', 'paragraph',
// Paragraph and heading nodes do not need special styling
'heading', 'hardBreak', 'decisionList', 'taskList', 'bulletList', 'orderedList', 'layoutSection'];
var CELL_NODES = ['tableCell', 'tableHeader'];
/** Panels, rules and media can also sit in a table cell, which is out of scope for block tags. */
var isInsideTable = function isInsideTable(doc, pos) {
return (0, _utils.findParentNodeClosestToPos)(doc.resolve(pos), function (node) {
return node.type.name === 'table';
}) !== undefined;
};
/**
* Whether this block can host a contributor tag of its own.
*
* The node type is resolved from the document rather than matched on `change.name`, so a schema
* variant is recognised as the type it varies — see `isTaggableBlockNode`. `from` is the node's own
* position, so `nodeAt` returns exactly the node being decorated.
*/
var canTagBlock = function canTagBlock(_ref) {
var _doc$nodeAt;
var doc = _ref.doc,
from = _ref.from;
if (doc === undefined) {
return false;
}
var nodeType = (_doc$nodeAt = doc.nodeAt(from)) === null || _doc$nodeAt === void 0 ? void 0 : _doc$nodeAt.type;
return nodeType !== undefined && (0, _taggableBlockNodes.isTaggableBlockNode)(nodeType) && !isInsideTable(doc, from);
};
/** Positioning context for the cell overlay widget decorations. */
var cellPositionStyle = (0, _lazyNodeView.convertToInlineCss)({
position: 'relative'
});
var getInsertedBlockNodeShape = function getInsertedBlockNodeShape(nodeName) {
switch (nodeName) {
case 'blockquote':
return 'quote';
case 'rule':
return 'rule';
case 'blockCard':
return 'cardBlock';
case 'extension':
case 'embedCard':
case 'listItem':
return 'marker';
default:
return 'node';
}
};
var getDeletedBlockNodeCategory = function getDeletedBlockNodeCategory(nodeName) {
switch (nodeName) {
case 'blockquote':
return 'quote';
// Media nodes inside mediaSingle should not get position:relative
// as it shifts the image outside its parent container (e.g. panel)
case 'media':
case 'panel':
return 'container';
case 'listItem':
return 'listItem';
case 'extension':
case 'embedCard':
return 'marker';
default:
return 'generic';
}
};
var getNodeClass = function getNodeClass(name) {
switch (name) {
case 'extension':
return 'show-diff-changed-decoration-node';
default:
return undefined;
}
};
var getBlockNodeStyleNext = function getBlockNodeStyleNext(_ref2) {
var nodeName = _ref2.nodeName,
colorScheme = _ref2.colorScheme,
_ref2$isInserted = _ref2.isInserted,
isInserted = _ref2$isInserted === void 0 ? true : _ref2$isInserted,
_ref2$isActive = _ref2.isActive,
isActive = _ref2$isActive === void 0 ? false : _ref2$isActive;
if (UNSTYLED_NODES.includes(nodeName)) {
return undefined;
}
if (CELL_NODES.includes(nodeName)) {
return cellPositionStyle;
}
var colors = _schemes.colorSchemeRegistry[colorScheme !== null && colorScheme !== void 0 ? colorScheme : DEFAULT_COLOR_SCHEME];
if (!isInserted) {
return (0, _factory.buildDeletedBlockNodeStyle)(colors, getDeletedBlockNodeCategory(nodeName), isActive);
}
return (0, _factory.buildInsertedBlockNodeStyle)(colors, getInsertedBlockNodeShape(nodeName), isActive);
};
/**
* Dispatches to the registry-driven implementation, or to the verbatim pre-refactor one for the
* OFF cohort of `platform_editor_show_diff_color_scheme_refactor`. Both are built to emit the
* same style strings for both shipped schemes; see EDITOR-8281.
*/
var getBlockNodeStyle = function getBlockNodeStyle(props) {
return (0, _isExperimentEnabled.isExperimentEnabled)('platform_editor_show_diff_color_scheme_refactor') ? getBlockNodeStyleNext(props) : (0, _createBlockChangedDecorationStyles.getBlockNodeStyleLegacy)(_objectSpread(_objectSpread({}, props), {}, {
colorScheme: (0, _schemes.getLegacyColorScheme)(props.colorScheme)
}));
};
/**
* A table cell is "empty" only when it has no content at all — no text AND no
* non-text leaf/inline nodes (media, emoji, mention, inlineCard, status, date…).
* `textContent` alone misses those non-text nodes, so we also reject any leaf or
* inline descendant. An empty ADF cell (`tableCell > empty paragraph`) returns
* true; a cell containing only media/emoji returns false.
*/
var isCellEmpty = function isCellEmpty(cellNode) {
if (cellNode.textContent.length > 0) {
return false;
}
var hasNonTextContent = false;
cellNode.descendants(function (node) {
if (hasNonTextContent) {
return false;
}
// A non-text leaf or inline node (media, emoji, mention, etc.) = real content.
if (!node.isText && (node.isLeaf || node.isInline)) {
hasNonTextContent = true;
return false;
}
return true;
});
return !hasNonTextContent;
};
/**
* Node decoration used for block-level insertions. When isActive, uses emphasised (pressed) styling.
*
* @param change Node range and name
* @param colorScheme Optional color scheme
* @param isActive Whether this node is part of the currently active/focused change
* @returns Prosemirror node decoration or undefined
*/
var createBlockChangedDecoration = exports.createBlockChangedDecoration = function createBlockChangedDecoration(_ref3) {
var attributionKey = _ref3.attributionKey,
change = _ref3.change,
colorScheme = _ref3.colorScheme,
_ref3$isInserted = _ref3.isInserted,
isInserted = _ref3$isInserted === void 0 ? true : _ref3$isInserted,
_ref3$isActive = _ref3.isActive,
isActive = _ref3$isActive === void 0 ? false : _ref3$isActive,
leftAnchorId = _ref3.leftAnchorId,
_ref3$shouldHideDelet = _ref3.shouldHideDeleted,
shouldHideDeleted = _ref3$shouldHideDelet === void 0 ? false : _ref3$shouldHideDelet,
_ref3$showContributor = _ref3.showContributorTags,
showContributorTags = _ref3$showContributor === void 0 ? false : _ref3$showContributor,
_ref3$showIndicators = _ref3.showIndicators,
showIndicators = _ref3$showIndicators === void 0 ? false : _ref3$showIndicators,
doc = _ref3.doc,
tagMountContext = _ref3.tagMountContext;
var decorations = [];
// Derived from the node range so it survives a recalculation, as in
// `createInlineChangedDecoration`. Changes are disjoint, so no two of them decorate one node.
var diffId = showContributorTags ? "block-".concat(change.from, "-").concat(change.to) : crypto.randomUUID();
var shouldTagBlock = showContributorTags && canTagBlock({
doc: doc,
from: change.from
});
// Named so this block's own tag can position against it; see `createContributorTagWidget`.
var tagAnchorName = shouldTagBlock && (0, _createContributorTagWidget.isContributorTagWidgetEnabled)() ? (0, _decorationKeys.buildAnchorDecorationKey)({
diffId: diffId,
anchorType: _decorationKeys.AnchorTypeKey.tag
}) : undefined;
if (shouldHideDeleted) {
return [_view.Decoration.node(change.from, change.to, {
style: displayNoneStyle
}, (0, _decorationKeys.buildDiffDecorationSpec)({
colorScheme: colorScheme,
decorationType: 'block',
diffId: diffId,
leftAnchorId: leftAnchorId,
isActive: isActive,
isInserted: isInserted,
nodeName: change.name
}))];
}
if (CELL_NODES.includes(change.name)) {
var cellOverlay = document.createElement('div');
var colors = _schemes.colorSchemeRegistry[colorScheme !== null && colorScheme !== void 0 ? colorScheme : DEFAULT_COLOR_SCHEME];
var isRoundedTable = (0, _isExperimentEnabled.isExperimentEnabled)('platform_editor_table_diff_rounded_corners');
// On an inverted diff, an empty cell being filled is an addition, so give it the
// added (purple) overlay instead of the deleted (grey) one (EDITOR-8442).
var cellNode = doc === null || doc === void 0 ? void 0 : doc.nodeAt(change.from);
var isEmptyCellBeingFilled = !isInserted && !!cellNode && isCellEmpty(cellNode);
var useAddedStyle = isInserted || isEmptyCellBeingFilled;
var cellOverlayStyle = (0, _isExperimentEnabled.isExperimentEnabled)('platform_editor_show_diff_color_scheme_refactor') ? useAddedStyle ? isRoundedTable ? (0, _factory.buildAddedCellOverlayRoundedStyle)(colors) : (0, _factory.buildAddedCellOverlayStyle)(colors) : isRoundedTable ? (0, _factory.buildDeletedCellOverlayRoundedStyle)(colors) : (0, _factory.buildDeletedCellOverlayStyle)(colors) : (0, _createBlockChangedDecorationStyles.resolveCellOverlayStyleLegacy)({
colorScheme: (0, _schemes.getLegacyColorScheme)(colorScheme),
isRoundedTable: isRoundedTable,
useAddedStyle: useAddedStyle
});
cellOverlay.setAttribute('style', cellOverlayStyle);
decorations.push(
// change.to - 1 to position the overlay inside the end of the cell
// this key doesn't use the spec / key builder since this is just for
// decorating the cells, this is part of a bigger table diff
_view.Decoration.widget(change.to - 1, cellOverlay, {
key: 'cell-overlay-decoration'
}));
}
// `change.name` verbatim: base-name resolution stays in `canTagBlock`, which needs tags on, so a
// schema variant's legacy style is untouched — see `resolveBaseNodeName`.
var nodeStyle = getBlockNodeStyle({
nodeName: change.name,
colorScheme: colorScheme,
isInserted: isInserted,
isActive: isActive
});
var style = tagAnchorName ? [nodeStyle, (0, _lazyNodeView.convertToInlineCss)({
anchorName: "--".concat(tagAnchorName)
}), (0, _fg.fg)('platform_editor_ai_show_diff_patch_1') ? _decorationKeys.scrollMarginTopStyle : undefined].filter(Boolean).join(' ') : nodeStyle;
var className = getNodeClass(change.name);
if (style || className) {
decorations.push(_view.Decoration.node(change.from, change.to, _objectSpread({
style: style,
'data-testid': 'show-diff-changed-decoration-node',
class: className
}, (shouldTagBlock || (0, _fg.fg)('platform_editor_ai_show_diff_patch_2')) && {
'data-diff-id': diffId
}), (0, _decorationKeys.buildDiffDecorationSpec)({
attributionKey: shouldTagBlock ? attributionKey : undefined,
colorScheme: colorScheme,
decorationType: 'block',
diffId: diffId,
leftAnchorId: leftAnchorId,
isActive: isActive,
isInserted: isInserted,
nodeName: change.name
})));
}
if (decorations.length === 0) {
// An unstyled node type (paragraph, heading, list, layout, …) emits no decoration at all, so
// there is nothing for a tag to hang off even when the node type would otherwise be taggable.
return decorations;
}
if (showIndicators && doc) {
decorations.push.apply(decorations, (0, _toConsumableArray2.default)((0, _createAnchorDecorationWidgets.createBlockIndicatorAnchorWidgets)({
doc: doc,
from: change.from,
to: change.to,
diffId: diffId,
leftAnchorId: leftAnchorId
})));
}
// `change.from` is just before the node, so the host sits outside it and the tag renders on the
// block's own top-left corner. Resolving inline content instead would put the host inside a
// container block's content DOM (panel, expand, blockquote).
if (shouldTagBlock && doc) {
var tagWidget = (0, _createContributorTagWidget.createContributorTagWidget)({
anchorAtRangeStart: true,
anchorName: tagAnchorName,
doc: doc,
from: change.from,
to: change.to,
diffId: diffId,
mountContext: tagMountContext
});
if (tagWidget) {
decorations.push.apply(decorations, (0, _toConsumableArray2.default)(tagWidget));
}
}
return decorations;
};