import { useTranslation } from "react-i18next"; import { useEffect, useState, useRef, useCallback, useContext } from "react"; import classNames from "classnames"; import ResolvedIcon from "./resolvedicon"; import { SettingsContext } from "utils/contexts/settings"; export default function QuickLaunch({ servicesAndBookmarks, searchString, setSearchString, isOpen, close, searchProvider, }) { const { t } = useTranslation(); const { settings } = useContext(SettingsContext); const { searchDescriptions, hideVisitURL } = settings?.quicklaunch ? settings.quicklaunch : { searchDescriptions: false, hideVisitURL: false }; const searchField = useRef(); const [results, setResults] = useState([]); const [currentItemIndex, setCurrentItemIndex] = useState(null); const [url, setUrl] = useState(null); function openCurrentItem(newWindow) { const result = results[currentItemIndex]; window.open(result.href, newWindow ? "_blank" : result.target ?? settings.target ?? "_blank", "noreferrer"); } const closeAndReset = useCallback(() => { close(false); setTimeout(() => { setSearchString(""); setCurrentItemIndex(null); }, 200); // delay a little for animations }, [close, setSearchString, setCurrentItemIndex]); function handleSearchChange(event) { const rawSearchString = event.target.value.toLowerCase(); try { if (!/.+[.:].+/g.test(rawSearchString)) throw new Error(); // basic test for probably a url let urlString = rawSearchString; if (urlString.indexOf("http") !== 0) urlString = `https://${rawSearchString}`; setUrl(new URL(urlString)); // basic validation } catch (e) { setUrl(null); } setSearchString(rawSearchString); } function handleSearchKeyDown(event) { if (!isOpen) return; if (event.key === "Escape") { closeAndReset(); event.preventDefault(); } 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); } function handleItemKeyDown(event) { if (!isOpen) return; // native button handles other keys if (event.key === "Escape") { closeAndReset(); event.preventDefault(); } } 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); } if (searchProvider) { newResults.push({ href: searchProvider.url + encodeURIComponent(searchString), name: `${searchProvider.name ?? t("quicklaunch.custom")} ${t("quicklaunch.search")} `, type: "search", }); } if (!hideVisitURL && url) { newResults.unshift({ href: url.toString(), name: `${t("quicklaunch.visit")} URL`, type: "url", }); } setResults(newResults); if (newResults.length) { setCurrentItemIndex(0); } } }, [searchString, servicesAndBookmarks, searchDescriptions, hideVisitURL, searchProvider, url, t]); 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); searchField.current.blur(); setTimeout(() => { setHidden(true); }, 300); // disable on close } }, [isOpen, closeAndReset]); function highlightText(text) { const parts = text.split(new RegExp(`(${searchString})`, "gi")); return ( {parts.map((part, i) => part.toLowerCase() === searchString.toLowerCase() ? ( // eslint-disable-next-line react/no-array-index-key {part} ) : ( part ), )} ); } return (