mirror of
https://github.com/docmost/docmost.git
synced 2026-08-30 10:26:25 +08:00
jump to search init
This commit is contained in:
+3
-1
@@ -181,7 +181,9 @@ function SearchAndReplaceDialog({ editor, editable = true }: PageFindDialogDialo
|
||||
|
||||
const location = useLocation();
|
||||
useEffect(() => {
|
||||
closeDialog();
|
||||
if(pageFindState.isOpen){
|
||||
closeDialog();
|
||||
}
|
||||
}, [location]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -65,7 +65,7 @@ import { useDebouncedCallback, useDocumentVisibility } from "@mantine/hooks";
|
||||
import { useIdle } from "@/hooks/use-idle.ts";
|
||||
import { queryClient } from "@/main.tsx";
|
||||
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 { FIVE_MINUTES } from "@/lib/constants.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 documentState = useDocumentVisibility();
|
||||
const { pageSlug } = useParams();
|
||||
const [searchParams] = useSearchParams();
|
||||
const q = searchParams.get("q");
|
||||
const slugId = extractPageSlugId(pageSlug);
|
||||
const currentPageEditMode = useAtomValue(currentPageEditModeAtom);
|
||||
const canScroll = useCallback(
|
||||
@@ -415,6 +417,37 @@ function CollabPageEditor({
|
||||
const hasConnectedOnceRef = useRef(false);
|
||||
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(() => {
|
||||
if (
|
||||
!hasConnectedOnceRef.current &&
|
||||
|
||||
@@ -30,6 +30,7 @@ export const buildPageUrl = (
|
||||
pageSlugId: string,
|
||||
pageTitle?: string,
|
||||
anchorId?: string,
|
||||
searchString?: string
|
||||
): string => {
|
||||
let url: string;
|
||||
if (spaceName === undefined) {
|
||||
@@ -37,6 +38,11 @@ export const buildPageUrl = (
|
||||
} else {
|
||||
url = `/s/${spaceName}/p/${buildPageSlug(pageSlugId, pageTitle)}`;
|
||||
}
|
||||
|
||||
if (searchString) {
|
||||
url += `?q=${encodeURIComponent(searchString)}`;
|
||||
}
|
||||
|
||||
return anchorId ? `${url}#${anchorId}` : url;
|
||||
};
|
||||
|
||||
@@ -45,13 +51,19 @@ export const buildSharedPageUrl = (opts: {
|
||||
pageSlugId: string;
|
||||
pageTitle?: string;
|
||||
anchorId?: string;
|
||||
searchString?: string
|
||||
}): string => {
|
||||
const { shareId, pageSlugId, pageTitle, anchorId } = opts;
|
||||
const { shareId, pageSlugId, pageTitle, anchorId, searchString } = opts;
|
||||
let url: string;
|
||||
if (!shareId) {
|
||||
url = `/share/p/${buildPageSlug(pageSlugId, pageTitle)}`;
|
||||
} else {
|
||||
url = `/share/${shareId}/p/${buildPageSlug(pageSlugId, pageTitle)}`;
|
||||
}
|
||||
|
||||
if (searchString) {
|
||||
url += `?q=${encodeURIComponent(searchString)}`;
|
||||
}
|
||||
|
||||
return anchorId ? `${url}#${anchorId}` : url;
|
||||
};
|
||||
|
||||
@@ -101,6 +101,8 @@ export function SearchResultItem({
|
||||
pageResult.space.slug,
|
||||
pageResult.slugId,
|
||||
pageResult.title,
|
||||
undefined,
|
||||
pageResult.matchedText
|
||||
)}
|
||||
style={{ userSelect: "none" }}
|
||||
>
|
||||
|
||||
@@ -14,6 +14,7 @@ export interface IPageSearch {
|
||||
updatedAt: Date;
|
||||
rank: string;
|
||||
highlight: string;
|
||||
matchedText: string;
|
||||
space: Partial<ISpace>;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ export class SearchResponseDto {
|
||||
creatorId: string;
|
||||
rank: number;
|
||||
highlight: string;
|
||||
matchedText?: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
space: Partial<Space>;
|
||||
|
||||
@@ -144,6 +144,11 @@ export class SearchService {
|
||||
result.highlight = result.highlight
|
||||
.replace(/\r\n|\r|\n/g, ' ')
|
||||
.replace(/\s+/g, ' ');
|
||||
|
||||
const matchedText = result.highlight.match(/<b>([^<]*)<\/b>/i);
|
||||
result.matchedText = matchedText?.[1] ?? null;
|
||||
} else {
|
||||
result.matchedText = null;
|
||||
}
|
||||
return result;
|
||||
});
|
||||
|
||||
@@ -40,6 +40,10 @@ declare module "@tiptap/core" {
|
||||
* @description Set search term in extension.
|
||||
*/
|
||||
setSearchTerm: (searchTerm: string) => ReturnType;
|
||||
/**
|
||||
* @description Set whole word search in extension.
|
||||
*/
|
||||
setWholeWord: (wholeWord: boolean) => ReturnType;
|
||||
/**
|
||||
* @description Set replace term in extension.
|
||||
*/
|
||||
@@ -85,10 +89,19 @@ const getRegex = (
|
||||
s: string,
|
||||
disableRegex: boolean,
|
||||
caseSensitive: boolean,
|
||||
wholeWord: boolean,
|
||||
): RegExp => {
|
||||
return RegExp(
|
||||
disableRegex ? s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") : s,
|
||||
caseSensitive ? "gu" : "gui",
|
||||
const pattern = disableRegex
|
||||
? s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
||||
: 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;
|
||||
caseSensitive: boolean;
|
||||
lastCaseSensitive: boolean;
|
||||
wholeWord: boolean;
|
||||
lastWholeWord: boolean;
|
||||
resultIndex: number;
|
||||
lastResultIndex: number;
|
||||
}
|
||||
@@ -285,6 +300,8 @@ export const SearchAndReplace = Extension.create<
|
||||
results: [],
|
||||
lastSearchTerm: "",
|
||||
caseSensitive: false,
|
||||
wholeWord: false,
|
||||
lastWholeWord: false,
|
||||
lastCaseSensitive: false,
|
||||
resultIndex: 0,
|
||||
lastResultIndex: 0,
|
||||
@@ -298,6 +315,17 @@ export const SearchAndReplace = Extension.create<
|
||||
({ editor }) => {
|
||||
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;
|
||||
},
|
||||
setReplaceTerm:
|
||||
@@ -411,6 +439,8 @@ export const SearchAndReplace = Extension.create<
|
||||
|
||||
const setLastSearchTerm = (t: string) =>
|
||||
(editor.storage.searchAndReplace.lastSearchTerm = t);
|
||||
const setLastWholeWord = (t: boolean) =>
|
||||
(editor.storage.searchAndReplace.lastWholeWord = t);
|
||||
const setLastCaseSensitive = (t: boolean) =>
|
||||
(editor.storage.searchAndReplace.lastCaseSensitive = t);
|
||||
const setLastResultIndex = (t: number) =>
|
||||
@@ -429,6 +459,8 @@ export const SearchAndReplace = Extension.create<
|
||||
lastSearchTerm,
|
||||
caseSensitive,
|
||||
lastCaseSensitive,
|
||||
wholeWord,
|
||||
lastWholeWord,
|
||||
resultIndex,
|
||||
lastResultIndex,
|
||||
} = storage;
|
||||
@@ -437,12 +469,14 @@ export const SearchAndReplace = Extension.create<
|
||||
!docChanged &&
|
||||
lastSearchTerm === searchTerm &&
|
||||
lastCaseSensitive === caseSensitive &&
|
||||
lastWholeWord === wholeWord &&
|
||||
lastResultIndex === resultIndex
|
||||
)
|
||||
return oldState;
|
||||
|
||||
setLastSearchTerm(searchTerm);
|
||||
setLastCaseSensitive(caseSensitive);
|
||||
setLastWholeWord(wholeWord);
|
||||
setLastResultIndex(resultIndex);
|
||||
|
||||
if (!searchTerm) {
|
||||
@@ -451,11 +485,11 @@ export const SearchAndReplace = Extension.create<
|
||||
}
|
||||
|
||||
const { decorationsToReturn, results } = processSearches(
|
||||
doc,
|
||||
getRegex(searchTerm, disableRegex, caseSensitive),
|
||||
searchResultClass,
|
||||
resultIndex,
|
||||
);
|
||||
doc,
|
||||
getRegex(searchTerm, disableRegex, caseSensitive, wholeWord),
|
||||
searchResultClass,
|
||||
resultIndex,
|
||||
);
|
||||
|
||||
editor.storage.searchAndReplace.results = results;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user