]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/nodes/callout.ts
Lexical: Added id support for all main block types
[bookstack] / resources / js / wysiwyg / nodes / callout.ts
1 import {
2     $createParagraphNode,
3     DOMConversion,
4     DOMConversionMap, DOMConversionOutput,
5     ElementNode,
6     LexicalEditor,
7     LexicalNode,
8     ParagraphNode, SerializedElementNode, Spread
9 } from 'lexical';
10 import type {EditorConfig} from "lexical/LexicalEditor";
11 import type {RangeSelection} from "lexical/LexicalSelection";
12 import {el} from "../utils/dom";
13
14 export type CalloutCategory = 'info' | 'danger' | 'warning' | 'success';
15
16 export type SerializedCalloutNode = Spread<{
17     category: CalloutCategory;
18     id: string;
19 }, SerializedElementNode>
20
21 export class CalloutNode extends ElementNode {
22     __id: string = '';
23     __category: CalloutCategory = 'info';
24
25     static getType() {
26         return 'callout';
27     }
28
29     static clone(node: CalloutNode) {
30         const newNode = new CalloutNode(node.__category, node.__key);
31         newNode.__id = node.__id;
32         return newNode;
33     }
34
35     constructor(category: CalloutCategory, key?: string) {
36         super(key);
37         this.__category = category;
38     }
39
40     setCategory(category: CalloutCategory) {
41         const self = this.getWritable();
42         self.__category = category;
43     }
44
45     getCategory(): CalloutCategory {
46         const self = this.getLatest();
47         return self.__category;
48     }
49
50     setId(id: string) {
51         const self = this.getWritable();
52         self.__id = id;
53     }
54
55     getId(): string {
56         const self = this.getLatest();
57         return self.__id;
58     }
59
60     createDOM(_config: EditorConfig, _editor: LexicalEditor) {
61         const element = document.createElement('p');
62         element.classList.add('callout', this.__category || '');
63         if (this.__id) {
64             element.setAttribute('id', this.__id);
65         }
66         return element;
67     }
68
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.
72         return false;
73     }
74
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);
79
80         newElement.setDirection(this.getDirection());
81         this.insertAfter(newElement, restoreSelection);
82
83         if (anchorOffset === 0 && !this.isEmpty() && selection) {
84             const paragraph = $createParagraphNode();
85             paragraph.select();
86             this.replace(paragraph, true);
87         }
88
89         return newElement;
90     }
91
92     static importDOM(): DOMConversionMap|null {
93         return {
94             p(node: HTMLElement): DOMConversion|null {
95                 if (node.classList.contains('callout')) {
96                     return {
97                         conversion: (element: HTMLElement): DOMConversionOutput|null => {
98                             let category: CalloutCategory = 'info';
99                             const categories: CalloutCategory[] = ['info', 'success', 'warning', 'danger'];
100
101                             for (const c of categories) {
102                                 if (element.classList.contains(c)) {
103                                     category = c;
104                                     break;
105                                 }
106                             }
107
108                             const node = new CalloutNode(category);
109                             if (element.id) {
110                                 node.setId(element.id);
111                             }
112
113                             return {
114                                 node,
115                             };
116                         },
117                         priority: 3,
118                     };
119                 }
120                 return null;
121             },
122         };
123     }
124
125     exportJSON(): SerializedCalloutNode {
126         return {
127             ...super.exportJSON(),
128             type: 'callout',
129             version: 1,
130             category: this.__category,
131             id: this.__id,
132         };
133     }
134
135     static importJSON(serializedNode: SerializedCalloutNode): CalloutNode {
136         const node = $createCalloutNode(serializedNode.category);
137         node.setId(serializedNode.id);
138         return node;
139     }
140
141 }
142
143 export function $createCalloutNode(category: CalloutCategory = 'info') {
144     return new CalloutNode(category);
145 }
146
147 export function $isCalloutNode(node: LexicalNode | null | undefined): node is CalloutNode {
148     return node instanceof CalloutNode;
149 }
150
151 export function $isCalloutNodeOfCategory(node: LexicalNode | null | undefined, category: CalloutCategory = 'info') {
152     return node instanceof CalloutNode && (node as CalloutNode).getCategory() === category;
153 }