Blocknote custom slash menu (#4517)

blocknote v12, cleaned up blockschema & specs, added custom slash menu
This commit is contained in:
brendanlaschke
2024-03-20 08:38:05 +01:00
committed by GitHub
parent 35d41e38c8
commit 017b09ba35
11 changed files with 308 additions and 183 deletions

View File

@ -67,6 +67,9 @@ export {
IconFilterOff,
IconForbid,
IconGripVertical,
IconH1,
IconH2,
IconH3,
IconHeadphones,
IconHeart,
IconHeartOff,
@ -97,6 +100,7 @@ export {
IconPencil,
IconPhone,
IconPhoto,
IconPilcrow,
IconPlug,
IconPlus,
IconPresentation,

View File

@ -1,12 +1,22 @@
import { BlockNoteEditor } from '@blocknote/core';
import { BlockNoteView } from '@blocknote/react';
import { ClipboardEvent } from 'react';
import { filterSuggestionItems } from '@blocknote/core';
import { BlockNoteView, SuggestionMenuController } from '@blocknote/react';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { blockSchema } from '@/activities/blocks/schema';
import { getSlashMenu } from '@/activities/blocks/slashMenu';
import {
CustomSlashMenu,
SuggestionItem,
} from '@/ui/input/editor/components/CustomSlashMenu';
interface BlockEditorProps {
editor: BlockNoteEditor;
editor: typeof blockSchema.BlockNoteEditor;
onFocus?: () => void;
onBlur?: () => void;
onPaste?: (event: ClipboardEvent) => void;
onChange?: () => void;
}
const StyledEditor = styled.div`
@ -22,7 +32,13 @@ const StyledEditor = styled.div`
}
`;
export const BlockEditor = ({ editor, onFocus, onBlur }: BlockEditorProps) => {
export const BlockEditor = ({
editor,
onFocus,
onBlur,
onChange,
onPaste,
}: BlockEditorProps) => {
const theme = useTheme();
const blockNoteTheme = theme.name === 'light' ? 'light' : 'dark';
@ -34,14 +50,33 @@ export const BlockEditor = ({ editor, onFocus, onBlur }: BlockEditorProps) => {
onBlur?.();
};
const handleChange = () => {
onChange?.();
};
const handlePaste = (event: ClipboardEvent) => {
onPaste?.(event);
};
return (
<StyledEditor>
<BlockNoteView
onFocus={handleFocus}
onBlur={handleBlur}
onPaste={handlePaste}
onChange={handleChange}
editor={editor}
theme={blockNoteTheme}
/>
slashMenu={false}
>
<SuggestionMenuController
triggerCharacter={'/'}
getItems={async (query) =>
filterSuggestionItems<SuggestionItem>(getSlashMenu(editor), query)
}
suggestionMenuComponent={CustomSlashMenu}
/>
</BlockNoteView>
</StyledEditor>
);
};

View File

@ -0,0 +1,42 @@
import { SuggestionMenuProps } from '@blocknote/react';
import styled from '@emotion/styled';
import { IconComponent } from '@/ui/display/icon/types/IconComponent';
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { MenuItemSuggestion } from '@/ui/navigation/menu-item/components/MenuItemSuggestion';
export type SuggestionItem = {
title: string;
onItemClick: () => void;
aliases?: string[];
Icon?: IconComponent;
};
type CustomSlashMenuProps = SuggestionMenuProps<SuggestionItem>;
const StyledSlashMenu = styled.div`
* {
box-sizing: content-box;
}
`;
export const CustomSlashMenu = (props: CustomSlashMenuProps) => {
return (
<StyledSlashMenu>
<DropdownMenu style={{ zIndex: 2001 }}>
<DropdownMenuItemsContainer>
{props.items.map((item, index) => (
<MenuItemSuggestion
key={item.title}
onClick={() => item.onItemClick()}
text={item.title}
LeftIcon={item.Icon}
selected={props.selectedIndex === index}
/>
))}
</DropdownMenuItemsContainer>
</DropdownMenu>
</StyledSlashMenu>
);
};

View File

@ -0,0 +1,79 @@
import { MouseEvent } from 'react';
import styled from '@emotion/styled';
import { IconComponent } from '@/ui/display/icon/types/IconComponent';
import { HOVER_BACKGROUND } from '@/ui/theme/constants/HoverBackground';
import { MenuItemLeftContent } from '../internals/components/MenuItemLeftContent';
import { StyledMenuItemLeftContent } from '../internals/components/StyledMenuItemBase';
export type MenuItemSuggestionProps = {
LeftIcon?: IconComponent | null;
text: string;
selected?: boolean;
className?: string;
onClick?: (event: MouseEvent<HTMLLIElement>) => void;
};
const StyledSuggestionMenuItem = styled.li<{
selected?: boolean;
}>`
--horizontal-padding: ${({ theme }) => theme.spacing(1)};
--vertical-padding: ${({ theme }) => theme.spacing(2)};
align-items: center;
border-radius: ${({ theme }) => theme.border.radius.sm};
cursor: pointer;
display: flex;
flex-direction: row;
font-size: ${({ theme }) => theme.font.size.sm};
gap: ${({ theme }) => theme.spacing(2)};
height: calc(32px - 2 * var(--vertical-padding));
justify-content: space-between;
padding: var(--vertical-padding) var(--horizontal-padding);
background: ${({ selected, theme }) =>
selected ? theme.background.transparent.medium : ''};
${HOVER_BACKGROUND};
position: relative;
user-select: none;
width: calc(100% - 2 * var(--horizontal-padding));
`;
export const MenuItemSuggestion = ({
LeftIcon,
text,
className,
selected,
onClick,
}: MenuItemSuggestionProps) => {
const handleMenuItemClick = (event: MouseEvent<HTMLLIElement>) => {
if (!onClick) return;
event.preventDefault();
event.stopPropagation();
onClick?.(event);
};
return (
<StyledSuggestionMenuItem
onClick={handleMenuItemClick}
className={className}
selected={selected}
>
<StyledMenuItemLeftContent>
<MenuItemLeftContent LeftIcon={LeftIcon ?? undefined} text={text} />
</StyledMenuItemLeftContent>
</StyledSuggestionMenuItem>
);
};