@atlaskit/editor-plugin-show-diff
Version:
ShowDiff plugin for @atlaskit/editor-core
269 lines (255 loc) • 12.8 kB
JavaScript
import { convertToInlineCss } from '@atlaskit/editor-common/lazy-node-view';
import { Decoration } from '@atlaskit/editor-prosemirror/view';
import { isExperimentEnabled } from '@atlaskit/platform-feature-experiments/is-experiment-enabled';
import { fg } from '@atlaskit/platform-feature-flags/fg';
import { CONTRIBUTOR_TAG_Z_INDEX } from '../../ui/ContributorTag/buildContributorTagDom';
import { isEmptyParagraphSlice } from '../utils/isEmptyParagraphSlice';
import { buildAtomicInlineChangedCSSVariables, buildDeletedInlineContentStyleExtended, buildDeletedInlineNodeCSSVariables, buildDeletedInlineStyle, buildDeletedInlineStyleStandard, buildInsertStyle, buildInsertStyleActive, buildInsertStyleExtended, buildInsertStyleExtendedActive, buildInsertStyleExtendedNoUnderline, buildInsertStyleExtendedNoUnderlineActive } from './colorSchemes/factory';
import { colorSchemeRegistry, getLegacyColorScheme } from './colorSchemes/schemes';
import { createInlineIndicatorAnchorWidgets } from './createAnchorDecorationWidgets';
import { createContributorTagWidget, isContributorTagWidgetEnabled } from './createContributorTagWidget';
import { getAtomicInlineNodeClassNameLegacy, resolveInlineChangedStyleLegacy } from './createInlineChangedDecoration.styles.legacy';
import { buildDiffDecorationSpec, scrollMarginTopStyle } from './decorationKeys';
import { REVEAL_ATTR, resolveRevealStyle } from './revealStyles';
const displayNoneStyle = convertToInlineCss({
display: 'none'
});
/**
* Stacks the change's highlight below its contributor tag, so no highlight can slice through a tag:
* a tag overlays the line *above* its own change, and a highlight already on that line — the tail of
* this change, or another change entirely — would otherwise paint straight over it.
*
* The tag still reads as attached to the change rather than laid on top of it, because it cannot
* paint over this highlight at all: its bottom edge is pinned to the highlight's top edge and
* clipped there. See `CONTRIBUTOR_TAG_Z_INDEX`.
*
* prosemirror-view places the tag's host widget as a *sibling* of this decoration's span, so
* painting order is decided by stacking level — and the host is absolutely positioned at
* `CONTRIBUTOR_TAG_Z_INDEX`. `position: relative` is what makes `z-index` apply to an otherwise
* static inline box; it adds no offset.
*/
const stackBelowContributorTagStyle = convertToInlineCss({
position: 'relative',
zIndex: CONTRIBUTOR_TAG_Z_INDEX - 1
});
const getColorScheme = colorScheme => colorSchemeRegistry[colorScheme !== null && colorScheme !== void 0 ? colorScheme : 'standard'];
/**
* Class names for an atomic inline node decoration (date, emoji, mention, status). Scheme-agnostic
* — `buildAtomicInlineChangedCSSVariables()` supplies the colour inline on the same element.
*/
/**
* Lets editor-core redraw the strike on a deleted status: `line-through` from an ancestor paints
* behind the lozenge's own background (EDITOR-9123). Not needed for deleted-content widgets —
* `createContentWrapper` appends a real positioned line.
*/
const DELETED_INLINE_NODE_CLASS = 'show-diff-deleted-inline-node';
const getAtomicInlineNodeClassName = inlineNodeName => {
const classNames = ['show-diff-atomic-inline-changed'];
if (inlineNodeName) {
classNames.push(`show-diff-atomic-inline-changed-${inlineNodeName}`);
}
return classNames.join(' ');
};
/** Inline style for inserted content. */
const getExtendedInsertStyle = (colors, isActive, hideAddedDiffsUnderline) => {
if (colors.insertedInlineTreatment === 'underline') {
// hideAddedDiffsUnderline only targets the 'borderBottom' rule, so it does not apply here.
return isActive ? buildInsertStyleActive(colors) : buildInsertStyle(colors);
}
if (isActive) {
return hideAddedDiffsUnderline ? buildInsertStyleExtendedNoUnderlineActive(colors) : buildInsertStyleExtendedActive(colors);
}
return hideAddedDiffsUnderline ? buildInsertStyleExtendedNoUnderline(colors) : buildInsertStyleExtended(colors);
};
/** Inline style for deleted content. */
const getExtendedDeletedStyle = (colors, isActive) => {
if (colors.deletedInlineTreatment === 'strikethrough') {
// Identical in both states — the block decoration carries the active emphasis.
return buildDeletedInlineStyle(colors, false);
}
return buildDeletedInlineStyleStandard(colors, isActive ? 'active' : 'default') + buildDeletedInlineContentStyleExtended(colors, isActive);
};
/** Registry-driven inline `style` for a changed-content decoration. */
const resolveInlineChangedStyleRefactored = ({
colors,
hideAddedDiffsUnderline,
isActive,
isInserted
}) => isInserted ? getExtendedInsertStyle(colors, isActive, hideAddedDiffsUnderline) : getExtendedDeletedStyle(colors, isActive);
/**
* Single gate for the inline `style` string: registry + factory when the refactor is on, the
* verbatim pre-refactor per-scheme constants when it is off.
*/
const resolveInlineChangedStyle = args => isExperimentEnabled('platform_editor_show_diff_color_scheme_refactor') ? resolveInlineChangedStyleRefactored(args) : resolveInlineChangedStyleLegacy({
...args,
colorScheme: getLegacyColorScheme(args.colorScheme)
});
/**
* Single gate for the atomic-inline-node decoration attributes. The two cohorts differ in the DOM:
* on, the colour rides on an inline CSS variable and the class list is scheme-agnostic; off, the
* colour comes from a `-traditional` class and no variable is emitted. Returned together so the
* class list and style suffix can never be mixed across cohorts.
*/
const resolveAtomicInlineAttrs = (inlineNodeName, colorScheme, colors) => isExperimentEnabled('platform_editor_show_diff_color_scheme_refactor') ? {
className: getAtomicInlineNodeClassName(inlineNodeName),
styleSuffix: buildAtomicInlineChangedCSSVariables(colors)
} : {
className: getAtomicInlineNodeClassNameLegacy(inlineNodeName, getLegacyColorScheme(colorScheme)),
styleSuffix: ''
};
export const getAtomicInlineChangedAttrs = (inlineNodeName, colorScheme) => resolveAtomicInlineAttrs(inlineNodeName, colorScheme, getColorScheme(colorScheme));
/**
* Inline decoration used for insertions as the content already exists in the document
*
* @param change Changeset "change" containing information about the change content + range
* @returns Prosemirror inline decoration
*/
export const createInlineChangedDecoration = ({
attributionKey,
change,
colorScheme,
isActive = false,
isInserted = true,
leftAnchorId,
isAtomicInlineNode = false,
shouldHideDeleted = false,
showContributorTags = false,
showIndicators = false,
doc,
hideAddedDiffsUnderline = false,
inlineNodeName,
hasDeletedWidget = false,
isDeletedWidgetBelow = false,
reveal,
tagMountContext
}) => {
var _revealed$style;
// Derived from the range so it survives a recalculation: stepping to the next/previous change
// rebuilds decorations without touching the doc, and a fresh id would change the React key and the
// tag host's key, remounting every tag mid-step. Random ids are kept when tags are off.
const diffId = showContributorTags ? `inline-${change.fromB}-${change.toB}` : crypto.randomUUID();
// Match the rendered shape: whitespace text is eligible, while an empty paragraph is structural
// content with no tag host.
const canTagChange = showContributorTags && !!doc && isContributorTagWidgetEnabled() && !isEmptyParagraphSlice(doc.slice(change.fromB, change.toB));
if (shouldHideDeleted) {
return [Decoration.inline(change.fromB, change.toB, {
style: displayNoneStyle
}, buildDiffDecorationSpec({
// No attribution: this decoration paints nothing and emits no tag host, so a tag
// resolved from it would have nowhere to render.
colorScheme,
decorationType: 'inline',
diffId,
leftAnchorId,
isActive,
isInserted
}))];
}
const colors = getColorScheme(colorScheme);
// The reveal withholds the static highlight so the wipe has something to reveal, so it supplies
// the whole style rather than adding to the usual one.
const revealed = resolveRevealStyle({
colorScheme,
hideAddedDiffsUnderline,
isActive,
isInserted,
reveal
});
const style = (_revealed$style = revealed === null || revealed === void 0 ? void 0 : revealed.style) !== null && _revealed$style !== void 0 ? _revealed$style : resolveInlineChangedStyle({
colors,
colorScheme,
hideAddedDiffsUnderline,
isActive,
isInserted
});
const isAtomicInlineInsertion = isAtomicInlineNode && isInserted;
const atomicInlineAttrs = isAtomicInlineInsertion ? resolveAtomicInlineAttrs(inlineNodeName, colorScheme, colors) : undefined;
// Every deletion: an inverted diff carries no `isAtomicInlineNode`, so editor-core's selectors do
// the matching.
const deletedInlineNodeAttrs = !isInserted && fg('platform_editor_ai_show_diff_patch_2') ? {
className: DELETED_INLINE_NODE_CLASS,
styleSuffix: buildDeletedInlineNodeCSSVariables(colors, isActive)
} : undefined;
// Built from the raw change range, not the indicator anchors resolved below: a tag lines up with
// the changed text, while the bar's anchors are held on a block boundary when a deleted widget
// renders beside them.
//
// Built up here, ahead of where it is pushed, so the highlight's stacking below keys off whether a
// tag exists rather than re-deriving the condition and risking disagreement.
const tagWidget = canTagChange && doc ? createContributorTagWidget({
doc,
from: change.fromB,
to: change.toB,
diffId,
mountContext: tagMountContext
}) : undefined;
const inlineStyle = [style, atomicInlineAttrs === null || atomicInlineAttrs === void 0 ? void 0 : atomicInlineAttrs.styleSuffix, deletedInlineNodeAttrs === null || deletedInlineNodeAttrs === void 0 ? void 0 : deletedInlineNodeAttrs.styleSuffix, tagWidget ? stackBelowContributorTagStyle : undefined, fg('platform_editor_ai_show_diff_patch_1') ? scrollMarginTopStyle : undefined].filter(Boolean).join('');
const decorations = [Decoration.inline(change.fromB, change.toB, {
style: inlineStyle,
...(atomicInlineAttrs && {
class: atomicInlineAttrs.className
}),
// Mutually exclusive with the above: atomic-inline attrs are insertion-only.
...(deletedInlineNodeAttrs && {
class: deletedInlineNodeAttrs.className
}),
...(revealed && {
[REVEAL_ATTR]: revealed.role
}),
'data-testid': 'show-diff-changed-decoration',
// Lets navigation select this exact diff, and contributor tags find it on hover.
...((showContributorTags || fg('platform_editor_ai_show_diff_patch_2')) && {
'data-diff-id': diffId
})
}, buildDiffDecorationSpec({
attributionKey: canTagChange ? attributionKey : undefined,
colorScheme,
decorationType: 'inline',
diffId,
leftAnchorId,
isActive,
isInserted
}))];
if (showIndicators && doc) {
// For paragraphs, fromB/toB land on the outer block
// boundary. Adjust anchor widgets into inline content so the indicator
// bar doesn't extend into the preceding block's margin.
// Skip when the deleted-content widget is rendered — the anchor must
// stay at the boundary to keep the indicator continuous.
//
// The widget only pins the end of the range it is anchored to. It sits at `change.fromB`
// by default, but `deletedDiffPlacement: 'bottom'` / `inlineDeletedDiffPlacement: 'after'`
// move it to `change.toB` so the deleted content reads after the added content. In that
// case the `from` anchor has no widget beside it any more and must be adjusted inward
// again, otherwise the over-long indicator bar that CCI-18224 fixed comes back.
let anchorFrom = change.fromB;
let anchorTo = change.toB;
if (!hasDeletedWidget || isDeletedWidgetBelow) {
var _$from$nodeAfter;
const $from = doc.resolve(anchorFrom);
if (!$from.parent.inlineContent && (_$from$nodeAfter = $from.nodeAfter) !== null && _$from$nodeAfter !== void 0 && _$from$nodeAfter.isTextblock) {
anchorFrom = anchorFrom + 1;
}
}
if (!hasDeletedWidget) {
var _$to$nodeBefore;
const $to = doc.resolve(anchorTo);
if (!$to.parent.inlineContent && (_$to$nodeBefore = $to.nodeBefore) !== null && _$to$nodeBefore !== void 0 && _$to$nodeBefore.isTextblock) {
anchorTo = anchorTo - 1;
}
}
decorations.push(...createInlineIndicatorAnchorWidgets({
doc,
from: anchorFrom,
to: anchorTo,
diffId,
leftAnchorId
}));
}
// Pushed after the indicator anchors so the decoration order is unchanged — see `tagWidget` above
// for why it is built before them.
if (tagWidget) {
decorations.push(...tagWidget);
}
return decorations;
};