From 5f3cade81060b1b4a5f2e7aee9ed9610ae8dafb9 Mon Sep 17 00:00:00 2001 From: Zeng1998 <1129142694@qq.com> Date: Fri, 25 Nov 2022 21:59:21 +0800 Subject: [PATCH] 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 --- web/src/components/Memo.tsx | 4 +++- web/src/components/MemoContent.tsx | 8 ++++++-- web/src/components/MemoList.tsx | 4 +++- web/src/labs/highlighter/index.ts | 26 ++++++++++++++++++++++++++ 4 files changed, 38 insertions(+), 4 deletions(-) create mode 100644 web/src/labs/highlighter/index.ts diff --git a/web/src/components/Memo.tsx b/web/src/components/Memo.tsx index 3ab5e69d..9708aa62 100644 --- a/web/src/components/Memo.tsx +++ b/web/src/components/Memo.tsx @@ -19,6 +19,7 @@ dayjs.extend(relativeTime); interface Props { memo: Memo; + highlightWord?: 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) => { - const memo = props.memo; + const { memo, highlightWord } = props; const { t, i18n } = useTranslation(); const navigate = useNavigate(); const [displayTimeStr, setDisplayTimeStr] = useState(getFormatedMemoTimeStr(memo.displayTs, i18n.language)); @@ -239,6 +240,7 @@ const Memo: React.FC = (props: Props) => { diff --git a/web/src/components/MemoContent.tsx b/web/src/components/MemoContent.tsx index 249dd88b..447b8897 100644 --- a/web/src/components/MemoContent.tsx +++ b/web/src/components/MemoContent.tsx @@ -1,6 +1,7 @@ import { useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { marked } from "../labs/marked"; +import { highlightWithWord } from "../labs/highlighter"; import Icon from "./Icon"; import { SETTING_IS_FOLDING_ENABLED_KEY, IS_FOLDING_ENABLED_DEFAULT_VALUE } from "../helpers/consts"; import useLocalStorage from "../hooks/useLocalStorage"; @@ -12,6 +13,7 @@ export interface DisplayConfig { interface Props { content: string; + highlightWord?: string; className?: string; displayConfig?: Partial; onMemoContentClick?: (e: React.MouseEvent) => void; @@ -29,7 +31,7 @@ const defaultDisplayConfig: DisplayConfig = { }; const MemoContent: React.FC = (props: Props) => { - const { className, content, onMemoContentClick, onMemoContentDoubleClick } = props; + const { className, content, highlightWord, onMemoContentClick, onMemoContentDoubleClick } = props; const foldedContent = useMemo(() => { const firstHorizontalRuleIndex = content.search(/^---$|^\*\*\*$|^___$/m); return firstHorizontalRuleIndex !== -1 ? content.slice(0, firstHorizontalRuleIndex) : content; @@ -86,7 +88,9 @@ const MemoContent: React.FC = (props: Props) => { className={`memo-content-text ${state.expandButtonStatus === 0 ? "expanded" : ""}`} onClick={handleMemoContentClick} onDoubleClick={handleMemoContentDoubleClick} - dangerouslySetInnerHTML={{ __html: marked(state.expandButtonStatus === 0 ? foldedContent : content) }} + dangerouslySetInnerHTML={{ + __html: highlightWithWord(marked(state.expandButtonStatus === 0 ? foldedContent : content), highlightWord), + }} > {state.expandButtonStatus !== -1 && (
diff --git a/web/src/components/MemoList.tsx b/web/src/components/MemoList.tsx index 161cf314..223d8f75 100644 --- a/web/src/components/MemoList.tsx +++ b/web/src/components/MemoList.tsx @@ -16,6 +16,7 @@ const MemoList = () => { const memoDisplayTsOption = useAppSelector((state) => state.user.user?.setting.memoDisplayTsOption); const { memos, isFetching } = useAppSelector((state) => state.memo); const [isComplete, setIsComplete] = useState(false); + const [highlightWord, setHighlightWord] = useState(""); const { tag: tagQuery, duration, type: memoType, text: textQuery, shortcutId, visibility } = query ?? {}; const shortcut = shortcutId ? shortcutService.getShortcutById(shortcutId) : null; @@ -103,6 +104,7 @@ const MemoList = () => { if (pageWrapper) { pageWrapper.scrollTo(0, 0); } + setHighlightWord(query?.text); }, [query]); useEffect(() => { @@ -131,7 +133,7 @@ const MemoList = () => { return (
{sortedMemos.map((memo) => ( - + ))} {isFetching ? (
diff --git a/web/src/labs/highlighter/index.ts b/web/src/labs/highlighter/index.ts new file mode 100644 index 00000000..985f8ac7 --- /dev/null +++ b/web/src/labs/highlighter/index.ts @@ -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"), `${keyword}`) ?? ""; + node.parentNode?.insertBefore(span, node); + node.parentNode?.removeChild(node); + } + for (const child of Array.from(node.childNodes)) { + walkthroughNodeWithKeyword(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); +};