BookStackApp/BookStack

View on GitHub
resources/js/wysiwyg/nodes/image.ts

Summary

Maintainability
C
1 day
Test Coverage
import {
    DOMConversion,
    DOMConversionMap,
    DOMConversionOutput, ElementNode,
    LexicalEditor, LexicalNode,
    Spread
} from "lexical";
import type {EditorConfig} from "lexical/LexicalEditor";
import {CommonBlockAlignment, extractAlignmentFromElement} from "./_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;
}