You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Connected.Components/Components/Mask/Mask.razor.cs

439 lines
13 KiB

2 years ago
// Copyright (c) MudBlazor 2021
// MudBlazor licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Text.RegularExpressions;
using Connected.Annotations;
using Connected.Extensions;
using Connected.Services;
using Connected.Utilities;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
namespace Connected.Components;
public partial class Mask : InputBase<string>, IDisposable
{
public Mask()
{
TextUpdateSuppression = false;
}
protected string Classname =>
new CssBuilder("mud-input")
.AddClass($"mud-input-{Variant.ToDescriptionString()}")
.AddClass($"mud-input-adorned-{Adornment.ToDescriptionString()}", Adornment != Adornment.None)
.AddClass($"mud-input-margin-{Margin.ToDescriptionString()}", when: () => Margin != Margin.None)
.AddClass("mud-input-underline", when: () => DisableUnderLine == false && Variant != Variant.Outlined)
.AddClass("mud-shrink",
when: () => !string.IsNullOrEmpty(Text) || Adornment == Adornment.Start ||
!string.IsNullOrWhiteSpace(Placeholder))
.AddClass("mud-disabled", Disabled)
.AddClass("mud-input-error", HasErrors)
.AddClass("mud-ltr", GetInputType() == InputType.Email || GetInputType() == InputType.Telephone)
.AddClass(Class)
.Build();
protected string InputClassname =>
new CssBuilder("mud-input-slot")
.AddClass("mud-input-root")
.AddClass($"mud-input-root-{Variant.ToDescriptionString()}")
.AddClass($"mud-input-root-adorned-{Adornment.ToDescriptionString()}", Adornment != Adornment.None)
.AddClass($"mud-input-root-margin-{Margin.ToDescriptionString()}", when: () => Margin != Margin.None)
.AddClass(Class)
.Build();
protected string AdornmentClassname =>
new CssBuilder("mud-input-adornment")
.AddClass($"mud-input-adornment-{Adornment.ToDescriptionString()}", Adornment != Adornment.None)
.AddClass($"mud-text", !string.IsNullOrEmpty(AdornmentText))
.AddClass($"mud-input-root-filled-shrink", Variant == Variant.Filled)
.AddClass(Class)
.Build();
protected string ClearButtonClassname =>
new CssBuilder()
// .AddClass("me-n1", Adornment == Adornment.End && HideSpinButtons == false)
.AddClass("mud-icon-button-edge-end", Adornment == Adornment.End)
// .AddClass("me-6", Adornment != Adornment.End && HideSpinButtons == false)
.AddClass("mud-icon-button-edge-margin-end", Adornment != Adornment.End)
.Build();
private ElementReference _elementReference;
private ElementReference _elementReference1;
private IJsEvent _jsEvent;
private IKeyInterceptor _keyInterceptor;
[Inject] private IKeyInterceptorFactory _keyInterceptorFactory { get; set; }
[Inject] private IJsEventFactory _jsEventFactory { get; set; }
[Inject] private IJsApiService _jsApiService { get; set; }
private string _elementId = "mask_" + Guid.NewGuid().ToString().Substring(0, 8);
private IMask _mask = new PatternMask("** **-** **");
/// <summary>
/// ChildContent will only be displayed if InputType.Hidden and if its not null. Required for Select
/// </summary>
[Parameter]
[Category(CategoryTypes.General.Appearance)]
public RenderFragment ChildContent { get; set; }
/// <summary>
/// Provide a masking object. Built-in masks are PatternMask, MultiMask, RegexMask and BlockMask
/// </summary>
[Parameter]
[Category(CategoryTypes.General.Data)]
public IMask MaskKind
{
get => _mask;
set => SetMask(value);
}
/// <summary>
/// Type of the input element. It should be a valid HTML5 input type.
/// </summary>
[Parameter]
[Category(CategoryTypes.FormComponent.ListAppearance)]
public InputType InputType { get; set; } = InputType.Text;
/// <summary>
/// Show clear button.
/// </summary>
[Parameter]
[Category(CategoryTypes.FormComponent.ListBehavior)]
public bool Clearable { get; set; } = false;
private bool _showClearable;
private void UpdateClearable(object value)
{
var showClearable = Clearable && !string.IsNullOrWhiteSpace(Text);
if (_showClearable != showClearable)
_showClearable = showClearable;
}
/// <summary>
/// Button click event for clear button. Called after text and value has been cleared.
/// </summary>
[Parameter]
[Category(CategoryTypes.FormComponent.ListAppearance)]
public EventCallback<MouseEventArgs> OnClearButtonClick { get; set; }
/// <summary>
/// Custom clear icon.
/// </summary>
[Parameter]
[Category(CategoryTypes.General.Appearance)]
public string ClearIcon { get; set; } = Icons.Material.Filled.Clear;
protected override async Task OnInitializedAsync()
{
if (Text != MaskKind.Text)
await SetTextAsync(MaskKind.Text, updateValue: false);
await base.OnInitializedAsync();
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
_jsEvent = _jsEventFactory.Create();
await _jsEvent.Connect(_elementId,
new JsEventOptions
{
//EnableLogging = true,
TargetClass = "mud-input-slot",
TagName = "INPUT"
});
_jsEvent.CaretPositionChanged += OnCaretPositionChanged;
_jsEvent.Paste += OnPaste;
_jsEvent.Select += OnSelect;
_keyInterceptor = _keyInterceptorFactory.Create();
await _keyInterceptor.Connect(_elementId, new KeyInterceptorOptions()
{
//EnableLogging = true,
TargetClass = "mud-input-slot",
Keys =
{
new KeyOptions
{
Key = " ", PreventDown = "key+none"
}, //prevent scrolling page, toggle open/close
new KeyOptions { Key = "ArrowUp", PreventDown = "key+none" }, // prevent scrolling page
new KeyOptions { Key = "ArrowDown", PreventDown = "key+none" }, // prevent scrolling page
new KeyOptions { Key = "PageUp", PreventDown = "key+none" }, // prevent scrolling page
new KeyOptions { Key = "PageDown", PreventDown = "key+none" }, // prevent scrolling page
new KeyOptions { Key = @"/^.$/", PreventDown = "key+none|key+shift" },
new KeyOptions { Key = "/./", SubscribeDown = true },
new KeyOptions { Key = "Backspace", PreventDown = "key+none" },
new KeyOptions { Key = "Delete", PreventDown = "key+none" },
},
});
_keyInterceptor.KeyDown += HandleKeyDownInternally;
}
if (_isFocused && MaskKind.Selection == null)
SetCaretPosition(MaskKind.CaretPos, _selection, render: false);
await base.OnAfterRenderAsync(firstRender);
}
private async void HandleKeyDownInternally(KeyboardEventArgs args)
{
await HandleKeyDown(args);
}
protected internal async Task HandleKeyDown(KeyboardEventArgs e)
{
try
{
if ((e.CtrlKey && e.Key != "Backspace") || e.AltKey || ReadOnly)
return;
switch (e.Key)
{
case "Backspace":
if (e.CtrlKey)
{
MaskKind.Clear();
await Update();
return;
}
MaskKind.Backspace();
await Update();
return;
case "Delete":
MaskKind.Delete();
await Update();
return;
}
if (Regex.IsMatch(e.Key, @"^.$"))
{
MaskKind.Insert(e.Key);
await Update();
}
}
finally
{
// call user callback
await OnKeyDown.InvokeAsync(e);
}
}
private bool _updating;
private async Task Update()
{
var caret = MaskKind.CaretPos;
var selection = MaskKind.Selection;
var text = MaskKind.Text;
var cleanText = MaskKind.GetCleanText();
_updating = true;
try
{
await base.SetTextAsync(text, updateValue: false);
if (Clearable)
UpdateClearable(Text);
var v = Converter.Get(cleanText);
Value = v;
await ValueChanged.InvokeAsync(v);
SetCaretPosition(caret, selection);
}
finally
{
_updating = false;
}
}
internal async void HandleClearButton(MouseEventArgs e)
{
MaskKind.Clear();
await Update();
await _elementReference.FocusAsync();
await OnClearButtonClick.InvokeAsync(e);
}
protected override async Task UpdateTextPropertyAsync(bool updateValue)
{
// allow this only via changes from the outside
if (_updating)
return;
var text = Converter.Set(Value);
var cleanText = MaskKind.GetCleanText();
if (cleanText == text || string.IsNullOrEmpty(cleanText) && string.IsNullOrEmpty(text))
return;
var maskText = MaskKind.Text;
MaskKind.SetText(text);
if (maskText == MaskKind.Text)
return; // no change, stop update loop
await Update();
}
protected override async Task UpdateValuePropertyAsync(bool updateText)
{
// allow this only via changes from the outside
if (_updating)
return;
var text = Text;
if (MaskKind.Text == text)
return;
var maskText = MaskKind.Text;
MaskKind.SetText(text);
if (maskText == MaskKind.Text)
return; // no change, stop update loop
await Update();
}
internal override InputType GetInputType() => InputType;
private string GetCounterText() => Counter == null
? string.Empty
: (Counter == 0
? (string.IsNullOrEmpty(Text) ? "0" : $"{Text.Length}")
: ((string.IsNullOrEmpty(Text) ? "0" : $"{Text.Length}") + $" / {Counter}"));
/// <summary>
/// Clear the text field.
/// </summary>
/// <returns></returns>
public Task Clear()
{
MaskKind.Clear();
return Update();
}
public override ValueTask FocusAsync()
{
return _elementReference.FocusAsync();
}
public override ValueTask SelectAsync()
{
return _elementReference.SelectAsync();
}
public override ValueTask SelectRangeAsync(int pos1, int pos2)
{
return _elementReference.SelectRangeAsync(pos1, pos2);
}
internal void OnCopy()
{
var text = Text;
if (MaskKind.Selection != null)
{
(_, text, _) = BaseMask.SplitSelection(text, MaskKind.Selection.Value);
}
_jsApiService.CopyToClipboardAsync(text);
}
internal async void OnPaste(string text)
{
if (text == null || ReadOnly)
return;
MaskKind.Insert(text);
await Update();
}
public void OnSelect(int start, int end)
{
MaskKind.Selection = _selection = (start, end);
}
internal void OnFocused(FocusEventArgs obj)
{
_isFocused = true;
}
protected internal override void OnBlurred(FocusEventArgs obj)
{
base.OnBlurred(obj);
_isFocused = false;
}
private int _caret;
private (int, int)? _selection;
private void SetCaretPosition(int caret, (int, int)? selection = null, bool render = true)
{
if (!_isFocused)
return;
_caret = caret;
if (caret == 0)
_caret = 0;
_selection = selection;
if (selection == null)
{
_elementReference.SelectRangeAsync(caret, caret).AndForget();
}
else
{
var sel = selection.Value;
_elementReference.SelectRangeAsync(sel.Item1, sel.Item2).AndForget();
}
}
// from JS event
internal void OnCaretPositionChanged(int pos)
{
if (MaskKind.Selection != null)
{
// do not clear selection if pos change is at selection border
var sel = MaskKind.Selection.Value;
if (pos == sel.Item1 || pos == sel.Item2)
return;
}
if (pos == MaskKind.CaretPos)
return;
MaskKind.Selection = null;
MaskKind.CaretPos = pos;
}
private void SetMask(IMask other)
{
if (_mask == null || other == null || _mask?.GetType() != other?.GetType())
{
_mask = other;
if (_mask == null)
_mask = new PatternMask("null ********"); // warn the user that the mask parameter is missing
return;
}
// set new mask properties without loosing state
_mask.UpdateFrom(other);
}
private async void OnCut(ClipboardEventArgs obj)
{
if (ReadOnly)
return;
if (_selection != null)
MaskKind.Delete();
await Update();
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (disposing == true)
{
_jsEvent?.Dispose();
if (_keyInterceptor != null)
{
_keyInterceptor.KeyDown -= HandleKeyDownInternally;
_keyInterceptor.Dispose();
}
_keyInterceptor?.Dispose();
}
}
}