From 3f86937f74f2e5dc1e11fc2fd84694ce0793308f Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sun, 15 Dec 2024 17:11:02 +0000 Subject: [PATCH] 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. --- .../js/wysiwyg/lexical/core/LexicalNode.ts | 7 +- resources/js/wysiwyg/lexical/html/index.ts | 5 + .../lexical/rich-text/LexicalDetailsNode.ts | 98 ++++++++----------- resources/js/wysiwyg/nodes.ts | 4 +- 4 files changed, 54 insertions(+), 60 deletions(-) diff --git a/resources/js/wysiwyg/lexical/core/LexicalNode.ts b/resources/js/wysiwyg/lexical/core/LexicalNode.ts index c6bc2e642..a6c9b6023 100644 --- a/resources/js/wysiwyg/lexical/core/LexicalNode.ts +++ b/resources/js/wysiwyg/lexical/core/LexicalNode.ts @@ -142,10 +142,15 @@ export type DOMConversionMap = 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) => Array; forChild?: DOMChildConversion; - node: null | LexicalNode | Array; + node: null | LexicalNode | Array | 'ignore'; }; export type DOMExportOutputMap = Map< diff --git a/resources/js/wysiwyg/lexical/html/index.ts b/resources/js/wysiwyg/lexical/html/index.ts index 3e962ec72..5c3cb6cce 100644 --- a/resources/js/wysiwyg/lexical/html/index.ts +++ b/resources/js/wysiwyg/lexical/html/index.ts @@ -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; diff --git a/resources/js/wysiwyg/lexical/rich-text/LexicalDetailsNode.ts b/resources/js/wysiwyg/lexical/rich-text/LexicalDetailsNode.ts index 178b0d953..18d471103 100644 --- a/resources/js/wysiwyg/lexical/rich-text/LexicalDetailsNode.ts +++ b/resources/js/wysiwyg/lexical/rich-text/LexicalDetailsNode.ts @@ -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; -} diff --git a/resources/js/wysiwyg/nodes.ts b/resources/js/wysiwyg/nodes.ts index eb836bdce..8a47f322d 100644 --- a/resources/js/wysiwyg/nodes.ts +++ b/resources/js/wysiwyg/nodes.ts @@ -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 | TableCellNode, ImageNode, // TODO - Alignment HorizontalRuleNode, - DetailsNode, SummaryNode, + DetailsNode, CodeBlockNode, DiagramNode, MediaNode, // TODO - Alignment