jump to search init

This commit is contained in:
Salihu
2026-08-28 23:05:14 +01:00
parent cd9c166927
commit 70c8c6cfda
8 changed files with 101 additions and 11 deletions
@@ -181,7 +181,9 @@ function SearchAndReplaceDialog({ editor, editable = true }: PageFindDialogDialo
const location = useLocation(); const location = useLocation();
useEffect(() => { useEffect(() => {
closeDialog(); if(pageFindState.isOpen){
closeDialog();
}
}, [location]); }, [location]);
return ( return (
@@ -65,7 +65,7 @@ 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";
import { IPage } from "@/features/page/types/page.types.ts"; import { IPage } from "@/features/page/types/page.types.ts";
import { useParams } from "react-router-dom"; import { useParams, useSearchParams } from "react-router-dom";
import { extractPageSlugId, platformModifierKey } from "@/lib"; import { extractPageSlugId, platformModifierKey } from "@/lib";
import { FIVE_MINUTES } from "@/lib/constants.ts"; import { FIVE_MINUTES } from "@/lib/constants.ts";
import { PageEditMode } from "@/features/user/types/user.types.ts"; import { PageEditMode } from "@/features/user/types/user.types.ts";
@@ -194,6 +194,8 @@ function CollabPageEditor({
const { isIdle, resetIdle } = useIdle(FIVE_MINUTES, { initialState: false }); const { isIdle, resetIdle } = useIdle(FIVE_MINUTES, { initialState: false });
const documentState = useDocumentVisibility(); const documentState = useDocumentVisibility();
const { pageSlug } = useParams(); const { pageSlug } = useParams();
const [searchParams] = useSearchParams();
const q = searchParams.get("q");
const slugId = extractPageSlugId(pageSlug); const slugId = extractPageSlugId(pageSlug);
const currentPageEditMode = useAtomValue(currentPageEditModeAtom); const currentPageEditMode = useAtomValue(currentPageEditModeAtom);
const canScroll = useCallback( const canScroll = useCallback(
@@ -415,6 +417,37 @@ function CollabPageEditor({
const hasConnectedOnceRef = useRef(false); const hasConnectedOnceRef = useRef(false);
const [showStatic, setShowStatic] = useState(true); const [showStatic, setShowStatic] = useState(true);
const appliedSearchRef = useRef(false);
useEffect(() => {
if (!editor || editor.isDestroyed) return;
if (showStatic || !isSynced) return;
if (!q?.trim() || appliedSearchRef.current) return;
const frame = requestAnimationFrame(() => {
if (!editor || editor.isDestroyed || !editor.view.dom.isConnected) return;
editor.commands.setSearchTerm(q);
editor.commands.resetIndex();
const { results, resultIndex } = editor.storage.searchAndReplace;
const position = results[resultIndex];
if (!position) return;
appliedSearchRef.current = true;
editor.commands.setSearchTerm(q)
editor.commands.setWholeWord(true);
const element = document.querySelector(".search-result-current");
element?.scrollIntoView({ behavior: "smooth", block: "center" });
editor.commands.setTextSelection(0)
});
return () => cancelAnimationFrame(frame);
}, [editor, isSynced, showStatic, q]);
useEffect(() => { useEffect(() => {
if ( if (
!hasConnectedOnceRef.current && !hasConnectedOnceRef.current &&
+13 -1
View File
@@ -30,6 +30,7 @@ export const buildPageUrl = (
pageSlugId: string, pageSlugId: string,
pageTitle?: string, pageTitle?: string,
anchorId?: string, anchorId?: string,
searchString?: string
): string => { ): string => {
let url: string; let url: string;
if (spaceName === undefined) { if (spaceName === undefined) {
@@ -37,6 +38,11 @@ export const buildPageUrl = (
} else { } else {
url = `/s/${spaceName}/p/${buildPageSlug(pageSlugId, pageTitle)}`; url = `/s/${spaceName}/p/${buildPageSlug(pageSlugId, pageTitle)}`;
} }
if (searchString) {
url += `?q=${encodeURIComponent(searchString)}`;
}
return anchorId ? `${url}#${anchorId}` : url; return anchorId ? `${url}#${anchorId}` : url;
}; };
@@ -45,13 +51,19 @@ export const buildSharedPageUrl = (opts: {
pageSlugId: string; pageSlugId: string;
pageTitle?: string; pageTitle?: string;
anchorId?: string; anchorId?: string;
searchString?: string
}): string => { }): string => {
const { shareId, pageSlugId, pageTitle, anchorId } = opts; const { shareId, pageSlugId, pageTitle, anchorId, searchString } = opts;
let url: string; let url: string;
if (!shareId) { if (!shareId) {
url = `/share/p/${buildPageSlug(pageSlugId, pageTitle)}`; url = `/share/p/${buildPageSlug(pageSlugId, pageTitle)}`;
} else { } else {
url = `/share/${shareId}/p/${buildPageSlug(pageSlugId, pageTitle)}`; url = `/share/${shareId}/p/${buildPageSlug(pageSlugId, pageTitle)}`;
} }
if (searchString) {
url += `?q=${encodeURIComponent(searchString)}`;
}
return anchorId ? `${url}#${anchorId}` : url; return anchorId ? `${url}#${anchorId}` : url;
}; };
@@ -101,6 +101,8 @@ export function SearchResultItem({
pageResult.space.slug, pageResult.space.slug,
pageResult.slugId, pageResult.slugId,
pageResult.title, pageResult.title,
undefined,
pageResult.matchedText
)} )}
style={{ userSelect: "none" }} style={{ userSelect: "none" }}
> >
@@ -14,6 +14,7 @@ export interface IPageSearch {
updatedAt: Date; updatedAt: Date;
rank: string; rank: string;
highlight: string; highlight: string;
matchedText: string;
space: Partial<ISpace>; space: Partial<ISpace>;
} }
@@ -8,6 +8,7 @@ export class SearchResponseDto {
creatorId: string; creatorId: string;
rank: number; rank: number;
highlight: string; highlight: string;
matchedText?: string;
createdAt: Date; createdAt: Date;
updatedAt: Date; updatedAt: Date;
space: Partial<Space>; space: Partial<Space>;
@@ -144,6 +144,11 @@ export class SearchService {
result.highlight = result.highlight result.highlight = result.highlight
.replace(/\r\n|\r|\n/g, ' ') .replace(/\r\n|\r|\n/g, ' ')
.replace(/\s+/g, ' '); .replace(/\s+/g, ' ');
const matchedText = result.highlight.match(/<b>([^<]*)<\/b>/i);
result.matchedText = matchedText?.[1] ?? null;
} else {
result.matchedText = null;
} }
return result; return result;
}); });
@@ -40,6 +40,10 @@ declare module "@tiptap/core" {
* @description Set search term in extension. * @description Set search term in extension.
*/ */
setSearchTerm: (searchTerm: string) => ReturnType; setSearchTerm: (searchTerm: string) => ReturnType;
/**
* @description Set whole word search in extension.
*/
setWholeWord: (wholeWord: boolean) => ReturnType;
/** /**
* @description Set replace term in extension. * @description Set replace term in extension.
*/ */
@@ -85,10 +89,19 @@ const getRegex = (
s: string, s: string,
disableRegex: boolean, disableRegex: boolean,
caseSensitive: boolean, caseSensitive: boolean,
wholeWord: boolean,
): RegExp => { ): RegExp => {
return RegExp( const pattern = disableRegex
disableRegex ? s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") : s, ? s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
caseSensitive ? "gu" : "gui", : s;
const finalPattern = wholeWord
? `(?<![\\p{L}\\p{N}_])(?:${pattern})(?![\\p{L}\\p{N}_])`
: pattern;
return new RegExp(
finalPattern,
caseSensitive ? "gu" : "giu",
); );
}; };
@@ -261,6 +274,8 @@ export interface SearchAndReplaceStorage {
lastSearchTerm: string; lastSearchTerm: string;
caseSensitive: boolean; caseSensitive: boolean;
lastCaseSensitive: boolean; lastCaseSensitive: boolean;
wholeWord: boolean;
lastWholeWord: boolean;
resultIndex: number; resultIndex: number;
lastResultIndex: number; lastResultIndex: number;
} }
@@ -285,6 +300,8 @@ export const SearchAndReplace = Extension.create<
results: [], results: [],
lastSearchTerm: "", lastSearchTerm: "",
caseSensitive: false, caseSensitive: false,
wholeWord: false,
lastWholeWord: false,
lastCaseSensitive: false, lastCaseSensitive: false,
resultIndex: 0, resultIndex: 0,
lastResultIndex: 0, lastResultIndex: 0,
@@ -298,6 +315,17 @@ export const SearchAndReplace = Extension.create<
({ editor }) => { ({ editor }) => {
editor.storage.searchAndReplace.searchTerm = searchTerm; editor.storage.searchAndReplace.searchTerm = searchTerm;
// clear whole word by default
// should remove if whole word toggle is added to search and replace dialog
editor.storage.searchAndReplace.wholeWord = false;
return false;
},
setWholeWord:
(wholeWord: boolean) =>
({ editor }) => {
editor.storage.searchAndReplace.wholeWord = wholeWord;
return false; return false;
}, },
setReplaceTerm: setReplaceTerm:
@@ -411,6 +439,8 @@ export const SearchAndReplace = Extension.create<
const setLastSearchTerm = (t: string) => const setLastSearchTerm = (t: string) =>
(editor.storage.searchAndReplace.lastSearchTerm = t); (editor.storage.searchAndReplace.lastSearchTerm = t);
const setLastWholeWord = (t: boolean) =>
(editor.storage.searchAndReplace.lastWholeWord = t);
const setLastCaseSensitive = (t: boolean) => const setLastCaseSensitive = (t: boolean) =>
(editor.storage.searchAndReplace.lastCaseSensitive = t); (editor.storage.searchAndReplace.lastCaseSensitive = t);
const setLastResultIndex = (t: number) => const setLastResultIndex = (t: number) =>
@@ -429,6 +459,8 @@ export const SearchAndReplace = Extension.create<
lastSearchTerm, lastSearchTerm,
caseSensitive, caseSensitive,
lastCaseSensitive, lastCaseSensitive,
wholeWord,
lastWholeWord,
resultIndex, resultIndex,
lastResultIndex, lastResultIndex,
} = storage; } = storage;
@@ -437,12 +469,14 @@ export const SearchAndReplace = Extension.create<
!docChanged && !docChanged &&
lastSearchTerm === searchTerm && lastSearchTerm === searchTerm &&
lastCaseSensitive === caseSensitive && lastCaseSensitive === caseSensitive &&
lastWholeWord === wholeWord &&
lastResultIndex === resultIndex lastResultIndex === resultIndex
) )
return oldState; return oldState;
setLastSearchTerm(searchTerm); setLastSearchTerm(searchTerm);
setLastCaseSensitive(caseSensitive); setLastCaseSensitive(caseSensitive);
setLastWholeWord(wholeWord);
setLastResultIndex(resultIndex); setLastResultIndex(resultIndex);
if (!searchTerm) { if (!searchTerm) {
@@ -451,11 +485,11 @@ export const SearchAndReplace = Extension.create<
} }
const { decorationsToReturn, results } = processSearches( const { decorationsToReturn, results } = processSearches(
doc, doc,
getRegex(searchTerm, disableRegex, caseSensitive), getRegex(searchTerm, disableRegex, caseSensitive, wholeWord),
searchResultClass, searchResultClass,
resultIndex, resultIndex,
); );
editor.storage.searchAndReplace.results = results; editor.storage.searchAndReplace.results = results;