summaryrefslogtreecommitdiff
path: root/Pages/Component/Button.razor
blob: 4212e509f28d112281939e16197a67c4d62f2a6e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
@using System.Timers
@implements IDisposable
@inject IJSRuntime js

<button
    @attributes=Attributes
    @onclick=Trigger
    @ref=button>

    @if(isLoading) {
        <svg
            width="15"
            height="15"
            viewBox="0 0 1000 1000"
            fill="none"
			class="loading">

            <path d="
                M 500 0
                A 500 500 0 1 1 0 500
                A 75 75 0 1 1 150 500
                A 350 350 0 1 0 500 150
                A 75 75 0 1 1 500 0
                Z"
                fill="currentColor">
            </path>
        </svg>
    } else if(IconUrl is not null) {
        <img src=@IconUrl />
    }

    @ChildContent
</button>

@code {
    [Parameter]
    public RenderFragment? ChildContent { get; set; }

    [Parameter]
    public Func<Task> OnClick { get; set; }

    [Parameter]
    public Color BackgroundColor { get; set; } = Color.ButtonSecondary;

    [Parameter]
    public string? IconUrl { get; set; }

    [Parameter]
    public char? ShortcutKey {
        get => shortcutKey;
        set {
            if(value is not null && !char.IsLetter((char) value))
                throw new System.ArgumentException("Cannot assign a non-letter to a shortcut");
            shortcutKey = value is null ? null : char.ToLower((char) value);
        }
    }

    [Parameter]
    public int? FontSize { get; set; }

    private Dictionary<string, object?> Attributes => new() {
        ["data-keyboard-shortcut"] = ShortcutKey,
        ["disabled"]               = isLoading,
        ["style"]                  = CssStyle
    };

    private string CssStyle => string.Join("", [
        $"--col-pri:{BackgroundColor.CssString};",
        FontSize is not null ? $"font-size:{FontSize}pt;" : null
    ]);

    private ElementReference button;

    private Timer loadingTimer;

    private char? shortcutKey;

    private object triggerLock = new();
    private bool   isRunning   = false;
    private bool   isLoading   = false;

    protected override void OnInitialized() {
        loadingTimer = new(50) {
            AutoReset = false
        };
        loadingTimer.Elapsed += TimerElapsed;
    }

    // Most button-click actions (e.g. triggering dialogs, page
    // navigation, etc) complete very quickly (under 50ms).
    // Triggering a render cycle to show a loading animation is
    // usually pointless as doing so would waste resources and
    // appear less seemless to the user who will see brief
    // flickering while the browser quickly recalculates it's
    // layout twice. To that end, a timer is used to only render
    // the loading animation on the button if the delegate task
    // takes longer than 50ms
    private void TimerElapsed(object? sender, EventArgs e) {
        lock(triggerLock) {
            if(isRunning)
                isLoading = true;
            InvokeAsync(() => StateHasChanged());
        }
    }

    private async Task Trigger() {
        lock(triggerLock) {
            if(isRunning)
                return;
            isRunning = true;
            loadingTimer.Start();
        }

        bool error = false;

        try {
            await Task.Run(OnClick);
        } catch {
            error = true;
        } finally {
            lock(triggerLock) {
                loadingTimer.Stop();
                isRunning = false;
                isLoading = false;
            }
        }

        await InvokeAsync(() => StateHasChanged());

        if(error)
            await js.InvokeVoidAsync("triggerButtonError", button);
    }

    public void Dispose() =>
        loadingTimer.Dispose();
}