@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.
Files changed (42) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.id.md +1 -1
  3. package/README.md +1 -1
  4. package/adapters/blazor/CflTypes.cs +11 -0
  5. package/adapters/blazor/FdyAutocomplete.razor +19 -0
  6. package/adapters/blazor/FdyAutocomplete.razor.cs +40 -0
  7. package/adapters/blazor/FdyCascade.razor +33 -0
  8. package/adapters/blazor/FdyCascade.razor.cs +38 -0
  9. package/adapters/blazor/FdyCascadeNode.cs +8 -0
  10. package/adapters/blazor/FdyCfl.razor +93 -0
  11. package/adapters/blazor/FdyCfl.razor.cs +146 -0
  12. package/adapters/blazor/FdyChart.razor +25 -0
  13. package/adapters/blazor/FdyChart.razor.cs +90 -0
  14. package/adapters/blazor/FdyChartSeries.cs +10 -0
  15. package/adapters/blazor/FdyCombo.razor +28 -0
  16. package/adapters/blazor/FdyCombo.razor.cs +86 -0
  17. package/adapters/blazor/FdyComboOption.cs +7 -0
  18. package/adapters/blazor/FdyDateRange.razor +16 -0
  19. package/adapters/blazor/FdyDateRange.razor.cs +58 -0
  20. package/adapters/blazor/FdyDatepicker.razor +13 -0
  21. package/adapters/blazor/FdyDatepicker.razor.cs +39 -0
  22. package/adapters/blazor/FdyDrawer.razor +36 -0
  23. package/adapters/blazor/FdyDrawer.razor.cs +88 -0
  24. package/adapters/blazor/FdyModal.razor +36 -0
  25. package/adapters/blazor/FdyModal.razor.cs +88 -0
  26. package/adapters/blazor/FdyTable.razor +140 -0
  27. package/adapters/blazor/FdyTable.razor.cs +200 -0
  28. package/adapters/blazor/FdyTableFilter.razor +77 -0
  29. package/adapters/blazor/FdyTableFilter.razor.cs +117 -0
  30. package/adapters/blazor/Freeday.Blazor.csproj +27 -0
  31. package/adapters/blazor/FreedayComponentBase.cs +73 -0
  32. package/adapters/blazor/TableModel.cs +196 -0
  33. package/adapters/blazor/TableTypes.cs +69 -0
  34. package/adapters/blazor/_Imports.razor +5 -0
  35. package/adapters/blazor/freeday-blazor.js +101 -3
  36. package/dist/asset.d.ts +6 -0
  37. package/dist/freeday-select.js +32 -8
  38. package/dist/freeday.d.ts +24 -0
  39. package/dist/freeday.js +32 -8
  40. package/package.json +14 -8
  41. package/src/freeday-select.js +32 -8
  42. package/tokens/breakpoints.d.ts +8 -0
@@ -0,0 +1,86 @@
1
+ using Microsoft.AspNetCore.Components;
2
+ using Microsoft.JSInterop;
3
+
4
+ namespace Freeday.Blazor;
5
+
6
+ public partial class FdyCombo<TValue>
7
+ {
8
+ /// <summary>The selected value. Two-way bindable via <c>@bind-Value</c>.</summary>
9
+ [Parameter, EditorRequired] public TValue Value { get; set; } = default!;
10
+ [Parameter] public EventCallback<TValue> ValueChanged { get; set; }
11
+
12
+ /// <summary>The options to choose from.</summary>
13
+ [Parameter, EditorRequired] public IReadOnlyList<FdyComboOption<TValue>> Options { get; set; }
14
+ = Array.Empty<FdyComboOption<TValue>>();
15
+
16
+ /// <summary>Shown when no option matches the current value.</summary>
17
+ [Parameter] public string? Placeholder { get; set; }
18
+
19
+ /// <summary>Id of the element labelling the combobox (for accessibility).</summary>
20
+ [Parameter] public string? AriaLabelledby { get; set; }
21
+
22
+ /// <summary>Explicit id for the combobox button (else an auto id is used).</summary>
23
+ [Parameter] public string? Id { get; set; }
24
+
25
+ [Parameter] public bool Disabled { get; set; }
26
+
27
+ /// <summary>Locked/view mode: stays focusable and shows its value but can't be opened or changed.</summary>
28
+ [Parameter] public bool Readonly { get; set; }
29
+
30
+ [Parameter] public bool Invalid { get; set; }
31
+
32
+ private readonly string _autoId = $"fdy-combo-{Guid.NewGuid():N}";
33
+ private TValue _lastValue = default!;
34
+ private bool _ready;
35
+
36
+ // The enhancer owns this combo's DOM after hydration (open/close, aria-selected, the button
37
+ // label). Re-rendering here would fight it — e.g. reconciling the listbox mid-close leaves it
38
+ // stuck open. Render once for the initial markup, then never again; push external Value changes
39
+ // through the enhancer via comboSetValue instead.
40
+ protected override bool ShouldRender() => !Hydrated;
41
+
42
+ private string ButtonId => Id ?? _autoId;
43
+ private string RootClass => Invalid ? "fdy-combo fdy-combo--error" : "fdy-combo";
44
+ private FdyComboOption<TValue>? SelectedOption
45
+ => Options.FirstOrDefault(o => EqualityComparer<TValue>.Default.Equals(o.Value, Value));
46
+ private string SelectedLabel => SelectedOption?.Label ?? Placeholder ?? string.Empty;
47
+ private string SelectedKey => KeyOf(Value);
48
+
49
+ private static string KeyOf(TValue value) => value?.ToString() ?? string.Empty;
50
+
51
+ // The enhancer owns this combo's DOM; init it directly (initAll would skip Root itself).
52
+ protected override ValueTask HydrateAsync() => JS.InvokeVoidAsync("FreedayCombo.init", Root);
53
+
54
+ protected override async ValueTask OnHydratedAsync()
55
+ {
56
+ _lastValue = Value;
57
+ await SubscribeAsync("fdy-change", nameof(OnFdyChange));
58
+ _ready = true;
59
+ }
60
+
61
+ protected override async Task OnParametersSetAsync()
62
+ {
63
+ // Push an externally-changed Value onto the enhancer-owned DOM (silent — no fdy-change echo).
64
+ if (_ready && !EqualityComparer<TValue>.Default.Equals(Value, _lastValue))
65
+ {
66
+ _lastValue = Value;
67
+ await JS.InvokeVoidAsync("FreedayBlazor.comboSetValue", Root, KeyOf(Value));
68
+ }
69
+ }
70
+
71
+ /// <summary>Invoked by the bridge when the user picks an option (enhancer's fdy-change).</summary>
72
+ [JSInvokable]
73
+ public async Task OnFdyChange(ChangeDetail detail)
74
+ {
75
+ FdyComboOption<TValue>? match = Options.FirstOrDefault(o => KeyOf(o.Value) == detail.Value);
76
+ if (match is not null)
77
+ {
78
+ _lastValue = match.Value;
79
+ Value = match.Value;
80
+ await ValueChanged.InvokeAsync(match.Value);
81
+ }
82
+ }
83
+
84
+ /// <summary>The detail shape of the enhancer's <c>fdy-change</c> event.</summary>
85
+ public sealed record ChangeDetail(string Value);
86
+ }
@@ -0,0 +1,7 @@
1
+ namespace Freeday.Blazor;
2
+
3
+ /// <summary>One selectable option for <see cref="FdyCombo{TValue}"/>.</summary>
4
+ /// <typeparam name="TValue">The bound value type (string, an enum, an int, …).</typeparam>
5
+ /// <param name="Value">The value bound through <c>@bind-Value</c> when this option is picked.</param>
6
+ /// <param name="Label">The visible text.</param>
7
+ public sealed record FdyComboOption<TValue>(TValue Value, string Label);
@@ -0,0 +1,16 @@
1
+ @namespace Freeday.Blazor
2
+ @inherits FreedayComponentBase<FdyDateRange>
3
+
4
+ @* Two linked seed datepickers (freeday-datepicker.js data-fdy-daterange: the end can't precede
5
+ the start). The daterange sits INSIDE Root so initAll(Root) finds and links it as a descendant.
6
+ Both children emit fdy-datepicker-change; the bridge's dateRangeOn tags each with its data-role
7
+ so From/To bind separately. Enhancer-owned DOM → render once (ShouldRender => false). *@
8
+ <div @ref="Root">
9
+ <div data-fdy-daterange role="group" aria-label="@AriaLabel">
10
+ <div data-fdy-datepicker data-role="from"
11
+ data-value="@From" data-placeholder="@FromPlaceholder" data-min="@Min" data-max="@Max"></div>
12
+ <span class="fdy-daterange__sep">@Separator</span>
13
+ <div data-fdy-datepicker data-role="to"
14
+ data-value="@To" data-placeholder="@ToPlaceholder" data-min="@Min" data-max="@Max"></div>
15
+ </div>
16
+ </div>
@@ -0,0 +1,58 @@
1
+ using Microsoft.AspNetCore.Components;
2
+ using Microsoft.JSInterop;
3
+
4
+ namespace Freeday.Blazor;
5
+
6
+ public partial class FdyDateRange
7
+ {
8
+ /// <summary>Range start (ISO <c>yyyy-MM-dd</c> or ""). Two-way bindable via <c>@bind-From</c>.</summary>
9
+ [Parameter] public string? From { get; set; }
10
+ [Parameter] public EventCallback<string?> FromChanged { get; set; }
11
+
12
+ /// <summary>Range end (ISO <c>yyyy-MM-dd</c> or ""). Two-way bindable via <c>@bind-To</c>.</summary>
13
+ [Parameter] public string? To { get; set; }
14
+ [Parameter] public EventCallback<string?> ToChanged { get; set; }
15
+
16
+ [Parameter] public string? AriaLabel { get; set; }
17
+ [Parameter] public string? FromPlaceholder { get; set; }
18
+ [Parameter] public string? ToPlaceholder { get; set; }
19
+ [Parameter] public string? Min { get; set; }
20
+ [Parameter] public string? Max { get; set; }
21
+ [Parameter] public string Separator { get; set; } = "–";
22
+
23
+ private int _token;
24
+
25
+ // initAll(Root) links the daterange (descendant) and inits both child pickers.
26
+ protected override ValueTask HydrateAsync() => JS.InvokeVoidAsync("FreedayDatepicker.initAll", Root);
27
+
28
+ protected override async ValueTask OnHydratedAsync()
29
+ => _token = await JS.InvokeAsync<int>("FreedayBlazor.dateRangeOn", Root, Self, nameof(OnChange));
30
+
31
+ protected override bool ShouldRender() => !Hydrated;
32
+
33
+ /// <summary>Invoked by the bridge for either child's change; routes by data-role.</summary>
34
+ [JSInvokable]
35
+ public async Task OnChange(RangeDetail detail)
36
+ {
37
+ if (detail.Role == "from")
38
+ {
39
+ From = detail.Value;
40
+ await FromChanged.InvokeAsync(detail.Value);
41
+ }
42
+ else if (detail.Role == "to")
43
+ {
44
+ To = detail.Value;
45
+ await ToChanged.InvokeAsync(detail.Value);
46
+ }
47
+ }
48
+
49
+ protected override async ValueTask CleanupAsync()
50
+ {
51
+ if (_token != 0)
52
+ {
53
+ await JS.InvokeVoidAsync("FreedayBlazor.off", _token);
54
+ }
55
+ }
56
+
57
+ public sealed record RangeDetail(string? Value, string? Role);
58
+ }
@@ -0,0 +1,13 @@
1
+ @namespace Freeday.Blazor
2
+ @inherits FreedayComponentBase<FdyDatepicker>
3
+
4
+ @* Seed-based wrapper over freeday-datepicker.js: the enhancer builds the input-styled
5
+ trigger + calendar popover from this empty div and emits fdy-datepicker-change. The
6
+ enhancer owns that DOM, so we render the seed once (ShouldRender => false) and read the
7
+ picked value back through @bind-Value. Value is an ISO date string ("yyyy-MM-dd") or "". *@
8
+ <div @ref="Root" data-fdy-datepicker
9
+ data-value="@Value"
10
+ data-label="@Label"
11
+ data-placeholder="@Placeholder"
12
+ data-min="@Min"
13
+ data-max="@Max"></div>
@@ -0,0 +1,39 @@
1
+ using Microsoft.AspNetCore.Components;
2
+ using Microsoft.JSInterop;
3
+
4
+ namespace Freeday.Blazor;
5
+
6
+ public partial class FdyDatepicker
7
+ {
8
+ /// <summary>Selected date as an ISO string (<c>yyyy-MM-dd</c>), or "" when unset.
9
+ /// Two-way bindable via <c>@bind-Value</c>.</summary>
10
+ [Parameter] public string? Value { get; set; }
11
+ [Parameter] public EventCallback<string?> ValueChanged { get; set; }
12
+
13
+ /// <summary>Accessible label for the trigger.</summary>
14
+ [Parameter] public string? Label { get; set; }
15
+ [Parameter] public string? Placeholder { get; set; }
16
+
17
+ /// <summary>Earliest selectable date (ISO <c>yyyy-MM-dd</c>); days before it are disabled.</summary>
18
+ [Parameter] public string? Min { get; set; }
19
+ [Parameter] public string? Max { get; set; }
20
+
21
+ protected override ValueTask HydrateAsync() => JS.InvokeVoidAsync("FreedayDatepicker.init", Root);
22
+
23
+ protected override async ValueTask OnHydratedAsync()
24
+ => await SubscribeAsync("fdy-datepicker-change", nameof(OnChange));
25
+
26
+ // The enhancer owns the built trigger + calendar; never let Blazor re-render the seed.
27
+ protected override bool ShouldRender() => !Hydrated;
28
+
29
+ /// <summary>Invoked by the bridge when the user picks (or clears) a date.</summary>
30
+ [JSInvokable]
31
+ public async Task OnChange(ChangeDetail detail)
32
+ {
33
+ Value = detail.Value;
34
+ await ValueChanged.InvokeAsync(detail.Value);
35
+ }
36
+
37
+ /// <summary>The detail shape of the enhancer's <c>fdy-datepicker-change</c> event.</summary>
38
+ public sealed record ChangeDetail(string? Value, string? Date);
39
+ }
@@ -0,0 +1,36 @@
1
+ @namespace Freeday.Blazor
2
+ @inherits FreedayComponentBase<FdyDrawer>
3
+
4
+ @* Controlled wrapper over freeday's .fdy-drawer native <dialog> side panel. Same contract and
5
+ bridge as FdyModal (open/close + Esc/backdrop route through freeday-blazor.js) applied to a
6
+ panel that anchors left (default) or right. Native <dialog> gives the focus trap, focus
7
+ restore, top-layer stacking and inert background for free. *@
8
+ <dialog @ref="Root" class="@DrawerClass" aria-labelledby="@_titleId">
9
+ <div class="fdy-drawer__header">
10
+ <h3 id="@_titleId" class="fdy-drawer__title">
11
+ @if (TitleContent is not null)
12
+ {
13
+ @TitleContent
14
+ }
15
+ else
16
+ {
17
+ @Title
18
+ }
19
+ </h3>
20
+ @if (Dismissible)
21
+ {
22
+ <button class="fdy-drawer__close" type="button" aria-label="@CloseLabel" @onclick="DismissAsync">&times;</button>
23
+ }
24
+ </div>
25
+
26
+ <div class="fdy-drawer__body">
27
+ @ChildContent
28
+ </div>
29
+
30
+ @if (FooterContent is not null)
31
+ {
32
+ <div class="fdy-drawer__footer">
33
+ @FooterContent
34
+ </div>
35
+ }
36
+ </dialog>
@@ -0,0 +1,88 @@
1
+ using Microsoft.AspNetCore.Components;
2
+ using Microsoft.JSInterop;
3
+
4
+ namespace Freeday.Blazor;
5
+
6
+ public partial class FdyDrawer
7
+ {
8
+ /// <summary>Whether the drawer is open. Two-way bindable via <c>@bind-Open</c>.</summary>
9
+ [Parameter] public bool Open { get; set; }
10
+ [Parameter] public EventCallback<bool> OpenChanged { get; set; }
11
+
12
+ /// <summary>Plain-text title; ignored when <see cref="TitleContent"/> is supplied.</summary>
13
+ [Parameter] public string Title { get; set; } = string.Empty;
14
+ [Parameter] public RenderFragment? TitleContent { get; set; }
15
+
16
+ /// <summary>The drawer body.</summary>
17
+ [Parameter] public RenderFragment? ChildContent { get; set; }
18
+
19
+ /// <summary>Optional footer (action buttons). Omitted entirely when null.</summary>
20
+ [Parameter] public RenderFragment? FooterContent { get; set; }
21
+
22
+ /// <summary>Which edge the panel anchors to: <c>left</c> (default) or <c>right</c>.</summary>
23
+ [Parameter] public string Side { get; set; } = "left";
24
+
25
+ /// <summary>When true (default), Esc and a backdrop click dismiss the drawer and a close
26
+ /// button is shown. Set false for a panel the user must resolve via an explicit action.</summary>
27
+ [Parameter] public bool Dismissible { get; set; } = true;
28
+
29
+ [Parameter] public string CloseLabel { get; set; } = "Close";
30
+
31
+ /// <summary>Raised after any dismissal (close button, Esc, or backdrop).</summary>
32
+ [Parameter] public EventCallback OnClose { get; set; }
33
+
34
+ private readonly string _titleId = $"fdy-drawer-{Guid.NewGuid():N}-title";
35
+ private int _dialogToken;
36
+ private bool _lastOpen;
37
+
38
+ private string DrawerClass => Side == "right" ? "fdy-drawer fdy-drawer--right" : "fdy-drawer";
39
+
40
+ protected override async ValueTask HydrateAsync()
41
+ {
42
+ _dialogToken = await JS.InvokeAsync<int>(
43
+ "FreedayBlazor.dialogInit", Root, Self, nameof(OnDismiss), Dismissible);
44
+ await SyncAsync();
45
+ }
46
+
47
+ protected override async Task OnParametersSetAsync()
48
+ {
49
+ // Only reconcile once the dialog is wired (token != 0) and the open state actually changed.
50
+ if (_dialogToken != 0 && Open != _lastOpen)
51
+ {
52
+ await SyncAsync();
53
+ }
54
+ }
55
+
56
+ private async ValueTask SyncAsync()
57
+ {
58
+ _lastOpen = Open;
59
+ await JS.InvokeVoidAsync(Open ? "FreedayBlazor.dialogShow" : "FreedayBlazor.dialogClose", Root);
60
+ }
61
+
62
+ /// <summary>Invoked by the bridge when the user dismisses via Esc or a backdrop click.</summary>
63
+ [JSInvokable]
64
+ public Task OnDismiss() => DismissAsync();
65
+
66
+ private async Task DismissAsync()
67
+ {
68
+ if (OpenChanged.HasDelegate)
69
+ {
70
+ await OpenChanged.InvokeAsync(false);
71
+ }
72
+ else
73
+ {
74
+ Open = false;
75
+ _lastOpen = false;
76
+ await JS.InvokeVoidAsync("FreedayBlazor.dialogClose", Root);
77
+ }
78
+ await OnClose.InvokeAsync();
79
+ }
80
+
81
+ protected override async ValueTask CleanupAsync()
82
+ {
83
+ if (_dialogToken != 0)
84
+ {
85
+ await JS.InvokeVoidAsync("FreedayBlazor.dialogDispose", _dialogToken);
86
+ }
87
+ }
88
+ }
@@ -0,0 +1,36 @@
1
+ @namespace Freeday.Blazor
2
+ @inherits FreedayComponentBase<FdyModal>
3
+
4
+ @* Controlled wrapper over freeday's .fdy-modal native <dialog>. Blazor can't call
5
+ showModal()/close() from C#, so open/close + Esc/backdrop dismissal route through the
6
+ freeday-blazor.js bridge. Native <dialog> gives the focus trap, focus restore, top-layer
7
+ stacking and inert background for free — this only reconciles it with Blazor state. *@
8
+ <dialog @ref="Root" class="@ModalClass" aria-labelledby="@_titleId">
9
+ <div class="fdy-modal__header">
10
+ <h3 id="@_titleId" class="fdy-modal__title">
11
+ @if (TitleContent is not null)
12
+ {
13
+ @TitleContent
14
+ }
15
+ else
16
+ {
17
+ @Title
18
+ }
19
+ </h3>
20
+ @if (Dismissible)
21
+ {
22
+ <button class="fdy-modal__close" type="button" aria-label="@CloseLabel" @onclick="DismissAsync">&times;</button>
23
+ }
24
+ </div>
25
+
26
+ <div class="fdy-modal__body">
27
+ @ChildContent
28
+ </div>
29
+
30
+ @if (FooterContent is not null)
31
+ {
32
+ <div class="fdy-modal__footer">
33
+ @FooterContent
34
+ </div>
35
+ }
36
+ </dialog>
@@ -0,0 +1,88 @@
1
+ using Microsoft.AspNetCore.Components;
2
+ using Microsoft.JSInterop;
3
+
4
+ namespace Freeday.Blazor;
5
+
6
+ public partial class FdyModal
7
+ {
8
+ /// <summary>Whether the dialog is open. Two-way bindable via <c>@bind-Open</c>.</summary>
9
+ [Parameter] public bool Open { get; set; }
10
+ [Parameter] public EventCallback<bool> OpenChanged { get; set; }
11
+
12
+ /// <summary>Plain-text title; ignored when <see cref="TitleContent"/> is supplied.</summary>
13
+ [Parameter] public string Title { get; set; } = string.Empty;
14
+ [Parameter] public RenderFragment? TitleContent { get; set; }
15
+
16
+ /// <summary>The dialog body.</summary>
17
+ [Parameter] public RenderFragment? ChildContent { get; set; }
18
+
19
+ /// <summary>Optional footer (action buttons). Omitted entirely when null.</summary>
20
+ [Parameter] public RenderFragment? FooterContent { get; set; }
21
+
22
+ /// <summary>Size modifier: <c>sm</c> | <c>md</c> | <c>lg</c> | <c>wide</c>.</summary>
23
+ [Parameter] public string? Size { get; set; }
24
+
25
+ /// <summary>When true (default), Esc and a backdrop click dismiss the dialog and a close
26
+ /// button is shown. Set false for a modal the user must resolve via an explicit action.</summary>
27
+ [Parameter] public bool Dismissible { get; set; } = true;
28
+
29
+ [Parameter] public string CloseLabel { get; set; } = "Close";
30
+
31
+ /// <summary>Raised after any dismissal (close button, Esc, or backdrop).</summary>
32
+ [Parameter] public EventCallback OnClose { get; set; }
33
+
34
+ private readonly string _titleId = $"fdy-modal-{Guid.NewGuid():N}-title";
35
+ private int _dialogToken;
36
+ private bool _lastOpen;
37
+
38
+ private string ModalClass => Size is null ? "fdy-modal" : $"fdy-modal fdy-modal--{Size}";
39
+
40
+ protected override async ValueTask HydrateAsync()
41
+ {
42
+ _dialogToken = await JS.InvokeAsync<int>(
43
+ "FreedayBlazor.dialogInit", Root, Self, nameof(OnDismiss), Dismissible);
44
+ await SyncAsync();
45
+ }
46
+
47
+ protected override async Task OnParametersSetAsync()
48
+ {
49
+ // Only reconcile once the dialog is wired (token != 0) and the open state actually changed.
50
+ if (_dialogToken != 0 && Open != _lastOpen)
51
+ {
52
+ await SyncAsync();
53
+ }
54
+ }
55
+
56
+ private async ValueTask SyncAsync()
57
+ {
58
+ _lastOpen = Open;
59
+ await JS.InvokeVoidAsync(Open ? "FreedayBlazor.dialogShow" : "FreedayBlazor.dialogClose", Root);
60
+ }
61
+
62
+ /// <summary>Invoked by the bridge when the user dismisses via Esc or a backdrop click.</summary>
63
+ [JSInvokable]
64
+ public Task OnDismiss() => DismissAsync();
65
+
66
+ private async Task DismissAsync()
67
+ {
68
+ if (OpenChanged.HasDelegate)
69
+ {
70
+ await OpenChanged.InvokeAsync(false);
71
+ }
72
+ else
73
+ {
74
+ Open = false;
75
+ _lastOpen = false;
76
+ await JS.InvokeVoidAsync("FreedayBlazor.dialogClose", Root);
77
+ }
78
+ await OnClose.InvokeAsync();
79
+ }
80
+
81
+ protected override async ValueTask CleanupAsync()
82
+ {
83
+ if (_dialogToken != 0)
84
+ {
85
+ await JS.InvokeVoidAsync("FreedayBlazor.dialogDispose", _dialogToken);
86
+ }
87
+ }
88
+ }
@@ -0,0 +1,140 @@
1
+ @namespace Freeday.Blazor
2
+ @typeparam TRow
3
+ @using Microsoft.AspNetCore.Components.Web
4
+
5
+ @* A controlled Blazor data table over freeday's .fdy-datatable / .fdy-table* / .fdy-filter* /
6
+ .fdy-pagination__* classes. Unlike the freeday-table.js enhancer (which snapshots static rows and
7
+ fights a framework's patcher), this reads Rows as the source of truth on every render. Two modes:
8
+ • Client mode (no Page): sorts/filters (and paginates when PageSize > 0) over the full Rows.
9
+ Sort/Filters are controlled when their EventCallback is wired, else internal.
10
+ • Server mode (Page set): Rows are rendered as given; the sort headers, filters and pager only
11
+ raise intent (SortChanged / FiltersChanged / PageChanged) for the caller to feed to its query. *@
12
+ <div class="fdy-datatable">
13
+ @if (Toolbar is not null)
14
+ {
15
+ <div class="fdy-table-toolbar">
16
+ @Toolbar
17
+ </div>
18
+ }
19
+
20
+ <div class="fdy-table-scroll">
21
+ <table class="fdy-table" aria-label="@AriaLabel">
22
+ <thead>
23
+ <tr>
24
+ @foreach (FdyTableColumn<TRow> col in Columns)
25
+ {
26
+ <th scope="col" style="@AlignStyle(col)" aria-sort="@AriaSortOf(col)">
27
+ @if (col.Sortable)
28
+ {
29
+ <button type="button" class="fdy-table__sortbtn" @onclick="() => OnSortClick(col)">@col.Label</button>
30
+ }
31
+ else
32
+ {
33
+ @col.Label
34
+ }
35
+ @if (col.Filter is { } ft)
36
+ {
37
+ <FdyTableFilter Label="@col.Label"
38
+ Type="ft"
39
+ Filter="@GetFilter(col.Key)"
40
+ Options="@EnumOptions(col)"
41
+ OnChange="f => OnFilterChange(col, f)" />
42
+ }
43
+ </th>
44
+ }
45
+ </tr>
46
+ </thead>
47
+ <tbody>
48
+ @if (Loading)
49
+ {
50
+ <tr><td colspan="@Columns.Count" class="fdy-table__state" role="status">@LoadingText</td></tr>
51
+ }
52
+ else if (_displayRows.Count == 0)
53
+ {
54
+ <tr>
55
+ <td colspan="@Columns.Count" class="fdy-table__state">
56
+ @if (EmptyContent is not null)
57
+ {
58
+ @EmptyContent
59
+ }
60
+ else
61
+ {
62
+ @EmptyText
63
+ }
64
+ </td>
65
+ </tr>
66
+ }
67
+ else
68
+ {
69
+ @foreach (TRow row in _displayRows)
70
+ {
71
+ TRow r = row;
72
+ <tr class="@RowClasses(r)"
73
+ tabindex="@(RowActivatable ? 0 : (int?)null)"
74
+ aria-expanded="@ExpandedAttr(r)"
75
+ @onclick="() => OnRowClick(r)"
76
+ @onkeydown="e => OnRowKeydown(e, r)">
77
+ @foreach (FdyTableColumn<TRow> col in Columns)
78
+ {
79
+ <td class="@CellClass(col)" style="@AlignStyle(col)">
80
+ @if (col.Cell is not null)
81
+ {
82
+ @col.Cell(r)
83
+ }
84
+ else
85
+ {
86
+ @TableModel.CellText(r, col)
87
+ }
88
+ </td>
89
+ }
90
+ </tr>
91
+ @if (RowDetail is not null && IsExpanded(r))
92
+ {
93
+ <tr class="fdy-table__detailrow">
94
+ <td colspan="@Columns.Count">@RowDetail(r)</td>
95
+ </tr>
96
+ }
97
+ }
98
+ }
99
+ </tbody>
100
+ </table>
101
+ </div>
102
+
103
+ @if (HasPager)
104
+ {
105
+ <div class="fdy-table-footer">
106
+ <span class="fdy-table-footer__info">Showing @RangeFrom–@RangeTo of @TotalCount</span>
107
+ <nav aria-label="Pagination">
108
+ <ul class="fdy-pagination__list">
109
+ <li>
110
+ <button type="button" class="fdy-pagination__link" aria-label="Previous page"
111
+ disabled="@(CurrentPage1 == 1)" @onclick="() => GoTo(CurrentPage1 - 1)">‹</button>
112
+ </li>
113
+ @foreach (int? p in Pages)
114
+ {
115
+ <li>
116
+ @if (p is null)
117
+ {
118
+ <span class="fdy-pagination__ellipsis">…</span>
119
+ }
120
+ else if (p == CurrentPage1)
121
+ {
122
+ <span class="fdy-pagination__link" aria-current="page">@p</span>
123
+ }
124
+ else
125
+ {
126
+ int page = p.Value;
127
+ <button type="button" class="fdy-pagination__link" aria-label="@($"Go to page {page}")"
128
+ @onclick="() => GoTo(page)">@(page)</button>
129
+ }
130
+ </li>
131
+ }
132
+ <li>
133
+ <button type="button" class="fdy-pagination__link" aria-label="Next page"
134
+ disabled="@(CurrentPage1 == TotalPages)" @onclick="() => GoTo(CurrentPage1 + 1)">›</button>
135
+ </li>
136
+ </ul>
137
+ </nav>
138
+ </div>
139
+ }
140
+ </div>