]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/lexical/rich-text/LexicalImageNode.ts
Opensearch: Fixed XML declaration when php short tags enabled
[bookstack] / resources / js / wysiwyg / lexical / rich-text / LexicalImageNode.ts
1 import {
2     DOMConversion,
3     DOMConversionMap,
4     DOMConversionOutput, ElementNode,
5     LexicalEditor, LexicalNode,
6     Spread
7 } from "lexical";
8 import type {EditorConfig} from "lexical/LexicalEditor";
9 import {CommonBlockAlignment, extractAlignmentFromElement} from "lexical/nodes/common";
10 import {$selectSingleNode} from "../../utils/selection";
11 import {SerializedElementNode} from "lexical/nodes/LexicalElementNode";
12
13 export interface ImageNodeOptions {
14     alt?: string;
15     width?: number;
16     height?: number;
17 }
18
19 export type SerializedImageNode = Spread<{
20     src: string;
21     alt: string;
22     width: number;
23     height: number;
24     alignment: CommonBlockAlignment;
25 }, SerializedElementNode>
26
27 export class ImageNode extends ElementNode {
28     __src: string = '';
29     __alt: string = '';
30     __width: number = 0;
31     __height: number = 0;
32     __alignment: CommonBlockAlignment = '';
33
34     static getType(): string {
35         return 'image';
36     }
37
38     static clone(node: ImageNode): ImageNode {
39         const newNode = new ImageNode(node.__src, {
40             alt: node.__alt,
41             width: node.__width,
42             height: node.__height,
43         }, node.__key);
44         newNode.__alignment = node.__alignment;
45         return newNode;
46     }
47
48     constructor(src: string, options: ImageNodeOptions, key?: string) {
49         super(key);
50         this.__src = src;
51         if (options.alt) {
52             this.__alt = options.alt;
53         }
54         if (options.width) {
55             this.__width = options.width;
56         }
57         if (options.height) {
58             this.__height = options.height;
59         }
60     }
61
62     setSrc(src: string): void {
63         const self = this.getWritable();
64         self.__src = src;
65     }
66
67     getSrc(): string {
68         const self = this.getLatest();
69         return self.__src;
70     }
71
72     setAltText(altText: string): void {
73         const self = this.getWritable();
74         self.__alt = altText;
75     }
76
77     getAltText(): string {
78         const self = this.getLatest();
79         return self.__alt;
80     }
81
82     setHeight(height: number): void {
83         const self = this.getWritable();
84         self.__height = height;
85     }
86
87     getHeight(): number {
88         const self = this.getLatest();
89         return self.__height;
90     }
91
92     setWidth(width: number): void {
93         const self = this.getWritable();
94         self.__width = width;
95     }
96
97     getWidth(): number {
98         const self = this.getLatest();
99         return self.__width;
100     }
101
102     setAlignment(alignment: CommonBlockAlignment) {
103         const self = this.getWritable();
104         self.__alignment = alignment;
105     }
106
107     getAlignment(): CommonBlockAlignment {
108         const self = this.getLatest();
109         return self.__alignment;
110     }
111
112     isInline(): boolean {
113         return true;
114     }
115
116     createDOM(_config: EditorConfig, _editor: LexicalEditor) {
117         const element = document.createElement('img');
118         element.setAttribute('src', this.__src);
119
120         if (this.__width) {
121             element.setAttribute('width', String(this.__width));
122         }
123         if (this.__height) {
124             element.setAttribute('height', String(this.__height));
125         }
126         if (this.__alt) {
127             element.setAttribute('alt', this.__alt);
128         }
129
130         if (this.__alignment) {
131             element.classList.add('align-' + this.__alignment);
132         }
133
134         element.addEventListener('click', e => {
135             _editor.update(() => {
136                 this.select();
137             });
138         });
139
140         return element;
141     }
142
143     updateDOM(prevNode: ImageNode, dom: HTMLElement) {
144         if (prevNode.__src !== this.__src) {
145             dom.setAttribute('src', this.__src);
146         }
147
148         if (prevNode.__width !== this.__width) {
149             if (this.__width) {
150                 dom.setAttribute('width', String(this.__width));
151             } else {
152                 dom.removeAttribute('width');
153             }
154         }
155
156         if (prevNode.__height !== this.__height) {
157             if (this.__height) {
158                 dom.setAttribute('height', String(this.__height));
159             } else {
160                 dom.removeAttribute('height');
161             }
162         }
163
164         if (prevNode.__alt !== this.__alt) {
165             if (this.__alt) {
166                 dom.setAttribute('alt', String(this.__alt));
167             } else {
168                 dom.removeAttribute('alt');
169             }
170         }
171
172         if (prevNode.__alignment !== this.__alignment) {
173             if (prevNode.__alignment) {
174                 dom.classList.remove('align-' + prevNode.__alignment);
175             }
176             if (this.__alignment) {
177                 dom.classList.add('align-' + this.__alignment);
178             }
179         }
180
181         return false;
182     }
183
184     static importDOM(): DOMConversionMap|null {
185         return {
186             img(node: HTMLElement): DOMConversion|null {
187                 return {
188                     conversion: (element: HTMLElement): DOMConversionOutput|null => {
189
190                         const src = element.getAttribute('src') || '';
191                         const options: ImageNodeOptions = {
192                             alt: element.getAttribute('alt') || '',
193                             height: Number.parseInt(element.getAttribute('height') || '0'),
194                             width: Number.parseInt(element.getAttribute('width') || '0'),
195                         }
196
197                         const node = new ImageNode(src, options);
198                         node.setAlignment(extractAlignmentFromElement(element));
199
200                         return { node };
201                     },
202                     priority: 3,
203                 };
204             },
205         };
206     }
207
208     exportJSON(): SerializedImageNode {
209         return {
210             ...super.exportJSON(),
211             type: 'image',
212             version: 1,
213             src: this.__src,
214             alt: this.__alt,
215             height: this.__height,
216             width: this.__width,
217             alignment: this.__alignment,
218         };
219     }
220
221     static importJSON(serializedNode: SerializedImageNode): ImageNode {
222         const node = $createImageNode(serializedNode.src, {
223             alt: serializedNode.alt,
224             width: serializedNode.width,
225             height: serializedNode.height,
226         });
227         node.setAlignment(serializedNode.alignment);
228         return node;
229     }
230 }
231
232 export function $createImageNode(src: string, options: ImageNodeOptions = {}): ImageNode {
233     return new ImageNode(src, options);
234 }
235
236 export function $isImageNode(node: LexicalNode | null | undefined) {
237     return node instanceof ImageNode;
238 }