Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(TextSearchHighlight):Detach HighlightComponent and TextCearch components #15

Merged
merged 5 commits into from
Sep 11, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/online-demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"postcss": "^8.4.41",
"tailwindcss": "^3.4.9",
"text-search-engine": "workspace:*",
"@text-search-engine/react": "workspace:*",
"typescript": "^5.5.4",
"vite": "^5.4.0"
},
Expand Down
26 changes: 25 additions & 1 deletion app/online-demo/src/pages/home/index.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,26 @@
import { Container, CssBaseline, Grid2 as Grid } from '@mui/material'

import { HighlightComponent, TextSearch } from '@text-search-engine/react'
import { useState } from 'react'
import * as TextSearchEngine from 'text-search-engine'
import type { Matrix } from 'text-search-engine'
import ListSearch from '../../components/ListSearch'
import ShowTip from '../../components/showTip'

window._TEXT_SEARCH_ENGINE_ = TextSearchEngine

export default function Home() {
const [hitRanges, setHitRanges] = useState<Matrix>([])
const [targetState, setTargetState] = useState('')
const [newHitRanges, setNewHitRanges] = useState<Matrix>([
[0, 3],
[10, 15],
])

const handleNewSearch = (ranges: Matrix) => {
setNewHitRanges(ranges)
console.log('New search result:', ranges)
}

return (
<div>
<CssBaseline />
Expand All @@ -16,6 +30,16 @@ export default function Home() {
<Grid size={{ xs: 24, md: 12 }}>
<ListSearch />
</Grid>
<Grid size={{ xs: 24, md: 12 }}>
<TextSearch source='这是一段需要搜索的长文本。可以在这里搜索任何内容。' target='搜索' />
</Grid>
<Grid size={{ xs: 24, md: 12 }}>
<h3>新的 TextSearch 示例</h3>
<TextSearch
source='这是使用新的 TextSearch 组件的示例文本。你可以在这里输入并搜索任何内容。这是使用新的 TextSearch 组件的示例文本。你可以在这里输入并搜索任何内容这是使用新的 TextSearch 组件的示例文本。你可以在这里输入并搜索任何内容'
onSearch={handleNewSearch}
/>
</Grid>
</Grid>
</Container>
</div>
Expand Down
16 changes: 13 additions & 3 deletions components/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@text-search-engine/react",
"version": "1.2.0",
"private": true,
"description": "",
"description": "React components for text search and highlighting",
"license": "MIT",
"author": "cjinhuo",
"main": "src/index.ts",
Expand All @@ -15,7 +15,17 @@
"bugs": {
"url": "https://github.com/cjinhuo/text-search-engine/issues"
},
"files": [],
"files": ["src"],
"scripts": {},
"dependencies": {}
"dependencies": {
"react": "^18.3.1",
"styled-components": "^6.1.13",
"text-search-engine": "^1.2.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
},
"devDependencies": {
"@types/react": "^18.3.3"
}
}
175 changes: 175 additions & 0 deletions components/react/src/TextSearchHighlight.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import React from 'react'
import { useCallback, useEffect, useRef, useState } from 'react'
import styled from 'styled-components'
import { type Matrix, search } from 'text-search-engine'

const HIGHLIGHT_TEXT_CLASS = 'hg-text'
const NORMAL_TEXT_CLASS = 'nm-text'

const HighlightContainer = styled.div`
display: flex;
width: 100%;
min-height: 20px;
font-size: 14px;
font-weight: 500;
overflow: hidden;
white-space: nowrap;
background-color: white;

.${NORMAL_TEXT_CLASS} {
color: var(--color-neutral-3, #666);
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}

.${HIGHLIGHT_TEXT_CLASS} {
white-space: nowrap;
color: var(--highlight-text, #000);
background-color: var(--highlight-bg, #ffffff);
border-radius: 4px;
padding: 0 2px;
}
`

interface HighlightComponentProps {
source: string
hitRanges: Matrix
width?: string | number
highlightStyle?: React.CSSProperties
mon1cakk marked this conversation as resolved.
Show resolved Hide resolved
}

export const HighlightComponent: React.FC<HighlightComponentProps> = ({
source,
hitRanges,
width = '100%',
mon1cakk marked this conversation as resolved.
Show resolved Hide resolved
highlightStyle,
}) => {
const containerRef = useRef<HTMLDivElement>(null)
const [renderedContent, setRenderedContent] = useState<React.ReactNode[]>([])

const updateContent = useCallback(() => {
if (!containerRef.current) return

const newContent: React.ReactNode[] = []
let lastIndex = 0

hitRanges.forEach(([start, end], index) => {
if (start > lastIndex) {
const normalText = source.slice(lastIndex, start).replace(/ /g, '\u00A0')
newContent.push(
<span key={`normal-${lastIndex}`} className={NORMAL_TEXT_CLASS}>
{normalText}
</span>
)
}

const highlightText = source.slice(start, end + 1).replace(/ /g, '\u00A0')
newContent.push(
<span key={`highlight-${start}`} className={HIGHLIGHT_TEXT_CLASS} style={highlightStyle}>
{highlightText}
</span>
)

lastIndex = end + 1
})

if (lastIndex < source.length) {
const remainingText = source.slice(lastIndex).replace(/ /g, '\u00A0')
newContent.push(
<span key={`normal-${lastIndex}`} className={NORMAL_TEXT_CLASS}>
{remainingText}
</span>
)
}

setRenderedContent(newContent)
}, [source, hitRanges, highlightStyle])

useEffect(() => {
const resizeObserver = new ResizeObserver(updateContent)
mon1cakk marked this conversation as resolved.
Show resolved Hide resolved
if (containerRef.current) {
resizeObserver.observe(containerRef.current)
}
updateContent() // Initial render
return () => resizeObserver.disconnect()
}, [updateContent])

return (
<HighlightContainer ref={containerRef} style={{ width }}>
{renderedContent}
</HighlightContainer>
)
}

interface TextSearchProps {
source: string
target?: string
onSearch?: (hitRanges: Matrix) => void
width?: string | number
highlightStyle?: React.CSSProperties
children?: React.ReactNode
}

export const TextSearch: React.FC<TextSearchProps> = ({
source,
target,
onSearch,
width = 'auto',
highlightStyle,
children,
}) => {
const [internalTarget, setInternalTarget] = useState(target || '')
const [hitRanges, setHitRanges] = useState<Matrix>([])

const handleSearch = useCallback(
(searchTarget: string) => {
const result = search(source, searchTarget)
setHitRanges(result || [])
if (onSearch) {
onSearch(result || [])
}
},
[source, onSearch]
)

useEffect(() => {
if (target !== undefined) {
setInternalTarget(target)
handleSearch(target)
}
}, [target, handleSearch])

const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newTarget = e.target.value
setInternalTarget(newTarget)
handleSearch(newTarget)
}

return (
<div style={{ width }}>
{target === undefined && (
<input
mon1cakk marked this conversation as resolved.
Show resolved Hide resolved
type='text'
value={internalTarget}
onChange={handleInputChange}
placeholder='输入搜索文本'
style={{
width: '100%',
marginBottom: '10px',
padding: '8px 12px',
fontSize: '16px',
border: '2px solid #007bff',
borderRadius: '4px',
outline: 'none',
}}
/>
)}
{children || (
<HighlightComponent source={source} hitRanges={hitRanges} width='100%' highlightStyle={highlightStyle} />
)}
</div>
)
}

export default TextSearch
1 change: 1 addition & 0 deletions components/react/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { HighlightComponent, TextSearch } from './TextSearchHighlight'
Loading