UNPKG

@applaudem/icon-selector-dialog

Version:

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

213 lines (206 loc) 12.3 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var jsxRuntime = require('react/jsx-runtime'); var react = require('react'); var react$1 = require('@iconify/react'); var material = require('@mui/material'); var MuiIcons = require('@mui/icons-material'); var iconList$1 = require('./iconList.cjs'); function _interopNamespace(e) { if (e && e.__esModule) return e; var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n["default"] = e; return Object.freeze(n); } var MuiIcons__namespace = /*#__PURE__*/_interopNamespace(MuiIcons); // Type assertion for the imported icon list const iconList = iconList$1["default"]; // 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__namespace.Search; const ClearIcon = MuiIcons__namespace.Clear; function IconSelector({ open, onClose, onSelect, currentIcon, }) { const [searchTerm, setSearchTerm] = react.useState(''); const [selectedIconSet, setSelectedIconSet] = react.useState(''); const [selectedCategory, setSelectedCategory] = react.useState(''); const [loading, setLoading] = react.useState(true); const [isSearching, setIsSearching] = react.useState(false); const [debouncedSearchResults, setDebouncedSearchResults] = react.useState([]); const [displaySearchTerm, setDisplaySearchTerm] = react.useState(''); const searchInputRef = react.useRef(null); // Initialize selected icon set react.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 = react.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 react.useEffect(() => { if (selectedIconSet && categories.length > 0) { setSelectedCategory(categories[0]); } }, [selectedIconSet, categories]); // Debounced search function with async processing const debouncedSearch = react.useCallback(material.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 = react.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 react.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 (jsxRuntime.jsx(material.Dialog, { open: open, onClose: onClose, children: jsxRuntime.jsx(material.DialogContent, { children: jsxRuntime.jsx(material.Box, { display: "flex", justifyContent: "center", alignItems: "center", p: 4, children: jsxRuntime.jsx(material.CircularProgress, {}) }) }) })); } return (jsxRuntime.jsxs(material.Dialog, { open: open, onClose: onClose, maxWidth: "md", fullWidth: true, children: [jsxRuntime.jsx(material.DialogTitle, { children: translations.title }), jsxRuntime.jsxs(material.DialogContent, { children: [jsxRuntime.jsxs(material.Box, { sx: { mb: 2 }, children: [jsxRuntime.jsxs(material.Box, { display: "grid", gridTemplateColumns: { xs: '1fr', md: '1fr 1fr' }, gap: 2, children: [jsxRuntime.jsx(material.Box, { children: jsxRuntime.jsxs(material.FormControl, { fullWidth: true, margin: "normal", children: [jsxRuntime.jsx(material.InputLabel, { children: translations.iconSet }), jsxRuntime.jsx(material.Select, { value: selectedIconSet, onChange: (e) => { setSelectedIconSet(e.target.value); setSelectedCategory(''); }, label: translations.iconSet, disabled: !!searchTerm, children: Object.keys(iconList).map((set) => (jsxRuntime.jsx(material.MenuItem, { value: set, children: set }, set))) })] }) }), jsxRuntime.jsx(material.Box, { children: jsxRuntime.jsxs(material.FormControl, { fullWidth: true, margin: "normal", children: [jsxRuntime.jsx(material.InputLabel, { children: translations.category }), jsxRuntime.jsx(material.Select, { value: selectedCategory, onChange: (e) => setSelectedCategory(e.target.value), label: translations.category, disabled: !!searchTerm, children: categories.map((category) => (jsxRuntime.jsx(material.MenuItem, { value: category, sx: category === 'Other' ? { borderTop: '1px solid', borderColor: 'divider', marginTop: 1, paddingTop: 1 } : {}, children: category }, category))) })] }) })] }), jsxRuntime.jsx(material.TextField, { inputRef: searchInputRef, fullWidth: true, placeholder: translations.searchPlaceholder, value: displaySearchTerm, onChange: handleSearchChange, margin: "normal", InputProps: { startAdornment: (jsxRuntime.jsx(SearchIcon, {})), endAdornment: isSearching ? (jsxRuntime.jsx(material.CircularProgress, { size: 20 })) : displaySearchTerm && (jsxRuntime.jsx(material.IconButton, { size: "small", onClick: () => { setDisplaySearchTerm(''); setSearchTerm(''); setDebouncedSearchResults([]); setIsSearching(false); }, edge: "end", "aria-label": translations.clearSearch, children: jsxRuntime.jsx(ClearIcon, { fontSize: "small" }) })) } })] }), isSearching ? (jsxRuntime.jsx(material.Box, { sx: { display: 'flex', justifyContent: 'center', p: 4 }, children: jsxRuntime.jsx(material.Typography, { color: "text.secondary", children: translations.searching }) })) : (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(material.Box, { display: "grid", gridTemplateColumns: { xs: 'repeat(2, 1fr)', sm: 'repeat(3, 1fr)', md: 'repeat(6, 1fr)' }, gap: 1, children: filteredIcons.map((iconName) => (jsxRuntime.jsxs(material.IconButton, { onClick: () => handleIconSelect(iconName), sx: Object.assign({ flexDirection: 'column', width: '100%', height: '80px', '&:hover': { backgroundColor: 'action.hover', } }, (currentIcon === iconName && { backgroundColor: 'action.selected', })), children: [jsxRuntime.jsx(react$1.Icon, { icon: iconName, width: "24", height: "24", style: { color: 'primary.main', } }), jsxRuntime.jsx(material.Typography, { variant: "caption", sx: { mt: 1, textAlign: 'center', maxWidth: '100%', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, children: iconName.split(':')[1] }), searchTerm && (jsxRuntime.jsx(material.Box, { sx: { mt: 0.5, display: 'flex', gap: 0.5, flexWrap: 'wrap', justifyContent: 'center' }, children: jsxRuntime.jsx(material.Typography, { variant: "caption", color: "text.secondary", sx: { fontSize: '0.7rem' }, children: getIconMetadata(iconName).setName }) }))] }, iconName))) }), searchTerm && filteredIcons.length === 0 && (jsxRuntime.jsx(material.Box, { sx: { mt: 4, textAlign: 'center' }, children: jsxRuntime.jsxs(material.Typography, { color: "text.secondary", children: [translations.noResults, " \"", searchTerm, "\""] }) }))] }))] })] })); } exports["default"] = IconSelector; //# sourceMappingURL=IconSelector.cjs.map