Untitled
unknown
csharp
7 months ago
38 kB
3
Indexable
Never
using System; using System.Collections.Generic; using System.Text; using System.Text.RegularExpressions; using static Cleverence.CompactForms.MaskedEditText.Mask.Options; using Characters = Cleverence.CompactForms.MaskedEditText.Mask.Options.Characters; namespace Cleverence.CompactForms.MaskedEditText { #if __ANDROID__ [DebuggerDisplay("{" + nameof(MaskString) + "}")] #endif public sealed class Mask { #region Public Properties /// <summary> /// Characters for filling empty spaces in mask /// </summary> public char MaskEmptyChar { get { return _maskEmptyChar; } set { _maskEmptyChar = value; } } private char _maskEmptyChar = '_'; /// <summary> /// Keyboard type used for this mask /// </summary> public KeyboardType KeyboardType { get { return _keyboardType; } } private KeyboardType _keyboardType = KeyboardType.None; #endregion #region Private Properties private readonly MaskNode _head; public string MaskString { get; private set; } private readonly Options.MaskOptions _maskOptions; #endregion #region Miscelanious /// <summary> /// Class that contains enums for operations with mask /// </summary> public class Options { /// <summary> /// Modes used for ValidateString function /// </summary> public enum Validation { /// <summary> /// Must match mask's length /// </summary> MatchLength, /// <summary> /// Can't exceed length of a mask, but can be less /// </summary> SmallerOrEqualLength, /// <summary> /// Can exceed length of a mask, but can't be smaller /// </summary> BiggerOrEqualLength, /// <summary> /// Can be bigger or smaller than mask's length /// </summary> FreeLength } /// <summary> /// Modes used for DecorateString function /// </summary> public enum DecorateOptions { /// <summary> /// Decorates and filling missing places with empty spaces /// </summary> DecorateEmpty, /// <summary> /// Stops once reaches end of string /// </summary> KeepEmpty } /// <summary> /// Modes used for special mask options /// </summary> [Flags] public enum MaskOptions { /// <summary> /// Default flag for initialization /// </summary> None = 0, /// <summary> /// Return empty string for ToString() in case if no data is entered /// </summary> HideMaskIfEmpty = 1 << 0, /// <summary> /// Replaces text on entering instead inserting /// </summary> ReplaceMode = 1 << 1, /// <summary> /// Removed characters are replaced with empty space instead of text being shifted left /// </summary> StaticMode = 1 << 2, /// <summary> /// Allows the mask to be expanded without limit /// </summary> Limitless = 1 << 3, /// <summary> /// Allows you to show an empty mask character for characters that do not have displayed characters /// </summary> DisplayMaskEmptyChar = 1 << 4, /// <summary> /// Allows you to ignore the position of the mask symbol when entering, which gives the effect of a "smart" mask that can determine whether the mask rules are satisfied with the current input /// </summary> IgnoreMaskCharPosition = 1 << 5, } /// <summary> /// Modes used for Count method /// </summary> [Flags] public enum Characters { None = 0, /// <summary> /// Decorators (hardcoded characters) /// </summary> Decorators = 1 << 0, /// <summary> /// Characters that users filled in /// </summary> Filled = 1 << 1, /// <summary> /// Characters that aren't filled by user /// </summary> Empty = 1 << 2, /// <summary> /// Overflowing characters (characters after the mask) /// </summary> Overflowing = 1 << 3, /// <summary> /// Overriden characters (returns overriden instead of empty) /// </summary> Overriden = 1 << 4, /// <summary> /// The characters that must be displayed /// </summary> ReqiredDisplayCharacter = 1 << 5, /// <summary> /// All characters /// </summary> All = Decorators | Filled | Empty | Overflowing | Overriden | ReqiredDisplayCharacter } } /// <summary> /// Convert mask into usable form from maskString /// </summary> /// <param name="maskString">String that contains mask</param> /// <param name="options">Additional mask options</param> public Mask(string maskString, INodeTypeProvider nodeTypeProvider, params Options.MaskOptions[] options) { MaskNode current = null; bool screening = false; List<char> debugString = new List<char>(maskString.Length); foreach (char character in maskString) { debugString.Add(character); if (character.Equals('\\')) { screening = true; continue; } NodeType info = new NodeType(null, KeyboardType.None, true); if (!screening) info = nodeTypeProvider.GetCharInfo(character); else screening = false; MaskNode node = new MaskNode() { Character = character, PreviousNode = current, Regex = info.RegEx, IsHardcoded = string.IsNullOrEmpty(info.RegEx), IsReqiredDisplayCharacter = info.IsReqiredDisplayCharacter, }; if (info.Type > KeyboardType) _keyboardType = info.Type; _head = _head ?? node; if (current != null) current.NextNode = node; current = node; } _head = _head ?? new MaskNode(); if (options.Length > 0) { foreach (Options.MaskOptions option in options) _maskOptions |= option; } else { _maskOptions = 0; } MaskString = new string(debugString.ToArray()); } #endregion #region Public methods /// <summary> /// Validate that testString matches this mask /// </summary> /// <param name="testString">String to test</param> /// <param name="mode">Validation mode</param> /// <param name="hasMask">If true - checks the message with the mask. Otherwise checks if the mask can be applied</param> /// <param name="allowEmpty">Include empty spaces in validation</param> /// <returns>True if string matches the mask</returns> public bool ValidateString(string testString, Options.Validation mode, bool hasMask, bool allowEmpty) { if (_head == null) { switch (mode) { case Options.Validation.FreeLength: case Options.Validation.SmallerOrEqualLength: case Options.Validation.BiggerOrEqualLength: return true; case Options.Validation.MatchLength: return false; } } bool result = true; if (testString.Length > 0) ForEachNode((n, c) => { Queue<char> characters = new Queue<char>(testString); char testChar = characters.Dequeue(); if (n.IsHardcoded) { if (hasMask && testChar != n.Character) { c.Break(); result = false; } } else { if (n.Regex != null && !Regex.Match(testChar.ToString(), n.Regex).Success) { if (!allowEmpty || !testChar.Equals(MaskEmptyChar)) { c.Break(); result = false; } } } }, null); if (!result) return false; if (!mode.Equals(Options.Validation.FreeLength)) { switch (mode) { case Options.Validation.SmallerOrEqualLength: if (!(testString.Length <= Count(false))) return false; return true; case Options.Validation.MatchLength: if (testString.Length != Count(false)) return false; return true; case Options.Validation.BiggerOrEqualLength: if (!(testString.Length >= Count(false))) return false; return true; default: return false; } } return true; } /// <summary> /// Decorates string with the mask, putting the control characters at correct places /// </summary> /// <param name="input">Input string to be decorated</param> /// <param name="mode">Decorating mode</param> /// <returns>Decorated string with mask characters or same input in case of validation failure</returns> public string DecorateString(string input, Options.DecorateOptions mode) { if (!ValidateString(input, Options.Validation.FreeLength, false, false)) return input; string finalString = ""; bool fillEmptySpaces = mode.Equals(Options.DecorateOptions.DecorateEmpty); MaskNode current = _head; int inputStrIdx = 0; if (current == null) return input; do { char inputChar = MaskEmptyChar; if (inputStrIdx < input.Length) inputChar = input[inputStrIdx]; else if (!fillEmptySpaces) break; if (current.IsHardcoded) { finalString += current.Character; continue; } finalString += inputChar; inputStrIdx++; } while ((current = current.NextNode) != null); return finalString; } /// <summary> /// Returns raw string without decorators /// </summary> /// <returns>String without decorations failure</returns> public string GetRaw() { string @string = ""; ForEachNode((node, control) => { if (!node.IsHardcoded && node.DisplayedCharacter != null) @string += node.DisplayedCharacter; }, null); return @string; } /// <summary> /// Gets the closest best position to put cursor at, based on the mask /// </summary> /// <param name="position">Current position to start searching from</param> /// <param name="opposite">Go in backward direction</param> /// <param name="minSteps">Minimum steps to take before stopping getting valid position</param> /// <param name="allowAfterTail">Allow null character after tail to be included in GetValidSelection</param> /// <returns>Best closest position for editing</returns> public int GetValidSelection(int position, bool opposite, int minSteps, bool allowAfterTail) { MaskNode current = _head; int validPos = position; for (int i = 0; i < position; i++) { if (current.NextNode == null) { position = allowAfterTail ? i + 1 : i; break; } current = current.NextNode; } if (!current.IsHardcoded) { if (minSteps == 0) return position; minSteps--; } if (opposite && current.PreviousNode != null) { while ((current = current.PreviousNode) != null) { if (position > 0) position--; if (!current.IsHardcoded && (minSteps == 0 || current.PreviousNode == null)) { validPos = position; break; } if (minSteps > 0) minSteps--; } } else if (opposite == false && current.NextNode != null) { while ((current = current.NextNode) != null) { position++; if (!current.IsHardcoded && (minSteps == 0 || current.NextNode == null)) { validPos = position; break; } if (minSteps > 0) minSteps--; } if (minSteps > 0 && allowAfterTail && current == null) validPos = position + 1; } else { if (current != null && current.NextNode == null && allowAfterTail) validPos = position + 1; } return Math.Max(0, Math.Min(Count(true), validPos)); } /// <summary> /// Fixes selection to return the position between hardcoded characters /// </summary> /// <param name="pos">Position of cursor</param> /// <returns>Valid position of cursor</returns> public int FixSelection(int pos) { MaskNode targetNode = GetNode(pos,false); if (targetNode == null) return GetValidSelection(pos,false,0,true); if (targetNode.IsHardcoded) { if(targetNode.PreviousNode != null && !targetNode.PreviousNode.IsHardcoded) return pos; } return GetValidSelection(pos,false,0,true); } /// <summary> /// Converts mask with data into string form /// </summary> /// <param name="showEmptyMask">Show empty mask</param> /// <param name="characters">Characters which to display</param> /// <returns>Filled string with data</returns> public string ToString(bool? showEmptyMask, params Characters[] characters) { showEmptyMask = showEmptyMask ?? (_maskOptions & Options.MaskOptions.HideMaskIfEmpty) == 0; Characters options = Characters.None; foreach (var character in characters) options |= character; string @string = ""; if (!showEmptyMask.Value && Count(Characters.Filled) == 0) return string.Empty; ForEachNode(n => ProcessNode(n, options, ref @string), null); return @string; } char? GetCharacter(char? @char) { return @char == '\0' ? null : @char; } void ProcessNode(MaskNode current, Characters options, ref string @string) { char? @char = null; if (current.DisplayedCharacter == null && options.HasFlag(Characters.Empty)) { if (current.OverrideCharacter != null && options.HasFlag(Characters.Overriden)) @char = current.OverrideCharacter; else if (current.IsHardcoded && options.HasFlag(Characters.Decorators)) @char = current.Character; else if (_maskOptions.HasFlag(Options.MaskOptions.DisplayMaskEmptyChar)) @char = MaskEmptyChar; else @char = ' '; } else if (current.DisplayedCharacter != null) { if ((options.HasFlag(Characters.Filled) && !current.IsTemporary) || (options.HasFlag(Characters.Overflowing) && current.IsTemporary)) @char = current.DisplayedCharacter; } @string += GetCharacter(@char); } public string ToString(params Characters[] characters) { return ToString(null, characters); } /// <summary> /// Converts mask with data into string form /// </summary> /// <returns>Filled string with data</returns> public override string ToString() { return ToString(Characters.All); } /// <summary> /// Inserts string starting at start position /// </summary> /// <param name="string">String to insert</param> /// <param name="start">Starting position</param> /// <param name="shift">New cursor position</param> /// <param name="createNewNodes">Create new nodes if needed</param> public void Insert(string @string, int start, out int shift, bool? createNewNodes) { createNewNodes = createNewNodes ?? _maskOptions.HasFlag(Options.MaskOptions.Limitless); if (_maskOptions.HasFlag(Options.MaskOptions.ReplaceMode)) { InternalInsert(@string, start, out shift, createNewNodes.Value); } else if (_maskOptions.HasFlag(Options.MaskOptions.IgnoreMaskCharPosition)) { InternalInsert2(@string, start, out shift, createNewNodes.Value); } else { string firstHalf = ""; string secondHalf = ""; int idx = 0; shift = 0; ForEachNode(n => { if (idx < start && !n.IsHardcoded && n.DisplayedCharacter != null) firstHalf += n.DisplayedCharacter; else if (idx >= start && !n.IsHardcoded && n.DisplayedCharacter != null) secondHalf += n.DisplayedCharacter; idx++; }, null); int _; InternalRemove(0, Count(true), out _); InternalInsert(firstHalf + @string, 0, out shift, createNewNodes.Value); InternalInsert(secondHalf, shift, out _, createNewNodes.Value); } } /// <summary> /// Inserts string starting at start position /// </summary> /// <param name="string">String to insert</param> /// <param name="start">Starting position</param> /// <param name="createNewNodes">Create new nodes if needed</param> public void Insert(string @string, int start, bool? createNewNodes) { int _; Insert(@string, start, out _, createNewNodes); } /// <summary> /// Removes certain amount of characters from mask filled data /// </summary> /// <param name="start">Starting index</param> /// <param name="count">Amount of characters to remove</param> /// <param name="shift">New cursor position</param> public void Remove(int start, int count, out int shift) { InternalRemove(start, count, out shift); if (_maskOptions.HasFlag(Options.MaskOptions.StaticMode)) return; string data = GetRaw(); int _; InternalRemove(0, Count(true), out _); InternalInsert(data, 0, out _, true); } /// <summary> /// Removes certain amount of characters from mask filled data /// </summary> /// <param name="start">Starting index</param> /// <param name="count">Amount of characters to remove</param> public void Remove(int start, int count) { int _; Remove(start, count, out _); } /// <summary> /// Get size of the mask /// </summary> /// <param name="countTemporary">Include temporary (overflowing) symbols</param> /// <returns>Size of the mask</returns> public int Count(bool countTemporary) { Characters options = Characters.Filled | Characters.Decorators | Characters.Empty; options |= countTemporary ? Characters.Overflowing : 0; return Count(options); } /// <summary> /// Get size of the data in mask by parameters /// </summary> /// <param name="options">Types of characters to count</param> /// <returns></returns> public int Count(Characters options) { int count = 0; ForEachNode((n, c) => { if ((options.HasFlag(Characters.Empty) && n.DisplayedCharacter == null && !n.IsHardcoded) || (options.HasFlag(Characters.Decorators) && n.IsHardcoded) || (options.HasFlag(Characters.Overflowing) && n.IsTemporary) || (options.HasFlag(Characters.Filled) && n.DisplayedCharacter != null) || (options.HasFlag(Characters.ReqiredDisplayCharacter) && !n.IsHardcoded && n.IsReqiredDisplayCharacter)) count++; }, null); return count; } /// <summary> /// Makes mask display specific characters that are passed in newMask /// </summary> /// <param name="newMask">New mask to display. Empty will clear overrides</param> public void OverrideMask(string newMask) { if (newMask.Length != Count(false)) return; Queue<char> characters = new Queue<char>(newMask); bool clearMask = characters.Count == 0; ForEachNode(n => { if (clearMask) n.OverrideCharacter = null; else n.OverrideCharacter = characters.Dequeue(); }, null); } /// <summary> /// Function to get the latest filled position. If it's decorator - then gives you position at the end of the decorators /// </summary> /// <returns>Latest filled position</returns> public int GetMaxFilledLength() { int count = 0; int lastDisplayedSymbolIdx = 0; MaskNode node = null; ForEachNode(n => { if (n.DisplayedCharacter != null) { lastDisplayedSymbolIdx = count; node = n; } count++; }, null); if ((node != null && node.NextNode != null && node.NextNode.IsHardcoded) || (node != null && node.IsHardcoded) || (_head != null && _head.IsHardcoded)) { int idx = GetValidSelection(lastDisplayedSymbolIdx + 1, false, 0, true); MaskNode potentialNode = GetNode(idx, false); if (potentialNode != null && potentialNode.IsHardcoded && potentialNode.NextNode == null) return lastDisplayedSymbolIdx + 1; return idx; } if (Count(Characters.Filled) == 0) return lastDisplayedSymbolIdx; else return lastDisplayedSymbolIdx + 1; } //public Android.Text.InputTypes GetKeyboardInputType(Android.Text.InputTypes inputType) => KeyboardType switch //{ // KeyboardType.Numeric when inputType == Android.Text.InputTypes.ClassText => Android.Text.InputTypes.ClassNumber, // KeyboardType.Alphanumeric when inputType == Android.Text.InputTypes.ClassText => Android.Text.InputTypes.TextVariationFilter, // _ => inputType //}; public void ForEachNode(Action<MaskNode> action, MaskNode startingNode) { ForEachNode((n, _) => action.Invoke(n), startingNode); } public void ForEachNode(Action<MaskNode, LoopControl> action, MaskNode startingNode) { MaskNode current = startingNode ?? _head; LoopControl control = new LoopControl(); if (current == null) return; do { action.Invoke(current, control); if (control.ProcessBreak()) break; } while ((current = current.NextNode) != null); } #endregion #region Private Methods private void InternalRemove(int start, int count, out int shift) { MaskNode startingNode = GetNode(start,false); shift = 0; if (startingNode == null) return; ForEachNode((node, control) => { if (count == 0) { control.Break(); return; } if (!node.IsHardcoded) { if (node.IsTemporary) { node.PreviousNode.NextNode = node.NextNode; if (node.NextNode != null) node.NextNode.PreviousNode = node.PreviousNode; } else { node.DisplayedCharacter = null; } } count--; }, startingNode); if (startingNode.IsHardcoded) shift = GetValidSelection(start, true,0,true) + 1; else if (startingNode.PreviousNode != null && startingNode.PreviousNode.IsHardcoded) shift = GetValidSelection(start - 1, true,0,true) + 1; else shift = GetValidSelection(start, true,0,true); } private void InternalInsert(string @string, int start, out int shift, bool createNewNodes) { MaskNode startingNode = GetNode(start, createNewNodes); Queue<char> characters = new Queue<char>(@string); bool validForm = ValidateString(@string, Options.Validation.BiggerOrEqualLength, true, false); shift = 0; if (startingNode == null) { shift = start; return; } int nodeIdx = 0; int newCursorPos = start; ForEachNode((node, control) => { if (characters.Count == 0) { control.Break(); return; } if (!node.IsHardcoded) { char @char = characters.Dequeue(); if (node.Regex != null && Regex.Match(new string(@char, 1), node.Regex).Success || node.IsTemporary) { node.DisplayedCharacter = @char; newCursorPos = nodeIdx + 1; } else if (!_maskOptions.HasFlag(Options.MaskOptions.StaticMode)) { do { if ((node.Regex == null || !Regex.Match(new string(@char, 1), node.Regex).Success) && !node.IsTemporary) continue; node.DisplayedCharacter = @char; newCursorPos = nodeIdx + 1; break; } while (characters.TryDequeue(out @char)); } } else if (validForm) { characters.Dequeue(); } if (characters.Count > 0 && createNewNodes && node.NextNode == null) node.NextNode = new MaskNode { IsTemporary = true, PreviousNode = node }; // ReSharper disable once AccessToModifiedClosure nodeIdx++; }, startingNode); //if (newCursorPos != start) newCursorPos++; shift = GetValidSelection(newCursorPos,false,0,true); } private void InternalInsert2(string @string, int start, out int shift, bool createNewNodes) { MaskNode startingNode = GetNode(start, createNewNodes); Queue<char> characters = new Queue<char>(@string); bool validForm = ValidateString(@string, Options.Validation.BiggerOrEqualLength, true, false); shift = 0; if (startingNode == null) { shift = start; return; } int nodeIdx = 0; int newCursorPos = start; var insertedString = new StringBuilder(); ForEachNode((node, control) => { if (characters.Count == 0) { control.Break(); return; } if (!node.IsHardcoded) { char @char = characters.Dequeue(); if (node.IsReqiredDisplayCharacter && node.Regex != null && Regex.Match(new string(@char, 1), node.Regex).Success || node.IsTemporary) { node.DisplayedCharacter = @char; } else { ForEachNode((internalNode, internalLoop) => { if (internalNode.IsReqiredDisplayCharacter && internalNode.DisplayedCharacter == null && node.Regex != null && Regex.Match(new string(@char, 1), node.Regex).Success || node.IsTemporary) { internalNode.DisplayedCharacter = @char; internalLoop.Break(); } else if (internalNode.IsReqiredDisplayCharacter && internalNode.DisplayedCharacter == null) { internalLoop.Break(); } }, node.NextNode); } //if (node.Regex != null && Regex.Match(new string(@char, 1), node.Regex).Success || node.IsTemporary) //{ // node.DisplayedCharacter = @char; // newCursorPos = nodeIdx + 1; //} //ForEachNode((internalNode, internalLoop) => //{ // if(internalNode.IsReqiredDisplayCharacter) // { // internalNode.DisplayedCharacter = @char; // } //}, node); } else if (validForm) { characters.Dequeue(); } if (characters.Count > 0 && createNewNodes && node.NextNode == null) node.NextNode = new MaskNode { IsTemporary = true, PreviousNode = node }; // ReSharper disable once AccessToModifiedClosure nodeIdx++; }, startingNode); //if (newCursorPos != start) newCursorPos++; shift = GetValidSelection(newCursorPos, false, 0, true); } private MaskNode GetNode(int index, bool createIfNotFound) { MaskNode current = _head; for (int i = 0; i < index; i++) { if (current.NextNode == null) { if (createIfNotFound) { MaskNode node = new MaskNode() { IsTemporary = true, PreviousNode = current }; current.NextNode = node; return node; } return null; } current = current.NextNode; } return current; } #endregion } #region Classes/Enums/Extensions public sealed class MaskNode { private MaskNode _previousNode; private MaskNode _nextNode; private string _regex = "."; private char _character; private char? _displayedCharacter; private char? _overrideCharacter; private bool _isHardcoded; private bool _isTemporary; private bool _isReqiredDisplayCharacter; internal MaskNode PreviousNode { get { return _previousNode; } set { _previousNode = value; } } internal MaskNode NextNode { get { return _nextNode; } set { _nextNode = value; } } internal string Regex { get { return _regex; } set { _regex = value; } } internal char Character { get { return _character; } set { _character = value; } } internal char? DisplayedCharacter { get { return _displayedCharacter; } set { _displayedCharacter = value; } } internal char? OverrideCharacter { get { return _overrideCharacter; } set { _overrideCharacter = value; } } internal bool IsHardcoded { get { return _isHardcoded; } set { _isHardcoded = value; } } internal bool IsTemporary { get { return _isTemporary; } set { _isTemporary = value; } } /// <summary> /// Требуется отображать символ? /// </summary> internal bool IsReqiredDisplayCharacter { get { return _isReqiredDisplayCharacter; } set { _isReqiredDisplayCharacter = value; } } } public sealed class LoopControl { private bool _willBreak; internal void Break() { _willBreak = true; } internal bool ProcessBreak() { bool result = _willBreak; _willBreak = false; return result; } } public sealed class NodeType { internal string RegEx { get; private set; } internal KeyboardType Type { get; private set; } /// <summary> /// Требуется ли отображать значение /// </summary> public bool IsReqiredDisplayCharacter { get; private set; } public NodeType(string regEx, KeyboardType type, bool isReqiredDisplayCharacter) { RegEx = regEx; Type = type; IsReqiredDisplayCharacter = isReqiredDisplayCharacter; } } public enum KeyboardType { None, Numeric, Alphanumeric } internal static class Extensions { internal static bool TryDequeue<T>(this Queue<T> obj, out T item) { item = default(T); if (obj.Count == 0) return false; item = obj.Dequeue(); return true; } internal static bool HasFlag(this Enum item1, Enum item2) { return (Convert.ToInt32(item1) & Convert.ToInt32(item2)) != 0; } } #endregion }
Leave a Comment