Lexical: Made summary part of details node

To provide more control of the summary as part of details.
To support, added a way to ignore elements during import DOM, allowing
up to read summaries when parsing details without duplicate nodes
involved.
This commit is contained in:
Dan Brown 2024-12-15 17:11:02 +00:00
parent 2f119d3033
commit 3f86937f74
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
4 changed files with 54 additions and 60 deletions

View File

@ -142,10 +142,15 @@ export type DOMConversionMap<T extends HTMLElement = HTMLElement> = Record<
>;
type NodeName = string;
/**
* Output for a DOM conversion.
* Node can be set to 'ignore' to ignore the conversion and handling of the DOMNode
* including all its children.
*/
export type DOMConversionOutput = {
after?: (childLexicalNodes: Array<LexicalNode>) => Array<LexicalNode>;
forChild?: DOMChildConversion;
node: null | LexicalNode | Array<LexicalNode>;
node: null | LexicalNode | Array<LexicalNode> | 'ignore';
};
export type DOMExportOutputMap = Map<

View File

@ -217,6 +217,11 @@ function $createNodesFromDOM(
if (transformOutput !== null) {
postTransform = transformOutput.after;
const transformNodes = transformOutput.node;
if (transformNodes === 'ignore') {
return lexicalNodes;
}
currentLexicalNode = Array.isArray(transformNodes)
? transformNodes[transformNodes.length - 1]
: transformNodes;

View File

@ -5,18 +5,19 @@ import {
LexicalEditor,
LexicalNode,
SerializedElementNode, Spread,
EditorConfig,
EditorConfig, DOMExportOutput,
} from 'lexical';
import {el} from "../../utils/dom";
import {extractDirectionFromElement} from "lexical/nodes/common";
export type SerializedDetailsNode = Spread<{
id: string;
summary: string;
}, SerializedElementNode>
export class DetailsNode extends ElementNode {
__id: string = '';
__summary: string = '';
static getType() {
return 'details';
@ -32,10 +33,21 @@ export class DetailsNode extends ElementNode {
return self.__id;
}
setSummary(summary: string) {
const self = this.getWritable();
self.__summary = summary;
}
getSummary(): string {
const self = this.getLatest();
return self.__summary;
}
static clone(node: DetailsNode): DetailsNode {
const newNode = new DetailsNode(node.__key);
newNode.__id = node.__id;
newNode.__dir = node.__dir;
newNode.__summary = node.__summary;
return newNode;
}
@ -49,6 +61,11 @@ export class DetailsNode extends ElementNode {
el.setAttribute('dir', this.__dir);
}
const summary = document.createElement('summary');
summary.textContent = this.__summary;
summary.setAttribute('contenteditable', 'false');
el.append(summary);
return el;
}
@ -71,20 +88,42 @@ export class DetailsNode extends ElementNode {
node.setDirection(extractDirectionFromElement(element));
}
const summaryElem = Array.from(element.children).find(e => e.nodeName === 'SUMMARY');
node.setSummary(summaryElem?.textContent || '');
return {node};
},
priority: 3,
};
},
summary(node: HTMLElement): DOMConversion|null {
return {
conversion: (element: HTMLElement): DOMConversionOutput|null => {
return {node: 'ignore'};
},
priority: 3,
};
},
};
}
exportDOM(editor: LexicalEditor): DOMExportOutput {
const element = this.createDOM(editor._config, editor);
const editable = element.querySelectorAll('[contenteditable]');
for (const elem of editable) {
elem.removeAttribute('contenteditable');
}
return {element};
}
exportJSON(): SerializedDetailsNode {
return {
...super.exportJSON(),
type: 'details',
version: 1,
id: this.__id,
summary: this.__summary,
};
}
@ -104,58 +143,3 @@ export function $createDetailsNode() {
export function $isDetailsNode(node: LexicalNode | null | undefined): node is DetailsNode {
return node instanceof DetailsNode;
}
export class SummaryNode extends ElementNode {
static getType() {
return 'summary';
}
static clone(node: SummaryNode) {
return new SummaryNode(node.__key);
}
createDOM(_config: EditorConfig, _editor: LexicalEditor) {
return el('summary');
}
updateDOM(prevNode: DetailsNode, dom: HTMLElement) {
return false;
}
static importDOM(): DOMConversionMap|null {
return {
summary(node: HTMLElement): DOMConversion|null {
return {
conversion: (element: HTMLElement): DOMConversionOutput|null => {
return {
node: new SummaryNode(),
};
},
priority: 3,
};
},
};
}
exportJSON(): SerializedElementNode {
return {
...super.exportJSON(),
type: 'summary',
version: 1,
};
}
static importJSON(serializedNode: SerializedElementNode): SummaryNode {
return $createSummaryNode();
}
}
export function $createSummaryNode(): SummaryNode {
return new SummaryNode();
}
export function $isSummaryNode(node: LexicalNode | null | undefined): node is SummaryNode {
return node instanceof SummaryNode;
}

View File

@ -8,7 +8,7 @@ import {
} from "lexical";
import {LinkNode} from "@lexical/link";
import {ImageNode} from "@lexical/rich-text/LexicalImageNode";
import {DetailsNode, SummaryNode} from "@lexical/rich-text/LexicalDetailsNode";
import {DetailsNode} from "@lexical/rich-text/LexicalDetailsNode";
import {ListItemNode, ListNode} from "@lexical/list";
import {TableCellNode, TableNode, TableRowNode} from "@lexical/table";
import {HorizontalRuleNode} from "@lexical/rich-text/LexicalHorizontalRuleNode";
@ -34,7 +34,7 @@ export function getNodesForPageEditor(): (KlassConstructor<typeof LexicalNode> |
TableCellNode,
ImageNode, // TODO - Alignment
HorizontalRuleNode,
DetailsNode, SummaryNode,
DetailsNode,
CodeBlockNode,
DiagramNode,
MediaNode, // TODO - Alignment