Skip to content

Commit 27474e0

Browse files
authored
feat(OtpInput): add OtpInput component (#5902)
* feat: 增加 OtpInput 组件 * refactor: 增加样式文件扩展名 * refactor: 增加 Readonly 功能 * feat: 增加禁用只读功能 * feat: 增加 MaxLength 属性 * refactor: 增加脚本 * doc: 增加路由 * doc: 增加源码映射文件 * feat: 增加 OtpInput 示例 * refactor: 增加禁用样式 * doc: 更新示例 * feat: 增加 ChildContent 模板 * refactor: 增加占位符 * doc: 更新示例 * refactor: 样式变量化 * refactor: 增加脚本 * feat: 增加数字类型仅录入一个约束 * refactor: 重构组件结构 * test: 增加单元测试 * refactor: 重构组件提高性能 * test: 更新单元测试 * refactor: 增加样式 * refactor: 增加过滤条件 * refactor: 增加同步逻辑 * refactor: 支持拷贝粘贴 * refactor: 更新 selector 值 * feat: 支持粘贴自动填充功能 * feat: 增加数据双向绑定支持 * refactor: 复用代码 * chore: 更新字典 * test: 更新单元测试 * refactor: 支持表单 * doc: 增加表单示例 * test: 更新示例 * doc: 增加本地化 * doc: 增加 IsNew 标志 * refactor: 重构代码
1 parent 6ca6d5a commit 27474e0

File tree

16 files changed

+545
-4
lines changed

16 files changed

+545
-4
lines changed

exclusion.dic

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,3 +109,5 @@ Pharmacode
109109
bluetooth
110110
iframe
111111
Sqlite
112+
maxlength
113+
inputmode
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
@page "/otp-input"
2+
@inject IStringLocalizer<OtpInputs> Localizer
3+
4+
<HeadContent>
5+
<style>
6+
.input-group-text {
7+
--bb-input-group-label-width: 100px;
8+
}
9+
10+
.otp-input-demo {
11+
text-align: center;
12+
}
13+
</style>
14+
</HeadContent>
15+
16+
<h3>@Localizer["OtpInputsTitle"]</h3>
17+
18+
<h4>@Localizer["OtpInputsDescription"]</h4>
19+
20+
<Tips class="mt-3">
21+
<p>@((MarkupString)Localizer["OtpInputsTips"].Value)</p>
22+
</Tips>
23+
24+
<DemoBlock Title="@Localizer["OtpInputsNormalTitle"]"
25+
Introduction="@Localizer["OtpInputsNormalIntro"]"
26+
Name="Normal">
27+
<div class="row g-3 mb-3">
28+
<div class="col-12 col-sm-6">
29+
<BootstrapInputGroup>
30+
<BootstrapInputGroupLabel DisplayText="Type"></BootstrapInputGroupLabel>
31+
<Select @bind-Value="_otpInputType"></Select>
32+
</BootstrapInputGroup>
33+
</div>
34+
<div class="col-12 col-sm-6">
35+
<BootstrapInputGroup>
36+
<BootstrapInputGroupLabel DisplayText="PlaceHolder"></BootstrapInputGroupLabel>
37+
<BootstrapInput @bind-Value="@_placeHolder"></BootstrapInput>
38+
</BootstrapInputGroup>
39+
</div>
40+
<div class="col-12 col-sm-6">
41+
<BootstrapInputGroup>
42+
<BootstrapInputGroupLabel DisplayText="Readonly"></BootstrapInputGroupLabel>
43+
<Switch @bind-Value="@_readonly"></Switch>
44+
</BootstrapInputGroup>
45+
</div>
46+
<div class="col-12 col-sm-6">
47+
<BootstrapInputGroup>
48+
<BootstrapInputGroupLabel DisplayText="Disable"></BootstrapInputGroupLabel>
49+
<Switch @bind-Value="@_disabled"></Switch>
50+
</BootstrapInputGroup>
51+
</div>
52+
</div>
53+
<OtpInput Value="@_value" Type="_otpInputType" PlaceHolder="@_placeHolder" IsReadonly="_readonly" IsDisabled="_disabled"></OtpInput>
54+
</DemoBlock>
55+
56+
<DemoBlock Title="@Localizer["OtpInputsValidateFormTitle"]"
57+
Introduction="@Localizer["OtpInputsValidateFormIntro"]"
58+
Name="ValidateForm">
59+
<ValidateForm Model="@_model">
60+
<div class="row g-3">
61+
<div class="col-12">
62+
<BootstrapInput @bind-Value="@_model.UserName" />
63+
</div>
64+
<div class="col-12">
65+
<GroupBox Title="2FA" class="otp-input-demo">
66+
<OtpInput @bind-Value="@_model.Password"></OtpInput>
67+
</GroupBox>
68+
</div>
69+
<div class="col-12">
70+
<Button ButtonType="ButtonType.Submit" Icon="fa-fw fa-solid fa-save" Text="Submit"></Button>
71+
</div>
72+
</div>
73+
</ValidateForm>
74+
</DemoBlock>
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the Apache 2.0 License
3+
// See the LICENSE file in the project root for more information.
4+
// Maintainer: Argo Zhang([email protected]) Website: https://www.blazor.zone
5+
6+
namespace BootstrapBlazor.Server.Components.Samples;
7+
8+
/// <summary>
9+
/// OtpInputs samples
10+
/// </summary>
11+
public partial class OtpInputs
12+
{
13+
private string _value = "818924";
14+
15+
private OtpInputType _otpInputType;
16+
17+
private string _placeHolder = "X";
18+
19+
private bool _readonly = false;
20+
21+
private bool _disabled = false;
22+
23+
private LoginModel _model = new() { UserName = "Admin", Password = "" };
24+
}

src/BootstrapBlazor.Server/Components/Samples/ValidateForms.razor

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,8 @@
6868
<div class="col-12 col-sm-6">
6969
<BootstrapInputGroupLabel @bind-Value="@Model2.Name" />
7070
<BootstrapInputGroup>
71-
<Display @bind-Value="@Model2.Name"></Display>
7271
<BootstrapInputGroupLabel DisplayText="Test" />
72+
<Display @bind-Value="@Model2.Name"></Display>
7373
</BootstrapInputGroup>
7474
</div>
7575
<div class="col-12 col-sm-6">

src/BootstrapBlazor.Server/Extensions/MenusLocalizerExtensions.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,12 @@ void AddForm(DemoMenuItem item)
409409
Url = "multi-select"
410410
},
411411
new()
412+
{
413+
IsNew = true,
414+
Text = Localizer["OtpInput"],
415+
Url = "otp-input"
416+
},
417+
new()
412418
{
413419
Text = Localizer["OnScreenKeyboard"],
414420
Url = "onscreen-keyboard"

src/BootstrapBlazor.Server/Locales/en-US.json

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4918,7 +4918,8 @@
49184918
"UniverIcon": "Univer Icons",
49194919
"Typed": "Typed",
49204920
"UniverSheet": "UniverSheet",
4921-
"ShieldBadge": "ShieldBadge"
4921+
"ShieldBadge": "ShieldBadge",
4922+
"OtpInput": "OtpInput"
49224923
},
49234924
"BootstrapBlazor.Server.Components.Samples.Table.TablesHeader": {
49244925
"TablesHeaderTitle": "Header grouping function",
@@ -7091,5 +7092,14 @@
70917092
"BootstrapBlazor.Server.Components.Samples.Tutorials.OnlineSheet": {
70927093
"ToastOnReadyTitle": "Collaboration Notification",
70937094
"ToastOnReadyContent": "After 4 seconds the table is updated by other writers to change the content"
7095+
},
7096+
"BootstrapBlazor.Server.Components.Samples.OtpInputs": {
7097+
"OtpInputsTitle": "OtpInput",
7098+
"OtpInputsDescription": "A secure verification password box based on OTP (One-Time Password) that is limited to one-time use and has a time limit",
7099+
"OtpInputsTips": "OTP (One Time Password, abbreviated as OTP): This is a security measure used to generate a unique password for each login or transaction. This component is used in conjunction with <code>ITotpService</code> <code>IHotpService</code> to greatly improve security.",
7100+
"OtpInputsNormalTitle": "Basic usage",
7101+
"OtpInputsNormalIntro": "Control the password type or length by setting parameters such as <code>Type</code>",
7102+
"OtpInputsValidateFormTitle": "ValidateForm",
7103+
"OtpInputsValidateFormIntro": "When the password is not provided, click the Submit button, the password box will be displayed in red and it will be prompted that it cannot be empty."
70947104
}
70957105
}

src/BootstrapBlazor.Server/Locales/zh-CN.json

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4918,7 +4918,8 @@
49184918
"UniverIcon": "Univer Icons",
49194919
"Typed": "打字机效果 Typed",
49204920
"UniverSheet": "表格组件 UniverSheet",
4921-
"ShieldBadge": "徽章组件 ShieldBadge"
4921+
"ShieldBadge": "徽章组件 ShieldBadge",
4922+
"OtpInput": "验证码输入框 OtpInput"
49224923
},
49234924
"BootstrapBlazor.Server.Components.Samples.Table.TablesHeader": {
49244925
"TablesHeaderTitle": "表头分组功能",
@@ -7091,5 +7092,14 @@
70917092
"BootstrapBlazor.Server.Components.Samples.Tutorials.OnlineSheet": {
70927093
"ToastOnReadyTitle": "在线表格协作通知",
70937094
"ToastOnReadyContent": "4 秒后表格更新其他写作人员更改内容"
7095+
},
7096+
"BootstrapBlazor.Server.Components.Samples.OtpInputs": {
7097+
"OtpInputsTitle": "OtpInput 密码框",
7098+
"OtpInputsDescription": "基于 OTP(One-Time Password‌) 仅限单次使用且具备时效性的安全验证密码框",
7099+
"OtpInputsTips": "OTP(One Time Password,简称OTP):这是一种安全措施,用于在每次登录或交易时生成一个唯一的密码,本组件配合 <code>ITotpService</code> <code>IHotpService</code> 使用大大提高安全性",
7100+
"OtpInputsNormalTitle": "基础用法",
7101+
"OtpInputsNormalIntro": "通过设置 <code>Type</code> 等参数控制密码类型或者长度",
7102+
"OtpInputsValidateFormTitle": "表单内使用",
7103+
"OtpInputsValidateFormIntro": "密码未提供值时,点击提交按钮,密码框显示为红色,并且提示不可为空"
70947104
}
70957105
}

src/BootstrapBlazor.Server/docs.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,8 @@
228228
"watermark": "Watermarks",
229229
"typed": "Typeds",
230230
"univer-sheet": "UniverSheets",
231-
"shield-badge": "ShieldBadges"
231+
"shield-badge": "ShieldBadges",
232+
"opt-input": "OtpInputs"
232233
},
233234
"video": {
234235
"table": "BV1ap4y1x7Qn?p=1",
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
@namespace BootstrapBlazor.Components
2+
@inherits ValidateBase<string>
3+
4+
<div @attributes="AdditionalAttributes" id="@Id" class="@ClassString">
5+
@for (var index = 0; index < Digits; index++)
6+
{
7+
@if(IsReadonly || IsDisabled)
8+
{
9+
<span class="@ItemClassString">
10+
@GetValueString(index)
11+
</span>
12+
}
13+
else
14+
{
15+
<input type="@TypeString" class="@InputClassString"
16+
maxlength="@MaxLengthString" inputmode="@TypeModeString" placeholder="@PlaceHolder"
17+
value="@GetValueString(index)" />
18+
}
19+
}
20+
</div>
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the Apache 2.0 License
3+
// See the LICENSE file in the project root for more information.
4+
// Maintainer: Argo Zhang([email protected]) Website: https://www.blazor.zone
5+
6+
namespace BootstrapBlazor.Components;
7+
8+
/// <summary>
9+
/// OTP input component
10+
/// </summary>
11+
[BootstrapModuleAutoLoader("Input/OtpInput.razor.js", JSObjectReference = true)]
12+
public partial class OtpInput
13+
{
14+
/// <summary>
15+
/// Gets or sets the length of the OTP input. Default is 6.
16+
/// </summary>
17+
[Parameter]
18+
public int Digits { get; set; } = 6;
19+
20+
/// <summary>
21+
/// Gets or sets whether the OTP input is readonly. Default is false.
22+
/// </summary>
23+
[Parameter]
24+
public bool IsReadonly { get; set; }
25+
26+
/// <summary>
27+
/// Gets or sets the value type of the OTP input. Default is <see cref="OtpInputType.Number"/>.
28+
/// </summary>
29+
[Parameter]
30+
public OtpInputType Type { get; set; }
31+
32+
/// <summary>
33+
/// Gets or sets the placeholder of the OTP input. Default is null.
34+
/// </summary>
35+
[Parameter]
36+
public string? PlaceHolder { get; set; }
37+
38+
private string? ClassString => CssBuilder.Default("bb-opt-input")
39+
.AddClassFromAttributes(AdditionalAttributes)
40+
.Build();
41+
42+
private string? ItemClassString => CssBuilder.Default("bb-opt-item")
43+
.AddClass("disabled", IsDisabled)
44+
.AddClass(ValidCss)
45+
.Build();
46+
47+
private string? InputClassString => CssBuilder.Default("bb-opt-item")
48+
.AddClass("input-number-fix", Type == OtpInputType.Number)
49+
.AddClass(ValidCss)
50+
.Build();
51+
52+
private string TypeString => Type switch
53+
{
54+
OtpInputType.Number => "number",
55+
OtpInputType.Password => "password",
56+
_ => "text"
57+
};
58+
59+
private string? MaxLengthString => Type switch
60+
{
61+
OtpInputType.Number => null,
62+
_ => "1"
63+
};
64+
65+
private string? TypeModeString => Type switch
66+
{
67+
OtpInputType.Number => "numeric",
68+
_ => null
69+
};
70+
71+
private char[] _values = [];
72+
73+
/// <summary>
74+
/// <inheritdoc/>
75+
/// </summary>
76+
protected override void OnParametersSet()
77+
{
78+
base.OnParametersSet();
79+
80+
Value ??= "";
81+
_values = new char[Digits];
82+
for (var index = 0; index < Digits; index++)
83+
{
84+
if (index < Value.Length)
85+
{
86+
_values[index] = Value[index];
87+
}
88+
}
89+
}
90+
91+
/// <summary>
92+
/// <inheritdoc/>
93+
/// </summary>
94+
/// <returns></returns>
95+
protected override Task InvokeInitAsync() => InvokeVoidAsync("init", Id, Interop, nameof(TriggerSetValue));
96+
97+
private char? GetValueString(int index)
98+
{
99+
return _values[index] != 0 ? _values[index] : null;
100+
}
101+
102+
103+
/// <summary>
104+
/// Trigger value changed event callback. Trigger by JavaScript.
105+
/// </summary>
106+
/// <param name="val"></param>
107+
/// <returns></returns>
108+
[JSInvokable]
109+
public Task TriggerSetValue(string val)
110+
{
111+
SetValue(val);
112+
return Task.CompletedTask;
113+
}
114+
}

0 commit comments

Comments
 (0)