diff --git a/src/components/search.jsx b/src/components/search.jsx new file mode 100644 index 000000000..a53ab59d4 --- /dev/null +++ b/src/components/search.jsx @@ -0,0 +1,93 @@ +import { useTranslation } from "react-i18next"; +import { useEffect, useState, useRef } from "react"; +import classNames from "classnames"; + +export default function Search({bookmarks, services, searchString, setSearchString, isOpen, close}) { + const { t, i18n } = useTranslation(); + const all = [...bookmarks.map(bg => bg.bookmarks).flat(), ...services.map(sg => sg.services).flat()]; + + const searchField = useRef(); + + const [results, setResults] = useState([]); + const [currentItemIndex, setCurrentItemIndex] = useState(null); + + function handleSearchChange(event) { + setSearchString(event.target.value.toLowerCase()) + } + + function handleSearchKeyDown(event) { + if (event.key === "Escape") { + setSearchString(""); + close(false); + } else if (event.key === "Enter" && results.length) { + setSearchString(""); + close(false); + const result = results[currentItemIndex]; + console.log("go to", result); + window.open(result.href, '_blank'); + } 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(); + } + } + + useEffect(() => { + if (searchString.length === 0) setResults([]); + else { + const newResults = all.filter(r => r.name.toLowerCase().includes(searchString)); + setResults(newResults); + if (newResults.length) { + setCurrentItemIndex(0); + } + } + }, [searchString]) + + + const [hidden, setHidden] = useState(true); + useEffect(() => { + if (isOpen) { + searchField.current.focus(); + setHidden(false); + } else { + setTimeout(() => { + setHidden(true); + }, 300); // disable on close + } + }, [isOpen]) + + return ( + + ); +} diff --git a/src/pages/index.jsx b/src/pages/index.jsx index b4eb50e18..3d6a2298b 100644 --- a/src/pages/index.jsx +++ b/src/pages/index.jsx @@ -21,6 +21,7 @@ import { SettingsContext } from "utils/contexts/settings"; import { bookmarksResponse, servicesResponse, widgetsResponse } from "utils/config/api-response"; import ErrorBoundary from "components/errorboundry"; import themes from "utils/styles/themes"; +import Search from "components/search"; const ThemeToggle = dynamic(() => import("components/toggles/theme"), { ssr: false, @@ -160,6 +161,11 @@ const headerStyles = { clean: "m-4 mb-0 sm:m-8 sm:mb-0", }; +function handleChange(event) { + // this.setState({value: event.target.value}); + console.log(event); +} + function Home({ initialSettings }) { const { i18n } = useTranslation(); const { theme, setTheme } = useContext(ThemeContext); @@ -188,6 +194,29 @@ function Home({ initialSettings }) { } }, [i18n, settings, color, setColor, theme, setTheme]); + const [searching, setSearching] = useState(false); + const [searchString, setSearchString] = useState(false); + + useEffect(() => { + document.addEventListener('keydown', handleKeyDown); + + function handleKeyDown(e) { + console.log(e.target.tagName, e.key, e); + if (e.target.tagName === "BODY") { + if (String.fromCharCode(e.keyCode).match(/(\w|\s)/g) && !(e. altKey || e.ctrlKey || e.metaKey || e.shiftKey)) { + setSearching(true); + } else if (e.key === "Escape") { + setSearchString(""); + setSearching(false); + } + } + } + + return function cleanup() { + document.removeEventListener('keydown', handleKeyDown); + } + }) + return ( <> @@ -211,6 +240,7 @@ function Home({ initialSettings }) { headerStyles[initialSettings.headerStyle || "underlined"] )} > + {widgets && ( <> {widgets