@atlaskit/editor-plugin-show-diff
Version:
ShowDiff plugin for @atlaskit/editor-core
306 lines (288 loc) • 14.8 kB
JavaScript
import _classCallCheck from "@babel/runtime/helpers/classCallCheck";
import _createClass from "@babel/runtime/helpers/createClass";
import { expandedState } from '@atlaskit/editor-common/expand';
import { DOMSerializer } from '@atlaskit/editor-prosemirror/model';
import { contains } from '@atlaskit/editor-prosemirror/utils';
import { isExperimentEnabled } from '@atlaskit/platform-feature-experiments/is-experiment-enabled';
import { fg } from '@atlaskit/platform-feature-flags/fg';
import { createEditorProxy } from './createEditorProxy';
import { getAtomicInlineChangedAttrs } from './decorations/createInlineChangedDecoration';
import { isInlineAttrChangeNodeName } from './decorations/utils/getAttrChangeRanges';
import { wrapInMarkViews } from './markViews';
/**
* Utilities for working with ProseMirror node views and DOM serialization within the
* Show Diff editor plugin.
*
* This module centralizes:
* - Access to the editor's `nodeViews` registry (when available on `EditorView`)
* - Safe attempts to instantiate a node view for a given node, with a blocklist to
* avoid node types that are known to be problematic in this context (e.g. tables)
* - Schema-driven serialization of nodes and fragments to DOM via `DOMSerializer`
*
* The Show Diff decorations leverage this to either render nodes using their
* corresponding node view implementation, or fall back to DOM serialization.
*/
/**
* Narrowed `EditorView` that exposes the internal `nodeViews` registry.
* Many editor instances provide this, but it's not part of the base type.
*/
/**
* Type guard to detect whether an `EditorView` exposes a `nodeViews` map.
*/
export function isEditorViewWithNodeViews(view) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return view.nodeViews !== undefined;
}
/** Expand-family nodes whose expanded state lives in a node-keyed WeakMap. */
var EXPAND_TYPES = new Set(['expand', 'nestedExpand']);
/** Merges an inline style string onto an element's existing `style` attribute. */
var appendInlineStyle = function appendInlineStyle(element, style) {
var _element$getAttribute;
var currentStyle = (_element$getAttribute = element.getAttribute('style')) !== null && _element$getAttribute !== void 0 ? _element$getAttribute : '';
var separator = currentStyle && style && !currentStyle.trimEnd().endsWith(';') ? '; ' : '';
element.setAttribute('style', "".concat(currentStyle).concat(separator).concat(style));
};
/** Tables are the only node views that read and write positions, so only they need a preview. */
var containsTable = function containsTable(node) {
var table = node.type.schema.nodes.table;
// `contains` only checks descendants, so the node itself is checked separately.
return !!table && (node.type === table || contains(node, table));
};
/**
* Encapsulates DOM serialization and node view access/creation.
*
* Responsible for:
* - Creating a `DOMSerializer` from the provided schema
* - Reading `nodeViews` from an `EditorView` (if present) or using an explicit mapping
* - Preventing node view creation for blocklisted node types
*/
export var NodeViewSerializer = /*#__PURE__*/function () {
function NodeViewSerializer(params) {
var _params$blocklist;
_classCallCheck(this, NodeViewSerializer);
if (params !== null && params !== void 0 && params.editorView) {
this.init({
editorView: params.editorView
});
}
this.nodeViewBlocklist = new Set((_params$blocklist = params.blocklist) !== null && _params$blocklist !== void 0 ? _params$blocklist : ['paragraph']);
}
/**
* Initializes or reinitializes the NodeViewSerializer with a new EditorView.
* This allows the same serializer instance to be reused across different editor states.
*/
return _createClass(NodeViewSerializer, [{
key: "init",
value: function init(params) {
var _params$editorView, _ref, _this$editorView;
this.serializer = DOMSerializer.fromSchema(params.editorView.state.schema);
if (isEditorViewWithNodeViews(params.editorView)) {
this.editorView = params.editorView;
}
var nodeViews =
// eslint-disable-next-line @typescript-eslint/no-explicit-any
((_params$editorView = params.editorView) === null || _params$editorView === void 0 ? void 0 : _params$editorView.nodeViews) || {};
this.nodeViews = (_ref = nodeViews !== null && nodeViews !== void 0 ? nodeViews : (_this$editorView = this.editorView) === null || _this$editorView === void 0 ? void 0 : _this$editorView.nodeViews) !== null && _ref !== void 0 ? _ref : {};
}
/**
* Appends serialized child nodes to the given contentDOM element.
*/
}, {
key: "appendChildNodes",
value: function appendChildNodes(children, contentDOM) {
var _this = this;
var basePos = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0;
var editorProxy = arguments.length > 3 ? arguments[3] : undefined;
var isInserted = arguments.length > 4 ? arguments[4] : undefined;
var colorScheme = arguments.length > 5 ? arguments[5] : undefined;
// A node's first child sits one position inside it, and each subsequent
// child is offset by the previous child's `nodeSize`. Tracking this lets nested node
// views resolve to their real depth instead of every one of them seeing depth 0.
var childPos = basePos + 1;
children.forEach(function (child) {
var childNode = _this.tryCreateNodeViewInner(child, childPos, editorProxy, isInserted, colorScheme) || _this.serializeNode(child);
if (childNode) {
// Give inserted atomic children the same box-shadow
// highlight an in-place attribute change already gets (`getAtomicInlineChangedAttrs`).
if (isInserted && isExperimentEnabled('platform_editor_show_diff_deleted_nodeview_content') && isInlineAttrChangeNodeName(child.type.name) && childNode instanceof HTMLElement) {
var _childNode$getAttribu;
var _getAtomicInlineChang = getAtomicInlineChangedAttrs(child.type.name, colorScheme),
className = _getAtomicInlineChang.className,
styleSuffix = _getAtomicInlineChang.styleSuffix;
var existingClassName = (_childNode$getAttribu = childNode.getAttribute('class')) !== null && _childNode$getAttribu !== void 0 ? _childNode$getAttribu : '';
childNode.setAttribute('class', existingClassName ? "".concat(existingClassName, " ").concat(className) : className);
if (styleSuffix) {
appendInlineStyle(childNode, styleSuffix);
}
}
contentDOM === null || contentDOM === void 0 || contentDOM.append(childNode);
}
childPos += child.nodeSize;
});
}
/**
* Attempts to create a node view for the given node.
*
* Returns `null` when there is no `EditorView`, no constructor for the node type,
* or the node type is blocklisted. Otherwise returns the constructed node view instance.
*
* `$sourcePos` is where the node sits in the doc it came from, so nested tables keep their depth.
*/
}, {
key: "tryCreateNodeView",
value: function tryCreateNodeView(targetNode) {
var _preview$rootPos;
var basePos = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
var $sourcePos = arguments.length > 2 ? arguments[2] : undefined;
var isInserted = arguments.length > 3 ? arguments[3] : undefined;
var colorScheme = arguments.length > 4 ? arguments[4] : undefined;
if (!this.editorView) {
return null;
}
// The preview becomes the document every nested node view resolves its position against,
// so positions stay internally consistent.
var preview = fg('platform_editor_ai_show_diff_patch_2') && containsTable(targetNode) ? createEditorProxy(this.editorView, targetNode, $sourcePos) : null;
return this.tryCreateNodeViewInner(targetNode, (_preview$rootPos = preview === null || preview === void 0 ? void 0 : preview.rootPos) !== null && _preview$rootPos !== void 0 ? _preview$rootPos : basePos, preview === null || preview === void 0 ? void 0 : preview.editorProxy, isInserted, colorScheme);
}
}, {
key: "tryCreateNodeViewInner",
value: function tryCreateNodeViewInner(targetNode) {
var _this$nodeViews;
var basePos = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
var editorProxy = arguments.length > 2 ? arguments[2] : undefined;
var isInserted = arguments.length > 3 ? arguments[3] : undefined;
var colorScheme = arguments.length > 4 ? arguments[4] : undefined;
if (!this.editorView) {
return null;
}
var constructor = (_this$nodeViews = this.nodeViews) === null || _this$nodeViews === void 0 ? void 0 : _this$nodeViews[targetNode.type.name];
var isBlocklisted = this.nodeViewBlocklist.has(targetNode.type.name);
// Do not bail out a blocklisted container when it holds an atomic inline node
// (e.g. date, status) whose schema toDOM is lossy. Text keeps bailing so the caller
// renders it inline
var hasAtomicInlineChild = function hasAtomicInlineChild(node) {
var found = false;
node.forEach(function (child) {
if (child.isLeaf && !child.isText) {
found = true;
}
});
return found;
};
var serializeAtomicContainer = isExperimentEnabled('platform_editor_show_diff_deleted_nodeview_content') && !targetNode.isInline && hasAtomicInlineChild(targetNode);
if (isBlocklisted && !serializeAtomicContainer) {
return null;
}
try {
// No constructor, or (gated) the node's own view is blocklisted: render the toDOM
// shell and recurse via appendChildNodes so nested atomic inline nodes render via
// their node views instead of their lossy schema toDOM.
if (!constructor || isExperimentEnabled('platform_editor_show_diff_deleted_nodeview_content') && isBlocklisted) {
var _targetNode$type$spec, _targetNode$type$spec2;
if (targetNode.isInline) {
return null;
}
var toDOMResult = (_targetNode$type$spec = (_targetNode$type$spec2 = targetNode.type.spec).toDOM) === null || _targetNode$type$spec === void 0 ? void 0 : _targetNode$type$spec.call(_targetNode$type$spec2, targetNode);
if (!toDOMResult) {
return null;
}
var _DOMSerializer$render = DOMSerializer.renderSpec(document, toDOMResult),
_dom = _DOMSerializer$render.dom,
_contentDOM = _DOMSerializer$render.contentDOM;
if (_dom instanceof HTMLElement) {
// Legacy shortcut: a single-child paragraph serializes its inline content
// directly (no <p> wrapper). Applies only to a non-blocklisted paragraph with
// no node view
if (!isExperimentEnabled('platform_editor_show_diff_deleted_nodeview_content') && targetNode.type.name === 'paragraph' && targetNode.children.length === 1) {
return this.serializeFragment(targetNode.content);
}
this.appendChildNodes(targetNode.children, _contentDOM, basePos, editorProxy, isInserted, colorScheme);
}
return this.withMarkViews(targetNode, _dom, editorProxy);
}
// The expand node view reads `expandedState.get(node) ?? false`, never `attrs.__expanded`.
// Open the preview so its tables get real layout; an existing entry is the live expand's
// own state, so leave it alone.
if (EXPAND_TYPES.has(targetNode.type.name) && !expandedState.has(targetNode) && fg('platform_editor_ai_show_diff_patch_2')) {
expandedState.set(targetNode, true);
}
// `isTableNested` is `doc.resolve(pos).depth > 0`, so the old hardcoded `0` made nested
// tables size as top-level. Positions are resolved against the preview document, so
// they are only meaningful with an editor proxy.
var view = editorProxy !== null && editorProxy !== void 0 ? editorProxy : this.editorView;
var docSize = view.state.doc.content.size;
var resolvedPos = editorProxy ? Math.max(0, Math.min(basePos, docSize)) : 0;
var _constructor = constructor(targetNode, view, function () {
return resolvedPos;
}, [], {}),
dom = _constructor.dom,
contentDOM = _constructor.contentDOM;
// Iteratively populate children
this.appendChildNodes(targetNode.children, contentDOM, basePos, editorProxy, isInserted, colorScheme);
return this.withMarkViews(targetNode, dom, editorProxy);
} catch (_unused) {
return null;
}
}
/** Wraps rendered DOM in its marks, rendered against the editor proxy when there is one. */
}, {
key: "withMarkViews",
value: function withMarkViews(targetNode, nodeDom, editorProxy) {
return wrapInMarkViews(targetNode, nodeDom, {
nodeViews: this.nodeViews,
view: editorProxy !== null && editorProxy !== void 0 ? editorProxy : this.editorView
});
}
/**
* Serializes a node to a DOM `Node` using the schema's `DOMSerializer`.
*/
}, {
key: "serializeNode",
value: function serializeNode(node) {
if (!this.serializer) {
throw new Error('NodeViewSerializer must be initialized with init() before use');
}
try {
return this.serializer.serializeNode(node);
} catch (_unused2) {
return null;
}
}
/**
* Serializes a fragment to a `DocumentFragment` using the schema's `DOMSerializer`.
*/
}, {
key: "serializeFragment",
value: function serializeFragment(fragment) {
if (!this.serializer) {
throw new Error('NodeViewSerializer must be initialized with init() before use');
}
try {
return this.serializer.serializeFragment(fragment);
} catch (_unused3) {
return null;
}
}
/**
* Returns a copy of the current node view blocklist.
*/
}, {
key: "getNodeViewBlocklist",
value: function getNodeViewBlocklist() {
return new Set(this.nodeViewBlocklist);
}
/**
* Returns a filtered copy of the node view blocklist, excluding specified node types.
* @param excludeTypes - Array of node type names to exclude from the blocklist
*/
}, {
key: "getFilteredNodeViewBlocklist",
value: function getFilteredNodeViewBlocklist(excludeTypes) {
var filtered = new Set(this.nodeViewBlocklist);
excludeTypes.forEach(function (type) {
return filtered.delete(type);
});
return filtered;
}
}]);
}();