Skip to content

Commit fdf68a2

Browse files
feat: add column resizing and reordering
1 parent 5e5a446 commit fdf68a2

15 files changed

Lines changed: 959 additions & 26 deletions

‎Components/FancyColumn.cs‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ public sealed class FancyColumn<TItem>
88
public bool Sortable { get; init; }
99
public bool Searchable { get; init; } = true;
1010
public bool Hideable { get; init; }
11+
public bool Resizable { get; init; } = true;
12+
public bool Reorderable { get; init; } = true;
13+
14+
public double? Width { get; init; }
15+
public double? MinWidth { get; init; }
16+
public double? MaxWidth { get; init; }
1117

1218
public string? HeaderClass { get; init; }
1319
public string? CellClass { get; init; }
@@ -22,4 +28,4 @@ public sealed class FancyColumn<TItem>
2228
public Func<TItem, string?>? SearchTextSelector { get; init; }
2329

2430
public RenderFragment<TItem>? CellTemplate { get; init; }
25-
}
31+
}

‎Components/FancyColumnLayout.cs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
public sealed record FancyColumnLayout(string Key, double? Width = null);
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
internal sealed class FancyColumnLayoutController<TItem>
2+
{
3+
private readonly List<string> order = [];
4+
private readonly Dictionary<string, double> widths = new(StringComparer.Ordinal);
5+
private IReadOnlyDictionary<string, FancyColumn<TItem>> columns = new Dictionary<string, FancyColumn<TItem>>();
6+
private bool initialized;
7+
8+
public IReadOnlyDictionary<string, double> Widths => widths;
9+
10+
public void Synchronize(
11+
IReadOnlyList<FancyColumn<TItem>> sourceColumns,
12+
IReadOnlyList<FancyColumnLayout>? initialLayout,
13+
string? pinnedColumnKey)
14+
{
15+
columns = sourceColumns
16+
.GroupBy(column => column.Key, StringComparer.Ordinal)
17+
.ToDictionary(group => group.Key, group => group.First(), StringComparer.Ordinal);
18+
19+
if (!initialized)
20+
{
21+
HashSet<string> addedKeys = new(StringComparer.Ordinal);
22+
foreach (FancyColumnLayout item in initialLayout ?? [])
23+
{
24+
if (columns.ContainsKey(item.Key) && addedKeys.Add(item.Key))
25+
{
26+
order.Add(item.Key);
27+
if (item.Width is double width && double.IsFinite(width))
28+
{
29+
widths[item.Key] = ClampWidth(item.Key, width);
30+
}
31+
}
32+
}
33+
34+
foreach (FancyColumn<TItem> column in sourceColumns)
35+
{
36+
if (addedKeys.Add(column.Key))
37+
{
38+
order.Add(column.Key);
39+
}
40+
41+
if (!widths.ContainsKey(column.Key) && column.Width is double width && double.IsFinite(width))
42+
{
43+
widths[column.Key] = ClampWidth(column.Key, width);
44+
}
45+
}
46+
47+
initialized = true;
48+
}
49+
else
50+
{
51+
order.RemoveAll(key => !columns.ContainsKey(key));
52+
widths.Keys.Where(key => !columns.ContainsKey(key)).ToList().ForEach(key => widths.Remove(key));
53+
54+
foreach (FancyColumn<TItem> column in sourceColumns)
55+
{
56+
if (!order.Contains(column.Key, StringComparer.Ordinal))
57+
{
58+
order.Add(column.Key);
59+
if (column.Width is double width && double.IsFinite(width))
60+
{
61+
widths[column.Key] = ClampWidth(column.Key, width);
62+
}
63+
}
64+
}
65+
}
66+
67+
if (pinnedColumnKey is not null && order.Remove(pinnedColumnKey))
68+
{
69+
order.Insert(0, pinnedColumnKey);
70+
}
71+
72+
foreach (string key in widths.Keys.ToList())
73+
{
74+
widths[key] = ClampWidth(key, widths[key]);
75+
}
76+
}
77+
78+
public IReadOnlyList<FancyColumn<TItem>> GetOrderedColumns() =>
79+
order.Where(columns.ContainsKey).Select(key => columns[key]).ToList();
80+
81+
public bool IsConstrained(string key) =>
82+
widths.ContainsKey(key) || (columns.TryGetValue(key, out FancyColumn<TItem>? column) && column.MaxWidth.HasValue);
83+
84+
public string? GetColumnStyle(string key)
85+
{
86+
if (widths.TryGetValue(key, out double width))
87+
{
88+
return FormattableString.Invariant($"width:{width:0.##}px;min-width:{width:0.##}px;max-width:{width:0.##}px;");
89+
}
90+
91+
return columns.TryGetValue(key, out FancyColumn<TItem>? column) && column.MaxWidth is double maxWidth
92+
? FormattableString.Invariant($"max-width:{maxWidth:0.##}px;")
93+
: null;
94+
}
95+
96+
public void ApplyMeasuredWidths(IEnumerable<FancyColumnLayout> measurements)
97+
{
98+
foreach (FancyColumnLayout measurement in measurements)
99+
{
100+
if (columns.ContainsKey(measurement.Key)
101+
&& measurement.Width is double width
102+
&& double.IsFinite(width))
103+
{
104+
widths[measurement.Key] = ClampWidth(measurement.Key, width);
105+
}
106+
}
107+
}
108+
109+
public bool TryReorder(string sourceKey, string targetKey, bool insertAfter, string? pinnedColumnKey)
110+
{
111+
if (sourceKey == targetKey
112+
|| !columns.TryGetValue(sourceKey, out FancyColumn<TItem>? source)
113+
|| !columns.TryGetValue(targetKey, out FancyColumn<TItem>? target)
114+
|| !source.Reorderable
115+
|| !target.Reorderable
116+
|| sourceKey == pinnedColumnKey
117+
|| targetKey == pinnedColumnKey)
118+
{
119+
return false;
120+
}
121+
122+
int sourceIndex = order.IndexOf(sourceKey);
123+
int targetIndex = order.IndexOf(targetKey);
124+
int lower = Math.Min(sourceIndex, targetIndex);
125+
int upper = Math.Max(sourceIndex, targetIndex);
126+
if (sourceIndex < 0 || targetIndex < 0
127+
|| order.Skip(lower + 1).Take(upper - lower - 1).Any(key => !columns[key].Reorderable || key == pinnedColumnKey))
128+
{
129+
return false;
130+
}
131+
132+
order.RemoveAt(sourceIndex);
133+
targetIndex = order.IndexOf(targetKey);
134+
order.Insert(targetIndex + (insertAfter ? 1 : 0), sourceKey);
135+
return true;
136+
}
137+
138+
public IReadOnlyList<FancyColumnLayout> GetLayout() =>
139+
order.Where(columns.ContainsKey)
140+
.Select(key => new FancyColumnLayout(key, widths.TryGetValue(key, out double width) ? width : null))
141+
.ToList();
142+
143+
private double ClampWidth(string key, double width)
144+
{
145+
FancyColumn<TItem> column = columns[key];
146+
double minimum = Math.Max(24, column.MinWidth ?? 24);
147+
double maximum = Math.Max(minimum, column.MaxWidth ?? double.MaxValue);
148+
return Math.Clamp(width, minimum, maximum);
149+
}
150+
}

‎Components/FancyRow.razor‎

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ else
1111
{
1212
FancyColumn<TItem> column = VisibleColumns[index];
1313

14-
<td class="@column.CellClass">
14+
<td class="@GetCellClass(column)" style="@GetCellStyle(column)">
1515
@if (index == 0 && RowLeadingContentTemplate is not null)
1616
{
1717
@RowLeadingContentTemplate(RowContext)
@@ -23,7 +23,10 @@ else
2323
}
2424
else
2525
{
26-
@column.ValueSelector?.Invoke(Item)
26+
<span class="fancy-cell-value"
27+
data-expandable="@(IsColumnConstrained(column) ? "true" : "false")">
28+
@column.ValueSelector?.Invoke(Item)
29+
</span>
2730
}
2831
</td>
2932
}
@@ -37,6 +40,9 @@ else
3740
[Parameter, EditorRequired]
3841
public required IReadOnlyList<FancyColumn<TItem>> VisibleColumns { get; set; }
3942

43+
[Parameter, EditorRequired]
44+
public required IReadOnlyDictionary<string, double> ColumnWidths { get; set; }
45+
4046
[Parameter]
4147
public Func<TItem, string?>? RowClassSelector { get; set; }
4248

@@ -71,4 +77,24 @@ else
7177
private string? GetRowClass() => RowClassSelector?.Invoke(Item);
7278

7379
private IReadOnlyDictionary<string, object?>? GetRowAttributes() => RowAttributesSelector?.Invoke(RowContext);
80+
81+
private string? GetCellClass(FancyColumn<TItem> column) =>
82+
IsColumnConstrained(column)
83+
? string.IsNullOrWhiteSpace(column.CellClass) ? "fancy-cell-constrained" : $"{column.CellClass} fancy-cell-constrained"
84+
: column.CellClass;
85+
86+
private string? GetCellStyle(FancyColumn<TItem> column)
87+
{
88+
if (ColumnWidths.TryGetValue(column.Key, out double width))
89+
{
90+
return FormattableString.Invariant($"width:{width:0.##}px;max-width:{width:0.##}px;");
91+
}
92+
93+
return column.MaxWidth is double maxWidth
94+
? FormattableString.Invariant($"max-width:{maxWidth:0.##}px;")
95+
: null;
96+
}
97+
98+
private bool IsColumnConstrained(FancyColumn<TItem> column) =>
99+
ColumnWidths.ContainsKey(column.Key) || column.MaxWidth.HasValue;
74100
}

0 commit comments

Comments
 (0)