feat: Double-link: Search interaction

This commit is contained in:
xiaodong zuo
2022-08-24 15:26:27 +08:00
parent 7d35303395
commit 9169421460
7 changed files with 77 additions and 68 deletions

View File

@@ -1,7 +1,5 @@
import { Route, Routes, useParams } from 'react-router';
import { useUserAndSpaces } from '@toeverything/datasource/state'; import { useUserAndSpaces } from '@toeverything/datasource/state';
import { Route, Routes, useParams } from 'react-router';
import { WorkspaceRootContainer } from './Container'; import { WorkspaceRootContainer } from './Container';
import { Page } from './docs'; import { Page } from './docs';
import { Edgeless } from './Edgeless'; import { Edgeless } from './Edgeless';
@@ -10,7 +8,7 @@ import Labels from './labels';
import Pages from './pages'; import Pages from './pages';
export function WorkspaceContainer() { export function WorkspaceContainer() {
const { workspace_id } = useParams(); const { workspace_id, page_id } = useParams();
const { user, currentSpaceId } = useUserAndSpaces(); const { user, currentSpaceId } = useUserAndSpaces();
if ( if (

View File

@@ -713,7 +713,10 @@ class SlateUtils {
if (!this.editor) { if (!this.editor) {
return undefined; return undefined;
} }
const { selectionEnd } = this.getSelectionStartAndEnd(); const selectionEnd = this.getSelectionStartAndEnd()?.selectionEnd;
if (!selectionEnd) {
return undefined;
}
return this.getStringBetween(this.getStart(), selectionEnd); return this.getStringBetween(this.getStart(), selectionEnd);
} }

View File

@@ -10,8 +10,6 @@
*/ */
import type { PatchNode } from '@toeverything/components/ui'; import type { PatchNode } from '@toeverything/components/ui';
import type { BlockFlavors } from '@toeverything/datasource/db-service'; import type { BlockFlavors } from '@toeverything/datasource/db-service';
import type { IdList, SelectionInfo, SelectionManager } from './selection';
import { Point } from '@toeverything/utils'; import { Point } from '@toeverything/utils';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import type { AsyncBlock } from './block'; import type { AsyncBlock } from './block';
@@ -20,6 +18,7 @@ import type { BlockCommands } from './commands/block-commands';
import type { DragDropManager } from './drag-drop'; import type { DragDropManager } from './drag-drop';
import { MouseManager } from './mouse'; import { MouseManager } from './mouse';
import { ScrollManager } from './scroll'; import { ScrollManager } from './scroll';
import type { IdList, SelectionInfo, SelectionManager } from './selection';
// import { BrowserClipboard } from './clipboard/browser-clipboard'; // import { BrowserClipboard } from './clipboard/browser-clipboard';
@@ -109,6 +108,7 @@ export interface Virgo {
getGroupBlockByPoint: (point: Point) => Promise<AsyncBlock>; getGroupBlockByPoint: (point: Point) => Promise<AsyncBlock>;
isEdgeless: boolean; isEdgeless: boolean;
mouseManager: MouseManager; mouseManager: MouseManager;
getHooks: () => HooksRunner & PluginHooks;
} }
export interface Plugin { export interface Plugin {

View File

@@ -192,9 +192,10 @@ export const DoubleLinkMenu = ({
anchorNode.id !== curBlockId && anchorNode.id !== curBlockId &&
editor.blockHelper.isSelectionCollapsed(anchorNode.id)) editor.blockHelper.isSelectionCollapsed(anchorNode.id))
) { ) {
const text = editor.blockHelper.getBlockTextBeforeSelection( const text =
anchorNode.id editor.blockHelper.getBlockTextBeforeSelection(
); anchorNode.id
) || '';
if (text.endsWith('[[')) { if (text.endsWith('[[')) {
resetState(curBlockId, anchorNode.id); resetState(curBlockId, anchorNode.id);
} }

View File

@@ -5,7 +5,12 @@ import {
styled, styled,
TransitionsModal, TransitionsModal,
} from '@toeverything/components/ui'; } from '@toeverything/components/ui';
import { BlockEditor, Virgo } from '@toeverything/framework/virgo'; import {
BlockEditor,
HookType,
PluginHooks,
Virgo,
} from '@toeverything/framework/virgo';
import { throttle } from '@toeverything/utils'; import { throttle } from '@toeverything/utils';
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router'; import { useNavigate, useParams } from 'react-router';
@@ -31,7 +36,7 @@ const styles = style9.create({
export type QueryResult = Awaited<ReturnType<BlockEditor['search']>>; export type QueryResult = Awaited<ReturnType<BlockEditor['search']>>;
const query_blocks = ( const queryBlocksExec = (
editor: Virgo, editor: Virgo,
search: string, search: string,
callback: (result: QueryResult) => void callback: (result: QueryResult) => void
@@ -39,45 +44,58 @@ const query_blocks = (
(editor as BlockEditor).search(search).then(pages => callback(pages)); (editor as BlockEditor).search(search).then(pages => callback(pages));
}; };
export const QueryBlocks = throttle(query_blocks, 500); export const QueryBlocks = throttle(queryBlocksExec, 500);
type SearchProps = { type SearchProps = {
onClose: () => void;
editor: Virgo; editor: Virgo;
hooks: PluginHooks;
}; };
export const Search = (props: SearchProps) => { export const Search = (props: SearchProps) => {
const { workspace_id } = useParams(); const { workspace_id: workspaceId } = useParams();
const navigate = useNavigate(); const navigate = useNavigate();
const [open, set_open] = useState(true); const [open, setOpen] = useState(false);
const [search, set_search] = useState(''); const [search, setSearch] = useState<string>();
const [result, set_result] = useState<QueryResult>([]); const [result, setResult] = useState<QueryResult>([]);
useEffect(() => { useEffect(() => {
QueryBlocks(props.editor, search, result => { search !== undefined &&
set_result(result); QueryBlocks(props.editor, search, result => {
}); setResult(result);
});
}, [props.editor, search]); }, [props.editor, search]);
const handle_navigate = useCallback( const handleNavigate = useCallback(
(id: string) => navigate(`/${workspace_id}/${id}`), (id: string) => navigate(`/${workspaceId}/${id}`),
[navigate, workspace_id] [navigate, workspaceId]
); );
const handleSearch = useCallback(() => {
setOpen(true);
setSearch('');
}, []);
useEffect(() => {
const sub = props.hooks.get(HookType.ON_SEARCH).subscribe(handleSearch);
return () => {
sub.unsubscribe();
};
}, [props, handleSearch]);
return ( return (
<TransitionsModal <TransitionsModal
open={open} open={open}
onClose={() => { onClose={() => {
set_open(false); setOpen(false);
props.onClose();
}} }}
> >
<Box className={styles('wrapper')}> <Box className={styles('wrapper')}>
<SearchInput <SearchInput
autoFocus autoFocus
value={search} value={search}
onChange={e => set_search(e.target.value)} onChange={e => setSearch(e.target.value)}
/> />
<ResultContainer <ResultContainer
sx={{ maxHeight: `${result.length * 28 + 32 + 20}px` }} sx={{ maxHeight: `${result.length * 28 + 32 + 20}px` }}
@@ -89,10 +107,12 @@ export const Search = (props: SearchProps) => {
<BlockPreview <BlockPreview
className={styles('resultItem')} className={styles('resultItem')}
key={block.id} key={block.id}
block={block} block={{
...block,
content: block.content || 'Untitled',
}}
onClick={() => { onClick={() => {
handle_navigate(block.id); handleNavigate(block.id);
props.onClose();
}} }}
/> />
))} ))}

View File

@@ -1,59 +1,40 @@
import { StrictMode } from 'react'; import { StrictMode } from 'react';
import { HookType } from '@toeverything/framework/virgo';
import { BasePlugin } from '../base-plugin'; import { BasePlugin } from '../base-plugin';
import { PluginRenderRoot } from '../utils'; import { PluginRenderRoot } from '../utils';
import { Search } from './Search'; import { Search } from './Search';
export class FullTextSearchPlugin extends BasePlugin { export class FullTextSearchPlugin extends BasePlugin {
#root?: PluginRenderRoot; private root?: PluginRenderRoot;
public static override get pluginName(): string { public static override get pluginName(): string {
return 'search'; return 'search';
} }
public override init(): void {
this.sub.add(
this.hooks.get(HookType.ON_SEARCH).subscribe(this._handleSearch)
);
}
protected override _onRender(): void { protected override _onRender(): void {
this.#root = new PluginRenderRoot({ this.root = new PluginRenderRoot({
name: FullTextSearchPlugin.pluginName, name: FullTextSearchPlugin.pluginName,
render: this.editor.reactRenderRoot.render, render: this.editor.reactRenderRoot.render,
}); });
this._renderSearch();
} }
private unmount() { private _renderSearch() {
if (this.#root) { if (this.root) {
this.editor.setHotKeysScope(); this.root.mount();
this.#root.unmount(); this.root.render(
// this.#root = undefined;
}
this.sub.unsubscribe();
}
private _handleSearch = () => {
this.editor.setHotKeysScope('search');
this.render_search();
};
private render_search() {
if (this.#root) {
this.#root.mount();
this.#root.render(
<StrictMode> <StrictMode>
<Search <Search editor={this.editor} hooks={this.hooks} />
onClose={() => this.unmount()}
editor={this.editor}
/>
</StrictMode> </StrictMode>
); );
} }
} }
public renderSearch() { public renderSearch() {
this.render_search(); this._renderSearch();
}
public override dispose() {
this.root?.unmount();
super.dispose();
} }
} }

View File

@@ -1,5 +1,3 @@
import { useMemo } from 'react';
import { import {
LogoIcon, LogoIcon,
SearchIcon, SearchIcon,
@@ -8,10 +6,11 @@ import {
} from '@toeverything/components/icons'; } from '@toeverything/components/icons';
import { IconButton, styled } from '@toeverything/components/ui'; import { IconButton, styled } from '@toeverything/components/ui';
import { import {
useCurrentEditors,
useLocalTrigger, useLocalTrigger,
useShowSettingsSidebar, useShowSettingsSidebar,
} from '@toeverything/datasource/state'; } from '@toeverything/datasource/state';
import { useCallback, useMemo } from 'react';
import { EditorBoardSwitcher } from './EditorBoardSwitcher'; import { EditorBoardSwitcher } from './EditorBoardSwitcher';
import { FileSystem, fsApiSupported } from './FileSystem'; import { FileSystem, fsApiSupported } from './FileSystem';
import { CurrentPageTitle } from './Title'; import { CurrentPageTitle } from './Title';
@@ -31,6 +30,14 @@ export const LayoutHeader = () => {
} }
}, [isLocalWorkspace]); }, [isLocalWorkspace]);
const { currentEditors } = useCurrentEditors();
const handleSearch = useCallback(() => {
for (const key in currentEditors || {}) {
currentEditors[key].getHooks().onSearch();
}
}, [currentEditors]);
return ( return (
<StyledContainerForHeaderRoot> <StyledContainerForHeaderRoot>
<StyledHeaderRoot> <StyledHeaderRoot>
@@ -48,8 +55,7 @@ export const LayoutHeader = () => {
<IconButton <IconButton
size="large" size="large"
hoverColor={'transparent'} hoverColor={'transparent'}
disabled={true} onClick={handleSearch}
style={{ cursor: 'not-allowed' }}
> >
<SearchIcon /> <SearchIcon />
</IconButton> </IconButton>