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;
|
||||
Reference in New Issue
Block a user