import { ActionIcon, Dialog, Flex, Text, Tooltip } from "@mantine/core"; import { IconArrowNarrowDown, IconArrowNarrowUp, IconX, } from "@tabler/icons-react"; import { useEditor } from "@tiptap/react"; import { isEditorReady } from "@docmost/editor-ext"; import React, { useCallback, useEffect, useRef, useState } from "react"; import classes from "./search-replace.module.css"; import { useTranslation } from "react-i18next"; import { useLocation, useNavigate } from "react-router-dom"; interface SearchNavigationDialogProps { editor: ReturnType; } interface SearchNavigationEvent extends CustomEvent { detail: { searchTerms: string[]; wholeWord?: boolean; }; } function SearchNavigationDialog({ editor }: SearchNavigationDialogProps) { const { t } = useTranslation(); const location = useLocation(); const navigate = useNavigate(); const [open, setOpen] = useState(false); const openRef = useRef(false); const [resultState, setResultState] = useState({ resultIndex: 0, resultsLength: 0, }); 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(); }; const close = useCallback(() => { if (!openRef.current) return; openRef.current = false; setOpen(false); if (isEditorReady(editor)) { editor.commands.setSearchTerms([""]); } const nextParams = new URLSearchParams(location.search); nextParams.delete("q"); nextParams.delete("m"); const nextSearch = nextParams.toString(); navigate( { pathname: location.pathname, search: nextSearch ? `?${nextSearch}` : "", hash: location.hash, }, { replace: true }, ); }, [editor, location.hash, location.pathname, location.search, navigate]); useEffect(() => { const handleOpen = (event: Event) => { const { searchTerms: terms, wholeWord = true } = ( event as SearchNavigationEvent ).detail; if (!terms?.length || !isEditorReady(editor)) return; openRef.current = false; editor.commands.setSearchTerms(terms); editor.commands.setWholeWord(wholeWord); editor.commands.resetIndex(); const { results, resultIndex } = editor.storage.searchAndReplace; openRef.current = true; if (results.length === 0) { close(); return; } setOpen(true); setResultState({ resultIndex, resultsLength: results.length, }); goToSelection(); }; const handleClose = () => { if (openRef.current) { close(); } }; document.addEventListener("openSearchNavigationDialog", handleOpen); document.addEventListener("openFindDialogFromEditor", handleClose); document.addEventListener("closeFindDialogFromEditor", handleClose); return () => { document.removeEventListener("openSearchNavigationDialog", handleOpen); document.removeEventListener("openFindDialogFromEditor", handleClose); document.removeEventListener("closeFindDialogFromEditor", handleClose); }; }, [close, editor]); useEffect(() => { const handleTransaction = () => { if (!openRef.current || editor.isDestroyed) return; const { results } = editor.storage.searchAndReplace; if (results.length === 0) { close(); } }; editor.on("transaction", handleTransaction); return () => { editor.off("transaction", handleTransaction); }; }, [close, editor]); return ( {resultState.resultsLength > 0 ? `${resultState.resultIndex + 1}/${resultState.resultsLength}` : t("Not found")} ); } export default SearchNavigationDialog;