♻️ refactor(ContentSearch): ContentSearch to use CSS highlights API (#7493)

This commit is contained in:
Kingsword 2025-06-28 20:04:03 +08:00 committed by GitHub
parent 8de6ae1772
commit 101d73fc10
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 223 additions and 442 deletions

View File

@ -173,12 +173,12 @@ ul {
color: var(--color-icon); color: var(--color-icon);
} }
span.highlight { ::highlight(search-matches) {
background-color: var(--color-background-highlight); background-color: var(--color-background-highlight);
color: var(--color-highlight); color: var(--color-highlight);
} }
span.highlight.selected { ::highlight(current-match) {
background-color: var(--color-background-highlight-accent); background-color: var(--color-background-highlight-accent);
} }

View File

@ -3,13 +3,10 @@ import NarrowLayout from '@renderer/pages/home/Messages/NarrowLayout'
import { Tooltip } from 'antd' import { Tooltip } from 'antd'
import { debounce } from 'lodash' import { debounce } from 'lodash'
import { CaseSensitive, ChevronDown, ChevronUp, User, WholeWord, X } from 'lucide-react' import { CaseSensitive, ChevronDown, ChevronUp, User, WholeWord, X } from 'lucide-react'
import React, { useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react' import React, { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
const HIGHLIGHT_CLASS = 'highlight'
const HIGHLIGHT_SELECT_CLASS = 'selected'
interface Props { interface Props {
children?: React.ReactNode children?: React.ReactNode
searchTarget: React.RefObject<React.ReactNode> | React.RefObject<HTMLElement> | HTMLElement searchTarget: React.RefObject<React.ReactNode> | React.RefObject<HTMLElement> | HTMLElement
@ -18,19 +15,14 @@ interface Props {
* *
* `true``node` * `true``node`
*/ */
filter: (node: Node) => boolean filter: NodeFilter
includeUser?: boolean includeUser?: boolean
onIncludeUserChange?: (value: boolean) => void onIncludeUserChange?: (value: boolean) => void
} }
enum SearchCompletedState { enum SearchCompletedState {
NotSearched, NotSearched,
FirstSearched Searched
}
enum SearchTargetIndex {
Next,
Prev
} }
export interface ContentSearchRef { export interface ContentSearchRef {
@ -47,60 +39,20 @@ export interface ContentSearchRef {
focus(): void focus(): void
} }
interface MatchInfo {
index: number
length: number
text: string
}
const escapeRegExp = (string: string): string => { const escapeRegExp = (string: string): string => {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // $& means the whole matched string return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // $& means the whole matched string
} }
const findWindowVerticalCenterElementIndex = (elementList: HTMLElement[]): number | null => { const findRangesInTarget = (
if (!elementList || elementList.length === 0) { target: HTMLElement,
return null filter: NodeFilter,
}
let closestElementIndex: number | null = null
let minVerticalDistance = Infinity
const windowCenterY = window.innerHeight / 2
for (let i = 0; i < elementList.length; i++) {
const element = elementList[i]
if (!(element instanceof HTMLElement)) {
continue
}
const rect = element.getBoundingClientRect()
if (rect.bottom < 0 || rect.top > window.innerHeight) {
continue
}
const elementCenterY = rect.top + rect.height / 2
const verticalDistance = Math.abs(elementCenterY - windowCenterY)
if (verticalDistance < minVerticalDistance) {
minVerticalDistance = verticalDistance
closestElementIndex = i
}
}
return closestElementIndex
}
const highlightText = (
textNode: Node,
searchText: string, searchText: string,
highlightClass: string,
isCaseSensitive: boolean, isCaseSensitive: boolean,
isWholeWord: boolean isWholeWord: boolean
): HTMLSpanElement[] | null => { ): Range[] => {
const textNodeParentNode: HTMLElement | null = textNode.parentNode as HTMLElement CSS.highlights.clear()
if (textNodeParentNode) { const ranges: Range[] = []
if (textNodeParentNode.classList.contains(highlightClass)) {
return null
}
}
if (textNode.nodeType !== Node.TEXT_NODE || !textNode.textContent) {
return null
}
const textContent = textNode.textContent
const escapedSearchText = escapeRegExp(searchText) const escapedSearchText = escapeRegExp(searchText)
// 检查搜索文本是否仅包含拉丁字母 // 检查搜索文本是否仅包含拉丁字母
@ -109,89 +61,66 @@ const highlightText = (
// 只有当搜索文本仅包含拉丁字母时才应用大小写敏感 // 只有当搜索文本仅包含拉丁字母时才应用大小写敏感
const regexFlags = hasOnlyLatinLetters && isCaseSensitive ? 'g' : 'gi' const regexFlags = hasOnlyLatinLetters && isCaseSensitive ? 'g' : 'gi'
const regexPattern = isWholeWord ? `\\b${escapedSearchText}\\b` : escapedSearchText const regexPattern = isWholeWord ? `\\b${escapedSearchText}\\b` : escapedSearchText
const regex = new RegExp(regexPattern, regexFlags) const searchRegex = new RegExp(regexPattern, regexFlags)
const treeWalker = document.createTreeWalker(target, NodeFilter.SHOW_TEXT, filter)
const allTextNodes: { node: Node; startOffset: number }[] = []
let fullText = ''
let match // 1. 拼接所有文本节点内容
const matches: MatchInfo[] = [] while (treeWalker.nextNode()) {
while ((match = regex.exec(textContent)) !== null) { allTextNodes.push({
if (typeof match.index === 'number' && typeof match[0] === 'string') { node: treeWalker.currentNode,
matches.push({ index: match.index, length: match[0].length, text: match[0] }) startOffset: fullText.length
} else { })
console.error('Unexpected match format:', match) fullText += treeWalker.currentNode.nodeValue
}
} }
if (matches.length === 0) { // 2.在完整文本中查找匹配项
return null let match: RegExpExecArray | null = null
} while ((match = searchRegex.exec(fullText))) {
const matchStart = match.index
const matchEnd = matchStart + match[0].length
const parentNode = textNode.parentNode // 3. 将匹配项的索引映射回DOM Range
if (!parentNode) { let startNode: Node | null = null
return null let endNode: Node | null = null
} let startOffset = 0
let endOffset = 0
const fragment = document.createDocumentFragment() // 找到起始节点和偏移
let currentIndex = 0 for (const nodeInfo of allTextNodes) {
const highlightTextSet = new Set<HTMLSpanElement>() if (
matchStart >= nodeInfo.startOffset &&
matches.forEach(({ index, length, text }) => { matchStart < nodeInfo.startOffset + (nodeInfo.node.nodeValue?.length ?? 0)
if (index > currentIndex) { ) {
fragment.appendChild(document.createTextNode(textContent.substring(currentIndex, index))) startNode = nodeInfo.node
} startOffset = matchStart - nodeInfo.startOffset
const highlightSpan = document.createElement('span') break
highlightSpan.className = highlightClass
highlightSpan.textContent = text // Use the matched text to preserve case if not case-sensitive
fragment.appendChild(highlightSpan)
highlightTextSet.add(highlightSpan)
currentIndex = index + length
})
if (currentIndex < textContent.length) {
fragment.appendChild(document.createTextNode(textContent.substring(currentIndex)))
}
parentNode.replaceChild(fragment, textNode)
return [...highlightTextSet]
}
const mergeAdjacentTextNodes = (node: HTMLElement) => {
const children = Array.from(node.childNodes)
const groups: Array<Node | { text: string; nodes: Node[] }> = []
let currentTextGroup: { text: string; nodes: Node[] } | null = null
for (const child of children) {
if (child.nodeType === Node.TEXT_NODE) {
if (currentTextGroup === null) {
currentTextGroup = {
text: child.textContent ?? '',
nodes: [child]
}
} else {
currentTextGroup.text += child.textContent
currentTextGroup.nodes.push(child)
} }
} else { }
if (currentTextGroup !== null) {
groups.push(currentTextGroup!) // 找到结束节点和偏移
currentTextGroup = null for (const nodeInfo of allTextNodes) {
if (
matchEnd > nodeInfo.startOffset &&
matchEnd <= nodeInfo.startOffset + (nodeInfo.node.nodeValue?.length ?? 0)
) {
endNode = nodeInfo.node
endOffset = matchEnd - nodeInfo.startOffset
break
} }
groups.push(child) }
// 如果起始和结束节点都找到了,则创建一个 Range
if (startNode && endNode) {
const range = new Range()
range.setStart(startNode, startOffset)
range.setEnd(endNode, endOffset)
ranges.push(range)
} }
} }
if (currentTextGroup !== null) { return ranges
groups.push(currentTextGroup)
}
const newChildren = groups.map((group) => {
if (group instanceof Node) {
return group
} else {
return document.createTextNode(group.text)
}
})
node.replaceChildren(...newChildren)
} }
// eslint-disable-next-line @eslint-react/no-forward-ref // eslint-disable-next-line @eslint-react/no-forward-ref
@ -206,328 +135,178 @@ export const ContentSearch = React.forwardRef<ContentSearchRef, Props>(
})() })()
const containerRef = React.useRef<HTMLDivElement>(null) const containerRef = React.useRef<HTMLDivElement>(null)
const searchInputRef = React.useRef<HTMLInputElement>(null) const searchInputRef = React.useRef<HTMLInputElement>(null)
const [searchResultIndex, setSearchResultIndex] = useState(0)
const [totalCount, setTotalCount] = useState(0)
const [enableContentSearch, setEnableContentSearch] = useState(false) const [enableContentSearch, setEnableContentSearch] = useState(false)
const [searchCompleted, setSearchCompleted] = useState(SearchCompletedState.NotSearched) const [searchCompleted, setSearchCompleted] = useState(SearchCompletedState.NotSearched)
const [isCaseSensitive, setIsCaseSensitive] = useState(false) const [isCaseSensitive, setIsCaseSensitive] = useState(false)
const [isWholeWord, setIsWholeWord] = useState(false) const [isWholeWord, setIsWholeWord] = useState(false)
const [shouldScroll, setShouldScroll] = useState(false) const [allRanges, setAllRanges] = useState<Range[]>([])
const highlightTextSet = useState(new Set<Node>())[0] const [currentIndex, setCurrentIndex] = useState(0)
const prevSearchText = useRef('') const prevSearchText = useRef('')
const { t } = useTranslation() const { t } = useTranslation()
const locateByIndex = (index: number, shouldScroll = true) => { const resetSearch = useCallback(() => {
if (target) { CSS.highlights.clear()
const highlightTextNodes = [...highlightTextSet] as HTMLElement[] setAllRanges([])
highlightTextNodes.sort((a, b) => { setSearchCompleted(SearchCompletedState.NotSearched)
const { top: aTop } = a.getBoundingClientRect() }, [])
const { top: bTop } = b.getBoundingClientRect()
return aTop - bTop const locateByIndex = useCallback(
}) (shouldScroll = true) => {
for (const node of highlightTextNodes) { // 清理旧的高亮
node.classList.remove(HIGHLIGHT_SELECT_CLASS) CSS.highlights.clear()
}
setSearchResultIndex(index) if (allRanges.length > 0) {
if (highlightTextNodes.length > 0) { // 1. 创建并注册所有匹配项的高亮
const highlightTextNode = highlightTextNodes[index] ?? null const allMatchesHighlight = new Highlight(...allRanges)
if (highlightTextNode) { CSS.highlights.set('search-matches', allMatchesHighlight)
highlightTextNode.classList.add(HIGHLIGHT_SELECT_CLASS)
// 2. 如果有当前项,为其创建并注册一个特殊的高亮
if (currentIndex !== -1 && allRanges[currentIndex]) {
const currentMatchRange = allRanges[currentIndex]
const currentMatchHighlight = new Highlight(currentMatchRange)
CSS.highlights.set('current-match', currentMatchHighlight)
// 3. 将当前项滚动到视图中
// 获取第一个文本节点的父元素来进行滚动
const parentElement = currentMatchRange.startContainer.parentElement
if (shouldScroll) { if (shouldScroll) {
highlightTextNode.scrollIntoView({ parentElement?.scrollIntoView({
behavior: 'smooth', behavior: 'smooth',
block: 'center' block: 'center',
// inline: 'center' 水平方向居中可能会导致 content 页面整体偏右, 使得左半部的内容被遮挡. 因此先注释掉该代码 inline: 'nearest'
}) })
} }
} }
} }
} },
} [allRanges, currentIndex]
)
const restoreHighlight = () => { const search = useCallback(() => {
const highlightTextParentNodeSet = new Set<HTMLElement>()
// Make a copy because the set might be modified during iteration indirectly
const nodesToRestore = [...highlightTextSet]
for (const highlightTextNode of nodesToRestore) {
if (highlightTextNode.textContent) {
const textNode = document.createTextNode(highlightTextNode.textContent)
const node = highlightTextNode as HTMLElement
if (node.parentNode) {
highlightTextParentNodeSet.add(node.parentNode as HTMLElement)
node.replaceWith(textNode) // This removes the node from the DOM
}
}
}
highlightTextSet.clear() // Clear the original set after processing
for (const parentNode of highlightTextParentNodeSet) {
mergeAdjacentTextNodes(parentNode)
}
// highlightTextSet.clear() // Already cleared
}
const search = (searchTargetIndex?: SearchTargetIndex): number | null => {
const searchText = searchInputRef.current?.value.trim() ?? null const searchText = searchInputRef.current?.value.trim() ?? null
setSearchCompleted(SearchCompletedState.Searched)
if (target && searchText !== null && searchText !== '') { if (target && searchText !== null && searchText !== '') {
restoreHighlight() const ranges = findRangesInTarget(target, filter, searchText, isCaseSensitive, isWholeWord)
const iter = document.createNodeIterator(target, NodeFilter.SHOW_TEXT) setAllRanges(ranges)
let textNode: Node | null setCurrentIndex(0)
const textNodeSet: Set<Node> = new Set()
while ((textNode = iter.nextNode())) {
if (filter(textNode)) {
textNodeSet.add(textNode)
}
}
const highlightTextSetTemp = new Set<HTMLSpanElement>()
for (const node of textNodeSet) {
const list = highlightText(node, searchText, HIGHLIGHT_CLASS, isCaseSensitive, isWholeWord)
if (list) {
list.forEach((node) => highlightTextSetTemp.add(node))
}
}
const highlightTextList = [...highlightTextSetTemp]
setTotalCount(highlightTextList.length)
highlightTextSetTemp.forEach((node) => highlightTextSet.add(node))
const changeIndex = () => {
let index: number
switch (searchTargetIndex) {
case SearchTargetIndex.Next:
{
index = (searchResultIndex + 1) % highlightTextList.length
}
break
case SearchTargetIndex.Prev:
{
index = (searchResultIndex - 1 + highlightTextList.length) % highlightTextList.length
}
break
default: {
index = searchResultIndex
}
}
return Math.max(index, 0)
}
const targetIndex = (() => {
switch (searchCompleted) {
case SearchCompletedState.NotSearched: {
setSearchCompleted(SearchCompletedState.FirstSearched)
const index = findWindowVerticalCenterElementIndex(highlightTextList)
if (index !== null) {
setSearchResultIndex(index)
return index
} else {
setSearchResultIndex(0)
return 0
}
}
case SearchCompletedState.FirstSearched: {
return changeIndex()
}
default: {
return null
}
}
})()
if (targetIndex === null) {
return null
} else {
const totalCount = highlightTextSet.size
if (targetIndex >= totalCount) {
return totalCount - 1
} else {
return targetIndex
}
}
} else {
return null
} }
} }, [target, filter, isCaseSensitive, isWholeWord])
const _searchHandlerDebounce = debounce(() => { const implementation = useMemo(
implementation.search() () => ({
}, 300) disable: () => {
const searchHandler = useCallback(_searchHandlerDebounce, [_searchHandlerDebounce]) setEnableContentSearch(false)
const userInputHandler = (event: React.ChangeEvent<HTMLInputElement>) => { CSS.highlights.clear()
const value = event.target.value.trim() },
if (value.length === 0) { enable: (initialText?: string) => {
restoreHighlight() setEnableContentSearch(true)
setTotalCount(0) if (searchInputRef.current) {
setSearchResultIndex(0) const inputEl = searchInputRef.current
setSearchCompleted(SearchCompletedState.NotSearched) if (initialText && initialText.trim().length > 0) {
} else { inputEl.value = initialText
// 用户输入时允许滚动 requestAnimationFrame(() => {
setShouldScroll(true) inputEl.focus()
searchHandler() inputEl.select()
} search()
prevSearchText.current = value CSS.highlights.clear()
}
const keyDownHandler = (event: React.KeyboardEvent<HTMLInputElement>) => {
const { code, key, shiftKey } = event
if (key === 'Process') {
return
}
switch (code) {
case 'Enter':
{
if (shiftKey) {
implementation.searchPrev()
} else {
implementation.searchNext()
}
event.preventDefault()
}
break
case 'Escape':
{
implementation.disable()
}
break
}
}
const searchInputFocus = () => requestAnimationFrame(() => searchInputRef.current?.focus())
const userOutlinedButtonOnClick = () => {
if (onIncludeUserChange) {
onIncludeUserChange(!includeUser)
}
searchInputFocus()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
const implementation = {
disable() {
setEnableContentSearch(false)
restoreHighlight()
setShouldScroll(false)
},
enable(initialText?: string) {
setEnableContentSearch(true)
setShouldScroll(false) // Default to false, search itself might set it to true
if (searchInputRef.current) {
const inputEl = searchInputRef.current
if (initialText && initialText.trim().length > 0) {
inputEl.value = initialText
// Trigger search after setting initial text
// Need to make sure search() uses the new value
// and also to focus and select
requestAnimationFrame(() => {
inputEl.focus()
inputEl.select()
setShouldScroll(true)
const targetIndex = search()
if (targetIndex !== null) {
locateByIndex(targetIndex, true) // Ensure scrolling
} else {
// If search returns null (e.g., empty input or no matches with initial text), clear state
restoreHighlight()
setTotalCount(0)
setSearchResultIndex(0)
setSearchCompleted(SearchCompletedState.NotSearched) setSearchCompleted(SearchCompletedState.NotSearched)
} })
}) } else {
} else { requestAnimationFrame(() => {
requestAnimationFrame(() => { inputEl.focus()
inputEl.focus() inputEl.select()
inputEl.select() })
})
// Only search if there's existing text and no new initialText
if (inputEl.value.trim()) {
const targetIndex = search()
if (targetIndex !== null) {
setSearchResultIndex(targetIndex)
// locateByIndex(targetIndex, false); // Don't scroll if just enabling with existing text
}
} }
} }
} },
}, searchNext: () => {
searchNext() { if (allRanges.length > 0) {
if (enableContentSearch) { setCurrentIndex((prev) => (prev < allRanges.length - 1 ? prev + 1 : 0))
const targetIndex = search(SearchTargetIndex.Next)
if (targetIndex !== null) {
locateByIndex(targetIndex)
} }
} },
}, searchPrev: () => {
searchPrev() { if (allRanges.length > 0) {
if (enableContentSearch) { setCurrentIndex((prev) => (prev > 0 ? prev - 1 : allRanges.length - 1))
const targetIndex = search(SearchTargetIndex.Prev)
if (targetIndex !== null) {
locateByIndex(targetIndex)
} }
} },
}, resetSearchState: () => {
resetSearchState() {
if (enableContentSearch) {
setSearchCompleted(SearchCompletedState.NotSearched) setSearchCompleted(SearchCompletedState.NotSearched)
// Maybe also reset index? Depends on desired behavior },
// setSearchResultIndex(0); search: () => {
search()
locateByIndex(true)
},
silentSearch: () => {
search()
locateByIndex(false)
},
focus: () => {
searchInputRef.current?.focus()
} }
}),
[allRanges.length, locateByIndex, search]
)
const _searchHandlerDebounce = useMemo(() => debounce(implementation.search, 300), [implementation.search])
const searchHandler = useCallback(() => {
_searchHandlerDebounce()
}, [_searchHandlerDebounce])
const userInputHandler = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.value.trim()
if (value.length === 0) {
resetSearch()
} else {
searchHandler()
}
prevSearchText.current = value
}, },
search() { [searchHandler, resetSearch]
if (enableContentSearch) { )
const targetIndex = search()
if (targetIndex !== null) { const keyDownHandler = useCallback(
locateByIndex(targetIndex, shouldScroll) (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter') {
event.preventDefault()
const value = (event.target as HTMLInputElement).value.trim()
if (value.length === 0) {
resetSearch()
return
}
if (event.shiftKey) {
implementation.searchPrev()
} else { } else {
// If search returns null (e.g., empty input), clear state implementation.searchNext()
restoreHighlight()
setTotalCount(0)
setSearchResultIndex(0)
setSearchCompleted(SearchCompletedState.NotSearched)
} }
} else if (event.key === 'Escape') {
implementation.disable()
} }
}, },
silentSearch() { [implementation, resetSearch]
if (enableContentSearch) { )
const targetIndex = search()
if (targetIndex !== null) {
// 只更新索引,不触发滚动
locateByIndex(targetIndex, false)
}
}
},
focus() {
searchInputFocus()
}
}
useImperativeHandle(ref, () => ({ const searchInputFocus = useCallback(() => {
disable() { requestAnimationFrame(() => searchInputRef.current?.focus())
implementation.disable() }, [])
},
enable(initialText?: string) { const userOutlinedButtonOnClick = useCallback(() => {
implementation.enable(initialText) onIncludeUserChange?.(!includeUser)
}, searchInputFocus()
searchNext() { }, [includeUser, onIncludeUserChange, searchInputFocus])
implementation.searchNext()
}, useImperativeHandle(ref, () => implementation, [implementation])
searchPrev() {
implementation.searchPrev() useEffect(() => {
}, locateByIndex()
search() { }, [currentIndex, locateByIndex])
implementation.search()
},
silentSearch() {
implementation.silentSearch()
},
focus() {
implementation.focus()
}
}))
// Re-run search when options change and search is active
useEffect(() => { useEffect(() => {
if (enableContentSearch && searchInputRef.current?.value.trim()) { if (enableContentSearch && searchInputRef.current?.value.trim()) {
implementation.search() search()
} }
}, [isCaseSensitive, isWholeWord, enableContentSearch, implementation]) // Add enableContentSearch dependency }, [isCaseSensitive, isWholeWord, enableContentSearch, search])
const prevButtonOnClick = () => { const prevButtonOnClick = () => {
implementation.searchPrev() implementation.searchPrev()
@ -589,11 +368,11 @@ export const ContentSearch = React.forwardRef<ContentSearchRef, Props>(
<Separator></Separator> <Separator></Separator>
<SearchResults> <SearchResults>
{searchCompleted !== SearchCompletedState.NotSearched ? ( {searchCompleted !== SearchCompletedState.NotSearched ? (
totalCount > 0 ? ( allRanges.length > 0 ? (
<> <>
<SearchResultCount>{searchResultIndex + 1}</SearchResultCount> <SearchResultCount>{currentIndex + 1}</SearchResultCount>
<SearchResultSeparator>/</SearchResultSeparator> <SearchResultSeparator>/</SearchResultSeparator>
<SearchResultTotalCount>{totalCount}</SearchResultTotalCount> <SearchResultTotalCount>{allRanges.length}</SearchResultTotalCount>
</> </>
) : ( ) : (
<NoResults>{t('common.no_results')}</NoResults> <NoResults>{t('common.no_results')}</NoResults>
@ -603,10 +382,10 @@ export const ContentSearch = React.forwardRef<ContentSearchRef, Props>(
)} )}
</SearchResults> </SearchResults>
<ToolBar> <ToolBar>
<ToolbarButton type="text" onClick={prevButtonOnClick} disabled={totalCount === 0}> <ToolbarButton type="text" onClick={prevButtonOnClick} disabled={allRanges.length === 0}>
<ChevronUp size={18} /> <ChevronUp size={18} />
</ToolbarButton> </ToolbarButton>
<ToolbarButton type="text" onClick={nextButtonOnClick} disabled={totalCount === 0}> <ToolbarButton type="text" onClick={nextButtonOnClick} disabled={allRanges.length === 0}>
<ChevronDown size={18} /> <ChevronDown size={18} />
</ToolbarButton> </ToolbarButton>
<ToolbarButton type="text" onClick={closeButtonOnClick}> <ToolbarButton type="text" onClick={closeButtonOnClick}>

View File

@ -55,28 +55,30 @@ const Chat: FC<Props> = (props) => {
} }
}) })
const contentSearchFilter = (node: Node): boolean => { const contentSearchFilter: NodeFilter = {
if (node.parentNode) { acceptNode(node) {
let parentNode: HTMLElement | null = node.parentNode as HTMLElement if (node.parentNode) {
while (parentNode?.parentNode) { let parentNode: HTMLElement | null = node.parentNode as HTMLElement
if (parentNode.classList.contains('MessageFooter')) { while (parentNode?.parentNode) {
return false if (parentNode.classList.contains('MessageFooter')) {
} return NodeFilter.FILTER_REJECT
}
if (filterIncludeUser) { if (filterIncludeUser) {
if (parentNode?.classList.contains('message-content-container')) { if (parentNode?.classList.contains('message-content-container')) {
return true return NodeFilter.FILTER_ACCEPT
} }
} else { } else {
if (parentNode?.classList.contains('message-content-container-assistant')) { if (parentNode?.classList.contains('message-content-container-assistant')) {
return true return NodeFilter.FILTER_ACCEPT
}
} }
parentNode = parentNode.parentNode as HTMLElement
} }
parentNode = parentNode.parentNode as HTMLElement return NodeFilter.FILTER_REJECT
} else {
return NodeFilter.FILTER_REJECT
} }
return false
} else {
return false
} }
} }