mirror of
https://github.com/radzenhq/radzen-blazor.git
synced 2026-07-08 18:16:08 +00:00
Remove narration comments, stale/incorrect comments, and XML doc comments on private and internal API across the Spreadsheet model, formula functions, editor, and tools. Keep comments documenting non-obvious rationale and XML docs on public API.
497 lines
13 KiB
C#
497 lines
13 KiB
C#
using Microsoft.AspNetCore.Components;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
|
|
using Radzen.Documents.Spreadsheet;
|
|
namespace Radzen.Blazor.Spreadsheet;
|
|
|
|
#nullable enable
|
|
|
|
/// <summary>
|
|
/// Represents a cell menu in a spreadsheet.
|
|
/// </summary>
|
|
public partial class CellMenu : ComponentBase
|
|
{
|
|
/// <summary>
|
|
/// The parent spreadsheet component.
|
|
/// </summary>
|
|
[CascadingParameter]
|
|
public ISpreadsheet? Spreadsheet { get; set; }
|
|
|
|
/// <summary>
|
|
/// Represents the sheet containing the cell menu.
|
|
/// </summary>
|
|
[Parameter]
|
|
public Worksheet Worksheet { get; set; } = default!;
|
|
|
|
/// <summary>
|
|
/// Represents the row index of the cell menu.
|
|
/// </summary>
|
|
[Parameter]
|
|
public int Row { get; set; }
|
|
|
|
/// <summary>
|
|
/// Represents the column index of the cell menu.
|
|
/// </summary>
|
|
[Parameter]
|
|
public int Column { get; set; }
|
|
|
|
/// <summary>
|
|
/// Invoked the user clicks the cancel button in the cell menu.
|
|
/// </summary>
|
|
[Parameter]
|
|
public EventCallback Cancel { get; set; }
|
|
|
|
/// <summary>
|
|
/// Invoked the user clicks the apply button in the cell menu.
|
|
/// </summary>
|
|
[Parameter]
|
|
public EventCallback<SheetFilter?> Apply { get; set; }
|
|
|
|
/// <summary>
|
|
/// Invoked when the user clicks the sort ascending option in the cell menu.
|
|
/// </summary>
|
|
[Parameter]
|
|
public EventCallback SortAscending { get; set; }
|
|
|
|
/// <summary>
|
|
/// Invoked when the user clicks the sort descending option in the cell menu.
|
|
/// </summary>
|
|
[Parameter]
|
|
public EventCallback SortDescending { get; set; }
|
|
|
|
/// <summary>
|
|
/// Invoked when the user clicks the clear filter option in the cell menu.
|
|
/// </summary>
|
|
[Parameter]
|
|
public EventCallback Clear { get; set; }
|
|
|
|
/// <summary>
|
|
/// Invoked when the user clicks the custom filter option in the cell menu.
|
|
/// </summary>
|
|
[Parameter]
|
|
public EventCallback CustomFilter { get; set; }
|
|
|
|
private readonly HashSet<object?> selectedFilterValues = [];
|
|
private List<(string Text, object? Value)>? cachedAvailableValues;
|
|
private bool? cachedShowBlank;
|
|
|
|
/// <inheritdoc />
|
|
public override async Task SetParametersAsync(ParameterView parameters)
|
|
{
|
|
var didRowChange = parameters.TryGetValue<int>(nameof(Row), out var row) && Row != row;
|
|
var didColumnChange = parameters.TryGetValue<int>(nameof(Column), out var column) && Column != column;
|
|
var didSheetChange = parameters.TryGetValue<Worksheet>(nameof(Worksheet), out var sheet) && Worksheet != sheet;
|
|
|
|
await base.SetParametersAsync(parameters);
|
|
|
|
if (didRowChange || didColumnChange || didSheetChange)
|
|
{
|
|
InvalidateCache();
|
|
InitializeSelectedFilterValues();
|
|
}
|
|
}
|
|
|
|
private void InvalidateCache()
|
|
{
|
|
cachedAvailableValues = null;
|
|
cachedShowBlank = null;
|
|
}
|
|
|
|
private void InitializeSelectedFilterValues()
|
|
{
|
|
selectedFilterValues.Clear();
|
|
|
|
var visitor = new InListCriterionVisitor(Column);
|
|
|
|
foreach (var filter in Worksheet.Filters)
|
|
{
|
|
filter.Criterion.Accept(visitor);
|
|
}
|
|
|
|
if (visitor.FoundValues.Count != 0)
|
|
{
|
|
foreach (var value in visitor.FoundValues)
|
|
{
|
|
if (value is string str)
|
|
{
|
|
CellData.TryConvertFromString(str, out var parsedValue, out _);
|
|
selectedFilterValues.Add(parsedValue);
|
|
}
|
|
else
|
|
{
|
|
selectedFilterValues.Add(value);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
var availableValues = LoadAvailableValues();
|
|
|
|
foreach (var (_, Value) in availableValues)
|
|
{
|
|
selectedFilterValues.Add(Value);
|
|
}
|
|
|
|
if (ShouldShowBlankOption())
|
|
{
|
|
selectedFilterValues.Add(null);
|
|
}
|
|
}
|
|
}
|
|
|
|
private class InListCriterionVisitor(int column) : FilterCriterionVisitorBase
|
|
{
|
|
private readonly int targetColumn = column;
|
|
public HashSet<object?> FoundValues { get; } = [];
|
|
|
|
public override void Visit(InListCriterion criterion)
|
|
{
|
|
if (criterion.Column == targetColumn)
|
|
{
|
|
foreach (var value in criterion.Values)
|
|
{
|
|
FoundValues.Add(value);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private sealed class FilterColumnVisitor : FilterCriterionVisitorBase
|
|
{
|
|
public HashSet<int> Columns { get; } = [];
|
|
|
|
protected override void DefaultVisit(FilterCriterion criterion)
|
|
{
|
|
if (criterion is FilterCriterionLeaf leaf)
|
|
{
|
|
Columns.Add(leaf.Column);
|
|
}
|
|
}
|
|
|
|
public override void Visit(TopFilterCriterion criterion) => Columns.Add(criterion.Column);
|
|
|
|
public override void Visit(DynamicFilterCriterion criterion) => Columns.Add(criterion.Column);
|
|
|
|
public override void Visit(CellColorFilterCriterion criterion) => Columns.Add(criterion.Column);
|
|
}
|
|
|
|
internal static bool IsRowHiddenByOtherColumnFilter(Worksheet worksheet, int row, int currentColumn)
|
|
{
|
|
foreach (var filter in worksheet.Filters)
|
|
{
|
|
if (FilterTargetsOnlyColumn(filter, currentColumn))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!filter.Criterion.Matches(worksheet, row))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static bool FilterTargetsOnlyColumn(SheetFilter filter, int column)
|
|
{
|
|
var visitor = new FilterColumnVisitor();
|
|
filter.Criterion.Accept(visitor);
|
|
return visitor.Columns.Count == 1 && visitor.Columns.Contains(column);
|
|
}
|
|
|
|
private async Task OnSortAscendingAsync()
|
|
{
|
|
await SortAscending.InvokeAsync();
|
|
}
|
|
|
|
private async Task OnSortDescendingAsync()
|
|
{
|
|
await SortDescending.InvokeAsync();
|
|
}
|
|
|
|
private async Task OnClearFilterAsync()
|
|
{
|
|
await Clear.InvokeAsync();
|
|
}
|
|
|
|
private async Task OnCancelFilterAsync()
|
|
{
|
|
await Cancel.InvokeAsync();
|
|
}
|
|
|
|
private void OnValueSelectionChanged(object? value, bool isChecked)
|
|
{
|
|
if (isChecked)
|
|
{
|
|
selectedFilterValues.Add(value);
|
|
}
|
|
else
|
|
{
|
|
selectedFilterValues.Remove(value);
|
|
}
|
|
}
|
|
|
|
private bool IsValueSelected(object? value)
|
|
{
|
|
return selectedFilterValues.Contains(value);
|
|
}
|
|
|
|
private bool? IsSelectAllChecked()
|
|
{
|
|
var availableValues = LoadAvailableValues();
|
|
var showBlank = ShouldShowBlankOption();
|
|
|
|
if (availableValues.Count == 0 && !showBlank)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var totalItems = availableValues.Count + (showBlank ? 1 : 0);
|
|
var selectedCount = availableValues.Count(v => selectedFilterValues.Contains(v.Value));
|
|
|
|
if (showBlank && selectedFilterValues.Contains(null))
|
|
{
|
|
selectedCount++;
|
|
}
|
|
|
|
return selectedCount switch
|
|
{
|
|
0 => false,
|
|
var count when count == totalItems => true,
|
|
_ => null
|
|
};
|
|
}
|
|
|
|
private void OnSelectAllChanged(bool? isChecked)
|
|
{
|
|
var availableValues = LoadAvailableValues();
|
|
var showBlank = ShouldShowBlankOption();
|
|
|
|
if (isChecked != false)
|
|
{
|
|
foreach (var (_, Value) in availableValues)
|
|
{
|
|
selectedFilterValues.Add(Value);
|
|
}
|
|
|
|
if (showBlank)
|
|
{
|
|
selectedFilterValues.Add(null);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
|
|
foreach (var (_, Value) in availableValues)
|
|
{
|
|
selectedFilterValues.Remove(Value);
|
|
}
|
|
|
|
selectedFilterValues.Remove(null);
|
|
}
|
|
}
|
|
|
|
private bool ShouldShowBlankOption()
|
|
{
|
|
if (cachedShowBlank.HasValue)
|
|
{
|
|
return cachedShowBlank.Value;
|
|
}
|
|
|
|
var result = ComputeShouldShowBlankOption();
|
|
cachedShowBlank = result;
|
|
return result;
|
|
}
|
|
|
|
private bool ComputeShouldShowBlankOption()
|
|
{
|
|
var rangeToUse = GetActiveFilterRange();
|
|
|
|
if (rangeToUse == RangeRef.Invalid)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Excel treats the first row as a header and excludes it from blank checking
|
|
// Check if any cell in the column (excluding header) has null or empty value
|
|
for (int row = rangeToUse.Start.Row + 1; row <= rangeToUse.End.Row; row++)
|
|
{
|
|
var cell = Worksheet.Cells[row, Column];
|
|
var value = cell.Value;
|
|
var text = cell.GetValue();
|
|
|
|
if (value is null || string.IsNullOrEmpty(text))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private bool IsBlankSelected()
|
|
{
|
|
return selectedFilterValues.Contains(null);
|
|
}
|
|
|
|
private void OnBlankSelectionChanged(bool isChecked)
|
|
{
|
|
if (isChecked)
|
|
{
|
|
selectedFilterValues.Add(null);
|
|
}
|
|
else
|
|
{
|
|
selectedFilterValues.Remove(null);
|
|
}
|
|
}
|
|
|
|
private async Task OnApplyFilterAsync()
|
|
{
|
|
var availableValues = LoadAvailableValues();
|
|
var showBlank = ShouldShowBlankOption();
|
|
var totalItems = availableValues.Count + (showBlank ? 1 : 0);
|
|
var selectedCount = availableValues.Count(v => selectedFilterValues.Contains(v.Value));
|
|
|
|
if (showBlank && selectedFilterValues.Contains(null))
|
|
{
|
|
selectedCount++;
|
|
}
|
|
|
|
// If all items are selected, clear the filter instead
|
|
if (selectedCount == totalItems && totalItems > 0)
|
|
{
|
|
await Clear.InvokeAsync();
|
|
return;
|
|
}
|
|
|
|
SheetFilter? filter = null;
|
|
|
|
if (selectedFilterValues.Count != 0)
|
|
{
|
|
var rangeToUse = GetActiveFilterRange();
|
|
|
|
if (rangeToUse != RangeRef.Invalid)
|
|
{
|
|
var columnRange = new RangeRef(
|
|
new CellRef(rangeToUse.Start.Row, Column),
|
|
new CellRef(rangeToUse.End.Row, Column)
|
|
);
|
|
|
|
filter = new SheetFilter(
|
|
new InListCriterion
|
|
{
|
|
Column = Column,
|
|
Values = [.. selectedFilterValues]
|
|
},
|
|
columnRange
|
|
);
|
|
}
|
|
}
|
|
|
|
await Apply.InvokeAsync(filter);
|
|
}
|
|
|
|
private bool CanApplyFilter()
|
|
{
|
|
return selectedFilterValues.Count != 0;
|
|
}
|
|
|
|
private bool HasFilterApplied()
|
|
=> Worksheet.Filters.Any(f => f.Range.Contains(Row, Column));
|
|
|
|
private List<(string Text, object? Value)> LoadAvailableValues()
|
|
{
|
|
if (cachedAvailableValues is not null)
|
|
{
|
|
return cachedAvailableValues;
|
|
}
|
|
|
|
cachedAvailableValues = ComputeAvailableValues();
|
|
return cachedAvailableValues;
|
|
}
|
|
|
|
private List<(string Text, object? Value)> ComputeAvailableValues()
|
|
{
|
|
var availableValues = new List<(string Text, object? Value)>();
|
|
var rangeToUse = GetActiveFilterRange();
|
|
|
|
if (rangeToUse != RangeRef.Invalid)
|
|
{
|
|
var uniqueValues = new List<(string Text, object? Value)>();
|
|
|
|
// Excel treats the first row as a header and excludes it from the available values
|
|
// Start from the second row (header row + 1)
|
|
for (int row = rangeToUse.Start.Row + 1; row <= rangeToUse.End.Row; row++)
|
|
{
|
|
if (IsRowHiddenByOtherColumnFilter(Worksheet, row, Column))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var cell = Worksheet.Cells[row, Column];
|
|
var value = cell.Value;
|
|
var text = cell.GetValueAsString();
|
|
|
|
if (!string.IsNullOrEmpty(text))
|
|
{
|
|
uniqueValues.Add((text, value));
|
|
}
|
|
}
|
|
|
|
availableValues = [.. uniqueValues
|
|
.DistinctBy(x => x.Value)
|
|
.OrderBy(x => x.Text)];
|
|
}
|
|
|
|
return availableValues;
|
|
}
|
|
|
|
private RangeRef GetActiveFilterRange()
|
|
{
|
|
foreach (var table in Worksheet.Tables)
|
|
{
|
|
if (table.Range.Contains(Row, Column))
|
|
{
|
|
return table.Range;
|
|
}
|
|
}
|
|
|
|
if (Worksheet.AutoFilter.Range is not null && Worksheet.AutoFilter.Range.Value.Contains(Row, Column))
|
|
{
|
|
return Worksheet.AutoFilter.Range.Value;
|
|
}
|
|
|
|
return RangeRef.Invalid;
|
|
}
|
|
|
|
private async Task OnCustomFilterAsync()
|
|
{
|
|
await CustomFilter.InvokeAsync();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Invoked when the user clicks "Top 10..." in the filter dropdown.
|
|
/// </summary>
|
|
[Parameter]
|
|
public EventCallback Top10Filter { get; set; }
|
|
|
|
/// <summary>
|
|
/// Invoked when the user clicks one of the dynamic filter shortcut items.
|
|
/// </summary>
|
|
[Parameter]
|
|
public EventCallback<DynamicFilterType> DynamicFilter { get; set; }
|
|
|
|
private async Task OnTop10FilterAsync()
|
|
{
|
|
await Top10Filter.InvokeAsync();
|
|
}
|
|
|
|
private async Task OnDynamicFilterAsync(DynamicFilterType type)
|
|
{
|
|
await DynamicFilter.InvokeAsync(type);
|
|
}
|
|
} |