mirror of
https://github.com/docmost/docmost.git
synced 2026-09-01 03:15:32 +08:00
search navigation dialog
This commit is contained in:
+182
@@ -0,0 +1,182 @@
|
|||||||
|
import { ActionIcon, Dialog, Flex, Text, Tooltip } from "@mantine/core";
|
||||||
|
import { IconArrowNarrowDown, IconArrowNarrowUp, IconX } from "@tabler/icons-react";
|
||||||
|
import { Editor, useEditor } from "@tiptap/react";
|
||||||
|
import { isEditorReady } from "@docmost/editor-ext";
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import classes from "./search-replace.module.css";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
interface SearchNavigationDialogProps {
|
||||||
|
editor: ReturnType<typeof useEditor>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SearchNavigationEvent extends CustomEvent {
|
||||||
|
detail: {
|
||||||
|
searchTerms: string[];
|
||||||
|
wholeWord?: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function SearchNavigationDialog({ editor }: SearchNavigationDialogProps) {
|
||||||
|
const {t} = useTranslation()
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [searchTerms, setSearchTerms] = useState<string[]>([]);
|
||||||
|
const [resultState, setResultState] = useState({
|
||||||
|
resultIndex: 0,
|
||||||
|
resultsLength: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSetSearchTerms = (editor: Editor, terms: string[]) => {
|
||||||
|
setSearchTerms(terms);
|
||||||
|
if (isEditorReady(editor)) {
|
||||||
|
editor.commands.setSearchTerms(terms);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const goToSelection = () => {
|
||||||
|
if (!isEditorReady(editor)) return;
|
||||||
|
|
||||||
|
const { results, resultIndex } = editor.storage.searchAndReplace;
|
||||||
|
const position = results[resultIndex];
|
||||||
|
|
||||||
|
setResultState({
|
||||||
|
resultsLength: results.length,
|
||||||
|
resultIndex,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!position) return;
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
document
|
||||||
|
.querySelector(".search-result-current")
|
||||||
|
?.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const next = () => {
|
||||||
|
if (!isEditorReady(editor)) return;
|
||||||
|
editor.commands.nextSearchResult();
|
||||||
|
goToSelection();
|
||||||
|
};
|
||||||
|
|
||||||
|
const previous = () => {
|
||||||
|
if (!isEditorReady(editor)) return;
|
||||||
|
editor.commands.previousSearchResult();
|
||||||
|
goToSelection();
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleOpen = (event: Event) => {
|
||||||
|
const { searchTerms: terms, wholeWord = true } = (
|
||||||
|
event as SearchNavigationEvent
|
||||||
|
).detail;
|
||||||
|
|
||||||
|
if (!terms?.length || !isEditorReady(editor)) return;
|
||||||
|
|
||||||
|
setOpen(true);
|
||||||
|
handleSetSearchTerms(editor, terms);
|
||||||
|
editor.commands.setWholeWord(wholeWord);
|
||||||
|
editor.commands.resetIndex();
|
||||||
|
|
||||||
|
const { results, resultIndex } = editor.storage.searchAndReplace;
|
||||||
|
setResultState({
|
||||||
|
resultIndex,
|
||||||
|
resultsLength: results.length,
|
||||||
|
});
|
||||||
|
|
||||||
|
goToSelection();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("openSearchNavigationDialog", handleOpen);
|
||||||
|
document.addEventListener("openFindDialogFromEditor", handleClose);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("openSearchNavigationDialog", handleOpen);
|
||||||
|
document.removeEventListener("openFindDialogFromEditor", handleClose);
|
||||||
|
};
|
||||||
|
}, [editor]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleTransaction = () => {
|
||||||
|
if (!open || editor.isDestroyed) return;
|
||||||
|
|
||||||
|
const { results } = editor.storage.searchAndReplace;
|
||||||
|
if (results.length === 0) {
|
||||||
|
setOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
editor.on("transaction", handleTransaction);
|
||||||
|
return () => {
|
||||||
|
editor.off("transaction", handleTransaction);
|
||||||
|
};
|
||||||
|
}, [editor, open]);
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
handleSetSearchTerms(editor, [""]);
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
className={classes.findDialog}
|
||||||
|
opened={open}
|
||||||
|
size="xs"
|
||||||
|
radius="md"
|
||||||
|
w="auto"
|
||||||
|
position={{ top: 90, right: 50 }}
|
||||||
|
withBorder
|
||||||
|
aria-label="Search navigation"
|
||||||
|
>
|
||||||
|
<Flex align="center" gap="xs">
|
||||||
|
<Text size="xs" style={{ flex: 1 }}>
|
||||||
|
{resultState.resultsLength > 0
|
||||||
|
? `${resultState.resultIndex + 1}/${resultState.resultsLength}`
|
||||||
|
: t("Not found")}
|
||||||
|
</Text>
|
||||||
|
<Tooltip label="Previous match">
|
||||||
|
<ActionIcon
|
||||||
|
variant="subtle"
|
||||||
|
color="gray"
|
||||||
|
onClick={previous}
|
||||||
|
aria-label="Previous match"
|
||||||
|
disabled={resultState.resultsLength === 0}
|
||||||
|
>
|
||||||
|
<IconArrowNarrowUp size={16} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip label="Next match">
|
||||||
|
<ActionIcon
|
||||||
|
variant="subtle"
|
||||||
|
color="gray"
|
||||||
|
onClick={next}
|
||||||
|
aria-label="Next match"
|
||||||
|
disabled={resultState.resultsLength === 0}
|
||||||
|
>
|
||||||
|
<IconArrowNarrowDown size={16} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip label="Close">
|
||||||
|
<ActionIcon
|
||||||
|
variant="subtle"
|
||||||
|
color="gray"
|
||||||
|
onClick={close}
|
||||||
|
aria-label="Close"
|
||||||
|
>
|
||||||
|
<IconX size={16} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
</Flex>
|
||||||
|
{searchTerms.length > 0 && (
|
||||||
|
<Text size="xs" c="dimmed" mt="xs" lineClamp={1}>
|
||||||
|
{searchTerms.join(", ")}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SearchNavigationDialog;
|
||||||
@@ -61,6 +61,7 @@ import ExcalidrawMenu from "./components/excalidraw/excalidraw-menu-lazy";
|
|||||||
import DrawioMenu from "./components/drawio/drawio-menu";
|
import DrawioMenu from "./components/drawio/drawio-menu";
|
||||||
import { useCollabToken } from "@/features/auth/queries/auth-query.tsx";
|
import { useCollabToken } from "@/features/auth/queries/auth-query.tsx";
|
||||||
import SearchAndReplaceDialog from "@/features/editor/components/search-and-replace/search-and-replace-dialog.tsx";
|
import SearchAndReplaceDialog from "@/features/editor/components/search-and-replace/search-and-replace-dialog.tsx";
|
||||||
|
import SearchNavigationDialog from "@/features/editor/components/search-and-replace/search-navigation-dialog.tsx";
|
||||||
import { useDebouncedCallback, useDocumentVisibility } from "@mantine/hooks";
|
import { useDebouncedCallback, useDocumentVisibility } from "@mantine/hooks";
|
||||||
import { useIdle } from "@/hooks/use-idle.ts";
|
import { useIdle } from "@/hooks/use-idle.ts";
|
||||||
import { queryClient } from "@/main.tsx";
|
import { queryClient } from "@/main.tsx";
|
||||||
@@ -195,7 +196,7 @@ function CollabPageEditor({
|
|||||||
const documentState = useDocumentVisibility();
|
const documentState = useDocumentVisibility();
|
||||||
const { pageSlug } = useParams();
|
const { pageSlug } = useParams();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const searchQueries = searchParams.getAll("q")
|
const searchQueries = searchParams.getAll("q");
|
||||||
const slugId = extractPageSlugId(pageSlug);
|
const slugId = extractPageSlugId(pageSlug);
|
||||||
const currentPageEditMode = useAtomValue(currentPageEditModeAtom);
|
const currentPageEditMode = useAtomValue(currentPageEditModeAtom);
|
||||||
const canScroll = useCallback(
|
const canScroll = useCallback(
|
||||||
@@ -423,28 +424,16 @@ function CollabPageEditor({
|
|||||||
if (!editor || editor.isDestroyed) return;
|
if (!editor || editor.isDestroyed) return;
|
||||||
if (showStatic || !isSynced) return;
|
if (showStatic || !isSynced) return;
|
||||||
if (!searchQueries.length || appliedSearchRef.current) return;
|
if (!searchQueries.length || appliedSearchRef.current) return;
|
||||||
|
|
||||||
const frame = requestAnimationFrame(() => {
|
|
||||||
if (!editor || editor.isDestroyed || !editor.view.dom.isConnected) return;
|
if (!editor || editor.isDestroyed || !editor.view.dom.isConnected) return;
|
||||||
|
|
||||||
editor.commands.setSearchTerms(searchQueries);
|
|
||||||
editor.commands.setWholeWord(true);
|
|
||||||
editor.commands.resetIndex();
|
|
||||||
|
|
||||||
const { results, resultIndex } = editor.storage.searchAndReplace;
|
|
||||||
const position = results[resultIndex];
|
|
||||||
|
|
||||||
if (!position) return;
|
|
||||||
|
|
||||||
appliedSearchRef.current = true;
|
appliedSearchRef.current = true;
|
||||||
|
|
||||||
const element = document.querySelector(".search-result-current");
|
document.dispatchEvent(
|
||||||
element?.scrollIntoView({ behavior: "smooth", block: "center" });
|
new CustomEvent("openSearchNavigationDialog", {
|
||||||
editor.commands.setTextSelection(0)
|
detail: { searchTerms: searchQueries, wholeWord: true },
|
||||||
});
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
return () => cancelAnimationFrame(frame);
|
|
||||||
}, [editor, isSynced, showStatic, searchQueries]);
|
}, [editor, isSynced, showStatic, searchQueries]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -470,6 +459,7 @@ function CollabPageEditor({
|
|||||||
{editor && (
|
{editor && (
|
||||||
<SearchAndReplaceDialog editor={editor} editable={editable} />
|
<SearchAndReplaceDialog editor={editor} editable={editable} />
|
||||||
)}
|
)}
|
||||||
|
{editor && <SearchNavigationDialog editor={editor} />}
|
||||||
|
|
||||||
{editor && editorIsEditable && (
|
{editor && editorIsEditable && (
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ export const buildSharedPageUrl = (opts: {
|
|||||||
pageSlugId: string;
|
pageSlugId: string;
|
||||||
pageTitle?: string;
|
pageTitle?: string;
|
||||||
anchorId?: string;
|
anchorId?: string;
|
||||||
search?: string[]
|
search?: string[];
|
||||||
}): string => {
|
}): string => {
|
||||||
const { shareId, pageSlugId, pageTitle, anchorId, search } = opts;
|
const { shareId, pageSlugId, pageTitle, anchorId, search } = opts;
|
||||||
let url: string;
|
let url: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user