feat: add toolbar styles and functionality for blog editor
All checks were successful
CI - Build and Push / Build and Push Docker Image (push) Successful in 18s
All checks were successful
CI - Build and Push / Build and Push Docker Image (push) Successful in 18s
feat: implement EditorTheme for consistent styling across editor components feat: define types for blog-related operations including image uploads and post creation feat: create DropdownColorPicker component for color selection in blog editor feat: implement ImageResizer component for resizing images in the blog editor feat: add export and import functionality for blog posts in JSON format feat: update main application routes to include CreatePost page feat: enhance Blog page with a button to navigate to CreatePost feat: implement CreatePost page with title, cover image upload, and content editor
This commit is contained in:
276
src/blog/nodes/ImageNode.tsx
Normal file
276
src/blog/nodes/ImageNode.tsx
Normal file
@@ -0,0 +1,276 @@
|
||||
import type {
|
||||
DOMConversionMap,
|
||||
DOMConversionOutput,
|
||||
DOMExportOutput,
|
||||
EditorConfig,
|
||||
LexicalNode,
|
||||
NodeKey,
|
||||
SerializedLexicalNode,
|
||||
Spread,
|
||||
} from 'lexical';
|
||||
import type { JSX } from 'react';
|
||||
|
||||
import {
|
||||
$applyNodeReplacement,
|
||||
DecoratorNode,
|
||||
} from 'lexical';
|
||||
import { Suspense, lazy } from 'react';
|
||||
|
||||
const ImageComponent = lazy(() => import('./ImageComponent'));
|
||||
|
||||
export interface ImagePayload {
|
||||
altText: string;
|
||||
height?: number;
|
||||
key?: NodeKey;
|
||||
maxWidth?: number;
|
||||
src: string;
|
||||
width?: number;
|
||||
fileKey?: string; // S3 file key
|
||||
uploadProgress?: number; // Upload progress (0-100)
|
||||
uploadError?: string; // Upload error message
|
||||
}
|
||||
|
||||
function $convertImageElement(domNode: Node): null | DOMConversionOutput {
|
||||
const img = domNode as HTMLImageElement;
|
||||
const src = img.getAttribute('src');
|
||||
if (!src || src.startsWith('file:///')) {
|
||||
return null;
|
||||
}
|
||||
const { alt: altText, width, height } = img;
|
||||
const fileKey = img.getAttribute('data-file-key') || undefined;
|
||||
const node = $createImageNode({ altText, height, src, width, fileKey });
|
||||
return { node };
|
||||
}
|
||||
|
||||
export type SerializedImageNode = Spread<
|
||||
{
|
||||
altText: string;
|
||||
height?: number;
|
||||
maxWidth: number;
|
||||
src: string;
|
||||
width?: number;
|
||||
fileKey?: string;
|
||||
uploadProgress?: number;
|
||||
uploadError?: string;
|
||||
},
|
||||
SerializedLexicalNode
|
||||
>;
|
||||
|
||||
export class ImageNode extends DecoratorNode<JSX.Element> {
|
||||
__src: string;
|
||||
__altText: string;
|
||||
__width: 'inherit' | number;
|
||||
__height: 'inherit' | number;
|
||||
__maxWidth: number;
|
||||
__fileKey?: string;
|
||||
__uploadProgress?: number;
|
||||
__uploadError?: string;
|
||||
|
||||
static getType(): string {
|
||||
return 'image';
|
||||
}
|
||||
|
||||
static clone(node: ImageNode): ImageNode {
|
||||
return new ImageNode(
|
||||
node.__src,
|
||||
node.__altText,
|
||||
node.__maxWidth,
|
||||
node.__width,
|
||||
node.__height,
|
||||
node.__fileKey,
|
||||
node.__uploadProgress,
|
||||
node.__uploadError,
|
||||
node.__key,
|
||||
);
|
||||
}
|
||||
|
||||
static importJSON(serializedNode: SerializedImageNode): ImageNode {
|
||||
const { altText, height, width, maxWidth, src, fileKey, uploadProgress, uploadError } = serializedNode;
|
||||
return $createImageNode({
|
||||
altText,
|
||||
height,
|
||||
maxWidth,
|
||||
src,
|
||||
width,
|
||||
fileKey,
|
||||
uploadProgress,
|
||||
uploadError,
|
||||
});
|
||||
}
|
||||
|
||||
exportDOM(): DOMExportOutput {
|
||||
const element = document.createElement('img');
|
||||
element.setAttribute('src', this.__src);
|
||||
element.setAttribute('alt', this.__altText);
|
||||
if (this.__fileKey) {
|
||||
element.setAttribute('data-file-key', this.__fileKey);
|
||||
}
|
||||
if (this.__width !== 'inherit') {
|
||||
element.setAttribute('width', this.__width.toString());
|
||||
}
|
||||
if (this.__height !== 'inherit') {
|
||||
element.setAttribute('height', this.__height.toString());
|
||||
}
|
||||
return { element };
|
||||
}
|
||||
|
||||
static importDOM(): DOMConversionMap | null {
|
||||
return {
|
||||
img: () => ({
|
||||
conversion: $convertImageElement,
|
||||
priority: 0,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
constructor(
|
||||
src: string,
|
||||
altText: string,
|
||||
maxWidth: number,
|
||||
width?: 'inherit' | number,
|
||||
height?: 'inherit' | number,
|
||||
fileKey?: string,
|
||||
uploadProgress?: number,
|
||||
uploadError?: string,
|
||||
key?: NodeKey,
|
||||
) {
|
||||
super(key);
|
||||
this.__src = src;
|
||||
this.__altText = altText;
|
||||
this.__maxWidth = maxWidth;
|
||||
this.__width = width || 'inherit';
|
||||
this.__height = height || 'inherit';
|
||||
this.__fileKey = fileKey;
|
||||
this.__uploadProgress = uploadProgress;
|
||||
this.__uploadError = uploadError;
|
||||
}
|
||||
|
||||
exportJSON(): SerializedImageNode {
|
||||
return {
|
||||
altText: this.getAltText(),
|
||||
height: this.__height === 'inherit' ? 0 : this.__height,
|
||||
maxWidth: this.__maxWidth,
|
||||
src: this.getSrc(),
|
||||
type: 'image',
|
||||
version: 1,
|
||||
width: this.__width === 'inherit' ? 0 : this.__width,
|
||||
fileKey: this.__fileKey,
|
||||
uploadProgress: this.__uploadProgress,
|
||||
uploadError: this.__uploadError,
|
||||
};
|
||||
}
|
||||
|
||||
setWidthAndHeight(
|
||||
width: 'inherit' | number,
|
||||
height: 'inherit' | number,
|
||||
): void {
|
||||
const writable = this.getWritable();
|
||||
writable.__width = width;
|
||||
writable.__height = height;
|
||||
}
|
||||
|
||||
setUploadProgress(progress: number): void {
|
||||
const writable = this.getWritable();
|
||||
writable.__uploadProgress = progress;
|
||||
}
|
||||
|
||||
setUploadError(error: string | undefined): void {
|
||||
const writable = this.getWritable();
|
||||
writable.__uploadError = error;
|
||||
}
|
||||
|
||||
setFileKey(fileKey: string): void {
|
||||
const writable = this.getWritable();
|
||||
writable.__fileKey = fileKey;
|
||||
}
|
||||
|
||||
setSrc(src: string): void {
|
||||
const writable = this.getWritable();
|
||||
writable.__src = src;
|
||||
}
|
||||
|
||||
createDOM(config: EditorConfig): HTMLElement {
|
||||
const span = document.createElement('span');
|
||||
const theme = config.theme;
|
||||
const className = theme.image;
|
||||
if (className !== undefined) {
|
||||
span.className = className;
|
||||
}
|
||||
return span;
|
||||
}
|
||||
|
||||
updateDOM(): false {
|
||||
return false;
|
||||
}
|
||||
|
||||
getSrc(): string {
|
||||
return this.__src;
|
||||
}
|
||||
|
||||
getAltText(): string {
|
||||
return this.__altText;
|
||||
}
|
||||
|
||||
getFileKey(): string | undefined {
|
||||
return this.__fileKey;
|
||||
}
|
||||
|
||||
getUploadProgress(): number | undefined {
|
||||
return this.__uploadProgress;
|
||||
}
|
||||
|
||||
getUploadError(): string | undefined {
|
||||
return this.__uploadError;
|
||||
}
|
||||
|
||||
decorate(): JSX.Element {
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<ImageComponent
|
||||
src={this.__src}
|
||||
altText={this.__altText}
|
||||
width={this.__width}
|
||||
height={this.__height}
|
||||
maxWidth={this.__maxWidth}
|
||||
nodeKey={this.getKey()}
|
||||
resizable={true}
|
||||
fileKey={this.__fileKey}
|
||||
uploadProgress={this.__uploadProgress}
|
||||
uploadError={this.__uploadError}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function $createImageNode({
|
||||
altText,
|
||||
height,
|
||||
maxWidth = 800,
|
||||
src,
|
||||
width,
|
||||
key,
|
||||
fileKey,
|
||||
uploadProgress,
|
||||
uploadError,
|
||||
}: ImagePayload): ImageNode {
|
||||
return $applyNodeReplacement(
|
||||
new ImageNode(
|
||||
src,
|
||||
altText,
|
||||
maxWidth,
|
||||
width,
|
||||
height,
|
||||
fileKey,
|
||||
uploadProgress,
|
||||
uploadError,
|
||||
key,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export function $isImageNode(
|
||||
node: LexicalNode | null | undefined,
|
||||
): node is ImageNode {
|
||||
return node instanceof ImageNode;
|
||||
}
|
||||
Reference in New Issue
Block a user