]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/nodes/callout.ts
Lexical: Added custom alignment handling for blocks
[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,
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
30     static getType() {
31         return 'callout';
32     }
33
34     static clone(node: CalloutNode) {
35         const newNode = new CalloutNode(node.__category, node.__key);
36         newNode.__id = node.__id;
37         return newNode;
38     }
39
40     constructor(category: CalloutCategory, key?: string) {
41         super(key);
42         this.__category = category;
43     }
44
45     setCategory(category: CalloutCategory) {
46         const self = this.getWritable();
47         self.__category = category;
48     }
49
50     getCategory(): CalloutCategory {
51         const self = this.getLatest();
52         return self.__category;
53     }
54
55     setId(id: string) {
56         const self = this.getWritable();
57         self.__id = id;
58     }
59
60     getId(): string {
61         const self = this.getLatest();
62         return self.__id;
63     }
64
65     setAlignment(alignment: CommonBlockAlignment) {
66         const self = this.getWritable();
67         self.__alignment = alignment;
68     }
69
70     getAlignment(): CommonBlockAlignment {
71         const self = this.getLatest();
72         return self.__alignment;
73     }
74
75     createDOM(_config: EditorConfig, _editor: LexicalEditor) {
76         const element = document.createElement('p');
77         element.classList.add('callout', this.__category || '');
78         updateElementWithCommonBlockProps(element, this);
79         return element;
80     }
81
82     updateDOM(prevNode: CalloutNode): boolean {
83         return prevNode.__category !== this.__category ||
84             commonPropertiesDifferent(prevNode, this);
85     }
86
87     insertNewAfter(selection: RangeSelection, restoreSelection?: boolean): CalloutNode|ParagraphNode {
88         const anchorOffset = selection ? selection.anchor.offset : 0;
89         const newElement = anchorOffset === this.getTextContentSize() || !selection
90             ? $createParagraphNode() : $createCalloutNode(this.__category);
91
92         newElement.setDirection(this.getDirection());
93         this.insertAfter(newElement, restoreSelection);
94
95         if (anchorOffset === 0 && !this.isEmpty() && selection) {
96             const paragraph = $createParagraphNode();
97             paragraph.select();
98             this.replace(paragraph, true);
99         }
100
101         return newElement;
102     }
103
104     static importDOM(): DOMConversionMap|null {
105         return {
106             p(node: HTMLElement): DOMConversion|null {
107                 if (node.classList.contains('callout')) {
108                     return {
109                         conversion: (element: HTMLElement): DOMConversionOutput|null => {
110                             let category: CalloutCategory = 'info';
111                             const categories: CalloutCategory[] = ['info', 'success', 'warning', 'danger'];
112
113                             for (const c of categories) {
114                                 if (element.classList.contains(c)) {
115                                     category = c;
116                                     break;
117                                 }
118                             }
119
120                             const node = new CalloutNode(category);
121                             setCommonBlockPropsFromElement(element, node);
122
123                             return {
124                                 node,
125                             };
126                         },
127                         priority: 3,
128                     };
129                 }
130                 return null;
131             },
132         };
133     }
134
135     exportJSON(): SerializedCalloutNode {
136         return {
137             ...super.exportJSON(),
138             type: 'callout',
139             version: 1,
140             category: this.__category,
141             id: this.__id,
142             alignment: this.__alignment,
143         };
144     }
145
146     static importJSON(serializedNode: SerializedCalloutNode): CalloutNode {
147         const node = $createCalloutNode(serializedNode.category);
148         node.setId(serializedNode.id);
149         node.setAlignment(serializedNode.alignment);
150         return node;
151     }
152
153 }
154
155 export function $createCalloutNode(category: CalloutCategory = 'info') {
156     return new CalloutNode(category);
157 }
158
159 export function $isCalloutNode(node: LexicalNode | null | undefined): node is CalloutNode {
160     return node instanceof CalloutNode;
161 }
162
163 export function $isCalloutNodeOfCategory(node: LexicalNode | null | undefined, category: CalloutCategory = 'info') {
164     return node instanceof CalloutNode && (node as CalloutNode).getCategory() === category;
165 }