@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,196 @@
|
|
|
1
|
+
using System.Collections.Concurrent;
|
|
2
|
+
using System.Globalization;
|
|
3
|
+
using System.Reflection;
|
|
4
|
+
using System.Text.RegularExpressions;
|
|
5
|
+
|
|
6
|
+
namespace Freeday.Blazor;
|
|
7
|
+
|
|
8
|
+
/// <summary>
|
|
9
|
+
/// The sort / filter / paginate logic for <see cref="FdyTable{TRow}"/> — the C# counterpart of the
|
|
10
|
+
/// framework-agnostic adapters/core/table-model.js shared by the Vue and React adapters. Kept as a
|
|
11
|
+
/// separate pure-function class (no rendering, no state) so it mirrors that reference faithfully and
|
|
12
|
+
/// stays unit-testable. Every method is pure: inputs are never mutated (rows are copied before sort).
|
|
13
|
+
/// </summary>
|
|
14
|
+
public static class TableModel
|
|
15
|
+
{
|
|
16
|
+
private static readonly ConcurrentDictionary<(Type, string), PropertyInfo?> PropCache = new();
|
|
17
|
+
private static readonly Regex NonNumeric = new(@"[^\d.-]", RegexOptions.Compiled);
|
|
18
|
+
|
|
19
|
+
/// <summary>Numeric value, tolerant of formatted strings ("Rp 1.000"); non-numeric -> 0.</summary>
|
|
20
|
+
private static double ToNumber(object? v)
|
|
21
|
+
{
|
|
22
|
+
if (v is null) return 0;
|
|
23
|
+
if (v is double d) return double.IsFinite(d) ? d : 0;
|
|
24
|
+
if (v is IConvertible && v is not string)
|
|
25
|
+
{
|
|
26
|
+
try { return Convert.ToDouble(v, CultureInfo.InvariantCulture); } catch { /* fall through */ }
|
|
27
|
+
}
|
|
28
|
+
string cleaned = NonNumeric.Replace(v.ToString() ?? string.Empty, string.Empty);
|
|
29
|
+
return double.TryParse(cleaned, NumberStyles.Any, CultureInfo.InvariantCulture, out double n) ? n : 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/// <summary>Epoch ms; unparseable -> 0.</summary>
|
|
33
|
+
private static double ToTime(object? v)
|
|
34
|
+
{
|
|
35
|
+
if (v is null) return 0;
|
|
36
|
+
if (v is DateTime dt) return new DateTimeOffset(dt.ToUniversalTime()).ToUnixTimeMilliseconds();
|
|
37
|
+
if (v is DateTimeOffset dto) return dto.ToUnixTimeMilliseconds();
|
|
38
|
+
return DateTimeOffset.TryParse(v.ToString(), CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out DateTimeOffset parsed)
|
|
39
|
+
? parsed.ToUnixTimeMilliseconds()
|
|
40
|
+
: 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// <summary>ISO calendar day (yyyy-MM-dd) — lexicographically comparable.</summary>
|
|
44
|
+
private static string DateOnly(object? v)
|
|
45
|
+
{
|
|
46
|
+
if (v is DateTime dt) return dt.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture);
|
|
47
|
+
if (v is DateTimeOffset dto) return dto.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture);
|
|
48
|
+
string s = v?.ToString() ?? string.Empty;
|
|
49
|
+
return s.Length >= 10 ? s[..10] : s;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/// <summary>Raw cell value for a column: the accessor if present, else the property named Key.</summary>
|
|
53
|
+
public static object? CellValue<TRow>(TRow row, FdyTableColumn<TRow> column)
|
|
54
|
+
{
|
|
55
|
+
if (column.Value is not null) return column.Value(row);
|
|
56
|
+
PropertyInfo? prop = PropCache.GetOrAdd((typeof(TRow), column.Key),
|
|
57
|
+
static k => k.Item1.GetProperty(k.Item2, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase));
|
|
58
|
+
return prop?.GetValue(row);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/// <summary>Cell value as trimmed display text (null -> "").</summary>
|
|
62
|
+
public static string CellText<TRow>(TRow row, FdyTableColumn<TRow> column)
|
|
63
|
+
{
|
|
64
|
+
object? v = CellValue(row, column);
|
|
65
|
+
return v is null ? string.Empty : (Convert.ToString(v, CultureInfo.CurrentCulture) ?? string.Empty).Trim();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/// <summary>Effective comparator type: explicit SortType, else derived from the filter type.</summary>
|
|
69
|
+
public static FdyColumnType ColumnSortType<TRow>(FdyTableColumn<TRow> column)
|
|
70
|
+
{
|
|
71
|
+
if (column.SortType is { } t) return t;
|
|
72
|
+
return column.Filter switch
|
|
73
|
+
{
|
|
74
|
+
FdyColumnFilterType.Number => FdyColumnType.Number,
|
|
75
|
+
FdyColumnFilterType.Date => FdyColumnType.Date,
|
|
76
|
+
_ => FdyColumnType.Text,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/// <summary>Ascending comparison of two raw cell values under a comparator type.</summary>
|
|
81
|
+
public static int CompareBy(FdyColumnType type, object? a, object? b) => type switch
|
|
82
|
+
{
|
|
83
|
+
FdyColumnType.Number => ToNumber(a).CompareTo(ToNumber(b)),
|
|
84
|
+
FdyColumnType.Date => ToTime(a).CompareTo(ToTime(b)),
|
|
85
|
+
_ => string.Compare(a?.ToString() ?? string.Empty, b?.ToString() ?? string.Empty,
|
|
86
|
+
StringComparison.CurrentCultureIgnoreCase),
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/// <summary>True when a filter would actually narrow the rows (worth applying / showing as active).</summary>
|
|
90
|
+
public static bool IsFilterActive(FdyColumnFilter? filter) => filter switch
|
|
91
|
+
{
|
|
92
|
+
FdyColumnFilter.Text t => t.Value.Trim().Length > 0,
|
|
93
|
+
FdyColumnFilter.Enum e => e.Values.Count > 0,
|
|
94
|
+
FdyColumnFilter.Number n => n.Min is not null || n.Max is not null,
|
|
95
|
+
FdyColumnFilter.Date d => d.From is not null || d.To is not null,
|
|
96
|
+
_ => false,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
private static bool RowPassesFilter<TRow>(TRow row, FdyTableColumn<TRow> column, FdyColumnFilter filter)
|
|
100
|
+
{
|
|
101
|
+
switch (filter)
|
|
102
|
+
{
|
|
103
|
+
case FdyColumnFilter.Text t:
|
|
104
|
+
{
|
|
105
|
+
string needle = t.Value.Trim().ToLowerInvariant();
|
|
106
|
+
return needle.Length == 0 || CellText(row, column).ToLowerInvariant().Contains(needle);
|
|
107
|
+
}
|
|
108
|
+
case FdyColumnFilter.Enum e:
|
|
109
|
+
return e.Values.Count == 0 || e.Values.Contains(CellText(row, column));
|
|
110
|
+
case FdyColumnFilter.Number n:
|
|
111
|
+
{
|
|
112
|
+
double v = ToNumber(CellValue(row, column));
|
|
113
|
+
if (n.Min is { } min && v < min) return false;
|
|
114
|
+
if (n.Max is { } max && v > max) return false;
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
case FdyColumnFilter.Date d:
|
|
118
|
+
{
|
|
119
|
+
string day = DateOnly(CellValue(row, column));
|
|
120
|
+
if (d.From is { } from && string.CompareOrdinal(day, from) < 0) return false;
|
|
121
|
+
if (d.To is { } to && string.CompareOrdinal(day, to) > 0) return false;
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
default:
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/// <summary>Rows passing every active column filter (logical AND). Input is never mutated.</summary>
|
|
130
|
+
public static List<TRow> FilterRows<TRow>(
|
|
131
|
+
IReadOnlyList<TRow> rows,
|
|
132
|
+
IReadOnlyList<FdyTableColumn<TRow>> columns,
|
|
133
|
+
IReadOnlyDictionary<string, FdyColumnFilter> filters)
|
|
134
|
+
{
|
|
135
|
+
List<(FdyTableColumn<TRow> Col, FdyColumnFilter Filter)> active = new();
|
|
136
|
+
foreach (FdyTableColumn<TRow> col in columns)
|
|
137
|
+
{
|
|
138
|
+
if (filters.TryGetValue(col.Key, out FdyColumnFilter? f) && IsFilterActive(f))
|
|
139
|
+
{
|
|
140
|
+
active.Add((col, f));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (active.Count == 0) return rows.ToList();
|
|
144
|
+
return rows.Where(row => active.All(e => RowPassesFilter(row, e.Col, e.Filter))).ToList();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/// <summary>Rows sorted by the given sort state. Stable via OrderBy; input is never mutated.</summary>
|
|
148
|
+
public static List<TRow> SortRows<TRow>(
|
|
149
|
+
IReadOnlyList<TRow> rows,
|
|
150
|
+
IReadOnlyList<FdyTableColumn<TRow>> columns,
|
|
151
|
+
FdySortState? sort)
|
|
152
|
+
{
|
|
153
|
+
if (sort is null) return rows.ToList();
|
|
154
|
+
FdyTableColumn<TRow>? column = columns.FirstOrDefault(c => c.Key == sort.Key);
|
|
155
|
+
if (column is null) return rows.ToList();
|
|
156
|
+
FdyColumnType type = ColumnSortType(column);
|
|
157
|
+
int factor = sort.Dir == FdySortDir.Desc ? -1 : 1;
|
|
158
|
+
return rows
|
|
159
|
+
.OrderBy(r => r, Comparer<TRow>.Create((a, b) => CompareBy(type, CellValue(a, column), CellValue(b, column)) * factor))
|
|
160
|
+
.ToList();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/// <summary>Page slice (0-based page index). A non-positive size returns all rows.</summary>
|
|
164
|
+
public static List<TRow> Paginate<TRow>(IReadOnlyList<TRow> rows, int pageIndex, int pageSize)
|
|
165
|
+
{
|
|
166
|
+
if (pageSize <= 0) return rows.ToList();
|
|
167
|
+
return rows.Skip(pageIndex * pageSize).Take(pageSize).ToList();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/// <summary>Distinct non-empty cell texts for a column, naturally sorted — the enum-filter source.</summary>
|
|
171
|
+
public static List<string> DistinctValues<TRow>(IReadOnlyList<TRow> rows, FdyTableColumn<TRow> column)
|
|
172
|
+
{
|
|
173
|
+
HashSet<string> seen = new();
|
|
174
|
+
List<string> outList = new();
|
|
175
|
+
foreach (TRow row in rows)
|
|
176
|
+
{
|
|
177
|
+
string v = CellText(row, column);
|
|
178
|
+
if (v.Length > 0 && seen.Add(v)) outList.Add(v);
|
|
179
|
+
}
|
|
180
|
+
outList.Sort((a, b) => string.Compare(a, b, StringComparison.CurrentCultureIgnoreCase));
|
|
181
|
+
return outList;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/// <summary>Page-number window: first and last page always shown, current ±1, null for the
|
|
185
|
+
/// "ellipsis" gaps between. <paramref name="current"/> and returned page numbers are 1-based.</summary>
|
|
186
|
+
public static List<int?> PageWindow(int current, int totalPages)
|
|
187
|
+
{
|
|
188
|
+
List<int?> outList = new();
|
|
189
|
+
for (int p = 1; p <= totalPages; p++)
|
|
190
|
+
{
|
|
191
|
+
if (p == 1 || p == totalPages || (p >= current - 1 && p <= current + 1)) outList.Add(p);
|
|
192
|
+
else if (outList.Count == 0 || outList[^1] is not null) outList.Add(null);
|
|
193
|
+
}
|
|
194
|
+
return outList;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
using Microsoft.AspNetCore.Components;
|
|
2
|
+
|
|
3
|
+
namespace Freeday.Blazor;
|
|
4
|
+
|
|
5
|
+
/// <summary>Sort direction for a column.</summary>
|
|
6
|
+
public enum FdySortDir { Asc, Desc }
|
|
7
|
+
|
|
8
|
+
/// <summary>A column's comparator type — how its values are ordered when sorting.</summary>
|
|
9
|
+
public enum FdyColumnType { Text, Number, Date }
|
|
10
|
+
|
|
11
|
+
/// <summary>Cell alignment. Maps to <c>text-align</c> on the header and body cells.</summary>
|
|
12
|
+
public enum FdyColumnAlign { Left, Right, Center }
|
|
13
|
+
|
|
14
|
+
/// <summary>The kind of column filter to offer (a funnel popover in the header).</summary>
|
|
15
|
+
public enum FdyColumnFilterType { Text, Enum, Number, Date }
|
|
16
|
+
|
|
17
|
+
/// <summary>Which column is sorted and in what direction.</summary>
|
|
18
|
+
public sealed record FdySortState(string Key, FdySortDir Dir);
|
|
19
|
+
|
|
20
|
+
/// <summary>Server-driven pagination state (0-based page index). Its presence on
|
|
21
|
+
/// <see cref="FdyTable{TRow}.Page"/> switches the table into server mode.</summary>
|
|
22
|
+
public sealed record FdyPageState(int Index, int Size, int Total);
|
|
23
|
+
|
|
24
|
+
/// <summary>The active filter for a single column (a closed set of shapes, one per filter type).</summary>
|
|
25
|
+
public abstract record FdyColumnFilter
|
|
26
|
+
{
|
|
27
|
+
public sealed record Text(string Value) : FdyColumnFilter;
|
|
28
|
+
public sealed record Enum(IReadOnlyList<string> Values) : FdyColumnFilter;
|
|
29
|
+
public sealed record Number(double? Min, double? Max) : FdyColumnFilter;
|
|
30
|
+
public sealed record Date(string? From, string? To) : FdyColumnFilter;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/// <summary>One column definition. <typeparamref name="TRow"/> is the row type.</summary>
|
|
34
|
+
/// <typeparam name="TRow">The row model.</typeparam>
|
|
35
|
+
public sealed class FdyTableColumn<TRow>
|
|
36
|
+
{
|
|
37
|
+
/// <summary>Row property key; the sort/filter identity and, when <see cref="Value"/> is unset,
|
|
38
|
+
/// the default cell accessor (resolved by reflection against a property of this name).</summary>
|
|
39
|
+
public required string Key { get; init; }
|
|
40
|
+
|
|
41
|
+
/// <summary>Header label.</summary>
|
|
42
|
+
public required string Label { get; init; }
|
|
43
|
+
|
|
44
|
+
/// <summary>Show a sort toggle in the header.</summary>
|
|
45
|
+
public bool Sortable { get; init; }
|
|
46
|
+
|
|
47
|
+
/// <summary>Offer a column filter of this type (null = no filter).</summary>
|
|
48
|
+
public FdyColumnFilterType? Filter { get; init; }
|
|
49
|
+
|
|
50
|
+
/// <summary>Cell alignment (null = left).</summary>
|
|
51
|
+
public FdyColumnAlign? Align { get; init; }
|
|
52
|
+
|
|
53
|
+
/// <summary>Render cells in the monospace data font (<c>.fdy-mono</c>).</summary>
|
|
54
|
+
public bool Mono { get; init; }
|
|
55
|
+
|
|
56
|
+
/// <summary>Override the comparator type; defaults to one derived from <see cref="Filter"/>, else Text.</summary>
|
|
57
|
+
public FdyColumnType? SortType { get; init; }
|
|
58
|
+
|
|
59
|
+
/// <summary>Typed value accessor for sort/filter/default cell text; defaults to the property named
|
|
60
|
+
/// <see cref="Key"/> (reflection). Prefer setting this for strongly-typed, allocation-free access.</summary>
|
|
61
|
+
public Func<TRow, object?>? Value { get; init; }
|
|
62
|
+
|
|
63
|
+
/// <summary>Explicit options for an enum filter; defaults to the distinct values in the current
|
|
64
|
+
/// rows. Provide this in server-paged mode, where the rows on screen are only one page.</summary>
|
|
65
|
+
public IReadOnlyList<string>? Options { get; init; }
|
|
66
|
+
|
|
67
|
+
/// <summary>Custom cell template; defaults to the accessor's text.</summary>
|
|
68
|
+
public RenderFragment<TRow>? Cell { get; init; }
|
|
69
|
+
}
|
|
@@ -55,11 +55,44 @@
|
|
|
55
55
|
return token;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
//
|
|
58
|
+
// Subscribe to a daterange's child datepicker changes, forwarding {value, role} so a Blazor
|
|
59
|
+
// wrapper can bind From/To separately — both children emit the same fdy-datepicker-change, so
|
|
60
|
+
// the originating child's data-role is what tells them apart.
|
|
61
|
+
function dateRangeOn(element, dotNetRef, methodName) {
|
|
62
|
+
var handler = function (e) {
|
|
63
|
+
var origin = e.target && e.target.closest ? e.target.closest('[data-role]') : null;
|
|
64
|
+
dotNetRef.invokeMethodAsync(methodName, {
|
|
65
|
+
value: (e.detail && e.detail.value) || '',
|
|
66
|
+
role: origin ? origin.getAttribute('data-role') : null
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
element.addEventListener('fdy-datepicker-change', handler);
|
|
70
|
+
var token = ++seq;
|
|
71
|
+
subs[token] = { el: element, type: 'fdy-datepicker-change', fn: handler };
|
|
72
|
+
return token;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Fire a .NET callback when a pointerdown lands outside `element`, or Escape is pressed —
|
|
76
|
+
// the light-dismiss primitive for Blazor-owned popovers (e.g. FdyTable's column filter, which
|
|
77
|
+
// is position:fixed in the top layer and so escapes the table's overflow). Returns an off() token.
|
|
78
|
+
function onOutside(element, dotNetRef, methodName) {
|
|
79
|
+
if (!element) return 0;
|
|
80
|
+
var onPointer = function (e) { if (!element.contains(e.target)) dotNetRef.invokeMethodAsync(methodName); };
|
|
81
|
+
var onKey = function (e) { if (e.key === 'Escape') dotNetRef.invokeMethodAsync(methodName); };
|
|
82
|
+
document.addEventListener('pointerdown', onPointer, true);
|
|
83
|
+
document.addEventListener('keydown', onKey, true);
|
|
84
|
+
var token = ++seq;
|
|
85
|
+
subs[token] = { el: document, type: 'pointerdown', fn: onPointer, key: onKey, capture: true };
|
|
86
|
+
return token;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Remove a subscription created by on() / onOutside(); removes with the same capture flag it
|
|
90
|
+
// was added with (a bubble-phase listener can't be removed with capture:true and vice versa).
|
|
59
91
|
function off(token) {
|
|
60
92
|
var s = subs[token];
|
|
61
93
|
if (s) {
|
|
62
|
-
s.el.removeEventListener(s.type, s.fn);
|
|
94
|
+
s.el.removeEventListener(s.type, s.fn, s.capture === true);
|
|
95
|
+
if (s.key) s.el.removeEventListener('keydown', s.key, true);
|
|
63
96
|
delete subs[token];
|
|
64
97
|
}
|
|
65
98
|
}
|
|
@@ -69,11 +102,76 @@
|
|
|
69
102
|
if (window.Freeday && typeof window.Freeday.toast === 'function') window.Freeday.toast(options);
|
|
70
103
|
}
|
|
71
104
|
|
|
105
|
+
// Passthrough to a combo's programmatic setter (window.FreedayCombo.setValue) so a
|
|
106
|
+
// Blazor @bind-Value can push an external value change onto the enhancer-owned DOM
|
|
107
|
+
// without the enhancer echoing an fdy-change back.
|
|
108
|
+
function comboSetValue(element, value) {
|
|
109
|
+
if (window.FreedayCombo && typeof window.FreedayCombo.setValue === 'function') {
|
|
110
|
+
window.FreedayCombo.setValue(element, value);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Repaint a chart from its current data-* attributes. FdyChart keeps the outer <div> +
|
|
115
|
+
// attributes under Blazor's control and calls this after each render, so the SVG re-renders
|
|
116
|
+
// when the bound data changes (window.FreedayChart.update === renderChart).
|
|
117
|
+
function chartUpdate(element) {
|
|
118
|
+
if (element && window.FreedayChart && typeof window.FreedayChart.update === 'function') {
|
|
119
|
+
window.FreedayChart.update(element);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// --- Native <dialog> control for FdyModal ---------------------------------
|
|
124
|
+
// The kit styles `.fdy-modal` as a <dialog> but nothing drives it; Blazor can't call
|
|
125
|
+
// showModal()/close() from C#, and Esc/backdrop dismissal must route through .NET state
|
|
126
|
+
// (not close the DOM behind its back). dialogInit wires cancel + backdrop → a [JSInvokable]
|
|
127
|
+
// dismiss callback; showModal()/close() are guarded (showModal on an open dialog throws).
|
|
128
|
+
var dialogSubs = {};
|
|
129
|
+
var dialogSeq = 0;
|
|
130
|
+
|
|
131
|
+
function dialogInit(dialog, dotNetRef, dismissMethod, dismissible) {
|
|
132
|
+
if (!dialog) return 0;
|
|
133
|
+
var onCancel = function (e) {
|
|
134
|
+
e.preventDefault(); // stop the native close; app state is the single source of truth
|
|
135
|
+
if (dismissible) dotNetRef.invokeMethodAsync(dismissMethod);
|
|
136
|
+
};
|
|
137
|
+
var onClick = function (e) {
|
|
138
|
+
// The ::backdrop is not a separate element — a click whose target is the dialog box
|
|
139
|
+
// itself (not its content) is a backdrop click.
|
|
140
|
+
if (dismissible && e.target === dialog) dotNetRef.invokeMethodAsync(dismissMethod);
|
|
141
|
+
};
|
|
142
|
+
dialog.addEventListener('cancel', onCancel);
|
|
143
|
+
dialog.addEventListener('click', onClick);
|
|
144
|
+
var token = ++dialogSeq;
|
|
145
|
+
dialogSubs[token] = { el: dialog, cancel: onCancel, click: onClick };
|
|
146
|
+
return token;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function dialogShow(dialog) {
|
|
150
|
+
if (dialog && typeof dialog.showModal === 'function' && !dialog.open) dialog.showModal();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function dialogClose(dialog) {
|
|
154
|
+
if (dialog && dialog.open) dialog.close();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function dialogDispose(token) {
|
|
158
|
+
var s = dialogSubs[token];
|
|
159
|
+
if (s) {
|
|
160
|
+
s.el.removeEventListener('cancel', s.cancel);
|
|
161
|
+
s.el.removeEventListener('click', s.click);
|
|
162
|
+
delete dialogSubs[token];
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
72
166
|
// Flip the document theme (data-theme on <html>), for the demo toggle.
|
|
73
167
|
function toggleTheme() {
|
|
74
168
|
var e = document.documentElement;
|
|
75
169
|
e.setAttribute('data-theme', e.getAttribute('data-theme') === 'dark' ? 'light' : 'dark');
|
|
76
170
|
}
|
|
77
171
|
|
|
78
|
-
window.FreedayBlazor = {
|
|
172
|
+
window.FreedayBlazor = {
|
|
173
|
+
initAll: initAll, on: on, off: off, onOutside: onOutside, toast: toast, toggleTheme: toggleTheme,
|
|
174
|
+
comboSetValue: comboSetValue, dateRangeOn: dateRangeOn, chartUpdate: chartUpdate,
|
|
175
|
+
dialogInit: dialogInit, dialogShow: dialogShow, dialogClose: dialogClose, dialogDispose: dialogDispose,
|
|
176
|
+
};
|
|
79
177
|
})();
|
package/dist/asset.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Shared type stub for every export imported purely for its side effects — the CSS
|
|
2
|
+
// bundles (`./css`, `./css/components`, `./tokens`) and the individual enhancer scripts
|
|
3
|
+
// (`./enhancers/*`). TypeScript only needs the `types` condition to *resolve* the module;
|
|
4
|
+
// there is no value surface to describe. (The root `.` import is different — it augments
|
|
5
|
+
// window.Freeday — so it points at ./freeday.d.ts instead.)
|
|
6
|
+
export {};
|
package/dist/freeday-select.js
CHANGED
|
@@ -91,7 +91,11 @@
|
|
|
91
91
|
setHighlight(-1);
|
|
92
92
|
if (openClose === close) openClose = null;
|
|
93
93
|
}
|
|
94
|
-
|
|
94
|
+
// Mark an option selected + reflect it in the button. `silent` skips the change
|
|
95
|
+
// event, close, and focus — used by the programmatic setValue() below so a host
|
|
96
|
+
// (e.g. a Blazor/framework wrapper) can push a value in without echoing an event
|
|
97
|
+
// back to itself or stealing focus. User-driven picks go through choose() (loud).
|
|
98
|
+
function select(opt, silent) {
|
|
95
99
|
options.forEach(function (other) {
|
|
96
100
|
var on = other === opt;
|
|
97
101
|
other.setAttribute('aria-selected', on ? 'true' : 'false');
|
|
@@ -101,12 +105,24 @@
|
|
|
101
105
|
root.setAttribute('data-value', opt.getAttribute('data-value') || '');
|
|
102
106
|
valueEl.textContent = optionLabel(opt);
|
|
103
107
|
valueEl.classList.remove('fdy-combo__value--placeholder');
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
108
|
+
if (!silent) {
|
|
109
|
+
root.dispatchEvent(new CustomEvent('fdy-change', {
|
|
110
|
+
bubbles: true,
|
|
111
|
+
detail: { value: opt.getAttribute('data-value') }
|
|
112
|
+
}));
|
|
113
|
+
close();
|
|
114
|
+
button.focus();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function choose(opt) { select(opt, false); }
|
|
118
|
+
// Programmatic set (no event, no focus change). No-op if no option matches.
|
|
119
|
+
function setValue(value) {
|
|
120
|
+
for (var i = 0; i < options.length; i++) {
|
|
121
|
+
if ((options[i].getAttribute('data-value') || '') === String(value)) {
|
|
122
|
+
select(options[i], true);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
110
126
|
}
|
|
111
127
|
|
|
112
128
|
button.addEventListener('click', function () {
|
|
@@ -167,6 +183,9 @@
|
|
|
167
183
|
root.addEventListener('focusout', function (e) {
|
|
168
184
|
if (!root.contains(e.relatedTarget)) close();
|
|
169
185
|
});
|
|
186
|
+
|
|
187
|
+
// Per-instance API for programmatic control (host wrappers / two-way binding).
|
|
188
|
+
root._fdyCombo = { setValue: setValue };
|
|
170
189
|
}
|
|
171
190
|
|
|
172
191
|
function initAll(context) {
|
|
@@ -190,5 +209,10 @@
|
|
|
190
209
|
initAll();
|
|
191
210
|
}
|
|
192
211
|
|
|
193
|
-
|
|
212
|
+
// Programmatic set on an already-initialised combo root (no fdy-change dispatched).
|
|
213
|
+
function setValue(root, value) {
|
|
214
|
+
if (root && root._fdyCombo) root._fdyCombo.setValue(value);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
window.FreedayCombo = { init: initCombo, initAll: initAll, setValue: setValue };
|
|
194
218
|
})();
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Type surface for the root side-effect import `@cahyo-dimas/freeday`, which loads every
|
|
2
|
+
// enhancer and registers the `window.Freeday` global. Authored from src/freeday-toast.js
|
|
3
|
+
// — keep in sync with it. (The per-component window.Freeday<Name> enhancer namespaces are
|
|
4
|
+
// intentionally not typed here; no consumer reconstructs those.)
|
|
5
|
+
|
|
6
|
+
export interface FreedayToastOptions {
|
|
7
|
+
/** Accent + ARIA role. Omitted = neutral. 'danger' uses role="alert"; the rest role="status". */
|
|
8
|
+
variant?: 'success' | 'warning' | 'danger' | 'info';
|
|
9
|
+
title?: string;
|
|
10
|
+
message?: string;
|
|
11
|
+
/** Milliseconds before auto-dismiss. Default 4000; 0 = sticky. */
|
|
12
|
+
timeout?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface FreedayGlobal {
|
|
16
|
+
/** Show a transient toast in a live region (created on first use). Returns the toast element. */
|
|
17
|
+
toast(opts?: FreedayToastOptions): HTMLElement;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
declare global {
|
|
21
|
+
interface Window {
|
|
22
|
+
Freeday?: FreedayGlobal;
|
|
23
|
+
}
|
|
24
|
+
}
|
package/dist/freeday.js
CHANGED
|
@@ -2340,7 +2340,11 @@
|
|
|
2340
2340
|
setHighlight(-1);
|
|
2341
2341
|
if (openClose === close) openClose = null;
|
|
2342
2342
|
}
|
|
2343
|
-
|
|
2343
|
+
// Mark an option selected + reflect it in the button. `silent` skips the change
|
|
2344
|
+
// event, close, and focus — used by the programmatic setValue() below so a host
|
|
2345
|
+
// (e.g. a Blazor/framework wrapper) can push a value in without echoing an event
|
|
2346
|
+
// back to itself or stealing focus. User-driven picks go through choose() (loud).
|
|
2347
|
+
function select(opt, silent) {
|
|
2344
2348
|
options.forEach(function (other) {
|
|
2345
2349
|
var on = other === opt;
|
|
2346
2350
|
other.setAttribute('aria-selected', on ? 'true' : 'false');
|
|
@@ -2350,12 +2354,24 @@
|
|
|
2350
2354
|
root.setAttribute('data-value', opt.getAttribute('data-value') || '');
|
|
2351
2355
|
valueEl.textContent = optionLabel(opt);
|
|
2352
2356
|
valueEl.classList.remove('fdy-combo__value--placeholder');
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2357
|
+
if (!silent) {
|
|
2358
|
+
root.dispatchEvent(new CustomEvent('fdy-change', {
|
|
2359
|
+
bubbles: true,
|
|
2360
|
+
detail: { value: opt.getAttribute('data-value') }
|
|
2361
|
+
}));
|
|
2362
|
+
close();
|
|
2363
|
+
button.focus();
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
function choose(opt) { select(opt, false); }
|
|
2367
|
+
// Programmatic set (no event, no focus change). No-op if no option matches.
|
|
2368
|
+
function setValue(value) {
|
|
2369
|
+
for (var i = 0; i < options.length; i++) {
|
|
2370
|
+
if ((options[i].getAttribute('data-value') || '') === String(value)) {
|
|
2371
|
+
select(options[i], true);
|
|
2372
|
+
return;
|
|
2373
|
+
}
|
|
2374
|
+
}
|
|
2359
2375
|
}
|
|
2360
2376
|
|
|
2361
2377
|
button.addEventListener('click', function () {
|
|
@@ -2416,6 +2432,9 @@
|
|
|
2416
2432
|
root.addEventListener('focusout', function (e) {
|
|
2417
2433
|
if (!root.contains(e.relatedTarget)) close();
|
|
2418
2434
|
});
|
|
2435
|
+
|
|
2436
|
+
// Per-instance API for programmatic control (host wrappers / two-way binding).
|
|
2437
|
+
root._fdyCombo = { setValue: setValue };
|
|
2419
2438
|
}
|
|
2420
2439
|
|
|
2421
2440
|
function initAll(context) {
|
|
@@ -2439,7 +2458,12 @@
|
|
|
2439
2458
|
initAll();
|
|
2440
2459
|
}
|
|
2441
2460
|
|
|
2442
|
-
|
|
2461
|
+
// Programmatic set on an already-initialised combo root (no fdy-change dispatched).
|
|
2462
|
+
function setValue(root, value) {
|
|
2463
|
+
if (root && root._fdyCombo) root._fdyCombo.setValue(value);
|
|
2464
|
+
}
|
|
2465
|
+
|
|
2466
|
+
window.FreedayCombo = { init: initCombo, initAll: initAll, setValue: setValue };
|
|
2443
2467
|
})();
|
|
2444
2468
|
|
|
2445
2469
|
/* Freeday — slider value binding (optional, zero-dependency).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cahyo-dimas/freeday",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.16.0",
|
|
4
4
|
"description": "Freeday — token-driven, framework-agnostic UI KIT (design source-of-truth).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -25,17 +25,18 @@
|
|
|
25
25
|
"adapters",
|
|
26
26
|
"tokens/tokens.json",
|
|
27
27
|
"tokens/breakpoints.mjs",
|
|
28
|
+
"tokens/breakpoints.d.ts",
|
|
28
29
|
"README.md",
|
|
29
30
|
"README.id.md",
|
|
30
31
|
"CHANGELOG.md"
|
|
31
32
|
],
|
|
32
33
|
"exports": {
|
|
33
|
-
".": "./dist/freeday.js",
|
|
34
|
-
"./css": "./dist/freeday.bundle.css",
|
|
35
|
-
"./css/components": "./dist/freeday.css",
|
|
36
|
-
"./tokens": "./dist/freeday.tokens.css",
|
|
37
|
-
"./breakpoints": "./tokens/breakpoints.mjs",
|
|
38
|
-
"./enhancers/*": "./dist/freeday-*.js",
|
|
34
|
+
".": { "types": "./dist/freeday.d.ts", "default": "./dist/freeday.js" },
|
|
35
|
+
"./css": { "types": "./dist/asset.d.ts", "default": "./dist/freeday.bundle.css" },
|
|
36
|
+
"./css/components": { "types": "./dist/asset.d.ts", "default": "./dist/freeday.css" },
|
|
37
|
+
"./tokens": { "types": "./dist/asset.d.ts", "default": "./dist/freeday.tokens.css" },
|
|
38
|
+
"./breakpoints": { "types": "./tokens/breakpoints.d.ts", "default": "./tokens/breakpoints.mjs" },
|
|
39
|
+
"./enhancers/*": { "types": "./dist/asset.d.ts", "default": "./dist/freeday-*.js" },
|
|
39
40
|
"./vue": {
|
|
40
41
|
"types": "./adapters/vue/index.d.ts",
|
|
41
42
|
"default": "./adapters/vue/index.js"
|
|
@@ -62,12 +63,17 @@
|
|
|
62
63
|
"scripts": {
|
|
63
64
|
"build": "node tokens/build.mjs",
|
|
64
65
|
"test": "node --test",
|
|
66
|
+
"test:browser": "node --test browser/vanilla.mjs browser/adapter.mjs",
|
|
65
67
|
"prepack": "node tokens/build.mjs",
|
|
66
68
|
"version": "node tokens/build.mjs && git add dist",
|
|
67
69
|
"typecheck:react": "tsc -p adapters/react/tsconfig.json --noEmit"
|
|
68
70
|
},
|
|
69
71
|
"devDependencies": {
|
|
70
72
|
"@types/react": "^19.2.17",
|
|
71
|
-
"
|
|
73
|
+
"esbuild": "^0.28.1",
|
|
74
|
+
"react": "^19.2.8",
|
|
75
|
+
"react-dom": "^19.2.8",
|
|
76
|
+
"typescript": "^7.0.2",
|
|
77
|
+
"vue": "^3.5.40"
|
|
72
78
|
}
|
|
73
79
|
}
|