fix: clean up jump search state on close

This commit is contained in:
Philipinho
2026-09-05 16:00:04 +01:00
parent 0e5f2972a2
commit e664f6521a
5 changed files with 109 additions and 43 deletions
@@ -181,10 +181,10 @@ function SearchAndReplaceDialog({ editor, editable = true }: PageFindDialogDialo
const location = useLocation(); const location = useLocation();
useEffect(() => { useEffect(() => {
if(pageFindState.isOpen){ if (pageFindState.isOpen) {
closeDialog(); closeDialog();
} }
}, [location]); }, [location.pathname]);
return ( return (
<Dialog <Dialog
@@ -1,10 +1,15 @@
import { ActionIcon, Dialog, Flex, Text, Tooltip } from "@mantine/core"; import { ActionIcon, Dialog, Flex, Text, Tooltip } from "@mantine/core";
import { IconArrowNarrowDown, IconArrowNarrowUp, IconX } from "@tabler/icons-react"; import {
import { useEditor } from "@tiptap/react"; IconArrowNarrowDown,
IconArrowNarrowUp,
IconX,
} from "@tabler/icons-react";
import { useEditor } from "@tiptap/react";
import { isEditorReady } from "@docmost/editor-ext"; import { isEditorReady } from "@docmost/editor-ext";
import React, { useEffect, useState } from "react"; import React, { useCallback, useEffect, useRef, useState } from "react";
import classes from "./search-replace.module.css"; import classes from "./search-replace.module.css";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useLocation, useNavigate } from "react-router-dom";
interface SearchNavigationDialogProps { interface SearchNavigationDialogProps {
editor: ReturnType<typeof useEditor>; editor: ReturnType<typeof useEditor>;
@@ -18,8 +23,11 @@ interface SearchNavigationEvent extends CustomEvent {
} }
function SearchNavigationDialog({ editor }: SearchNavigationDialogProps) { function SearchNavigationDialog({ editor }: SearchNavigationDialogProps) {
const {t} = useTranslation() const { t } = useTranslation();
const location = useLocation();
const navigate = useNavigate();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const openRef = useRef(false);
const [resultState, setResultState] = useState({ const [resultState, setResultState] = useState({
resultIndex: 0, resultIndex: 0,
resultsLength: 0, resultsLength: 0,
@@ -56,6 +64,28 @@ function SearchNavigationDialog({ editor }: SearchNavigationDialogProps) {
goToSelection(); 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(() => { useEffect(() => {
const handleOpen = (event: Event) => { const handleOpen = (event: Event) => {
const { searchTerms: terms, wholeWord = true } = ( const { searchTerms: terms, wholeWord = true } = (
@@ -64,12 +94,19 @@ function SearchNavigationDialog({ editor }: SearchNavigationDialogProps) {
if (!terms?.length || !isEditorReady(editor)) return; if (!terms?.length || !isEditorReady(editor)) return;
setOpen(true); openRef.current = false;
editor.commands.setSearchTerms(terms); editor.commands.setSearchTerms(terms);
editor.commands.setWholeWord(wholeWord); editor.commands.setWholeWord(wholeWord);
editor.commands.resetIndex(); editor.commands.resetIndex();
const { results, resultIndex } = editor.storage.searchAndReplace; const { results, resultIndex } = editor.storage.searchAndReplace;
openRef.current = true;
if (results.length === 0) {
close();
return;
}
setOpen(true);
setResultState({ setResultState({
resultIndex, resultIndex,
resultsLength: results.length, resultsLength: results.length,
@@ -79,25 +116,29 @@ function SearchNavigationDialog({ editor }: SearchNavigationDialogProps) {
}; };
const handleClose = () => { const handleClose = () => {
setOpen(false); if (openRef.current) {
close();
}
}; };
document.addEventListener("openSearchNavigationDialog", handleOpen); document.addEventListener("openSearchNavigationDialog", handleOpen);
document.addEventListener("openFindDialogFromEditor", handleClose); document.addEventListener("openFindDialogFromEditor", handleClose);
document.addEventListener("closeFindDialogFromEditor", handleClose);
return () => { return () => {
document.removeEventListener("openSearchNavigationDialog", handleOpen); document.removeEventListener("openSearchNavigationDialog", handleOpen);
document.removeEventListener("openFindDialogFromEditor", handleClose); document.removeEventListener("openFindDialogFromEditor", handleClose);
document.removeEventListener("closeFindDialogFromEditor", handleClose);
}; };
}, [editor]); }, [close, editor]);
useEffect(() => { useEffect(() => {
const handleTransaction = () => { const handleTransaction = () => {
if (!open || editor.isDestroyed) return; if (!openRef.current || editor.isDestroyed) return;
const { results } = editor.storage.searchAndReplace; const { results } = editor.storage.searchAndReplace;
if (results.length === 0) { if (results.length === 0) {
setOpen(false); close();
} }
}; };
@@ -105,12 +146,7 @@ function SearchNavigationDialog({ editor }: SearchNavigationDialogProps) {
return () => { return () => {
editor.off("transaction", handleTransaction); editor.off("transaction", handleTransaction);
}; };
}, [editor, open]); }, [close, editor]);
const close = () => {
editor.commands.setSearchTerms([""]);
setOpen(false);
};
return ( return (
<Dialog <Dialog
@@ -0,0 +1,47 @@
import { useEffect, useRef } from "react";
import type { useEditor } from "@tiptap/react";
interface UseSearchNavigationParamsProps {
editor: ReturnType<typeof useEditor>;
isSynced: boolean;
pageId: string;
searchParams: URLSearchParams;
showStatic: boolean;
}
export function useSearchNavigationParams({
editor,
isSynced,
pageId,
searchParams,
showStatic,
}: UseSearchNavigationParamsProps) {
const appliedSearchKeyRef = useRef<string | null>(null);
const searchKey = `${pageId}:${searchParams.toString()}`;
useEffect(() => {
const searchQueries = searchParams.getAll("q");
if (!searchQueries.length) {
appliedSearchKeyRef.current = null;
return;
}
if (
!editor ||
editor.isDestroyed ||
!editor.view.dom.isConnected ||
appliedSearchKeyRef.current === searchKey
) {
return;
}
const match = searchParams.get("m");
appliedSearchKeyRef.current = searchKey;
document.dispatchEvent(
new CustomEvent("openSearchNavigationDialog", {
detail: { searchTerms: searchQueries, wholeWord: match === "whole" },
}),
);
}, [editor, isSynced, searchKey, searchParams, showStatic]);
}
@@ -62,6 +62,7 @@ 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 SearchNavigationDialog from "@/features/editor/components/search-and-replace/search-navigation-dialog.tsx";
import { useSearchNavigationParams } from "@/features/editor/components/search-and-replace/use-search-navigation-params.ts";
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";
@@ -417,31 +418,13 @@ function CollabPageEditor({
const hasConnectedOnceRef = useRef(false); const hasConnectedOnceRef = useRef(false);
const [showStatic, setShowStatic] = useState(true); const [showStatic, setShowStatic] = useState(true);
const appliedSearchKeyRef = useRef<string | null>(null); useSearchNavigationParams({
const searchKey = `${pageId}:${searchParams.toString()}`; editor,
isSynced,
useEffect(() => { pageId,
if ( searchParams,
!editor || showStatic,
editor.isDestroyed || });
!editor.view.dom.isConnected ||
appliedSearchKeyRef.current === searchKey
) {
return;
}
const searchQueries = searchParams.getAll("q");
const match = searchParams.get("m");
if (!searchQueries.length) return;
appliedSearchKeyRef.current = searchKey;
document.dispatchEvent(
new CustomEvent("openSearchNavigationDialog", {
detail: { searchTerms: searchQueries, wholeWord: match === "whole" },
})
);
}, [editor, isSynced, showStatic, searchKey, searchParams]);
useEffect(() => { useEffect(() => {
if ( if (