Files
AFFiNE-Mirror/libs/components/editor-plugins/src/menu/link-menu/LinkMenu.tsx
T
2022-09-05 11:39:31 +08:00

260 lines
8.5 KiB
TypeScript

import { CommonListItem, isUrl } from '@toeverything/components/common';
import { LinkIcon } from '@toeverything/components/icons';
import {
ListButton,
MuiClickAwayListener,
MuiGrow as Grow,
MuiPaper as Paper,
MuiPopper as Popper,
styled,
} from '@toeverything/components/ui';
import { PluginHooks, Virgo } from '@toeverything/framework/virgo';
import {
ChangeEvent,
KeyboardEvent,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { useParams } from 'react-router-dom';
import { QueryBlocks, QueryResult } from '../../search';
import { DoubleLinkMenuContainer } from '../double-link-menu/Container';
const ADD_NEW_SUB_PAGE = 'AddNewSubPage';
const ADD_NEW_PAGE = 'AddNewPage';
export type LinkMenuProps = {
editor: Virgo;
hooks: PluginHooks;
};
type LinkMenuStyle = {
left: number;
top: number;
height: number;
};
const normalizeUrl = (url: string) => {
// eslint-disable-next-line no-restricted-globals
return /^https?/.test(url) ? url : `${location.protocol}//${url}`;
};
export const LinkMenu = ({ editor, hooks }: LinkMenuProps) => {
const { page_id: curPageId } = useParams();
const [isOpen, setIsOpen] = useState(false);
const [anchorEl, setAnchorEl] = useState(null);
const dialogRef = useRef<HTMLDivElement>();
const inputEl = useRef<HTMLInputElement>();
const [linkMenuStyle, setLinkMenuStyle] = useState<LinkMenuStyle>({
left: 0,
top: 0,
height: 0,
});
const url = '';
const [curBlockId, setCurBlockId] = useState<string>();
const [searchText, setSearchText] = useState<string>();
const [searchResultBlocks, setSearchResultBlocks] = useState<QueryResult>(
[]
);
const menuTypes = useMemo(() => {
return Object.values(searchResultBlocks)
.map(({ id }) => id)
.concat([ADD_NEW_SUB_PAGE, ADD_NEW_PAGE]);
}, [searchResultBlocks]);
const menuItems: CommonListItem[] = useMemo(() => {
const items: CommonListItem[] = [];
if (searchResultBlocks?.length > 0) {
items.push({
renderCustom: () => {
return <ListButton content={'LINK TO PAGE'} />;
},
});
items.push(
...(searchResultBlocks?.map(
block =>
({
block: {
...block,
content: block.content || 'Untitled',
},
} as CommonListItem)
) || [])
);
}
return items;
}, [searchResultBlocks]);
useEffect(() => {
const text = searchText;
QueryBlocks(editor, text, result => {
result = result.filter(item => item.id !== curPageId);
setSearchResultBlocks(result);
});
}, [editor, searchText, curPageId]);
const hideMenu = useCallback(() => {
setIsOpen(false);
editor.blockHelper.removeDoubleLinkSearchSlash(curBlockId);
editor.scrollManager.unLock();
}, [curBlockId, editor]);
const resetState = useCallback(
(preNodeId: string, nextNodeId: string) => {
setCurBlockId(nextNodeId);
setSearchText('');
setIsOpen(true);
editor.scrollManager.lock();
const clientRect =
editor.selection.currentSelectInfo?.browserSelection
?.getRangeAt(0)
?.getBoundingClientRect();
if (clientRect) {
const rectTop = clientRect.top;
const { top, left } = editor.container.getBoundingClientRect();
setLinkMenuStyle({
top: rectTop - top,
left: clientRect.left - left,
height: clientRect.height,
});
setAnchorEl(dialogRef.current);
}
},
[editor]
);
useEffect(() => {
const showLinkMenu = () => {
const { anchorNode } = editor.selection.currentSelectInfo;
resetState('', anchorNode.id);
};
editor.plugins.observe('showAddLink', showLinkMenu);
return () => editor.plugins.unobserve('showAddLink', showLinkMenu);
}, [editor, resetState]);
const handleSelected = async (id: string) => {};
const handleFilterChange = useCallback(
async (e: ChangeEvent<HTMLInputElement>) => {
const text = e.target.value;
await setSearchText(text);
},
[]
);
const addLinkUrlToText = () => {
const newUrl = inputEl.current.value;
if (newUrl && newUrl !== url && isUrl(normalizeUrl(newUrl))) {
editor.blockHelper.wrapLink(curBlockId, newUrl);
hideMenu();
return;
}
};
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
addLinkUrlToText();
}
if (e.key === 'Escape') {
hideMenu();
}
};
return (
<div
ref={dialogRef}
style={{
position: 'absolute',
width: '10px',
...linkMenuStyle,
}}
>
{isOpen && (
<MuiClickAwayListener onClickAway={() => hideMenu()}>
<Popper
open={isOpen}
anchorEl={anchorEl}
transition
placement="bottom-start"
>
{({ TransitionProps }) => (
<Grow
{...TransitionProps}
style={{
transformOrigin: 'left bottom',
}}
>
<Paper>
<LinkModalContainer>
<LinkModalContainerIcon>
<LinkIcon
style={{
fontSize: '16px',
marginTop: '2px',
}}
/>
</LinkModalContainerIcon>
<LinkModalContainerInput
onKeyDown={handleKeyDown}
placeholder="Paste link url"
autoComplete="off"
value={searchText}
onChange={handleFilterChange}
ref={inputEl}
/>
</LinkModalContainer>
{menuItems.length > 0 && (
<DoubleLinkMenuContainer
editor={editor}
hooks={hooks}
blockId={curBlockId}
onSelected={handleSelected}
onClose={hideMenu}
items={menuItems}
types={menuTypes}
/>
)}
</Paper>
</Grow>
)}
</Popper>
</MuiClickAwayListener>
)}
</div>
);
};
const LinkModalContainer = styled('div')(({ theme }) => ({
display: 'flex',
borderRadius: '4px',
boxShadow: theme.affine.shadows.shadow1,
backgroundColor: '#fff',
alignItems: 'center',
zIndex: '1',
}));
const LinkModalContainerIcon = styled('div')(({ theme }) => ({
display: 'flex',
width: '16px',
margin: '0 16px 0 4px',
color: '#4C6275',
}));
const LinkModalContainerInput = styled('input')(({ theme }) => ({
flex: '1',
outline: 'none',
border: 'none',
padding: '8px',
fontFamily: 'Helvetica,Arial,"Microsoft Yahei",SimHei,sans-serif',
'::-webkit-input-placeholder': {
color: '#98acbd',
},
color: '#4C6275',
}));