@cahyo-dimas/freeday 1.14.0 → 1.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -0
- package/README.id.md +1 -1
- package/README.md +1 -1
- package/adapters/blazor/CflTypes.cs +11 -0
- package/adapters/blazor/FdyAutocomplete.razor +19 -0
- package/adapters/blazor/FdyAutocomplete.razor.cs +40 -0
- package/adapters/blazor/FdyCascade.razor +33 -0
- package/adapters/blazor/FdyCascade.razor.cs +38 -0
- package/adapters/blazor/FdyCascadeNode.cs +8 -0
- package/adapters/blazor/FdyCfl.razor +93 -0
- package/adapters/blazor/FdyCfl.razor.cs +146 -0
- package/adapters/blazor/FdyChart.razor +25 -0
- package/adapters/blazor/FdyChart.razor.cs +90 -0
- package/adapters/blazor/FdyChartSeries.cs +10 -0
- package/adapters/blazor/FdyCombo.razor +28 -0
- package/adapters/blazor/FdyCombo.razor.cs +86 -0
- package/adapters/blazor/FdyComboOption.cs +7 -0
- package/adapters/blazor/FdyDateRange.razor +16 -0
- package/adapters/blazor/FdyDateRange.razor.cs +58 -0
- package/adapters/blazor/FdyDatepicker.razor +13 -0
- package/adapters/blazor/FdyDatepicker.razor.cs +39 -0
- package/adapters/blazor/FdyDrawer.razor +36 -0
- package/adapters/blazor/FdyDrawer.razor.cs +88 -0
- package/adapters/blazor/FdyModal.razor +36 -0
- package/adapters/blazor/FdyModal.razor.cs +88 -0
- package/adapters/blazor/FdyTable.razor +140 -0
- package/adapters/blazor/FdyTable.razor.cs +200 -0
- package/adapters/blazor/FdyTableFilter.razor +77 -0
- package/adapters/blazor/FdyTableFilter.razor.cs +117 -0
- package/adapters/blazor/Freeday.Blazor.csproj +27 -0
- package/adapters/blazor/FreedayComponentBase.cs +73 -0
- package/adapters/blazor/TableModel.cs +196 -0
- package/adapters/blazor/TableTypes.cs +69 -0
- package/adapters/blazor/_Imports.razor +5 -0
- package/adapters/blazor/freeday-blazor.js +101 -3
- package/dist/asset.d.ts +6 -0
- package/dist/freeday-select.js +32 -8
- package/dist/freeday.d.ts +24 -0
- package/dist/freeday.js +32 -8
- package/package.json +14 -8
- package/src/freeday-select.js +32 -8
- package/tokens/breakpoints.d.ts +8 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
using Microsoft.AspNetCore.Components;
|
|
2
|
+
using Microsoft.AspNetCore.Components.Web;
|
|
3
|
+
|
|
4
|
+
namespace Freeday.Blazor;
|
|
5
|
+
|
|
6
|
+
public partial class FdyTable<TRow>
|
|
7
|
+
{
|
|
8
|
+
/// <summary>Column definitions (order = display order).</summary>
|
|
9
|
+
[Parameter, EditorRequired] public IReadOnlyList<FdyTableColumn<TRow>> Columns { get; set; } = Array.Empty<FdyTableColumn<TRow>>();
|
|
10
|
+
|
|
11
|
+
/// <summary>The rows to display. The single source of truth, read on every render.</summary>
|
|
12
|
+
[Parameter, EditorRequired] public IReadOnlyList<TRow> Rows { get; set; } = Array.Empty<TRow>();
|
|
13
|
+
|
|
14
|
+
/// <summary>A stable key per row (for keying + expansion identity).</summary>
|
|
15
|
+
[Parameter, EditorRequired] public Func<TRow, object> RowKey { get; set; } = default!;
|
|
16
|
+
|
|
17
|
+
/// <summary>Controlled sort. Wire <c>@bind-Sort</c> (or SortChanged) to own sorting; omit for internal.</summary>
|
|
18
|
+
[Parameter] public FdySortState? Sort { get; set; }
|
|
19
|
+
[Parameter] public EventCallback<FdySortState?> SortChanged { get; set; }
|
|
20
|
+
|
|
21
|
+
/// <summary>Controlled filter map keyed by column key. Wire FiltersChanged to own filtering; omit for internal.</summary>
|
|
22
|
+
[Parameter] public IReadOnlyDictionary<string, FdyColumnFilter>? Filters { get; set; }
|
|
23
|
+
[Parameter] public EventCallback<IReadOnlyDictionary<string, FdyColumnFilter>> FiltersChanged { get; set; }
|
|
24
|
+
|
|
25
|
+
/// <summary>Server pagination state (0-based). Presence switches the table into server mode.</summary>
|
|
26
|
+
[Parameter] public FdyPageState? Page { get; set; }
|
|
27
|
+
[Parameter] public EventCallback<FdyPageState> PageChanged { get; set; }
|
|
28
|
+
|
|
29
|
+
/// <summary>Client-side page size when <see cref="Page"/> is absent; 0 = render all rows (no pager).</summary>
|
|
30
|
+
[Parameter] public int PageSize { get; set; }
|
|
31
|
+
|
|
32
|
+
[Parameter] public bool Loading { get; set; }
|
|
33
|
+
[Parameter] public string LoadingText { get; set; } = "Loading…";
|
|
34
|
+
[Parameter] public string EmptyText { get; set; } = "No data";
|
|
35
|
+
[Parameter] public RenderFragment? EmptyContent { get; set; }
|
|
36
|
+
[Parameter] public string? AriaLabel { get; set; }
|
|
37
|
+
|
|
38
|
+
/// <summary>Opt in to row activation: rows become focusable and raise <see cref="RowActivate"/>.</summary>
|
|
39
|
+
[Parameter] public bool RowActivatable { get; set; }
|
|
40
|
+
[Parameter] public EventCallback<TRow> RowActivate { get; set; }
|
|
41
|
+
|
|
42
|
+
/// <summary>Per-row class hook, e.g. to mark a selected row.</summary>
|
|
43
|
+
[Parameter] public Func<TRow, string?>? RowClass { get; set; }
|
|
44
|
+
|
|
45
|
+
/// <summary>Controlled: row keys whose <see cref="RowDetail"/> is shown beneath them.</summary>
|
|
46
|
+
[Parameter] public IReadOnlyList<object>? ExpandedKeys { get; set; }
|
|
47
|
+
[Parameter] public RenderFragment<TRow>? RowDetail { get; set; }
|
|
48
|
+
|
|
49
|
+
/// <summary>Optional toolbar (search box, actions) above the table.</summary>
|
|
50
|
+
[Parameter] public RenderFragment? Toolbar { get; set; }
|
|
51
|
+
|
|
52
|
+
private FdySortState? _internalSort;
|
|
53
|
+
private Dictionary<string, FdyColumnFilter> _internalFilters = new();
|
|
54
|
+
private int _internalPageIndex;
|
|
55
|
+
|
|
56
|
+
private List<TRow> _displayRows = new();
|
|
57
|
+
private int _totalCount;
|
|
58
|
+
|
|
59
|
+
private static readonly IReadOnlyDictionary<string, FdyColumnFilter> EmptyFilters = new Dictionary<string, FdyColumnFilter>();
|
|
60
|
+
|
|
61
|
+
private bool ServerPaged => Page is not null;
|
|
62
|
+
private bool SortControlled => ServerPaged || SortChanged.HasDelegate;
|
|
63
|
+
private bool FiltersControlled => ServerPaged || FiltersChanged.HasDelegate;
|
|
64
|
+
|
|
65
|
+
private FdySortState? EffectiveSort => SortControlled ? Sort : _internalSort;
|
|
66
|
+
private IReadOnlyDictionary<string, FdyColumnFilter> EffectiveFilters =>
|
|
67
|
+
FiltersControlled ? (Filters ?? EmptyFilters) : _internalFilters;
|
|
68
|
+
|
|
69
|
+
private int PageSizeEff => ServerPaged ? Page!.Size : PageSize;
|
|
70
|
+
private int CurrentPage1 => (ServerPaged ? Page!.Index : _internalPageIndex) + 1;
|
|
71
|
+
private int TotalCount => _totalCount;
|
|
72
|
+
private int TotalPages => PageSizeEff > 0 ? Math.Max(1, (int)Math.Ceiling((double)TotalCount / PageSizeEff)) : 1;
|
|
73
|
+
private bool HasPager => PageSizeEff > 0 && TotalPages > 1;
|
|
74
|
+
private List<int?> Pages => TableModel.PageWindow(CurrentPage1, TotalPages);
|
|
75
|
+
private int RangeFrom => TotalCount == 0 ? 0 : (CurrentPage1 - 1) * PageSizeEff + 1;
|
|
76
|
+
private int RangeTo => TotalCount == 0 ? 0 : RangeFrom - 1 + _displayRows.Count;
|
|
77
|
+
|
|
78
|
+
protected override void OnParametersSet() => Recompute();
|
|
79
|
+
|
|
80
|
+
// Recompute the visible rows from the current effective sort/filter/page. Called on every
|
|
81
|
+
// parameter change and after any internal-state mutation (sort/filter/page click).
|
|
82
|
+
private void Recompute()
|
|
83
|
+
{
|
|
84
|
+
List<TRow> filteredSorted = ServerPaged
|
|
85
|
+
? Rows.ToList()
|
|
86
|
+
: TableModel.SortRows(TableModel.FilterRows(Rows, Columns, EffectiveFilters), Columns, EffectiveSort);
|
|
87
|
+
|
|
88
|
+
_totalCount = ServerPaged ? Page!.Total : filteredSorted.Count;
|
|
89
|
+
|
|
90
|
+
if (!ServerPaged && PageSize > 0)
|
|
91
|
+
{
|
|
92
|
+
// Keep the page in range when a filter shrank the row set.
|
|
93
|
+
int tp = Math.Max(1, (int)Math.Ceiling((double)_totalCount / PageSize));
|
|
94
|
+
if (_internalPageIndex > tp - 1) _internalPageIndex = Math.Max(0, tp - 1);
|
|
95
|
+
_displayRows = TableModel.Paginate(filteredSorted, _internalPageIndex, PageSize);
|
|
96
|
+
}
|
|
97
|
+
else
|
|
98
|
+
{
|
|
99
|
+
_displayRows = filteredSorted;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private FdyColumnFilter? GetFilter(string key) => EffectiveFilters.TryGetValue(key, out FdyColumnFilter? f) ? f : null;
|
|
104
|
+
|
|
105
|
+
private IReadOnlyList<string> EnumOptions(FdyTableColumn<TRow> col) =>
|
|
106
|
+
col.Options ?? TableModel.DistinctValues(Rows, col);
|
|
107
|
+
|
|
108
|
+
private string? AriaSortOf(FdyTableColumn<TRow> col)
|
|
109
|
+
{
|
|
110
|
+
FdySortState? s = EffectiveSort;
|
|
111
|
+
if (s is null || s.Key != col.Key) return null;
|
|
112
|
+
return s.Dir == FdySortDir.Asc ? "ascending" : "descending";
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private async Task OnSortClick(FdyTableColumn<TRow> col)
|
|
116
|
+
{
|
|
117
|
+
if (!col.Sortable) return;
|
|
118
|
+
FdySortState? cur = EffectiveSort;
|
|
119
|
+
FdySortState next = cur is not null && cur.Key == col.Key
|
|
120
|
+
? new FdySortState(col.Key, cur.Dir == FdySortDir.Asc ? FdySortDir.Desc : FdySortDir.Asc)
|
|
121
|
+
: new FdySortState(col.Key, FdySortDir.Asc);
|
|
122
|
+
if (SortControlled)
|
|
123
|
+
{
|
|
124
|
+
await SortChanged.InvokeAsync(next);
|
|
125
|
+
}
|
|
126
|
+
else
|
|
127
|
+
{
|
|
128
|
+
_internalSort = next;
|
|
129
|
+
_internalPageIndex = 0;
|
|
130
|
+
Recompute();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private async Task OnFilterChange(FdyTableColumn<TRow> col, FdyColumnFilter? filter)
|
|
135
|
+
{
|
|
136
|
+
Dictionary<string, FdyColumnFilter> next = new(EffectiveFilters);
|
|
137
|
+
if (filter is null) next.Remove(col.Key);
|
|
138
|
+
else next[col.Key] = filter;
|
|
139
|
+
if (FiltersControlled)
|
|
140
|
+
{
|
|
141
|
+
await FiltersChanged.InvokeAsync(next);
|
|
142
|
+
}
|
|
143
|
+
else
|
|
144
|
+
{
|
|
145
|
+
_internalFilters = next;
|
|
146
|
+
_internalPageIndex = 0;
|
|
147
|
+
Recompute();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private async Task GoTo(int page1)
|
|
152
|
+
{
|
|
153
|
+
int clamped = Math.Min(Math.Max(1, page1), TotalPages);
|
|
154
|
+
int index0 = clamped - 1;
|
|
155
|
+
if (ServerPaged)
|
|
156
|
+
{
|
|
157
|
+
await PageChanged.InvokeAsync(new FdyPageState(index0, Page!.Size, Page.Total));
|
|
158
|
+
}
|
|
159
|
+
else
|
|
160
|
+
{
|
|
161
|
+
_internalPageIndex = index0;
|
|
162
|
+
Recompute();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private string? CellClass(FdyTableColumn<TRow> col) => col.Mono ? "fdy-mono" : null;
|
|
167
|
+
|
|
168
|
+
private static string? AlignStyle(FdyTableColumn<TRow> col) => col.Align switch
|
|
169
|
+
{
|
|
170
|
+
FdyColumnAlign.Right => "text-align:right",
|
|
171
|
+
FdyColumnAlign.Center => "text-align:center",
|
|
172
|
+
FdyColumnAlign.Left => "text-align:left",
|
|
173
|
+
_ => null,
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
private string RowClasses(TRow row)
|
|
177
|
+
{
|
|
178
|
+
string? custom = RowClass?.Invoke(row);
|
|
179
|
+
string activatable = RowActivatable ? "fdy-table__row--activatable" : string.Empty;
|
|
180
|
+
return string.Join(" ", new[] { custom, activatable }.Where(s => !string.IsNullOrEmpty(s))!);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private async Task OnRowClick(TRow row)
|
|
184
|
+
{
|
|
185
|
+
if (RowActivatable) await RowActivate.InvokeAsync(row);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Enter/Space activate a focused row. Blazor's KeyboardEventArgs can't tell whether the row
|
|
189
|
+
// itself vs an inner control is the target, so activatable rows should not embed focusable
|
|
190
|
+
// controls (the common drill-in-row pattern) — matching the Vue guard's practical effect.
|
|
191
|
+
private async Task OnRowKeydown(KeyboardEventArgs e, TRow row)
|
|
192
|
+
{
|
|
193
|
+
if (!RowActivatable) return;
|
|
194
|
+
if (e.Key is not ("Enter" or " ")) return;
|
|
195
|
+
await RowActivate.InvokeAsync(row);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private bool IsExpanded(TRow row) => ExpandedKeys?.Contains(RowKey(row)) == true;
|
|
199
|
+
private string? ExpandedAttr(TRow row) => RowDetail is null ? null : (IsExpanded(row) ? "true" : "false");
|
|
200
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
@namespace Freeday.Blazor
|
|
2
|
+
|
|
3
|
+
@* Internal to FdyTable: one column's header funnel button + its type-aware filter popover
|
|
4
|
+
(text / enum / number / date), over freeday's .fdy-table__filterbtn + .fdy-filter* classes.
|
|
5
|
+
The panel is position:fixed (top layer) so it escapes the table's overflow; light-dismiss
|
|
6
|
+
(outside pointer / Esc) is wired through FreedayBlazor.onOutside. Purely controlled — it renders
|
|
7
|
+
the current Filter and raises the next one (or null to clear). *@
|
|
8
|
+
<span @ref="_root" class="fdy-table__filterwrap">
|
|
9
|
+
<button type="button"
|
|
10
|
+
class="@(Active ? "fdy-table__filterbtn is-active" : "fdy-table__filterbtn")"
|
|
11
|
+
aria-haspopup="dialog"
|
|
12
|
+
aria-pressed="@Active"
|
|
13
|
+
aria-expanded="@_open"
|
|
14
|
+
aria-label="@($"Filter {Label}")"
|
|
15
|
+
@onclick="Toggle"
|
|
16
|
+
@onclick:stopPropagation="true">
|
|
17
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
18
|
+
<path d="M3 5h18l-7 8v5l-4 2v-7z" />
|
|
19
|
+
</svg>
|
|
20
|
+
</button>
|
|
21
|
+
|
|
22
|
+
@if (_open)
|
|
23
|
+
{
|
|
24
|
+
<div class="fdy-filter" role="dialog" aria-label="@($"Filter {Label}")">
|
|
25
|
+
@switch (Type)
|
|
26
|
+
{
|
|
27
|
+
case FdyColumnFilterType.Text:
|
|
28
|
+
<div class="fdy-filter__title">Contains text</div>
|
|
29
|
+
<input class="fdy-input" type="search" placeholder="Contains…" value="@TextValue"
|
|
30
|
+
@oninput="OnTextInput" />
|
|
31
|
+
break;
|
|
32
|
+
|
|
33
|
+
case FdyColumnFilterType.Enum:
|
|
34
|
+
<div class="fdy-filter__title">Show values</div>
|
|
35
|
+
<div class="fdy-filter__list">
|
|
36
|
+
@foreach (string val in Options)
|
|
37
|
+
{
|
|
38
|
+
<label class="fdy-filter__check">
|
|
39
|
+
<input type="checkbox" class="fdy-checkbox"
|
|
40
|
+
checked="@EnumValues.Contains(val)"
|
|
41
|
+
@onchange="e => OnEnumToggle(val, (bool)(e.Value ?? false))" />
|
|
42
|
+
@val
|
|
43
|
+
</label>
|
|
44
|
+
}
|
|
45
|
+
</div>
|
|
46
|
+
break;
|
|
47
|
+
|
|
48
|
+
case FdyColumnFilterType.Number:
|
|
49
|
+
<div class="fdy-filter__title">Value range</div>
|
|
50
|
+
<div class="fdy-filter__range">
|
|
51
|
+
<input class="fdy-input" type="text" inputmode="numeric" placeholder="Min" value="@NumMin"
|
|
52
|
+
@oninput="e => OnNumber(true, e.Value?.ToString())" />
|
|
53
|
+
<span aria-hidden="true">–</span>
|
|
54
|
+
<input class="fdy-input" type="text" inputmode="numeric" placeholder="Max" value="@NumMax"
|
|
55
|
+
@oninput="e => OnNumber(false, e.Value?.ToString())" />
|
|
56
|
+
</div>
|
|
57
|
+
break;
|
|
58
|
+
|
|
59
|
+
case FdyColumnFilterType.Date:
|
|
60
|
+
<div class="fdy-filter__title">Date range</div>
|
|
61
|
+
<div class="fdy-filter__range">
|
|
62
|
+
<input class="fdy-input" type="date" aria-label="From" value="@DateFrom"
|
|
63
|
+
@oninput="e => OnDate(true, e.Value?.ToString())" />
|
|
64
|
+
<span aria-hidden="true">–</span>
|
|
65
|
+
<input class="fdy-input" type="date" aria-label="To" value="@DateTo"
|
|
66
|
+
@oninput="e => OnDate(false, e.Value?.ToString())" />
|
|
67
|
+
</div>
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
<div class="fdy-filter__foot">
|
|
72
|
+
<button type="button" class="fdy-btn fdy-btn--ghost fdy-btn--sm" @onclick="Reset">Reset</button>
|
|
73
|
+
<button type="button" class="fdy-btn fdy-btn--sm" @onclick="() => Close()">Close</button>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
}
|
|
77
|
+
</span>
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
using System.Globalization;
|
|
2
|
+
using Microsoft.AspNetCore.Components;
|
|
3
|
+
using Microsoft.JSInterop;
|
|
4
|
+
|
|
5
|
+
namespace Freeday.Blazor;
|
|
6
|
+
|
|
7
|
+
public partial class FdyTableFilter : ComponentBase, IAsyncDisposable
|
|
8
|
+
{
|
|
9
|
+
[Inject] private IJSRuntime JS { get; set; } = default!;
|
|
10
|
+
|
|
11
|
+
[Parameter, EditorRequired] public string Label { get; set; } = string.Empty;
|
|
12
|
+
[Parameter, EditorRequired] public FdyColumnFilterType Type { get; set; }
|
|
13
|
+
|
|
14
|
+
/// <summary>The current filter for this column (null = none).</summary>
|
|
15
|
+
[Parameter] public FdyColumnFilter? Filter { get; set; }
|
|
16
|
+
|
|
17
|
+
/// <summary>Distinct values for an enum filter (computed by the parent over the full row set).</summary>
|
|
18
|
+
[Parameter] public IReadOnlyList<string> Options { get; set; } = Array.Empty<string>();
|
|
19
|
+
|
|
20
|
+
/// <summary>The next filter for this column, or null to clear it.</summary>
|
|
21
|
+
[Parameter] public EventCallback<FdyColumnFilter?> OnChange { get; set; }
|
|
22
|
+
|
|
23
|
+
private ElementReference _root;
|
|
24
|
+
private bool _open;
|
|
25
|
+
private DotNetObjectReference<FdyTableFilter>? _self;
|
|
26
|
+
private int _outsideToken;
|
|
27
|
+
|
|
28
|
+
private bool Active => TableModel.IsFilterActive(Filter);
|
|
29
|
+
|
|
30
|
+
// Typed reads of the current filter for the inputs (fall back to the empty shape per type).
|
|
31
|
+
private string TextValue => (Filter as FdyColumnFilter.Text)?.Value ?? string.Empty;
|
|
32
|
+
private IReadOnlyList<string> EnumValues => (Filter as FdyColumnFilter.Enum)?.Values ?? Array.Empty<string>();
|
|
33
|
+
private string NumMin => (Filter as FdyColumnFilter.Number)?.Min?.ToString(CultureInfo.InvariantCulture) ?? string.Empty;
|
|
34
|
+
private string NumMax => (Filter as FdyColumnFilter.Number)?.Max?.ToString(CultureInfo.InvariantCulture) ?? string.Empty;
|
|
35
|
+
private string DateFrom => (Filter as FdyColumnFilter.Date)?.From ?? string.Empty;
|
|
36
|
+
private string DateTo => (Filter as FdyColumnFilter.Date)?.To ?? string.Empty;
|
|
37
|
+
|
|
38
|
+
private async Task Apply(FdyColumnFilter next)
|
|
39
|
+
=> await OnChange.InvokeAsync(TableModel.IsFilterActive(next) ? next : null);
|
|
40
|
+
|
|
41
|
+
private static double? ParseNum(string? v)
|
|
42
|
+
{
|
|
43
|
+
string t = (v ?? string.Empty).Trim();
|
|
44
|
+
if (t.Length == 0) return null;
|
|
45
|
+
return double.TryParse(t, NumberStyles.Any, CultureInfo.InvariantCulture, out double n) ? n : null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private Task OnTextInput(ChangeEventArgs e) => Apply(new FdyColumnFilter.Text(e.Value?.ToString() ?? string.Empty));
|
|
49
|
+
|
|
50
|
+
private Task OnEnumToggle(string value, bool check)
|
|
51
|
+
{
|
|
52
|
+
List<string> set = EnumValues.Where(v => v != value).ToList();
|
|
53
|
+
if (check) set.Add(value);
|
|
54
|
+
return Apply(new FdyColumnFilter.Enum(set));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private Task OnNumber(bool isMin, string? raw)
|
|
58
|
+
{
|
|
59
|
+
double? v = ParseNum(raw);
|
|
60
|
+
return Apply(new FdyColumnFilter.Number(
|
|
61
|
+
isMin ? v : ParseNum(NumMin),
|
|
62
|
+
isMin ? ParseNum(NumMax) : v));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private Task OnDate(bool isFrom, string? raw)
|
|
66
|
+
{
|
|
67
|
+
string? val = string.IsNullOrEmpty(raw) ? null : raw;
|
|
68
|
+
return Apply(new FdyColumnFilter.Date(
|
|
69
|
+
isFrom ? val : (string.IsNullOrEmpty(DateFrom) ? null : DateFrom),
|
|
70
|
+
isFrom ? (string.IsNullOrEmpty(DateTo) ? null : DateTo) : val));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private async Task Toggle()
|
|
74
|
+
{
|
|
75
|
+
if (_open) { await Close(); return; }
|
|
76
|
+
_open = true;
|
|
77
|
+
_self ??= DotNetObjectReference.Create(this);
|
|
78
|
+
_outsideToken = await JS.InvokeAsync<int>("FreedayBlazor.onOutside", _root, _self, nameof(CloseFromJs));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private async Task Close()
|
|
82
|
+
{
|
|
83
|
+
if (!_open) return;
|
|
84
|
+
_open = false;
|
|
85
|
+
if (_outsideToken != 0)
|
|
86
|
+
{
|
|
87
|
+
try { await JS.InvokeVoidAsync("FreedayBlazor.off", _outsideToken); }
|
|
88
|
+
catch (JSDisconnectedException) { /* runtime gone */ }
|
|
89
|
+
_outsideToken = 0;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/// <summary>Invoked by the bridge on an outside pointerdown or Escape.</summary>
|
|
94
|
+
[JSInvokable]
|
|
95
|
+
public async Task CloseFromJs()
|
|
96
|
+
{
|
|
97
|
+
await Close();
|
|
98
|
+
StateHasChanged();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private async Task Reset()
|
|
102
|
+
{
|
|
103
|
+
await OnChange.InvokeAsync(null);
|
|
104
|
+
await Close();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
public async ValueTask DisposeAsync()
|
|
108
|
+
{
|
|
109
|
+
if (_outsideToken != 0)
|
|
110
|
+
{
|
|
111
|
+
try { await JS.InvokeVoidAsync("FreedayBlazor.off", _outsideToken); }
|
|
112
|
+
catch (JSDisconnectedException) { /* runtime gone */ }
|
|
113
|
+
}
|
|
114
|
+
_self?.Dispose();
|
|
115
|
+
GC.SuppressFinalize(this);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<!-- Freeday — Blazor component library (Razor Class Library).
|
|
2
|
+
Typed wrappers over the vanilla `fdy-*` enhancers; the host page must load
|
|
3
|
+
dist/freeday.js + adapters/blazor/freeday-blazor.js. Targets net8.0 (LTS) so it can
|
|
4
|
+
be referenced from net8/9/10 apps. Consume via <ProjectReference> to this .csproj
|
|
5
|
+
(e.g. from node_modules/@cahyo-dimas/freeday/adapters/blazor/). -->
|
|
6
|
+
<Project Sdk="Microsoft.NET.Sdk.Razor">
|
|
7
|
+
|
|
8
|
+
<PropertyGroup>
|
|
9
|
+
<TargetFramework>net8.0</TargetFramework>
|
|
10
|
+
<Nullable>enable</Nullable>
|
|
11
|
+
<ImplicitUsings>enable</ImplicitUsings>
|
|
12
|
+
<RootNamespace>Freeday.Blazor</RootNamespace>
|
|
13
|
+
<AssemblyName>Freeday.Blazor</AssemblyName>
|
|
14
|
+
<IsPackable>false</IsPackable>
|
|
15
|
+
<!-- freeday-blazor.js is shipped/loaded as a plain <script>, not an RCL static asset. -->
|
|
16
|
+
<DefaultItemExcludes>$(DefaultItemExcludes);freeday-blazor.js</DefaultItemExcludes>
|
|
17
|
+
</PropertyGroup>
|
|
18
|
+
|
|
19
|
+
<ItemGroup>
|
|
20
|
+
<SupportedPlatform Include="browser" />
|
|
21
|
+
</ItemGroup>
|
|
22
|
+
|
|
23
|
+
<ItemGroup>
|
|
24
|
+
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="8.0.8" />
|
|
25
|
+
</ItemGroup>
|
|
26
|
+
|
|
27
|
+
</Project>
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
using Microsoft.AspNetCore.Components;
|
|
2
|
+
using Microsoft.JSInterop;
|
|
3
|
+
|
|
4
|
+
namespace Freeday.Blazor;
|
|
5
|
+
|
|
6
|
+
/// <summary>
|
|
7
|
+
/// Shared lifecycle for Freeday Blazor components that wrap a vanilla <c>fdy-*</c> enhancer:
|
|
8
|
+
/// hydrate the enhancer over the Blazor-rendered markup once on first render, forward its
|
|
9
|
+
/// bubbling <c>fdy-*</c> events to <c>[JSInvokable]</c> handlers, and clean both up on dispose.
|
|
10
|
+
/// The host page must load <c>dist/freeday.js</c> and <c>adapters/blazor/freeday-blazor.js</c>.
|
|
11
|
+
/// </summary>
|
|
12
|
+
/// <typeparam name="TSelf">The concrete component type (CRTP) so the .NET reference handed to
|
|
13
|
+
/// JS is typed to the instance and resolves its <c>[JSInvokable]</c> methods.</typeparam>
|
|
14
|
+
public abstract class FreedayComponentBase<TSelf> : ComponentBase, IAsyncDisposable
|
|
15
|
+
where TSelf : FreedayComponentBase<TSelf>
|
|
16
|
+
{
|
|
17
|
+
[Inject] protected IJSRuntime JS { get; set; } = default!;
|
|
18
|
+
|
|
19
|
+
/// <summary>The component's root element, handed to the enhancer for hydration.</summary>
|
|
20
|
+
protected ElementReference Root;
|
|
21
|
+
|
|
22
|
+
private DotNetObjectReference<TSelf>? _self;
|
|
23
|
+
private readonly List<int> _tokens = new();
|
|
24
|
+
private bool _hydrated;
|
|
25
|
+
|
|
26
|
+
/// <summary>True once the enhancer has been hydrated over the first render. Components whose
|
|
27
|
+
/// DOM is owned by the enhancer (combo, datepicker, …) should stop rendering after this
|
|
28
|
+
/// (<c>ShouldRender() => !Hydrated</c>) so Blazor never fights the enhancer's imperative
|
|
29
|
+
/// DOM mutations; subsequent changes are pushed through the enhancer's API instead.</summary>
|
|
30
|
+
protected bool Hydrated => _hydrated;
|
|
31
|
+
|
|
32
|
+
/// <summary>A stable .NET reference the JS bridge invokes callbacks on.</summary>
|
|
33
|
+
protected DotNetObjectReference<TSelf> Self => _self ??= DotNetObjectReference.Create((TSelf)this);
|
|
34
|
+
|
|
35
|
+
/// <summary>Hydrate the enhancer(s) over this component's markup. Defaults to re-running every
|
|
36
|
+
/// registered enhancer over <see cref="Root"/>; override to call a specific enhancer.</summary>
|
|
37
|
+
protected virtual ValueTask HydrateAsync() => JS.InvokeVoidAsync("FreedayBlazor.initAll", Root);
|
|
38
|
+
|
|
39
|
+
/// <summary>Runs once after the first render, right after <see cref="HydrateAsync"/> — wire event
|
|
40
|
+
/// subscriptions here.</summary>
|
|
41
|
+
protected virtual ValueTask OnHydratedAsync() => ValueTask.CompletedTask;
|
|
42
|
+
|
|
43
|
+
/// <summary>Extra teardown beyond event unsubscription (e.g. a dialog subscription).</summary>
|
|
44
|
+
protected virtual ValueTask CleanupAsync() => ValueTask.CompletedTask;
|
|
45
|
+
|
|
46
|
+
protected override async Task OnAfterRenderAsync(bool firstRender)
|
|
47
|
+
{
|
|
48
|
+
if (!firstRender || _hydrated) return;
|
|
49
|
+
_hydrated = true;
|
|
50
|
+
await HydrateAsync();
|
|
51
|
+
await OnHydratedAsync();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/// <summary>Subscribe a bubbling <c>fdy-*</c> event on <see cref="Root"/> to a
|
|
55
|
+
/// <c>[JSInvokable]</c> method; the subscription is removed on dispose.</summary>
|
|
56
|
+
protected async Task SubscribeAsync(string eventName, string method)
|
|
57
|
+
{
|
|
58
|
+
_tokens.Add(await JS.InvokeAsync<int>("FreedayBlazor.on", Root, eventName, Self, method));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
public async ValueTask DisposeAsync()
|
|
62
|
+
{
|
|
63
|
+
foreach (int token in _tokens)
|
|
64
|
+
{
|
|
65
|
+
try { await JS.InvokeVoidAsync("FreedayBlazor.off", token); }
|
|
66
|
+
catch (JSDisconnectedException) { /* circuit/runtime already gone */ }
|
|
67
|
+
}
|
|
68
|
+
try { await CleanupAsync(); }
|
|
69
|
+
catch (JSDisconnectedException) { /* circuit/runtime already gone */ }
|
|
70
|
+
_self?.Dispose();
|
|
71
|
+
GC.SuppressFinalize(this);
|
|
72
|
+
}
|
|
73
|
+
}
|