BookStack/resources/js/wysiwyg/nodes/image.ts
Dan Brown e5b6d28bca
Lexical: Revamped image node resize method
Changed from using a decorator to using a helper that watches for image
selections to then display a resize helper.
Also changes resizer to use a ghost and apply changes on end instead of
continuosly during resize.
2024-09-07 18:39:58 +01:00

238 lines
6.4 KiB
TypeScript

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,
});
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;
}