]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/nodes/callout.ts
Move settings category layouts into their own view folder
[bookstack] / resources / js / wysiwyg / nodes / callout.ts
1 import {
2     $createParagraphNode,
3     DOMConversion,
4     DOMConversionMap, DOMConversionOutput,
5     ElementNode,
6     LexicalEditor,
7     LexicalNode,
8     ParagraphNode, Spread
9 } from 'lexical';
10 import type {EditorConfig} from "lexical/LexicalEditor";
11 import type {RangeSelection} from "lexical/LexicalSelection";
12 import {
13     CommonBlockAlignment, commonPropertiesDifferent, deserializeCommonBlockNode,
14     SerializedCommonBlockNode,
15     setCommonBlockPropsFromElement,
16     updateElementWithCommonBlockProps
17 } from "./_common";
18
19 export type CalloutCategory = 'info' | 'danger' | 'warning' | 'success';
20
21 export type SerializedCalloutNode = Spread<{
22     category: CalloutCategory;
23 }, SerializedCommonBlockNode>
24
25 export class CalloutNode extends ElementNode {
26     __id: string = '';
27     __category: CalloutCategory = 'info';
28     __alignment: CommonBlockAlignment = '';
29     __inset: number = 0;
30
31     static getType() {
32         return 'callout';
33     }
34
35     static clone(node: CalloutNode) {
36         const newNode = new CalloutNode(node.__category, node.__key);
37         newNode.__id = node.__id;
38         newNode.__alignment = node.__alignment;
39         newNode.__inset = node.__inset;
40         return newNode;
41     }
42
43     constructor(category: CalloutCategory, key?: string) {
44         super(key);
45         this.__category = category;
46     }
47
48     setCategory(category: CalloutCategory) {
49         const self = this.getWritable();
50         self.__category = category;
51     }
52
53     getCategory(): CalloutCategory {
54         const self = this.getLatest();
55         return self.__category;
56     }
57
58     setId(id: string) {
59         const self = this.getWritable();
60         self.__id = id;
61     }
62
63     getId(): string {
64         const self = this.getLatest();
65         return self.__id;
66     }
67
68     setAlignment(alignment: CommonBlockAlignment) {
69         const self = this.getWritable();
70         self.__alignment = alignment;
71     }
72
73     getAlignment(): CommonBlockAlignment {
74         const self = this.getLatest();
75         return self.__alignment;
76     }
77
78     setInset(size: number) {
79         const self = this.getWritable();
80         self.__inset = size;
81     }
82
83     getInset(): number {
84         const self = this.getLatest();
85         return self.__inset;
86     }
87
88     createDOM(_config: EditorConfig, _editor: LexicalEditor) {
89         const element = document.createElement('p');
90         element.classList.add('callout', this.__category || '');
91         updateElementWithCommonBlockProps(element, this);
92         return element;
93     }
94
95     updateDOM(prevNode: CalloutNode): boolean {
96         return prevNode.__category !== this.__category ||
97             commonPropertiesDifferent(prevNode, this);
98     }
99
100     insertNewAfter(selection: RangeSelection, restoreSelection?: boolean): CalloutNode|ParagraphNode {
101         const anchorOffset = selection ? selection.anchor.offset : 0;
102         const newElement = anchorOffset === this.getTextContentSize() || !selection
103             ? $createParagraphNode() : $createCalloutNode(this.__category);
104
105         newElement.setDirection(this.getDirection());
106         this.insertAfter(newElement, restoreSelection);
107
108         if (anchorOffset === 0 && !this.isEmpty() && selection) {
109             const paragraph = $createParagraphNode();
110             paragraph.select();
111             this.replace(paragraph, true);
112         }
113
114         return newElement;
115     }
116
117     static importDOM(): DOMConversionMap|null {
118         return {
119             p(node: HTMLElement): DOMConversion|null {
120                 if (node.classList.contains('callout')) {
121                     return {
122                         conversion: (element: HTMLElement): DOMConversionOutput|null => {
123                             let category: CalloutCategory = 'info';
124                             const categories: CalloutCategory[] = ['info', 'success', 'warning', 'danger'];
125
126                             for (const c of categories) {
127                                 if (element.classList.contains(c)) {
128                                     category = c;
129                                     break;
130                                 }
131                             }
132
133                             const node = new CalloutNode(category);
134                             setCommonBlockPropsFromElement(element, node);
135
136                             return {
137                                 node,
138                             };
139                         },
140                         priority: 3,
141                     };
142                 }
143                 return null;
144             },
145         };
146     }
147
148     exportJSON(): SerializedCalloutNode {
149         return {
150             ...super.exportJSON(),
151             type: 'callout',
152             version: 1,
153             category: this.__category,
154             id: this.__id,
155             alignment: this.__alignment,
156             inset: this.__inset,
157         };
158     }
159
160     static importJSON(serializedNode: SerializedCalloutNode): CalloutNode {
161         const node = $createCalloutNode(serializedNode.category);
162         deserializeCommonBlockNode(serializedNode, node);
163         return node;
164     }
165
166 }
167
168 export function $createCalloutNode(category: CalloutCategory = 'info') {
169     return new CalloutNode(category);
170 }
171
172 export function $isCalloutNode(node: LexicalNode | null | undefined): node is CalloutNode {
173     return node instanceof CalloutNode;
174 }
175
176 export function $isCalloutNodeOfCategory(node: LexicalNode | null | undefined, category: CalloutCategory = 'info') {
177     return node instanceof CalloutNode && (node as CalloutNode).getCategory() === category;
178 }