feat: highlight the searched text in memo content (#514)

* feat: highlight the searched text in memo content

* update

* update

* update

* update

Co-authored-by: boojack <stevenlgtm@gmail.com>
pull/575/head
Zeng1998 2 years ago committed by GitHub
parent 072851e3ba
commit 5f3cade810
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -19,6 +19,7 @@ dayjs.extend(relativeTime);
interface Props { interface Props {
memo: Memo; memo: Memo;
highlightWord?: string;
} }
export const getFormatedMemoTimeStr = (time: number, locale = "en"): string => { export const getFormatedMemoTimeStr = (time: number, locale = "en"): string => {
@ -30,7 +31,7 @@ export const getFormatedMemoTimeStr = (time: number, locale = "en"): string => {
}; };
const Memo: React.FC<Props> = (props: Props) => { const Memo: React.FC<Props> = (props: Props) => {
const memo = props.memo; const { memo, highlightWord } = props;
const { t, i18n } = useTranslation(); const { t, i18n } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const [displayTimeStr, setDisplayTimeStr] = useState<string>(getFormatedMemoTimeStr(memo.displayTs, i18n.language)); const [displayTimeStr, setDisplayTimeStr] = useState<string>(getFormatedMemoTimeStr(memo.displayTs, i18n.language));
@ -239,6 +240,7 @@ const Memo: React.FC<Props> = (props: Props) => {
</div> </div>
<MemoContent <MemoContent
content={memo.content} content={memo.content}
highlightWord={highlightWord}
onMemoContentClick={handleMemoContentClick} onMemoContentClick={handleMemoContentClick}
onMemoContentDoubleClick={handleMemoContentDoubleClick} onMemoContentDoubleClick={handleMemoContentDoubleClick}
/> />

@ -1,6 +1,7 @@
import { useEffect, useMemo, useRef, useState } from "react"; import { useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { marked } from "../labs/marked"; import { marked } from "../labs/marked";
import { highlightWithWord } from "../labs/highlighter";
import Icon from "./Icon"; import Icon from "./Icon";
import { SETTING_IS_FOLDING_ENABLED_KEY, IS_FOLDING_ENABLED_DEFAULT_VALUE } from "../helpers/consts"; import { SETTING_IS_FOLDING_ENABLED_KEY, IS_FOLDING_ENABLED_DEFAULT_VALUE } from "../helpers/consts";
import useLocalStorage from "../hooks/useLocalStorage"; import useLocalStorage from "../hooks/useLocalStorage";
@ -12,6 +13,7 @@ export interface DisplayConfig {
interface Props { interface Props {
content: string; content: string;
highlightWord?: string;
className?: string; className?: string;
displayConfig?: Partial<DisplayConfig>; displayConfig?: Partial<DisplayConfig>;
onMemoContentClick?: (e: React.MouseEvent) => void; onMemoContentClick?: (e: React.MouseEvent) => void;
@ -29,7 +31,7 @@ const defaultDisplayConfig: DisplayConfig = {
}; };
const MemoContent: React.FC<Props> = (props: Props) => { const MemoContent: React.FC<Props> = (props: Props) => {
const { className, content, onMemoContentClick, onMemoContentDoubleClick } = props; const { className, content, highlightWord, onMemoContentClick, onMemoContentDoubleClick } = props;
const foldedContent = useMemo(() => { const foldedContent = useMemo(() => {
const firstHorizontalRuleIndex = content.search(/^---$|^\*\*\*$|^___$/m); const firstHorizontalRuleIndex = content.search(/^---$|^\*\*\*$|^___$/m);
return firstHorizontalRuleIndex !== -1 ? content.slice(0, firstHorizontalRuleIndex) : content; return firstHorizontalRuleIndex !== -1 ? content.slice(0, firstHorizontalRuleIndex) : content;
@ -86,7 +88,9 @@ const MemoContent: React.FC<Props> = (props: Props) => {
className={`memo-content-text ${state.expandButtonStatus === 0 ? "expanded" : ""}`} className={`memo-content-text ${state.expandButtonStatus === 0 ? "expanded" : ""}`}
onClick={handleMemoContentClick} onClick={handleMemoContentClick}
onDoubleClick={handleMemoContentDoubleClick} onDoubleClick={handleMemoContentDoubleClick}
dangerouslySetInnerHTML={{ __html: marked(state.expandButtonStatus === 0 ? foldedContent : content) }} dangerouslySetInnerHTML={{
__html: highlightWithWord(marked(state.expandButtonStatus === 0 ? foldedContent : content), highlightWord),
}}
></div> ></div>
{state.expandButtonStatus !== -1 && ( {state.expandButtonStatus !== -1 && (
<div className="expand-btn-container"> <div className="expand-btn-container">

@ -16,6 +16,7 @@ const MemoList = () => {
const memoDisplayTsOption = useAppSelector((state) => state.user.user?.setting.memoDisplayTsOption); const memoDisplayTsOption = useAppSelector((state) => state.user.user?.setting.memoDisplayTsOption);
const { memos, isFetching } = useAppSelector((state) => state.memo); const { memos, isFetching } = useAppSelector((state) => state.memo);
const [isComplete, setIsComplete] = useState<boolean>(false); const [isComplete, setIsComplete] = useState<boolean>(false);
const [highlightWord, setHighlightWord] = useState<string | undefined>("");
const { tag: tagQuery, duration, type: memoType, text: textQuery, shortcutId, visibility } = query ?? {}; const { tag: tagQuery, duration, type: memoType, text: textQuery, shortcutId, visibility } = query ?? {};
const shortcut = shortcutId ? shortcutService.getShortcutById(shortcutId) : null; const shortcut = shortcutId ? shortcutService.getShortcutById(shortcutId) : null;
@ -103,6 +104,7 @@ const MemoList = () => {
if (pageWrapper) { if (pageWrapper) {
pageWrapper.scrollTo(0, 0); pageWrapper.scrollTo(0, 0);
} }
setHighlightWord(query?.text);
}, [query]); }, [query]);
useEffect(() => { useEffect(() => {
@ -131,7 +133,7 @@ const MemoList = () => {
return ( return (
<div className="memo-list-container"> <div className="memo-list-container">
{sortedMemos.map((memo) => ( {sortedMemos.map((memo) => (
<Memo key={`${memo.id}-${memo.displayTs}`} memo={memo} /> <Memo key={`${memo.id}-${memo.displayTs}`} memo={memo} highlightWord={highlightWord} />
))} ))}
{isFetching ? ( {isFetching ? (
<div className="status-text-container fetching-tip"> <div className="status-text-container fetching-tip">

@ -0,0 +1,26 @@
const escapeRegExp = (str: string): string => {
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
};
const walkthroughNodeWithKeyword = (node: HTMLElement, keyword: string) => {
if (node.nodeType === 3) {
const span = document.createElement("span");
span.innerHTML = node.nodeValue?.replace(new RegExp(keyword, "g"), `<mark>${keyword}</mark>`) ?? "";
node.parentNode?.insertBefore(span, node);
node.parentNode?.removeChild(node);
}
for (const child of Array.from(node.childNodes)) {
walkthroughNodeWithKeyword(<HTMLElement>child, keyword);
}
return node.innerHTML;
};
export const highlightWithWord = (html: string, keyword?: string): string => {
if (!keyword) {
return html;
}
keyword = escapeRegExp(keyword);
const wrap = document.createElement("div");
wrap.innerHTML = html;
return walkthroughNodeWithKeyword(wrap, keyword);
};
Loading…
Cancel
Save