mirror of
https://github.com/usememos/memos.git
synced 2025-03-04 09:19:09 +08:00
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>
This commit is contained in:
parent
072851e3ba
commit
5f3cade810
4 changed files with 38 additions and 4 deletions
|
@ -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: Props) => {
|
||||
const memo = props.memo;
|
||||
const { memo, highlightWord } = props;
|
||||
const { t, i18n } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const [displayTimeStr, setDisplayTimeStr] = useState<string>(getFormatedMemoTimeStr(memo.displayTs, i18n.language));
|
||||
|
@ -239,6 +240,7 @@ const Memo: React.FC<Props> = (props: Props) => {
|
|||
</div>
|
||||
<MemoContent
|
||||
content={memo.content}
|
||||
highlightWord={highlightWord}
|
||||
onMemoContentClick={handleMemoContentClick}
|
||||
onMemoContentDoubleClick={handleMemoContentDoubleClick}
|
||||
/>
|
||||
|
|
|
@ -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<DisplayConfig>;
|
||||
onMemoContentClick?: (e: React.MouseEvent) => void;
|
||||
|
@ -29,7 +31,7 @@ const defaultDisplayConfig: DisplayConfig = {
|
|||
};
|
||||
|
||||
const MemoContent: React.FC<Props> = (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: 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),
|
||||
}}
|
||||
></div>
|
||||
{state.expandButtonStatus !== -1 && (
|
||||
<div className="expand-btn-container">
|
||||
|
|
|
@ -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<boolean>(false);
|
||||
const [highlightWord, setHighlightWord] = useState<string | undefined>("");
|
||||
|
||||
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 (
|
||||
<div className="memo-list-container">
|
||||
{sortedMemos.map((memo) => (
|
||||
<Memo key={`${memo.id}-${memo.displayTs}`} memo={memo} />
|
||||
<Memo key={`${memo.id}-${memo.displayTs}`} memo={memo} highlightWord={highlightWord} />
|
||||
))}
|
||||
{isFetching ? (
|
||||
<div className="status-text-container fetching-tip">
|
||||
|
|
26
web/src/labs/highlighter/index.ts
Normal file
26
web/src/labs/highlighter/index.ts
Normal file
|
@ -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…
Reference in a new issue