You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Connected.Components/Components/ChipSet/ChipSet.razor.cs

343 lines
8.7 KiB

2 years ago
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();
/// <summary>
/// Child content of component.
/// </summary>
[Parameter]
[Category(CategoryTypes.ChipSet.Behavior)]
public RenderFragment ChildContent { get; set; }
/// <summary>
/// Allows to select more than one chip.
/// </summary>
[Parameter]
[Category(CategoryTypes.ChipSet.Behavior)]
public bool MultiSelection { get; set; } = false;
/// <summary>
/// Will not allow to deselect the selected chip in single selection mode.
/// </summary>
[Parameter]
[Category(CategoryTypes.ChipSet.Behavior)]
public bool Mandatory { get; set; } = false;
/// <summary>
/// Will make all chips closable.
/// </summary>
[Parameter]
[Category(CategoryTypes.ChipSet.Behavior)]
public bool AllClosable { get; set; } = false;
/// <summary>
/// Will show a check-mark for the selected components.
/// </summary>
[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();
}
}
/// <summary>
/// Will make all chips read only.
/// </summary>
[Parameter]
[Category(CategoryTypes.ChipSet.Behavior)]
public bool ReadOnly { get; set; } = false;
/// <summary>
/// The currently selected chip in Choice mode
/// </summary>
[Parameter]
[Category(CategoryTypes.ChipSet.Behavior)]
public Chip SelectedChip
{
get { return _chips.OfType<Chip>().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);
}
}
/// <summary>
/// Called when the selected chip changes, in Choice mode
/// </summary>
[Parameter]
public EventCallback<Chip> SelectedChipChanged { get; set; }
/// <summary>
/// The currently selected chips in Filter mode
/// </summary>
[Parameter]
[Category(CategoryTypes.ChipSet.Behavior)]
public Chip[] SelectedChips
{
get { return _chips.OfType<Chip>().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<Chip>(value);
foreach (var chip in _chips)
{
chip.IsSelected = selected.Contains(chip);
}
}
StateHasChanged();
}
}
protected override void OnInitialized()
{
base.OnInitialized();
if (_selectedValues == null)
_selectedValues = new HashSet<object>(_comparer);
_initialSelectedValues = new HashSet<object>(_selectedValues, _comparer);
}
private IEqualityComparer<object> _comparer;
private HashSet<object> _selectedValues;
private HashSet<object> _initialSelectedValues;
/// <summary>
/// The Comparer to use for comparing selected values internally.
/// </summary>
[Parameter]
[Category(CategoryTypes.ChipSet.Behavior)]
public IEqualityComparer<object> Comparer
{
get => _comparer;
set
{
_comparer = value;
// Apply comparer and refresh selected values
_selectedValues = new HashSet<object>(_selectedValues, _comparer);
SelectedValues = _selectedValues;
}
}
/// <summary>
/// Called when the selection changed, in Filter mode
/// </summary>
[Parameter]
public EventCallback<Chip[]> SelectedChipsChanged { get; set; }
/// <summary>
/// The current selected value.
/// Note: make the list Clickable for item selection to work.
/// </summary>
[Parameter]
[Category(CategoryTypes.ChipSet.Behavior)]
public ICollection<object> SelectedValues
{
get => _selectedValues;
set
{
if (value == null)
SetSelectedValues(new object[0]);
else
SetSelectedValues(value.ToArray()).AndForget();
}
}
/// <summary>
/// Called whenever the selection changed
/// </summary>
[Parameter] public EventCallback<ICollection<object>> SelectedValuesChanged { get; set; }
internal Task SetSelectedValues(object[] values)
{
HashSet<object> newValues = null;
if (values == null)
values = new object[0];
if (MultiSelection)
newValues = new HashSet<object>(values, _comparer);
else
{
newValues = new HashSet<object>(_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();
}
/// <summary>
/// Called when a Chip was deleted (by click on the close icon)
/// </summary>
[Parameter]
public EventCallback<Chip> 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<Chip> _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<object>(_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;
}
}