I am trying to make the custom editor as per the requirement. I need a block editor and choose block note one. I on + we can see the suggestion list.
This is how I have used the editor and added suggestion list, and my custom toolbar also:
<BlockNoteView
editor={editor}
onChange={() => handleEditorChange(editor.document)}
formattingToolbar={false}
sideMenu={false}
slashMenu={false}
className='dream-editor w-full'
>
<SuggestionMenuController
triggerCharacter='/'
suggestionMenuComponent={(props) => (
<SuggestionList {...props} editor={editor} setIsVoiceToTextOpen={setIsVoiceToTextOpen} handleRewriteWithAI={handleRewriteWithAI} />
)}
/>
<SideMenuController sideMenu={CustomSideMenu} />
<FormattingToolbarController
formattingToolbar={() => (
<CustomFormattingToolbar
editor={editor}
setIsVoiceToTextOpen={setIsVoiceToTextOpen}
handleRewriteWithAI={handleRewriteWithAI}
/>
)}
/>
</BlockNoteView>
Also the following is my CustomSuggestionList
:
/* eslint-disable jsx-a11y/no-static-element-interactions */
import React from 'react';
import {
BasicTextStyleButton,
CreateLinkButton
} from '@blocknote/react';
import ReWriteWithAiIcon from '@/assests/svg/ReWriteWithAiIcon';
import VoiceToText from '@/assests/svg/VoiceToText';
import CustomImageUploader from './CustomImageUploader';
import { CustomColorsButton } from './CustomStyleButton';
import CustomTextTypeSelector from './CustomTextTypeSelector';
import '@blocknote/core/fonts/inter.css';
// Define a type for the toolbar options
interface ToolbarOption {
title?: string;
icon?: React.ReactNode;
className?: string;
component?: React.ReactNode;
onClick?: (e: React.MouseEvent<HTMLDivElement>) => void;
}
const SuggestionList = ({
editor,
selectedIndex,
setIsVoiceToTextOpen,
handleRewriteWithAI
}: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
editor: any;
selectedIndex: number;
setIsVoiceToTextOpen: React.Dispatch<React.SetStateAction<boolean>>;
handleRewriteWithAI: () => void;
}) => {
const toolbarOptions: ToolbarOption[] = [
{
title: 'Rewrite With AI',
icon: <ReWriteWithAiIcon />,
className: 'gradient-text border-0 bg-transparent p-2 font-semibold',
onClick: handleRewriteWithAI
},
{
title: 'Voice-to-Text',
icon: <VoiceToText />,
className: 'text-neutrals-300 border-0 bg-transparent px-2 font-medium',
onClick: () => setIsVoiceToTextOpen(true)
},
{
component: <CustomTextTypeSelector editor={editor}/>,
},
{
component: <CustomColorsButton trigger="hover"/>
},
{
component: <BasicTextStyleButton basicTextStyle='bold' />
},
{
component: <BasicTextStyleButton basicTextStyle='italic' />
},
{
component: <BasicTextStyleButton basicTextStyle='underline' />
},
{
component: <CreateLinkButton />
},
{
component: <CustomImageUploader editor={editor} />,
}
];
return (
<div className='slash-menu bg-dark-mode-800 rounded-lg px-3 flex justify-self-center items-center'>
{toolbarOptions.map((option, index) => (
<div
key={Number(index)}
className={`slash-menu-item flex gap-2 ${option.className || ''} ${selectedIndex === index ? ' selected' : ''}`}
onClick={(e) => {
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
option.onClick && option.onClick(e);
}}
// onMouseDown={(e) => {
// e.preventDefault();
// console.log("Event prevented!");
// }}
>
{option.icon}{option.title}
{option.component && option.component}
</div>
))}
</div>
);
};
export default SuggestionList;
Also here’s my custom image uploader I have used
import React, { useState } from 'react';
import { message, Upload } from 'antd';
import { UploadProps } from 'antd/es/upload';
import GalleryIcon from '@/assests/svg/GalleryIcon';
import StandardButton from '../common/StandardButton';
// Define the ImageBlock type
type ImageBlock = {
id: string;
type: 'image';
props: {
url: string;
caption: string;
previewWidth: number;
name: string;
};
};
const CustomImageUploader = ({ editor }) => {
const [fileList, setFileList] = useState([]);
const uploadProps: UploadProps = {
name: 'image',
multiple: false,
action: `${process.env.NEXT_PUBLIC_APP_API_ENDPOINT ?? ''}/upload/single-image`,
data: { folder: 'dreams' },
headers: {
Authorization: `Bearer ${process.env.NEXT_PUBLIC_APP_STATIC_TOKEN ?? ''}`
},
beforeUpload(file) {
console.log("File is uploaded 123")
const isImage = ['image/jpeg', 'image/png'].includes(file.type);
console.log("File is uploaded 123")
if (!isImage) {
message.error('You can only upload JPG, JPEG, and PNG files!');
}
return isImage || Upload.LIST_IGNORE;
},
onChange(info) {
console.log("File is uploaded 123")
const { status } = info.file;
setFileList(info.fileList);
if (status === 'done') {
message.success(info.file.response?.message);
const {
base_url: baseURL,
internal_path: internalPath,
image,
id
} = info.file.response?.data || {};
if (baseURL && internalPath && image) {
const imageUrl = `${baseURL}${internalPath}${image}`;
const imageBlock: ImageBlock = {
id: id || 'unique-id',
type: 'image',
props: {
url: imageUrl,
caption: info.file.name || '',
previewWidth: 512,
name: image
}
};
const referenceBlock = editor.document[0];
if (referenceBlock) {
editor.insertBlocks([imageBlock], referenceBlock, 'after');
} else {
message.error('No reference block found.');
}
} else {
message.error('Invalid image response data.');
}
} else if (status === 'error') {
message.error(info.file.response?.message || 'Image upload failed.');
}
},
fileList
};
return (
<Upload {...uploadProps} showUploadList={false} >
<StandardButton
icon={<GalleryIcon color='#AEAEC4' />}
className='border-0 bg-transparent'
/>
</Upload>
);
};
export default CustomImageUploader;
The issue I am facing is, when I see the suggestion list and click on image uploader, I get the option to select the image. But when I select the image, it is not getting uploaded.
Other options are working fine, just link and image is not working.
Strange thing is I have used the same image uploader in my custom toolbar with same options but it’s working fine there.