@atlaskit/editor-plugin-show-diff
Version:
ShowDiff plugin for @atlaskit/editor-core
172 lines (160 loc) • 6.11 kB
JavaScript
import { DOMSerializer } from '@atlaskit/editor-prosemirror/model';
/**
* 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;
}
/**
* 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 class NodeViewSerializer {
constructor(params) {
var _params$blocklist;
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.
*/
init(params) {
var _params$editorView, _ref, _this$editorView;
this.serializer = DOMSerializer.fromSchema(params.editorView.state.schema);
if (isEditorViewWithNodeViews(params.editorView)) {
this.editorView = params.editorView;
}
const 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.
*/
appendChildNodes(children, contentDOM) {
children.forEach(child => {
const childNode = this.tryCreateNodeView(child) || this.serializeNode(child);
if (childNode) {
contentDOM === null || contentDOM === void 0 ? void 0 : contentDOM.append(childNode);
}
});
}
/**
* 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.
*/
tryCreateNodeView(targetNode) {
var _this$nodeViews;
if (!this.editorView) {
return null;
}
const constructor = (_this$nodeViews = this.nodeViews) === null || _this$nodeViews === void 0 ? void 0 : _this$nodeViews[targetNode.type.name];
if (this.nodeViewBlocklist.has(targetNode.type.name)) {
return null;
}
try {
if (!constructor) {
var _targetNode$type$spec, _targetNode$type$spec2;
if (targetNode.isInline) {
return null;
}
const 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;
}
const {
dom,
contentDOM
} = DOMSerializer.renderSpec(document, toDOMResult);
if (dom instanceof HTMLElement) {
if (targetNode.type.name === 'paragraph' && targetNode.children.length === 1) {
return this.serializeFragment(targetNode.content);
}
// Iteratively populate children
this.appendChildNodes(targetNode.children, contentDOM);
}
return dom;
}
const {
dom,
contentDOM
} = constructor(targetNode, this.editorView, () => 0, [], {});
// Iteratively populate children
this.appendChildNodes(targetNode.children, contentDOM);
return dom;
} catch (e) {
return null;
}
}
/**
* Serializes a node to a DOM `Node` using the schema's `DOMSerializer`.
*/
serializeNode(node) {
if (!this.serializer) {
throw new Error('NodeViewSerializer must be initialized with init() before use');
}
try {
return this.serializer.serializeNode(node);
} catch (e) {
return null;
}
}
/**
* Serializes a fragment to a `DocumentFragment` using the schema's `DOMSerializer`.
*/
serializeFragment(fragment) {
if (!this.serializer) {
throw new Error('NodeViewSerializer must be initialized with init() before use');
}
try {
return this.serializer.serializeFragment(fragment);
} catch (e) {
return null;
}
}
/**
* Returns a copy of the current node view blocklist.
*/
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
*/
getFilteredNodeViewBlocklist(excludeTypes) {
const filtered = new Set(this.nodeViewBlocklist);
excludeTypes.forEach(type => filtered.delete(type));
return filtered;
}
}