mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-14 13:25:12 +00:00
153 lines
4.7 KiB
TypeScript
153 lines
4.7 KiB
TypeScript
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
import style9 from 'style9';
|
|
|
|
import { MuiClickAwayListener } from '@toeverything/components/ui';
|
|
import { Virgo, HookType, PluginHooks } from '@toeverything/framework/virgo';
|
|
import { Point } from '@toeverything/utils';
|
|
|
|
import { ReferenceMenuContainer } from './Container';
|
|
import { QueryBlocks, QueryResult } from '../../search';
|
|
|
|
export type ReferenceMenuProps = {
|
|
editor: Virgo;
|
|
hooks: PluginHooks;
|
|
style?: { left: number; top: number };
|
|
};
|
|
|
|
export type RefLinkComponent = {
|
|
type: 'reflink';
|
|
reference: string;
|
|
};
|
|
|
|
const BEFORE_REGEX = /\[\[(.*)$/;
|
|
|
|
export const ReferenceMenu = ({ editor, hooks, style }: ReferenceMenuProps) => {
|
|
const [is_show, set_is_show] = useState(false);
|
|
const [block_id, set_block_id] = useState<string>();
|
|
const [position, set_position] = useState<Point>(new Point(0, 0));
|
|
|
|
const [search_text, set_search_text] = useState<string>('');
|
|
const [search_blocks, set_search_blocks] = useState<QueryResult>([]);
|
|
|
|
useEffect(() => {
|
|
QueryBlocks(editor, search_text, result => set_search_blocks(result));
|
|
}, [editor, search_text]);
|
|
|
|
const search_block_ids = useMemo(
|
|
() => Object.values(search_blocks).map(({ id }) => id),
|
|
[search_blocks]
|
|
);
|
|
|
|
const handle_search = useCallback(
|
|
async (event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
const { type, anchorNode } = editor.selection.currentSelectInfo;
|
|
if (
|
|
type === 'Range' &&
|
|
anchorNode &&
|
|
editor.blockHelper.isSelectionCollapsed(anchorNode.id)
|
|
) {
|
|
const text = editor.blockHelper.getBlockTextBeforeSelection(
|
|
anchorNode.id
|
|
);
|
|
const matched = BEFORE_REGEX.exec(text)?.[1];
|
|
|
|
if (typeof matched === 'string') {
|
|
if (event.key === '[') set_is_show(true);
|
|
|
|
set_block_id(anchorNode.id);
|
|
set_search_text(matched);
|
|
|
|
const rect =
|
|
editor.selection.currentSelectInfo?.browserSelection
|
|
?.getRangeAt(0)
|
|
?.getBoundingClientRect();
|
|
if (rect) {
|
|
set_position(new Point(rect.left, rect.top + 24));
|
|
}
|
|
} else if (is_show) {
|
|
set_is_show(false);
|
|
}
|
|
}
|
|
},
|
|
[editor, is_show]
|
|
);
|
|
|
|
const handle_keyup = useCallback(
|
|
(event: React.KeyboardEvent<HTMLDivElement>) => handle_search(event),
|
|
[handle_search]
|
|
);
|
|
|
|
const handle_key_down = useCallback(
|
|
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
if (event.code === 'Escape') {
|
|
set_is_show(false);
|
|
}
|
|
},
|
|
[]
|
|
);
|
|
|
|
useEffect(() => {
|
|
const sub = hooks
|
|
.get(HookType.ON_ROOT_NODE_KEYUP)
|
|
.subscribe(handle_keyup);
|
|
sub.add(
|
|
hooks
|
|
.get(HookType.ON_ROOT_NODE_KEYDOWN_CAPTURE)
|
|
.subscribe(handle_key_down)
|
|
);
|
|
|
|
return () => {
|
|
sub.unsubscribe();
|
|
};
|
|
}, [handle_keyup, handle_key_down, hooks]);
|
|
|
|
const handle_selected = async (reference: string) => {
|
|
if (block_id) {
|
|
const { anchorNode } = editor.selection.currentSelectInfo;
|
|
editor.blockHelper.insertReference(
|
|
reference,
|
|
anchorNode.id,
|
|
editor.selection.currentSelectInfo?.browserSelection,
|
|
-search_text.length - 2
|
|
);
|
|
}
|
|
|
|
set_is_show(false);
|
|
};
|
|
|
|
const handle_close = () => {
|
|
block_id && editor.blockHelper.removeSearchSlash(block_id);
|
|
};
|
|
|
|
return (
|
|
<div
|
|
className={styles('referenceMenu')}
|
|
style={{ top: position.y, left: position.x }}
|
|
onKeyUp={handle_keyup}
|
|
>
|
|
<MuiClickAwayListener onClickAway={() => set_is_show(false)}>
|
|
<div>
|
|
<ReferenceMenuContainer
|
|
editor={editor}
|
|
hooks={hooks}
|
|
style={style}
|
|
isShow={is_show && !!search_text}
|
|
blockId={block_id}
|
|
onSelected={handle_selected}
|
|
onClose={handle_close}
|
|
searchBlocks={search_blocks}
|
|
types={search_block_ids}
|
|
/>
|
|
</div>
|
|
</MuiClickAwayListener>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const styles = style9.create({
|
|
referenceMenu: {
|
|
position: 'absolute',
|
|
zIndex: 1,
|
|
},
|
|
});
|