BookStackApp_BookStack/resources/js/wysiwyg/lexical/rich-text/LexicalDetailsNode.ts
Dan Brown 5887322178
Lexical: Added details toolbar
Includes unwrap and toggle open actions.
2024-12-15 18:13:49 +00:00

176 lines
4.7 KiB
TypeScript

import {
DOMConversion,
DOMConversionMap, DOMConversionOutput,
ElementNode,
LexicalEditor,
LexicalNode,
SerializedElementNode, Spread,
EditorConfig, DOMExportOutput,
} from 'lexical';
import {extractDirectionFromElement} from "lexical/nodes/common";
export type SerializedDetailsNode = Spread<{
id: string;
summary: string;
}, SerializedElementNode>
export class DetailsNode extends ElementNode {
__id: string = '';
__summary: string = '';
__open: boolean = false;
static getType() {
return 'details';
}
setId(id: string) {
const self = this.getWritable();
self.__id = id;
}
getId(): string {
const self = this.getLatest();
return self.__id;
}
setSummary(summary: string) {
const self = this.getWritable();
self.__summary = summary;
}
getSummary(): string {
const self = this.getLatest();
return self.__summary;
}
setOpen(open: boolean) {
const self = this.getWritable();
self.__open = open;
}
getOpen(): boolean {
const self = this.getLatest();
return self.__open;
}
static clone(node: DetailsNode): DetailsNode {
const newNode = new DetailsNode(node.__key);
newNode.__id = node.__id;
newNode.__dir = node.__dir;
newNode.__summary = node.__summary;
newNode.__open = node.__open;
return newNode;
}
createDOM(_config: EditorConfig, _editor: LexicalEditor) {
const el = document.createElement('details');
if (this.__id) {
el.setAttribute('id', this.__id);
}
if (this.__dir) {
el.setAttribute('dir', this.__dir);
}
if (this.__open) {
el.setAttribute('open', 'true');
}
const summary = document.createElement('summary');
summary.textContent = this.__summary;
summary.setAttribute('contenteditable', 'false');
summary.addEventListener('click', event => {
event.preventDefault();
_editor.update(() => {
this.select();
})
});
el.append(summary);
return el;
}
updateDOM(prevNode: DetailsNode, dom: HTMLElement) {
if (prevNode.__open !== this.__open) {
dom.toggleAttribute('open', this.__open);
}
return prevNode.__id !== this.__id
|| prevNode.__dir !== this.__dir
|| prevNode.__summary !== this.__summary;
}
static importDOM(): DOMConversionMap|null {
return {
details(node: HTMLElement): DOMConversion|null {
return {
conversion: (element: HTMLElement): DOMConversionOutput|null => {
const node = new DetailsNode();
if (element.id) {
node.setId(element.id);
}
if (element.dir) {
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');
}
element.removeAttribute('open');
return {element};
}
exportJSON(): SerializedDetailsNode {
return {
...super.exportJSON(),
type: 'details',
version: 1,
id: this.__id,
summary: this.__summary,
};
}
static importJSON(serializedNode: SerializedDetailsNode): DetailsNode {
const node = $createDetailsNode();
node.setId(serializedNode.id);
node.setDirection(serializedNode.direction);
return node;
}
}
export function $createDetailsNode() {
return new DetailsNode();
}
export function $isDetailsNode(node: LexicalNode | null | undefined): node is DetailsNode {
return node instanceof DetailsNode;
}