From 4faa22305fd7a64731d52edfea7694e8cff06b09 Mon Sep 17 00:00:00 2001 From: canisminor1990 Date: Sun, 24 Nov 2024 18:19:02 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=92=84=20style:=20Update=20SearchResult?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + src/slots/SearchBar/index.tsx | 2 +- src/slots/SearchResult/index.tsx | 246 +++++++++++++++++++++++++++++++ 3 files changed, 248 insertions(+), 1 deletion(-) create mode 100644 src/slots/SearchResult/index.tsx diff --git a/package.json b/package.json index 3b9868d..942e231 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,7 @@ "dependencies": { "@floating-ui/react": "^0.26.28", "ahooks": "^3.8.1", + "animated-scroll-to": "^2.3.0", "chalk": "^4.1.2", "fast-deep-equal": "^3.1.3", "history": "^5.3.0", diff --git a/src/slots/SearchBar/index.tsx b/src/slots/SearchBar/index.tsx index 874be2e..7757e0e 100644 --- a/src/slots/SearchBar/index.tsx +++ b/src/slots/SearchBar/index.tsx @@ -1,8 +1,8 @@ import { SearchBar as Input } from '@lobehub/ui'; import { useSiteSearch } from 'dumi'; -import SearchResult from 'dumi/theme-default/slots/SearchResult'; import { memo, useState } from 'react'; +import SearchResult from '../SearchResult'; import { useStyles } from './style'; const SearchBar = memo(() => { diff --git a/src/slots/SearchResult/index.tsx b/src/slots/SearchResult/index.tsx new file mode 100644 index 0000000..1a1964f --- /dev/null +++ b/src/slots/SearchResult/index.tsx @@ -0,0 +1,246 @@ +import { Icon } from '@lobehub/ui'; +import animateScrollTo from 'animated-scroll-to'; +import { Empty, Typography } from 'antd'; +import { useTheme } from 'antd-style'; +import { FormattedMessage, Link, history, useLocation, type useSiteSearch } from 'dumi'; +import { FileBox, FileIcon, HeadingIcon, LetterText, LucideIcon } from 'lucide-react'; +import React, { Fragment, memo, useCallback, useEffect, useState } from 'react'; +import { Center, Flexbox } from 'react-layout-kit'; + +const ICONS_MAPPING: { [key: string]: LucideIcon } = { + content: LetterText, + demo: FileBox, + page: FileIcon, + title: HeadingIcon, +}; + +type ISearchResult = ReturnType['result']; + +type ISearchFlatData = ( + | { + type: 'title'; + value: Pick; + } + | { + activeIndex: number; + type: 'hint'; + value: ISearchResult[0]['hints'][0]; + } +)[]; + +const Highlight = memo<{ + texts: ISearchResult[0]['hints'][0]['highlightTexts']; +}>((props) => { + return ( + <> + {props.texts.map((text, idx) => ( + {text.highlighted ? {text.text} : text.text} + ))} + + ); +}); + +const useFlatSearchData = (data: ISearchResult) => { + const update = useCallback((): [ISearchFlatData, number] => { + let activeIndex = 0; + const ret: ISearchFlatData = []; + + for (const item of data) { + if (item.title) { + ret.push({ + type: 'title', + value: { + title: item.title, + }, + }); + } + for (const hint of item.hints) { + ret.push({ + activeIndex: activeIndex++, + type: 'hint', + value: hint, + }); + } + } + + return [ret, activeIndex]; + }, [data]); + const [flatData, setFlatData] = useState(update); + + useEffect(() => { + setFlatData(update); + }, [data]); + + return flatData; +}; + +const SearchResult = memo<{ + data: ISearchResult; + loading: boolean; + onItemSelect?: (item: ISearchResult[0]['hints'][0]) => void; +}>((props) => { + const theme = useTheme(); + const [data, histsCount] = useFlatSearchData(props.data); + const [activeIndex, setActiveIndex] = useState(-1); + const { pathname } = useLocation(); + + const onItemSelect = (item: ISearchResult[0]['hints'][0]) => { + props.onItemSelect?.(item); + + const url = new URL(item?.link, location.origin); + if (url?.pathname === pathname && !url.hash) { + setTimeout(() => { + animateScrollTo(0, { + maxDuration: 300, + }); + }, 1); + } + }; + + useEffect(() => { + const handler = (ev: KeyboardEvent) => { + // TODO: scroll into view for invisible items + if (ev.key === 'ArrowDown') { + setActiveIndex((activeIndex + 1) % histsCount); + } else if (ev.key === 'ArrowUp') { + setActiveIndex((activeIndex + histsCount - 1) % histsCount); + } else if (ev.key === 'Enter' && activeIndex >= 0) { + const item = data.find((item) => item.type === 'hint' && item.activeIndex === activeIndex)! + .value as ISearchResult[0]['hints'][0]; + + history.push(item.link); + onItemSelect?.(item); + (document.activeElement as HTMLInputElement).blur(); + } + + if (['Escape', 'Enter'].includes(ev.key)) { + setActiveIndex(-1); + } + }; + + document.addEventListener('keydown', handler); + + return () => document.removeEventListener('keydown', handler); + }); + + let returnNode: React.ReactNode = null; + + if (props.loading) { + returnNode = ( + } + image={Empty.PRESENTED_IMAGE_SIMPLE} + /> + ); + } else if (props.data.length > 0) { + returnNode = ( + + {data.map((item, i) => + item.type === 'title' ? ( + + + {item.value.title} + + + ) : ( + + onItemSelect?.(item.value)} + style={{ color: 'inherit' }} + to={item.value.link} + > + +
+ +
+ + + + + + + + +
+ +
+ ), + )} +
+ ); + } else { + returnNode = ( + } + image={Empty.PRESENTED_IMAGE_SIMPLE} + /> + ); + } + + return ( + ev.preventDefault()} + onMouseEnter={() => setActiveIndex(-1)} + onMouseUpCapture={() => { + (document.activeElement as HTMLInputElement).blur(); + }} + > + {returnNode} + + ); +}); + +export default SearchResult;