File size: 4,760 Bytes
aa2e6af | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 | import { Search, X } from 'lucide-react';
import React, { useState, useMemo, useCallback } from 'react';
import { useLocalize } from '~/hooks';
import { cn } from '~/utils';
// This is a generic that can be added to Menu and Select components
export default function MultiSearch({
value,
onChange,
placeholder,
className = '',
}: {
value: string | null;
onChange: (filter: string) => void;
placeholder?: string;
className?: string;
}) {
const localize = useLocalize();
const onChangeHandler: React.ChangeEventHandler<HTMLInputElement> = useCallback(
(e) => onChange(e.target.value),
[],
);
return (
<div
className={cn(
'group sticky left-0 top-0 z-10 flex h-12 items-center gap-2 bg-gradient-to-b from-white from-65% to-transparent px-2 px-3 py-2 text-black transition-colors duration-300 focus:bg-gradient-to-b focus:from-white focus:to-white/50 dark:from-gray-700 dark:to-transparent dark:text-white dark:focus:from-white/10 dark:focus:to-white/20',
className,
)}
>
<Search className="h-4 w-4 text-gray-500 transition-colors duration-300 dark:group-focus-within:text-gray-300 dark:group-hover:text-gray-300" />
<input
type="text"
value={value || ''}
onChange={onChangeHandler}
placeholder={placeholder || localize('com_ui_select_search_model')}
className="flex-1 rounded-md border-none bg-transparent px-2.5 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-gray-700/10 dark:focus:ring-gray-200/10"
/>
<div className="relative flex h-5 w-5 items-center justify-end text-gray-500">
<X
className={cn(
'text-gray-500 dark:text-gray-300',
value?.length ? 'cursor-pointer opacity-100' : 'opacity-0',
)}
onClick={() => onChange('')}
/>
</div>
</div>
);
}
/**
* Helper function that will take a multiSearch input
* @param node
*/
function defaultGetStringKey(node: unknown): string {
if (typeof node === 'string') {
// BUGFIX: Detect psedeo separators and make sure they don't appear in the list when filtering items
// it makes sure (for the most part) that the model name starts and ends with dashes
// The long-term fix here would be to enable seperators (model groupings) but there's no
// feature mocks for such a thing yet
if (node.startsWith('---') && node.endsWith('---')) {
return '';
}
return node.toUpperCase();
}
// This should be a noop, but it's here for redundancy
return '';
}
/**
* Hook for conditionally making a multi-element list component into a sortable component
* Returns a RenderNode for search input when search functionality is available
* @param availableOptions
* @param placeholder
* @param getTextKeyOverride
* @param className - Additional classnames to add to the search container
* @param disabled - If the search should be disabled
* @returns
*/
export function useMultiSearch<OptionsType extends unknown[]>({
availableOptions,
placeholder,
getTextKeyOverride,
className,
disabled = false,
}: {
availableOptions: OptionsType;
placeholder?: string;
getTextKeyOverride?: (node: OptionsType[0]) => string;
className?: string;
disabled?: boolean;
}): [OptionsType, React.ReactNode] {
const [filterValue, setFilterValue] = useState<string | null>(null);
// We conditionally show the search when there's more than 10 elements in the menu
const shouldShowSearch = availableOptions.length > 10 && !disabled;
// Define the helper function used to enable search
// If this is invalidly described, we will assume developer error - tf. avoid rendering
const getTextKeyHelper = getTextKeyOverride || defaultGetStringKey;
// Iterate said options
const filteredOptions = useMemo(() => {
if (!shouldShowSearch || !filterValue || !availableOptions.length) {
// Don't render if available options aren't present, there's no filter active
return availableOptions;
}
// Filter through the values, using a simple text-based search
// nothing too fancy, but we can add a better search algo later if we need
const upperFilterValue = filterValue.toUpperCase();
return availableOptions.filter((value) =>
getTextKeyHelper(value).includes(upperFilterValue),
) as OptionsType;
}, [availableOptions, getTextKeyHelper, filterValue, shouldShowSearch]);
const onSearchChange = useCallback((nextFilterValue) => setFilterValue(nextFilterValue), []);
const searchRender = shouldShowSearch ? (
<MultiSearch
value={filterValue}
className={className}
onChange={onSearchChange}
placeholder={placeholder}
/>
) : null;
return [filteredOptions, searchRender];
}
|