@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
JavaScript
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