mirror of
https://github.com/docmost/docmost.git
synced 2026-09-01 03:15:32 +08:00
support multiple text matching
This commit is contained in:
+2
-2
@@ -66,7 +66,7 @@ function SearchAndReplaceDialog({ editor, editable = true }: PageFindDialogDialo
|
|||||||
}
|
}
|
||||||
// Clear search term in editor
|
// Clear search term in editor
|
||||||
if (isEditorReady(editor)) {
|
if (isEditorReady(editor)) {
|
||||||
editor.commands.setSearchTerm("");
|
editor.commands.setSearchTerms([""]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -117,7 +117,7 @@ function SearchAndReplaceDialog({ editor, editable = true }: PageFindDialogDialo
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isEditorReady(editor)) return;
|
if (!isEditorReady(editor)) return;
|
||||||
editor.commands.setSearchTerm(searchText);
|
editor.commands.setSearchTerms([searchText]);
|
||||||
editor.commands.resetIndex();
|
editor.commands.resetIndex();
|
||||||
editor.commands.selectCurrentItem();
|
editor.commands.selectCurrentItem();
|
||||||
}, [searchText]);
|
}, [searchText]);
|
||||||
|
|||||||
@@ -195,7 +195,7 @@ function CollabPageEditor({
|
|||||||
const documentState = useDocumentVisibility();
|
const documentState = useDocumentVisibility();
|
||||||
const { pageSlug } = useParams();
|
const { pageSlug } = useParams();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const q = searchParams.get("q");
|
const searchQueries = searchParams.getAll("q")
|
||||||
const slugId = extractPageSlugId(pageSlug);
|
const slugId = extractPageSlugId(pageSlug);
|
||||||
const currentPageEditMode = useAtomValue(currentPageEditModeAtom);
|
const currentPageEditMode = useAtomValue(currentPageEditModeAtom);
|
||||||
const canScroll = useCallback(
|
const canScroll = useCallback(
|
||||||
@@ -422,12 +422,13 @@ function CollabPageEditor({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!editor || editor.isDestroyed) return;
|
if (!editor || editor.isDestroyed) return;
|
||||||
if (showStatic || !isSynced) return;
|
if (showStatic || !isSynced) return;
|
||||||
if (!q?.trim() || appliedSearchRef.current) return;
|
if (!searchQueries.length || appliedSearchRef.current) return;
|
||||||
|
|
||||||
const frame = requestAnimationFrame(() => {
|
const frame = requestAnimationFrame(() => {
|
||||||
if (!editor || editor.isDestroyed || !editor.view.dom.isConnected) return;
|
if (!editor || editor.isDestroyed || !editor.view.dom.isConnected) return;
|
||||||
|
|
||||||
editor.commands.setSearchTerm(q);
|
editor.commands.setSearchTerms(searchQueries);
|
||||||
|
editor.commands.setWholeWord(true);
|
||||||
editor.commands.resetIndex();
|
editor.commands.resetIndex();
|
||||||
|
|
||||||
const { results, resultIndex } = editor.storage.searchAndReplace;
|
const { results, resultIndex } = editor.storage.searchAndReplace;
|
||||||
@@ -437,8 +438,6 @@ function CollabPageEditor({
|
|||||||
|
|
||||||
appliedSearchRef.current = true;
|
appliedSearchRef.current = true;
|
||||||
|
|
||||||
editor.commands.setSearchTerm(q)
|
|
||||||
editor.commands.setWholeWord(true);
|
|
||||||
const element = document.querySelector(".search-result-current");
|
const element = document.querySelector(".search-result-current");
|
||||||
element?.scrollIntoView({ behavior: "smooth", block: "center" });
|
element?.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||||
editor.commands.setTextSelection(0)
|
editor.commands.setTextSelection(0)
|
||||||
@@ -446,7 +445,7 @@ function CollabPageEditor({
|
|||||||
|
|
||||||
|
|
||||||
return () => cancelAnimationFrame(frame);
|
return () => cancelAnimationFrame(frame);
|
||||||
}, [editor, isSynced, showStatic, q]);
|
}, [editor, isSynced, showStatic, searchQueries]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export const buildPageUrl = (
|
|||||||
pageSlugId: string,
|
pageSlugId: string,
|
||||||
pageTitle?: string,
|
pageTitle?: string,
|
||||||
anchorId?: string,
|
anchorId?: string,
|
||||||
searchString?: string
|
search?: string[],
|
||||||
): string => {
|
): string => {
|
||||||
let url: string;
|
let url: string;
|
||||||
if (spaceName === undefined) {
|
if (spaceName === undefined) {
|
||||||
@@ -39,8 +39,19 @@ export const buildPageUrl = (
|
|||||||
url = `/s/${spaceName}/p/${buildPageSlug(pageSlugId, pageTitle)}`;
|
url = `/s/${spaceName}/p/${buildPageSlug(pageSlugId, pageTitle)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (searchString) {
|
if (search?.length > 0) {
|
||||||
url += `?q=${encodeURIComponent(searchString)}`;
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
|
search
|
||||||
|
.map((term) => term.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.forEach((term) => params.append("q", term));
|
||||||
|
|
||||||
|
const queryString = params.toString();
|
||||||
|
|
||||||
|
if (queryString) {
|
||||||
|
url += `?${queryString}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return anchorId ? `${url}#${anchorId}` : url;
|
return anchorId ? `${url}#${anchorId}` : url;
|
||||||
@@ -51,9 +62,9 @@ export const buildSharedPageUrl = (opts: {
|
|||||||
pageSlugId: string;
|
pageSlugId: string;
|
||||||
pageTitle?: string;
|
pageTitle?: string;
|
||||||
anchorId?: string;
|
anchorId?: string;
|
||||||
searchString?: string
|
search?: string[]
|
||||||
}): string => {
|
}): string => {
|
||||||
const { shareId, pageSlugId, pageTitle, anchorId, searchString } = opts;
|
const { shareId, pageSlugId, pageTitle, anchorId, search } = opts;
|
||||||
let url: string;
|
let url: string;
|
||||||
if (!shareId) {
|
if (!shareId) {
|
||||||
url = `/share/p/${buildPageSlug(pageSlugId, pageTitle)}`;
|
url = `/share/p/${buildPageSlug(pageSlugId, pageTitle)}`;
|
||||||
@@ -61,8 +72,19 @@ export const buildSharedPageUrl = (opts: {
|
|||||||
url = `/share/${shareId}/p/${buildPageSlug(pageSlugId, pageTitle)}`;
|
url = `/share/${shareId}/p/${buildPageSlug(pageSlugId, pageTitle)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (searchString) {
|
if (search?.length > 0) {
|
||||||
url += `?q=${encodeURIComponent(searchString)}`;
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
|
search
|
||||||
|
.map((term) => term.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.forEach((term) => params.append("q", term));
|
||||||
|
|
||||||
|
const queryString = params.toString();
|
||||||
|
|
||||||
|
if (queryString) {
|
||||||
|
url += `?${queryString}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return anchorId ? `${url}#${anchorId}` : url;
|
return anchorId ? `${url}#${anchorId}` : url;
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export interface IPageSearch {
|
|||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
rank: string;
|
rank: string;
|
||||||
highlight: string;
|
highlight: string;
|
||||||
matchedText: string;
|
matchedText: string[];
|
||||||
space: Partial<ISpace>;
|
space: Partial<ISpace>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export class SearchResponseDto {
|
|||||||
creatorId: string;
|
creatorId: string;
|
||||||
rank: number;
|
rank: number;
|
||||||
highlight: string;
|
highlight: string;
|
||||||
matchedText?: string;
|
matchedText?: string[];
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
space: Partial<Space>;
|
space: Partial<Space>;
|
||||||
|
|||||||
@@ -140,16 +140,20 @@ export class SearchService {
|
|||||||
|
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
const searchResults = results.map((result: SearchResponseDto) => {
|
const searchResults = results.map((result: SearchResponseDto) => {
|
||||||
if (result.highlight) {
|
if (!result.highlight) {
|
||||||
result.highlight = result.highlight
|
result.matchedText = [];
|
||||||
.replace(/\r\n|\r|\n/g, ' ')
|
return result;
|
||||||
.replace(/\s+/g, ' ');
|
|
||||||
|
|
||||||
const matchedText = result.highlight.match(/<b>([^<]*)<\/b>/i);
|
|
||||||
result.matchedText = matchedText?.[1] ?? null;
|
|
||||||
} else {
|
|
||||||
result.matchedText = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
result.highlight = result.highlight
|
||||||
|
.replace(/\r\n|\r|\n/g, ' ')
|
||||||
|
.replace(/\s+/g, ' ');
|
||||||
|
|
||||||
|
result.matchedText = Array.from(
|
||||||
|
result.highlight.matchAll(/<b>([^<]*)<\/b>/gi),
|
||||||
|
(match) => match[1],
|
||||||
|
);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ declare module "@tiptap/core" {
|
|||||||
/**
|
/**
|
||||||
* @description Set search term in extension.
|
* @description Set search term in extension.
|
||||||
*/
|
*/
|
||||||
setSearchTerm: (searchTerm: string) => ReturnType;
|
setSearchTerms: (searchTerms: string[]) => ReturnType;
|
||||||
/**
|
/**
|
||||||
* @description Set whole word search in extension.
|
* @description Set whole word search in extension.
|
||||||
*/
|
*/
|
||||||
@@ -86,14 +86,18 @@ interface TextNodesWithPosition {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getRegex = (
|
const getRegex = (
|
||||||
s: string,
|
searchTerms: string[],
|
||||||
disableRegex: boolean,
|
disableRegex: boolean,
|
||||||
caseSensitive: boolean,
|
caseSensitive: boolean,
|
||||||
wholeWord: boolean,
|
wholeWord: boolean,
|
||||||
): RegExp => {
|
): RegExp => {
|
||||||
const pattern = disableRegex
|
const terms = searchTerms.filter(Boolean).sort((a, b) => b.length - a.length);
|
||||||
? s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
||||||
: s;
|
const pattern = terms
|
||||||
|
.map((term) =>
|
||||||
|
disableRegex ? term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") : term,
|
||||||
|
)
|
||||||
|
.join("|");
|
||||||
|
|
||||||
const finalPattern = wholeWord
|
const finalPattern = wholeWord
|
||||||
? `(?<![\\p{L}\\p{N}_])(?:${pattern})(?![\\p{L}\\p{N}_])`
|
? `(?<![\\p{L}\\p{N}_])(?:${pattern})(?![\\p{L}\\p{N}_])`
|
||||||
@@ -268,10 +272,10 @@ export interface SearchAndReplaceOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface SearchAndReplaceStorage {
|
export interface SearchAndReplaceStorage {
|
||||||
searchTerm: string;
|
searchTerms: string[];
|
||||||
replaceTerm: string;
|
replaceTerm: string;
|
||||||
results: Range[];
|
results: Range[];
|
||||||
lastSearchTerm: string;
|
lastSearchTerms: string[];
|
||||||
caseSensitive: boolean;
|
caseSensitive: boolean;
|
||||||
lastCaseSensitive: boolean;
|
lastCaseSensitive: boolean;
|
||||||
wholeWord: boolean;
|
wholeWord: boolean;
|
||||||
@@ -295,10 +299,10 @@ export const SearchAndReplace = Extension.create<
|
|||||||
|
|
||||||
addStorage() {
|
addStorage() {
|
||||||
return {
|
return {
|
||||||
searchTerm: "",
|
searchTerms: [],
|
||||||
replaceTerm: "",
|
replaceTerm: "",
|
||||||
results: [],
|
results: [],
|
||||||
lastSearchTerm: "",
|
lastSearchTerms: [],
|
||||||
caseSensitive: false,
|
caseSensitive: false,
|
||||||
wholeWord: false,
|
wholeWord: false,
|
||||||
lastWholeWord: false,
|
lastWholeWord: false,
|
||||||
@@ -310,10 +314,10 @@ export const SearchAndReplace = Extension.create<
|
|||||||
|
|
||||||
addCommands() {
|
addCommands() {
|
||||||
return {
|
return {
|
||||||
setSearchTerm:
|
setSearchTerms:
|
||||||
(searchTerm: string) =>
|
(searchTerms: string[]) =>
|
||||||
({ editor }) => {
|
({ editor }) => {
|
||||||
editor.storage.searchAndReplace.searchTerm = searchTerm;
|
editor.storage.searchAndReplace.searchTerms = searchTerms.filter(Boolean);
|
||||||
|
|
||||||
// clear whole word by default
|
// clear whole word by default
|
||||||
// should remove if whole word toggle is added to search and replace dialog
|
// should remove if whole word toggle is added to search and replace dialog
|
||||||
@@ -321,7 +325,7 @@ export const SearchAndReplace = Extension.create<
|
|||||||
|
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
setWholeWord:
|
setWholeWord:
|
||||||
(wholeWord: boolean) =>
|
(wholeWord: boolean) =>
|
||||||
({ editor }) => {
|
({ editor }) => {
|
||||||
editor.storage.searchAndReplace.wholeWord = wholeWord;
|
editor.storage.searchAndReplace.wholeWord = wholeWord;
|
||||||
@@ -437,8 +441,8 @@ export const SearchAndReplace = Extension.create<
|
|||||||
const editor = this.editor;
|
const editor = this.editor;
|
||||||
const { searchResultClass, disableRegex } = this.options;
|
const { searchResultClass, disableRegex } = this.options;
|
||||||
|
|
||||||
const setLastSearchTerm = (t: string) =>
|
const setLastSearchTerms = (terms: string[]) =>
|
||||||
(editor.storage.searchAndReplace.lastSearchTerm = t);
|
(editor.storage.searchAndReplace.lastSearchTerms = [...terms]);
|
||||||
const setLastWholeWord = (t: boolean) =>
|
const setLastWholeWord = (t: boolean) =>
|
||||||
(editor.storage.searchAndReplace.lastWholeWord = t);
|
(editor.storage.searchAndReplace.lastWholeWord = t);
|
||||||
const setLastCaseSensitive = (t: boolean) =>
|
const setLastCaseSensitive = (t: boolean) =>
|
||||||
@@ -455,10 +459,10 @@ export const SearchAndReplace = Extension.create<
|
|||||||
const storage = editor.storage.searchAndReplace;
|
const storage = editor.storage.searchAndReplace;
|
||||||
if (!storage) return oldState;
|
if (!storage) return oldState;
|
||||||
const {
|
const {
|
||||||
searchTerm,
|
searchTerms,
|
||||||
lastSearchTerm,
|
|
||||||
caseSensitive,
|
|
||||||
lastCaseSensitive,
|
lastCaseSensitive,
|
||||||
|
lastSearchTerms,
|
||||||
|
caseSensitive,
|
||||||
wholeWord,
|
wholeWord,
|
||||||
lastWholeWord,
|
lastWholeWord,
|
||||||
resultIndex,
|
resultIndex,
|
||||||
@@ -467,26 +471,32 @@ export const SearchAndReplace = Extension.create<
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
!docChanged &&
|
!docChanged &&
|
||||||
lastSearchTerm === searchTerm &&
|
searchTerms.length === lastSearchTerms.length &&
|
||||||
|
searchTerms.every((term, index) => term === lastSearchTerms[index]) &&
|
||||||
lastCaseSensitive === caseSensitive &&
|
lastCaseSensitive === caseSensitive &&
|
||||||
lastWholeWord === wholeWord &&
|
lastWholeWord === wholeWord &&
|
||||||
lastResultIndex === resultIndex
|
lastResultIndex === resultIndex
|
||||||
)
|
)
|
||||||
return oldState;
|
return oldState;
|
||||||
|
|
||||||
setLastSearchTerm(searchTerm);
|
setLastSearchTerms(searchTerms);
|
||||||
setLastCaseSensitive(caseSensitive);
|
setLastCaseSensitive(caseSensitive);
|
||||||
setLastWholeWord(wholeWord);
|
setLastWholeWord(wholeWord);
|
||||||
setLastResultIndex(resultIndex);
|
setLastResultIndex(resultIndex);
|
||||||
|
|
||||||
if (!searchTerm) {
|
if (searchTerms.length === 0) {
|
||||||
editor.storage.searchAndReplace.results = [];
|
editor.storage.searchAndReplace.results = [];
|
||||||
return DecorationSet.empty;
|
return DecorationSet.empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { decorationsToReturn, results } = processSearches(
|
const { decorationsToReturn, results } = processSearches(
|
||||||
doc,
|
doc,
|
||||||
getRegex(searchTerm, disableRegex, caseSensitive, wholeWord),
|
getRegex(
|
||||||
|
searchTerms,
|
||||||
|
disableRegex,
|
||||||
|
caseSensitive,
|
||||||
|
wholeWord,
|
||||||
|
),
|
||||||
searchResultClass,
|
searchResultClass,
|
||||||
resultIndex,
|
resultIndex,
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user