]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/nodes/image.ts
Lexical: Made image resize handles functional
[bookstack] / resources / js / wysiwyg / nodes / image.ts
1 import {
2     DecoratorNode,
3     DOMConversion,
4     DOMConversionMap,
5     DOMConversionOutput,
6     LexicalEditor, LexicalNode,
7     SerializedLexicalNode,
8     Spread
9 } from "lexical";
10 import type {EditorConfig} from "lexical/LexicalEditor";
11 import {el} from "../helpers";
12 import {EditorDecoratorAdapter} from "../ui/framework/decorator";
13
14 export interface ImageNodeOptions {
15     alt?: string;
16     width?: number;
17     height?: number;
18 }
19
20 export type SerializedImageNode = Spread<{
21     src: string;
22     alt: string;
23     width: number;
24     height: number;
25 }, SerializedLexicalNode>
26
27 export class ImageNode extends DecoratorNode<EditorDecoratorAdapter> {
28     __src: string = '';
29     __alt: string = '';
30     __width: number = 0;
31     __height: number = 0;
32     // TODO - Alignment
33
34     static getType(): string {
35         return 'image';
36     }
37
38     static clone(node: ImageNode): ImageNode {
39         return new ImageNode(node.__src, {
40             alt: node.__alt,
41             width: node.__width,
42             height: node.__height,
43         });
44     }
45
46     constructor(src: string, options: ImageNodeOptions, key?: string) {
47         super(key);
48         this.__src = src;
49         if (options.alt) {
50             this.__alt = options.alt;
51         }
52         if (options.width) {
53             this.__width = options.width;
54         }
55         if (options.height) {
56             this.__height = options.height;
57         }
58     }
59
60     setAltText(altText: string): void {
61         const self = this.getWritable();
62         self.__alt = altText;
63     }
64
65     getAltText(): string {
66         const self = this.getLatest();
67         return self.__alt;
68     }
69
70     setHeight(height: number): void {
71         const self = this.getWritable();
72         self.__height = height;
73     }
74
75     getHeight(): number {
76         const self = this.getLatest();
77         return self.__height;
78     }
79
80     setWidth(width: number): void {
81         const self = this.getWritable();
82         self.__width = width;
83     }
84
85     getWidth(): number {
86         const self = this.getLatest();
87         return self.__width;
88     }
89
90     isInline(): boolean {
91         return true;
92     }
93
94     decorate(editor: LexicalEditor, config: EditorConfig): EditorDecoratorAdapter {
95         return {
96             type: 'image',
97             getNode: () => this,
98         };
99     }
100
101     createDOM(_config: EditorConfig, _editor: LexicalEditor) {
102         const element = document.createElement('img');
103         element.setAttribute('src', this.__src);
104
105         if (this.__width) {
106             element.setAttribute('width', String(this.__width));
107         }
108         if (this.__height) {
109             element.setAttribute('height', String(this.__height));
110         }
111         if (this.__alt) {
112             element.setAttribute('alt', this.__alt);
113         }
114         return el('span', {class: 'editor-image-wrap'}, [
115             element,
116         ]);
117     }
118
119     updateDOM(prevNode: ImageNode, dom: HTMLElement) {
120         const image = dom.querySelector('img');
121         if (!image) return false;
122
123         if (prevNode.__src !== this.__src) {
124             image.setAttribute('src', this.__src);
125         }
126
127         if (prevNode.__width !== this.__width) {
128             if (this.__width) {
129                 image.setAttribute('width', String(this.__width));
130             } else {
131                 image.removeAttribute('width');
132             }
133         }
134
135         if (prevNode.__height !== this.__height) {
136             if (this.__height) {
137                 image.setAttribute('height', String(this.__height));
138             } else {
139                 image.removeAttribute('height');
140             }
141         }
142
143         if (prevNode.__alt !== this.__alt) {
144             if (this.__alt) {
145                 image.setAttribute('alt', String(this.__alt));
146             } else {
147                 image.removeAttribute('alt');
148             }
149         }
150
151         return false;
152     }
153
154     static importDOM(): DOMConversionMap|null {
155         return {
156             img(node: HTMLElement): DOMConversion|null {
157                 return {
158                     conversion: (element: HTMLElement): DOMConversionOutput|null => {
159
160                         const src = element.getAttribute('src') || '';
161                         const options: ImageNodeOptions = {
162                             alt: element.getAttribute('alt') || '',
163                             height: Number.parseInt(element.getAttribute('height') || '0'),
164                             width: Number.parseInt(element.getAttribute('width') || '0'),
165                         }
166
167                         return {
168                             node: new ImageNode(src, options),
169                         };
170                     },
171                     priority: 3,
172                 };
173             },
174         };
175     }
176
177     exportJSON(): SerializedImageNode {
178         return {
179             type: 'image',
180             version: 1,
181             src: this.__src,
182             alt: this.__alt,
183             height: this.__height,
184             width: this.__width
185         };
186     }
187
188     static importJSON(serializedNode: SerializedImageNode): ImageNode {
189         return $createImageNode(serializedNode.src, {
190             alt: serializedNode.alt,
191             width: serializedNode.width,
192             height: serializedNode.height,
193         });
194     }
195 }
196
197 export function $createImageNode(src: string, options: ImageNodeOptions = {}): ImageNode {
198     return new ImageNode(src, options);
199 }
200
201 export function $isImageNode(node: LexicalNode | null | undefined) {
202     return node instanceof ImageNode;
203 }