4 DOMConversionMap, DOMConversionOutput,
8 ParagraphNode, SerializedElementNode, Spread
10 import type {EditorConfig} from "lexical/LexicalEditor";
11 import type {RangeSelection} from "lexical/LexicalSelection";
12 import {el} from "../utils/dom";
14 export type CalloutCategory = 'info' | 'danger' | 'warning' | 'success';
16 export type SerializedCalloutNode = Spread<{
17 category: CalloutCategory;
19 }, SerializedElementNode>
21 export class CalloutNode extends ElementNode {
23 __category: CalloutCategory = 'info';
29 static clone(node: CalloutNode) {
30 const newNode = new CalloutNode(node.__category, node.__key);
31 newNode.__id = node.__id;
35 constructor(category: CalloutCategory, key?: string) {
37 this.__category = category;
40 setCategory(category: CalloutCategory) {
41 const self = this.getWritable();
42 self.__category = category;
45 getCategory(): CalloutCategory {
46 const self = this.getLatest();
47 return self.__category;
51 const self = this.getWritable();
56 const self = this.getLatest();
60 createDOM(_config: EditorConfig, _editor: LexicalEditor) {
61 const element = document.createElement('p');
62 element.classList.add('callout', this.__category || '');
64 element.setAttribute('id', this.__id);
69 updateDOM(prevNode: unknown, dom: HTMLElement) {
70 // Returning false tells Lexical that this node does not need its
71 // DOM element replacing with a new copy from createDOM.
75 insertNewAfter(selection: RangeSelection, restoreSelection?: boolean): CalloutNode|ParagraphNode {
76 const anchorOffset = selection ? selection.anchor.offset : 0;
77 const newElement = anchorOffset === this.getTextContentSize() || !selection
78 ? $createParagraphNode() : $createCalloutNode(this.__category);
80 newElement.setDirection(this.getDirection());
81 this.insertAfter(newElement, restoreSelection);
83 if (anchorOffset === 0 && !this.isEmpty() && selection) {
84 const paragraph = $createParagraphNode();
86 this.replace(paragraph, true);
92 static importDOM(): DOMConversionMap|null {
94 p(node: HTMLElement): DOMConversion|null {
95 if (node.classList.contains('callout')) {
97 conversion: (element: HTMLElement): DOMConversionOutput|null => {
98 let category: CalloutCategory = 'info';
99 const categories: CalloutCategory[] = ['info', 'success', 'warning', 'danger'];
101 for (const c of categories) {
102 if (element.classList.contains(c)) {
108 const node = new CalloutNode(category);
110 node.setId(element.id);
125 exportJSON(): SerializedCalloutNode {
127 ...super.exportJSON(),
130 category: this.__category,
135 static importJSON(serializedNode: SerializedCalloutNode): CalloutNode {
136 const node = $createCalloutNode(serializedNode.category);
137 node.setId(serializedNode.id);
143 export function $createCalloutNode(category: CalloutCategory = 'info') {
144 return new CalloutNode(category);
147 export function $isCalloutNode(node: LexicalNode | null | undefined): node is CalloutNode {
148 return node instanceof CalloutNode;
151 export function $isCalloutNodeOfCategory(node: LexicalNode | null | undefined, category: CalloutCategory = 'info') {
152 return node instanceof CalloutNode && (node as CalloutNode).getCategory() === category;