using Blazored.LocalStorage; using Lantean.QBTMud.Helpers; using Lantean.QBTMud.Models; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Web; using MudBlazor; namespace Lantean.QBTMud.Components.UI { public partial class DynamicTable : MudComponentBase { private static readonly string _typeName = typeof(T).Name; private readonly string _columnSelectionStorageKey = $"DynamicTable{_typeName}.ColumnSelection"; private readonly string _columnSortStorageKey = $"DynamicTable{_typeName}.ColumnSort"; private readonly string _columnWidthsStorageKey = $"DynamicTable{_typeName}.ColumnWidths"; private readonly string _columnOrderStorageKey = $"DynamicTable{_typeName}.ColumnOrder"; [Inject] public ILocalStorageService LocalStorage { get; set; } = default!; [Inject] public IDialogService DialogService { get; set; } = default!; [Parameter] [EditorRequired] public IEnumerable> ColumnDefinitions { get; set; } = []; [Parameter] [EditorRequired] public IEnumerable? Items { get; set; } [Parameter] public bool MultiSelection { get; set; } [Parameter] public bool Striped { get; set; } [Parameter] public bool Hover { get; set; } [Parameter] public bool PreSorted { get; set; } [Parameter] public bool SelectOnRowClick { get; set; } [Parameter] public EventCallback> OnRowClick { get; set; } [Parameter] public HashSet SelectedItems { get; set; } = []; [Parameter] public EventCallback> SelectedItemsChanged { get; set; } [Parameter] public EventCallback SelectedItemChanged { get; set; } [Parameter] public Func, bool> ColumnFilter { get; set; } = t => true; [Parameter] public EventCallback SortColumnChanged { get; set; } [Parameter] public EventCallback SortDirectionChanged { get; set; } [Parameter] public EventCallback> SelectedColumnsChanged { get; set; } [Parameter] public EventCallback> OnTableDataContextMenu { get; set; } [Parameter] public EventCallback> OnTableDataLongPress { get; set; } [Parameter] public Func? RowClassFunc { get; set; } protected IEnumerable? OrderedItems => GetOrderedItems(); protected HashSet SelectedColumns { get; set; } = []; private static readonly IReadOnlyList> EmptyColumns = Array.Empty>(); private Dictionary _columnWidths = []; private Dictionary _columnOrder = []; private string? _sortColumn; private SortDirection _sortDirection; private DateTimeOffset? _suppressRowClickUntil; private readonly Dictionary _tds = []; private IReadOnlyList> _visibleColumns = EmptyColumns; private bool _columnsDirty = true; private IEnumerable>? _lastColumnDefinitions; protected override async Task OnInitializedAsync() { HashSet selectedColumns; var storedSelectedColumns = await LocalStorage.GetItemAsync>(_columnSelectionStorageKey); if (storedSelectedColumns is not null) { selectedColumns = storedSelectedColumns; } else { selectedColumns = ColumnDefinitions.Where(c => c.Enabled).Select(c => c.Id).ToHashSet(); } if (!SelectedColumns.SetEquals(selectedColumns)) { SelectedColumns = selectedColumns; await SelectedColumnsChanged.InvokeAsync(SelectedColumns); } else { SelectedColumns = selectedColumns; } _lastColumnDefinitions = ColumnDefinitions; MarkColumnsDirty(); string? sortColumn; SortDirection sortDirection; var sortData = await LocalStorage.GetItemAsync(_columnSortStorageKey); if (sortData is not null) { sortColumn = sortData.SortColumn; sortDirection = sortData.SortDirection; } else { sortColumn = ColumnDefinitions.First(c => c.Enabled).Id; sortDirection = SortDirection.Ascending; } if (_sortColumn != sortColumn) { _sortColumn = sortColumn; await SortColumnChanged.InvokeAsync(_sortColumn); } if (_sortDirection != sortDirection) { _sortDirection = sortDirection; await SortDirectionChanged.InvokeAsync(_sortDirection); } MarkColumnsDirty(); var storedColumnsWidths = await LocalStorage.GetItemAsync>(_columnWidthsStorageKey); if (storedColumnsWidths is not null) { _columnWidths = storedColumnsWidths; } MarkColumnsDirty(); } protected override void OnParametersSet() { base.OnParametersSet(); if (!ReferenceEquals(_lastColumnDefinitions, ColumnDefinitions)) { _lastColumnDefinitions = ColumnDefinitions; MarkColumnsDirty(); } } private IEnumerable? GetOrderedItems() { if (Items is null) { return null; } if (PreSorted) { return Items; } var sortSelector = ColumnDefinitions.FirstOrDefault(c => c.Id == _sortColumn)?.SortSelector; if (sortSelector is null) { return Items; } return Items.OrderByDirection(_sortDirection, sortSelector); } protected IReadOnlyList> GetColumns() { if (!_columnsDirty) { return _visibleColumns; } _visibleColumns = BuildVisibleColumns(); _columnsDirty = false; return _visibleColumns; } private IReadOnlyList> BuildVisibleColumns() { var filteredColumns = ColumnDefinitions .Where(c => SelectedColumns.Contains(c.Id)) .Where(ColumnFilter) .ToList(); if (filteredColumns.Count == 0) { return EmptyColumns; } List> orderedColumns; if (_columnOrder.Count == 0) { orderedColumns = filteredColumns; } else { var orderLookup = _columnOrder.OrderBy(entry => entry.Value).ToList(); var columnDictionary = filteredColumns.ToDictionary(c => c.Id); orderedColumns = new List>(filteredColumns.Count); foreach (var (columnId, _) in orderLookup) { if (!columnDictionary.TryGetValue(columnId, out var column)) { continue; } orderedColumns.Add(column); } if (orderedColumns.Count != filteredColumns.Count) { var existingIds = new HashSet(orderedColumns.Select(c => c.Id)); foreach (var column in filteredColumns) { if (existingIds.Add(column.Id)) { orderedColumns.Add(column); } } } } foreach (var column in orderedColumns) { if (_columnWidths.TryGetValue(column.Id, out var value)) { column.Width = value; } } return orderedColumns; } private async Task SetSort(string columnId, SortDirection sortDirection) { if (sortDirection == SortDirection.None) { return; } await LocalStorage.SetItemAsync(_columnSortStorageKey, new SortData(columnId, sortDirection)); if (_sortColumn != columnId) { _sortColumn = columnId; await SortColumnChanged.InvokeAsync(_sortColumn); } if (_sortDirection != sortDirection) { _sortDirection = sortDirection; await SortDirectionChanged.InvokeAsync(_sortDirection); } } protected async Task OnRowClickInternal(TableRowClickEventArgs eventArgs) { if (_suppressRowClickUntil is not null) { if (DateTimeOffset.UtcNow <= _suppressRowClickUntil.Value) { _suppressRowClickUntil = null; return; } _suppressRowClickUntil = null; } if (eventArgs.Item is null) { return; } if (MultiSelection) { if (eventArgs.MouseEventArgs.CtrlKey) { if (SelectedItems.Contains(eventArgs.Item)) { SelectedItems.Remove(eventArgs.Item); } else { SelectedItems.Add(eventArgs.Item); } } else if (eventArgs.MouseEventArgs.AltKey) { SelectedItems.Clear(); SelectedItems.Add(eventArgs.Item); } else { if (!SelectedItems.Contains(eventArgs.Item)) { SelectedItems.Clear(); SelectedItems.Add(eventArgs.Item); } } } else if (SelectOnRowClick && !SelectedItems.Contains(eventArgs.Item)) { SelectedItems.Clear(); SelectedItems.Add(eventArgs.Item); await SelectedItemChanged.InvokeAsync(eventArgs.Item); } await OnRowClick.InvokeAsync(eventArgs); } protected string RowStyleFuncInternal(T item, int index) { var style = "user-select: none; cursor: pointer;"; if (SelectOnRowClick && SelectedItems.Contains(item)) { style += " background-color: var(--mud-palette-gray-dark); color: var(--mud-palette-gray-light) !important;"; } return style; } protected string RowClassFuncInternal(T item, int index) { if (RowClassFunc is not null) { return RowClassFunc(item, index); } return string.Empty; } protected async Task SelectedItemsChangedInternal(HashSet selectedItems) { await SelectedItemsChanged.InvokeAsync(selectedItems); SelectedItems = selectedItems; } protected Task OnContextMenuInternal(MouseEventArgs eventArgs, string columnId, T item) { var data = _tds[columnId]; return OnTableDataContextMenu.InvokeAsync(new TableDataContextMenuEventArgs(eventArgs, data, item)); } protected Task OnLongPressInternal(LongPressEventArgs eventArgs, string columnId, T item) { _suppressRowClickUntil = DateTimeOffset.UtcNow.AddMilliseconds(500); var data = _tds[columnId]; return OnTableDataLongPress.InvokeAsync(new TableDataLongPressEventArgs(eventArgs, data, item)); } public async Task ShowColumnOptionsDialog() { var result = await DialogService.ShowColumnsOptionsDialog(ColumnDefinitions.Where(ColumnFilter).ToList(), SelectedColumns, _columnWidths, _columnOrder); if (result == default) { return; } if (!SelectedColumns.SetEquals(result.SelectedColumns)) { SelectedColumns = result.SelectedColumns; await LocalStorage.SetItemAsync(_columnSelectionStorageKey, SelectedColumns); await SelectedColumnsChanged.InvokeAsync(SelectedColumns); MarkColumnsDirty(); } if (!DictionaryEqual(_columnWidths, result.ColumnWidths)) { _columnWidths = result.ColumnWidths; await LocalStorage.SetItemAsync(_columnWidthsStorageKey, _columnWidths); MarkColumnsDirty(); } if (!DictionaryEqual(_columnOrder, result.ColumnOrder)) { _columnOrder = result.ColumnOrder; await LocalStorage.SetItemAsync(_columnOrderStorageKey, _columnOrder); MarkColumnsDirty(); } } private static bool DictionaryEqual(Dictionary left, Dictionary right) where TKey : notnull { return left.Keys.Count == right.Keys.Count && left.Keys.All(k => right.ContainsKey(k) && Equals(left[k], right[k])); } private static string? GetColumnStyle(ColumnDefinition column) { string? style = null; if (column.Width.HasValue) { style = $"width: {column.Width.Value}px; max-width: {column.Width.Value}px;"; } return style; } private string? GetColumnClass(ColumnDefinition column, T data) { var className = column.Class; if (column.ClassFunc is not null) { var funcClass = column.ClassFunc(data); if (funcClass is not null) { if (className is null) { className = funcClass; } else { className = $"{className} {funcClass}"; } } } if (column.Width.HasValue) { className = string.IsNullOrWhiteSpace(className) ? "overflow-cell" : $"overflow-cell {className}"; } if (OnTableDataContextMenu.HasDelegate) { className = string.IsNullOrWhiteSpace(className) ? "no-default-context-menu" : $"no-default-context-menu {className}"; } if (OnTableDataLongPress.HasDelegate) { className = string.IsNullOrWhiteSpace(className) ? "unselectable" : $"unselectable {className}"; } return className; } private void MarkColumnsDirty() { _columnsDirty = true; _visibleColumns = EmptyColumns; } private sealed record SortData { public SortData(string sortColumn, SortDirection sortDirection) { SortColumn = sortColumn; SortDirection = sortDirection; } public string SortColumn { get; init; } public SortDirection SortDirection { get; init; } } } }