"use client"; import React, { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState, } from "react"; import Link from "next/link"; import { Button } from "@/components/ui/button"; import { Command, CommandGroup, CommandInput, CommandItem, CommandList, } from "@/components/ui/command"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; import { useDoBookmarkSearch } from "@/lib/hooks/bookmark-search"; import { useTranslation } from "@/lib/i18n/client"; import { cn } from "@/lib/utils"; import { History } from "lucide-react"; import { useSearchHistory } from "@karakeep/shared-react/hooks/search-history"; import { EditListModal } from "../lists/EditListModal"; import QueryExplainerTooltip from "./QueryExplainerTooltip"; const MAX_DISPLAY_SUGGESTIONS = 5; function useFocusSearchOnKeyPress( inputRef: React.RefObject, value: string, setValue: (value: string) => void, setPopoverOpen: React.Dispatch>, ) { useEffect(() => { function handleKeyPress(e: KeyboardEvent) { if (!inputRef.current) { return; } if ((e.metaKey || e.ctrlKey) && e.code === "KeyK") { e.preventDefault(); inputRef.current.focus(); // Move the cursor to the end of the input field, so you can continue typing const length = inputRef.current.value.length; inputRef.current.setSelectionRange(length, length); setPopoverOpen(true); } if (e.code === "Escape" && e.target == inputRef.current && value !== "") { e.preventDefault(); inputRef.current.blur(); setValue(""); } } document.addEventListener("keydown", handleKeyPress); return () => { document.removeEventListener("keydown", handleKeyPress); }; }, [inputRef, value, setValue, setPopoverOpen]); } const SearchInput = React.forwardRef< HTMLInputElement, React.HTMLAttributes & { loading?: boolean } >(({ className, ...props }, ref) => { const { t } = useTranslation(); const { debounceSearch, searchQuery, doSearch, parsedSearchQuery, isInSearchPage, } = useDoBookmarkSearch(); const { addTerm, history } = useSearchHistory({ getItem: (k: string) => localStorage.getItem(k), setItem: (k: string, v: string) => localStorage.setItem(k, v), removeItem: (k: string) => localStorage.removeItem(k), }); const [value, setValue] = React.useState(searchQuery); const [isPopoverOpen, setIsPopoverOpen] = useState(false); const [newNestedListModalOpen, setNewNestedListModalOpen] = useState(false); const inputRef = useRef(null); const isHistorySelected = useRef(false); const handleValueChange = useCallback( (newValue: string) => { setValue(newValue); debounceSearch(newValue); isHistorySelected.current = false; // Reset flag when user types }, [debounceSearch], ); const suggestions = useMemo(() => { if (value.trim() === "") { // Show recent items when not typing return history.slice(0, MAX_DISPLAY_SUGGESTIONS); } else { // Show filtered items when typing return history .filter((item) => item.toLowerCase().includes(value.toLowerCase())) .slice(0, MAX_DISPLAY_SUGGESTIONS); } }, [history, value]); const isPopoverVisible = isPopoverOpen && suggestions.length > 0; const handleHistorySelect = useCallback( (term: string) => { isHistorySelected.current = true; setValue(term); doSearch(term); addTerm(term); setIsPopoverOpen(false); inputRef.current?.blur(); }, [doSearch], ); const handleCommandKeyDown = useCallback((e: React.KeyboardEvent) => { if (e.key === "Enter") { const selectedItem = document.querySelector( '[cmdk-item][data-selected="true"]', ); const isPlaceholderSelected = selectedItem?.getAttribute("data-value") === "-"; if (!selectedItem || isPlaceholderSelected) { e.preventDefault(); setIsPopoverOpen(false); inputRef.current?.blur(); } } else if (e.key === "Escape") { e.preventDefault(); setIsPopoverOpen(false); inputRef.current?.blur(); } }, []); useFocusSearchOnKeyPress(inputRef, value, setValue, setIsPopoverOpen); useImperativeHandle(ref, () => inputRef.current!); useEffect(() => { if (!isInSearchPage) { setValue(""); } }, [isInSearchPage]); const handleFocus = useCallback(() => { setIsPopoverOpen(true); }, []); const handleBlur = useCallback(() => { // Only add to history if it wasn't a history selection if (value && !isHistorySelected.current) { addTerm(value); } // Reset the flag isHistorySelected.current = false; setIsPopoverOpen(false); }, [value, addTerm]); return (
{parsedSearchQuery.result === "full" && parsedSearchQuery.text.length == 0 && ( )}
e.preventDefault()} onCloseAutoFocus={(e) => e.preventDefault()} > {/* prevent cmdk auto select the first suggestion -> https://github.com/pacocoursey/cmdk/issues/171*/} {suggestions.map((term) => ( handleHistorySelect(term)} onMouseDown={() => { isHistorySelected.current = true; }} className="cursor-pointer" > {term} ))}
); }); SearchInput.displayName = "SearchInput"; export { SearchInput };