]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/nodes/callout.ts
Lexical: Split helpers to utils, refactored files
[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
13 export type CalloutCategory = 'info' | 'danger' | 'warning' | 'success';
14
15 export type SerializedCalloutNode = Spread<{
16     category: CalloutCategory;
17 }, SerializedElementNode>
18
19 export class CalloutNode extends ElementNode {
20
21     __category: CalloutCategory = 'info';
22
23     static getType() {
24         return 'callout';
25     }
26
27     static clone(node: CalloutNode) {
28         return new CalloutNode(node.__category, node.__key);
29     }
30
31     constructor(category: CalloutCategory, key?: string) {
32         super(key);
33         this.__category = category;
34     }
35
36     setCategory(category: CalloutCategory) {
37         const self = this.getWritable();
38         self.__category = category;
39     }
40
41     getCategory(): CalloutCategory {
42         const self = this.getLatest();
43         return self.__category;
44     }
45
46     createDOM(_config: EditorConfig, _editor: LexicalEditor) {
47         const element = document.createElement('p');
48         element.classList.add('callout', this.__category || '');
49         return element;
50     }
51
52     updateDOM(prevNode: unknown, dom: HTMLElement) {
53         // Returning false tells Lexical that this node does not need its
54         // DOM element replacing with a new copy from createDOM.
55         return false;
56     }
57
58     insertNewAfter(selection: RangeSelection, restoreSelection?: boolean): CalloutNode|ParagraphNode {
59         const anchorOffset = selection ? selection.anchor.offset : 0;
60         const newElement = anchorOffset === this.getTextContentSize() || !selection
61             ? $createParagraphNode() : $createCalloutNode(this.__category);
62
63         newElement.setDirection(this.getDirection());
64         this.insertAfter(newElement, restoreSelection);
65
66         if (anchorOffset === 0 && !this.isEmpty() && selection) {
67             const paragraph = $createParagraphNode();
68             paragraph.select();
69             this.replace(paragraph, true);
70         }
71
72         return newElement;
73     }
74
75     static importDOM(): DOMConversionMap|null {
76         return {
77             p(node: HTMLElement): DOMConversion|null {
78                 if (node.classList.contains('callout')) {
79                     return {
80                         conversion: (element: HTMLElement): DOMConversionOutput|null => {
81                             let category: CalloutCategory = 'info';
82                             const categories: CalloutCategory[] = ['info', 'success', 'warning', 'danger'];
83
84                             for (const c of categories) {
85                                 if (element.classList.contains(c)) {
86                                     category = c;
87                                     break;
88                                 }
89                             }
90
91                             return {
92                                 node: new CalloutNode(category),
93                             };
94                         },
95                         priority: 3,
96                     };
97                 }
98                 return null;
99             },
100         };
101     }
102
103     exportJSON(): SerializedCalloutNode {
104         return {
105             ...super.exportJSON(),
106             type: 'callout',
107             version: 1,
108             category: this.__category,
109         };
110     }
111
112     static importJSON(serializedNode: SerializedCalloutNode): CalloutNode {
113         return $createCalloutNode(serializedNode.category);
114     }
115
116 }
117
118 export function $createCalloutNode(category: CalloutCategory = 'info') {
119     return new CalloutNode(category);
120 }
121
122 export function $isCalloutNode(node: LexicalNode | null | undefined) {
123     return node instanceof CalloutNode;
124 }
125
126 export function $isCalloutNodeOfCategory(node: LexicalNode | null | undefined, category: CalloutCategory = 'info') {
127     return node instanceof CalloutNode && (node as CalloutNode).getCategory() === category;
128 }