UNPKG

react-native-advanced-input-mask

Version:

Text input mask for React Native on iOS, Android and web. Synchronous and easy formatting without hustle

320 lines (260 loc) 9.42 kB
import { AFFINITY_CALCULATION_STRATEGY } from "../enums"; import { calculateAffinityOfMask } from "./helper/affinityCalculationStrategy"; import Mask from "./helper/Mask"; import RTLMask from "./helper/RTLMask"; import CaretString from "./model/CaretString"; import { CaretGravityType } from "./model/types"; import type { Notation } from "../types"; import type { MaskResult } from "./model/types"; import type { NativeSyntheticEvent, TextInputChangeEventData, TextInputFocusEventData, } from "react-native"; type Field = HTMLInputElement; type MaskAffinity = { mask: Mask; affinity: number }; class MaskedTextChangedListener { public primaryFormat: string; public affineFormats: string[]; public customNotations: Notation[]; public affinityCalculationStrategy: AFFINITY_CALCULATION_STRATEGY; public autocomplete: boolean; public autoskip: boolean; public rightToLeft: boolean; public textField: Field | null = null; public allowedKeys: string; public autocompleteOnFocus?: boolean; public defaultValue?: string; private validationRegex?: RegExp; private afterText: string = ""; constructor( primaryFormat: string, affineFormats: string[] = [], customNotations: Notation[] = [], affinityCalculationStrategy: AFFINITY_CALCULATION_STRATEGY = AFFINITY_CALCULATION_STRATEGY.WHOLE_STRING, autocomplete: boolean = true, autoskip: boolean = false, rightToLeft: boolean = false, allowedKeys: string = "", validationRegex?: string, autocompleteOnFocus: boolean = false, defaultValue?: string, ) { this.primaryFormat = primaryFormat; this.affineFormats = affineFormats; this.customNotations = customNotations; this.affinityCalculationStrategy = affinityCalculationStrategy; this.autocomplete = autocomplete; this.autoskip = autoskip; this.rightToLeft = rightToLeft; this.allowedKeys = allowedKeys; this.autocompleteOnFocus = autocompleteOnFocus; this.setValidationRegex(validationRegex); this.setDefaultText(defaultValue); } public get primaryMask(): Mask { return this.maskGetOrCreate(this.primaryFormat, this.customNotations); } public setText(text: string, autocomplete?: boolean): void { if (!this.textField) { return; } const newText = this.prepareText(text); if (!this.isValidText(text)) { return; } const useAutocomplete = autocomplete ?? this.autocomplete; const textAndCaret = new CaretString(newText, newText.length, { type: CaretGravityType.Forward, autocomplete: useAutocomplete, autoskip: false, }); const result: MaskResult = this.pickMask(textAndCaret).apply(textAndCaret); this.textField.value = result.formattedText.string; this.textField.setSelectionRange( result.formattedText.caretPosition, result.formattedText.caretPosition, ); this.afterText = result.formattedText.string; } public setAllowedKeys = (allowedKeys: string): void => { this.allowedKeys = allowedKeys; if (this.textField && allowedKeys) { this.setText(this.textField.value, false); } }; public get placeholder(): string { return this.primaryMask.placeholder(); } public get acceptableTextLength(): number { return this.primaryMask.acceptableTextLength(); } public get totalTextLength(): number { return this.primaryMask.totalTextLength(); } public get acceptableValueLength(): number { return this.primaryMask.acceptableValueLength(); } public get totalValueLength(): number { return this.primaryMask.totalValueLength(); } public setTextField = (textField: Field): void => { this.textField = textField; Object.assign(textField, { setText: (text: string, autocomplete?: boolean) => this.setText(text, autocomplete), clear: () => this.setText("", false), }); }; public handleTextChange = ( event: NativeSyntheticEvent<TextInputChangeEventData>, ): MaskResult => { const { nativeEvent, target } = event; const { inputType, text } = nativeEvent as NativeSyntheticEvent<TextInputChangeEventData>["nativeEvent"] & InputEvent; const textField = target as unknown as HTMLInputElement; const isDeletion = inputType === "deleteContentForward" || inputType === "deleteContentBackward"; const selectionStart = textField.selectionStart || 0; const isInside = selectionStart < text.length; const caretPosition = isDeletion || isInside ? selectionStart : text.length; const useAutocomplete = isDeletion ? false : this.autocomplete; const useAutoskip = isDeletion ? this.autoskip : false; const caretGravity = { type: isDeletion ? CaretGravityType.Backward : CaretGravityType.Forward, autoskip: useAutoskip, autocomplete: useAutocomplete, }; const newText = this.prepareText(text); if (!this.isValidText(text)) { const textAndCaret = new CaretString(this.afterText, caretPosition, { type: CaretGravityType.Backward, autocomplete: false, autoskip: false, }); const result = this.pickMask(textAndCaret).apply(textAndCaret); const currentCaretPosition = textField.selectionEnd; textField.value = result.formattedText.string; if (currentCaretPosition && currentCaretPosition > 0) { textField.setSelectionRange( currentCaretPosition - 1, currentCaretPosition - 1, ); } return result; } const textAndCaret = new CaretString(newText, caretPosition, caretGravity); const result = this.pickMask(textAndCaret).apply(textAndCaret); textField.value = result.formattedText.string; textField.setSelectionRange( result.formattedText.caretPosition, result.formattedText.caretPosition, ); this.afterText = result.formattedText.string; return result; }; private prepareText = (text: string): string => { return this.allowedKeys ? [...text] .filter( (char) => !this.allowedKeys || this.allowedKeys.includes(char), ) .join("") : text; }; private isValidText = (text: string): boolean => this.validationRegex ? this.validationRegex.test(text) : true; private setDefaultText = (defaultValue?: string): void => { this.defaultValue = defaultValue; if (defaultValue) { const textAndCaret = new CaretString(defaultValue, defaultValue.length, { type: CaretGravityType.Forward, autocomplete: false, autoskip: false, }); const result: MaskResult = this.pickMask(textAndCaret).apply(textAndCaret); this.afterText = result.formattedText.string; } }; handleFocus = ( event: NativeSyntheticEvent<TextInputFocusEventData>, ): void => { if (this.autocompleteOnFocus) { const textField = event.target as unknown as HTMLInputElement; const text = textField.value.length > 0 ? textField.value : ""; const textAndCaret = new CaretString(text, text.length, { type: CaretGravityType.Forward, autocomplete: true, autoskip: false, }); const result = this.pickMask(textAndCaret).apply(textAndCaret); this.afterText = result.formattedText.string; textField.value = this.afterText; textField.setSelectionRange( result.formattedText.caretPosition, result.formattedText.caretPosition, ); } }; pickMask = (text: CaretString): Mask => { if (this.affineFormats.length === 0) { return this.primaryMask; } const primaryAffinity = this.calculateAffinity(this.primaryMask, text); const masksAndAffinities: MaskAffinity[] = []; for (const format of this.affineFormats) { const candidateMask = this.maskGetOrCreate(format, this.customNotations); const affinity = this.calculateAffinity(candidateMask, text); masksAndAffinities.push({ mask: candidateMask, affinity }); } masksAndAffinities.sort((a, b) => b.affinity - a.affinity); let insertIndex = -1; for (let i = 0; i < masksAndAffinities.length; i++) { const affinity = masksAndAffinities[i]?.affinity; if (affinity && primaryAffinity === affinity) { insertIndex = i; break; } } if (insertIndex >= 0) { masksAndAffinities.splice(insertIndex, 0, { mask: this.primaryMask, affinity: primaryAffinity, }); } else { masksAndAffinities.push({ mask: this.primaryMask, affinity: primaryAffinity, }); } return masksAndAffinities[0]!.mask; }; public setValidationRegex = (validationRegex?: string) => { if (validationRegex) { this.validationRegex = new RegExp(validationRegex); } else { this.validationRegex = undefined; } }; public getValidationRegex = (): string | undefined => { return this.validationRegex?.source; }; private maskGetOrCreate = ( format: string, customNotations: Notation[], ): Mask => this.rightToLeft ? RTLMask.getOrCreate(format, customNotations) : Mask.getOrCreate(format, customNotations); private calculateAffinity(mask: Mask, text: CaretString): number { return calculateAffinityOfMask( this.affinityCalculationStrategy, mask, text, ); } } export default MaskedTextChangedListener;