// 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, 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("** **-** **"); /// /// ChildContent will only be displayed if InputType.Hidden and if its not null. Required for Select /// [Parameter] [Category(CategoryTypes.General.Appearance)] public RenderFragment ChildContent { get; set; } /// /// Provide a masking object. Built-in masks are PatternMask, MultiMask, RegexMask and BlockMask /// [Parameter] [Category(CategoryTypes.General.Data)] public IMask MaskKind { get => _mask; set => SetMask(value); } /// /// Type of the input element. It should be a valid HTML5 input type. /// [Parameter] [Category(CategoryTypes.FormComponent.ListAppearance)] public InputType InputType { get; set; } = InputType.Text; /// /// Show clear button. /// [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; } /// /// Button click event for clear button. Called after text and value has been cleared. /// [Parameter] [Category(CategoryTypes.FormComponent.ListAppearance)] public EventCallback OnClearButtonClick { get; set; } /// /// Custom clear icon. /// [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.ConvertBack(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.Convert(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}")); /// /// Clear the text field. /// /// 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(); } } }