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; 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 = { export type DOMConversionOutput = {
after?: (childLexicalNodes: Array<LexicalNode>) => Array<LexicalNode>; after?: (childLexicalNodes: Array<LexicalNode>) => Array<LexicalNode>;
forChild?: DOMChildConversion; forChild?: DOMChildConversion;
node: null | LexicalNode | Array<LexicalNode>; node: null | LexicalNode | Array<LexicalNode> | 'ignore';
}; };
export type DOMExportOutputMap = Map< export type DOMExportOutputMap = Map<

View File

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

View File

@ -5,18 +5,19 @@ import {
LexicalEditor, LexicalEditor,
LexicalNode, LexicalNode,
SerializedElementNode, Spread, SerializedElementNode, Spread,
EditorConfig, EditorConfig, DOMExportOutput,
} from 'lexical'; } from 'lexical';
import {el} from "../../utils/dom";
import {extractDirectionFromElement} from "lexical/nodes/common"; import {extractDirectionFromElement} from "lexical/nodes/common";
export type SerializedDetailsNode = Spread<{ export type SerializedDetailsNode = Spread<{
id: string; id: string;
summary: string;
}, SerializedElementNode> }, SerializedElementNode>
export class DetailsNode extends ElementNode { export class DetailsNode extends ElementNode {
__id: string = ''; __id: string = '';
__summary: string = '';
static getType() { static getType() {
return 'details'; return 'details';
@ -32,10 +33,21 @@ export class DetailsNode extends ElementNode {
return self.__id; 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 { static clone(node: DetailsNode): DetailsNode {
const newNode = new DetailsNode(node.__key); const newNode = new DetailsNode(node.__key);
newNode.__id = node.__id; newNode.__id = node.__id;
newNode.__dir = node.__dir; newNode.__dir = node.__dir;
newNode.__summary = node.__summary;
return newNode; return newNode;
} }
@ -49,6 +61,11 @@ export class DetailsNode extends ElementNode {
el.setAttribute('dir', this.__dir); el.setAttribute('dir', this.__dir);
} }
const summary = document.createElement('summary');
summary.textContent = this.__summary;
summary.setAttribute('contenteditable', 'false');
el.append(summary);
return el; return el;
} }
@ -71,20 +88,42 @@ export class DetailsNode extends ElementNode {
node.setDirection(extractDirectionFromElement(element)); node.setDirection(extractDirectionFromElement(element));
} }
const summaryElem = Array.from(element.children).find(e => e.nodeName === 'SUMMARY');
node.setSummary(summaryElem?.textContent || '');
return {node}; return {node};
}, },
priority: 3, 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 { exportJSON(): SerializedDetailsNode {
return { return {
...super.exportJSON(), ...super.exportJSON(),
type: 'details', type: 'details',
version: 1, version: 1,
id: this.__id, id: this.__id,
summary: this.__summary,
}; };
} }
@ -104,58 +143,3 @@ export function $createDetailsNode() {
export function $isDetailsNode(node: LexicalNode | null | undefined): node is DetailsNode { export function $isDetailsNode(node: LexicalNode | null | undefined): node is DetailsNode {
return node instanceof 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"; } from "lexical";
import {LinkNode} from "@lexical/link"; import {LinkNode} from "@lexical/link";
import {ImageNode} from "@lexical/rich-text/LexicalImageNode"; 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 {ListItemNode, ListNode} from "@lexical/list";
import {TableCellNode, TableNode, TableRowNode} from "@lexical/table"; import {TableCellNode, TableNode, TableRowNode} from "@lexical/table";
import {HorizontalRuleNode} from "@lexical/rich-text/LexicalHorizontalRuleNode"; import {HorizontalRuleNode} from "@lexical/rich-text/LexicalHorizontalRuleNode";
@ -34,7 +34,7 @@ export function getNodesForPageEditor(): (KlassConstructor<typeof LexicalNode> |
TableCellNode, TableCellNode,
ImageNode, // TODO - Alignment ImageNode, // TODO - Alignment
HorizontalRuleNode, HorizontalRuleNode,
DetailsNode, SummaryNode, DetailsNode,
CodeBlockNode, CodeBlockNode,
DiagramNode, DiagramNode,
MediaNode, // TODO - Alignment MediaNode, // TODO - Alignment