UNPKG

@applaudem/icon-selector-dialog

Version:

A searchable icon selector dialog component for React with Material-UI and Iconify

189 lines (186 loc) 11.1 kB
import { jsx, jsxs, Fragment } from 'react/jsx-runtime'; import { useState, useRef, useEffect, useMemo, useCallback } from 'react'; import { Icon } from '@iconify/react'; import { debounce, Dialog, DialogContent, Box, CircularProgress, DialogTitle, FormControl, InputLabel, Select, MenuItem, TextField, IconButton, Typography } from '@mui/material'; import * as MuiIcons from '@mui/icons-material'; import iconListJson from './iconList.mjs'; // Type assertion for the imported icon list const iconList = iconListJson; // Simple Dutch translations const translations = { title: "Kies een icoon", iconSet: "Icon Set", category: "Categorie", searchPlaceholder: "Zoek in alle icon sets en categorieën...", searching: "Zoeken...", noResults: "Geen iconen gevonden voor", clearSearch: "zoekopdracht wissen" }; // Replace individual icon imports with references from MuiIcons const SearchIcon = MuiIcons.Search; const ClearIcon = MuiIcons.Clear; function IconSelector({ open, onClose, onSelect, currentIcon, }) { const [searchTerm, setSearchTerm] = useState(''); const [selectedIconSet, setSelectedIconSet] = useState(''); const [selectedCategory, setSelectedCategory] = useState(''); const [loading, setLoading] = useState(true); const [isSearching, setIsSearching] = useState(false); const [debouncedSearchResults, setDebouncedSearchResults] = useState([]); const [displaySearchTerm, setDisplaySearchTerm] = useState(''); const searchInputRef = useRef(null); // Initialize selected icon set useEffect(() => { if (open && !selectedIconSet && Object.keys(iconList).length > 0) { setSelectedIconSet(Object.keys(iconList)[0]); setLoading(false); } }, [open, selectedIconSet]); // Get sorted categories for current icon set (with "Other" at the bottom) const categories = useMemo(() => { if (!selectedIconSet) return []; const allCategories = Object.keys(iconList[selectedIconSet].categories); return allCategories .filter(cat => cat !== 'Other') .sort() .concat(['Other']); }, [selectedIconSet]); // Initialize selected category when icon set changes useEffect(() => { if (selectedIconSet && categories.length > 0) { setSelectedCategory(categories[0]); } }, [selectedIconSet, categories]); // Debounced search function with async processing const debouncedSearch = useCallback(debounce((searchTerm) => { if (!searchTerm) { setDebouncedSearchResults([]); setIsSearching(false); return; } // Use requestAnimationFrame to prevent UI blocking requestAnimationFrame(() => { // Break up the search into chunks using setTimeout const searchTermLower = searchTerm.toLowerCase(); const results = []; const iconSets = Object.entries(iconList); let currentSetIndex = 0; function processNextIconSet() { if (currentSetIndex >= iconSets.length) { // All sets processed setDebouncedSearchResults(results); setIsSearching(false); return; } const [, setData] = iconSets[currentSetIndex]; Object.entries(setData.categories).forEach(([, icons]) => { const matchingIcons = icons.filter(icon => icon.toLowerCase().includes(searchTermLower)); results.push(...matchingIcons); }); currentSetIndex++; setTimeout(processNextIconSet, 0); } processNextIconSet(); }); }, 500), []); // Handle search input const handleSearchChange = (e) => { const value = e.target.value; setDisplaySearchTerm(value); if (value) { setIsSearching(true); setSearchTerm(value); debouncedSearch(value); } else { setIsSearching(false); setSearchTerm(''); setDebouncedSearchResults([]); } }; // Get filtered icons based on search term or current selection const filteredIcons = useMemo(() => { if (searchTerm) { return debouncedSearchResults; } else if (selectedIconSet && selectedCategory) { return iconList[selectedIconSet].categories[selectedCategory] || []; } return []; }, [searchTerm, selectedIconSet, selectedCategory, debouncedSearchResults]); const handleIconSelect = (iconName) => { onSelect(iconName); onClose(); }; // Get icon set and category for an icon (for displaying in search results) const getIconMetadata = (iconName) => { for (const [setName, setData] of Object.entries(iconList)) { for (const [categoryName, icons] of Object.entries(setData.categories)) { if (icons.includes(iconName)) { return { setName, categoryName }; } } } return { setName: '', categoryName: '' }; }; // Add event listener for keypress useEffect(() => { if (!open) return; const handleKeyPress = (e) => { const target = e.target; if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA') { return; } // Ignore special keys like Control, Alt, etc. if (e.ctrlKey || e.altKey || e.metaKey) { return; } // Focus the search input and start typing if (searchInputRef.current) { searchInputRef.current.focus(); } }; window.addEventListener('keydown', handleKeyPress); return () => window.removeEventListener('keydown', handleKeyPress); }, [open]); if (loading) { return (jsx(Dialog, { open: open, onClose: onClose, children: jsx(DialogContent, { children: jsx(Box, { display: "flex", justifyContent: "center", alignItems: "center", p: 4, children: jsx(CircularProgress, {}) }) }) })); } return (jsxs(Dialog, { open: open, onClose: onClose, maxWidth: "md", fullWidth: true, children: [jsx(DialogTitle, { children: translations.title }), jsxs(DialogContent, { children: [jsxs(Box, { sx: { mb: 2 }, children: [jsxs(Box, { display: "grid", gridTemplateColumns: { xs: '1fr', md: '1fr 1fr' }, gap: 2, children: [jsx(Box, { children: jsxs(FormControl, { fullWidth: true, margin: "normal", children: [jsx(InputLabel, { children: translations.iconSet }), jsx(Select, { value: selectedIconSet, onChange: (e) => { setSelectedIconSet(e.target.value); setSelectedCategory(''); }, label: translations.iconSet, disabled: !!searchTerm, children: Object.keys(iconList).map((set) => (jsx(MenuItem, { value: set, children: set }, set))) })] }) }), jsx(Box, { children: jsxs(FormControl, { fullWidth: true, margin: "normal", children: [jsx(InputLabel, { children: translations.category }), jsx(Select, { value: selectedCategory, onChange: (e) => setSelectedCategory(e.target.value), label: translations.category, disabled: !!searchTerm, children: categories.map((category) => (jsx(MenuItem, { value: category, sx: category === 'Other' ? { borderTop: '1px solid', borderColor: 'divider', marginTop: 1, paddingTop: 1 } : {}, children: category }, category))) })] }) })] }), jsx(TextField, { inputRef: searchInputRef, fullWidth: true, placeholder: translations.searchPlaceholder, value: displaySearchTerm, onChange: handleSearchChange, margin: "normal", InputProps: { startAdornment: (jsx(SearchIcon, {})), endAdornment: isSearching ? (jsx(CircularProgress, { size: 20 })) : displaySearchTerm && (jsx(IconButton, { size: "small", onClick: () => { setDisplaySearchTerm(''); setSearchTerm(''); setDebouncedSearchResults([]); setIsSearching(false); }, edge: "end", "aria-label": translations.clearSearch, children: jsx(ClearIcon, { fontSize: "small" }) })) } })] }), isSearching ? (jsx(Box, { sx: { display: 'flex', justifyContent: 'center', p: 4 }, children: jsx(Typography, { color: "text.secondary", children: translations.searching }) })) : (jsxs(Fragment, { children: [jsx(Box, { display: "grid", gridTemplateColumns: { xs: 'repeat(2, 1fr)', sm: 'repeat(3, 1fr)', md: 'repeat(6, 1fr)' }, gap: 1, children: filteredIcons.map((iconName) => (jsxs(IconButton, { onClick: () => handleIconSelect(iconName), sx: Object.assign({ flexDirection: 'column', width: '100%', height: '80px', '&:hover': { backgroundColor: 'action.hover', } }, (currentIcon === iconName && { backgroundColor: 'action.selected', })), children: [jsx(Icon, { icon: iconName, width: "24", height: "24", style: { color: 'primary.main', } }), jsx(Typography, { variant: "caption", sx: { mt: 1, textAlign: 'center', maxWidth: '100%', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, children: iconName.split(':')[1] }), searchTerm && (jsx(Box, { sx: { mt: 0.5, display: 'flex', gap: 0.5, flexWrap: 'wrap', justifyContent: 'center' }, children: jsx(Typography, { variant: "caption", color: "text.secondary", sx: { fontSize: '0.7rem' }, children: getIconMetadata(iconName).setName }) }))] }, iconName))) }), searchTerm && filteredIcons.length === 0 && (jsx(Box, { sx: { mt: 4, textAlign: 'center' }, children: jsxs(Typography, { color: "text.secondary", children: [translations.noResults, " \"", searchTerm, "\""] }) }))] }))] })] })); } export { IconSelector as default }; //# sourceMappingURL=IconSelector.mjs.map