import { useTranslation } from "react-i18next"; import { useEffect, useState, useRef, useCallback, useContext } from "react"; import classNames from "classnames"; import { resolveIcon } from "./services/item"; import { SettingsContext } from "utils/contexts/settings"; export default function QuickLaunch({servicesAndBookmarks, searchString, setSearchString, isOpen, close, searchDescriptions}) { const { t } = useTranslation(); const { settings } = useContext(SettingsContext); const searchField = useRef(); const [results, setResults] = useState([]); const [currentItemIndex, setCurrentItemIndex] = useState(null); function openCurrentItem(newWindow) { const result = results[currentItemIndex]; window.open(result.href, newWindow ? "_blank" : result.target ?? settings.target ?? "_blank"); } const closeAndReset = useCallback(() => { close(false); setTimeout(() => { setSearchString(""); setCurrentItemIndex(null); }, 200); // delay a little for animations }, [close, setSearchString, setCurrentItemIndex]); function handleSearchChange(event) { setSearchString(event.target.value.toLowerCase()) } function handleSearchKeyDown(event) { if (event.key === "Escape") { closeAndReset(); } else if (event.key === "Enter" && results.length) { closeAndReset(); openCurrentItem(event.metaKey); } else if (event.key === "ArrowDown" && results[currentItemIndex + 1]) { setCurrentItemIndex(currentItemIndex + 1); event.preventDefault(); } else if (event.key === "ArrowUp" && currentItemIndex > 0) { setCurrentItemIndex(currentItemIndex - 1); event.preventDefault(); } } function handleItemHover(event) { setCurrentItemIndex(parseInt(event.target?.dataset?.index, 10)); } function handleItemClick(event) { closeAndReset(); openCurrentItem(event.metaKey); } useEffect(() => { if (searchString.length === 0) setResults([]); else { let newResults = servicesAndBookmarks.filter(r => { const nameMatch = r.name.toLowerCase().includes(searchString); let descriptionMatch; if (searchDescriptions) { descriptionMatch = r.description?.toLowerCase().includes(searchString) r.priority = nameMatch ? 2 * (+nameMatch) : +descriptionMatch; // eslint-disable-line no-param-reassign } return nameMatch || descriptionMatch; }); if (searchDescriptions) { newResults = newResults.sort((a, b) => b.priority - a.priority); } setResults(newResults); if (newResults.length) { setCurrentItemIndex(0); } } }, [searchString, servicesAndBookmarks, searchDescriptions]); const [hidden, setHidden] = useState(true); useEffect(() => { function handleBackdropClick(event) { if (event.target?.tagName === "DIV") closeAndReset(); } if (isOpen) { searchField.current.focus(); document.body.addEventListener('click', handleBackdropClick); setHidden(false); } else { document.body.removeEventListener('click', handleBackdropClick); setTimeout(() => { setHidden(true); }, 300); // disable on close } }, [isOpen, closeAndReset]); function highlightText(text) { const parts = text.split(new RegExp(`(${searchString})`, 'gi')); return {parts.map(part => part.toLowerCase() === searchString.toLowerCase() ? {part} : part)}; } return (