import {type CSSResultGroup, html, nothing, unsafeCSS} from 'lit'; import {classMap} from 'lit/directives/class-map.js'; import {defaultValue} from '../../internal/default-value'; import {property, query, state} from 'lit/decorators.js'; import {FormControlController} from '../../internal/form'; import ZincElement from '../../internal/zinc-element'; import ZnButton from '../button'; import ZnDialog from '../dialog'; import ZnIcon from '../icon'; import ZnInput from '../input'; import ZnOption from '../option'; import ZnSelect from '../select'; import type {ZincFormControl} from '../../internal/zinc-element'; import {brandIcons} from './brand-icons'; import {lineIcons} from './line-icons'; import { materialIcons, material_outlinedIcons, material_roundIcons, material_sharpIcons, material_two_toneIcons, material_symbols_outlinedIcons, } from './material-icons'; import styles from './icon-picker.scss'; export default class ZnIconPicker extends ZincElement implements ZincFormControl { static styles: CSSResultGroup = unsafeCSS(styles); static dependencies = { 'zn-icon': ZnIcon, 'zn-button': ZnButton, 'zn-dialog': ZnDialog, 'zn-input': ZnInput, 'zn-select': ZnSelect, 'zn-option': ZnOption }; private readonly formControlController = new FormControlController(this, { assumeInteractionOn: ['zn-change'] }); @property() name = ''; @property() icon = ''; @property() label = ''; @property() library: string = 'material'; @property() color: string = ''; @property({type: Boolean, attribute: 'no-color'}) noColor: boolean = false; @property({type: Boolean, attribute: 'no-library'}) noLibrary: boolean = false; @property({attribute: 'help-text'}) helpText: string = ''; @property({type: Boolean, reflect: true}) disabled = false; @property({type: Boolean, reflect: true}) required = false; @property({reflect: true}) form: string; @defaultValue() defaultValue = ''; @state() private _dialogOpen = false; @state() private _searchQuery = ''; @state() private _iconList: string[] = []; @state() private _filteredIcons: string[] = []; // Pending selections (not committed until confirm) @state() private _pendingIcon = ''; @state() private _pendingLibrary = ''; @state() private _pendingColor = ''; @query('zn-dialog') private _dialog: ZnDialog; get value(): string { return this.icon; } set value(val: string) { this.icon = val; } get validity(): ValidityState { if (this.required && !this.icon) { return { valid: false, valueMissing: true, badInput: false, customError: false, patternMismatch: false, rangeOverflow: false, rangeUnderflow: false, stepMismatch: false, tooLong: false, tooShort: false, typeMismatch: false, } as ValidityState; } return { valid: true, valueMissing: false, badInput: false, customError: false, patternMismatch: false, rangeOverflow: false, rangeUnderflow: false, stepMismatch: false, tooLong: false, tooShort: false, typeMismatch: false, } as ValidityState; } get validationMessage(): string { return this.required && !this.icon ? 'Please select an icon.' : ''; } checkValidity(): boolean { return this.validity.valid; } getForm(): HTMLFormElement | null { return this.formControlController.getForm(); } reportValidity(): boolean { return this.checkValidity(); } setCustomValidity(_message: string) { this.formControlController.updateValidity(); } private static readonly freeInputLibraries = new Set(['gravatar', 'libravatar', 'avatar']); private isFreeInputLibrary(library: string): boolean { return ZnIconPicker.freeInputLibraries.has(library); } private getIconsForLibrary(library: string): string[] { switch (library) { case 'material': return materialIcons; case 'material-outlined': return material_outlinedIcons; case 'material-round': return material_roundIcons; case 'material-sharp': return material_sharpIcons; case 'material-two-tone': return material_two_toneIcons; case 'material-symbols-outlined': return material_symbols_outlinedIcons; case 'brands': return brandIcons; case 'line': return lineIcons; default: return materialIcons; } } private async openDialog() { this._pendingIcon = this.icon; this._pendingLibrary = this.library; this._pendingColor = this.color; this._searchQuery = ''; this._iconList = this.getIconsForLibrary(this.library); this._filteredIcons = this._iconList.slice(0, 200); this._dialogOpen = true; await this.updateComplete; this._dialog.show(); } private closeDialog() { this._dialog.hide(); this._dialogOpen = false; } private handleConfirm() { this.icon = this._pendingIcon; this.library = this._pendingLibrary; this.color = this._pendingColor; this.closeDialog(); this.emit('zn-change'); } private handleCancel() { this.closeDialog(); } private handleSearchInput(e: Event) { const input = e.target as HTMLInputElement; this._searchQuery = input.value.toLowerCase(); this.filterIcons(); } private filterIcons() { if (!this._searchQuery) { this._filteredIcons = this._iconList.slice(0, 200); } else { this._filteredIcons = this._iconList .filter(name => name.includes(this._searchQuery)) .slice(0, 200); } } private handleIconSelect(iconName: string) { this._pendingIcon = iconName; } private handleLibraryChange(e: Event) { const select = e.target as HTMLSelectElement; const wasFreeInput = this.isFreeInputLibrary(this._pendingLibrary); this._pendingLibrary = select.value; const isFreeInput = this.isFreeInputLibrary(this._pendingLibrary); // Clear pending icon when switching between grid and free-input modes if (wasFreeInput !== isFreeInput) { this._pendingIcon = ''; } if (!isFreeInput) { this._iconList = this.getIconsForLibrary(this._pendingLibrary); this.filterIcons(); } } private handleColorInput(e: Event) { const input = e.target as ZnInput; this._pendingColor = input.value; } private handleFreeInput(e: Event) { const input = e.target as HTMLInputElement; this._pendingIcon = input.value; } private handleClear(e: Event) { e.stopPropagation(); this.icon = ''; this.color = ''; this.emit('zn-change'); } private _handleTriggerClick() { if (this.disabled) return; this.openDialog(); } private _handleTriggerKeyDown(event: KeyboardEvent) { if (this.disabled) { return; } if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); this._handleTriggerClick(); } } render() { const hasLabel = !!this.label; const hasHelpText = !!this.helpText; const hasIcon = !!this.icon; return html`