feat: find and replace in editor (#689)

* feat: page find and replace

* * Refactor search and replace directory

* bugfix scroll

* Fix search and replace functionality for macOS and improve UX

- Fixed cmd+f shortcut to work on macOS (using 'Mod' key instead of 'Control')
- Added search functionality to title editor
- Fixed "Not found" message showing when search term is empty
- Fixed tooltip error when clicking replace button
- Changed replace button from icon to text for consistency
- Reduced width of search input fields for better UI
- Fixed result index after replace operation to prevent out-of-bounds error
- Added missing translation strings for search and replace dialog
- Updated tooltip to show platform-specific shortcuts (⌘F on Mac, Ctrl-F on others)

* Hide replace functionality for users with view-only permissions

- Added editable prop to SearchAndReplaceDialog component
- Pass editable state from PageEditor to SearchAndReplaceDialog
- Conditionally render replace button based on edit permissions
- Hide replace input section for view-only users
- Disable Alt+R shortcut when user lacks edit permissions

* Fix search dialog not closing properly when navigating away

- Clear all state (search text, replace text) when closing dialog
- Reset replace button visibility state on close
- Clear editor search term to remove highlights
- Ensure dialog closes properly when route changes

* fix: preserve text marks (comments, etc.) when replacing text in search and replace

- Collect all marks that span the text being replaced using nodesBetween
- Apply collected marks to the replacement text to maintain formatting
- Fixes issue where comment marks were being removed during text replacement

* ignore type error

---------

Co-authored-by: Philipinho <16838612+Philipinho@users.noreply.github.com>
This commit is contained in:
fuscodev
2025-07-10 05:40:07 +02:00
committed by GitHub
parent f80004817c
commit 29388636bf
14 changed files with 903 additions and 13 deletions

View File

@ -10,8 +10,11 @@ import {
pageEditorAtom,
titleEditorAtom,
} from "@/features/editor/atoms/editor-atoms";
import { updatePageData, useUpdateTitlePageMutation } from "@/features/page/queries/page-query";
import { useDebouncedCallback } from "@mantine/hooks";
import {
updatePageData,
useUpdateTitlePageMutation,
} from "@/features/page/queries/page-query";
import { useDebouncedCallback, getHotkeyHandler } from "@mantine/hooks";
import { useAtom } from "jotai";
import { useQueryEmit } from "@/features/websocket/use-query-emit.ts";
import { History } from "@tiptap/extension-history";
@ -40,7 +43,8 @@ export function TitleEditor({
editable,
}: TitleEditorProps) {
const { t } = useTranslation();
const { mutateAsync: updateTitlePageMutationAsync } = useUpdateTitlePageMutation();
const { mutateAsync: updateTitlePageMutationAsync } =
useUpdateTitlePageMutation();
const pageEditor = useAtomValue(pageEditorAtom);
const [, setTitleEditor] = useAtom(titleEditorAtom);
const emit = useQueryEmit();
@ -108,7 +112,12 @@ export function TitleEditor({
spaceId: page.spaceId,
entity: ["pages"],
id: page.id,
payload: { title: page.title, slugId: page.slugId, parentPageId: page.parentPageId, icon: page.icon },
payload: {
title: page.title,
slugId: page.slugId,
parentPageId: page.parentPageId,
icon: page.icon,
},
};
if (page.title !== titleEditor.getText()) return;
@ -152,13 +161,19 @@ export function TitleEditor({
}
}, [userPageEditMode, titleEditor, editable]);
const openSearchDialog = () => {
const event = new CustomEvent("openFindDialogFromEditor", {});
document.dispatchEvent(event);
};
function handleTitleKeyDown(event: any) {
if (!titleEditor || !pageEditor || event.shiftKey) return;
// Prevent focus shift when IME composition is active
// Prevent focus shift when IME composition is active
// `keyCode === 229` is added to support Safari where `isComposing` may not be reliable
if (event.nativeEvent.isComposing || event.nativeEvent.keyCode === 229) return;
if (event.nativeEvent.isComposing || event.nativeEvent.keyCode === 229)
return;
const { key } = event;
const { $head } = titleEditor.state.selection;
@ -172,5 +187,16 @@ export function TitleEditor({
}
}
return <EditorContent editor={titleEditor} onKeyDown={handleTitleKeyDown} />;
return (
<EditorContent
editor={titleEditor}
onKeyDown={(event) => {
// First handle the search hotkey
getHotkeyHandler([["mod+F", openSearchDialog]])(event);
// Then handle other key events
handleTitleKeyDown(event);
}}
/>
);
}