import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react' import { createPortal } from 'react-dom' import { collectSkillLeavesFromTree, buildSkillCatalogTree } from '../utils/skillCatalogTree' import SkillTreePickerPanel from './SkillTreePickerPanel' const PANEL_MAX_HEIGHT = 360 const PANEL_MIN_HEIGHT = 140 const PANEL_Z_INDEX = 1100 function normId(id) { return String(id) } /** * Mehrfachauswahl Fähigkeiten mit hierarchischer Treeview („Alle“) und Pfad-Suche. */ export default function SkillTreeMultiSelect({ value = [], onChange, skills = [], placeholder = 'Fähigkeit suchen …', browseLabel = '▼ Katalog', emptyHint = 'Keine Treffer', className = '', usePortal = true, }) { const [query, setQuery] = useState('') const [open, setOpen] = useState(false) const [browseTree, setBrowseTree] = useState(false) const [panelStyle, setPanelStyle] = useState(null) const rootRef = useRef(null) const fieldRef = useRef(null) const panelRef = useRef(null) const tree = useMemo(() => buildSkillCatalogTree(skills), [skills]) const selectedSet = useMemo(() => new Set(value.map(normId)), [value]) const leaves = useMemo(() => collectSkillLeavesFromTree(tree, value), [tree, value]) const selectedLabels = useMemo(() => { return value.map((id) => { const leaf = leaves.find((l) => normId(l.id) === normId(id)) || leaves.find(() => false) const fromSkills = skills.find((s) => normId(s.id) === normId(id)) return leaf?.pathLabel || fromSkills?.name || `#${id}` }) }, [value, leaves, skills]) const addId = useCallback( (id) => { const sid = normId(id) if (selectedSet.has(sid)) return onChange([...value, id]) setQuery('') setBrowseTree(false) }, [value, onChange, selectedSet] ) const removeAt = useCallback( (idx) => { onChange(value.filter((_, i) => i !== idx)) }, [value, onChange] ) useEffect(() => { const onDoc = (e) => { const t = e.target if (rootRef.current?.contains(t) || panelRef.current?.contains(t)) return setOpen(false) setBrowseTree(false) } document.addEventListener('mousedown', onDoc) return () => document.removeEventListener('mousedown', onDoc) }, []) const updatePanelPosition = useCallback(() => { const anchor = fieldRef.current if (!anchor) return const rect = anchor.getBoundingClientRect() const gap = 4 const margin = 12 const spaceBelow = window.innerHeight - rect.bottom - gap - margin const spaceAbove = rect.top - gap - margin const openUp = spaceBelow < PANEL_MIN_HEIGHT && spaceAbove > spaceBelow const available = Math.max(0, openUp ? spaceAbove : spaceBelow) const maxHeight = Math.min(PANEL_MAX_HEIGHT, Math.max(PANEL_MIN_HEIGHT, available)) const top = openUp ? Math.max(margin, rect.top - gap - maxHeight) : rect.bottom + gap setPanelStyle({ position: 'fixed', left: rect.left, width: rect.width, top, maxHeight, zIndex: PANEL_Z_INDEX, }) }, []) useLayoutEffect(() => { if (!open || !usePortal) { setPanelStyle(null) return undefined } updatePanelPosition() window.addEventListener('resize', updatePanelPosition) window.addEventListener('scroll', updatePanelPosition, true) return () => { window.removeEventListener('resize', updatePanelPosition) window.removeEventListener('scroll', updatePanelPosition, true) } }, [open, usePortal, updatePanelPosition, query, browseTree, value.length]) const showTree = browseTree || !query.trim() const panelContent = showTree ? ( addId(id)} pickMode="multi" /> ) : ( ) const dropdownPanel = open && (usePortal ? panelStyle : true) ? (
{panelContent}
) : null return (
{value.map((id, idx) => ( ))}
{ setQuery(e.target.value) setOpen(true) setBrowseTree(false) }} onFocus={() => setOpen(true)} autoComplete="off" aria-expanded={open} />
{!usePortal && dropdownPanel} {usePortal && dropdownPanel ? createPortal(dropdownPanel, document.body) : null}
) }