♻️ 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)
}
}
if (matches.length === 0) {
return null
}
const parentNode = textNode.parentNode
if (!parentNode) {
return null
}
const fragment = document.createDocumentFragment()
let currentIndex = 0
const highlightTextSet = new Set<HTMLSpanElement>()
matches.forEach(({ index, length, text }) => {
if (index > currentIndex) {
fragment.appendChild(document.createTextNode(textContent.substring(currentIndex, index)))
}
const highlightSpan = document.createElement('span')
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
}) })
fullText += treeWalker.currentNode.nodeValue
if (currentIndex < textContent.length) {
fragment.appendChild(document.createTextNode(textContent.substring(currentIndex)))
} }
parentNode.replaceChild(fragment, textNode) // 2.在完整文本中查找匹配项
return [...highlightTextSet] let match: RegExpExecArray | null = null
} while ((match = searchRegex.exec(fullText))) {
const matchStart = match.index
const matchEnd = matchStart + match[0].length
const mergeAdjacentTextNodes = (node: HTMLElement) => { // 3. 将匹配项的索引映射回DOM Range
const children = Array.from(node.childNodes) let startNode: Node | null = null
const groups: Array<Node | { text: string; nodes: Node[] }> = [] let endNode: Node | null = null
let currentTextGroup: { text: string; nodes: Node[] } | null = null let startOffset = 0
let endOffset = 0
for (const child of children) { // 找到起始节点和偏移
if (child.nodeType === Node.TEXT_NODE) { for (const nodeInfo of allTextNodes) {
if (currentTextGroup === null) { if (
currentTextGroup = { matchStart >= nodeInfo.startOffset &&
text: child.textContent ?? '', matchStart < nodeInfo.startOffset + (nodeInfo.node.nodeValue?.length ?? 0)
nodes: [child] ) {
} startNode = nodeInfo.node
} else { startOffset = matchStart - nodeInfo.startOffset
currentTextGroup.text += child.textContent break
currentTextGroup.nodes.push(child)
}
} else {
if (currentTextGroup !== null) {
groups.push(currentTextGroup!)
currentTextGroup = null
}
groups.push(child)
} }
} }
if (currentTextGroup !== null) { // 找到结束节点和偏移
groups.push(currentTextGroup) 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
}
} }
const newChildren = groups.map((group) => { // 如果起始和结束节点都找到了,则创建一个 Range
if (group instanceof Node) { if (startNode && endNode) {
return group const range = new Range()
} else { range.setStart(startNode, startOffset)
return document.createTextNode(group.text) range.setEnd(endNode, endOffset)
ranges.push(range)
}
} }
})
node.replaceChildren(...newChildren) return ranges
} }
// 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) => {
const { top: aTop } = a.getBoundingClientRect()
const { top: bTop } = b.getBoundingClientRect()
return aTop - bTop
})
for (const node of highlightTextNodes) {
node.classList.remove(HIGHLIGHT_SELECT_CLASS)
}
setSearchResultIndex(index)
if (highlightTextNodes.length > 0) {
const highlightTextNode = highlightTextNodes[index] ?? null
if (highlightTextNode) {
highlightTextNode.classList.add(HIGHLIGHT_SELECT_CLASS)
if (shouldScroll) {
highlightTextNode.scrollIntoView({
behavior: 'smooth',
block: 'center'
// inline: 'center' 水平方向居中可能会导致 content 页面整体偏右, 使得左半部的内容被遮挡. 因此先注释掉该代码
})
}
}
}
}
}
const restoreHighlight = () => {
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
if (target && searchText !== null && searchText !== '') {
restoreHighlight()
const iter = document.createNodeIterator(target, NodeFilter.SHOW_TEXT)
let textNode: Node | null
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
}
}
const _searchHandlerDebounce = debounce(() => {
implementation.search()
}, 300)
const searchHandler = useCallback(_searchHandlerDebounce, [_searchHandlerDebounce])
const userInputHandler = (event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.value.trim()
if (value.length === 0) {
restoreHighlight()
setTotalCount(0)
setSearchResultIndex(0)
setSearchCompleted(SearchCompletedState.NotSearched) setSearchCompleted(SearchCompletedState.NotSearched)
} else { }, [])
// 用户输入时允许滚动
setShouldScroll(true)
searchHandler()
}
prevSearchText.current = value
}
const keyDownHandler = (event: React.KeyboardEvent<HTMLInputElement>) => { const locateByIndex = useCallback(
const { code, key, shiftKey } = event (shouldScroll = true) => {
if (key === 'Process') { // 清理旧的高亮
return CSS.highlights.clear()
}
switch (code) { if (allRanges.length > 0) {
case 'Enter': // 1. 创建并注册所有匹配项的高亮
{ const allMatchesHighlight = new Highlight(...allRanges)
if (shiftKey) { CSS.highlights.set('search-matches', allMatchesHighlight)
implementation.searchPrev()
} else {
implementation.searchNext()
}
event.preventDefault()
}
break
case 'Escape':
{
implementation.disable()
}
break
}
}
const searchInputFocus = () => requestAnimationFrame(() => searchInputRef.current?.focus()) // 2. 如果有当前项,为其创建并注册一个特殊的高亮
if (currentIndex !== -1 && allRanges[currentIndex]) {
const currentMatchRange = allRanges[currentIndex]
const currentMatchHighlight = new Highlight(currentMatchRange)
CSS.highlights.set('current-match', currentMatchHighlight)
const userOutlinedButtonOnClick = () => { // 3. 将当前项滚动到视图中
if (onIncludeUserChange) { // 获取第一个文本节点的父元素来进行滚动
onIncludeUserChange(!includeUser) const parentElement = currentMatchRange.startContainer.parentElement
if (shouldScroll) {
parentElement?.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'nearest'
})
}
} }
searchInputFocus()
} }
// eslint-disable-next-line react-hooks/exhaustive-deps
const implementation = {
disable() {
setEnableContentSearch(false)
restoreHighlight()
setShouldScroll(false)
}, },
enable(initialText?: string) { [allRanges, currentIndex]
)
const search = useCallback(() => {
const searchText = searchInputRef.current?.value.trim() ?? null
setSearchCompleted(SearchCompletedState.Searched)
if (target && searchText !== null && searchText !== '') {
const ranges = findRangesInTarget(target, filter, searchText, isCaseSensitive, isWholeWord)
setAllRanges(ranges)
setCurrentIndex(0)
}
}, [target, filter, isCaseSensitive, isWholeWord])
const implementation = useMemo(
() => ({
disable: () => {
setEnableContentSearch(false)
CSS.highlights.clear()
},
enable: (initialText?: string) => {
setEnableContentSearch(true) setEnableContentSearch(true)
setShouldScroll(false) // Default to false, search itself might set it to true
if (searchInputRef.current) { if (searchInputRef.current) {
const inputEl = searchInputRef.current const inputEl = searchInputRef.current
if (initialText && initialText.trim().length > 0) { if (initialText && initialText.trim().length > 0) {
inputEl.value = initialText 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(() => { requestAnimationFrame(() => {
inputEl.focus() inputEl.focus()
inputEl.select() inputEl.select()
setShouldScroll(true) search()
const targetIndex = search() CSS.highlights.clear()
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 (enableContentSearch) { if (allRanges.length > 0) {
const targetIndex = search(SearchTargetIndex.Next) setCurrentIndex((prev) => (prev < allRanges.length - 1 ? prev + 1 : 0))
if (targetIndex !== null) {
locateByIndex(targetIndex)
}
} }
}, },
searchPrev() { searchPrev: () => {
if (enableContentSearch) { if (allRanges.length > 0) {
const targetIndex = search(SearchTargetIndex.Prev) setCurrentIndex((prev) => (prev > 0 ? prev - 1 : allRanges.length - 1))
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: () => {
if (enableContentSearch) { search()
const targetIndex = search() locateByIndex(true)
if (targetIndex !== null) {
locateByIndex(targetIndex, shouldScroll)
} else {
// If search returns null (e.g., empty input), clear state
restoreHighlight()
setTotalCount(0)
setSearchResultIndex(0)
setSearchCompleted(SearchCompletedState.NotSearched)
}
}
}, },
silentSearch() { silentSearch: () => {
if (enableContentSearch) { search()
const targetIndex = search() locateByIndex(false)
if (targetIndex !== null) {
// 只更新索引,不触发滚动
locateByIndex(targetIndex, false)
}
}
}, },
focus() { focus: () => {
searchInputFocus() searchInputRef.current?.focus()
}
} }
}),
[allRanges.length, locateByIndex, search]
)
useImperativeHandle(ref, () => ({ const _searchHandlerDebounce = useMemo(() => debounce(implementation.search, 300), [implementation.search])
disable() {
implementation.disable() 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
}, },
enable(initialText?: string) { [searchHandler, resetSearch]
implementation.enable(initialText) )
},
searchNext() { const keyDownHandler = useCallback(
implementation.searchNext() (event: React.KeyboardEvent<HTMLInputElement>) => {
}, if (event.key === 'Enter') {
searchPrev() { event.preventDefault()
const value = (event.target as HTMLInputElement).value.trim()
if (value.length === 0) {
resetSearch()
return
}
if (event.shiftKey) {
implementation.searchPrev() implementation.searchPrev()
}, } else {
search() { implementation.searchNext()
implementation.search()
},
silentSearch() {
implementation.silentSearch()
},
focus() {
implementation.focus()
} }
})) } else if (event.key === 'Escape') {
implementation.disable()
}
},
[implementation, resetSearch]
)
const searchInputFocus = useCallback(() => {
requestAnimationFrame(() => searchInputRef.current?.focus())
}, [])
const userOutlinedButtonOnClick = useCallback(() => {
onIncludeUserChange?.(!includeUser)
searchInputFocus()
}, [includeUser, onIncludeUserChange, searchInputFocus])
useImperativeHandle(ref, () => implementation, [implementation])
useEffect(() => {
locateByIndex()
}, [currentIndex, locateByIndex])
// 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 = {
acceptNode(node) {
if (node.parentNode) { if (node.parentNode) {
let parentNode: HTMLElement | null = node.parentNode as HTMLElement let parentNode: HTMLElement | null = node.parentNode as HTMLElement
while (parentNode?.parentNode) { while (parentNode?.parentNode) {
if (parentNode.classList.contains('MessageFooter')) { if (parentNode.classList.contains('MessageFooter')) {
return false 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 false return NodeFilter.FILTER_REJECT
} else { } else {
return false return NodeFilter.FILTER_REJECT
}
} }
} }