UNPKG

@atlaskit/editor-plugin-show-diff

Version:

ShowDiff plugin for @atlaskit/editor-core

313 lines (295 loc) • 15.4 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.NodeViewSerializer = void 0; exports.isEditorViewWithNodeViews = isEditorViewWithNodeViews; var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck")); var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass")); var _expand = require("@atlaskit/editor-common/expand"); var _model = require("@atlaskit/editor-prosemirror/model"); var _utils = require("@atlaskit/editor-prosemirror/utils"); var _isExperimentEnabled = require("@atlaskit/platform-feature-experiments/is-experiment-enabled"); var _fg = require("@atlaskit/platform-feature-flags/fg"); var _createEditorProxy = require("./createEditorProxy"); var _createInlineChangedDecoration = require("./decorations/createInlineChangedDecoration"); var _getAttrChangeRanges = require("./decorations/utils/getAttrChangeRanges"); var _markViews = require("./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. */ 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 || (0, _utils.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 */ var NodeViewSerializer = exports.NodeViewSerializer = /*#__PURE__*/function () { function NodeViewSerializer(params) { var _params$blocklist; (0, _classCallCheck2.default)(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 (0, _createClass2.default)(NodeViewSerializer, [{ key: "init", value: function init(params) { var _params$editorView, _ref, _this$editorView; this.serializer = _model.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 && (0, _isExperimentEnabled.isExperimentEnabled)('platform_editor_show_diff_deleted_nodeview_content') && (0, _getAttrChangeRanges.isInlineAttrChangeNodeName)(child.type.name) && childNode instanceof HTMLElement) { var _childNode$getAttribu; var _getAtomicInlineChang = (0, _createInlineChangedDecoration.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 = (0, _fg.fg)('platform_editor_ai_show_diff_patch_2') && containsTable(targetNode) ? (0, _createEditorProxy.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 = (0, _isExperimentEnabled.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 || (0, _isExperimentEnabled.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 = _model.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 (!(0, _isExperimentEnabled.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) && !_expand.expandedState.has(targetNode) && (0, _fg.fg)('platform_editor_ai_show_diff_patch_2')) { _expand.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 (0, _markViews.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; } }]); }();