2022-10-18 23:10:44 -07:00
|
|
|
import { useTranslation } from "react-i18next";
|
2022-10-19 23:07:01 -07:00
|
|
|
import { useEffect, useState, useRef, useCallback, useContext } from "react";
|
2022-10-18 23:10:44 -07:00
|
|
|
import classNames from "classnames";
|
2022-10-19 00:54:35 -07:00
|
|
|
|
2022-11-04 17:38:33 -04:00
|
|
|
import ResolvedIcon from "./resolvedicon";
|
2022-10-18 23:10:44 -07:00
|
|
|
|
2022-10-19 23:07:01 -07:00
|
|
|
import { SettingsContext } from "utils/contexts/settings";
|
|
|
|
|
2023-10-17 23:26:55 -07:00
|
|
|
export default function QuickLaunch({
|
|
|
|
servicesAndBookmarks,
|
|
|
|
searchString,
|
|
|
|
setSearchString,
|
|
|
|
isOpen,
|
|
|
|
close,
|
|
|
|
searchProvider,
|
|
|
|
}) {
|
2022-10-19 00:54:35 -07:00
|
|
|
const { t } = useTranslation();
|
2022-10-19 23:07:01 -07:00
|
|
|
const { settings } = useContext(SettingsContext);
|
2023-10-17 23:26:55 -07:00
|
|
|
const { searchDescriptions, hideVisitURL } = settings?.quicklaunch
|
|
|
|
? settings.quicklaunch
|
|
|
|
: { searchDescriptions: false, hideVisitURL: false };
|
2022-10-18 23:10:44 -07:00
|
|
|
|
|
|
|
const searchField = useRef();
|
|
|
|
|
|
|
|
const [results, setResults] = useState([]);
|
|
|
|
const [currentItemIndex, setCurrentItemIndex] = useState(null);
|
2023-02-27 18:21:24 -08:00
|
|
|
const [url, setUrl] = useState(null);
|
2022-10-18 23:10:44 -07:00
|
|
|
|
2022-10-19 23:07:01 -07:00
|
|
|
function openCurrentItem(newWindow) {
|
2022-10-19 14:23:51 -07:00
|
|
|
const result = results[currentItemIndex];
|
2023-10-17 23:26:55 -07:00
|
|
|
window.open(result.href, newWindow ? "_blank" : result.target ?? settings.target ?? "_blank", "noreferrer");
|
2022-10-19 14:23:51 -07:00
|
|
|
}
|
|
|
|
|
2022-10-19 20:16:29 -07:00
|
|
|
const closeAndReset = useCallback(() => {
|
2022-10-19 09:04:34 -07:00
|
|
|
close(false);
|
2022-10-19 20:16:29 -07:00
|
|
|
setTimeout(() => {
|
|
|
|
setSearchString("");
|
2022-10-19 23:07:01 -07:00
|
|
|
setCurrentItemIndex(null);
|
2022-10-19 20:16:29 -07:00
|
|
|
}, 200); // delay a little for animations
|
2022-10-19 23:07:01 -07:00
|
|
|
}, [close, setSearchString, setCurrentItemIndex]);
|
2022-10-19 09:04:34 -07:00
|
|
|
|
2022-10-18 23:10:44 -07:00
|
|
|
function handleSearchChange(event) {
|
2023-02-27 18:21:24 -08:00
|
|
|
const rawSearchString = event.target.value.toLowerCase();
|
|
|
|
try {
|
|
|
|
if (!/.+[.:].+/g.test(rawSearchString)) throw new Error(); // basic test for probably a url
|
|
|
|
let urlString = rawSearchString;
|
2023-10-17 23:26:55 -07:00
|
|
|
if (urlString.indexOf("http") !== 0) urlString = `https://${rawSearchString}`;
|
2023-02-27 18:21:24 -08:00
|
|
|
setUrl(new URL(urlString)); // basic validation
|
|
|
|
} catch (e) {
|
|
|
|
setUrl(null);
|
|
|
|
}
|
|
|
|
setSearchString(rawSearchString);
|
2022-10-18 23:10:44 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
function handleSearchKeyDown(event) {
|
2022-10-21 22:02:36 -07:00
|
|
|
if (!isOpen) return;
|
2023-01-30 10:16:45 +02:00
|
|
|
|
2022-10-18 23:10:44 -07:00
|
|
|
if (event.key === "Escape") {
|
2022-10-19 20:16:29 -07:00
|
|
|
closeAndReset();
|
2022-10-21 22:02:36 -07:00
|
|
|
event.preventDefault();
|
2022-10-18 23:10:44 -07:00
|
|
|
} else if (event.key === "Enter" && results.length) {
|
2022-10-19 20:16:29 -07:00
|
|
|
closeAndReset();
|
2022-10-19 23:07:01 -07:00
|
|
|
openCurrentItem(event.metaKey);
|
2022-10-19 00:54:35 -07:00
|
|
|
} else if (event.key === "ArrowDown" && results[currentItemIndex + 1]) {
|
2022-10-18 23:10:44 -07:00
|
|
|
setCurrentItemIndex(currentItemIndex + 1);
|
|
|
|
event.preventDefault();
|
2022-10-19 00:54:35 -07:00
|
|
|
} else if (event.key === "ArrowUp" && currentItemIndex > 0) {
|
2022-10-18 23:10:44 -07:00
|
|
|
setCurrentItemIndex(currentItemIndex - 1);
|
|
|
|
event.preventDefault();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-19 13:55:18 -07:00
|
|
|
function handleItemHover(event) {
|
2022-10-19 17:10:01 -07:00
|
|
|
setCurrentItemIndex(parseInt(event.target?.dataset?.index, 10));
|
2022-10-19 13:55:18 -07:00
|
|
|
}
|
|
|
|
|
2022-10-19 23:07:01 -07:00
|
|
|
function handleItemClick(event) {
|
2022-10-19 20:16:29 -07:00
|
|
|
closeAndReset();
|
2022-10-19 23:07:01 -07:00
|
|
|
openCurrentItem(event.metaKey);
|
2022-10-19 09:04:34 -07:00
|
|
|
}
|
|
|
|
|
2023-08-29 09:28:23 -07:00
|
|
|
function handleItemKeyDown(event) {
|
|
|
|
if (!isOpen) return;
|
|
|
|
|
|
|
|
// native button handles other keys
|
|
|
|
if (event.key === "Escape") {
|
|
|
|
closeAndReset();
|
|
|
|
event.preventDefault();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-18 23:10:44 -07:00
|
|
|
useEffect(() => {
|
|
|
|
if (searchString.length === 0) setResults([]);
|
|
|
|
else {
|
2023-10-17 23:26:55 -07:00
|
|
|
let newResults = servicesAndBookmarks.filter((r) => {
|
2022-10-20 11:12:47 -07:00
|
|
|
const nameMatch = r.name.toLowerCase().includes(searchString);
|
|
|
|
let descriptionMatch;
|
|
|
|
if (searchDescriptions) {
|
2023-10-17 23:26:55 -07:00
|
|
|
descriptionMatch = r.description?.toLowerCase().includes(searchString);
|
|
|
|
r.priority = nameMatch ? 2 * +nameMatch : +descriptionMatch; // eslint-disable-line no-param-reassign
|
2022-10-20 11:12:47 -07:00
|
|
|
}
|
|
|
|
return nameMatch || descriptionMatch;
|
|
|
|
});
|
|
|
|
|
|
|
|
if (searchDescriptions) {
|
|
|
|
newResults = newResults.sort((a, b) => b.priority - a.priority);
|
|
|
|
}
|
2023-03-03 12:41:33 +01:00
|
|
|
|
2023-01-30 23:38:37 -08:00
|
|
|
if (searchProvider) {
|
2023-10-17 23:26:55 -07:00
|
|
|
newResults.push({
|
|
|
|
href: searchProvider.url + encodeURIComponent(searchString),
|
|
|
|
name: `${searchProvider.name ?? t("quicklaunch.custom")} ${t("quicklaunch.search")} `,
|
|
|
|
type: "search",
|
|
|
|
});
|
2023-01-30 23:38:37 -08:00
|
|
|
}
|
2022-10-20 11:12:47 -07:00
|
|
|
|
2023-02-27 18:21:24 -08:00
|
|
|
if (!hideVisitURL && url) {
|
2023-10-17 23:26:55 -07:00
|
|
|
newResults.unshift({
|
|
|
|
href: url.toString(),
|
|
|
|
name: `${t("quicklaunch.visit")} URL`,
|
|
|
|
type: "url",
|
|
|
|
});
|
2023-02-27 18:21:24 -08:00
|
|
|
}
|
|
|
|
|
2022-10-18 23:10:44 -07:00
|
|
|
setResults(newResults);
|
2022-10-20 11:12:47 -07:00
|
|
|
|
2022-10-18 23:10:44 -07:00
|
|
|
if (newResults.length) {
|
|
|
|
setCurrentItemIndex(0);
|
|
|
|
}
|
|
|
|
}
|
2023-02-27 18:21:24 -08:00
|
|
|
}, [searchString, servicesAndBookmarks, searchDescriptions, hideVisitURL, searchProvider, url, t]);
|
2022-10-18 23:10:44 -07:00
|
|
|
|
|
|
|
const [hidden, setHidden] = useState(true);
|
|
|
|
useEffect(() => {
|
2022-10-19 20:16:29 -07:00
|
|
|
function handleBackdropClick(event) {
|
|
|
|
if (event.target?.tagName === "DIV") closeAndReset();
|
|
|
|
}
|
2023-01-30 10:16:45 +02:00
|
|
|
|
2022-10-18 23:10:44 -07:00
|
|
|
if (isOpen) {
|
|
|
|
searchField.current.focus();
|
2023-10-17 23:26:55 -07:00
|
|
|
document.body.addEventListener("click", handleBackdropClick);
|
2022-10-18 23:10:44 -07:00
|
|
|
setHidden(false);
|
|
|
|
} else {
|
2023-10-17 23:26:55 -07:00
|
|
|
document.body.removeEventListener("click", handleBackdropClick);
|
2022-10-21 22:02:36 -07:00
|
|
|
searchField.current.blur();
|
2022-10-18 23:10:44 -07:00
|
|
|
setTimeout(() => {
|
|
|
|
setHidden(true);
|
|
|
|
}, 300); // disable on close
|
|
|
|
}
|
2022-10-19 20:16:29 -07:00
|
|
|
}, [isOpen, closeAndReset]);
|
2022-10-18 23:10:44 -07:00
|
|
|
|
2022-10-20 11:12:47 -07:00
|
|
|
function highlightText(text) {
|
2023-10-17 23:26:55 -07:00
|
|
|
const parts = text.split(new RegExp(`(${searchString})`, "gi"));
|
|
|
|
return (
|
|
|
|
<span>
|
|
|
|
{parts.map((part, i) =>
|
|
|
|
part.toLowerCase() === searchString.toLowerCase() ? (
|
|
|
|
// eslint-disable-next-line react/no-array-index-key
|
|
|
|
<span key={`${searchString}_${i}`} className="bg-theme-300/10">
|
|
|
|
{part}
|
|
|
|
</span>
|
|
|
|
) : (
|
|
|
|
part
|
|
|
|
),
|
|
|
|
)}
|
|
|
|
</span>
|
|
|
|
);
|
2022-10-20 11:12:47 -07:00
|
|
|
}
|
|
|
|
|
2022-10-18 23:10:44 -07:00
|
|
|
return (
|
2023-10-17 23:26:55 -07:00
|
|
|
<div
|
|
|
|
className={classNames(
|
|
|
|
"relative z-40 ease-in-out duration-300 transition-opacity",
|
|
|
|
hidden && !isOpen && "hidden",
|
|
|
|
!hidden && isOpen && "opacity-100",
|
|
|
|
!isOpen && "opacity-0",
|
|
|
|
)}
|
|
|
|
role="dialog"
|
|
|
|
aria-modal="true"
|
|
|
|
>
|
2022-10-19 00:54:35 -07:00
|
|
|
<div className="fixed inset-0 bg-gray-500 bg-opacity-50" />
|
2022-11-29 13:26:35 -08:00
|
|
|
<div className="fixed inset-0 z-20 overflow-y-auto">
|
2022-10-18 23:10:44 -07:00
|
|
|
<div className="flex min-h-full min-w-full items-start justify-center text-center">
|
2022-10-19 20:16:29 -07:00
|
|
|
<dialog className="mt-[10%] min-w-[80%] max-w-[90%] md:min-w-[40%] rounded-md p-0 block font-medium text-theme-700 dark:text-theme-200 dark:hover:text-theme-300 shadow-md shadow-theme-900/10 dark:shadow-theme-900/20 bg-theme-50 dark:bg-theme-800">
|
2023-10-17 23:26:55 -07:00
|
|
|
<input
|
|
|
|
placeholder="Search"
|
|
|
|
className={classNames(
|
|
|
|
results.length > 0 && "rounded-t-md",
|
|
|
|
results.length === 0 && "rounded-md",
|
|
|
|
"w-full p-4 m-0 border-0 border-b border-slate-700 focus:border-slate-700 focus:outline-0 focus:ring-0 text-sm md:text-xl text-theme-700 dark:text-theme-200 bg-theme-60 dark:bg-theme-800",
|
|
|
|
)}
|
|
|
|
type="text"
|
|
|
|
autoCorrect="false"
|
|
|
|
ref={searchField}
|
|
|
|
value={searchString}
|
|
|
|
onChange={handleSearchChange}
|
|
|
|
onKeyDown={handleSearchKeyDown}
|
|
|
|
/>
|
|
|
|
{results.length > 0 && (
|
|
|
|
<ul className="max-h-[60vh] overflow-y-auto m-2">
|
|
|
|
{results.map((r, i) => (
|
|
|
|
<li key={r.container ?? r.app ?? `${r.name}-${r.href}`}>
|
|
|
|
<button
|
|
|
|
type="button"
|
|
|
|
data-index={i}
|
|
|
|
onMouseEnter={handleItemHover}
|
|
|
|
onClick={handleItemClick}
|
|
|
|
onKeyDown={handleItemKeyDown}
|
|
|
|
className={classNames(
|
|
|
|
"flex flex-row w-full items-center justify-between rounded-md text-sm md:text-xl py-2 px-4 cursor-pointer text-theme-700 dark:text-theme-200",
|
|
|
|
i === currentItemIndex && "bg-theme-300/50 dark:bg-theme-700/50",
|
|
|
|
)}
|
|
|
|
>
|
|
|
|
<div className="flex flex-row items-center mr-4 pointer-events-none">
|
|
|
|
{(r.icon || r.abbr) && (
|
|
|
|
<div className="w-5 text-xs mr-4">
|
|
|
|
{r.icon && <ResolvedIcon icon={r.icon} />}
|
|
|
|
{r.abbr && r.abbr}
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
<div className="flex flex-col md:flex-row text-left items-baseline mr-4 pointer-events-none">
|
|
|
|
<span className="mr-4">{r.name}</span>
|
|
|
|
{r.description && (
|
|
|
|
<span className="text-xs text-theme-600 text-light">
|
|
|
|
{searchDescriptions && r.priority < 2 ? highlightText(r.description) : r.description}
|
|
|
|
</span>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div className="text-xs text-theme-600 font-bold pointer-events-none">
|
|
|
|
{t(`quicklaunch.${r.type ? r.type.toLowerCase() : "bookmark"}`)}
|
2022-10-19 17:10:01 -07:00
|
|
|
</div>
|
2023-10-17 23:26:55 -07:00
|
|
|
</button>
|
|
|
|
</li>
|
|
|
|
))}
|
|
|
|
</ul>
|
|
|
|
)}
|
2022-10-19 20:16:29 -07:00
|
|
|
</dialog>
|
2022-10-18 23:10:44 -07:00
|
|
|
</div>
|
|
|
|
</div>
|
2022-10-19 00:54:35 -07:00
|
|
|
</div>
|
2022-10-18 23:10:44 -07:00
|
|
|
);
|
|
|
|
}
|