✨(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:
@@ -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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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: '/',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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;
|
|
||||||
|
|||||||
@@ -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]);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user