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