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();
useEffect(() => {
if(pageFindState.isOpen){
if (pageFindState.isOpen) {
closeDialog();
}
}, [location]);
}, [location.pathname]);
return (
<Dialog
@@ -1,10 +1,15 @@
import { ActionIcon, Dialog, Flex, Text, Tooltip } from "@mantine/core";
import { IconArrowNarrowDown, IconArrowNarrowUp, IconX } from "@tabler/icons-react";
import { useEditor } from "@tiptap/react";
import {
IconArrowNarrowDown,
IconArrowNarrowUp,
IconX,
} from "@tabler/icons-react";
import { useEditor } from "@tiptap/react";
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 { useTranslation } from "react-i18next";
import { useLocation, useNavigate } from "react-router-dom";
interface SearchNavigationDialogProps {
editor: ReturnType<typeof useEditor>;
@@ -18,8 +23,11 @@ interface SearchNavigationEvent extends CustomEvent {
}
function SearchNavigationDialog({ editor }: SearchNavigationDialogProps) {
const {t} = useTranslation()
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,
@@ -56,6 +64,28 @@ function SearchNavigationDialog({ editor }: SearchNavigationDialogProps) {
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 } = (
@@ -64,12 +94,19 @@ function SearchNavigationDialog({ editor }: SearchNavigationDialogProps) {
if (!terms?.length || !isEditorReady(editor)) return;
setOpen(true);
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,
@@ -79,25 +116,29 @@ function SearchNavigationDialog({ editor }: SearchNavigationDialogProps) {
};
const handleClose = () => {
setOpen(false);
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);
};
}, [editor]);
}, [close, editor]);
useEffect(() => {
const handleTransaction = () => {
if (!open || editor.isDestroyed) return;
if (!openRef.current || editor.isDestroyed) return;
const { results } = editor.storage.searchAndReplace;
if (results.length === 0) {
setOpen(false);
close();
}
};
@@ -105,12 +146,7 @@ function SearchNavigationDialog({ editor }: SearchNavigationDialogProps) {
return () => {
editor.off("transaction", handleTransaction);
};
}, [editor, open]);
const close = () => {
editor.commands.setSearchTerms([""]);
setOpen(false);
};
}, [close, editor]);
return (
<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 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 { useSearchNavigationParams } from "@/features/editor/components/search-and-replace/use-search-navigation-params.ts";
import { useDebouncedCallback, useDocumentVisibility } from "@mantine/hooks";
import { useIdle } from "@/hooks/use-idle.ts";
import { queryClient } from "@/main.tsx";
@@ -417,31 +418,13 @@ function CollabPageEditor({
const hasConnectedOnceRef = useRef(false);
const [showStatic, setShowStatic] = useState(true);
const appliedSearchKeyRef = useRef<string | null>(null);
const searchKey = `${pageId}:${searchParams.toString()}`;
useEffect(() => {
if (
!editor ||
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]);
useSearchNavigationParams({
editor,
isSynced,
pageId,
searchParams,
showStatic,
});
useEffect(() => {
if (