feat: Link feature add search function

This commit is contained in:
xiaodong zuo
2022-09-01 10:44:21 +08:00
parent 73d6e34c5d
commit e18a35989f
9 changed files with 367 additions and 2 deletions
@@ -4,4 +4,5 @@ export { GroupMenuPlugin } from './group-menu';
export { InlineMenuPlugin } from './inline-menu';
export { LeftMenuPlugin } from './left-menu/LeftMenuPlugin';
export { MENU_WIDTH as menuWidth } from './left-menu/menu-config';
export { LinkMenuPlugin } from './link-menu';
export { SelectionGroupPlugin } from './selection-group-menu';
@@ -185,6 +185,8 @@ const common_handler_for_inline_menu: ClickItemHandler = ({
editor,
blockId: anchorNodeId,
});
editor.plugins.emit('showAddLink');
setShow(false);
break;
case inlineMenuNamesKeys.code:
toggle_text_format({
@@ -0,0 +1,263 @@
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 showDoubleLink = () => {
const { anchorNode } = editor.selection.currentSelectInfo;
resetState('', anchorNode.id);
};
editor.plugins.observe('showAddLink', showDoubleLink);
return () => editor.plugins.unobserve('showAddLink', showDoubleLink);
}, [editor, resetState]);
const handleSelected = async (id: string) => {
if (curBlockId) {
}
};
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, like https://affine.pro"
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',
width: '354px',
}));
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',
}));
@@ -0,0 +1,33 @@
import { StrictMode } from 'react';
import { BasePlugin } from '../../base-plugin';
import { PluginRenderRoot } from '../../utils';
import { LinkMenu } from './LinkMenu';
const PLUGIN_NAME = 'reference-menu';
export class LinkMenuPlugin extends BasePlugin {
private _root?: PluginRenderRoot;
public static override get pluginName(): string {
return PLUGIN_NAME;
}
protected override _onRender(): void {
this._root = new PluginRenderRoot({
name: PLUGIN_NAME,
render: this.editor.reactRenderRoot.render,
});
this._root.mount();
this._root?.render(
<StrictMode>
<LinkMenu editor={this.editor} hooks={this.hooks} />
</StrictMode>
);
}
public override dispose() {
this._root?.unmount();
super.dispose();
}
}
@@ -0,0 +1 @@
export { LinkMenuPlugin } from './Plugin';