feat: Intra-line double link interaction

This commit is contained in:
xiaodong zuo
2022-08-19 05:23:25 +08:00
parent 919e0d08d5
commit a58825dd2f
3 changed files with 195 additions and 197 deletions
@@ -57,7 +57,9 @@ export const CommonList = (props: MenuItemsProps) => {
>
{usedItems?.length ? (
usedItems.map((item, idx) => {
if (item.block) {
if (item.renderCustom) {
return item.renderCustom(item);
} else if (item.block) {
return (
<BlockPreview
className={clsx(
@@ -1,21 +1,17 @@
import React, { useEffect, useState, useCallback, useRef } from 'react';
import { Virgo, PluginHooks, HookType } from '@toeverything/framework/virgo';
import {
CommonList,
CommonListItem,
commonListContainer,
CommonListItem,
} from '@toeverything/components/common';
import { domToRect } from '@toeverything/utils';
import { styled } from '@toeverything/components/ui';
import { QueryResult } from '../../search';
import { HookType, PluginHooks, Virgo } from '@toeverything/framework/virgo';
import { domToRect } from '@toeverything/utils';
import React, { useCallback, useEffect, useRef, useState } from 'react';
export type DoubleLinkMenuContainerProps = {
editor: Virgo;
hooks: PluginHooks;
style?: React.CSSProperties;
isShow?: boolean;
blockId: string;
onSelected?: (item: string) => void;
onClose?: () => void;
@@ -25,7 +21,6 @@ export type DoubleLinkMenuContainerProps = {
export const DoubleLinkMenuContainer = ({
hooks,
isShow = false,
onSelected,
onClose,
types,
@@ -66,12 +61,13 @@ export const DoubleLinkMenuContainer = ({
}, [needCheckIntoView, currentItem]);
useEffect(() => {
if (isShow && types && !currentItem) setCurrentItem(types[0]);
if (!isShow) onClose?.();
}, [currentItem, isShow, onClose, types]);
if (types && !currentItem) {
setCurrentItem(types[0]);
}
}, [currentItem, onClose, types]);
useEffect(() => {
if (isShow && types) {
if (types) {
if (!types.includes(currentItem)) {
setNeedCheckIntoView(true);
if (types.length) {
@@ -81,11 +77,11 @@ export const DoubleLinkMenuContainer = ({
}
}
}
}, [isShow, types, currentItem]);
}, [types, currentItem]);
const handleClickUp = useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (isShow && types && event.code === 'ArrowUp') {
if (types && event.code === 'ArrowUp') {
event.preventDefault();
if (!currentItem && types.length) {
setCurrentItem(types[types.length - 1]);
@@ -99,12 +95,12 @@ export const DoubleLinkMenuContainer = ({
}
}
},
[isShow, types, currentItem]
[types, currentItem]
);
const handleClickDown = useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (isShow && types && event.code === 'ArrowDown') {
if (types && event.code === 'ArrowDown') {
event.preventDefault();
if (!currentItem && types.length) {
setCurrentItem(types[0]);
@@ -118,17 +114,17 @@ export const DoubleLinkMenuContainer = ({
}
}
},
[isShow, types, currentItem]
[types, currentItem]
);
const handleClickEnter = useCallback(
async (event: React.KeyboardEvent<HTMLDivElement>) => {
if (isShow && event.code === 'Enter' && currentItem) {
if (event.code === 'Enter' && currentItem) {
event.preventDefault();
onSelected && onSelected(currentItem);
}
},
[isShow, currentItem, onSelected]
[currentItem, onSelected]
);
const handleKeyDown = useCallback(
@@ -150,7 +146,7 @@ export const DoubleLinkMenuContainer = ({
};
}, [hooks, handleKeyDown]);
return isShow ? (
return (
<RootContainer
ref={menuRef}
onKeyDownCapture={handleKeyDown}
@@ -165,11 +161,10 @@ export const DoubleLinkMenuContainer = ({
/>
</ContentContainer>
</RootContainer>
) : null;
);
};
const RootContainer = styled('div')(({ theme }) => ({
// position: 'fixed',
zIndex: 1,
maxHeight: '525px',
borderRadius: '10px',
@@ -1,10 +1,10 @@
import { CommonListItem } from '@toeverything/components/common';
import { AddIcon } from '@toeverything/components/icons';
import {
Input,
ListButton,
MuiClickAwayListener,
MuiGrow as Grow,
MuiOutlinedInput as OutlinedInput,
MuiPaper as Paper,
MuiPopper as Popper,
styled,
@@ -13,6 +13,7 @@ import { services } from '@toeverything/datasource/db-service';
import { HookType, PluginHooks, Virgo } from '@toeverything/framework/virgo';
import { getPageId } from '@toeverything/utils';
import React, {
ChangeEvent,
useCallback,
useEffect,
useMemo,
@@ -24,6 +25,7 @@ import { DoubleLinkMenuContainer } from './Container';
const ADD_NEW_SUB_PAGE = 'AddNewSubPage';
const ADD_NEW_PAGE = 'AddNewPage';
const ARRAY_KEYS = ['ArrowRight', 'ArrowLeft', 'ArrowUp', 'ArrowDown'];
export type DoubleLinkMenuProps = {
editor: Virgo;
@@ -31,7 +33,7 @@ export type DoubleLinkMenuProps = {
style?: { left: number; top: number };
};
type DoubleMenuStyle = {
type DoubleLinkMenuStyle = {
left: number;
top: number;
height: number;
@@ -42,58 +44,63 @@ export const DoubleLinkMenu = ({
hooks,
style,
}: DoubleLinkMenuProps) => {
const [isShow, setIsShow] = useState(false);
const [blockId, setBlockId] = useState<string>();
const [searchText, setSearchText] = useState<string>('');
const [searchBlocks, setSearchBlocks] = useState<QueryResult>([]);
const [items, setItems] = useState<CommonListItem[]>([]);
const [isNewPage, setIsNewPage] = useState(false);
const [isOpen, setIsOpen] = useState(false);
const [anchorEl, setAnchorEl] = useState(null);
const ref = useRef();
const ref1 = useRef<HTMLInputElement>();
const [referenceMenuStyle, setReferenceMenuStyle] =
useState<DoubleMenuStyle>({
const dialogRef = useRef<HTMLDivElement>();
const newPageSearchRef = useRef<HTMLInputElement>();
const [doubleLinkMenuStyle, setDoubleLinkMenuStyle] =
useState<DoubleLinkMenuStyle>({
left: 0,
top: 0,
height: 0,
});
useEffect(() => {
QueryBlocks(editor, searchText, result => {
setSearchBlocks(result);
ref1?.current?.focus();
const items: CommonListItem[] = [];
if (searchBlocks?.length > 0) {
if (isNewPage) {
items.push({
renderCustom: () => {
return <ListButton content={'SUGGESTED'} />;
},
});
} else {
items.push({
renderCustom: () => {
return <ListButton content={'LINK TO PAGE'} />;
},
});
}
items.push(
...(searchBlocks?.map(
block => ({ block } as CommonListItem)
) || [])
);
}
const [curBlockId, setCurBlockId] = useState<string>();
const [searchText, setSearchText] = useState<string>('');
const [inAddNewPage, setInAddNewPage] = useState(false);
const [filterText, setFilterText] = useState<string>('');
const [searchResultBlocks, setSearchResultBlocks] = useState<QueryResult>(
[]
);
if (items.length > 0) {
items.push({ divider: 'newPage' });
}
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({
content: {
id: ADD_NEW_SUB_PAGE,
content: 'Add new sub-page',
icon: AddIcon,
renderCustom: () => {
return (
<ListButton
content={
inAddNewPage ? 'SUGGESTED' : 'LINK TO PAGE'
}
/>
);
},
});
items.push(
...(searchResultBlocks?.map(
block => ({ block } as CommonListItem)
) || [])
);
}
if (items.length > 0) {
items.push({ divider: 'newPage' });
}
items.push({
content: {
id: ADD_NEW_SUB_PAGE,
content: 'Add new sub-page',
icon: AddIcon,
},
});
!inAddNewPage &&
items.push({
content: {
id: ADD_NEW_PAGE,
@@ -101,26 +108,28 @@ export const DoubleLinkMenu = ({
icon: AddIcon,
},
});
return items;
}, [searchResultBlocks, inAddNewPage]);
setItems(items);
useEffect(() => {
const text = inAddNewPage ? filterText : searchText;
QueryBlocks(editor, text, result => {
setSearchResultBlocks(result);
});
}, [editor, searchText, isNewPage]);
const types = useMemo(() => {
return Object.values(searchBlocks)
.map(({ id }) => id)
.concat([ADD_NEW_SUB_PAGE, ADD_NEW_PAGE]);
}, [searchBlocks]);
}, [editor, searchText, filterText, inAddNewPage]);
const hideMenu = useCallback(() => {
setIsShow(false);
setIsNewPage(false);
editor.blockHelper.removeDoubleLinkSearchSlash(blockId);
setIsOpen(false);
setInAddNewPage(false);
editor.blockHelper.removeDoubleLinkSearchSlash(curBlockId);
editor.scrollManager.unLock();
}, [blockId, editor.blockHelper, editor.scrollManager]);
}, [curBlockId, editor]);
const handleSearch = useCallback(
const searchChange = useCallback(
async (event: React.KeyboardEvent<HTMLDivElement>) => {
if (ARRAY_KEYS.includes(event.key)) {
return;
}
const { type, anchorNode } = editor.selection.currentSelectInfo;
if (
type === 'Range' &&
@@ -130,24 +139,31 @@ export const DoubleLinkMenu = ({
const text = editor.blockHelper.getBlockTextBeforeSelection(
anchorNode.id
);
if (text.endsWith('[[')) {
if (
[
'ArrowRight',
'ArrowLeft',
'ArrowUp',
'ArrowDown',
].includes(event.key)
) {
return;
}
if (event.key === 'Backspace') {
hideMenu();
return;
}
setBlockId(anchorNode.id);
editor.blockHelper.removeDoubleLinkSearchSlash(blockId);
editor.blockHelper.removeDoubleLinkSearchSlash(curBlockId);
setCurBlockId(anchorNode.id);
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();
setDoubleLinkMenuStyle({
top: rectTop - top,
left: clientRect.left - left,
height: clientRect.height,
});
setAnchorEl(dialogRef.current);
}
setTimeout(() => {
const textSelection =
editor.blockHelper.selectionToSlateRange(
@@ -163,40 +179,22 @@ export const DoubleLinkMenu = ({
);
}
});
setSearchText('');
setIsShow(true);
editor.scrollManager.lock();
const rect =
editor.selection.currentSelectInfo?.browserSelection
?.getRangeAt(0)
?.getBoundingClientRect();
if (rect) {
const rectTop = rect.top;
const { top, left } =
editor.container.getBoundingClientRect();
setReferenceMenuStyle({
top: rectTop - top,
left: rect.left - left,
height: rect.height,
});
setAnchorEl(ref.current);
}
}
}
if (isShow) {
if (isOpen) {
const searchText =
editor.blockHelper.getDoubleLinkSearchSlashText(blockId);
editor.blockHelper.getDoubleLinkSearchSlashText(curBlockId);
if (searchText && searchText.startsWith('[[')) {
setSearchText(searchText.slice(2).trim());
}
}
},
[editor, isShow, blockId, hideMenu]
[editor, isOpen, curBlockId, hideMenu]
);
const handleKeyup = useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => handleSearch(event),
[handleSearch]
(event: React.KeyboardEvent<HTMLDivElement>) => searchChange(event),
[searchChange]
);
const handleKeyDown = useCallback(
@@ -223,75 +221,86 @@ export const DoubleLinkMenu = ({
};
}, [handleKeyup, handleKeyDown, hooks]);
const handleSelected = async (linkBlockId: string) => {
if (blockId) {
if (linkBlockId === ADD_NEW_SUB_PAGE) {
handleAddSubPage();
return;
}
if (linkBlockId === ADD_NEW_PAGE) {
setIsNewPage(true);
return;
}
if (isNewPage) {
const newPage = await services.api.editorBlock.create({
workspace: editor.workspace,
type: 'page' as const,
});
await services.api.pageTree.addChildPageToWorkspace(
editor.workspace,
linkBlockId,
newPage.id
);
linkBlockId = newPage.id;
}
editor.blockHelper.setSelectDoubleLinkSearchSlash(blockId);
const insertDoubleLink = useCallback(
async (pageId: string) => {
editor.blockHelper.setSelectDoubleLinkSearchSlash(curBlockId);
await editor.blockHelper.insertDoubleLink(
editor.workspace,
linkBlockId,
blockId
pageId,
curBlockId
);
hideMenu();
},
[editor, curBlockId, hideMenu]
);
const addSubPage = useCallback(
async (parentPageId: string) => {
const newPage = await services.api.editorBlock.create({
workspace: editor.workspace,
type: 'page' as const,
});
services.api.editorBlock.update({
id: newPage.id,
workspace: editor.workspace,
properties: {
text: { value: [{ text: searchText }] },
},
});
await services.api.pageTree.addChildPageToWorkspace(
editor.workspace,
parentPageId,
newPage.id
);
return newPage.id;
},
[searchText, editor]
);
const handleSelected = async (id: string) => {
if (curBlockId) {
if (id === ADD_NEW_PAGE) {
setInAddNewPage(true);
setTimeout(() => {
newPageSearchRef.current?.focus();
});
return;
}
if (id === ADD_NEW_SUB_PAGE) {
const pageId = await addSubPage(getPageId());
insertDoubleLink(pageId);
return;
}
if (inAddNewPage) {
const pageId = await addSubPage(id);
insertDoubleLink(pageId);
} else {
insertDoubleLink(id);
}
}
};
const handleClose = () => {
blockId && editor.blockHelper.removeDoubleLinkSearchSlash(blockId);
};
const handleFilterChange = useCallback(
async (e: ChangeEvent<HTMLInputElement>) => {
const text = e.target.value;
const handleAddSubPage = async () => {
const newPage = await services.api.editorBlock.create({
workspace: editor.workspace,
type: 'page' as const,
});
setIsNewPage(false);
services.api.editorBlock.update({
id: newPage.id,
workspace: editor.workspace,
properties: {
text: { value: [{ text: searchText }] },
},
});
await services.api.pageTree.addChildPageToWorkspace(
editor.workspace,
getPageId(),
newPage.id
);
handleSelected(newPage.id);
};
await setFilterText(text);
},
[]
);
return (
<div
ref={ref}
ref={dialogRef}
style={{
position: 'absolute',
width: '10px',
...referenceMenuStyle,
...doubleLinkMenuStyle,
}}
>
<MuiClickAwayListener onClickAway={() => hideMenu()}>
<Popper
open={isShow}
open={isOpen}
anchorEl={anchorEl}
transition
placement="bottom-start"
@@ -304,25 +313,26 @@ export const DoubleLinkMenu = ({
}}
>
<Paper>
<DoubleLinkMenuWrapper>
<SearchContainer ref={ref1}>
<OutlinedInput
placeholder="Filter actions..."
onChange={() => {}}
{inAddNewPage && (
<NewPageSearchContainer>
<Input
ref={newPageSearchRef}
value={filterText}
onChange={handleFilterChange}
placeholder="Search page to add to..."
/>
</SearchContainer>
<DoubleLinkMenuContainer
editor={editor}
hooks={hooks}
style={style}
isShow={true}
blockId={blockId}
onSelected={handleSelected}
onClose={handleClose}
items={items}
types={types}
/>
</DoubleLinkMenuWrapper>
</NewPageSearchContainer>
)}
<DoubleLinkMenuContainer
editor={editor}
hooks={hooks}
style={style}
blockId={curBlockId}
onSelected={handleSelected}
onClose={hideMenu}
items={menuItems}
types={menuTypes}
/>
</Paper>
</Grow>
)}
@@ -332,15 +342,6 @@ export const DoubleLinkMenu = ({
);
};
const DoubleLinkMenuWrapper = styled('div')({
zIndex: 1,
});
const SearchContainer = styled('div')({
padding: '8px 8px',
input: {
height: '28px',
padding: '5px 10px',
with: '300px',
},
const NewPageSearchContainer = styled('div')({
padding: '8px 8px 0px 8px',
});