]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/nodes/image.ts
Lexical: Started build of image node and decoration UI
[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
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 }, SerializedLexicalNode>
25
26 export class ImageNode extends DecoratorNode<HTMLElement> {
27     __src: string = '';
28     __alt: string = '';
29     __width: number = 0;
30     __height: number = 0;
31     // TODO - Alignment
32
33     static getType(): string {
34         return 'image';
35     }
36
37     static clone(node: ImageNode): ImageNode {
38         return new ImageNode(node.__src, {
39             alt: node.__alt,
40             width: node.__width,
41             height: node.__height,
42         });
43     }
44
45     constructor(src: string, options: ImageNodeOptions, key?: string) {
46         super(key);
47         this.__src = src;
48         if (options.alt) {
49             this.__alt = options.alt;
50         }
51         if (options.width) {
52             this.__width = options.width;
53         }
54         if (options.height) {
55             this.__height = options.height;
56         }
57     }
58
59     setAltText(altText: string): void {
60         const self = this.getWritable();
61         self.__alt = altText;
62     }
63
64     getAltText(): string {
65         const self = this.getLatest();
66         return self.__alt;
67     }
68
69     setHeight(height: number): void {
70         const self = this.getWritable();
71         self.__height = height;
72     }
73
74     getHeight(): number {
75         const self = this.getLatest();
76         return self.__height;
77     }
78
79     setWidth(width: number): void {
80         const self = this.getWritable();
81         self.__width = width;
82     }
83
84     getWidth(): number {
85         const self = this.getLatest();
86         return self.__width;
87     }
88
89     isInline(): boolean {
90         return true;
91     }
92
93     decorate(editor: LexicalEditor, config: EditorConfig): HTMLElement {
94         console.log('decorate!');
95         return el('div', {
96             class: 'editor-image-decorator',
97         }, ['decoration!!!']);
98     }
99
100     createDOM(_config: EditorConfig, _editor: LexicalEditor) {
101         const element = document.createElement('img');
102         element.setAttribute('src', this.__src);
103         element.textContent
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: unknown, dom: HTMLElement) {
120         // Returning false tells Lexical that this node does not need its
121         // DOM element replacing with a new copy from createDOM.
122         return false;
123     }
124
125     static importDOM(): DOMConversionMap|null {
126         return {
127             img(node: HTMLElement): DOMConversion|null {
128                 return {
129                     conversion: (element: HTMLElement): DOMConversionOutput|null => {
130
131                         const src = element.getAttribute('src') || '';
132                         const options: ImageNodeOptions = {
133                             alt: element.getAttribute('alt') || '',
134                             height: Number.parseInt(element.getAttribute('height') || '0'),
135                             width: Number.parseInt(element.getAttribute('width') || '0'),
136                         }
137
138                         return {
139                             node: new ImageNode(src, options),
140                         };
141                     },
142                     priority: 3,
143                 };
144             },
145         };
146     }
147
148     exportJSON(): SerializedImageNode {
149         return {
150             type: 'image',
151             version: 1,
152             src: this.__src,
153             alt: this.__alt,
154             height: this.__height,
155             width: this.__width
156         };
157     }
158
159     static importJSON(serializedNode: SerializedImageNode): ImageNode {
160         return $createImageNode(serializedNode.src, {
161             alt: serializedNode.alt,
162             width: serializedNode.width,
163             height: serializedNode.height,
164         });
165     }
166 }
167
168 export function $createImageNode(src: string, options: ImageNodeOptions = {}): ImageNode {
169     return new ImageNode(src, options);
170 }
171
172 export function $isImageNode(node: LexicalNode | null | undefined) {
173     return node instanceof ImageNode;
174 }