diff --git a/src/components/ui/combobox.tsx b/src/components/ui/combobox.tsx index 58a644a..939487e 100644 --- a/src/components/ui/combobox.tsx +++ b/src/components/ui/combobox.tsx @@ -1,7 +1,7 @@ 'use client' import * as React from 'react' -import {CheckIcon, ChevronsUpDown} from 'lucide-react' +import {CheckIcon, ChevronDown, ChevronRight, ChevronsUpDown} from 'lucide-react' import {merge} from '@/lib/utils' import {Button} from '@/components/ui/button' @@ -10,7 +10,7 @@ import { PopoverContent, PopoverTrigger, } from '@/components/ui/popover' -import {ReactNode, useRef, useState} from 'react' +import {ReactNode, useState} from 'react' import {Input} from '@/components/ui/input' type ComboboxItem = { @@ -28,6 +28,7 @@ type ComboboxProps = { value?: string[] onChange?: (value: string[]) => void children?: React.ReactNode + searchPlaceholder?: string } export function Combobox(props: ComboboxProps) { @@ -56,22 +57,51 @@ export function Combobox(props: ComboboxProps) { const [wait, setWait] = useState(false) const [filter, setFilter] = useState('') const [filtered, setFiltered] = useState([]) + const [expandedKeys, setExpandedKeys] = useState>(new Set()) - const onFilter = async () => { + const toggleExpand = (key: string) => { + setExpandedKeys((prev) => { + const next = new Set(prev) + if (next.has(key)) { + next.delete(key) + } + else { + next.add(key) + } + return next + }) + } + + const onSearch = async () => { if (wait) return const cond = filter.trim() - console.log('onFilter', cond) setWait(true) if (cond.length > 0) { - setFiltered(mapFilter(JSON.parse(JSON.stringify(props.options)), cond)) + const result = mapFilter(JSON.parse(JSON.stringify(props.options)), cond) + setFiltered(result) + setExpandedKeys(collectAllKeys(result)) } else { setFiltered(props.options) + setExpandedKeys(new Set()) } - console.log('onFilter end') setWait(false) } + const collectAllKeys = (items: ComboboxItem[]): Set => { + const keys = new Set() + const walk = (list: ComboboxItem[]) => { + list.forEach((item) => { + if (item.children?.length) { + keys.add(item.value) + walk(item.children) + } + }) + } + walk(items) + return keys + } + const mapFilter = (items: ComboboxItem[], cond: string): ComboboxItem[] => { const nItems: ComboboxItem[] = [] items.forEach((item) => { @@ -97,6 +127,7 @@ export function Combobox(props: ComboboxProps) { if (status) { setFiltered(props.options) setFilter('') + setExpandedKeys(new Set()) } }}> @@ -117,18 +148,18 @@ export function Combobox(props: ComboboxProps) {
setFilter(event.target.value)} /> -
@@ -136,11 +167,13 @@ export function Combobox(props: ComboboxProps) { { - console.log(value.map(item => item.value)) - props.onChange?.(value.map(item => item.value)) + expandedKeys={expandedKeys} + onToggleExpand={toggleExpand} + onChange={(pathValue) => { + props.onChange?.(pathValue) setOpen(false) - }}/> + }} + />
@@ -150,51 +183,76 @@ export function Combobox(props: ComboboxProps) { function OptionList(props: { options: ComboboxItem[] value: string[] - onChange: (value: ComboboxItem[]) => void - path?: ComboboxItem[] + expandedKeys: Set + onToggleExpand: (key: string) => void + onChange: (value: string[]) => void + path?: string[] depth?: number }) { const depth = props.depth || 0 - const parent = props.path || [] - const indent = depth * 16 + const parentPath = props.path || [] return ( -
    +
      {props.options.map((item, i) => { - const path = [...parent, item] - const pathValue = path.map(item => item.value) - const equal = pathValue.join(`.`) === props.value?.join('.') + const path = [...parentPath, item.value] + const hasChildren = !!item.children?.length + const expanded = props.expandedKeys.has(item.value) + const isCurrentLevel = path.length === props.value.filter(Boolean).length + const isActivePath = path.every((v, idx) => v === props.value[idx]) + return (
    • - props.onChange?.(path)}/> - {item.children?.length - && - } +
      + {hasChildren ? ( + { + e.stopPropagation() + props.onToggleExpand(item.value) + }} + > + {expanded + ? + : + } + + ) : ( + + )} + props.onChange(path)} + > + {item.label} + + {isCurrentLevel && isActivePath && ( + + )} +
      + {hasChildren && expanded && ( + + )}
    • ) })}
    ) } - -function OptionItem(props: { - key: string - item: ComboboxItem - active: boolean - onChange?: () => void -}) { - return ( -
    - {props.item.label} -
    - ) -}