import { MenuItem, PureMenu } from '@affine/component'; import { MuiClickAwayListener } from '@affine/component'; import type { SerializedBlock } from '@blocksuite/blocks'; import { getCurrentBlockRange, getCurrentNativeRange, getVirgoByModel, } from '@blocksuite/blocks/std'; import type { Page } from '@blocksuite/store'; import { assertExists } from '@blocksuite/store'; import type { FC } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react'; export type BookMarkProps = { page: Page; }; type ShortcutMap = { [key: string]: (e: KeyboardEvent, page: Page) => void; }; const menuOptions = [ { id: 'dismiss', label: 'Dismiss', }, { id: 'bookmark', label: 'Create bookmark', }, ]; const handleEnter = ({ page, selectedOption, callback, }: { page: Page; selectedOption: keyof ShortcutMap; callback: () => void; }) => { if (selectedOption === 'dismiss') { return callback(); } const blockRange = getCurrentBlockRange(page) as Exclude< ReturnType, null >; const vEditor = getVirgoByModel(blockRange.models[0]); const linkInfo = vEditor! .getDeltasByVRange({ index: blockRange.startOffset, length: 0, }) .find(delta => delta[0]?.attributes?.link); if (!linkInfo) { return; } const [, { index, length }] = linkInfo; const link = linkInfo[0]?.attributes?.link as string; const model = blockRange.models[0]; const parent = page.getParent(model); assertExists(parent); const currentBlockIndex = parent.children.indexOf(model); page.addBlock( 'affine:bookmark', { url: link }, parent, currentBlockIndex + 1 ); vEditor!.deleteText({ index, length, }); if (model.isEmpty()) { page.deleteBlock(model); } return callback(); }; const shouldShowBookmarkMenu = (pastedBlocks: SerializedBlock[]) => { if (!pastedBlocks.length || pastedBlocks.length > 1) { return; } const [firstBlock] = pastedBlocks; if ( !firstBlock.text || !firstBlock.text.length || firstBlock.text.length > 1 ) { return; } return !!firstBlock.text[0].attributes?.link; }; export const BookMarkUI: FC = ({ page }) => { const [anchor, setAnchor] = useState(null); const [selectedOption, setSelectedOption] = useState( menuOptions[0].id ); const shortcutMap = useMemo( () => ({ ArrowUp: () => { const curIndex = menuOptions.findIndex( ({ id }) => id === selectedOption ); if (menuOptions[curIndex - 1]) { setSelectedOption(menuOptions[curIndex - 1].id); } else if (curIndex === -1) { setSelectedOption(menuOptions[0].id); } else { setSelectedOption(menuOptions[menuOptions.length - 1].id); } }, ArrowDown: () => { const curIndex = menuOptions.findIndex( ({ id }) => id === selectedOption ); if (curIndex !== -1 && menuOptions[curIndex + 1]) { setSelectedOption(menuOptions[curIndex + 1].id); } else { setSelectedOption(menuOptions[0].id); } }, Enter: () => handleEnter({ page, selectedOption, callback: () => { setAnchor(null); }, }), Escape: () => { setAnchor(null); }, }), [page, selectedOption] ); const onKeydown = useCallback( (e: KeyboardEvent) => { const shortcut = shortcutMap[e.key]; if (shortcut) { e.stopPropagation(); e.preventDefault(); shortcut(e, page); } else { setAnchor(null); } }, [page, shortcutMap] ); useEffect(() => { const disposer = page.slots.pasted.on(pastedBlocks => { if (!shouldShowBookmarkMenu(pastedBlocks)) { return; } setTimeout(() => { setAnchor(getCurrentNativeRange()); }, 100); }); return () => { disposer.dispose(); }; }, [onKeydown, page, shortcutMap]); useEffect(() => { if (anchor) { document.addEventListener('keydown', onKeydown, { capture: true }); } else { // reset status and remove event setSelectedOption(menuOptions[0].id); document.removeEventListener('keydown', onKeydown, { capture: true }); } return () => { document.removeEventListener('keydown', onKeydown, { capture: true }); }; }, [anchor, onKeydown]); return anchor ? ( { setAnchor(null); setSelectedOption(''); }} >
{menuOptions.map(({ id, label }) => { return ( { handleEnter({ page, selectedOption: id, callback: () => { setAnchor(null); }, }); }} disableHover={true} onMouseEnter={() => { setSelectedOption(id); }} > {label} ); })}
) : null; };