import React, {
|
type ChangeEvent,
|
type KeyboardEvent,
|
useCallback,
|
useEffect,
|
useImperativeHandle,
|
useRef,
|
useState,
|
} from "react";
|
|
import "./TaxonomySearch.scss";
|
import { cn } from "../../utils/bem";
|
import type { AntTaxonomyItem } from "./NewTaxonomy";
|
import debounce from "lodash/debounce";
|
|
type TaxonomySearchProps = {
|
treeData: AntTaxonomyItem[];
|
onChange: (list: AntTaxonomyItem[], expandedKeys: React.Key[] | null) => void;
|
};
|
|
export type TaxonomySearchRef = {
|
resetValue: () => void;
|
focus: () => void;
|
};
|
|
const TaxonomySearch = React.forwardRef<TaxonomySearchRef, TaxonomySearchProps>(({ treeData, onChange }, ref) => {
|
useImperativeHandle(ref, (): TaxonomySearchRef => {
|
return {
|
resetValue() {
|
setInputValue("");
|
onChange(treeData, []);
|
},
|
focus() {
|
return inputRef.current?.focus();
|
},
|
};
|
});
|
|
const inputRef = useRef<HTMLInputElement>();
|
const [inputValue, setInputValue] = useState("");
|
|
useEffect(() => {
|
const _filteredData = filterTreeData(treeData, inputValue);
|
|
onChange(_filteredData.filteredDataTree, null);
|
}, [treeData]);
|
|
// When the treeNode has additional formatting because of `hint` or `color` props,
|
// the `treeNode.title` is not a string but a react component,
|
// so we have to look for the title in children (1 or 2 levels deep)
|
const getTitle = useCallback((treeNodeTitle: any): string => {
|
if (typeof treeNodeTitle === "string") return treeNodeTitle;
|
|
if (typeof treeNodeTitle.props.children === "object") return getTitle(treeNodeTitle.props.children);
|
|
return treeNodeTitle.props.children;
|
}, []);
|
|
// To filter the treeData items that match with the searchValue
|
const filterTreeNode = useCallback((searchValue: string, treeNode: AntTaxonomyItem) => {
|
const lowerSearchValue = String(searchValue).toLowerCase();
|
const lowerResultValue = getTitle(treeNode.title);
|
|
if (!lowerSearchValue) {
|
return false;
|
}
|
|
return String(lowerResultValue).toLowerCase().includes(lowerSearchValue);
|
}, []);
|
|
// It's running recursively through treeData and its children filtering the content that match with the search value
|
const filterTreeData = useCallback((treeData: AntTaxonomyItem[], searchValue: string) => {
|
const _expandedKeys: React.Key[] = [];
|
|
if (!searchValue) {
|
return {
|
filteredDataTree: treeData,
|
expandedKeys: _expandedKeys,
|
};
|
}
|
|
const dig = (list: AntTaxonomyItem[], keepAll = false) => {
|
return list.reduce<AntTaxonomyItem[]>((total, dataNode) => {
|
const children = dataNode.children;
|
|
const match = keepAll || filterTreeNode(searchValue, dataNode);
|
const childList = children?.length ? dig(children, match) : undefined;
|
|
if (match || childList?.length) {
|
if (!keepAll && dataNode.children?.length) _expandedKeys.push(dataNode.key);
|
|
total.push({
|
...dataNode,
|
isLeaf: !childList?.length,
|
children: childList,
|
});
|
}
|
|
return total;
|
}, []);
|
};
|
|
return {
|
filteredDataTree: dig(treeData),
|
expandedKeys: _expandedKeys,
|
};
|
}, []);
|
|
const handleSearch = useCallback(
|
debounce(async (e: ChangeEvent<HTMLInputElement>) => {
|
const _filteredData = filterTreeData(treeData, e.target.value);
|
|
onChange(_filteredData.filteredDataTree, _filteredData.expandedKeys);
|
}, 300),
|
[treeData],
|
);
|
|
return (
|
<input
|
ref={inputRef as any}
|
value={inputValue}
|
onChange={(e: ChangeEvent<HTMLInputElement>) => {
|
setInputValue(e.target.value);
|
handleSearch(e);
|
}}
|
onKeyDown={(e: KeyboardEvent<HTMLInputElement>) => {
|
// to prevent selected items from being deleted
|
if (e.key === "Backspace" || e.key === "Delete") e.stopPropagation();
|
}}
|
placeholder={"搜索"}
|
data-testid={"taxonomy-search"}
|
name={"taxonomy-search-input"}
|
className={cn("taxonomy-search-input").toClassName()}
|
/>
|
);
|
});
|
|
export { TaxonomySearch };
|