Lexical: Started build of image node and decoration UI

This commit is contained in:
Dan Brown 2024-06-03 16:56:31 +01:00
parent 7c504a10a8
commit a74e04141c
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
5 changed files with 195 additions and 2 deletions

View File

@ -0,0 +1,174 @@
import {
DecoratorNode,
DOMConversion,
DOMConversionMap,
DOMConversionOutput,
LexicalEditor, LexicalNode,
SerializedLexicalNode,
Spread
} from "lexical";
import type {EditorConfig} from "lexical/LexicalEditor";
import {el} from "../helpers";
export interface ImageNodeOptions {
alt?: string;
width?: number;
height?: number;
}
export type SerializedImageNode = Spread<{
src: string;
alt: string;
width: number;
height: number;
}, SerializedLexicalNode>
export class ImageNode extends DecoratorNode<HTMLElement> {
__src: string = '';
__alt: string = '';
__width: number = 0;
__height: number = 0;
// TODO - Alignment
static getType(): string {
return 'image';
}
static clone(node: ImageNode): ImageNode {
return new ImageNode(node.__src, {
alt: node.__alt,
width: node.__width,
height: node.__height,
});
}
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;
}
}
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;
}
isInline(): boolean {
return true;
}
decorate(editor: LexicalEditor, config: EditorConfig): HTMLElement {
console.log('decorate!');
return el('div', {
class: 'editor-image-decorator',
}, ['decoration!!!']);
}
createDOM(_config: EditorConfig, _editor: LexicalEditor) {
const element = document.createElement('img');
element.setAttribute('src', this.__src);
element.textContent
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);
}
return el('span', {class: 'editor-image-wrap'}, [
element,
]);
}
updateDOM(prevNode: unknown, dom: HTMLElement) {
// Returning false tells Lexical that this node does not need its
// DOM element replacing with a new copy from createDOM.
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'),
}
return {
node: new ImageNode(src, options),
};
},
priority: 3,
};
},
};
}
exportJSON(): SerializedImageNode {
return {
type: 'image',
version: 1,
src: this.__src,
alt: this.__alt,
height: this.__height,
width: this.__width
};
}
static importJSON(serializedNode: SerializedImageNode): ImageNode {
return $createImageNode(serializedNode.src, {
alt: serializedNode.alt,
width: serializedNode.width,
height: serializedNode.height,
});
}
}
export function $createImageNode(src: string, options: ImageNodeOptions = {}): ImageNode {
return new ImageNode(src, options);
}
export function $isImageNode(node: LexicalNode | null | undefined) {
return node instanceof ImageNode;
}

View File

@ -3,6 +3,7 @@ import {CalloutNode} from './callout';
import {ElementNode, KlassConstructor, LexicalNode, LexicalNodeReplacement, ParagraphNode} from "lexical"; import {ElementNode, KlassConstructor, LexicalNode, LexicalNodeReplacement, ParagraphNode} from "lexical";
import {CustomParagraphNode} from "./custom-paragraph"; import {CustomParagraphNode} from "./custom-paragraph";
import {LinkNode} from "@lexical/link"; import {LinkNode} from "@lexical/link";
import {ImageNode} from "./image";
/** /**
* Load the nodes for lexical. * Load the nodes for lexical.
@ -12,6 +13,7 @@ export function getNodesForPageEditor(): (KlassConstructor<typeof LexicalNode> |
CalloutNode, // Todo - Create custom CalloutNode, // Todo - Create custom
HeadingNode, // Todo - Create custom HeadingNode, // Todo - Create custom
QuoteNode, // Todo - Create custom QuoteNode, // Todo - Create custom
ImageNode,
CustomParagraphNode, CustomParagraphNode,
{ {
replace: ParagraphNode, replace: ParagraphNode,

View File

@ -67,7 +67,6 @@ export class FormatPreviewButton extends EditorButton {
}, [this.getLabel()]); }, [this.getLabel()]);
const stylesToApply = this.getStylesFromPreview(); const stylesToApply = this.getStylesFromPreview();
console.log(stylesToApply);
for (const style of Object.keys(stylesToApply)) { for (const style of Object.keys(stylesToApply)) {
preview.style.setProperty(style, stylesToApply[style]); preview.style.setProperty(style, stylesToApply[style]);
} }

View File

@ -7,6 +7,9 @@ import {
import {getMainEditorFullToolbar} from "./toolbars"; import {getMainEditorFullToolbar} from "./toolbars";
import {EditorUIManager} from "./framework/manager"; import {EditorUIManager} from "./framework/manager";
import {link as linkFormDefinition} from "./defaults/form-definitions"; import {link as linkFormDefinition} from "./defaults/form-definitions";
import {DecoratorListener} from "lexical/LexicalEditor";
import type {NodeKey} from "lexical/LexicalNode";
import {el} from "../helpers";
export function buildEditorUI(element: HTMLElement, editor: LexicalEditor) { export function buildEditorUI(element: HTMLElement, editor: LexicalEditor) {
const manager = new EditorUIManager(); const manager = new EditorUIManager();
@ -28,6 +31,20 @@ export function buildEditorUI(element: HTMLElement, editor: LexicalEditor) {
form: linkFormDefinition, form: linkFormDefinition,
}); });
// Register decorator listener
// Maybe move to manager?
const domDecorateListener: DecoratorListener<HTMLElement> = (decorator: Record<NodeKey, HTMLElement>) => {
const keys = Object.keys(decorator);
for (const key of keys) {
const decoratedEl = editor.getElementByKey(key);
const decoratorEl = decorator[key];
if (decoratedEl) {
decoratedEl.append(decoratorEl);
}
}
}
editor.registerDecoratorListener(domDecorateListener);
// Update button states on editor selection change // Update button states on editor selection change
editor.registerCommand(SELECTION_CHANGE_COMMAND, () => { editor.registerCommand(SELECTION_CHANGE_COMMAND, () => {
const selection = $getSelection(); const selection = $getSelection();

View File

@ -9,13 +9,14 @@
<div class="editor-container"> <div class="editor-container">
<div refs="wysiwyg-editor@edit-area" contenteditable="true"> <div refs="wysiwyg-editor@edit-area" contenteditable="true">
<p id="Content!">Some <strong>content</strong> here</p> <p id="Content!">Some <strong>content</strong> here</p>
<p>Content with image in, before text. <img src="https://bookstack.local/bookstack/uploads/images/gallery/2022-03/scaled-1680-/cats-image-2400x1000-2.jpg" width="200" alt="Sleepy meow"> After text.</p>
<p>This has a <a href="https://example.com" target="_blank" title="Link to example">link</a> in it</p> <p>This has a <a href="https://example.com" target="_blank" title="Link to example">link</a> in it</p>
<h2>List below this h2 header</h2> <h2>List below this h2 header</h2>
<ul> <ul>
<li>Hello</li> <li>Hello</li>
</ul> </ul>
<p class="callout danger"> <p class="callout info">
Hello there, this is an info callout Hello there, this is an info callout
</p> </p>
</div> </div>