feat: Double-link: Search interaction
This commit is contained in:
@@ -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 (
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user