238 lines
		
	
	
		
			6.5 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
			
		
		
	
	
			238 lines
		
	
	
		
			6.5 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
| import {
 | |
|     DOMConversion,
 | |
|     DOMConversionMap,
 | |
|     DOMConversionOutput, ElementNode,
 | |
|     LexicalEditor, LexicalNode,
 | |
|     Spread
 | |
| } from "lexical";
 | |
| import type {EditorConfig} from "lexical/LexicalEditor";
 | |
| import {CommonBlockAlignment, extractAlignmentFromElement} from "lexical/nodes/common";
 | |
| import {$selectSingleNode} from "../../utils/selection";
 | |
| import {SerializedElementNode} from "lexical/nodes/LexicalElementNode";
 | |
| 
 | |
| export interface ImageNodeOptions {
 | |
|     alt?: string;
 | |
|     width?: number;
 | |
|     height?: number;
 | |
| }
 | |
| 
 | |
| export type SerializedImageNode = Spread<{
 | |
|     src: string;
 | |
|     alt: string;
 | |
|     width: number;
 | |
|     height: number;
 | |
|     alignment: CommonBlockAlignment;
 | |
| }, SerializedElementNode>
 | |
| 
 | |
| export class ImageNode extends ElementNode {
 | |
|     __src: string = '';
 | |
|     __alt: string = '';
 | |
|     __width: number = 0;
 | |
|     __height: number = 0;
 | |
|     __alignment: CommonBlockAlignment = '';
 | |
| 
 | |
|     static getType(): string {
 | |
|         return 'image';
 | |
|     }
 | |
| 
 | |
|     static clone(node: ImageNode): ImageNode {
 | |
|         const newNode = new ImageNode(node.__src, {
 | |
|             alt: node.__alt,
 | |
|             width: node.__width,
 | |
|             height: node.__height,
 | |
|         }, node.__key);
 | |
|         newNode.__alignment = node.__alignment;
 | |
|         return newNode;
 | |
|     }
 | |
| 
 | |
|     constructor(src: string, options: ImageNodeOptions, key?: string) {
 | |
|         super(key);
 | |
|         this.__src = src;
 | |
|         if (options.alt) {
 | |
|             this.__alt = options.alt;
 | |
|         }
 | |
|         if (options.width) {
 | |
|             this.__width = options.width;
 | |
|         }
 | |
|         if (options.height) {
 | |
|             this.__height = options.height;
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     setSrc(src: string): void {
 | |
|         const self = this.getWritable();
 | |
|         self.__src = src;
 | |
|     }
 | |
| 
 | |
|     getSrc(): string {
 | |
|         const self = this.getLatest();
 | |
|         return self.__src;
 | |
|     }
 | |
| 
 | |
|     setAltText(altText: string): void {
 | |
|         const self = this.getWritable();
 | |
|         self.__alt = altText;
 | |
|     }
 | |
| 
 | |
|     getAltText(): string {
 | |
|         const self = this.getLatest();
 | |
|         return self.__alt;
 | |
|     }
 | |
| 
 | |
|     setHeight(height: number): void {
 | |
|         const self = this.getWritable();
 | |
|         self.__height = height;
 | |
|     }
 | |
| 
 | |
|     getHeight(): number {
 | |
|         const self = this.getLatest();
 | |
|         return self.__height;
 | |
|     }
 | |
| 
 | |
|     setWidth(width: number): void {
 | |
|         const self = this.getWritable();
 | |
|         self.__width = width;
 | |
|     }
 | |
| 
 | |
|     getWidth(): number {
 | |
|         const self = this.getLatest();
 | |
|         return self.__width;
 | |
|     }
 | |
| 
 | |
|     setAlignment(alignment: CommonBlockAlignment) {
 | |
|         const self = this.getWritable();
 | |
|         self.__alignment = alignment;
 | |
|     }
 | |
| 
 | |
|     getAlignment(): CommonBlockAlignment {
 | |
|         const self = this.getLatest();
 | |
|         return self.__alignment;
 | |
|     }
 | |
| 
 | |
|     isInline(): boolean {
 | |
|         return true;
 | |
|     }
 | |
| 
 | |
|     createDOM(_config: EditorConfig, _editor: LexicalEditor) {
 | |
|         const element = document.createElement('img');
 | |
|         element.setAttribute('src', this.__src);
 | |
| 
 | |
|         if (this.__width) {
 | |
|             element.setAttribute('width', String(this.__width));
 | |
|         }
 | |
|         if (this.__height) {
 | |
|             element.setAttribute('height', String(this.__height));
 | |
|         }
 | |
|         if (this.__alt) {
 | |
|             element.setAttribute('alt', this.__alt);
 | |
|         }
 | |
| 
 | |
|         if (this.__alignment) {
 | |
|             element.classList.add('align-' + this.__alignment);
 | |
|         }
 | |
| 
 | |
|         element.addEventListener('click', e => {
 | |
|             _editor.update(() => {
 | |
|                 $selectSingleNode(this);
 | |
|             });
 | |
|         });
 | |
| 
 | |
|         return element;
 | |
|     }
 | |
| 
 | |
|     updateDOM(prevNode: ImageNode, dom: HTMLElement) {
 | |
|         if (prevNode.__src !== this.__src) {
 | |
|             dom.setAttribute('src', this.__src);
 | |
|         }
 | |
| 
 | |
|         if (prevNode.__width !== this.__width) {
 | |
|             if (this.__width) {
 | |
|                 dom.setAttribute('width', String(this.__width));
 | |
|             } else {
 | |
|                 dom.removeAttribute('width');
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         if (prevNode.__height !== this.__height) {
 | |
|             if (this.__height) {
 | |
|                 dom.setAttribute('height', String(this.__height));
 | |
|             } else {
 | |
|                 dom.removeAttribute('height');
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         if (prevNode.__alt !== this.__alt) {
 | |
|             if (this.__alt) {
 | |
|                 dom.setAttribute('alt', String(this.__alt));
 | |
|             } else {
 | |
|                 dom.removeAttribute('alt');
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         if (prevNode.__alignment !== this.__alignment) {
 | |
|             if (prevNode.__alignment) {
 | |
|                 dom.classList.remove('align-' + prevNode.__alignment);
 | |
|             }
 | |
|             if (this.__alignment) {
 | |
|                 dom.classList.add('align-' + this.__alignment);
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         return false;
 | |
|     }
 | |
| 
 | |
|     static importDOM(): DOMConversionMap|null {
 | |
|         return {
 | |
|             img(node: HTMLElement): DOMConversion|null {
 | |
|                 return {
 | |
|                     conversion: (element: HTMLElement): DOMConversionOutput|null => {
 | |
| 
 | |
|                         const src = element.getAttribute('src') || '';
 | |
|                         const options: ImageNodeOptions = {
 | |
|                             alt: element.getAttribute('alt') || '',
 | |
|                             height: Number.parseInt(element.getAttribute('height') || '0'),
 | |
|                             width: Number.parseInt(element.getAttribute('width') || '0'),
 | |
|                         }
 | |
| 
 | |
|                         const node = new ImageNode(src, options);
 | |
|                         node.setAlignment(extractAlignmentFromElement(element));
 | |
| 
 | |
|                         return { node };
 | |
|                     },
 | |
|                     priority: 3,
 | |
|                 };
 | |
|             },
 | |
|         };
 | |
|     }
 | |
| 
 | |
|     exportJSON(): SerializedImageNode {
 | |
|         return {
 | |
|             ...super.exportJSON(),
 | |
|             type: 'image',
 | |
|             version: 1,
 | |
|             src: this.__src,
 | |
|             alt: this.__alt,
 | |
|             height: this.__height,
 | |
|             width: this.__width,
 | |
|             alignment: this.__alignment,
 | |
|         };
 | |
|     }
 | |
| 
 | |
|     static importJSON(serializedNode: SerializedImageNode): ImageNode {
 | |
|         const node = $createImageNode(serializedNode.src, {
 | |
|             alt: serializedNode.alt,
 | |
|             width: serializedNode.width,
 | |
|             height: serializedNode.height,
 | |
|         });
 | |
|         node.setAlignment(serializedNode.alignment);
 | |
|         return node;
 | |
|     }
 | |
| }
 | |
| 
 | |
| export function $createImageNode(src: string, options: ImageNodeOptions = {}): ImageNode {
 | |
|     return new ImageNode(src, options);
 | |
| }
 | |
| 
 | |
| export function $isImageNode(node: LexicalNode | null | undefined) {
 | |
|     return node instanceof ImageNode;
 | |
| } |