UNPKG

@atlaskit/editor-plugin-show-diff

Version:

ShowDiff plugin for @atlaskit/editor-core

262 lines (255 loc) 12.3 kB
import _toConsumableArray from "@babel/runtime/helpers/toConsumableArray"; import { convertToInlineCss } from '@atlaskit/editor-common/lazy-node-view'; import { Decoration } from '@atlaskit/editor-prosemirror/view'; import { fg } from '@atlaskit/platform-feature-flags'; import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals'; import { editingStyle, editingStyleActive, deletedContentStyle, deletedContentStyleActive, deletedContentStyleNew, deletedContentStyleNewActive, deletedContentStyleUnbounded } from './colorSchemes/standard'; import { traditionalInsertStyle, traditionalInsertStyleActive, deletedTraditionalContentStyle, deletedTraditionalContentStyleActive, deletedTraditionalContentStyleUnbounded, deletedTraditionalContentStyleUnboundedActive } from './colorSchemes/traditional'; import { createChangedRowDecorationWidgets } from './createChangedRowDecorationWidgets'; import { findSafeInsertPos } from './utils/findSafeInsertPos'; import { wrapBlockNodeView } from './utils/wrapBlockNodeView'; var getDeletedContentStyleUnbounded = function getDeletedContentStyleUnbounded(colorScheme) { var isActive = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; if (colorScheme === 'traditional' && isActive) { return deletedTraditionalContentStyleUnboundedActive; } return colorScheme === 'traditional' ? deletedTraditionalContentStyleUnbounded : deletedContentStyleUnbounded; }; var getInsertedContentStyle = function getInsertedContentStyle(colorScheme) { var isActive = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; if (colorScheme === 'traditional') { if (isActive) { return traditionalInsertStyleActive; } return traditionalInsertStyle; } if (isActive) { return editingStyleActive; } return editingStyle; }; var getDeletedContentStyle = function getDeletedContentStyle(colorScheme) { var isActive = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; 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; }; /** * Wraps content with deleted styling without opacity (for use when content is a direct child of dom) */ var createDeletedStyleWrapperWithoutOpacity = function createDeletedStyleWrapperWithoutOpacity(colorScheme, isActive) { var wrapper = document.createElement('span'); wrapper.setAttribute('style', getDeletedContentStyle(colorScheme, isActive)); return wrapper; }; /** * CSS backgrounds don't work when applied to a wrapper around a paragraph, so * the wrapper needs to be injected inside the node around the child content */ var injectInnerWrapper = function injectInnerWrapper(_ref) { var node = _ref.node, colorScheme = _ref.colorScheme, isActive = _ref.isActive, isInserted = _ref.isInserted; var wrapper = document.createElement('span'); wrapper.setAttribute('style', isInserted ? getInsertedContentStyle(colorScheme, isActive) : getDeletedContentStyle(colorScheme, isActive)); _toConsumableArray(node.childNodes).forEach(function (child) { var removedChild = node.removeChild(child); wrapper.append(removedChild); }); node.appendChild(wrapper); return node; }; var createContentWrapper = function createContentWrapper(colorScheme) { var isActive = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; var isInserted = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; var wrapper = document.createElement('span'); var baseStyle = convertToInlineCss({ position: 'relative', width: 'fit-content' }); if (expValEquals('platform_editor_diff_plugin_extended', 'isEnabled', true)) { if (isInserted) { wrapper.setAttribute('style', "".concat(baseStyle).concat(getInsertedContentStyle(colorScheme, isActive))); } else { wrapper.setAttribute('style', "".concat(baseStyle).concat(getDeletedContentStyle(colorScheme, isActive))); var strikethrough = document.createElement('span'); strikethrough.setAttribute('style', getDeletedContentStyleUnbounded(colorScheme, isActive)); wrapper.append(strikethrough); } } else { wrapper.setAttribute('style', "".concat(baseStyle).concat(getDeletedContentStyle(colorScheme, isActive))); var _strikethrough = document.createElement('span'); _strikethrough.setAttribute('style', getDeletedContentStyleUnbounded(colorScheme, isActive)); wrapper.append(_strikethrough); } return wrapper; }; /** * This function is used to create a decoration widget to show content * that is not in the current document. */ export var createNodeChangedDecorationWidget = function createNodeChangedDecorationWidget(_ref2) { var _slice$content, _slice$content2, _slice$content3; var change = _ref2.change, doc = _ref2.doc, nodeViewSerializer = _ref2.nodeViewSerializer, colorScheme = _ref2.colorScheme, newDoc = _ref2.newDoc, intl = _ref2.intl, activeIndexPos = _ref2.activeIndexPos, _ref2$isInserted = _ref2.isInserted, isInserted = _ref2$isInserted === void 0 ? false : _ref2$isInserted; var slice = doc.slice(change.fromA, change.toA); var shouldSkipDeletedEmptyParagraphDecoration = !isInserted && (slice === null || slice === void 0 || (_slice$content = slice.content) === null || _slice$content === void 0 ? void 0 : _slice$content.childCount) === 1 && (slice === null || slice === void 0 || (_slice$content2 = slice.content) === null || _slice$content2 === void 0 || (_slice$content2 = _slice$content2.firstChild) === null || _slice$content2 === void 0 ? void 0 : _slice$content2.type.name) === 'paragraph' && (slice === null || slice === void 0 || (_slice$content3 = slice.content) === null || _slice$content3 === void 0 || (_slice$content3 = _slice$content3.firstChild) === null || _slice$content3 === void 0 ? void 0 : _slice$content3.content.size) === 0 && fg('platform_editor_show_diff_scroll_navigation'); // Widget decoration used for deletions as the content is not in the document // and we want to display the deleted content with a style. var safeInsertPos = findSafeInsertPos(newDoc, change.fromB, slice); var isActive = activeIndexPos && safeInsertPos === activeIndexPos.from && safeInsertPos === activeIndexPos.to; if (slice.content.content.length === 0 || shouldSkipDeletedEmptyParagraphDecoration) { return; } var isTableCellContent = slice.content.content.some(function () { return slice.content.content.some(function (siblingNode) { return ['tableHeader', 'tableCell'].includes(siblingNode.type.name); }); }); var isTableRowContent = slice.content.content.some(function () { return slice.content.content.some(function (siblingNode) { return ['tableRow'].includes(siblingNode.type.name); }); }); if (isTableCellContent) { return; } if (isTableRowContent) { return createChangedRowDecorationWidgets({ changes: [change], originalDoc: doc, newDoc: newDoc, nodeViewSerializer: nodeViewSerializer, colorScheme: colorScheme, isInserted: isInserted }); } var serializer = nodeViewSerializer; // For non-table content, use the existing span wrapper approach var dom = document.createElement('span'); /* * The thinking is we separate out the fragment we got from doc.slice * and if it's the first or last content, we go in however many the sliced Open * or sliced End depth is and match only the entire node. */ slice.content.forEach(function (node) { // Helper function to handle multiple child nodes var handleMultipleChildNodes = function handleMultipleChildNodes(node) { if (node.content.childCount > 1 && node.type.inlineContent) { node.content.forEach(function (childNode) { var childNodeView = serializer.tryCreateNodeView(childNode); if (childNodeView) { var lineBreak = document.createElement('br'); dom.append(lineBreak); var wrapper = createContentWrapper(colorScheme, isActive, isInserted); wrapper.append(childNodeView); dom.append(wrapper); } else { // Fallback to serializing the individual child node var serializedChild = serializer.serializeNode(childNode); if (serializedChild) { var _wrapper = createContentWrapper(colorScheme, isActive, isInserted); _wrapper.append(serializedChild); dom.append(_wrapper); } } }); return true; // Indicates we handled multiple children } return false; // Indicates single child, continue with normal logic }; // Determine which node to use and how to serialize var isFirst = slice.content.firstChild === node; var isLast = slice.content.lastChild === node; var hasInlineContent = node.content.childCount > 0 && node.type.inlineContent === true; var fallbackSerialization; if (handleMultipleChildNodes(node)) { return; } if ((isFirst || isLast && slice.content.childCount > 2) && hasInlineContent) { fallbackSerialization = function fallbackSerialization() { return serializer.serializeFragment(node.content); }; } else if (isLast && slice.content.childCount === 2) { fallbackSerialization = function fallbackSerialization() { if (node.type.name === 'text') { return document.createTextNode(node.text || ''); } if (node.type.name === 'paragraph') { var lineBreak = document.createElement('br'); dom.append(lineBreak); return serializer.serializeFragment(node.content); } return serializer.serializeFragment(node.content); }; } else { fallbackSerialization = function fallbackSerialization() { return serializer.serializeNode(node); }; } // Try to create node view, fallback to serialization var nodeView = serializer.tryCreateNodeView(node); if (nodeView) { if (node.isInline) { var wrapper = createContentWrapper(colorScheme, isActive, isInserted); wrapper.append(nodeView); dom.append(wrapper); } else { // Handle all block nodes with unified function wrapBlockNodeView({ dom: dom, nodeView: nodeView, targetNode: node, colorScheme: colorScheme, intl: intl, isActive: isActive, isInserted: isInserted }); } } else if (nodeViewSerializer.getFilteredNodeViewBlocklist(['paragraph', 'tableRow']).has(node.type.name)) { // Skip the case where the node is a paragraph or table row that way it can still be rendered and delete the entire table return; } else { var fallbackNode = fallbackSerialization(); if (fallbackNode) { if (expValEquals('platform_editor_diff_plugin_extended', 'isEnabled', true) || fg('platform_editor_show_diff_scroll_navigation')) { if (fallbackNode instanceof HTMLElement) { var injectedNode = injectInnerWrapper({ node: fallbackNode, colorScheme: colorScheme, isActive: isActive, isInserted: isInserted }); dom.append(injectedNode); } else { var _wrapper2 = createContentWrapper(colorScheme, isActive, isInserted); _wrapper2.append(fallbackNode); dom.append(_wrapper2); } } else { var _wrapper3 = createDeletedStyleWrapperWithoutOpacity(colorScheme, isActive); _wrapper3.append(fallbackNode); dom.append(_wrapper3); } } } }); dom.setAttribute('data-testid', 'show-diff-deleted-decoration'); var decorations = []; decorations.push(Decoration.widget(safeInsertPos, dom, { key: "diff-widget-".concat(isActive ? 'active' : 'inactive') })); return decorations; };