!3539 feat(#I61QLU): redesign modal component support dynamic load javascript

* chore: bump version 7.0.3-beta01
* test: 增加单元测试
* test: 增加代码覆盖率
* refactor: EditDialog 代码复用
* doc: 增加 ShowLoading 示例
* chore: 打包脚本
* feat: DialogBase 支持脚本动态加载
* refactor: 更新单元测试
* test: 增加单元测试
* test: 增加 Category 单元测试
* test: 更新单元测试
* test: 更新单元测试
* chore: 排除单元测试
* feat: 增加按钮参数
* test: 修复失败的单元测试
* test: 更新单元测试
* chore: 更新打包文件
* feat: 适配 Swal 组件
* feat: 增加 ShowHeader 参数
* refactor: 重构 Modal 组件
* refactor: 重构 Dialog 组件
* doc: 更新 Modal 示例
* doc: 更改资源文件
* doc: 更新示例
* feat: 增加 Modal 动态脚本
* chore: 更新资源文件
This commit is contained in:
Argo 2022-11-17 09:47:46 +00:00
parent 7b2a662c49
commit 2ed76dfa40
42 changed files with 673 additions and 999 deletions

View File

@ -1,8 +1,8 @@
<h3>无限弹窗示例</h3>
<h3>无限弹窗示例 @Title</h3>
<Tab>
<TabItem Text="用户管理">
<div>我是用户管理 @Value</div>
<div>我是用户管理</div>
<Button Text="弹窗" OnClick="@OnClickButton" />
</TabItem>
<TabItem Text="菜单管理">

View File

@ -9,29 +9,15 @@ namespace BootstrapBlazor.Shared.Components;
/// </summary>
public partial class DialogDemo
{
/// <summary>
///
/// </summary>
[NotNull]
public string? Value { get; set; }
[Inject]
[NotNull]
private DialogService? DialogService { get; set; }
/// <summary>
/// OnInitialized 方法
/// </summary>
protected override void OnInitialized()
{
base.OnInitialized();
Value = DateTime.Now.ToString();
}
private string Title { get; } = DateTime.Now.ToString();
private Task OnClickButton() => DialogService.Show(new DialogOption()
{
Title = $"弹窗 {Value}",
Title = "Pop-up",
Component = BootstrapDynamicComponent.CreateComponent<DialogDemo>()
});
}

View File

@ -349,10 +349,7 @@
"P2": "A dialog box pops up, suitable for scenarios that require more customization",
"P3": "Popup title",
"P4": "popup text",
"P5": "by setting",
"P6": "component",
"P7": "Parameter, whether to open the pop-up window is supported",
"P8": "Please click the back button to set and then click the <b>popup</b> button to test the effect",
"P5": "by setting <code>Modal</code> component <code>IsKeyboard</code> parameter, whether to open the pop-up window is supported <kbd>ESC</kbd>, Please click the back button to set and then click the <b>popup</b> button to test the effect",
"P9": "Pop-ups",
"P10": "Default popup",
"P11": "I am the text in the pop-up window",
@ -772,8 +769,7 @@
"P74": "Fullscreen popup (&lt; 1200px)",
"P75": "Print button",
"P76": "Show a print preview button on the <code>Header</code> by setting <code>ShowPrintButton</code>",
"P77": "by setting",
"P78": "Change the <b>Print Preview</b> button text",
"P77": "by setting <code>PrintButtonText</code> change the <b>Print Preview</b> button text",
"P79": "Click to open Dialog",
"P80": "Full screen popup",
"P81": "Show a window maximize button on the <code>Header</code> by setting <code>ShowMaximizeButton</code>",

View File

@ -350,10 +350,7 @@
"P2": "弹出一个对话框,适合需要定制性更大的场景",
"P3": "弹窗标题",
"P4": "弹窗正文",
"P5": "通过设置",
"P6": "组件的",
"P7": "参数,开启弹窗是否支持",
"P8": "请点击后面按钮设置后再点击 <b>弹窗</b> 按钮测试效果",
"P5": "通过设置 <code>Modal</code> 组件的 <code>IsKeyboard</code> 参数,开启弹窗是否支持 <kbd>ESC</kbd>,请点击后面按钮设置后再点击 <b>弹窗</b> 按钮测试效果",
"P9": "弹窗",
"P10": "默认弹窗",
"P11": "我是弹窗内正文",
@ -773,8 +770,7 @@
"P74": "全屏弹窗(&lt; 1200px)",
"P75": "打印按钮",
"P76": "通过设置 <code>ShowPrintButton</code> 使 <code>Header</code> 上显示一个打印预览按钮",
"P77": "通过设置",
"P78": "更改 <b>打印预览</b> 按钮文字",
"P77": "通过设置 <code>PrintButtonText</code> 更改 <b>打印预览</b> 按钮文字",
"P79": "点击打开 Dialog",
"P80": "全屏弹窗",
"P81": "通过设置 <code>ShowMaximizeButton</code> 使 <code>Header</code> 上显示一个窗口最大化按钮",

View File

@ -141,12 +141,12 @@ await DialogService.Show(op);</Pre>
<Button OnClick="@OnSaveDialogClick">@Localizer["P71"]</Button>
</DemoBlock>
<DemoBlock Title="@Localizer["P72"]" Introduction="@Localizer["P73"]" Name="ModalDialog">
<Button OnClick="@OnSizeDialogClick">@Localizer["P74"]</Button>
<DemoBlock Title="@Localizer["P72"]" Introduction="@Localizer["P73"]" Name="Size">
<Button OnClick="@OnSizeDialogClick">@((MarkupString)Localizer["P74"].Value)</Button>
</DemoBlock>
<DemoBlock Title="@Localizer["P75"]" Introduction="@Localizer["P76"]" Name="PrintDialog">
<p>@Localizer["P77"] <code>@nameof(DialogOption.PrintButtonText)</code> @Localizer["P78"]</p>
<p>@((MarkupString)Localizer["P77"].Value)</p>
<Button OnClick="@OnPrintDialogClick">@Localizer["P79"]</Button>
</DemoBlock>

View File

@ -89,9 +89,8 @@ public sealed partial class Dialogs
Title = "I am the popup created by the service",
BodyTemplate = BootstrapDynamicComponent.CreateComponent<Button>(new Dictionary<string, object?>
{
[nameof(Button.ChildContent)] = new RenderFragment(builder => builder.AddContent(0, "我是服务创建的按钮"))
})
.Render()
[nameof(Button.ChildContent)] = new RenderFragment(builder => builder.AddContent(0, "Button"))
}).Render()
});
private async Task Show()
@ -148,7 +147,7 @@ public sealed partial class Dialogs
// Modal 组件 ShownCallbackAsync 触发后调用 Option 实例的 ShownCallbackAsync
[nameof(ShownCallbackDummy.ShownTodo)] = new Action<Func<Task>>(cb =>
{
option.ShownCallbackAsync = async () =>
option.OnShownAsync = async () =>
{
await cb();
};
@ -270,7 +269,7 @@ public sealed partial class Dialogs
{
await DialogService.Show(new DialogOption()
{
Title = $"弹窗 {DateTime.Now}",
Title = $"Multiple Pop-up",
Component = BootstrapDynamicComponent.CreateComponent<DialogDemo>()
});
}
@ -282,8 +281,14 @@ public sealed partial class Dialogs
Title = "Edit popup",
Model = new Foo(),
RowType = RowType.Inline,
ShowLoading = true,
ItemsPerRow = 2,
ItemChangedType = ItemChangedType.Update
ItemChangedType = ItemChangedType.Update,
OnEditAsync = async context =>
{
await Task.Delay(2000);
return false;
}
};
await DialogService.ShowEditDialog(option);
}

View File

@ -5,7 +5,7 @@
<h4>@Localizer["H2"]</h4>
<DemoBlock Title="@Localizer["P1"]" Introduction="@Localizer["P2"]" Name="Normal">
<div class="modal d-block position-relative">
<div class="modal d-block position-relative" style="z-index: 2;">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
@ -23,7 +23,7 @@
</div>
</div>
<div class="mt-3">
<p>@Localizer["P5"] <code>Modal</code> @Localizer["P6"] <code>IsKeyboard</code> @Localizer["P7"] <kbd>ESC</kbd>@Localizer["P8"]</p>
<p>@((MarkupString)Localizer["P5"].Value)</p>
<Button OnClick="async e => await Modal.Toggle()">@Localizer["P9"]</Button>
<Button OnClick="OnClickKeyboard" Text="@($"Keyboard: {IsKeyboard}")" class="ms-3" />
</div>
@ -231,7 +231,7 @@
<DemoBlock Title="弹窗已显示回调方法" Introduction="通过设置 <code>ShownCallbackAsync</code> 回调委托,弹窗显示后回调此方法" Name="ShownCallbackAsync">
<Button OnClick="@(async e => await ShownCallbackModal.Toggle())">弹窗</Button>
<Modal @ref="ShownCallbackModal" ShownCallbackAsync="OnShownCallbackAsync">
<Modal @ref="ShownCallbackModal" OnShownAsync="OnShownCallbackAsync">
<ModalDialog Title="ShownCallbackAsync 回调示例">
<BodyTemplate>
<div>我是弹窗内正文</div>

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<Version>7.0.2</Version>
<Version>7.0.3-beta01</Version>
</PropertyGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net5.0'">

View File

@ -1,9 +1,9 @@
@namespace BootstrapBlazor.Components
@inherits BootstrapComponentBase
<Modal AdditionalAttributes="@AdditionalAttributes" @ref="ModalContainer" IsKeyboard="@IsKeyboard" ShownCallbackAsync="@OnShownCallbackAsync">
@foreach (var parameter in DialogParameters)
<Modal AdditionalAttributes="@AdditionalAttributes" @ref="ModalContainer" IsBackdrop="IsBackdrop" IsKeyboard="@IsKeyboard" OnShownAsync="@OnShownAsync" OnCloseAsync="OnCloseAsync">
@for (var index = 0; index < DialogParameters.Count; index++)
{
@RenderDialog(parameter)
@RenderDialog(index, DialogParameters[index])
}
</Modal>

View File

@ -22,6 +22,8 @@ public partial class Dialog : IDisposable
private bool IsKeyboard { get; set; }
private bool IsBackdrop { get; set; }
/// <summary>
/// DialogServices 服务实例
/// </summary>
@ -29,9 +31,13 @@ public partial class Dialog : IDisposable
[NotNull]
private DialogService? DialogService { get; set; }
private bool IsShowDialog { get; set; }
[NotNull]
private Func<Task>? OnShownAsync { get; set; }
private Func<Task>? ShownCallbackAsync { get; set; }
[NotNull]
private Func<Task>? OnCloseAsync { get; set; }
private Dictionary<string, object>? CurrentParameter { get; set; }
/// <summary>
/// OnInitialized 方法
@ -53,27 +59,47 @@ public partial class Dialog : IDisposable
{
await base.OnAfterRenderAsync(firstRender);
if (IsShowDialog)
if (CurrentParameter != null)
{
IsShowDialog = false;
await ModalContainer.Show();
}
}
private Task Show(DialogOption option)
{
ShownCallbackAsync = async () =>
OnShownAsync = async () =>
{
if (option.ShownCallbackAsync != null)
if (option.OnShownAsync != null)
{
await option.ShownCallbackAsync();
await option.OnShownAsync();
}
};
OnCloseAsync = async () =>
{
// 回调 OnCloseAsync
if (option.OnCloseAsync != null)
{
await option.OnCloseAsync();
}
// 移除当前 DialogParameter
if (CurrentParameter != null)
{
DialogParameters.Remove(CurrentParameter);
// 多弹窗支持
CurrentParameter = DialogParameters.LastOrDefault();
StateHasChanged();
}
};
IsKeyboard = option.IsKeyboard;
IsBackdrop = option.IsBackdrop;
option.Dialog = ModalContainer;
var parameters = option.ToAttributes();
var parameters = option.ToAttributes();
var content = option.BodyTemplate ?? option.Component?.Render();
if (content != null)
{
@ -115,50 +141,23 @@ public partial class Dialog : IDisposable
parameters.Add(nameof(ModalDialog.SaveButtonText), option.SaveButtonText);
}
parameters.Add(nameof(ModalDialog.OnClose), new Func<Task>(async () =>
{
// 回调 OnClose 方法
// 移除当前对话框
if (option.OnCloseAsync != null)
{
await option.OnCloseAsync();
}
DialogParameters.Remove(parameters);
// 支持多级弹窗
await ModalContainer.CloseOrPopDialog();
StateHasChanged();
}));
// 保存当前 Dialog 参数
CurrentParameter = parameters;
// 添加 ModalDialog 到容器中
DialogParameters.Add(parameters);
if (DialogParameters.Count == 1)
{
IsShowDialog = true;
}
StateHasChanged();
return Task.CompletedTask;
}
private RenderFragment RenderDialog(IEnumerable<KeyValuePair<string, object>> parameter) => builder =>
private static RenderFragment RenderDialog(int index, IEnumerable<KeyValuePair<string, object>> parameter) => builder =>
{
builder.OpenComponent<ModalDialog>(0);
builder.AddMultipleAttributes(1, parameter);
builder.AddComponentReferenceCapture(2, dialog =>
{
var modal = (ModalDialog)dialog;
ModalContainer.ShowDialog(modal);
});
builder.OpenComponent<ModalDialog>(100 + index);
builder.AddMultipleAttributes(101 + index, parameter);
builder.SetKey(parameter);
builder.CloseComponent();
};
private async Task OnShownCallbackAsync()
{
if (ShownCallbackAsync != null)
{
await ShownCallbackAsync();
}
}
/// <summary>
/// Dispose 方法
/// </summary>

View File

@ -7,7 +7,7 @@ namespace BootstrapBlazor.Components;
/// <summary>
///
/// </summary>
public abstract class DialogBase<TModel> : ComponentBase
public abstract class DialogBase<TModel> : BootstrapModuleComponentBase
{
/// <summary>
/// 获得/设置 EditModel 实例

View File

@ -65,6 +65,11 @@ public class DialogOption
/// </summary>
public bool IsKeyboard { get; set; } = true;
/// <summary>
/// 获得/设置 是否支持点击遮罩关闭弹窗 默认 false
/// </summary>
public bool IsBackdrop { get; set; }
/// <summary>
/// 获得/设置 是否显示 Footer 默认为 true
/// </summary>
@ -153,7 +158,7 @@ public class DialogOption
/// <summary>
/// 获得/设置 弹窗已显示时回调此方法
/// </summary>
public Func<Task>? ShownCallbackAsync { get; set; }
public Func<Task>? OnShownAsync { get; set; }
/// <summary>
/// 将参数转换为组件属性方法

View File

@ -1,11 +0,0 @@
(function ($) {
$.extend({
bb_form_load: function (el, method) {
var $el = $(el);
if (method === 'show')
$el.addClass('show');
else
$el.removeClass('show');
}
});
})(jQuery);

View File

@ -9,44 +9,21 @@
@BodyTemplate(Model)
</div>
<div class="form-footer">
@if (FooterTemplate != null)
{
<CascadingValue Value="OnCloseAsync" IsFixed>
@FooterTemplate(Model)
</CascadingValue>
}
else
{
if (!IsTracking)
{
<Button Color="Color.Secondary" Icon="fa-solid fa-xmark" Text="@CloseButtonText" OnClickWithoutRender="OnClickClose" />
}
<Button Color="Color.Primary" ButtonType="ButtonType.Submit" Icon="fa-solid fa-floppy-disk" Text="@SaveButtonText" />
}
@RenderFooter
</div>
}
else
{
<EditorForm TModel="TModel" Items="Items" ItemChangedType="ItemChangedType" ItemsPerRow="ItemsPerRow" RowType="RowType" LabelAlign="LabelAlign" ShowLabel="ShowLabel" ShowUnsetGroupItemsOnTop="ShowUnsetGroupItemsOnTop">
<Buttons>
@if (FooterTemplate != null)
{
<CascadingValue Value="OnCloseAsync" IsFixed>
@FooterTemplate(Model)
</CascadingValue>
}
else
{
if (!IsTracking)
{
<Button Color="Color.Secondary" Icon="fa-solid fa-xmark" Text="@CloseButtonText" OnClickWithoutRender="OnClickClose" />
}
<Button Color="Color.Primary" ButtonType="ButtonType.Submit" Icon="fa-solid fa-floppy-disk" Text="@SaveButtonText" />
}
@RenderFooter
</Buttons>
</EditorForm>
}
@if (ShowLoading)
{
<div class="form-loader" id="@Id">
<Spinner Color="Color.Primary" />
</div>
}
</ValidateForm>
<div class="form-loader" @ref="SpinnerElement">
<Spinner Color="Color.Primary" />
</div>

View File

@ -10,10 +10,9 @@ namespace BootstrapBlazor.Components;
/// <summary>
/// 编辑弹窗组件
/// </summary>
[JSModuleAutoLoader("edit-dialog")]
public partial class EditDialog<TModel>
{
private ElementReference SpinnerElement { get; set; }
/// <summary>
/// 获得/设置 保存回调委托
/// </summary>
@ -75,10 +74,6 @@ public partial class EditDialog<TModel>
[NotNull]
private IStringLocalizer<EditDialog<TModel>>? Localizer { get; set; }
[Inject]
[NotNull]
private IJSRuntime? JSRuntime { get; set; }
/// <summary>
/// OnParametersSet 方法
/// </summary>
@ -114,7 +109,37 @@ public partial class EditDialog<TModel>
{
if (ShowLoading)
{
await JSRuntime.InvokeVoidAsync(SpinnerElement, "bb_form_load", state ? "show" : "hide");
await InvokeExecuteAsync(Id, state);
}
}
private RenderFragment RenderFooter => builder =>
{
if (FooterTemplate != null)
{
builder.OpenComponent<CascadingValue<Func<Task>?>>(0);
builder.AddAttribute(1, nameof(CascadingValue<Func<Task>?>.Value), OnCloseAsync);
builder.AddAttribute(2, nameof(CascadingValue<Func<Task>?>.IsFixed), true);
builder.AddAttribute(3, nameof(CascadingValue<Func<Task>?>.ChildContent), FooterTemplate(Model));
builder.CloseComponent();
}
else
{
if (!IsTracking)
{
builder.OpenComponent<Button>(20);
builder.AddAttribute(21, nameof(Button.Color), Color.Secondary);
builder.AddAttribute(22, nameof(Button.Icon), "fa-solid fa-xmark");
builder.AddAttribute(23, nameof(Button.Text), CloseButtonText);
builder.AddAttribute(24, nameof(Button.OnClickWithoutRender), OnClickClose);
builder.CloseComponent();
}
builder.OpenComponent<Button>(30);
builder.AddAttribute(31, nameof(Button.Color), Color.Primary);
builder.AddAttribute(32, nameof(Button.Icon), "fa-solid fa-floppy-disk");
builder.AddAttribute(33, nameof(Button.Text), SaveButtonText);
builder.AddAttribute(34, nameof(Button.ButtonType), ButtonType.Submit);
builder.CloseComponent();
}
};
}

View File

@ -9,6 +9,7 @@ namespace BootstrapBlazor.Components;
/// <summary>
/// 查询弹窗组件
/// </summary>
[JSModuleNotInherited]
public partial class SearchDialog<TModel>
{
/// <summary>

View File

@ -1,126 +0,0 @@
(function ($) {
$.extend({
bb_modal_dialog: function (el, obj, method) {
var $el = $(el);
$el.data('bb_dotnet_invoker', { obj, method });
// monitor mousedown ready to drag dialog
var originX = 0;
var originY = 0;
var dialogWidth = 0;
var dialogHeight = 0;
var pt = { top: 0, left: 0 };
if ($el.hasClass('is-draggable')) {
$el.find('.btn-maximize').click(function () {
$button = $(this);
var status = $button.attr('aria-label');
if (status === "maximize") {
$el.css({
"marginLeft": "auto",
"width": $el.width(),
});
}
else {
var handler = window.setInterval(function () {
if ($el.attr('style')) {
$el.removeAttr('style');
}
else {
window.clearInterval(handler);
}
}, 100);
}
});
$el.css({
"marginLeft": "auto"
});
$el.find('.modal-header').drag(
function (e) {
originX = e.clientX || e.touches[0].clientX;
originY = e.clientY || e.touches[0].clientY;
// 弹窗大小
dialogWidth = $el.width();
dialogHeight = $el.height();
// 偏移量
pt.top = parseInt($el.css('marginTop').replace("px", ""));
pt.left = parseInt($el.css('marginLeft').replace("px", ""));
$el.css({ "marginLeft": pt.left, "marginTop": pt.top });
// 固定大小
$el.css("width", dialogWidth);
this.addClass('is-drag');
},
function (e) {
var eventX = e.clientX || e.changedTouches[0].clientX;
var eventY = e.clientY || e.changedTouches[0].clientY;
newValX = pt.left + Math.ceil(eventX - originX);
newValY = pt.top + Math.ceil(eventY - originY);
if (newValX <= 0) newValX = 0;
if (newValY <= 0) newValY = 0;
if (newValX + dialogWidth < $(window).width()) {
$el.css({ "marginLeft": newValX });
}
if (newValY + dialogHeight < $(window).height()) {
$el.css({ "marginTop": newValY });
}
},
function (e) {
this.removeClass('is-drag');
}
);
}
},
bb_modal: function (el, obj, method, callback) {
var $el = $(el);
if (method === 'dispose') {
$el.remove();
}
else if (method === 'init') {
function keyHandler() {
var e = event;
if (e.key === 'Escape') {
var $dialog = $el.find('.modal-dialog');
var invoker = $dialog.data('bb_dotnet_invoker');
if (invoker != null) {
invoker.obj.invokeMethodAsync(invoker.method);
}
}
};
if ($el.closest('.swal').length === 0) {
// move self end of the body
$('body').append($el);
}
$el.on('shown.bs.modal', function () {
var keyboard = $el.attr('data-bs-keyboard') === "true";
if (keyboard === true) {
document.addEventListener('keyup', keyHandler, false);
}
obj.invokeMethodAsync(callback);
});
$el.on('hide.bs.modal', function () {
var keyboard = $el.attr('data-bs-keyboard') === "true";
if (keyboard === true) {
document.removeEventListener('keyup', keyHandler, false);
}
})
}
else {
if (method !== 'hide' && method !== 'dispose') {
var instance = bootstrap.Modal.getInstance(el);
if (instance != null) {
instance._config.keyboard = false;
}
}
$el.modal(method);
}
}
});
})(jQuery);

View File

@ -1,7 +1,7 @@
@namespace BootstrapBlazor.Components
@inherits BootstrapComponentBase
@inherits BootstrapModuleComponentBase
<div @attributes="@AdditionalAttributes" class="@ClassString" tabindex="-1" role="dialog" aria-hidden="true" data-bs-backdrop="@Backdrop" data-bs-keyboard="@KeyboardString" @ref="ModalElement">
<div @attributes="@AdditionalAttributes" class="@ClassString" tabindex="-1" role="dialog" data-bs-backdrop="@Backdrop" data-bs-keyboard="@KeyboardString" id="@Id">
<CascadingValue Value="this" IsFixed="true">
@ChildContent
</CascadingValue>

View File

@ -5,15 +5,11 @@
namespace BootstrapBlazor.Components;
/// <summary>
///
/// Modal 组件
/// </summary>
public partial class Modal : IAsyncDisposable
[JSModuleAutoLoader(JSObjectReference = true)]
public partial class Modal
{
/// <summary>
/// 获得/设置 DOM 元素实例
/// </summary>
private ElementReference ModalElement { get; set; }
/// <summary>
/// 获得 样式字符串
/// </summary>
@ -24,7 +20,7 @@ public partial class Modal : IAsyncDisposable
/// <summary>
/// 获得 ModalDialog 集合
/// </summary>
private List<ModalDialog> Dialogs { get; } = new(8);
protected List<ModalDialog> Dialogs { get; } = new(8);
/// <summary>
/// 获得/设置 是否后台关闭弹窗 默认 false
@ -54,8 +50,22 @@ public partial class Modal : IAsyncDisposable
/// 获得/设置 弹窗已显示时回调此方法
/// </summary>
[Parameter]
[Obsolete("Call OnShownAsync")]
[ExcludeFromCodeCoverage]
public Func<Task>? ShownCallbackAsync { get; set; }
/// <summary>
/// 获得/设置 弹窗已显示时回调此方法
/// </summary>
[Parameter]
public Func<Task>? OnShownAsync { get; set; }
/// <summary>
/// 获得/设置 关闭弹窗回调委托
/// </summary>
[Parameter]
public Func<Task>? OnCloseAsync { get; set; }
/// <summary>
/// 获得 后台关闭弹窗设置
/// </summary>
@ -63,7 +73,11 @@ public partial class Modal : IAsyncDisposable
private string? KeyboardString => IsKeyboard ? "true" : "false";
private JSInterop<Modal>? Interop { get; set; }
/// <summary>
/// <inheritdoc/>
/// </summary>
/// <returns></returns>
protected override Task ModuleInitAsync() => InvokeInitAsync(Id, nameof(ShownCallback), nameof(CloseCallback));
/// <summary>
/// 添加对话框方法
@ -71,140 +85,89 @@ public partial class Modal : IAsyncDisposable
/// <param name="dialog"></param>
internal void AddDialog(ModalDialog dialog)
{
if (!Dialogs.Any())
{
dialog.IsShown = true;
}
Dialogs.Add(dialog);
ResetShownDialog(dialog);
}
/// <summary>
/// 移除对话框方法
/// </summary>
/// <param name="dialog"></param>
internal void RemoveDialog(ModalDialog? dialog = null)
internal void RemoveDialog(ModalDialog dialog)
{
if (dialog == null)
// 移除当前弹窗
Dialogs.Remove(dialog);
if (Dialogs.Any())
{
dialog = Dialogs.LastOrDefault();
dialog?.Close();
}
else
{
Dialogs.Remove(dialog);
ResetShownDialog(Dialogs.Last());
}
}
/// <summary>
/// 显示指定对话框方法
/// </summary>
/// <param name="dialog"></param>
internal void ShowDialog(ModalDialog? dialog = null)
private void ResetShownDialog(ModalDialog dialog)
{
dialog ??= Dialogs.LastOrDefault();
if (dialog != null)
// 保证新添加的 Dialog 为当前弹窗
Dialogs.ForEach(d =>
{
Dialogs.ForEach(d => d.IsShown = d == dialog);
}
d.IsShown = d == dialog;
});
}
/// <summary>
/// OnAfterRenderAsync 方法
/// </summary>
/// <param name="firstRender"></param>
/// <returns></returns>
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);
if (firstRender)
{
Interop ??= new(JSRuntime);
await Interop.InvokeVoidAsync(this, ModalElement, "bb_modal", "init", nameof(Shown));
}
}
/// <summary>
/// 弹窗已经弹出回调方法
/// 弹窗已经弹出回调方法 JSInvoke 调用
/// </summary>
/// <returns></returns>
[JSInvokable]
public async Task Shown()
public async Task ShownCallback()
{
if (ShownCallbackAsync != null)
if (OnShownAsync != null)
{
await ShownCallbackAsync();
await OnShownAsync();
}
}
/// <summary>
/// 弹窗已经关闭回调方法 JSInvoke 调用
/// </summary>
/// <returns></returns>
[JSInvokable]
public async Task CloseCallback()
{
// 移除当前弹窗
var dialog = Dialogs.FirstOrDefault(d => d.IsShown);
if (dialog != null)
{
Dialogs.Remove(dialog);
}
// 多级弹窗支持
if (Dialogs.Any())
{
ResetShownDialog(Dialogs.Last());
}
if (OnCloseAsync != null)
{
await OnCloseAsync();
}
}
/// <summary>
/// 弹窗状态切换方法
/// </summary>
public async ValueTask Toggle()
{
var dialog = Dialogs.FirstOrDefault();
if (dialog != null)
{
dialog.IsShown = true;
}
if (Interop != null)
{
await Interop.InvokeVoidAsync(this, ModalElement, "bb_modal", "toggle");
}
}
public Task Toggle() => InvokeExecuteAsync(Id, "show");
/// <summary>
/// 显示弹窗方法
/// </summary>
/// <returns></returns>
public async ValueTask Show()
{
var dialog = Dialogs.LastOrDefault();
if (dialog != null)
{
Dialogs.ForEach(d => d.IsShown = dialog == d);
}
if (Interop != null)
{
await Interop.InvokeVoidAsync(this, ModalElement, "bb_modal", "show");
}
}
public Task Show() => InvokeExecuteAsync(Id, "show");
/// <summary>
/// 关闭当前弹窗方法
/// </summary>
/// <returns></returns>
public async Task Close()
{
var dialog = Dialogs.FirstOrDefault(d => d.IsShown);
if (dialog != null)
{
await dialog.Close();
}
else
{
await CloseOrPopDialog();
}
}
/// <summary>
/// 内部使用如果还有弹窗继续显示,如果没有弹窗关闭所有
/// </summary>
/// <returns></returns>
internal async ValueTask CloseOrPopDialog()
{
if (Dialogs.Any())
{
ShowDialog();
}
else if (Interop != null)
{
// 全部关闭
await Interop.InvokeVoidAsync(this, ModalElement, "bb_modal", "hide");
}
}
public Task Close() => InvokeExecuteAsync(Id, "hide");
/// <summary>
/// 设置 Header 文字方法
@ -213,40 +176,6 @@ public partial class Modal : IAsyncDisposable
public void SetHeaderText(string text)
{
var dialog = Dialogs.FirstOrDefault(d => d.IsShown);
if (dialog != null)
{
dialog.SetHeaderText(text);
}
}
/// <summary>
/// Dispose
/// </summary>
/// <param name="disposing"></param>
protected virtual async ValueTask DisposeAsyncCore(bool disposing)
{
if (disposing)
{
// 切换线程防止 JS 清理 DOM 后 C# 代码报错
// https://gitee.com/LongbowEnterprise/BootstrapBlazor/issues/I4PKOC
await Task.Delay(300);
// JS 清理 DOM
if (Interop != null)
{
await Interop.InvokeVoidAsync(this, ModalElement, "bb_modal", "dispose");
Interop.Dispose();
Interop = null;
}
}
}
/// <summary>
/// Dispose 方法
/// </summary>
public async ValueTask DisposeAsync()
{
await DisposeAsyncCore(true);
GC.SuppressFinalize(this);
dialog?.SetHeaderText(text);
}
}

View File

@ -1,36 +1,39 @@
@namespace BootstrapBlazor.Components
@inherits BootstrapComponentBase
<div class="@ClassName" role="document" @ref="DialogElement">
<div class="@ClassName">
<div class="modal-content">
<div class="modal-header">
@if (HeaderTemplate != null)
{
@HeaderTemplate
}
else
{
<h5 class="modal-title flex-fill">@Title</h5>
}
<div class="modal-header-buttons">
@if (HeaderToolbarTemplate != null)
@if (ShowHeader)
{
<div class="modal-header">
@if (HeaderTemplate != null)
{
@HeaderToolbarTemplate
@HeaderTemplate
}
@if (ShowPrintButton && ShowPrintButtonInHeader)
else
{
<PrintButton Color="Color.Primary" class="btn-print" Text="@PrintButtonText" />
}
@if (ShowMaximizeButton)
{
<Button Color="Color.None" class="btn-maximize" aria-label="@MaximizeAriaLabel" OnClick="@OnToggleMaximize" Icon="@MaximizeIcon"></Button>
}
@if (ShowHeaderCloseButton)
{
<Button Color="Color.None" class="btn-close" aria-label="Close" OnClickWithoutRender="@OnClickClose"></Button>
<h5 class="modal-title flex-fill">@Title</h5>
}
<div class="modal-header-buttons">
@if (HeaderToolbarTemplate != null)
{
@HeaderToolbarTemplate
}
@if (ShowPrintButton && ShowPrintButtonInHeader)
{
<PrintButton Color="Color.Primary" class="btn-print" Text="@PrintButtonText" />
}
@if (ShowMaximizeButton)
{
<Button Color="Color.None" class="btn-maximize" aria-label="@MaximizeAriaLabel" OnClick="@OnToggleMaximize" Icon="@MaximizeIcon"></Button>
}
@if (ShowHeaderCloseButton)
{
<Button Color="Color.None" class="btn-close" aria-label="Close" OnClickWithoutRender="@OnClickClose"></Button>
}
</div>
</div>
</div>
}
<CascadingValue Name="BodyContext" Value="@BodyContext" IsFixed="true">
<div class="modal-body">
@RenderBodyTemplate()

View File

@ -11,11 +11,6 @@ namespace BootstrapBlazor.Components;
/// </summary>
public partial class ModalDialog : IHandlerException, IDisposable
{
private ElementReference DialogElement { get; set; }
[NotNull]
private JSInterop<ModalDialog>? Interop { get; set; }
private string MaximizeAriaLabel => MaximizeStatus ? "maximize" : "restore";
/// <summary>
@ -109,6 +104,12 @@ public partial class ModalDialog : IHandlerException, IDisposable
[Parameter]
public bool ShowHeaderCloseButton { get; set; } = true;
/// <summary>
/// 获得/设置 是否显示 Header 默认为 true
/// </summary>
[Parameter]
public bool ShowHeader { get; set; } = true;
/// <summary>
/// 获得/设置 是否显示 Footer 默认为 true
/// </summary>
@ -157,11 +158,11 @@ public partial class ModalDialog : IHandlerException, IDisposable
[Parameter]
public RenderFragment? HeaderTemplate { get; set; }
/// <summary>
/// 获得/设置 关闭弹窗回调委托
/// </summary>
[Parameter]
public Func<Task>? OnClose { get; set; }
///// <summary>
///// 获得/设置 关闭弹窗回调委托
///// </summary>
//[Parameter]
//public Func<Task>? OnClose { get; set; }
/// <summary>
/// 获得/设置 保存按钮回调委托
@ -194,7 +195,7 @@ public partial class ModalDialog : IHandlerException, IDisposable
/// </summary>
[CascadingParameter]
[NotNull]
public Modal? Modal { get; set; }
protected Modal? Modal { get; set; }
[Inject]
[NotNull]
@ -208,9 +209,6 @@ public partial class ModalDialog : IHandlerException, IDisposable
base.OnInitialized();
ErrorLogger?.Register(this);
Interop = new JSInterop<ModalDialog>(JSRuntime);
Modal.AddDialog(this);
}
@ -226,21 +224,6 @@ public partial class ModalDialog : IHandlerException, IDisposable
PrintButtonText ??= Localizer[nameof(PrintButtonText)];
}
/// <summary>
/// OnAfterRenderAsync 方法
/// </summary>
/// <param name="firstRender"></param>
/// <returns></returns>
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);
if (firstRender)
{
await Interop.InvokeVoidAsync(this, DialogElement, "bb_modal_dialog", nameof(Close));
}
}
/// <summary>
/// 设置 Header 文字方法
/// </summary>
@ -251,16 +234,7 @@ public partial class ModalDialog : IHandlerException, IDisposable
StateHasChanged();
}
private async Task OnClickClose()
{
Modal.RemoveDialog(this);
await Modal.CloseOrPopDialog();
if (OnClose != null)
{
await OnClose();
}
}
private async Task OnClickClose() => await Modal.Close();
private bool MaximizeStatus { get; set; }
@ -285,13 +259,6 @@ public partial class ModalDialog : IHandlerException, IDisposable
}
}
/// <summary>
/// Close 方法 客户端按 ESC 键盘时调用
/// </summary>
/// <returns></returns>
[JSInvokable]
public Task Close() => OnClickClose();
private RenderFragment RenderBodyTemplate() => builder =>
{
builder.AddContent(0, _errorContent ?? BodyTemplate);
@ -324,9 +291,7 @@ public partial class ModalDialog : IHandlerException, IDisposable
if (disposing)
{
ErrorLogger?.UnRegister(this);
Interop.Dispose();
Interop = null;
Modal.RemoveDialog(this);
}
}

View File

@ -15,7 +15,7 @@ public abstract class PopupOptionBase
public string? Content { get; set; }
/// <summary>
/// 获得/设置 是否自动隐藏 默认 true 自动关闭
/// 获得/设置 是否自动隐藏 默认 true 自动关闭 <see cref="SweetAlert"/> 默认 false
/// </summary>
public bool IsAutoHide { get; set; } = true;

View File

@ -12,7 +12,7 @@ public class SwalOption : PopupOptionBase
/// <summary>
/// 获得/设置 相关弹窗实例
/// </summary>
internal Modal? Dialog { get; set; }
internal Modal? Modal { get; set; }
/// <summary>
/// 获得/设置 模态弹窗返回值任务实例
@ -34,11 +34,6 @@ public class SwalOption : PopupOptionBase
/// </summary>
public string? Title { get; set; }
/// <summary>
/// 获得/设置 相关连数据,多用于传值使用
/// </summary>
public object? BodyContext { get; set; }
/// <summary>
/// 获得/设置 ModalBody 组件
/// </summary>
@ -64,6 +59,31 @@ public class SwalOption : PopupOptionBase
/// </summary>
public RenderFragment? ButtonTemplate { get; set; }
/// <summary>
/// 获得/设置 关闭按钮图标 默认 fa-solid fa-xmark
/// </summary>
public string? CloseButtonIcon { get; set; }
/// <summary>
/// 获得/设置 确认按钮图标 默认 fa-solid fa-check
/// </summary>
public string? ConfirmButtonIcon { get; set; }
/// <summary>
/// 获得/设置 关闭按钮文字 默认为 关闭
/// </summary>
public string? CloseButtonText { get; set; }
/// <summary>
/// 获得/设置 确认按钮文字 默认为 确认
/// </summary>
public string? ConfirmButtonText { get; set; }
/// <summary>
/// 获得/设置 取消按钮文字 默认为 取消
/// </summary>
public string? CancelButtonText { get; set; }
/// <summary>
///
/// </summary>
@ -76,18 +96,22 @@ public class SwalOption : PopupOptionBase
/// 将参数转换为组件属性方法
/// </summary>
/// <returns></returns>
public Dictionary<string, object?> ToAttributes()
public Dictionary<string, object> ToAttributes()
{
var parameters = new Dictionary<string, object?>
var parameters = new Dictionary<string, object>
{
[nameof(Size)] = Size.Medium,
[nameof(ModalDialog.IsCentered)] = true,
[nameof(ModalDialog.IsScrolling)] = false,
[nameof(ModalDialog.ShowCloseButton)] = false,
[nameof(ShowFooter)] = false,
[nameof(ModalDialog.Title)] = Title,
[nameof(BodyContext)] = BodyContext
[nameof(ModalDialog.ShowHeader)] = false,
[nameof(ModalDialog.ShowFooter)] = false
};
if (!string.IsNullOrEmpty(Title))
{
parameters.Add(nameof(Title), Title);
}
return parameters;
}
@ -97,9 +121,9 @@ public class SwalOption : PopupOptionBase
/// <param name="returnValue">模态弹窗返回值 默认为 true</param>
public async Task Close(bool returnValue = true)
{
if (Dialog != null)
if (Modal != null)
{
await Dialog.Close();
await Modal.Close();
}
if (IsModalConfirm)

View File

@ -4,11 +4,6 @@
z-index: var(--bb-swal-zindex);
}
.swal .modal-header,
.swal .modal-header-buttons {
display: none;
}
.swal2-icon,
.swal2-actions {
display: flex;

View File

@ -3,7 +3,7 @@
@inject SwalService Swal
<div class="swal">
<Modal AdditionalAttributes="@AdditionalAttributes" @ref="ModalContainer">
<Modal @ref="ModalContainer" IsKeyboard="false" OnCloseAsync="OnCloseAsync">
@RenderDialog()
</Modal>
</div>

View File

@ -31,7 +31,10 @@ public partial class SweetAlert : IDisposable
private CancellationTokenSource? DelayToken { get; set; }
[NotNull]
private Dictionary<string, object?>? DialogParameter { get; set; }
private Dictionary<string, object>? DialogParameter { get; set; }
[NotNull]
private Func<Task>? OnCloseAsync { get; set; }
/// <summary>
/// OnInitialized 方法
@ -60,42 +63,47 @@ public partial class SweetAlert : IDisposable
if (IsAutoHide && Delay > 0)
{
if (DelayToken == null)
{
DelayToken = new CancellationTokenSource();
}
await DelayCloseAsync();
}
}
[ExcludeFromCodeCoverage]
async Task DelayCloseAsync()
{
DelayToken ??= new CancellationTokenSource();
try
{
await Task.Delay(Delay, DelayToken.Token);
await ModalContainer.Close();
}
catch
{
if (!DelayToken.IsCancellationRequested)
{
// 自动关闭弹窗
await ModalContainer.Close();
}
}
}
}
private Task Show(SwalOption option)
{
IsAutoHide = option.IsAutoHide;
Delay = option.Delay;
option.Dialog = ModalContainer;
var parameters = option.ToAttributes();
parameters.Add(nameof(ModalDialog.OnClose), new Func<Task>(async () =>
OnCloseAsync = () =>
{
if (IsAutoHide && DelayToken != null)
{
DelayToken.Cancel();
DelayToken = null;
}
DialogParameter = null;
await ModalContainer.CloseOrPopDialog();
StateHasChanged();
}));
// 移除当前 DialogParameter
DialogParameter = null;
StateHasChanged();
return Task.CompletedTask;
};
IsAutoHide = option.IsAutoHide;
Delay = option.Delay;
option.Modal = ModalContainer;
var parameters = option.ToAttributes();
parameters.Add(nameof(ModalDialog.BodyTemplate), BootstrapDynamicComponent.CreateComponent<SweetAlertBody>(SweetAlertBody.Parse(option)).Render());
DialogParameter = parameters;
@ -110,10 +118,7 @@ public partial class SweetAlert : IDisposable
{
var index = 0;
builder.OpenComponent<ModalDialog>(index++);
foreach (var p in DialogParameter)
{
builder.AddAttribute(index++, p.Key, p.Value);
}
builder.AddMultipleAttributes(index++, DialogParameter);
builder.CloseComponent();
}
};
@ -141,7 +146,7 @@ public partial class SweetAlert : IDisposable
/// </summary>
public void Dispose()
{
Dispose(disposing: true);
Dispose(true);
GC.SuppressFinalize(this);
}
}

View File

@ -44,17 +44,11 @@ else
<div class="swal2-actions">
@if (ShowClose)
{
<button type="button" class="btn btn-secondary" @onclick="@OnClickClose">
<i class="fa-solid fa-xmark"></i>
<span>@(IsConfirm ? CancelButtonText : CloseButtonText)</span>
</button>
<Button Color="Color.Secondary" Icon="@CloseButtonIcon" Text="@InternalCloseButtonText" OnClickWithoutRender="OnClickClose" />
}
@if (IsConfirm)
{
<button type="button" class="btn btn-danger ms-3" @onclick="@OnClickConfirm">
<i class="fa-solid fa-check"></i>
<span>@ConfirmButtonText</span>
</button>
<Button Color="Color.Danger" Icon="@ConfirmButtonIcon" Text="@ConfirmButtonText" OnClickWithoutRender="OnClickConfirm" class="ms-3" />
}
@ButtonTemplate
</div>

View File

@ -11,10 +11,13 @@ namespace BootstrapBlazor.Components;
/// </summary>
public partial class SweetAlertBody
{
private string InternalCloseButtonText => IsConfirm ? CancelButtonText : CloseButtonText;
/// <summary>
/// 获得/设置 关闭按钮文字 默认为 关闭
/// </summary>
[Parameter]
[NotNull]
public string? CloseButtonText { get; set; }
/// <summary>
@ -67,17 +70,31 @@ public partial class SweetAlertBody
[Parameter]
public bool IsConfirm { get; set; }
/// <summary>
/// 获得/设置 关闭按钮图标 默认 fa-solid fa-xmark
/// </summary>
[Parameter]
[NotNull]
public string? CloseButtonIcon { get; set; }
/// <summary>
/// 获得/设置 确认按钮图标 默认 fa-solid fa-check
/// </summary>
[Parameter]
[NotNull]
public string? ConfirmButtonIcon { get; set; }
/// <summary>
/// 获得/设置 关闭按钮回调方法
/// </summary>
[Parameter]
public Action? OnClose { get; set; }
public Func<Task>? OnCloseAsync { get; set; }
/// <summary>
/// 获得/设置 确认按钮回调方法
/// </summary>
[Parameter]
public Action? OnConfirm { get; set; }
public Func<Task>? OnConfirmAsync { get; set; }
/// <summary>
/// 获得/设置 显示内容模板
@ -120,13 +137,18 @@ public partial class SweetAlertBody
[nameof(SweetAlertBody.ShowClose)] = option.ShowClose,
[nameof(SweetAlertBody.IsConfirm)] = option.IsModalConfirm,
[nameof(SweetAlertBody.ShowFooter)] = option.ShowFooter,
[nameof(SweetAlertBody.OnClose)] = new Action(async () => await option.Close(false)),
[nameof(SweetAlertBody.OnConfirm)] = new Action(async () => await option.Close(true)),
[nameof(SweetAlertBody.OnCloseAsync)] = () => option.Close(false),
[nameof(SweetAlertBody.OnConfirmAsync)] = () => option.Close(true),
[nameof(SweetAlertBody.Title)] = option.Title,
[nameof(SweetAlertBody.Content)] = option.Content,
[nameof(SweetAlertBody.BodyTemplate)] = option.BodyTemplate,
[nameof(SweetAlertBody.FooterTemplate)] = option.FooterTemplate,
[nameof(SweetAlertBody.ButtonTemplate)] = option.ButtonTemplate
[nameof(SweetAlertBody.ButtonTemplate)] = option.ButtonTemplate,
[nameof(SweetAlertBody.CloseButtonIcon)] = option.CloseButtonIcon,
[nameof(SweetAlertBody.ConfirmButtonIcon)] = option.ConfirmButtonIcon,
[nameof(SweetAlertBody.CloseButtonText)] = option.CloseButtonText,
[nameof(SweetAlertBody.CancelButtonText)] = option.CancelButtonText,
[nameof(SweetAlertBody.ConfirmButtonText)] = option.ConfirmButtonText
};
/// <summary>
@ -139,9 +161,24 @@ public partial class SweetAlertBody
CloseButtonText ??= Localizer[nameof(CloseButtonText)];
CancelButtonText ??= Localizer[nameof(CancelButtonText)];
ConfirmButtonText ??= Localizer[nameof(ConfirmButtonText)];
CloseButtonIcon ??= "fa-solid fa-xmark";
ConfirmButtonIcon ??= "fa-solid fa-check";
}
private void OnClickClose() => OnClose?.Invoke();
private async Task OnClickClose()
{
if (OnCloseAsync != null)
{
await OnCloseAsync();
}
}
private void OnClickConfirm() => OnConfirm?.Invoke();
private async Task OnClickConfirm()
{
if (OnConfirmAsync != null)
{
await OnConfirmAsync();
}
}
}

View File

@ -11,12 +11,12 @@
--bb-table-header-min-height: 37px;
--bb-table-footer-font-weight: blod;
--bb-table-footer-border-top: 2px solid #dee2e6;
--bb-table-loader-bg: #f8f9fa;
--bb-table-card-row-padding: .75rem .5rem;
--bb-table-columnlist-max-height: var(--bb-dropdown-max-height);
--bs-table-striped-bg: rgba(0,0,0,.05);
--bs-table-hover-bg: rgba(0,0,0,.075);
--bb-table-search-body-margin: 1rem;
--bb-loader-bg: #f8f9fa;
}
.table-container .table:not(.table-excel) .switch {
@ -364,7 +364,7 @@
right: 0;
top: 0;
bottom: 0;
background-color: var(--bb-table-loader-bg);
background-color: var(--bb-loader-bg);
z-index: 5;
}

View File

@ -1,4 +1,7 @@
.form-control {
form {
--bb-loader-bg: #f8f9fa;
}
.form-control {
--bb-form-control-padding: 0.375rem 0.75rem;
padding: var(--bb-form-control-padding);
border: 1px solid var(--bs-border-color);

View File

@ -26,8 +26,7 @@ public static class DialogServiceExtensions
[nameof(SearchDialog<TModel>.Items)] = option.Items ?? Utility.GenerateColumns<TModel>(item => item.Searchable),
[nameof(SearchDialog<TModel>.OnResetSearchClick)] = new Func<Task>(async () =>
{
option.OnCloseAsync = null;
option.Dialog.RemoveDialog();
await option.Dialog.Close();
if (option.OnResetSearchClick != null)
{
await option.OnResetSearchClick();
@ -35,8 +34,7 @@ public static class DialogServiceExtensions
}),
[nameof(SearchDialog<TModel>.OnSearchClick)] = new Func<Task>(async () =>
{
option.OnCloseAsync = null;
option.Dialog.RemoveDialog();
await option.Dialog.Close();
if (option.OnSearchClick != null)
{
await option.OnSearchClick();
@ -70,8 +68,7 @@ public static class DialogServiceExtensions
[nameof(EditDialog<TModel>.Items)] = option.Items ?? Utility.GenerateColumns<TModel>(item => item.Editable),
[nameof(EditDialog<TModel>.OnCloseAsync)] = new Func<Task>(async () =>
{
option.Dialog.RemoveDialog();
await option.Dialog.CloseOrPopDialog();
await option.Dialog.Close();
}),
[nameof(EditDialog<TModel>.OnSaveAsync)] = new Func<EditContext, Task>(async context =>
{
@ -80,8 +77,7 @@ public static class DialogServiceExtensions
var ret = await option.OnEditAsync(context);
if (ret)
{
option.Dialog.RemoveDialog();
await option.Dialog.CloseOrPopDialog();
await option.Dialog.Close();
}
}
}),

View File

@ -20,6 +20,6 @@ public static class SwalExtensions
{
option.IsModalConfirm = true;
await service.Show(option, swal);
return !option.IsModalConfirm || await option.ReturnTask.Task;
return await option.ReturnTask.Task;
}
}

View File

@ -1292,18 +1292,6 @@
});
})(jQuery);
(function ($) {
$.extend({
bb_form_load: function (el, method) {
var $el = $(el);
if (method === 'show')
$el.addClass('show');
else
$el.removeClass('show');
}
});
})(jQuery);
(function ($) {
$.extend({
bb_drawer: function (el, open) {
@ -1850,133 +1838,6 @@
});
})(jQuery);
(function ($) {
$.extend({
bb_modal_dialog: function (el, obj, method) {
var $el = $(el);
$el.data('bb_dotnet_invoker', { obj, method });
// monitor mousedown ready to drag dialog
var originX = 0;
var originY = 0;
var dialogWidth = 0;
var dialogHeight = 0;
var pt = { top: 0, left: 0 };
if ($el.hasClass('is-draggable')) {
$el.find('.btn-maximize').click(function () {
$button = $(this);
var status = $button.attr('aria-label');
if (status === "maximize") {
$el.css({
"marginLeft": "auto",
"width": $el.width(),
});
}
else {
var handler = window.setInterval(function () {
if ($el.attr('style')) {
$el.removeAttr('style');
}
else {
window.clearInterval(handler);
}
}, 100);
}
});
$el.css({
"marginLeft": "auto"
});
$el.find('.modal-header').drag(
function (e) {
originX = e.clientX || e.touches[0].clientX;
originY = e.clientY || e.touches[0].clientY;
// 弹窗大小
dialogWidth = $el.width();
dialogHeight = $el.height();
// 偏移量
pt.top = parseInt($el.css('marginTop').replace("px", ""));
pt.left = parseInt($el.css('marginLeft').replace("px", ""));
$el.css({ "marginLeft": pt.left, "marginTop": pt.top });
// 固定大小
$el.css("width", dialogWidth);
this.addClass('is-drag');
},
function (e) {
var eventX = e.clientX || e.changedTouches[0].clientX;
var eventY = e.clientY || e.changedTouches[0].clientY;
newValX = pt.left + Math.ceil(eventX - originX);
newValY = pt.top + Math.ceil(eventY - originY);
if (newValX <= 0) newValX = 0;
if (newValY <= 0) newValY = 0;
if (newValX + dialogWidth < $(window).width()) {
$el.css({ "marginLeft": newValX });
}
if (newValY + dialogHeight < $(window).height()) {
$el.css({ "marginTop": newValY });
}
},
function (e) {
this.removeClass('is-drag');
}
);
}
},
bb_modal: function (el, obj, method, callback) {
var $el = $(el);
if (method === 'dispose') {
$el.remove();
}
else if (method === 'init') {
function keyHandler() {
var e = event;
if (e.key === 'Escape') {
var $dialog = $el.find('.modal-dialog');
var invoker = $dialog.data('bb_dotnet_invoker');
if (invoker != null) {
invoker.obj.invokeMethodAsync(invoker.method);
}
}
};
if ($el.closest('.swal').length === 0) {
// move self end of the body
$('body').append($el);
}
$el.on('shown.bs.modal', function () {
var keyboard = $el.attr('data-bs-keyboard') === "true";
if (keyboard === true) {
document.addEventListener('keyup', keyHandler, false);
}
obj.invokeMethodAsync(callback);
});
$el.on('hide.bs.modal', function () {
var keyboard = $el.attr('data-bs-keyboard') === "true";
if (keyboard === true) {
document.removeEventListener('keyup', keyHandler, false);
}
})
}
else {
if (method !== 'hide' && method !== 'dispose') {
var instance = bootstrap.Modal.getInstance(el);
if (instance != null) {
instance._config.keyboard = false;
}
}
$el.modal(method);
}
}
});
})(jQuery);
(function ($) {
$.extend({
bb_notify_checkPermission: function (obj, method, requestPermission) {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -19279,18 +19279,6 @@ return jQuery;
});
})(jQuery);
(function ($) {
$.extend({
bb_form_load: function (el, method) {
var $el = $(el);
if (method === 'show')
$el.addClass('show');
else
$el.removeClass('show');
}
});
})(jQuery);
(function ($) {
$.extend({
bb_drawer: function (el, open) {
@ -19837,133 +19825,6 @@ return jQuery;
});
})(jQuery);
(function ($) {
$.extend({
bb_modal_dialog: function (el, obj, method) {
var $el = $(el);
$el.data('bb_dotnet_invoker', { obj, method });
// monitor mousedown ready to drag dialog
var originX = 0;
var originY = 0;
var dialogWidth = 0;
var dialogHeight = 0;
var pt = { top: 0, left: 0 };
if ($el.hasClass('is-draggable')) {
$el.find('.btn-maximize').click(function () {
$button = $(this);
var status = $button.attr('aria-label');
if (status === "maximize") {
$el.css({
"marginLeft": "auto",
"width": $el.width(),
});
}
else {
var handler = window.setInterval(function () {
if ($el.attr('style')) {
$el.removeAttr('style');
}
else {
window.clearInterval(handler);
}
}, 100);
}
});
$el.css({
"marginLeft": "auto"
});
$el.find('.modal-header').drag(
function (e) {
originX = e.clientX || e.touches[0].clientX;
originY = e.clientY || e.touches[0].clientY;
// 弹窗大小
dialogWidth = $el.width();
dialogHeight = $el.height();
// 偏移量
pt.top = parseInt($el.css('marginTop').replace("px", ""));
pt.left = parseInt($el.css('marginLeft').replace("px", ""));
$el.css({ "marginLeft": pt.left, "marginTop": pt.top });
// 固定大小
$el.css("width", dialogWidth);
this.addClass('is-drag');
},
function (e) {
var eventX = e.clientX || e.changedTouches[0].clientX;
var eventY = e.clientY || e.changedTouches[0].clientY;
newValX = pt.left + Math.ceil(eventX - originX);
newValY = pt.top + Math.ceil(eventY - originY);
if (newValX <= 0) newValX = 0;
if (newValY <= 0) newValY = 0;
if (newValX + dialogWidth < $(window).width()) {
$el.css({ "marginLeft": newValX });
}
if (newValY + dialogHeight < $(window).height()) {
$el.css({ "marginTop": newValY });
}
},
function (e) {
this.removeClass('is-drag');
}
);
}
},
bb_modal: function (el, obj, method, callback) {
var $el = $(el);
if (method === 'dispose') {
$el.remove();
}
else if (method === 'init') {
function keyHandler() {
var e = event;
if (e.key === 'Escape') {
var $dialog = $el.find('.modal-dialog');
var invoker = $dialog.data('bb_dotnet_invoker');
if (invoker != null) {
invoker.obj.invokeMethodAsync(invoker.method);
}
}
};
if ($el.closest('.swal').length === 0) {
// move self end of the body
$('body').append($el);
}
$el.on('shown.bs.modal', function () {
var keyboard = $el.attr('data-bs-keyboard') === "true";
if (keyboard === true) {
document.addEventListener('keyup', keyHandler, false);
}
obj.invokeMethodAsync(callback);
});
$el.on('hide.bs.modal', function () {
var keyboard = $el.attr('data-bs-keyboard') === "true";
if (keyboard === true) {
document.removeEventListener('keyup', keyHandler, false);
}
})
}
else {
if (method !== 'hide' && method !== 'dispose') {
var instance = bootstrap.Modal.getInstance(el);
if (instance != null) {
instance._config.keyboard = false;
}
}
$el.modal(method);
}
}
});
})(jQuery);
(function ($) {
$.extend({
bb_notify_checkPermission: function (obj, method, requestPermission) {

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,12 @@
import BlazorComponent from "./base/blazor-component.js"
export class EditDialog extends BlazorComponent {
_execute(args) {
var show = args[0]
if (show) {
this._element.classList.add('show')
} else {
this._element.classList.remove('show')
}
}
}

View File

@ -0,0 +1,92 @@
import BlazorComponent from "./base/blazor-component.js"
import EventHandler from "./base/event-handler.js"
export class Modal extends BlazorComponent {
_init() {
this._invoker = this._config.arguments[0]
this._invokerShownMethod = this._config.arguments[1]
this._invokerCloseMethod = this._config.arguments[2]
this._setEventListeners()
}
_setEventListeners() {
EventHandler.on(this._element, 'shown.bs.modal', () => {
this._invoker.invokeMethodAsync(this._invokerShownMethod)
})
EventHandler.on(this._element, 'hide.bs.modal', () => {
this._invoker.invokeMethodAsync(this._invokerCloseMethod)
})
console.log('pop1')
this._pop = () => {
console.log('pop2')
if (this._modal) {
this._modal._dialog.remove()
this._modal.dispose()
this._modal = null
document.body.classList.remove('modal-open');
document.body.style.paddingLeft = '';
document.body.style.paddingRight = '';
document.body.style.overflow = '';
}
}
EventHandler.on(window, 'popstate', this._pop)
}
_execute(args) {
const method = args[1]
if (method === 'show') {
this._show()
} else if (method === 'hide') {
this._hide()
} else if (method === 'toggle') {
this._toggle()
}
}
_show() {
const dialogs = this._element.querySelectorAll('.modal-dialog')
if (dialogs.length === 1) {
const keyboard = this._element.getAttribute('data-bs-keyboard') === 'true'
let backdrop = this._element.getAttribute('data-bs-backdrop')
if (backdrop === null) {
backdrop = true
}
if (!this._modal) {
this._modal = bootstrap.Modal.getOrCreateInstance(this._element)
}
this._modal._config.keyboard = keyboard
this._modal._config.backdrop = backdrop
this._modal.show()
} else {
this._invoker.invokeMethodAsync(this._invokerShownMethod)
}
}
_hide() {
const dialogs = this._element.querySelectorAll('.modal-dialog')
if (dialogs.length === 1) {
this._modal.hide()
} else {
this._invoker.invokeMethodAsync(this._invokerCloseMethod)
}
}
_toggle() {
if (this._modal) {
this._modal.toggle()
} else {
this._show()
}
}
_dispose() {
EventHandler.off(this._element, 'shown.bs.modal')
EventHandler.off(this._element, 'hide.bs.modal')
EventHandler.off(window, 'popstate', this._pop)
if (this._modal) {
this._modal.dispose()
}
}
}

View File

@ -27,12 +27,14 @@ public class DialogTest : DialogTestBase
FooterTemplate = builder => builder.AddContent(0, "Test-FooterTemplate"),
Class = "test-class",
ShowMaximizeButton = true,
IsBackdrop = false,
OnCloseAsync = () =>
{
closed = true;
return Task.CompletedTask;
}
}));
Assert.Contains("data-bs-backdrop=\"static\"", cut.Markup);
// 全屏按钮
Assert.Contains("btn-maximize", cut.Markup);
@ -45,16 +47,22 @@ public class DialogTest : DialogTestBase
// 测试关闭逻辑
var modal = cut.FindComponent<Modal>();
cut.InvokeAsync(() => modal.Instance.Close());
cut.InvokeAsync(async () =>
{
await modal.Instance.Close();
await modal.Instance.CloseCallback();
});
Assert.True(closed);
// 测试 HeaderToolbarTemplate
cut.InvokeAsync(() => dialog.Show(new DialogOption()
{
IsBackdrop = true,
HeaderToolbarTemplate = builder => builder.AddContent(0, "Test-HeaderToolbarTemplate"),
}));
Assert.DoesNotContain("data-bs-backdrop", cut.FindComponent<Modal>().Markup);
Assert.Contains("Test-HeaderToolbarTemplate", cut.Markup);
cut.InvokeAsync(() => modal.Instance.Close());
cut.InvokeAsync(() => modal.Instance.CloseCallback());
// 测试 Component 赋值逻辑
cut.InvokeAsync(() => dialog.Show(new DialogOption()
@ -64,7 +72,7 @@ public class DialogTest : DialogTestBase
}));
Assert.Contains("class=\"btn btn-primary\"", cut.Markup);
modal = cut.FindComponent<Modal>();
cut.InvokeAsync(() => modal.Instance.Close());
cut.InvokeAsync(() => modal.Instance.CloseCallback());
// 测试 Component 与 BodyTemplate 均为 null 逻辑
cut.InvokeAsync(() => dialog.Show(new DialogOption()
@ -72,7 +80,7 @@ public class DialogTest : DialogTestBase
Component = null,
BodyTemplate = null
}));
cut.InvokeAsync(() => modal.Instance.Close());
cut.InvokeAsync(() => modal.Instance.CloseCallback());
#endregion
#region ShownCallbackAsync
@ -80,7 +88,7 @@ public class DialogTest : DialogTestBase
var option1 = new DialogOption
{
BodyTemplate = builder => builder.AddContent(0, "Test-BodyTemplate"),
ShownCallbackAsync = () =>
OnShownAsync = () =>
{
shown = true;
return Task.CompletedTask;
@ -88,10 +96,9 @@ public class DialogTest : DialogTestBase
};
cut.InvokeAsync(() => dialog.Show(option1));
modal = cut.FindComponent<Modal>();
cut.InvokeAsync(() => modal.Instance.ShownCallbackAsync!.Invoke());
cut.InvokeAsync(() => modal.Instance.ShownCallback());
Assert.True(shown);
var button = cut.FindComponents<Button>().First(b => b.Instance.Text == "关闭");
cut.InvokeAsync(() => button.Instance.OnClickWithoutRender!.Invoke());
cut.InvokeAsync(() => modal.Instance.CloseCallback());
#endregion
#region ShowSearchDialog
@ -112,13 +119,15 @@ public class DialogTest : DialogTestBase
cut.InvokeAsync(() => dialog.ShowSearchDialog(option));
// 重置按钮委托为空 null
button = cut.FindComponents<Button>().First(b => b.Instance.Text == "重置");
var button = cut.FindComponents<Button>().First(b => b.Instance.Text == "重置");
cut.InvokeAsync(() => button.Instance.OnClickWithoutRender!.Invoke());
cut.InvokeAsync(() => modal.Instance.CloseCallback());
// 搜索按钮委托为空
cut.InvokeAsync(() => dialog.ShowSearchDialog(option));
button = cut.FindComponents<Button>().First(b => b.Instance.Text == "查询");
cut.InvokeAsync(() => button.Instance.OnClickWithoutRender!.Invoke());
cut.InvokeAsync(() => modal.Instance.CloseCallback());
// 重置按钮
var reset = false;
@ -130,6 +139,7 @@ public class DialogTest : DialogTestBase
cut.InvokeAsync(() => dialog.ShowSearchDialog(option));
button = cut.FindComponents<Button>().First(b => b.Instance.Text == "重置");
cut.InvokeAsync(() => button.Instance.OnClickWithoutRender!.Invoke());
cut.InvokeAsync(() => modal.Instance.CloseCallback());
Assert.True(reset);
// 搜索按钮
@ -143,6 +153,7 @@ public class DialogTest : DialogTestBase
cut.InvokeAsync(() => dialog.ShowSearchDialog(option));
button = cut.FindComponents<Button>().First(b => b.Instance.Text == "查询");
cut.InvokeAsync(() => button.Instance.OnClickWithoutRender!.Invoke());
cut.InvokeAsync(() => modal.Instance.CloseCallback());
Assert.True(search);
#endregion
@ -158,7 +169,7 @@ public class DialogTest : DialogTestBase
ShowLabel = true
};
cut.InvokeAsync(() => dialog.ShowEditDialog(editOption));
cut.InvokeAsync(() => modal.Instance.Close());
cut.InvokeAsync(() => modal.Instance.CloseCallback());
// 设置关闭回调
closed = false;
@ -168,8 +179,10 @@ public class DialogTest : DialogTestBase
return Task.CompletedTask;
};
cut.InvokeAsync(() => dialog.ShowEditDialog(editOption));
// 点击关闭按钮
button = cut.FindComponents<Button>().First(b => b.Instance.Text == "关闭");
cut.InvokeAsync(() => button.Instance.OnClickWithoutRender!.Invoke());
cut.InvokeAsync(() => modal.Instance.CloseCallback());
Assert.True(closed);
// 设置保存回调
@ -198,23 +211,53 @@ public class DialogTest : DialogTestBase
var form = cut.Find("form");
form.Submit();
Assert.True(saved);
cut.InvokeAsync(() => modal.Instance.CloseCallback());
// edit dialog is tracking true
editOption.IsTracking = true;
cut.InvokeAsync(() => dialog.ShowEditDialog(editOption));
button = cut.FindComponents<Button>().FirstOrDefault(b => b.Instance.Text == "关闭");
Assert.Null(button);
cut.InvokeAsync(() => modal.Instance.CloseCallback());
// edit dialog is tracking false
editOption.IsTracking = false;
cut.InvokeAsync(() => dialog.ShowEditDialog(editOption));
button = cut.FindComponents<Button>().FirstOrDefault(b => b.Instance.Text == "关闭");
Assert.NotNull(button);
cut.InvokeAsync(() => modal.Instance.CloseCallback());
// Edit Dialog FooterTemplate
editOption.DialogFooterTemplate = modal => builder => builder.AddContent(0, "footer-template");
cut.InvokeAsync(() => dialog.ShowEditDialog(editOption));
cut.Contains("footer-template");
cut.InvokeAsync(() => modal.Instance.CloseCallback());
// body template is not null
editOption.DialogBodyTemplate = modal => builder => builder.AddContent(0, "body-template");
cut.InvokeAsync(() => dialog.ShowEditDialog(editOption));
cut.Contains("body-template");
cut.Contains("footer-template");
cut.InvokeAsync(() => modal.Instance.CloseCallback());
// 测试 DialogBodyTemplate
editOption.DialogBodyTemplate = foo => builder => builder.AddContent(0, "test");
cut.InvokeAsync(() => dialog.ShowEditDialog(editOption));
form.Submit();
cut.InvokeAsync(() => modal.Instance.CloseCallback());
// DisableAutoSubmitFormByEnter
editOption.DisableAutoSubmitFormByEnter = true;
cut.InvokeAsync(() => dialog.ShowEditDialog(editOption));
cut.Contains("data-bb-dissubmit=\"true\"");
form.Submit();
cut.InvokeAsync(() => modal.Instance.CloseCallback());
// Modal is Null
editOption.Model = null;
Assert.ThrowsAsync<InvalidOperationException>(() => cut.InvokeAsync(() => dialog.ShowEditDialog(editOption)));
cut.InvokeAsync(() => cut.Find(".btn-close").Click());
cut.InvokeAsync(() => modal.Instance.CloseCallback());
#endregion
#region ShowModal
@ -244,6 +287,7 @@ public class DialogTest : DialogTestBase
cut.InvokeAsync(() => dialog.ShowModal<MockModalDialog>(resultOption));
button = cut.FindComponents<Button>().First(b => b.Instance.Text == "Test-Yes");
cut.InvokeAsync(() => button.Instance.OnClick.InvokeAsync());
cut.InvokeAsync(() => modal.Instance.CloseCallback());
Assert.True(result);
// 点击的是 No 按钮
@ -259,6 +303,7 @@ public class DialogTest : DialogTestBase
cut.InvokeAsync(() => dialog.ShowModal<MockModalDialog>(resultOption));
button = cut.FindComponents<Button>().First(b => b.Instance.Text == "取消");
cut.InvokeAsync(() => button.Instance.OnClick.InvokeAsync());
cut.InvokeAsync(() => modal.Instance.CloseCallback());
Assert.False(result);
// 点击关闭按钮
@ -275,10 +320,12 @@ public class DialogTest : DialogTestBase
cut.InvokeAsync(() => dialog.ShowModal<MockModalDialog>(resultOption));
button = cut.FindComponents<Button>().First(b => b.Instance.Text == "关闭");
cut.InvokeAsync(() => button.Instance.OnClick.InvokeAsync());
cut.InvokeAsync(() => modal.Instance.CloseCallback());
cut.InvokeAsync(() => dialog.ShowModal<MockModalDialogClosingFalse>(resultOption));
button = cut.FindComponents<Button>().First(b => b.Instance.Text == "关闭");
cut.InvokeAsync(() => button.Instance.OnClick.InvokeAsync());
cut.InvokeAsync(() => modal.Instance.CloseCallback());
#endregion
#region
@ -308,13 +355,11 @@ public class DialogTest : DialogTestBase
Assert.Equal(2, cut.FindComponents<ModalDialog>().Count);
// 关闭第二个弹窗
var btnClose = cut.FindAll(".btn-close")[cut.FindAll(".btn-close").Count - 1];
btnClose.Click();
cut.InvokeAsync(() => modal.Instance.CloseCallback());
Assert.Equal(1, cut.FindComponents<ModalDialog>().Count);
// 关闭第一个弹窗
btnClose = cut.FindAll(".btn-close")[cut.FindAll(".btn-close").Count - 1];
btnClose.Click();
cut.InvokeAsync(() => modal.Instance.CloseCallback());
Assert.Equal(0, cut.FindComponents<ModalDialog>().Count);
#endregion
@ -324,8 +369,7 @@ public class DialogTest : DialogTestBase
FullScreenSize = FullScreenSize.Large
}));
Assert.Contains("modal-fullscreen-lg-down", cut.Markup);
btnClose = cut.FindAll(".btn-close")[cut.FindAll(".btn-close").Count - 1];
btnClose.Click();
cut.InvokeAsync(() => modal.Instance.CloseCallback());
#endregion
#region IsCenter
@ -334,16 +378,14 @@ public class DialogTest : DialogTestBase
IsCentered = true
}));
Assert.Contains("modal-dialog-centered", cut.Markup);
btnClose = cut.FindAll(".btn-close")[cut.FindAll(".btn-close").Count - 1];
btnClose.Click();
cut.InvokeAsync(() => modal.Instance.CloseCallback());
cut.InvokeAsync(() => dialog.Show(new DialogOption()
{
IsCentered = false
}));
Assert.DoesNotContain("modal-dialog-centered", cut.Markup);
btnClose = cut.FindAll(".btn-close")[cut.FindAll(".btn-close").Count - 1];
btnClose.Click();
cut.InvokeAsync(() => modal.Instance.CloseCallback());
#endregion
#region IsKeyboard
@ -352,16 +394,14 @@ public class DialogTest : DialogTestBase
IsKeyboard = true
}));
Assert.Contains("data-bs-keyboard=\"true\"", cut.Markup);
btnClose = cut.FindAll(".btn-close")[cut.FindAll(".btn-close").Count - 1];
btnClose.Click();
cut.InvokeAsync(() => modal.Instance.CloseCallback());
cut.InvokeAsync(() => dialog.Show(new DialogOption()
{
IsKeyboard = false
}));
Assert.DoesNotContain("data-bs-keyboard\"false\"", cut.Markup);
btnClose = cut.FindAll(".btn-close")[cut.FindAll(".btn-close").Count - 1];
btnClose.Click();
cut.InvokeAsync(() => modal.Instance.CloseCallback());
#endregion
#region ShowHeaderCloseButton
@ -369,16 +409,14 @@ public class DialogTest : DialogTestBase
{
ShowHeaderCloseButton = true
}));
btnClose = cut.FindAll(".btn-close")[cut.FindAll(".btn-close").Count - 1];
btnClose.Click();
cut.InvokeAsync(() => modal.Instance.CloseCallback());
cut.InvokeAsync(() => dialog.Show(new DialogOption()
{
ShowHeaderCloseButton = false
}));
Assert.DoesNotContain("btn-close", cut.Markup);
btnClose = cut.FindAll(".btn-secondary")[cut.FindAll(".btn-secondary").Count - 1];
btnClose.Click();
cut.InvokeAsync(() => modal.Instance.CloseCallback());
#endregion
#region ShowPrintButton
@ -387,16 +425,14 @@ public class DialogTest : DialogTestBase
ShowPrintButton = true
}));
Assert.Contains("btn-print", cut.Markup);
btnClose = cut.FindAll(".btn-close")[cut.FindAll(".btn-close").Count - 1];
btnClose.Click();
cut.InvokeAsync(() => modal.Instance.CloseCallback());
cut.InvokeAsync(() => dialog.Show(new DialogOption()
{
ShowPrintButton = false
}));
Assert.DoesNotContain("btn-print", cut.Markup);
btnClose = cut.FindAll(".btn-close")[cut.FindAll(".btn-close").Count - 1];
btnClose.Click();
cut.InvokeAsync(() => modal.Instance.CloseCallback());
cut.InvokeAsync(() => dialog.Show(new DialogOption()
{
@ -406,8 +442,7 @@ public class DialogTest : DialogTestBase
}));
Assert.Contains("btn-print", cut.Markup);
Assert.Contains("Print-Test", cut.Markup);
btnClose = cut.FindAll(".btn-close")[cut.FindAll(".btn-close").Count - 1];
btnClose.Click();
cut.InvokeAsync(() => modal.Instance.CloseCallback());
#endregion
#region ShowSaveButton
@ -420,8 +455,7 @@ public class DialogTest : DialogTestBase
}));
Assert.Contains("Save-Test", cut.Markup);
Assert.Contains("Close-Test", cut.Markup);
btnClose = cut.FindAll(".btn-close")[cut.FindAll(".btn-close").Count - 1];
btnClose.Click();
cut.InvokeAsync(() => modal.Instance.CloseCallback());
#endregion
#region OnSaveAsync
@ -437,43 +471,45 @@ public class DialogTest : DialogTestBase
return Task.FromResult(save);
}
}));
btnClose = cut.FindAll(".btn-primary")[cut.FindAll(".btn-primary").Count - 1];
btnClose.Click();
var btnClose = cut.FindComponents<Button>().First(i => i.Instance.Icon == "fa-solid fa-fw fa-floppy-disk");
cut.InvokeAsync(() => btnClose.Instance.OnClickWithoutRender!.Invoke());
cut.InvokeAsync(() => modal.Instance.CloseCallback());
Assert.True(save);
#endregion
#region ShowSaveDialog
cut.InvokeAsync(() => dialog.ShowSaveDialog<MockDialogTest>("Title", () => Task.FromResult(true), p => { }, op => op.Class = "test"));
modal.FindAll("button")[modal.FindAll("button").Count - 1].Click();
cut.InvokeAsync(() => dialog.ShowSaveDialog<MockDialogTest>("Title"));
modal.FindAll("button")[modal.FindAll("button").Count - 1].Click();
cut.InvokeAsync(() => modal.Instance.CloseCallback());
#endregion
#region ShowValidateFormDialog
cut.InvokeAsync(() => dialog.ShowValidateFormDialog<MockValidateFormDialog>("ValidateFormDialog"));
var btn = cut.Find(".btn-close");
cut.InvokeAsync(() => btn.Click());
cut.InvokeAsync(() => modal.Instance.CloseCallback());
Dictionary<string, object?> parameterFactory(DialogOption op) => new();
void ConfigureOption(DialogOption op) => op.Class = "ValidateFormDialog-Class";
cut.InvokeAsync(() => dialog.ShowValidateFormDialog<MockValidateFormDialog>("ValidateFormDialog", parameterFactory, ConfigureOption));
btn = cut.Find(".btn-close");
cut.InvokeAsync(() => btn.Click());
cut.InvokeAsync(() => modal.Instance.CloseCallback());
#endregion
#region ShowCloseDialog
cut.InvokeAsync(() => dialog.ShowCloseDialog<MockValidateFormDialog>("CloseDialog", null, ConfigureOption));
btn = cut.Find(".btn-close");
cut.InvokeAsync(() => btn.Click());
cut.InvokeAsync(() => modal.Instance.CloseCallback());
cut.InvokeAsync(() => dialog.ShowCloseDialog<MockValidateFormDialog>("CloseDialog"));
btn = cut.Find(".btn-close");
cut.InvokeAsync(() => btn.Click());
cut.InvokeAsync(() => modal.Instance.CloseCallback());
cut.InvokeAsync(() => dialog.ShowCloseDialog<MockValidateFormDialog>("CloseDialog", parameter =>
{
parameter.Add("Class", "test");
}));
cut.InvokeAsync(() => modal.Instance.CloseCallback());
#endregion
}
private class MockValidateFormDialog : ComponentBase
{
[Parameter]
public string? Class { get; set; }
}
private class MockDialogTest : ComponentBase

View File

@ -77,6 +77,16 @@ public class ModalTest : BootstrapBlazorTestBase
Assert.Equal("Test-Header", header.TextContent);
}
[Fact]
public void SetHeaderText_Null()
{
var cut = Context.RenderComponent<MockModal>(pb =>
{
pb.AddChildContent<ModalDialog>();
});
cut.Instance.TestSetHeaderText();
}
[Fact]
public async Task ShownCallbackAsync_Ok()
{
@ -94,7 +104,7 @@ public class ModalTest : BootstrapBlazorTestBase
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
builder.OpenComponent<MockModal>(0);
builder.AddAttribute(1, nameof(Modal.ShownCallbackAsync), () =>
builder.AddAttribute(1, nameof(Modal.OnShownAsync), () =>
{
Value = true;
return Task.CompletedTask;
@ -105,13 +115,15 @@ public class ModalTest : BootstrapBlazorTestBase
private class MockModal : Modal
{
/// <summary>
///
/// </summary>
/// <returns></returns>
public async Task Show_Test()
{
await base.Shown();
await base.ShownCallback();
}
public void TestSetHeaderText()
{
Dialogs.Clear();
base.SetHeaderText("");
}
}
}

View File

@ -7,7 +7,7 @@ namespace UnitTest.Components;
public class SwalTest : SwalTestBase
{
[Fact]
public async Task Show_Ok()
public void Show_Ok()
{
var cut = Context.RenderComponent<BootstrapBlazorRoot>(pb =>
{
@ -16,165 +16,161 @@ public class SwalTest : SwalTestBase
var swal = cut.FindComponent<MockSwalTest>().Instance.SwalService;
await cut.InvokeAsync(async () => await swal.Show(new SwalOption()
cut.InvokeAsync(() => swal.Show(new SwalOption()
{
BodyTemplate = builder => builder.AddContent(0, "Test-BodyTemplate"),
FooterTemplate = builder => builder.AddContent(0, "Test-FooterTemplate"),
ButtonTemplate = builder => builder.AddContent(0, "Test-ButtonTemplate"),
ShowFooter = true,
ShowClose = true,
BodyContext = null
CloseButtonIcon = "test-close-icon",
CloseButtonText = "test-button-text-Cancel"
}));
// 代码覆盖模板单元测试
Assert.Contains("Test-BodyTemplate", cut.Markup);
Assert.Contains("Test-FooterTemplate", cut.Markup);
Assert.Contains("Test-ButtonTemplate", cut.Markup);
Assert.Contains("test-close-icon", cut.Markup);
Assert.Contains("test-button-text-Cancel", cut.Markup);
// 测试关闭逻辑
var modal = cut.FindComponent<Modal>();
await cut.InvokeAsync(() => modal.Instance.Close());
var modals = cut.FindComponents<Modal>();
var modal = modals[modals.Count - 1];
cut.InvokeAsync(() => modal.Instance.CloseCallback());
// 测试 Category
await cut.InvokeAsync(() => swal.Show(new SwalOption()
cut.InvokeAsync(() => swal.Show(new SwalOption()
{
Content = "I am Eror",
Category = SwalCategory.Error
}));
Assert.Contains("swal2-x-mark-line-left", cut.Markup);
cut.InvokeAsync(() => modal.Instance.CloseCallback());
modal = cut.FindComponent<Modal>();
await cut.InvokeAsync(() => modal.Instance.Close());
// 测试 Category
cut.InvokeAsync(() => swal.Show(new SwalOption()
{
Content = "I am Eror",
Category = SwalCategory.Information
}));
Assert.Contains("swal2-info", cut.Markup);
cut.InvokeAsync(() => modal.Instance.CloseCallback());
//测试Content
await cut.InvokeAsync(() => swal.Show(new SwalOption()
// 测试 Category
cut.InvokeAsync(() => swal.Show(new SwalOption()
{
Content = "I am Eror",
Category = SwalCategory.Warning
}));
Assert.Contains("swal2-warning", cut.Markup);
cut.InvokeAsync(() => modal.Instance.CloseCallback());
// 测试 Category
cut.InvokeAsync(() => swal.Show(new SwalOption()
{
Content = "I am Eror",
Category = SwalCategory.Question
}));
Assert.Contains("swal2-question", cut.Markup);
cut.InvokeAsync(() => modal.Instance.CloseCallback());
//测试 Content
cut.InvokeAsync(() => swal.Show(new SwalOption()
{
Content = "I am Swal",
}));
Assert.Contains("I am Swal", cut.Markup);
cut.InvokeAsync(() => modal.Instance.CloseCallback());
modal = cut.FindComponent<Modal>();
await cut.InvokeAsync(() => modal.Instance.Close());
//测试Title
await cut.InvokeAsync(() => swal.Show(new SwalOption()
//测试 Title
cut.InvokeAsync(() => swal.Show(new SwalOption()
{
Content = "I am Title",
Title = "I am Title",
}));
Assert.Contains("I am Title", cut.Markup);
cut.InvokeAsync(() => modal.Instance.CloseCallback());
modal = cut.FindComponent<Modal>();
await cut.InvokeAsync(() => modal.Instance.Close());
//测试Title
await cut.InvokeAsync(() => swal.Show(new SwalOption()
//测试 Title
cut.InvokeAsync(() => swal.Show(new SwalOption()
{
ForceDelay = true,
Delay = 1000
Title = "I am Title",
Content = "I am Swal",
}));
modal = cut.FindComponent<Modal>();
await cut.InvokeAsync(() => modal.Instance.Close());
//测试Title
await cut.InvokeAsync(() => swal.Show(new SwalOption()
{
ForceDelay = true,
Delay = 1000,
}));
modal = cut.FindComponent<Modal>();
await cut.InvokeAsync(() => modal.Instance.Close());
Assert.Contains("I am Title", cut.Markup);
cut.InvokeAsync(() => modal.Instance.CloseCallback());
//测试关闭按钮
await cut.InvokeAsync(() => swal.Show(new SwalOption()
cut.InvokeAsync(() => swal.Show(new SwalOption()
{
Content = "I am Swal",
IsAutoHide = true,
Delay = 1000
}));
var button = cut.Find(".btn-secondary");
button.Click();
cut.InvokeAsync(() => button.Click());
cut.InvokeAsync(() => modal.Instance.CloseCallback());
//测试Modal取消
var cancel = true;
_ = Task.Run(() =>
// auto close
cut.InvokeAsync(() => swal.Show(new SwalOption()
{
cut.InvokeAsync(async () => cancel = await swal.ShowModal(new SwalOption()
{
Content = "I am Swal"
}));
});
await Task.Delay(100);
var cancelbutton = cut.Find(".btn-secondary");
cancelbutton.Click();
Assert.False(cancel);
//测试Modal确认
var confirm = false;
_ = Task.Run(() => cut.InvokeAsync(async () =>
{
confirm = await swal.ShowModal(new SwalOption()
{
Content = "I am Swal"
});
}));
await Task.Delay(100);
var confirmbutton = cut.Find(".btn-danger");
confirmbutton.Click();
Assert.True(confirm);
cut.SetParametersAndRender(pb =>
{
pb.AddChildContent<Select<string>>(pb =>
{
pb.Add(a => a.OnBeforeSelectedItemChange, item => Task.FromResult(true));
pb.Add(a => a.SwalFooter, "Test-Swal-Footer");
pb.Add(a => a.SwalCategory, SwalCategory.Question);
pb.Add(a => a.SwalTitle, "Test-Swal-Title");
pb.Add(a => a.SwalContent, "Test-Swal-Content");
pb.Add(a => a.Items, new SelectedItem[]
{
new SelectedItem("1", "Test1"),
new SelectedItem("2", "Test2")
});
});
});
await cut.InvokeAsync(() => cut.Find(".dropdown-item").Click());
Assert.Contains("Test-Swal-Title", cut.Markup);
Assert.Contains("Test-Swal-Content", cut.Markup);
Assert.Contains("Test-Swal-Footer", cut.Markup);
await cut.InvokeAsync(() => cut.Find(".swal2-actions button").Click());
Assert.DoesNotContain("Test-Swal-Content", cut.Markup);
// 测试自动关闭
await cut.InvokeAsync(() => swal.Show(new SwalOption()
{
Content = "I am Swal",
Content = "I am auto hide",
IsAutoHide = true,
Delay = 100
}));
while (cut.Markup.Contains("I am Swal"))
{
await Task.Delay(100);
}
Thread.Sleep(150);
// 弹窗显示
cut.Contains("I am auto hide");
Thread.Sleep(150);
// 模拟关闭
cut.InvokeAsync(() => modal.Instance.CloseCallback());
cut.DoesNotContain("I am auto hide");
// 不关闭弹窗测试 Dispose
await cut.InvokeAsync(() => swal.Show(new SwalOption()
// 模态框
bool result = false;
Task.Run(async () => await cut.InvokeAsync(async () =>
{
Content = "I am Swal",
IsAutoHide = true,
Delay = 1000
result = await swal.ShowModal(new SwalOption()
{
Content = "I am Modal Swal",
CancelButtonText = "test-cancel-text",
ConfirmButtonIcon = "test-confirm-icon",
ConfirmButtonText = "test-confirm-text"
});
}));
}
var tick = DateTime.Now;
while (!cut.Markup.Contains("test-cancel-text"))
{
Thread.Sleep(100);
if (DateTime.Now > tick.AddSeconds(1))
{
break;
}
}
cut.Contains("test-cancel-text");
cut.Contains("I am Modal Swal");
cut.Contains("test-confirm-icon");
cut.Contains("test-confirm-text");
// 触发确认按钮
button = cut.Find(".btn-danger");
cut.InvokeAsync(() => button.Click());
cut.InvokeAsync(() => modal.Instance.CloseCallback());
// 自动隐藏时间未到时触发 Disposing
cut.InvokeAsync(() => swal.Show(new SwalOption()
{
Content = "I am auto hide",
IsAutoHide = true,
Delay = 4000
}));
Thread.Sleep(150);
// 弹窗显示
cut.Contains("I am auto hide");
var alert = cut.FindComponent<SweetAlert>();
alert.Dispose();
}
private class MockSwalTest : ComponentBase
{