mirror of
https://github.com/streetwriters/notesnook.git
synced 2025-12-22 22:49:45 +01:00
web: fix scroll to search result (#8290)
* web: (wip) fix scroll to search result Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com> * web: fix scroll to search result Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
This commit is contained in:
@@ -242,8 +242,7 @@ const MemoizedEditorView = React.memo(EditorView, (prev, next) => {
|
|||||||
prev.session.type === next.session.type &&
|
prev.session.type === next.session.type &&
|
||||||
prev.session.needsHydration === next.session.needsHydration &&
|
prev.session.needsHydration === next.session.needsHydration &&
|
||||||
prev.session.activeBlockId === next.session.activeBlockId &&
|
prev.session.activeBlockId === next.session.activeBlockId &&
|
||||||
prev.session.activeSearchResultIndex ===
|
prev.session.activeSearchResultId === next.session.activeSearchResultId
|
||||||
next.session.activeSearchResultIndex
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
function EditorView({
|
function EditorView({
|
||||||
@@ -855,28 +854,29 @@ function useScrollToBlock(session: EditorSession) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function useScrollToSearchResult(session: EditorSession) {
|
function useScrollToSearchResult(session: EditorSession) {
|
||||||
const index = useEditorStore(
|
const id = useEditorStore(
|
||||||
(store) => store.getSession(session.id)?.activeSearchResultIndex
|
(store) => store.getSession(session.id)?.activeSearchResultId
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (index === undefined) return;
|
if (id === undefined) return;
|
||||||
const scrollContainer = document.getElementById(
|
const scrollContainer = document.getElementById(
|
||||||
`editorScroll_${session.id}`
|
`editorScroll_${session.id}`
|
||||||
);
|
);
|
||||||
scrollContainer?.closest(".active")?.classList.add("searching");
|
scrollContainer?.closest(".active")?.classList.add("searching");
|
||||||
const elements = scrollContainer?.getElementsByTagName("nn-search-result");
|
const element = scrollContainer?.querySelector(`nn-search-result#${id}`);
|
||||||
setTimeout(
|
setTimeout(
|
||||||
() =>
|
() =>
|
||||||
elements
|
element?.scrollIntoView({
|
||||||
?.item(index)
|
block: "center",
|
||||||
?.scrollIntoView({ block: "center", behavior: "instant" }),
|
behavior: "instant"
|
||||||
|
}),
|
||||||
100
|
100
|
||||||
);
|
);
|
||||||
useEditorStore.getState().updateSession(session.id, [session.type], {
|
useEditorStore.getState().updateSession(session.id, [session.type], {
|
||||||
activeSearchResultIndex: undefined
|
activeSearchResultId: undefined
|
||||||
});
|
});
|
||||||
}, [session.id, session.type, index]);
|
}, [session.id, session.type, id]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isFile(e: DragEvent) {
|
function isFile(e: DragEvent) {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
|
|
||||||
import { HighlightedResult } from "@notesnook/core";
|
import { HighlightedResult } from "@notesnook/core";
|
||||||
import { Button, Flex, Text } from "@theme-ui/components";
|
import { Button, Flex, Text } from "@theme-ui/components";
|
||||||
import React, { useState } from "react";
|
import React, { Fragment, useState } from "react";
|
||||||
import { useEditorStore } from "../../stores/editor-store";
|
import { useEditorStore } from "../../stores/editor-store";
|
||||||
import { useStore as useNoteStore } from "../../stores/note-store";
|
import { useStore as useNoteStore } from "../../stores/note-store";
|
||||||
import ListItem from "../list-item";
|
import ListItem from "../list-item";
|
||||||
@@ -112,11 +112,11 @@ function SearchResult(props: SearchResultProps) {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{item.title.map((match) => (
|
{item.title.map((match) => (
|
||||||
<>
|
<Fragment key={match.id}>
|
||||||
<span>{match.prefix}</span>
|
<span>{match.prefix}</span>
|
||||||
<span className="match">{match.match}</span>
|
<span className="match">{match.match}</span>
|
||||||
{match.suffix ? <span>{match.suffix}</span> : null}
|
{match.suffix ? <span>{match.suffix}</span> : null}
|
||||||
</>
|
</Fragment>
|
||||||
))}
|
))}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
@@ -146,14 +146,14 @@ function SearchResult(props: SearchResultProps) {
|
|||||||
useEditorStore.getState().openSession(item.id, {
|
useEditorStore.getState().openSession(item.id, {
|
||||||
rawContent: item.rawContent,
|
rawContent: item.rawContent,
|
||||||
force: true,
|
force: true,
|
||||||
activeSearchResultIndex: findSelectedMatchIndex(item, index)
|
activeSearchResultId: match[0].id
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
onMiddleClick={() => {
|
onMiddleClick={() => {
|
||||||
useEditorStore.getState().openSession(item.id, {
|
useEditorStore.getState().openSession(item.id, {
|
||||||
openInNewTab: true,
|
openInNewTab: true,
|
||||||
rawContent: item.rawContent,
|
rawContent: item.rawContent,
|
||||||
activeSearchResultIndex: findSelectedMatchIndex(item, index)
|
activeSearchResultId: match[0].id
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
title={
|
title={
|
||||||
@@ -171,11 +171,11 @@ function SearchResult(props: SearchResultProps) {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{match.map((match) => (
|
{match.map((match) => (
|
||||||
<>
|
<Fragment key={match.id}>
|
||||||
<span>{match.prefix}</span>
|
<span>{match.prefix}</span>
|
||||||
<span className="match">{match.match}</span>
|
<span className="match">{match.match}</span>
|
||||||
{match.suffix ? <span>{match.suffix}</span> : null}
|
{match.suffix ? <span>{match.suffix}</span> : null}
|
||||||
</>
|
</Fragment>
|
||||||
))}
|
))}
|
||||||
</Text>
|
</Text>
|
||||||
}
|
}
|
||||||
@@ -215,11 +215,3 @@ async function menuItems(
|
|||||||
color: colors[0]
|
color: colors[0]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function findSelectedMatchIndex(item: HighlightedResult, matchIndex: number) {
|
|
||||||
let activeIndex = 0;
|
|
||||||
for (let i = 0; i <= matchIndex - 1; ++i) {
|
|
||||||
activeIndex += item.content[i].length;
|
|
||||||
}
|
|
||||||
return activeIndex;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -85,9 +85,9 @@ export type BaseEditorSession = {
|
|||||||
*/
|
*/
|
||||||
activeBlockId?: string;
|
activeBlockId?: string;
|
||||||
/**
|
/**
|
||||||
* The index of search result to scroll to after opening the session successfully.
|
* The id of search result to scroll to after opening the session successfully.
|
||||||
*/
|
*/
|
||||||
activeSearchResultIndex?: number;
|
activeSearchResultId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type LockedEditorSession = BaseEditorSession & {
|
export type LockedEditorSession = BaseEditorSession & {
|
||||||
@@ -666,7 +666,7 @@ class EditorStore extends BaseStore<EditorStore> {
|
|||||||
silent?: boolean;
|
silent?: boolean;
|
||||||
openInNewTab?: boolean;
|
openInNewTab?: boolean;
|
||||||
rawContent?: string;
|
rawContent?: string;
|
||||||
activeSearchResultIndex?: number;
|
activeSearchResultId?: string;
|
||||||
} = {}
|
} = {}
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
const {
|
const {
|
||||||
@@ -790,7 +790,7 @@ class EditorStore extends BaseStore<EditorStore> {
|
|||||||
id: sessionId,
|
id: sessionId,
|
||||||
content,
|
content,
|
||||||
activeBlockId: options.activeBlockId,
|
activeBlockId: options.activeBlockId,
|
||||||
activeSearchResultIndex: options.activeSearchResultIndex,
|
activeSearchResultId: options.activeSearchResultId,
|
||||||
tabId
|
tabId
|
||||||
},
|
},
|
||||||
options.silent
|
options.silent
|
||||||
@@ -814,7 +814,7 @@ class EditorStore extends BaseStore<EditorStore> {
|
|||||||
color: colors[0]?.fromId,
|
color: colors[0]?.fromId,
|
||||||
tags,
|
tags,
|
||||||
activeBlockId: options.activeBlockId,
|
activeBlockId: options.activeBlockId,
|
||||||
activeSearchResultIndex: options.activeSearchResultIndex,
|
activeSearchResultId: options.activeSearchResultId,
|
||||||
tabId
|
tabId
|
||||||
},
|
},
|
||||||
options.silent
|
options.silent
|
||||||
@@ -835,7 +835,7 @@ class EditorStore extends BaseStore<EditorStore> {
|
|||||||
? { ...content, data: options.rawContent }
|
? { ...content, data: options.rawContent }
|
||||||
: content,
|
: content,
|
||||||
activeBlockId: options.activeBlockId,
|
activeBlockId: options.activeBlockId,
|
||||||
activeSearchResultIndex: options.activeSearchResultIndex,
|
activeSearchResultId: options.activeSearchResultId,
|
||||||
tabId
|
tabId
|
||||||
},
|
},
|
||||||
options.silent
|
options.silent
|
||||||
|
|||||||
@@ -62,10 +62,8 @@ type FuzzySearchField<T> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const MATCH_TAG_NAME = "nn-search-result";
|
const MATCH_TAG_NAME = "nn-search-result";
|
||||||
const MATCH_TAG_OPEN = `<${MATCH_TAG_NAME}>`;
|
|
||||||
const MATCH_TAG_CLOSE = `</${MATCH_TAG_NAME}>`;
|
|
||||||
const MATCH_TAG_REGEX = new RegExp(
|
const MATCH_TAG_REGEX = new RegExp(
|
||||||
`<${MATCH_TAG_NAME}>(.*?)<\\/${MATCH_TAG_NAME}>`,
|
`<${MATCH_TAG_NAME}\\s+id="(.+?)">(.*?)<\\/${MATCH_TAG_NAME}>`,
|
||||||
"gm"
|
"gm"
|
||||||
);
|
);
|
||||||
export default class Lookup {
|
export default class Lookup {
|
||||||
@@ -778,10 +776,11 @@ function highlightQueries(
|
|||||||
const regex = new RegExp(patterns.join("|"), "gi");
|
const regex = new RegExp(patterns.join("|"), "gi");
|
||||||
|
|
||||||
let hasMatches = false;
|
let hasMatches = false;
|
||||||
|
let matchIdCounter = 0;
|
||||||
|
|
||||||
const result = text.replace(regex, (match) => {
|
const result = text.replace(regex, (match) => {
|
||||||
hasMatches = true;
|
hasMatches = true;
|
||||||
return `${MATCH_TAG_OPEN}${match}${MATCH_TAG_CLOSE}`;
|
return createSearchResultTag(match, `match-${++matchIdCounter}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
return { text: result, hasMatches };
|
return { text: result, hasMatches };
|
||||||
@@ -796,10 +795,11 @@ export function splitHighlightedMatch(text: string): Match[][] {
|
|||||||
let matches: Match[] = [];
|
let matches: Match[] = [];
|
||||||
let totalLength = 0;
|
let totalLength = 0;
|
||||||
|
|
||||||
for (let i = 0; i < parts.length - 1; i += 2) {
|
for (let i = 0; i < parts.length - 1; i += 3) {
|
||||||
const prefix = parts[i];
|
const prefix = parts[i];
|
||||||
const match = parts[i + 1];
|
const matchId = parts[i + 1];
|
||||||
let suffix = parts.at(i + 2);
|
const match = parts[i + 2];
|
||||||
|
let suffix = parts[i + 3];
|
||||||
const matchLength = prefix.length + match.length + (suffix?.length || 0);
|
const matchLength = prefix.length + match.length + (suffix?.length || 0);
|
||||||
|
|
||||||
if (totalLength > 120 && matches.length > 0) {
|
if (totalLength > 120 && matches.length > 0) {
|
||||||
@@ -815,14 +815,15 @@ export function splitHighlightedMatch(text: string): Match[][] {
|
|||||||
suffix,
|
suffix,
|
||||||
Math.max(suffix.length / 2, 60)
|
Math.max(suffix.length / 2, 60)
|
||||||
);
|
);
|
||||||
parts[i + 2] = remaining;
|
parts[i + 3] = remaining;
|
||||||
suffix = _suffix;
|
suffix = _suffix;
|
||||||
}
|
}
|
||||||
|
|
||||||
matches.push({
|
matches.push({
|
||||||
match,
|
match,
|
||||||
prefix: prefix.replace(/\s{2,}/gm, " ").trimStart(),
|
prefix: prefix.replace(/\s{2,}/gm, " ").trimStart(),
|
||||||
suffix: suffix || ""
|
suffix: suffix || "",
|
||||||
|
id: matchId || undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
totalLength += matchLength;
|
totalLength += matchLength;
|
||||||
@@ -944,7 +945,8 @@ function stringToMatch(str: string): Match[] {
|
|||||||
{
|
{
|
||||||
prefix: str,
|
prefix: str,
|
||||||
match: "",
|
match: "",
|
||||||
suffix: ""
|
suffix: "",
|
||||||
|
id: undefined
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -962,6 +964,7 @@ function highlightHtmlContent(html: string, queries: string[]): string {
|
|||||||
const searchRegex = new RegExp(`(${patterns.join("|")})`, "gi");
|
const searchRegex = new RegExp(`(${patterns.join("|")})`, "gi");
|
||||||
|
|
||||||
let result = "";
|
let result = "";
|
||||||
|
let matchIdCounter = 0;
|
||||||
|
|
||||||
// Stack to track elements and their buffered content
|
// Stack to track elements and their buffered content
|
||||||
interface ElementInfo {
|
interface ElementInfo {
|
||||||
@@ -981,9 +984,8 @@ function highlightHtmlContent(html: string, queries: string[]): string {
|
|||||||
// Reset regex state after test
|
// Reset regex state after test
|
||||||
searchRegex.lastIndex = 0;
|
searchRegex.lastIndex = 0;
|
||||||
|
|
||||||
const processed = text.replace(
|
const processed = text.replace(searchRegex, (match) =>
|
||||||
searchRegex,
|
createSearchResultTag(match, `match-${++matchIdCounter}`)
|
||||||
"<nn-search-result>$1</nn-search-result>"
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (hasMatch) {
|
if (hasMatch) {
|
||||||
@@ -1150,18 +1152,23 @@ function getMatchScore(
|
|||||||
|
|
||||||
function textContainsTokens(text: string, tokens: QueryTokens) {
|
function textContainsTokens(text: string, tokens: QueryTokens) {
|
||||||
const lowerCasedText = text.toLowerCase();
|
const lowerCasedText = text.toLowerCase();
|
||||||
|
|
||||||
|
const createTagPattern = (token: string) => {
|
||||||
|
return `<${MATCH_TAG_NAME}\\s+id="(.+?)">${token}<\\/${MATCH_TAG_NAME}>`;
|
||||||
|
};
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!tokens.notTokens.every(
|
!tokens.notTokens.every(
|
||||||
(t) => !lowerCasedText.includes(`${MATCH_TAG_OPEN}${t}${MATCH_TAG_CLOSE}`)
|
(t) => !new RegExp(createTagPattern(t), "i").test(lowerCasedText)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return false;
|
return false;
|
||||||
return (
|
return (
|
||||||
tokens.andTokens.every((t) =>
|
tokens.andTokens.every((t) =>
|
||||||
lowerCasedText.includes(`${MATCH_TAG_OPEN}${t}${MATCH_TAG_CLOSE}`)
|
new RegExp(createTagPattern(t), "i").test(lowerCasedText)
|
||||||
) ||
|
) ||
|
||||||
tokens.orTokens.some((t) =>
|
tokens.orTokens.some((t) =>
|
||||||
lowerCasedText.includes(`${MATCH_TAG_OPEN}${t}${MATCH_TAG_CLOSE}`)
|
new RegExp(createTagPattern(t), "i").test(lowerCasedText)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1198,3 +1205,7 @@ function transformTokens(tokens: QueryTokens | undefined) {
|
|||||||
allTokens: [...andTokens, ...orTokens]
|
allTokens: [...andTokens, ...orTokens]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createSearchResultTag(content: string, id: string) {
|
||||||
|
return `<${MATCH_TAG_NAME} id="${id}">${content}</${MATCH_TAG_NAME}>`;
|
||||||
|
}
|
||||||
|
|||||||
@@ -494,6 +494,7 @@ export type Match = {
|
|||||||
prefix: string;
|
prefix: string;
|
||||||
match: string;
|
match: string;
|
||||||
suffix: string;
|
suffix: string;
|
||||||
|
id?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface HighlightedResult extends BaseItem<"searchResult"> {
|
export interface HighlightedResult extends BaseItem<"searchResult"> {
|
||||||
|
|||||||
@@ -176,7 +176,7 @@ export function extractMatchingBlocks(html: string, matchTagName: string) {
|
|||||||
const parser = new Parser(
|
const parser = new Parser(
|
||||||
{
|
{
|
||||||
ontext: (data) => (text += data),
|
ontext: (data) => (text += data),
|
||||||
onopentag(name) {
|
onopentag(name, attributes) {
|
||||||
if (!INLINE_TAGS.includes(name) && name !== matchTagName) {
|
if (!INLINE_TAGS.includes(name) && name !== matchTagName) {
|
||||||
openedTag = name;
|
openedTag = name;
|
||||||
text = "";
|
text = "";
|
||||||
@@ -184,7 +184,12 @@ export function extractMatchingBlocks(html: string, matchTagName: string) {
|
|||||||
}
|
}
|
||||||
if (name === matchTagName) {
|
if (name === matchTagName) {
|
||||||
hasMatches = true;
|
hasMatches = true;
|
||||||
text += `<${name}>`;
|
let tagString = `<${name}`;
|
||||||
|
if (attributes.id) {
|
||||||
|
tagString += ` id="${attributes.id}"`;
|
||||||
|
}
|
||||||
|
tagString += ">";
|
||||||
|
text += tagString;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onclosetag(name) {
|
onclosetag(name) {
|
||||||
|
|||||||
@@ -26,6 +26,18 @@ export const SearchResult = Mark.create({
|
|||||||
return [{ tag: "nn-search-result" }];
|
return [{ tag: "nn-search-result" }];
|
||||||
},
|
},
|
||||||
|
|
||||||
|
addAttributes() {
|
||||||
|
return {
|
||||||
|
id: {
|
||||||
|
default: null,
|
||||||
|
parseHTML: (element) => element.getAttribute("id"),
|
||||||
|
renderHTML: (attributes) => {
|
||||||
|
return attributes.id ? { id: attributes.id } : {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
renderHTML({ HTMLAttributes }) {
|
renderHTML({ HTMLAttributes }) {
|
||||||
return [
|
return [
|
||||||
"nn-search-result",
|
"nn-search-result",
|
||||||
|
|||||||
Reference in New Issue
Block a user