UNPKG

@atlaskit/editor-plugin-show-diff

Version:

ShowDiff plugin for @atlaskit/editor-core

406 lines (394 loc) 16.8 kB
import { convertToInlineCss } from '@atlaskit/editor-common/lazy-node-view'; import { trackChangesMessages } from '@atlaskit/editor-common/messages'; import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals'; import { deletedBlockOutline, deletedBlockOutlineActive, deletedBlockOutlineRounded, deletedBlockOutlineRoundedActive, deletedContentStyle, deletedContentStyleActive, deletedContentStyleNew, deletedContentStyleNewActive, deletedStyleQuoteNodeWithLozenge, deletedStyleQuoteNodeWithLozengeActive, editingStyle, editingStyleActive, editingStyleNode, addedCellOverlayStyle, deletedCellOverlayStyle } from '../colorSchemes/standard'; import { deletedTraditionalBlockOutline, deletedTraditionalBlockOutlineActive, deletedTraditionalBlockOutlineRounded, deletedTraditionalBlockOutlineRoundedActive, deletedTraditionalContentStyle, deletedTraditionalContentStyleActive, deletedTraditionalStyleQuoteNode, deletedTraditionalStyleQuoteNodeActive, traditionalInsertStyle, traditionalInsertStyleActive, traditionalStyleNode, traditionalAddedCellOverlayStyle, deletedTraditionalCellOverlayStyle } from '../colorSchemes/traditional'; var lozengeStyle = convertToInlineCss({ display: 'inline-flex', boxSizing: 'border-box', position: 'static', blockSize: 'min-content', borderRadius: "var(--ds-radius-small, 4px)", overflow: 'hidden', paddingInlineStart: "var(--ds-space-050, 4px)", paddingInlineEnd: "var(--ds-space-050, 4px)", backgroundColor: "var(--ds-background-accent-gray-subtler, #DDDEE1)", font: "var(--ds-font-body-small, normal 400 12px/16px \"Atlassian Sans\", ui-sans-serif, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Ubuntu, \"Helvetica Neue\", sans-serif)", fontWeight: "var(--ds-font-weight-bold, 653)", textOverflow: 'ellipsis', whiteSpace: 'nowrap', color: "var(--ds-text-warning-inverse, #292A2E)" }); var lozengeStyleActiveStandard = convertToInlineCss({ display: 'inline-flex', boxSizing: 'border-box', position: 'static', blockSize: 'min-content', borderRadius: "var(--ds-radius-small, 4px)", overflow: 'hidden', paddingInlineStart: "var(--ds-space-050, 4px)", paddingInlineEnd: "var(--ds-space-050, 4px)", backgroundColor: "var(--ds-background-accent-red-subtler-pressed, #FD9891)", font: "var(--ds-font-body-small, normal 400 12px/16px \"Atlassian Sans\", ui-sans-serif, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Ubuntu, \"Helvetica Neue\", sans-serif)", fontWeight: "var(--ds-font-weight-bold, 653)", textOverflow: 'ellipsis', whiteSpace: 'nowrap', color: "var(--ds-text-warning-inverse, #292A2E)" }); var lozengeStyleActiveTraditional = convertToInlineCss({ display: 'inline-flex', boxSizing: 'border-box', position: 'static', blockSize: 'min-content', borderRadius: "var(--ds-radius-small, 4px)", overflow: 'hidden', paddingInlineStart: "var(--ds-space-050, 4px)", paddingInlineEnd: "var(--ds-space-050, 4px)", backgroundColor: "var(--ds-background-accent-red-subtler-pressed, #FD9891)", font: "var(--ds-font-body-small, normal 400 12px/16px \"Atlassian Sans\", ui-sans-serif, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Ubuntu, \"Helvetica Neue\", sans-serif)", fontWeight: "var(--ds-font-weight-bold, 653)", textOverflow: 'ellipsis', whiteSpace: 'nowrap', color: "var(--ds-text-warning-inverse, #292A2E)" }); var getChangedContentStyle = function getChangedContentStyle(colorScheme) { var isActive = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; var isInserted = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; if (expValEquals('platform_editor_diff_plugin_extended', 'isEnabled', true) && isInserted) { if (colorScheme === 'traditional') { return isActive ? traditionalInsertStyleActive : traditionalInsertStyle; } return isActive ? editingStyleActive : editingStyle; } if (colorScheme === 'traditional') { return isActive ? deletedTraditionalContentStyleActive : deletedTraditionalContentStyle; } if (isActive) { return expValEquals('platform_editor_enghealth_a11y_jan_fixes', 'isEnabled', true) ? deletedContentStyleNewActive : deletedContentStyleActive; } return expValEquals('platform_editor_enghealth_a11y_jan_fixes', 'isEnabled', true) ? deletedContentStyleNew : deletedContentStyle; }; var getChangedNodeStyle = function getChangedNodeStyle(nodeName, colorScheme) { var isInserted = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; var isActive = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false; var isTraditional = colorScheme === 'traditional'; if (expValEquals('platform_editor_diff_plugin_extended', 'isEnabled', true) && isInserted) { if (shouldApplyStylesDirectly(nodeName)) { return undefined; } if (isTraditional) { return traditionalStyleNode; } return editingStyleNode; } switch (nodeName) { case 'blockquote': if (isTraditional) { return isActive ? deletedTraditionalStyleQuoteNodeActive : deletedTraditionalStyleQuoteNode; } return isActive ? deletedStyleQuoteNodeWithLozengeActive : deletedStyleQuoteNodeWithLozenge; case 'expand': case 'decisionList': if (isTraditional) { return isActive ? deletedTraditionalBlockOutlineActive : deletedTraditionalBlockOutline; } return isActive ? deletedBlockOutlineActive : deletedBlockOutline; case 'panel': case 'codeBlock': if (isTraditional) { return isActive ? deletedTraditionalBlockOutlineRoundedActive : deletedTraditionalBlockOutlineRounded; } return isActive ? deletedBlockOutlineRoundedActive : deletedBlockOutlineRounded; default: return undefined; } }; var shouldShowRemovedLozenge = function shouldShowRemovedLozenge(nodeName) { switch (nodeName) { case 'expand': case 'codeBlock': case 'mediaSingle': case 'panel': case 'decisionList': case 'embedCard': case 'blockquote': return true; default: return false; } }; var shouldAddShowDiffDeletedNodeClass = function shouldAddShowDiffDeletedNodeClass(nodeName) { switch (nodeName) { case 'mediaSingle': case 'embedCard': case 'blockquote': return true; default: return false; } }; /** * Checks if a node should apply deleted styles directly without wrapper * to preserve natural block-level margins */ var shouldApplyStylesDirectly = function shouldApplyStylesDirectly(nodeName) { return nodeName === 'heading'; }; var applyCellOverlayStyles = function applyCellOverlayStyles(_ref) { var element = _ref.element, colorScheme = _ref.colorScheme, isInserted = _ref.isInserted; element.querySelectorAll('td, th').forEach(function (cell) { var overlay = document.createElement('span'); var overlayStyle = colorScheme === 'traditional' ? isInserted ? traditionalAddedCellOverlayStyle : deletedTraditionalCellOverlayStyle : isInserted ? addedCellOverlayStyle : deletedCellOverlayStyle; overlay.setAttribute('style', overlayStyle); cell.appendChild(overlay); }); }; /** * Creates a "Removed" lozenge to be displayed at the top right corner of deleted block nodes */ var createRemovedLozenge = function createRemovedLozenge(intl) { var isActive = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; var colorScheme = arguments.length > 2 ? arguments[2] : undefined; var container = document.createElement('span'); var containerStyle = convertToInlineCss({ position: 'absolute', top: "var(--ds-space-075, 6px)", right: "var(--ds-space-075, 6px)", zIndex: 2, pointerEvents: 'none', display: 'flex' }); container.setAttribute('style', containerStyle); container.setAttribute('data-testid', 'show-diff-removed-lozenge'); // Create vanilla HTML lozenge element with Atlaskit Lozenge styling (visual refresh) var lozengeElement = document.createElement('span'); var lozengeInnerStyle = isActive && colorScheme === 'traditional' ? lozengeStyleActiveTraditional : isActive ? lozengeStyleActiveStandard : lozengeStyle; lozengeElement.setAttribute('style', lozengeInnerStyle); lozengeElement.textContent = intl.formatMessage(trackChangesMessages.removed).toUpperCase(); container.appendChild(lozengeElement); return container; }; /** * Wraps a block node in a container with relative positioning to support absolute positioned lozenge */ var createBlockNodeWrapper = function createBlockNodeWrapper() { var wrapper = document.createElement('div'); var baseStyle = convertToInlineCss({ position: 'relative', display: 'block', opacity: 1 }); wrapper.setAttribute('style', baseStyle); return wrapper; }; /** * Applies styles directly to an HTML element by merging with existing styles */ var applyStylesToElement = function applyStylesToElement(_ref2) { var element = _ref2.element, targetNode = _ref2.targetNode, colorScheme = _ref2.colorScheme, isActive = _ref2.isActive, isInserted = _ref2.isInserted; var currentStyle = element.getAttribute('style') || ''; var contentStyle = getChangedContentStyle(colorScheme, isActive, isInserted); var nodeSpecificStyle = getChangedNodeStyle(targetNode.type.name, colorScheme, isInserted, isActive) || ''; element.setAttribute('style', "".concat(currentStyle).concat(contentStyle).concat(nodeSpecificStyle)); }; /** * Creates a content wrapper with deleted styles for a block node */ var createBlockNodeContentWrapper = function createBlockNodeContentWrapper(_ref3) { var nodeView = _ref3.nodeView, targetNode = _ref3.targetNode, colorScheme = _ref3.colorScheme, isActive = _ref3.isActive, isInserted = _ref3.isInserted; var contentWrapper = document.createElement('div'); var nodeStyle = getChangedNodeStyle(targetNode.type.name, colorScheme, isInserted, isActive); contentWrapper.setAttribute('style', "".concat(getChangedContentStyle(colorScheme, isActive, isInserted)).concat(nodeStyle || '')); contentWrapper.append(nodeView); return contentWrapper; }; /** * Handles embedCard node rendering with lozenge attached to the rich-media-item container. * Since embedCard content loads asynchronously, we use a MutationObserver * to wait for the rich-media-item to appear before attaching the lozenge. * @returns true if embedCard was handled */ var handleEmbedCardWithLozenge = function handleEmbedCardWithLozenge(_ref4) { var dom = _ref4.dom, nodeView = _ref4.nodeView, targetNode = _ref4.targetNode, lozenge = _ref4.lozenge, colorScheme = _ref4.colorScheme, _ref4$isActive = _ref4.isActive, isActive = _ref4$isActive === void 0 ? false : _ref4$isActive; if (targetNode.type.name !== 'embedCard' || !(nodeView instanceof HTMLElement)) { return false; } var richMediaItem = nodeView.querySelector('.rich-media-item'); if (richMediaItem instanceof HTMLElement) { richMediaItem.appendChild(lozenge); } else { var observer = new MutationObserver(function (_, obs) { var loadedRichMedia = nodeView.querySelector('.rich-media-item'); if (loadedRichMedia instanceof HTMLElement) { loadedRichMedia.appendChild(lozenge); obs.disconnect(); } }); observer.observe(nodeView, { childList: true, subtree: true }); } if (shouldAddShowDiffDeletedNodeClass(targetNode.type.name)) { var showDiffDeletedNodeClass = colorScheme === 'traditional' ? 'show-diff-deleted-node-traditional' : 'show-diff-deleted-node'; nodeView.classList.add(showDiffDeletedNodeClass); if (isActive) { nodeView.classList.add('show-diff-deleted-active'); } } dom.append(nodeView); return true; }; /** * Handles special mediaSingle node rendering with lozenge on child media element * @returns true if mediaSingle was handled, false otherwise */ var handleMediaSingleWithLozenge = function handleMediaSingleWithLozenge(_ref5) { var dom = _ref5.dom, nodeView = _ref5.nodeView, targetNode = _ref5.targetNode, lozenge = _ref5.lozenge, colorScheme = _ref5.colorScheme, _ref5$isActive = _ref5.isActive, isActive = _ref5$isActive === void 0 ? false : _ref5$isActive; if (targetNode.type.name !== 'mediaSingle' || !(nodeView instanceof HTMLElement)) { return false; } var mediaNode = nodeView.querySelector('[data-prosemirror-node-name="media"]'); if (!mediaNode || !(mediaNode instanceof HTMLElement)) { return false; } // Add relative positioning to media node to anchor lozenge var currentStyle = mediaNode.getAttribute('style') || ''; var relativePositionStyle = convertToInlineCss({ position: 'relative' }); mediaNode.setAttribute('style', "".concat(currentStyle).concat(relativePositionStyle)); mediaNode.append(lozenge); // Add deleted node class if needed if (shouldAddShowDiffDeletedNodeClass(targetNode.type.name)) { var showDiffDeletedNodeClass = colorScheme === 'traditional' ? 'show-diff-deleted-node-traditional' : 'show-diff-deleted-node'; nodeView.classList.add(showDiffDeletedNodeClass); if (isActive) { nodeView.classList.add('show-diff-deleted-active'); } } dom.append(nodeView); return true; }; /** * Appends a block node with wrapper, lozenge, and appropriate styling */ var wrapBlockNode = function wrapBlockNode(_ref6) { var dom = _ref6.dom, nodeView = _ref6.nodeView, targetNode = _ref6.targetNode, colorScheme = _ref6.colorScheme, intl = _ref6.intl, _ref6$isActive = _ref6.isActive, isActive = _ref6$isActive === void 0 ? false : _ref6$isActive, _ref6$isInserted = _ref6.isInserted, isInserted = _ref6$isInserted === void 0 ? false : _ref6$isInserted; var blockWrapper = createBlockNodeWrapper(); if (shouldShowRemovedLozenge(targetNode.type.name) && (!expValEquals('platform_editor_diff_plugin_extended', 'isEnabled', true) || !isInserted)) { var lozenge = createRemovedLozenge(intl, isActive, colorScheme); if (handleEmbedCardWithLozenge({ dom: dom, nodeView: nodeView, targetNode: targetNode, lozenge: lozenge, colorScheme: colorScheme, isActive: isActive })) { return; } if (handleMediaSingleWithLozenge({ dom: dom, nodeView: nodeView, targetNode: targetNode, lozenge: lozenge, colorScheme: colorScheme, isActive: isActive })) { return; } blockWrapper.append(lozenge); } var contentWrapper = createBlockNodeContentWrapper({ nodeView: nodeView, targetNode: targetNode, colorScheme: colorScheme, isActive: isActive, isInserted: isInserted }); blockWrapper.append(contentWrapper); if (nodeView instanceof HTMLElement && shouldAddShowDiffDeletedNodeClass(targetNode.type.name)) { var showDiffDeletedNodeClass = colorScheme === 'traditional' ? 'show-diff-deleted-node-traditional' : 'show-diff-deleted-node'; nodeView.classList.add(showDiffDeletedNodeClass); if (isActive) { nodeView.classList.add('show-diff-deleted-active'); } } dom.append(blockWrapper); }; /** * Handles all block node rendering with appropriate deleted styling. * For heading nodes, applies styles directly to preserve natural margins. * For other block nodes, uses wrapper approach with optional lozenge. */ export var wrapBlockNodeView = function wrapBlockNodeView(_ref7) { var dom = _ref7.dom, nodeView = _ref7.nodeView, targetNode = _ref7.targetNode, colorScheme = _ref7.colorScheme, intl = _ref7.intl, _ref7$isActive = _ref7.isActive, isActive = _ref7$isActive === void 0 ? false : _ref7$isActive, _ref7$isInserted = _ref7.isInserted, isInserted = _ref7$isInserted === void 0 ? false : _ref7$isInserted; if (shouldApplyStylesDirectly(targetNode.type.name) && nodeView instanceof HTMLElement) { // Apply deleted styles directly to preserve natural block-level margins applyStylesToElement({ element: nodeView, targetNode: targetNode, colorScheme: colorScheme, isActive: isActive, isInserted: isInserted }); dom.append(nodeView); } else if (targetNode.type.name === 'table' && nodeView instanceof HTMLElement && expValEquals('platform_editor_diff_plugin_extended', 'isEnabled', true)) { applyCellOverlayStyles({ element: nodeView, colorScheme: colorScheme, isInserted: isInserted }); dom.append(nodeView); } else { // Use wrapper approach for other block nodes wrapBlockNode({ dom: dom, nodeView: nodeView, targetNode: targetNode, colorScheme: colorScheme, intl: intl, isActive: isActive, isInserted: isInserted }); } };