Skip to content

Commit

Permalink
align blocknote with raak
Browse files Browse the repository at this point in the history
  • Loading branch information
LemonardoD committed Nov 8, 2024
1 parent 7bdfae2 commit 7defd22
Show file tree
Hide file tree
Showing 9 changed files with 233 additions and 68 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
ColorStyleButton,
CreateLinkButton,
FileCaptionButton,
FileDownloadButton,
FileReplaceButton,
FormattingToolbar,
FormattingToolbarController,
Expand All @@ -23,6 +24,7 @@ export const CustomFormattingToolbar = ({ config }: { config: CustomFormatToolBa

{config.fileCaption && <FileCaptionButton key={'fileCaptionButton'} />}
{config.replaceFile && <FileReplaceButton key={'replaceFileButton'} />}
<FileDownloadButton key={'downloadButton'} />

{config.textColorSelect && <ColorStyleButton key={'colorStyleButton'} />}

Expand Down
12 changes: 0 additions & 12 deletions frontend/src/modules/common/blocknote/custom-side-menu.tsx

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import type {
BlockSchema,
DefaultBlockSchema,
DefaultInlineContentSchema,
DefaultStyleSchema,
InlineContentSchema,
StyleSchema,
} from '@blocknote/core';
import { DragHandleMenu, type SideMenuProps, useComponentsContext } from '@blocknote/react';
import { GripVertical } from 'lucide-react';

type CustomDragHandleButtonProps<
BSchema extends BlockSchema = DefaultBlockSchema,
I extends InlineContentSchema = DefaultInlineContentSchema,
S extends StyleSchema = DefaultStyleSchema,
> = Omit<SideMenuProps<BSchema, I, S>, 'addBlock'> & {
haveDropDown?: boolean;
position?: 'left' | 'right' | 'top' | 'bottom';
};
export const CustomDragHandleButton = <
BSchema extends BlockSchema = DefaultBlockSchema,
I extends InlineContentSchema = DefaultInlineContentSchema,
S extends StyleSchema = DefaultStyleSchema,
>(
props: CustomDragHandleButtonProps<BSchema, I, S>,
) => {
// biome-ignore lint/style/noNonNullAssertion: req by author
const Components = useComponentsContext()!;

const Content = props.dragHandleMenu || DragHandleMenu;

// Wrapper to match the signature of onDragStart
const handleDragStart = (e: React.DragEvent<Element>) => {
if (props.blockDragStart) {
const eventData = {
dataTransfer: e.dataTransfer,
clientY: e.clientY,
};
props.blockDragStart(eventData, props.block);
}
};
return (
<Components.Generic.Menu.Root
onOpenChange={(open: boolean) => {
if (open) props.freezeMenu();
else props.unfreezeMenu();
}}
position={props.position}
>
{props.haveDropDown ? (
<Components.Generic.Menu.Trigger>
<Components.SideMenu.Button
label="Open side menu"
draggable={true}
onDragStart={handleDragStart}
onDragEnd={props.blockDragEnd}
className={'bn-button'}
icon={<GripVertical size={22} data-test="dragHandle" />}
/>
</Components.Generic.Menu.Trigger>
) : (
<Components.SideMenu.Button
label="Drag button"
draggable={true}
onDragStart={handleDragStart}
onDragEnd={props.blockDragEnd}
className={'bn-button'}
icon={<GripVertical size={22} data-test="dragHandle" />}
/>
)}

<Content block={props.block} />
</Components.Generic.Menu.Root>
);
};
28 changes: 28 additions & 0 deletions frontend/src/modules/common/blocknote/custom-side-menu/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { DragHandleMenu, SideMenu, SideMenuController } from '@blocknote/react';
import { CustomDragHandleButton } from './drag-handle-button';
import { ResetBlockTypeItem } from './reset-block-type';

const typeOnSlashMenuAppearance = ['paragraph', 'heading', 'bulletListItem', 'numberedListItem', 'checkListItem'];

// in this menu we have only drag button
export const CustomSideMenu = () => (
<SideMenuController
sideMenu={(props) => (
<SideMenu {...props}>
<CustomDragHandleButton
haveDropDown={typeOnSlashMenuAppearance.includes(props.block.type)}
dragHandleMenu={(props) => (
<>
{typeOnSlashMenuAppearance.includes(props.block.type) ? (
<DragHandleMenu {...props}>
<ResetBlockTypeItem {...props} />
</DragHandleMenu>
) : null}
</>
)}
{...props}
/>
</SideMenu>
)}
/>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import type { Block, BlockConfig, BlockSchema, InlineContentSchema, StyleSchema } from '@blocknote/core';
import {
type BlockTypeSelectItem,
type DragHandleMenuProps,
blockTypeSelectItems,
useBlockNoteEditor,
useComponentsContext,
useDictionary,
} from '@blocknote/react';
import { useMemo } from 'react';
import { customBlockTypeSelectItems } from '~/modules/common/blocknote/blocknote-config';
import type { BlockTypes } from '~/modules/common/blocknote/types';

export function ResetBlockTypeItem(props: DragHandleMenuProps) {
// biome-ignore lint/style/noNonNullAssertion: required by author
const Components = useComponentsContext()!;
const dict = useDictionary();

const editor = useBlockNoteEditor<BlockSchema, InlineContentSchema, StyleSchema>();

const filteredItems = useMemo(() => {
return blockTypeSelectItems(dict).filter((item) => customBlockTypeSelectItems.includes(item.type as BlockTypes));
}, [editor, dict]);

const shouldShow: boolean = useMemo(
() => filteredItems.find((item) => item.type === props.block.type) !== undefined,
[props.block.type, filteredItems],
);

const fullItems = useMemo(() => {
const onClick = (item: BlockTypeSelectItem) => {
editor.focus();

editor.updateBlock(props.block, {
type: item.type,
//In our case we pass props cos by it we get heading level: 1 | 2 | 3
// biome-ignore lint/suspicious/noExplicitAny: required by author
props: item.props as any,
});
};

return filteredItems.map((item) => {
const { icon: Icon, isSelected, name } = item;
return {
type: item.type,
title: name,
icon: <Icon size={16} />,
onClick: () => onClick(item),
isSelected: isSelected(props.block as unknown as Block<Record<string, BlockConfig>, InlineContentSchema, StyleSchema>),
};
});
}, [props.block, filteredItems, editor]);

if (!shouldShow || !editor.isEditable) return null;

return (
<>
{fullItems.map((el) => {
let isSelected = false;
if (props.block.type === 'heading') {
isSelected = el.title.includes(props.block.props.level.toString());
} else isSelected = props.block.type === el.type;
return (
<Components.Generic.Menu.Item className="bn-menu-item" key={el.title} onClick={el.onClick} icon={el.icon} checked={isSelected}>
{el.title}
</Components.Generic.Menu.Item>
);
})}
</>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,8 @@ export const slashMenu = (
);

const triggerItemClick = (item: DefaultReactSuggestionItem, event: KeyboardEvent | React.MouseEvent<HTMLDivElement, MouseEvent>) => {
onItemClick?.(item);
event.preventDefault();
onItemClick?.(item);
};

useEffect(() => {
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/modules/common/blocknote/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ p.bn-inline-content code {
line-height: 1.3;
}

.bn-inline-content:has(> .ProseMirror-trailingBreak)::before {
color: var(--bloknote-placeholder);
}

.bn-container.bn-shadcn a {
text-decoration: underline;
}
Expand Down
105 changes: 51 additions & 54 deletions frontend/src/modules/common/upload/blocknote-upload-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { PartialBlock } from '@blocknote/core';
import { type FilePanelProps, useBlockNoteEditor } from '@blocknote/react';
import { DialogDescription } from '@radix-ui/react-dialog';
import type { UppyFile } from '@uppy/core';
import type React from 'react';
import { useTranslation } from 'react-i18next';
import type { UppyBody, UppyMeta } from '~/lib/imado';
import UploadUppy from '~/modules/common/upload/upload-uppy';
Expand All @@ -27,60 +28,56 @@ const blockTypes = {
},
};

const UppyFilePanel =
(
onCreateCallback?: (
result: {
file: UppyFile<UppyMeta, UppyBody>;
url: string;
}[],
) => void,
) =>
(props: FilePanelProps) => {
const { t } = useTranslation();
const { block } = props;
const editor = useBlockNoteEditor();
const type = (block.type as keyof typeof blockTypes) || 'file';
interface UppyFilePanelProps {
onCreateCallback?: (result: { file: UppyFile<UppyMeta, UppyBody>; url: string }[]) => void;
}

return (
<Dialog defaultOpen onOpenChange={() => editor.filePanel?.closeMenu()}>
<DialogContent className="md:max-w-xl">
<DialogHeader>
<DialogTitle className="h-6">{t(`common:upload_${type}`)}</DialogTitle>
<DialogDescription className="hidden" />
</DialogHeader>
<UploadUppy
isPublic={true}
uploadType={UploadType.Personal}
uppyOptions={{
restrictions: {
maxFileSize: 10 * 1024 * 1024, // 10MB
maxNumberOfFiles: 1,
allowedFileTypes: blockTypes[type].allowedFileTypes,
minFileSize: null,
maxTotalFileSize: 10 * 1024 * 1024, // 10MB
minNumberOfFiles: null,
requiredMetaFields: [],
},
}}
plugins={blockTypes[type].plugins}
imageMode="attachment"
callback={async (result) => {
for (const res of result) {
const updateData: PartialBlock = {
props: {
name: res.file.name,
url: res.url,
},
};
editor.updateBlock(block, updateData);
}
onCreateCallback?.(result);
}}
/>
</DialogContent>
</Dialog>
);
};
const UppyFilePanel: React.FC<UppyFilePanelProps & FilePanelProps> = ({ onCreateCallback, ...props }) => {
const { t } = useTranslation();
const { block } = props;

const editor = useBlockNoteEditor();
const type = (block.type as keyof typeof blockTypes) || 'file';

return (
<Dialog defaultOpen onOpenChange={() => editor.filePanel?.closeMenu()}>
<DialogContent className="md:max-w-xl">
<DialogHeader>
<DialogTitle className="h-6">{t(`common:upload_${type}`)}</DialogTitle>
<DialogDescription className="hidden" />
</DialogHeader>
<UploadUppy
isPublic={true}
uploadType={UploadType.Personal}
uppyOptions={{
restrictions: {
maxFileSize: 10 * 1024 * 1024, // 10MB
maxNumberOfFiles: 1,
allowedFileTypes: blockTypes[type].allowedFileTypes,
minFileSize: null,
maxTotalFileSize: 10 * 1024 * 1024, // 10MB
minNumberOfFiles: null,
requiredMetaFields: [],
},
}}
plugins={blockTypes[type].plugins}
imageMode="attachment"
callback={async (result) => {
for (const res of result) {
const updateData: PartialBlock = {
props: {
name: res.file.name,
url: res.url,
},
};
editor.updateBlock(block, updateData);
}
onCreateCallback?.(result);
}}
/>
</DialogContent>
</Dialog>
);
};

export default UppyFilePanel;
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ const OrganizationsNewsletterForm: React.FC<NewsletterFormProps> = ({ organizati
updateData={onChange}
className="min-h-20 pl-10 pr-6 p-3 border rounded-md"
allowedFilePanelTypes={['image', 'file']}
filePanel={UppyFilePanel()}
filePanel={(props) => <UppyFilePanel {...props} />}
/>
</Suspense>
</FormControl>
Expand Down

0 comments on commit 7defd22

Please sign in to comment.