783da8e21a
Replace raw-JSON text inputs with rich UI: script parameter/return types use a JSON Schema builder (SchemaBuilder + JsonSchemaShapeParser, with a migration to convert existing definitions); alarm trigger config uses a type-aware editor with a flattened attribute picker (AlarmTriggerEditor). AlarmActor gains optional direction (rising/falling/either) on RateOfChange triggers.
155 lines
5.5 KiB
Plaintext
155 lines
5.5 KiB
Plaintext
@* Single global host component for IDialogService. Mounted once in MainLayout.
|
|
Listens to DialogService.OnChange and renders the current dialog state.
|
|
z-index ladder follows the same convention as ConfirmDialog/DiffDialog:
|
|
Toast container 1090 > this modal 1055 > this backdrop 1040. *@
|
|
@implements IDisposable
|
|
@inject IDialogService Service
|
|
@inject IJSRuntime JS
|
|
|
|
@if (Service is DialogService svc && svc.Current is { } state)
|
|
{
|
|
<div class="modal-backdrop fade show"></div>
|
|
<div @ref="_modalRef"
|
|
class="modal fade show d-block"
|
|
tabindex="-1"
|
|
role="dialog"
|
|
aria-modal="true"
|
|
@onkeydown="OnKeyDown">
|
|
<div class="modal-dialog modal-dialog-centered" role="document">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">@state.Title</h5>
|
|
<button type="button" class="btn-close" aria-label="Close" @onclick="Cancel"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
@if (state.Kind == DialogKind.Confirm)
|
|
{
|
|
<p class="mb-0">@state.Body</p>
|
|
}
|
|
else
|
|
{
|
|
<label class="form-label">@state.Body</label>
|
|
<input @ref="_promptInputRef"
|
|
class="form-control form-control-sm"
|
|
placeholder="@state.Placeholder"
|
|
value="@_promptValue"
|
|
@oninput="OnPromptInput" />
|
|
}
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-outline-secondary btn-sm" @onclick="Cancel">Cancel</button>
|
|
<button type="button"
|
|
class="btn @(state.Danger ? "btn-danger" : "btn-primary") btn-sm"
|
|
@onclick="Confirm">
|
|
@ConfirmLabel(state)
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
@code {
|
|
private ElementReference _modalRef;
|
|
private ElementReference _promptInputRef;
|
|
private string _promptValue = string.Empty;
|
|
private DialogState? _lastSeenState;
|
|
private DialogState? _focusedForState;
|
|
|
|
protected override void OnInitialized()
|
|
{
|
|
// OnChange lives on the concrete DialogService — the interface stays
|
|
// narrow (just ConfirmAsync / PromptAsync). DI hands us the concrete
|
|
// instance, so a cast here is safe.
|
|
if (Service is DialogService svc) svc.OnChange += OnServiceChanged;
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (Service is DialogService svc) svc.OnChange -= OnServiceChanged;
|
|
}
|
|
|
|
private void OnServiceChanged()
|
|
{
|
|
// Seed prompt input value when a new prompt dialog opens.
|
|
if (Service is DialogService s && s.Current is { Kind: DialogKind.Prompt } promptState
|
|
&& !ReferenceEquals(promptState, _lastSeenState))
|
|
{
|
|
_promptValue = promptState.PromptInitial;
|
|
}
|
|
_lastSeenState = (Service as DialogService)?.Current;
|
|
InvokeAsync(StateHasChanged);
|
|
}
|
|
|
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
|
{
|
|
var current = (Service as DialogService)?.Current;
|
|
if (current is not null)
|
|
{
|
|
try { await JS.InvokeVoidAsync("document.body.classList.add", "modal-open"); }
|
|
catch { /* prerender: no JS — ignore */ }
|
|
|
|
// Focus once per opened dialog. Without this guard, every input
|
|
// keystroke triggers a re-render which would re-focus the modal
|
|
// element and yank the caret off the prompt input.
|
|
if (!ReferenceEquals(current, _focusedForState))
|
|
{
|
|
_focusedForState = current;
|
|
try
|
|
{
|
|
if (current.Kind == DialogKind.Prompt)
|
|
await _promptInputRef.FocusAsync();
|
|
else
|
|
await _modalRef.FocusAsync();
|
|
}
|
|
catch { /* element not yet attached: ignore */ }
|
|
}
|
|
}
|
|
else
|
|
{
|
|
_focusedForState = null;
|
|
try { await JS.InvokeVoidAsync("document.body.classList.remove", "modal-open"); }
|
|
catch { /* prerender: no JS — ignore */ }
|
|
}
|
|
}
|
|
|
|
private void OnKeyDown(KeyboardEventArgs e)
|
|
{
|
|
if (e.Key == "Escape")
|
|
{
|
|
Cancel();
|
|
}
|
|
}
|
|
|
|
private void OnPromptInput(ChangeEventArgs e)
|
|
{
|
|
_promptValue = e.Value?.ToString() ?? string.Empty;
|
|
}
|
|
|
|
private void Cancel()
|
|
{
|
|
if (Service is not DialogService svc || svc.Current is null) return;
|
|
var resolveValue = svc.Current.Kind == DialogKind.Confirm
|
|
? (object)false
|
|
: (object?)null;
|
|
_promptValue = string.Empty;
|
|
svc.Resolve(resolveValue);
|
|
}
|
|
|
|
private void Confirm()
|
|
{
|
|
if (Service is not DialogService svc || svc.Current is null) return;
|
|
var resolveValue = svc.Current.Kind == DialogKind.Confirm
|
|
? (object)true
|
|
: (object?)_promptValue;
|
|
_promptValue = string.Empty;
|
|
svc.Resolve(resolveValue);
|
|
}
|
|
|
|
private static string ConfirmLabel(DialogState state) => state.Kind switch
|
|
{
|
|
DialogKind.Prompt => "Save",
|
|
_ => state.Danger ? "Delete" : "Confirm",
|
|
};
|
|
}
|