using Connected.Annotations; using Connected.Utilities; using Microsoft.AspNetCore.Components; namespace Connected.Components; public partial class ChipSet : UIComponent, IDisposable { protected string Classname => new CssBuilder("mud-chipset") .AddClass(Class) .Build(); /// /// Child content of component. /// [Parameter] [Category(CategoryTypes.ChipSet.Behavior)] public RenderFragment ChildContent { get; set; } /// /// Allows to select more than one chip. /// [Parameter] [Category(CategoryTypes.ChipSet.Behavior)] public bool MultiSelection { get; set; } = false; /// /// Will not allow to deselect the selected chip in single selection mode. /// [Parameter] [Category(CategoryTypes.ChipSet.Behavior)] public bool Mandatory { get; set; } = false; /// /// Will make all chips closable. /// [Parameter] [Category(CategoryTypes.ChipSet.Behavior)] public bool AllClosable { get; set; } = false; /// /// Will show a check-mark for the selected components. /// [Parameter] [Category(CategoryTypes.ChipSet.Appearance)] public bool Filter { get => _filter; set { if (_filter == value) return; _filter = value; StateHasChanged(); foreach (var chip in _chips) chip.ForceRerender(); } } /// /// Will make all chips read only. /// [Parameter] [Category(CategoryTypes.ChipSet.Behavior)] public bool ReadOnly { get; set; } = false; /// /// The currently selected chip in Choice mode /// [Parameter] [Category(CategoryTypes.ChipSet.Behavior)] public Chip SelectedChip { get { return _chips.OfType().FirstOrDefault(x => x.IsSelected); } set { if (value == null) { foreach (var chip in _chips) { chip.IsSelected = false; } } else { foreach (var chip in _chips) { chip.IsSelected = (chip == value); } } this.InvokeAsync(StateHasChanged); } } /// /// Called when the selected chip changes, in Choice mode /// [Parameter] public EventCallback SelectedChipChanged { get; set; } /// /// The currently selected chips in Filter mode /// [Parameter] [Category(CategoryTypes.ChipSet.Behavior)] public Chip[] SelectedChips { get { return _chips.OfType().Where(x => x.IsSelected).ToArray(); } set { if (value == null || value.Length == 0) { foreach (var chip in _chips) { chip.IsSelected = false; } } else { var selected = new HashSet(value); foreach (var chip in _chips) { chip.IsSelected = selected.Contains(chip); } } StateHasChanged(); } } protected override void OnInitialized() { base.OnInitialized(); if (_selectedValues == null) _selectedValues = new HashSet(_comparer); _initialSelectedValues = new HashSet(_selectedValues, _comparer); } private IEqualityComparer _comparer; private HashSet _selectedValues; private HashSet _initialSelectedValues; /// /// The Comparer to use for comparing selected values internally. /// [Parameter] [Category(CategoryTypes.ChipSet.Behavior)] public IEqualityComparer Comparer { get => _comparer; set { _comparer = value; // Apply comparer and refresh selected values _selectedValues = new HashSet(_selectedValues, _comparer); SelectedValues = _selectedValues; } } /// /// Called when the selection changed, in Filter mode /// [Parameter] public EventCallback SelectedChipsChanged { get; set; } /// /// The current selected value. /// Note: make the list Clickable for item selection to work. /// [Parameter] [Category(CategoryTypes.ChipSet.Behavior)] public ICollection SelectedValues { get => _selectedValues; set { if (value == null) SetSelectedValues(new object[0]); else SetSelectedValues(value.ToArray()).AndForget(); } } /// /// Called whenever the selection changed /// [Parameter] public EventCallback> SelectedValuesChanged { get; set; } internal Task SetSelectedValues(object[] values) { HashSet newValues = null; if (values == null) values = new object[0]; if (MultiSelection) newValues = new HashSet(values, _comparer); else { newValues = new HashSet(_comparer); if (values.Length > 0) newValues.Add(values.First()); } // avoid update with same values if (_selectedValues.IsEqualTo(newValues)) return Task.CompletedTask; _selectedValues = newValues; foreach (var chip in _chips.ToArray()) { var isSelected = _selectedValues.Contains(chip.Value); chip.IsSelected = isSelected; } return NotifySelection(); } /// /// Called when a Chip was deleted (by click on the close icon) /// [Parameter] public EventCallback OnClose { get; set; } internal Task Add(Chip chip) { _chips.Add(chip); if (_selectedValues.Contains(chip.Value)) chip.IsSelected = true; return CheckDefault(chip); } internal void Remove(Chip chip) { _chips.Remove(chip); if (chip.IsSelected) { _selectedValues.Remove(chip.Value); NotifySelection().AndForget(); } } private async Task CheckDefault(Chip chip) { if (!MultiSelection) return; if (chip.DefaultProcessed) return; chip.DefaultProcessed = true; if (chip.Default == null) return; var oldSelected = chip.IsSelected; chip.IsSelected = chip.Default == true; if (chip.IsSelected != oldSelected) { if (chip.IsSelected) _selectedValues.Add(chip.Value); else _selectedValues.Remove(chip.Value); await NotifySelection(); } } private HashSet _chips = new(); private bool _filter; internal Task OnChipClicked(Chip chip) { var wasSelected = chip.IsSelected; if (MultiSelection) { chip.IsSelected = !chip.IsSelected; } else { foreach (var ch in _chips) { ch.IsSelected = (ch == chip); // <-- exclusively select the one chip only, thus all others must be deselected } if (!Mandatory) chip.IsSelected = !wasSelected; } UpdateSelectedValues(); return NotifySelection(); } private void UpdateSelectedValues() { _selectedValues = new HashSet(_chips.Where(x => x.IsSelected).Select(x => x.Value), _comparer); } private object[] _lastSelectedValues = null; private async Task NotifySelection() { if (_disposed) return; // to avoid endless notification loops we check if selection has really changed if (_selectedValues.IsEqualTo(_lastSelectedValues)) return; _lastSelectedValues = _selectedValues.ToArray(); await SelectedChipChanged.InvokeAsync(SelectedChip); await SelectedChipsChanged.InvokeAsync(SelectedChips); await SelectedValuesChanged.InvokeAsync(SelectedValues); StateHasChanged(); } public void OnChipDeleted(Chip chip) { Remove(chip); OnClose.InvokeAsync(chip); } protected override async void OnAfterRender(bool firstRender) { if (firstRender) await SelectDefaultChips(); base.OnAfterRender(firstRender); } private async Task SelectDefaultChips() { if (!MultiSelection) { var anySelected = false; var defaultChip = _chips.LastOrDefault(chip => chip.Default == true); if (defaultChip != null) { defaultChip.IsSelected = true; anySelected = true; } if (anySelected) { UpdateSelectedValues(); await NotifySelection(); } } } private bool _disposed; public void Dispose() { _disposed = true; } }