Fixes: #20625 Original PR: #20626 New changes: - Now the text says "Close menu" instead of "No command found". - "Close menu" is interactive like other commands ( With keyboard and with mouse ). - It closes automatically when user type 3 extra character past the point of no result. https://github.com/user-attachments/assets/9c1315b4-5a63-424d-8da8-dc1283535725 --------- Co-authored-by: Charles Bochet <charles@twenty.com>
225 lines
5.8 KiB
TypeScript
225 lines
5.8 KiB
TypeScript
import {
|
|
filterSuggestionItems,
|
|
SuggestionMenu,
|
|
} from '@blocknote/core/extensions';
|
|
import { BlockNoteView } from '@blocknote/mantine';
|
|
import { SuggestionMenuController } from '@blocknote/react';
|
|
import { useLingui } from '@lingui/react/macro';
|
|
import { styled } from '@linaria/react';
|
|
import { type ClipboardEvent, useContext } from 'react';
|
|
import { type BLOCK_SCHEMA } from '@/blocknote-editor/blocks/Schema';
|
|
import { getSlashMenu } from '@/blocknote-editor/utils/getSlashMenu';
|
|
import { CustomMentionMenu } from '@/blocknote-editor/components/CustomMentionMenu';
|
|
import { CustomSideMenu } from '@/blocknote-editor/components/CustomSideMenu';
|
|
import {
|
|
CustomSlashMenu,
|
|
type SuggestionItem,
|
|
} from '@/blocknote-editor/components/CustomSlashMenu';
|
|
import { useMentionMenu } from '@/mention/hooks/useMentionMenu';
|
|
import { IconX } from 'twenty-ui/display';
|
|
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
|
|
|
interface BlockEditorProps {
|
|
editor: typeof BLOCK_SCHEMA.BlockNoteEditor;
|
|
onFocus?: () => void;
|
|
onBlur?: () => void;
|
|
onPaste?: (event: ClipboardEvent) => void;
|
|
onChange?: () => void;
|
|
readonly?: boolean;
|
|
}
|
|
|
|
// oxlint-disable-next-line twenty/no-hardcoded-colors
|
|
const StyledEditor = styled.div`
|
|
width: 100%;
|
|
|
|
& .editor {
|
|
background: transparent;
|
|
color: ${themeCssVariables.font.color.primary};
|
|
font-size: 13px;
|
|
min-height: 400px;
|
|
}
|
|
& .editor [class^='_inlineContent']:before {
|
|
color: ${themeCssVariables.font.color.tertiary};
|
|
font-style: normal !important;
|
|
}
|
|
& .editor .bn-inline-content:has(> .ProseMirror-trailingBreak):before {
|
|
font-style: normal;
|
|
}
|
|
& .mantine-ActionIcon-icon {
|
|
background: transparent;
|
|
height: 20px;
|
|
width: 20px;
|
|
}
|
|
& .bn-container .bn-drag-handle {
|
|
height: 20px;
|
|
width: 20px;
|
|
}
|
|
& .bn-block-content[data-content-type='checkListItem'] > div > div {
|
|
align-items: center;
|
|
display: flex;
|
|
}
|
|
& .bn-drag-handle-menu {
|
|
backdrop-filter: ${themeCssVariables.blur.medium};
|
|
background: ${themeCssVariables.background.transparent.secondary};
|
|
border: 1px solid ${themeCssVariables.border.color.medium};
|
|
border-radius: 8px;
|
|
box-shadow:
|
|
0px 2px 4px rgba(0, 0, 0, 0.04),
|
|
2px 4px 16px rgba(0, 0, 0, 0.12);
|
|
left: 26px;
|
|
min-height: 96px;
|
|
min-width: 160px;
|
|
padding: 4px;
|
|
}
|
|
|
|
& .bn-editor {
|
|
padding-inline: 0px;
|
|
}
|
|
|
|
& .bn-inline-content {
|
|
width: 100%;
|
|
}
|
|
|
|
& .bn-container .bn-suggestion-menu-item:hover {
|
|
background-color: blue;
|
|
}
|
|
|
|
& .bn-suggestion-menu {
|
|
backdrop-filter: ${themeCssVariables.blur.medium};
|
|
background: ${themeCssVariables.background.transparent.secondary};
|
|
border: 1px solid ${themeCssVariables.border.color.medium};
|
|
border-radius: 8px;
|
|
padding: 4px;
|
|
}
|
|
|
|
& .mantine-Menu-item {
|
|
background-color: transparent;
|
|
color: ${themeCssVariables.font.color.secondary};
|
|
font-family: ${themeCssVariables.font.family};
|
|
|
|
font-style: normal;
|
|
font-weight: ${themeCssVariables.font.weight.regular};
|
|
min-height: 32px;
|
|
min-width: 152px;
|
|
}
|
|
& .mantine-ActionIcon-root:hover {
|
|
backdrop-filter: blur(20px);
|
|
background: ${themeCssVariables.background.transparent.primary};
|
|
border: 1px solid ${themeCssVariables.border.color.light};
|
|
box-shadow:
|
|
0px 0px 4px rgba(0, 0, 0, 0.08),
|
|
0px 2px 4px rgba(0, 0, 0, 0.04);
|
|
}
|
|
& .bn-side-menu .mantine-UnstyledButton-root:not(.mantine-Menu-item) svg {
|
|
height: 16px;
|
|
width: 16px;
|
|
}
|
|
|
|
& .bn-mantine .bn-side-menu > [draggable='true'] {
|
|
margin-bottom: 5px;
|
|
}
|
|
& .bn-color-picker-dropdown {
|
|
margin-left: 8px;
|
|
}
|
|
|
|
& .bn-inline-content a {
|
|
color: ${themeCssVariables.color.blue};
|
|
}
|
|
|
|
& .bn-inline-content code {
|
|
background-color: ${themeCssVariables.background.transparent.light};
|
|
border: 1px solid ${themeCssVariables.font.color.extraLight};
|
|
border-radius: 4px;
|
|
color: ${themeCssVariables.font.color.danger};
|
|
font-family: monospace;
|
|
font-size: 0.9rem;
|
|
padding: 2px 4px;
|
|
}
|
|
|
|
& .bn-mantine {
|
|
container-type: inline-size;
|
|
}
|
|
|
|
& .bn-mantine .bn-panel {
|
|
width: min(500px, 100cqi);
|
|
}
|
|
`;
|
|
|
|
export const BlockEditor = ({
|
|
editor,
|
|
onFocus,
|
|
onBlur,
|
|
onChange,
|
|
onPaste,
|
|
readonly,
|
|
}: BlockEditorProps) => {
|
|
const { colorScheme } = useContext(ThemeContext);
|
|
const { t } = useLingui();
|
|
|
|
const blockNoteTheme = colorScheme === 'light' ? 'light' : 'dark';
|
|
const getMentionItems = useMentionMenu(editor);
|
|
|
|
const getSlashMenuItems = async (query: string) => {
|
|
const filtered = filterSuggestionItems<SuggestionItem>(
|
|
getSlashMenu(editor),
|
|
query,
|
|
);
|
|
|
|
if (filtered.length > 0) {
|
|
return filtered;
|
|
}
|
|
|
|
return [
|
|
{
|
|
title: t`Close menu`,
|
|
Icon: IconX,
|
|
onItemClick: () => editor.getExtension(SuggestionMenu)?.closeMenu(),
|
|
},
|
|
];
|
|
};
|
|
|
|
const handleFocus = () => {
|
|
onFocus?.();
|
|
};
|
|
|
|
const handleBlur = () => {
|
|
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}
|
|
sideMenu={false}
|
|
editable={!readonly}
|
|
>
|
|
<CustomSideMenu editor={editor} />
|
|
<SuggestionMenuController
|
|
triggerCharacter="/"
|
|
getItems={getSlashMenuItems}
|
|
suggestionMenuComponent={CustomSlashMenu}
|
|
/>
|
|
<SuggestionMenuController
|
|
triggerCharacter="@"
|
|
getItems={async (query) => getMentionItems(query)}
|
|
suggestionMenuComponent={CustomMentionMenu}
|
|
/>
|
|
</BlockNoteView>
|
|
</StyledEditor>
|
|
);
|
|
};
|