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