373 lines
11 KiB
Plaintext
373 lines
11 KiB
Plaintext
@namespace SufiChain.SufiBlazor.Components.Overlays
|
||
@implements IAsyncDisposable
|
||
@inject IJSRuntime JSRuntime
|
||
@inject IStringLocalizer<SufiBlazorResource> L
|
||
|
||
<dialog @ref="_dialogRef"
|
||
class="@CssClass"
|
||
style="@EffectiveStyle"
|
||
@onkeydown="HandleKeyDown"
|
||
@oncancel="HandleCancel"
|
||
@oncancel:preventDefault="true"
|
||
@onclose="HandleNativeClose"
|
||
aria-labelledby="@(Title != null ? _titleId : null)"
|
||
aria-modal="true"
|
||
@attributes="FilteredAttributes">
|
||
<div class="sb-dialog__container" @onclick:stopPropagation="true">
|
||
@if (ShowHeader)
|
||
{
|
||
<header class="sb-dialog__header">
|
||
@if (Header != null)
|
||
{
|
||
@Header
|
||
}
|
||
else if (Title != null)
|
||
{
|
||
<h2 id="@_titleId" class="sb-dialog__title">@Title</h2>
|
||
}
|
||
@if (ShowCloseButton)
|
||
{
|
||
<button type="button"
|
||
class="sb-dialog__close-btn"
|
||
@onclick="HandleCloseButtonClick"
|
||
aria-label="@L["Close"]">
|
||
<span aria-hidden="true">×</span>
|
||
</button>
|
||
}
|
||
</header>
|
||
}
|
||
<div class="sb-dialog__body">
|
||
@ChildContent
|
||
</div>
|
||
@if (Footer != null)
|
||
{
|
||
<footer class="sb-dialog__footer">
|
||
@Footer
|
||
</footer>
|
||
}
|
||
</div>
|
||
</dialog>
|
||
|
||
@code {
|
||
private ElementReference _dialogRef;
|
||
private readonly string _titleId = $"sb-dialog-title-{Guid.NewGuid():N}";
|
||
private bool _isOpen;
|
||
private bool _hasRendered;
|
||
private bool _pendingOpenSync;
|
||
private bool _suppressNextNativeClose;
|
||
|
||
/// <summary>
|
||
/// Whether the dialog is open.
|
||
/// </summary>
|
||
[Parameter]
|
||
public bool Open { get; set; }
|
||
|
||
/// <summary>
|
||
/// Callback when open state changes.
|
||
/// </summary>
|
||
[Parameter]
|
||
public EventCallback<bool> OpenChanged { get; set; }
|
||
|
||
/// <summary>
|
||
/// Dialog title displayed in header.
|
||
/// </summary>
|
||
[Parameter]
|
||
public string? Title { get; set; }
|
||
|
||
/// <summary>
|
||
/// Custom header content (replaces Title).
|
||
/// </summary>
|
||
[Parameter]
|
||
public RenderFragment? Header { get; set; }
|
||
|
||
/// <summary>
|
||
/// Footer content for actions.
|
||
/// </summary>
|
||
[Parameter]
|
||
public RenderFragment? Footer { get; set; }
|
||
|
||
/// <summary>
|
||
/// Dialog body content.
|
||
/// </summary>
|
||
[Parameter]
|
||
public RenderFragment? ChildContent { get; set; }
|
||
|
||
/// <summary>
|
||
/// Dialog size preset (e.g. Lg). Used when Width/MaxWidth/Height/MaxHeight are not set; with Size alone the modal uses the preset dimensions.
|
||
/// </summary>
|
||
[Parameter]
|
||
public SbDialogSize Size { get; set; } = SbDialogSize.Md;
|
||
|
||
/// <summary>
|
||
/// Optional. Override width. Value is applied as-is to CSS (e.g. "800px", "80%", "50vw"). When set, overrides Size for width.
|
||
/// </summary>
|
||
[Parameter]
|
||
public string? Width { get; set; }
|
||
|
||
/// <summary>
|
||
/// Optional. Override max-width. Value is applied as-is to CSS (e.g. "800px", "80%", "1200px"). When set, overrides Size for max-width.
|
||
/// </summary>
|
||
[Parameter]
|
||
public string? MaxWidth { get; set; }
|
||
|
||
/// <summary>
|
||
/// Optional. Override height. Value is applied as-is to CSS (e.g. "80vh", "600px", "80%").
|
||
/// </summary>
|
||
[Parameter]
|
||
public string? Height { get; set; }
|
||
|
||
/// <summary>
|
||
/// Optional. Override max-height. Value is applied as-is to CSS (e.g. "90vh", "800px", "80%").
|
||
/// </summary>
|
||
[Parameter]
|
||
public string? MaxHeight { get; set; }
|
||
|
||
/// <summary>
|
||
/// Whether to close on Escape key.
|
||
/// </summary>
|
||
[Parameter]
|
||
public bool CloseOnEscape { get; set; } = true;
|
||
|
||
/// <summary>
|
||
/// Whether to close when clicking backdrop. Default is false so backdrop clicks do nothing.
|
||
/// </summary>
|
||
[Parameter]
|
||
public bool CloseOnBackdropClick { get; set; } = false;
|
||
|
||
/// <summary>
|
||
/// Whether to show the close button.
|
||
/// </summary>
|
||
[Parameter]
|
||
public bool ShowCloseButton { get; set; } = true;
|
||
|
||
/// <summary>
|
||
/// Optional callback when the header close (X) button is clicked.
|
||
/// Runs before the standard close flow; the dialog still invokes OpenChanged(false).
|
||
/// </summary>
|
||
[Parameter]
|
||
public EventCallback OnCloseButtonClick { get; set; }
|
||
|
||
/// <summary>
|
||
/// Callback when dialog closes with reason.
|
||
/// </summary>
|
||
[Parameter]
|
||
public EventCallback<SbDialogCloseReason> OnClose { get; set; }
|
||
|
||
/// <summary>
|
||
/// When true, the dialog body uses overflow: visible so dropdowns (e.g. SbSelect) inside the modal
|
||
/// are not clipped by the body. Use for modals that contain selects or other overflow menus.
|
||
/// </summary>
|
||
[Parameter]
|
||
public bool AllowDropdownOverflow { get; set; }
|
||
|
||
/// <summary>
|
||
/// Additional CSS classes.
|
||
/// </summary>
|
||
[Parameter]
|
||
public string? Class { get; set; }
|
||
|
||
/// <summary>
|
||
/// Inline styles.
|
||
/// </summary>
|
||
[Parameter]
|
||
public string? Style { get; set; }
|
||
|
||
/// <summary>
|
||
/// Additional HTML attributes.
|
||
/// </summary>
|
||
[Parameter(CaptureUnmatchedValues = true)]
|
||
public Dictionary<string, object>? AdditionalAttributes { get; set; }
|
||
|
||
private bool ShowHeader => Header != null || Title != null || ShowCloseButton;
|
||
|
||
private static string? GetDimensionValue(string? parameterValue, Dictionary<string, object>? attrs, params string[] keys)
|
||
{
|
||
if (!string.IsNullOrWhiteSpace(parameterValue))
|
||
return parameterValue.Trim();
|
||
if (attrs == null)
|
||
return null;
|
||
foreach (var key in keys)
|
||
{
|
||
if (attrs.TryGetValue(key, out var v) && v is string s && !string.IsNullOrWhiteSpace(s))
|
||
return s.Trim();
|
||
}
|
||
return null;
|
||
}
|
||
|
||
private bool HasDimensionOverrides
|
||
{
|
||
get
|
||
{
|
||
var w = GetDimensionValue(Width, AdditionalAttributes, "Width", "width");
|
||
var mw = GetDimensionValue(MaxWidth, AdditionalAttributes, "MaxWidth", "max-width", "maxwidth");
|
||
var h = GetDimensionValue(Height, AdditionalAttributes, "Height", "height");
|
||
var mh = GetDimensionValue(MaxHeight, AdditionalAttributes, "MaxHeight", "max-height", "maxheight");
|
||
return w != null || mw != null || h != null || mh != null;
|
||
}
|
||
}
|
||
|
||
private IReadOnlyDictionary<string, object>? FilteredAttributes
|
||
{
|
||
get
|
||
{
|
||
if (AdditionalAttributes == null || AdditionalAttributes.Count == 0)
|
||
return AdditionalAttributes;
|
||
var omit = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||
{ "width", "maxwidth", "max-width", "height", "maxheight", "max-height" };
|
||
var filtered = new Dictionary<string, object>(AdditionalAttributes);
|
||
foreach (var key in AdditionalAttributes.Keys)
|
||
{
|
||
if (omit.Contains(key))
|
||
filtered.Remove(key);
|
||
}
|
||
return filtered.Count == AdditionalAttributes.Count ? AdditionalAttributes : filtered;
|
||
}
|
||
}
|
||
|
||
private string EffectiveStyle
|
||
{
|
||
get
|
||
{
|
||
var parts = new List<string>();
|
||
var w = GetDimensionValue(Width, AdditionalAttributes, "Width", "width");
|
||
if (w != null)
|
||
parts.Add($"width: {w}");
|
||
var mw = GetDimensionValue(MaxWidth, AdditionalAttributes, "MaxWidth", "max-width", "maxwidth");
|
||
if (mw != null)
|
||
parts.Add($"max-width: {mw}");
|
||
var h = GetDimensionValue(Height, AdditionalAttributes, "Height", "height");
|
||
if (h != null)
|
||
parts.Add($"height: {h}");
|
||
var mh = GetDimensionValue(MaxHeight, AdditionalAttributes, "MaxHeight", "max-height", "maxheight");
|
||
if (mh != null)
|
||
parts.Add($"max-height: {mh}");
|
||
if (!string.IsNullOrWhiteSpace(Style))
|
||
parts.Add(Style.Trim());
|
||
return parts.Count > 0 ? string.Join("; ", parts) : string.Empty;
|
||
}
|
||
}
|
||
|
||
private string CssClass
|
||
{
|
||
get
|
||
{
|
||
var classes = new List<string> { "sb-dialog" };
|
||
if (!HasDimensionOverrides)
|
||
classes.Add($"sb-dialog--{Size.ToString().ToLowerInvariant()}");
|
||
if (AllowDropdownOverflow)
|
||
classes.Add("sb-dialog--allow-dropdown-overflow");
|
||
if (!string.IsNullOrWhiteSpace(Class))
|
||
classes.Add(Class);
|
||
return string.Join(" ", classes);
|
||
}
|
||
}
|
||
|
||
protected override async Task OnParametersSetAsync()
|
||
{
|
||
if (Open != _isOpen)
|
||
{
|
||
_isOpen = Open;
|
||
await SyncNativeOpenStateAsync();
|
||
}
|
||
}
|
||
|
||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||
{
|
||
_hasRendered = true;
|
||
|
||
if (_pendingOpenSync)
|
||
{
|
||
await SyncNativeOpenStateAsync();
|
||
}
|
||
}
|
||
|
||
private async Task ShowAsync()
|
||
{
|
||
await JSRuntime.InvokeVoidAsync("SufiBlazor.dialog.showModal", _dialogRef);
|
||
}
|
||
|
||
private async Task HideAsync()
|
||
{
|
||
await JSRuntime.InvokeVoidAsync("SufiBlazor.dialog.close", _dialogRef);
|
||
}
|
||
|
||
private async Task SyncNativeOpenStateAsync()
|
||
{
|
||
if (!_hasRendered)
|
||
{
|
||
_pendingOpenSync = true;
|
||
return;
|
||
}
|
||
|
||
_pendingOpenSync = false;
|
||
|
||
if (_isOpen)
|
||
{
|
||
await ShowAsync();
|
||
}
|
||
else
|
||
{
|
||
await HideAsync();
|
||
}
|
||
}
|
||
|
||
private async Task HandleCloseButtonClick()
|
||
{
|
||
if (OnCloseButtonClick.HasDelegate)
|
||
{
|
||
await OnCloseButtonClick.InvokeAsync();
|
||
}
|
||
|
||
await CloseAsync(SbDialogCloseReason.CloseButton);
|
||
}
|
||
|
||
private async Task CloseAsync(SbDialogCloseReason reason)
|
||
{
|
||
_suppressNextNativeClose = true;
|
||
_isOpen = false;
|
||
await SyncNativeOpenStateAsync();
|
||
await OnClose.InvokeAsync(reason);
|
||
await OpenChanged.InvokeAsync(false);
|
||
}
|
||
|
||
private async Task HandleKeyDown(KeyboardEventArgs args)
|
||
{
|
||
if (args.Key == "Escape" && CloseOnEscape)
|
||
{
|
||
await CloseAsync(SbDialogCloseReason.Escape);
|
||
}
|
||
}
|
||
|
||
private async Task HandleCancel()
|
||
{
|
||
// Native dialog cancel event (Escape key)
|
||
if (CloseOnEscape)
|
||
{
|
||
await CloseAsync(SbDialogCloseReason.Escape);
|
||
}
|
||
}
|
||
|
||
private async Task HandleNativeClose()
|
||
{
|
||
if (_suppressNextNativeClose)
|
||
{
|
||
_suppressNextNativeClose = false;
|
||
return;
|
||
}
|
||
|
||
if (!_isOpen)
|
||
{
|
||
return;
|
||
}
|
||
|
||
_isOpen = false;
|
||
await OnClose.InvokeAsync(SbDialogCloseReason.CloseButton);
|
||
await OpenChanged.InvokeAsync(false);
|
||
}
|
||
|
||
public async ValueTask DisposeAsync()
|
||
{
|
||
// Cleanup if needed
|
||
}
|
||
}
|