(frontend) create editor shortcuts hook

We created the editor shortcuts hook to handle
the shortcuts for the editor.
We implemented the following shortcuts:
- "@" to open the interlinking inline content
This commit is contained in:
Anthony LC
2025-04-27 22:21:20 +02:00
parent 2a7c0ef800
commit e7709badbb
8 changed files with 86 additions and 8 deletions

View File

@@ -761,4 +761,20 @@ test.describe('Doc Editor', () => {
await verifyDocName(page, docChild2); await verifyDocName(page, docChild2);
}); });
test('it checks interlink shortcut @', async ({ page, browserName }) => {
const [randomDoc] = await createDoc(page, 'doc-interlink', browserName, 1);
await verifyDocName(page, randomDoc);
const editor = page.locator('.bn-block-outer').last();
await editor.click();
await page.keyboard.press('@');
await expect(
page.locator(
"span[data-inline-content-type='interlinkingSearchInline'] input",
),
).toBeVisible();
});
}); });

View File

@@ -19,8 +19,13 @@ import { Box, TextErrors } from '@/components';
import { Doc, useIsCollaborativeEditable } from '@/docs/doc-management'; import { Doc, useIsCollaborativeEditable } from '@/docs/doc-management';
import { useAuth } from '@/features/auth'; import { useAuth } from '@/features/auth';
import { useHeadings, useUploadFile, useUploadStatus } from '../hook/'; import {
import useSaveDoc from '../hook/useSaveDoc'; useHeadings,
useSaveDoc,
useShortcuts,
useUploadFile,
useUploadStatus,
} from '../hook';
import { useEditorStore } from '../stores'; import { useEditorStore } from '../stores';
import { cssEditor } from '../styles'; import { cssEditor } from '../styles';
import { DocsBlockNoteEditor } from '../types'; import { DocsBlockNoteEditor } from '../types';
@@ -153,6 +158,7 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
); );
useHeadings(editor); useHeadings(editor);
useShortcuts(editor);
useUploadStatus(editor); useUploadStatus(editor);
useEffect(() => { useEffect(() => {

View File

@@ -13,6 +13,10 @@ export const InterlinkingSearchInlineContent = createReactInlineContentSpec(
{ {
type: 'interlinkingSearchInline', type: 'interlinkingSearchInline',
propSchema: { propSchema: {
trigger: {
default: '/',
values: ['/', '@'],
},
disabled: { disabled: {
default: false, default: false,
values: [true, false], values: [true, false],
@@ -26,7 +30,13 @@ export const InterlinkingSearchInlineContent = createReactInlineContentSpec(
return null; return null;
} }
return <SearchPage {...props} contentRef={props.contentRef} />; return (
<SearchPage
{...props}
trigger={props.inlineContent.props.trigger}
contentRef={props.contentRef}
/>
);
}, },
}, },
); );
@@ -45,6 +55,7 @@ export const getInterlinkinghMenuItems = (
type: 'interlinkingSearchInline', type: 'interlinkingSearchInline',
props: { props: {
disabled: false, disabled: false,
trigger: '/',
}, },
}, },
]); ]);

View File

@@ -44,6 +44,7 @@ const inputStyle = css`
`; `;
type SearchPageProps = { type SearchPageProps = {
trigger: string;
updateInlineContent: ( updateInlineContent: (
update: PartialCustomInlineContentFromConfig< update: PartialCustomInlineContentFromConfig<
{ {
@@ -52,6 +53,9 @@ type SearchPageProps = {
disabled: { disabled: {
default: boolean; default: boolean;
}; };
trigger: {
default: string;
};
}; };
content: 'styled'; content: 'styled';
}, },
@@ -63,6 +67,7 @@ type SearchPageProps = {
export const SearchPage = ({ export const SearchPage = ({
contentRef, contentRef,
trigger,
updateInlineContent, updateInlineContent,
}: SearchPageProps) => { }: SearchPageProps) => {
const { colorsTokens } = useCunninghamTheme(); const { colorsTokens } = useCunninghamTheme();
@@ -106,7 +111,7 @@ export const SearchPage = ({
tabIndex={-1} // Ensure the span is focusable tabIndex={-1} // Ensure the span is focusable
> >
{' '} {' '}
/ {trigger}
<Box <Box
as="input" as="input"
$padding={{ left: '3px' }} $padding={{ left: '3px' }}
@@ -128,6 +133,7 @@ export const SearchPage = ({
type: 'interlinkingSearchInline', type: 'interlinkingSearchInline',
props: { props: {
disabled: true, disabled: true,
trigger,
}, },
}); });
@@ -212,6 +218,7 @@ export const SearchPage = ({
type: 'interlinkingSearchInline', type: 'interlinkingSearchInline',
props: { props: {
disabled: true, disabled: true,
trigger,
}, },
}); });

View File

@@ -5,7 +5,7 @@ import * as Y from 'yjs';
import { AppWrapper } from '@/tests/utils'; import { AppWrapper } from '@/tests/utils';
import useSaveDoc from '../useSaveDoc'; import { useSaveDoc } from '../useSaveDoc';
jest.mock('next/router', () => ({ jest.mock('next/router', () => ({
useRouter: jest.fn(), useRouter: jest.fn(),

View File

@@ -1,3 +1,4 @@
export * from './useHeadings'; export * from './useHeadings';
export * from './useSaveDoc'; export * from './useSaveDoc';
export * from './useShortcuts';
export * from './useUploadFile'; export * from './useUploadFile';

View File

@@ -10,7 +10,7 @@ import { toBase64 } from '../utils';
const SAVE_INTERVAL = 60000; const SAVE_INTERVAL = 60000;
const useSaveDoc = ( export const useSaveDoc = (
docId: string, docId: string,
yDoc: Y.Doc, yDoc: Y.Doc,
canSave: boolean, canSave: boolean,
@@ -105,5 +105,3 @@ const useSaveDoc = (
}; };
}, [router.events, saveDoc]); }, [router.events, saveDoc]);
}; };
export default useSaveDoc;

View File

@@ -0,0 +1,39 @@
import { useEffect } from 'react';
import { DocsBlockNoteEditor } from '../types';
export const useShortcuts = (editor: DocsBlockNoteEditor) => {
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === '@' && editor?.isFocused()) {
const selection = window.getSelection();
const previousChar =
selection?.anchorNode?.textContent?.charAt(
selection.anchorOffset - 1,
) || '';
if (![' ', ''].includes(previousChar)) {
return;
}
event.preventDefault();
editor.insertInlineContent([
{
type: 'interlinkingSearchInline',
props: {
disabled: false,
trigger: '@',
},
},
]);
}
};
// Attach the event listener to the document instead of the window
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [editor]);
};