@atlaskit/editor-plugin-show-diff
Version:
ShowDiff plugin for @atlaskit/editor-core
262 lines (255 loc) • 12.3 kB
JavaScript
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;
};