mirror of
https://github.com/BookStackApp/BookStack.git
synced 2024-10-01 01:36:00 -04:00
Lexical: Started build of image node and decoration UI
This commit is contained in:
parent
7c504a10a8
commit
a74e04141c
174
resources/js/wysiwyg/nodes/image.ts
Normal file
174
resources/js/wysiwyg/nodes/image.ts
Normal 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;
|
||||||
|
}
|
@ -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,
|
||||||
|
@ -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]);
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
@ -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>
|
||||||
|
Loading…
Reference in New Issue
Block a user