├── .nvmrc ├── package ├── .nvmrc ├── ios │ ├── .swift-version │ ├── .swiftformat │ ├── MaskedTextInput-Bridging-Header.h │ ├── .swiftlint.yml │ ├── extentions │ │ ├── Notations.swift │ │ └── AffinityCalculationStrategy.swift │ ├── AdvancedTextInputMaskDecoratorViewManager.swift │ ├── MaskedTextInputManager.mm │ ├── AdvancedtextInputMaskDecoratorView.h │ ├── AdvancedInputMaskDelegateWrapper.swift │ ├── RNConversions.h │ ├── AdvancedTextInputViewContainer.h │ ├── NotifyingAdvancedTexInputMaskListener.swift │ └── .clang-format ├── src │ ├── __tests__ │ │ └── index.test.tsx │ ├── native │ │ ├── architecture.ts │ │ ├── specs │ │ │ └── AdvancedTextInputMaskDecoratorViewNativeComponent.ts │ │ └── views │ │ │ └── MaskedTextInput │ │ │ └── index.tsx │ ├── enums.ts │ ├── web │ │ ├── helper │ │ │ ├── FormatError.ts │ │ │ ├── RTLCaretStringIterator.ts │ │ │ ├── AutocompletionStack.ts │ │ │ ├── CaretStringIterator.ts │ │ │ ├── RTLMask.ts │ │ │ └── affinityCalculationStrategy.ts │ │ ├── model │ │ │ ├── state │ │ │ │ ├── EOLState.ts │ │ │ │ ├── State.ts │ │ │ │ ├── FixedState.ts │ │ │ │ ├── FreeState.ts │ │ │ │ ├── OptionalValueState.ts │ │ │ │ └── ValueState.ts │ │ │ ├── utils.ts │ │ │ ├── CaretString.ts │ │ │ ├── constants.ts │ │ │ └── types.ts │ │ ├── hooks │ │ │ └── useMaskedTextInputListener │ │ │ │ ├── types.ts │ │ │ │ └── index.ts │ │ └── views │ │ │ └── MaskedTextInput │ │ │ └── index.tsx │ ├── index.tsx │ ├── index.web.tsx │ └── types.ts ├── tsconfig.build.json ├── android │ ├── src │ │ ├── main │ │ │ ├── AndroidManifestNew.xml │ │ │ ├── AndroidManifest.xml │ │ │ └── java │ │ │ │ └── com │ │ │ │ └── maskedtextinput │ │ │ │ ├── events │ │ │ │ ├── EventNames.kt │ │ │ │ └── ChangeTextEvent.kt │ │ │ │ ├── mappers │ │ │ │ ├── AffinityCalculationStrategyMapper.kt │ │ │ │ └── NotationMapper.kt │ │ │ │ ├── MaskedTextInputPackage.kt │ │ │ │ ├── listeners │ │ │ │ ├── MaskedTextValueListener.kt │ │ │ │ └── ReactMaskedTextChangeListener.kt │ │ │ │ └── transformation │ │ │ │ └── CustomTransformationMethod.kt │ │ ├── newarch │ │ │ └── java │ │ │ │ └── com │ │ │ │ └── maskedtextinput │ │ │ │ └── AdvancedTextInputMaskDecoratorViewManagerSpec.kt │ │ └── oldarch │ │ │ └── java │ │ │ └── com │ │ │ └── maskedtextinput │ │ │ └── AdvancedTextInputMaskDecoratorViewManagerSpec.kt │ ├── gradle.properties │ └── build.gradle ├── tsconfig.json ├── jest │ └── index.js ├── react-native-advanced-input-mask.podspec └── package.json ├── apps └── example │ ├── .watchmanconfig │ ├── jest.config.js │ ├── .bundle │ └── config │ ├── app.json │ ├── android │ ├── app │ │ ├── src │ │ │ ├── main │ │ │ │ ├── res │ │ │ │ │ ├── values │ │ │ │ │ │ ├── strings.xml │ │ │ │ │ │ └── styles.xml │ │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ │ └── drawable │ │ │ │ │ │ └── rn_edit_text_material.xml │ │ │ │ ├── AndroidManifest.xml │ │ │ │ └── java │ │ │ │ │ └── com │ │ │ │ │ └── maskedtextinputexample │ │ │ │ │ ├── MainActivity.kt │ │ │ │ │ └── MainApplication.kt │ │ │ └── debug │ │ │ │ └── AndroidManifest.xml │ │ ├── debug.keystore │ │ ├── proguard-rules.pro │ │ └── build.gradle │ ├── gradle │ │ └── wrapper │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ ├── settings.gradle │ ├── build.gradle │ ├── gradle.properties │ └── gradlew.bat │ ├── ios │ ├── MaskedTextInputExample │ │ ├── Images.xcassets │ │ │ ├── Contents.json │ │ │ └── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ ├── PrivacyInfo.xcprivacy │ │ ├── AppDelegate.swift │ │ ├── Info.plist │ │ └── LaunchScreen.storyboard │ ├── MaskedTextInputExample.xcworkspace │ │ ├── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ │ └── contents.xcworkspacedata │ ├── .xcode.env │ ├── Podfile │ └── MaskedTextInputExample.xcodeproj │ │ └── xcshareddata │ │ └── xcschemes │ │ └── MaskedTextInputExample.xcscheme │ ├── index.js │ ├── tsconfig.json │ ├── src │ ├── navigation │ │ ├── index.tsx │ │ ├── Root │ │ │ ├── types.ts │ │ │ └── index.tsx │ │ └── screenNames.ts │ ├── components │ │ ├── TextInput │ │ │ ├── styles.ts │ │ │ └── index.tsx │ │ ├── Button │ │ │ ├── styles.ts │ │ │ └── index.tsx │ │ ├── replicas │ │ │ └── touchables │ │ │ │ ├── index.ts │ │ │ │ ├── types │ │ │ │ └── index.ts │ │ │ │ └── TouchableHighlight │ │ │ │ └── index.tsx │ │ ├── BaseTextInput │ │ │ ├── styles.ts │ │ │ └── index.tsx │ │ └── MenuItem │ │ │ ├── styles.ts │ │ │ └── index.tsx │ ├── screens │ │ ├── Date │ │ │ ├── styles.ts │ │ │ └── index.tsx │ │ ├── IBAN │ │ │ ├── styles.ts │ │ │ └── index.tsx │ │ ├── ControlledInput │ │ │ ├── styles.ts │ │ │ └── index.tsx │ │ ├── CustomNotations │ │ │ ├── styles.ts │ │ │ └── index.tsx │ │ ├── ValidationRegEx │ │ │ ├── styles.ts │ │ │ └── index.tsx │ │ ├── RNTextInput │ │ │ ├── styles.ts │ │ │ └── index.tsx │ │ ├── Phone │ │ │ ├── styles.ts │ │ │ └── index.tsx │ │ ├── AllowedKeys │ │ │ ├── styles.ts │ │ │ └── index.tsx │ │ └── Main │ │ │ ├── styles.ts │ │ │ ├── index.tsx │ │ │ └── constants.ts │ └── App.tsx │ ├── index.web.js │ ├── public │ └── index.html │ ├── Gemfile │ ├── metro.config.js │ ├── babel.config.js │ ├── package.json │ ├── README.md │ ├── Gemfile.lock │ └── webpack.config.js ├── .gitattributes ├── babel.config.js ├── gifs └── demo.gif ├── clang-format.sh ├── scripts └── size.js ├── .yarnrc.yml ├── e2e └── .maestro │ ├── set-text.yaml │ ├── phone-input.yaml │ ├── clear-text.yaml │ ├── controlled-input.yaml │ └── validation-regex.yaml ├── .editorconfig ├── lefthook.yml ├── .prettierignore ├── .github ├── workflows │ ├── build-library.yml │ ├── tests.yml │ ├── lint.yml │ ├── verify-android.yml │ ├── build-android.yml │ ├── build-web.yml │ ├── publish.yml │ ├── build-ios.yml │ ├── verify-ios.yml │ ├── ios-e2e-test.yml │ ├── size-diff-calculation.yml │ └── android-e2e-test.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── actions │ ├── setup │ │ └── action.yml │ ├── build-android │ │ └── action.yml │ └── build-ios │ │ └── action.yml └── PULL_REQUEST_TEMPLATE.md ├── tsconfig.json ├── LICENSE ├── .gitignore ├── package.json └── CONTRIBUTING.md /.nvmrc: -------------------------------------------------------------------------------- 1 | v20.19.5 2 | -------------------------------------------------------------------------------- /package/.nvmrc: -------------------------------------------------------------------------------- 1 | v20.19.5 2 | -------------------------------------------------------------------------------- /package/ios/.swift-version: -------------------------------------------------------------------------------- 1 | 5.6 -------------------------------------------------------------------------------- /apps/example/.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /package/ios/.swiftformat: -------------------------------------------------------------------------------- 1 | --indent 2 2 | 3 | --exclude Pods,Generated -------------------------------------------------------------------------------- /package/src/__tests__/index.test.tsx: -------------------------------------------------------------------------------- 1 | it.todo("write a test"); 2 | -------------------------------------------------------------------------------- /apps/example/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "react-native", 3 | }; 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.pbxproj -text 2 | # specific for windows script files 3 | *.bat text eol=crlf -------------------------------------------------------------------------------- /apps/example/.bundle/config: -------------------------------------------------------------------------------- 1 | BUNDLE_PATH: "vendor/bundle" 2 | BUNDLE_FORCE_RUBY_PLATFORM: 1 3 | -------------------------------------------------------------------------------- /package/src/native/architecture.ts: -------------------------------------------------------------------------------- 1 | export const IS_FABRIC = "nativeFabricUIManager" in global; 2 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ["module:@react-native/babel-preset"], 3 | }; 4 | -------------------------------------------------------------------------------- /gifs/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IvanIhnatsiuk/react-native-advanced-input-mask/HEAD/gifs/demo.gif -------------------------------------------------------------------------------- /apps/example/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "MaskedTextInputExample", 3 | "displayName": "MaskedTextInputExample" 4 | } 5 | -------------------------------------------------------------------------------- /package/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "compilerOptions": { 4 | "paths": {} 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /package/android/src/main/AndroidManifestNew.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /package/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig", 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "baseUrl": "." 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /apps/example/android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | MaskedTextInputExample 3 | 4 | -------------------------------------------------------------------------------- /apps/example/ios/MaskedTextInputExample/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "version": 1, 4 | "author": "xcode" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /apps/example/android/app/debug.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IvanIhnatsiuk/react-native-advanced-input-mask/HEAD/apps/example/android/app/debug.keystore -------------------------------------------------------------------------------- /clang-format.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | find ./package/ios -iname *.h -o -iname *.cpp -o -iname *.m -o -iname *.mm | grep -v -e Pods -e build | xargs clang-format -i -n --Werror -------------------------------------------------------------------------------- /package/android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /package/src/enums.ts: -------------------------------------------------------------------------------- 1 | export const enum AFFINITY_CALCULATION_STRATEGY { 2 | WHOLE_STRING = 0, 3 | PREFIX = 1, 4 | CAPACITY = 2, 5 | EXTRACTED_VALUE_CAPACITY = 3, 6 | } 7 | -------------------------------------------------------------------------------- /apps/example/android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IvanIhnatsiuk/react-native-advanced-input-mask/HEAD/apps/example/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /scripts/size.js: -------------------------------------------------------------------------------- 1 | const { exec } = require("child_process"); 2 | 3 | exec("npm pack --json", { cwd: "package" }, (error, stdout, stderr) => { 4 | console.log(JSON.parse(stdout)[0].size); 5 | }); 6 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | compressionLevel: mixed 2 | 3 | enableGlobalCache: false 4 | 5 | nmHoistingLimits: workspaces 6 | 7 | nodeLinker: node-modules 8 | 9 | yarnPath: .yarn/releases/yarn-4.6.0.cjs 10 | -------------------------------------------------------------------------------- /package/android/src/main/java/com/maskedtextinput/events/EventNames.kt: -------------------------------------------------------------------------------- 1 | package com.maskedtextinput.events 2 | 3 | object EventNames { 4 | const val CHANGE_TEXT_EVENT = "onAdvancedMaskTextChange" 5 | } 6 | -------------------------------------------------------------------------------- /apps/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IvanIhnatsiuk/react-native-advanced-input-mask/HEAD/apps/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /apps/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IvanIhnatsiuk/react-native-advanced-input-mask/HEAD/apps/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /apps/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IvanIhnatsiuk/react-native-advanced-input-mask/HEAD/apps/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /apps/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IvanIhnatsiuk/react-native-advanced-input-mask/HEAD/apps/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /apps/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IvanIhnatsiuk/react-native-advanced-input-mask/HEAD/apps/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /apps/example/index.js: -------------------------------------------------------------------------------- 1 | import { AppRegistry } from "react-native"; 2 | 3 | import { name as appName } from "./app.json"; 4 | import App from "./src/App"; 5 | 6 | AppRegistry.registerComponent(appName, () => App); 7 | -------------------------------------------------------------------------------- /apps/example/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IvanIhnatsiuk/react-native-advanced-input-mask/HEAD/apps/example/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /apps/example/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IvanIhnatsiuk/react-native-advanced-input-mask/HEAD/apps/example/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /apps/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IvanIhnatsiuk/react-native-advanced-input-mask/HEAD/apps/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /apps/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IvanIhnatsiuk/react-native-advanced-input-mask/HEAD/apps/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /apps/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IvanIhnatsiuk/react-native-advanced-input-mask/HEAD/apps/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /apps/example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig", 3 | "references": [ 4 | { 5 | "path": "../../package" 6 | } 7 | ], 8 | "compilerOptions": { 9 | "rootDir": "../.." 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /package/android/gradle.properties: -------------------------------------------------------------------------------- 1 | MaskedTextInput_kotlinVersion=1.9.24 2 | MaskedTextInput_minSdkVersion=21 3 | MaskedTextInput_targetSdkVersion=35 4 | MaskedTextInput_compileSdkVersion=35 5 | MaskedTextInput_ndkversion=26.1.10909125 6 | -------------------------------------------------------------------------------- /package/src/web/helper/FormatError.ts: -------------------------------------------------------------------------------- 1 | class FormatError extends Error { 2 | constructor() { 3 | super("Format error"); 4 | Object.setPrototypeOf(this, FormatError.prototype); 5 | } 6 | } 7 | 8 | export default FormatError; 9 | -------------------------------------------------------------------------------- /apps/example/src/navigation/index.tsx: -------------------------------------------------------------------------------- 1 | import { createStaticNavigation } from "@react-navigation/native"; 2 | 3 | import RootStack from "./Root"; 4 | 5 | const Navigation = createStaticNavigation(RootStack); 6 | 7 | export default Navigation; 8 | -------------------------------------------------------------------------------- /e2e/.maestro/set-text.yaml: -------------------------------------------------------------------------------- 1 | appId: "com.maskedtextinputexample" 2 | --- 3 | - launchApp 4 | - tapOn: 5 | id: "phone-input" 6 | - tapOn: "+1 (000) 000-0000" 7 | - tapOn: "Set text" 8 | - assertVisible: "+1 (999) 999" 9 | - assertVisible: "formatted value +1 (999) 999" 10 | -------------------------------------------------------------------------------- /package/src/web/model/state/EOLState.ts: -------------------------------------------------------------------------------- 1 | import State from "./State"; 2 | 3 | class EOLState extends State { 4 | accept: (char: string) => null = () => { 5 | return null; 6 | }; 7 | 8 | toString: () => string = () => "EOL"; 9 | } 10 | 11 | export default EOLState; 12 | -------------------------------------------------------------------------------- /apps/example/src/components/TextInput/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from "react-native"; 2 | 3 | export default StyleSheet.create({ 4 | text: { 5 | fontSize: 14, 6 | marginBottom: 8, 7 | marginRight: 8, 8 | color: "#000", 9 | }, 10 | button: { 11 | marginBottom: 8, 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /apps/example/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /e2e/.maestro/phone-input.yaml: -------------------------------------------------------------------------------- 1 | appId: "com.maskedtextinputexample" 2 | --- 3 | - launchApp 4 | - tapOn: 5 | id: "phone-input" 6 | - tapOn: "+1 (000) 000-0000" 7 | - inputText: 1234567890 8 | - assertVisible: "+1 (234) 567-890" 9 | - tapOn: 10 | point: "50%,37%" 11 | - tapOn: "Clear text" 12 | - assertVisible: "+1 (000) 000-0000" 13 | -------------------------------------------------------------------------------- /apps/example/index.web.js: -------------------------------------------------------------------------------- 1 | import { AppRegistry } from "react-native"; 2 | 3 | import { name as appName } from "./app.json"; 4 | import App from "./src/App"; 5 | 6 | AppRegistry.registerComponent(appName, () => App); 7 | 8 | AppRegistry.runApplication(appName, { 9 | initialProps: {}, 10 | rootTag: document.getElementById("root"), 11 | }); 12 | -------------------------------------------------------------------------------- /apps/example/ios/MaskedTextInputExample.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /apps/example/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Demo Project 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /package/ios/MaskedTextInput-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import 4 | #import 5 | #import 6 | #import 7 | #import 8 | #import 9 | -------------------------------------------------------------------------------- /apps/example/ios/MaskedTextInputExample.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | indent_style = space 10 | indent_size = 2 11 | 12 | end_of_line = lf 13 | charset = utf-8 14 | trim_trailing_whitespace = true 15 | insert_final_newline = true 16 | -------------------------------------------------------------------------------- /apps/example/src/navigation/Root/types.ts: -------------------------------------------------------------------------------- 1 | import type RootStack from "./index"; 2 | import type { StaticParamList } from "@react-navigation/native"; 3 | 4 | export type RootStackParamList = StaticParamList; 5 | 6 | declare global { 7 | namespace ReactNavigation { 8 | interface RootParamList extends RootStackParamList {} 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /package/jest/index.js: -------------------------------------------------------------------------------- 1 | import { TextInput } from "react-native"; 2 | 3 | export const AFFINITY_CALCULATION_STRATEGY = { 4 | WHOLE_STRING: 0, 5 | PREFIX: 1, 6 | CAPACITY: 2, 7 | EXTRACTED_VALUE_CAPACITY: 3, 8 | }; 9 | 10 | const mock = { 11 | MaskedTextInput: TextInput, 12 | AFFINITY_CALCULATION_STRATEGY, 13 | }; 14 | 15 | module.exports = mock; 16 | -------------------------------------------------------------------------------- /apps/example/src/screens/Date/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from "react-native"; 2 | 3 | const styles = StyleSheet.create({ 4 | container: { 5 | flexShrink: 0, 6 | }, 7 | contentContainer: { 8 | paddingVertical: 20, 9 | flex: 1, 10 | justifyContent: "center", 11 | alignItems: "center", 12 | }, 13 | }); 14 | 15 | export default styles; 16 | -------------------------------------------------------------------------------- /apps/example/src/screens/IBAN/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from "react-native"; 2 | 3 | const styles = StyleSheet.create({ 4 | container: { 5 | flexShrink: 0, 6 | }, 7 | contentContainer: { 8 | paddingVertical: 20, 9 | flex: 1, 10 | justifyContent: "center", 11 | alignItems: "center", 12 | }, 13 | }); 14 | 15 | export default styles; 16 | -------------------------------------------------------------------------------- /apps/example/src/screens/ControlledInput/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from "react-native"; 2 | 3 | const styles = StyleSheet.create({ 4 | container: { 5 | flexShrink: 0, 6 | }, 7 | contentContainer: { 8 | paddingVertical: 20, 9 | flex: 1, 10 | justifyContent: "center", 11 | alignItems: "center", 12 | }, 13 | }); 14 | 15 | export default styles; 16 | -------------------------------------------------------------------------------- /apps/example/src/screens/CustomNotations/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from "react-native"; 2 | 3 | const styles = StyleSheet.create({ 4 | container: { 5 | flexShrink: 0, 6 | }, 7 | contentContainer: { 8 | paddingVertical: 20, 9 | flex: 1, 10 | justifyContent: "center", 11 | alignItems: "center", 12 | }, 13 | }); 14 | 15 | export default styles; 16 | -------------------------------------------------------------------------------- /apps/example/src/screens/ValidationRegEx/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from "react-native"; 2 | 3 | const styles = StyleSheet.create({ 4 | container: { 5 | flexShrink: 0, 6 | }, 7 | contentContainer: { 8 | paddingVertical: 20, 9 | flex: 1, 10 | justifyContent: "center", 11 | alignItems: "center", 12 | }, 13 | }); 14 | 15 | export default styles; 16 | -------------------------------------------------------------------------------- /package/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { AFFINITY_CALCULATION_STRATEGY } from "./enums"; 2 | import MaskedTextInput from "./native/views/MaskedTextInput"; 3 | 4 | import type { MaskedTextInputProps, MaskedTextInputRef } from "./types"; 5 | 6 | export type { MaskedTextInputProps, MaskedTextInputRef }; 7 | 8 | export { AFFINITY_CALCULATION_STRATEGY }; 9 | 10 | export { MaskedTextInput }; 11 | -------------------------------------------------------------------------------- /package/src/web/model/utils.ts: -------------------------------------------------------------------------------- 1 | import type { Ellipsis, StateType } from "./types"; 2 | import type { Notation } from "../../types"; 3 | 4 | export const getCharacterTypeString = ( 5 | state?: StateType | Notation | Ellipsis, 6 | ): string => { 7 | if (!state) { 8 | return "[?]"; 9 | } 10 | 11 | return "name" in state ? state.typeString : `[${state.character}]`; 12 | }; 13 | -------------------------------------------------------------------------------- /apps/example/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 9 | 10 | -------------------------------------------------------------------------------- /apps/example/android/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { includeBuild("../node_modules/@react-native/gradle-plugin") } 2 | plugins { id("com.facebook.react.settings") } 3 | extensions.configure(com.facebook.react.ReactSettingsExtension){ ex -> ex.autolinkLibrariesFromCommand() } 4 | include ':app' 5 | rootProject.name = 'MaskedTextInputExample' 6 | includeBuild('../node_modules/@react-native/gradle-plugin') 7 | -------------------------------------------------------------------------------- /apps/example/src/screens/RNTextInput/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from "react-native"; 2 | 3 | const styles = StyleSheet.create({ 4 | container: { 5 | flexShrink: 0, 6 | }, 7 | contentContainer: { 8 | flex: 1, 9 | justifyContent: "center", 10 | alignItems: "center", 11 | }, 12 | textInput: { 13 | marginBottom: 8, 14 | }, 15 | }); 16 | 17 | export default styles; 18 | -------------------------------------------------------------------------------- /apps/example/src/navigation/screenNames.ts: -------------------------------------------------------------------------------- 1 | const enum ScreenNames { 2 | Main = "Main", 3 | Date = "Date", 4 | RNTextInput = "RNTextInput", 5 | PhoneInput = "PhoneInput", 6 | CustomNotations = "CustomNotations", 7 | IBAN = "IBAN", 8 | ControlledInput = "ControlledInput", 9 | AllowedKeys = "AllowedKeys", 10 | ValidationRegex = "ValidationRegex", 11 | } 12 | 13 | export default ScreenNames; 14 | -------------------------------------------------------------------------------- /apps/example/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /apps/example/src/screens/Phone/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from "react-native"; 2 | 3 | const styles = StyleSheet.create({ 4 | container: { 5 | flexShrink: 0, 6 | }, 7 | contentContainer: { 8 | paddingVertical: 20, 9 | flex: 1, 10 | justifyContent: "center", 11 | alignItems: "center", 12 | }, 13 | button: { 14 | marginBottom: 8, 15 | }, 16 | }); 17 | 18 | export default styles; 19 | -------------------------------------------------------------------------------- /e2e/.maestro/clear-text.yaml: -------------------------------------------------------------------------------- 1 | appId: "com.maskedtextinputexample" 2 | --- 3 | - launchApp 4 | - tapOn: 5 | id: "phone-input" 6 | - tapOn: "+1 (000) 000-0000" 7 | - inputText: 1 8 | - assertVisible: "formatted value +1 (" 9 | - tapOn: 10 | point: "50%,37%" 11 | - tapOn: "Clear text" 12 | - assertVisible: "+1 (000) 000-0000" 13 | - tapOn: "+1 (000) 000-0000" 14 | - inputText: 1 15 | - assertVisible: "formatted value +1 (" 16 | -------------------------------------------------------------------------------- /lefthook.yml: -------------------------------------------------------------------------------- 1 | pre-commit: 2 | parallel: true 3 | commands: 4 | lint: 5 | glob: "*.{js,ts,jsx,tsx}" 6 | run: npx eslint {staged_files} 7 | types: 8 | glob: "*.{js,ts, jsx, tsx}" 9 | run: npx tsc --noEmit 10 | format: 11 | run: yarn prettier '**/*' --ignore-unknown --check 12 | commit-msg: 13 | parallel: true 14 | commands: 15 | commitlint: 16 | run: npx commitlint --edit 17 | -------------------------------------------------------------------------------- /apps/example/src/screens/AllowedKeys/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from "react-native"; 2 | 3 | const styles = StyleSheet.create({ 4 | container: { 5 | flexShrink: 0, 6 | }, 7 | contentContainer: { 8 | paddingVertical: 20, 9 | flex: 1, 10 | justifyContent: "center", 11 | alignItems: "center", 12 | }, 13 | allowedKeysInput: { 14 | marginBottom: 20, 15 | }, 16 | }); 17 | 18 | export default styles; 19 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | package/lib/ 2 | **/node_modules/ 3 | 4 | .yarn/* 5 | 6 | package/android/build/* 7 | 8 | apps/example/android/build/* 9 | apps/example/android/app/build/* 10 | apps/example/android/.idea/* 11 | apps/example/android/.gradle/* 12 | apps/example/android/.kotlin/* 13 | apps/example/ios/build/ 14 | apps/example/ios/Pods/* 15 | apps/example/android/app/build/* 16 | apps/example/android/app/.cxx/* 17 | 18 | apps/example/vendor/* -------------------------------------------------------------------------------- /package/ios/.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | # trailing comma is forced by swiftformat 3 | - trailing_comma 4 | - opening_brace 5 | 6 | line_length: 7 | warning: 120 8 | ignores_urls: true 9 | 10 | type_name: 11 | min_length: 3 12 | max_length: 50 13 | 14 | opening_brace: 15 | # this is controlled by `swiftformat` and we can not change config of formatter 16 | ignore_multiline_statement_conditions: true 17 | 18 | excluded: 19 | - Pods 20 | -------------------------------------------------------------------------------- /package/src/web/hooks/useMaskedTextInputListener/types.ts: -------------------------------------------------------------------------------- 1 | import type { MaskedTextInputOwnProps } from "../../../types"; 2 | import type { 3 | NativeSyntheticEvent, 4 | TextInputChangeEventData, 5 | TextInputFocusEventData, 6 | } from "react-native"; 7 | 8 | export type Props = MaskedTextInputOwnProps & { 9 | onChange?: (event: NativeSyntheticEvent) => void; 10 | onFocus?: (event: NativeSyntheticEvent) => void; 11 | }; 12 | -------------------------------------------------------------------------------- /e2e/.maestro/controlled-input.yaml: -------------------------------------------------------------------------------- 1 | appId: "com.maskedtextinputexample" 2 | --- 3 | - launchApp 4 | - tapOn: 5 | id: "controlled-text-input" 6 | - assertVisible: "+1 (111" 7 | - assertVisible: "formatted value +1 (111" 8 | - tapOn: "+1 (111" 9 | - inputText: 123456789 10 | - assertVisible: "extracted value 1111234567" 11 | - assertVisible: "formatted value +1 (111) 123-4567" 12 | - assertVisible: "+1 (111) 123-4567" 13 | - eraseText: 17 14 | - assertVisible: "+1 (000) 000 0000" 15 | -------------------------------------------------------------------------------- /package/src/web/helper/RTLCaretStringIterator.ts: -------------------------------------------------------------------------------- 1 | import CaretStringIterator from "./CaretStringIterator"; 2 | 3 | import type CaretString from "../model/CaretString"; 4 | 5 | class RTLCaretStringIterator extends CaretStringIterator { 6 | constructor(caretString: CaretString) { 7 | super(caretString); 8 | } 9 | 10 | insertionAffectsCaret: () => boolean = () => 11 | this.currentIndex <= this.caretString.caretPosition; 12 | } 13 | 14 | export default RTLCaretStringIterator; 15 | -------------------------------------------------------------------------------- /apps/example/android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | -------------------------------------------------------------------------------- /package/src/web/helper/AutocompletionStack.ts: -------------------------------------------------------------------------------- 1 | import type { Next } from "../model/types"; 2 | 3 | class AutocompletionStack extends Array { 4 | push(item: Next | null): number { 5 | if (item === null) { 6 | this.length = 0; 7 | 8 | return 0; 9 | } 10 | 11 | return super.push(item); 12 | } 13 | 14 | pop(): Next { 15 | return super.pop()!; 16 | } 17 | 18 | empty(): boolean { 19 | return this.length === 0; 20 | } 21 | } 22 | 23 | export default AutocompletionStack; 24 | -------------------------------------------------------------------------------- /apps/example/src/components/Button/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from "react-native"; 2 | 3 | export const ANDROID_PRESSABLE_RIPPLE = { borderless: false }; 4 | 5 | export default StyleSheet.create({ 6 | container: { 7 | marginHorizontal: 8, 8 | minWidth: "90%", 9 | }, 10 | buttonContainer: { 11 | alignItems: "center", 12 | backgroundColor: "#EAB68F", 13 | borderRadius: 6, 14 | padding: 12, 15 | }, 16 | buttonText: { 17 | color: "black", 18 | fontSize: 14, 19 | }, 20 | }); 21 | -------------------------------------------------------------------------------- /apps/example/src/components/replicas/touchables/index.ts: -------------------------------------------------------------------------------- 1 | import { memo } from "react"; 2 | import { Platform } from "react-native"; 3 | import { RectButton } from "react-native-gesture-handler"; 4 | 5 | import TouchableHighlight from "./TouchableHighlight"; 6 | 7 | import type { TouchableComponentType } from "./types"; 8 | 9 | const Touchable = Platform.select({ 10 | ios: TouchableHighlight as TouchableComponentType, 11 | default: memo(RectButton) as TouchableComponentType, 12 | }); 13 | 14 | export default Touchable; 15 | -------------------------------------------------------------------------------- /package/src/web/model/state/State.ts: -------------------------------------------------------------------------------- 1 | import type { Next } from "../types"; 2 | 3 | abstract class State { 4 | child: State | null = null; 5 | 6 | constructor(child: State | null) { 7 | this.child = child; 8 | } 9 | 10 | abstract accept: (char: string) => Next | null; 11 | 12 | nextState = (): State => this.child!; 13 | 14 | autocomplete = (): Next | null => null; 15 | 16 | toString = (): string => 17 | `BASE -> ${this.child ? this.child.toString() : "null"}`; 18 | } 19 | 20 | export default State; 21 | -------------------------------------------------------------------------------- /e2e/.maestro/validation-regex.yaml: -------------------------------------------------------------------------------- 1 | appId: "com.maskedtextinputexample" 2 | --- 3 | - launchApp 4 | - tapOn: 5 | id: "validation-regex" 6 | - tapOn: "22.11" 7 | - assertVisible: "22.11" 8 | - eraseText: 5 9 | - inputText: "aa11.11" 10 | - assertVisible: "formatted value 11.11" 11 | - assertVisible: "extracted value 1111" 12 | - assertVisible: "11.11" 13 | - eraseText: 5 14 | - inputText: "123456,.78" 15 | - assertVisible: "123456.78" 16 | - assertVisible: "extracted value 12345678" 17 | - assertVisible: "formatted value 123456.78" 18 | -------------------------------------------------------------------------------- /.github/workflows/build-library.yml: -------------------------------------------------------------------------------- 1 | name: Build Library 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | build-library: 13 | runs-on: ubuntu-latest 14 | defaults: 15 | run: 16 | working-directory: package 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | 21 | - name: Setup 22 | uses: ./.github/actions/setup 23 | 24 | - name: Build package 25 | run: yarn prepare 26 | -------------------------------------------------------------------------------- /apps/example/ios/.xcode.env: -------------------------------------------------------------------------------- 1 | # This `.xcode.env` file is versioned and is used to source the environment 2 | # used when running script phases inside Xcode. 3 | # To customize your local environment, you can create an `.xcode.env.local` 4 | # file that is not versioned. 5 | 6 | # NODE_BINARY variable contains the PATH to the node executable. 7 | # 8 | # Customize the NODE_BINARY variable here. 9 | # For example, to use nvm with brew, add the following line 10 | # . "$(brew --prefix nvm)/nvm.sh" --no-use 11 | export NODE_BINARY=$(command -v node) 12 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: unit-tests 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths: 7 | - ".github/workflows/tests.yml" 8 | pull_request: 9 | branches: [main] 10 | paths: 11 | - ".github/workflows/tests.yml" 12 | 13 | jobs: 14 | test: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | 20 | - name: Setup 21 | uses: ./.github/actions/setup 22 | 23 | - name: Run unit tests 24 | run: yarn test --maxWorkers=2 --coverage 25 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | lint: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | 18 | - name: Setup 19 | uses: ./.github/actions/setup 20 | 21 | - name: Lint files 22 | run: yarn lint 23 | 24 | - name: Typecheck files 25 | run: yarn typecheck 26 | - name: Check formatting 27 | run: yarn format --check 28 | -------------------------------------------------------------------------------- /apps/example/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # You may use http://rbenv.org/ or https://rvm.io/ to install and use this version 4 | ruby ">= 2.6.10" 5 | 6 | # Exclude problematic versions of cocoapods and activesupport that causes build failures. 7 | gem 'cocoapods', '>= 1.13', '!= 1.15.0', '!= 1.15.1' 8 | gem 'activesupport', '>= 6.1.7.5', '!= 7.1.0' 9 | gem 'xcodeproj', '< 1.26.0' 10 | gem 'concurrent-ruby', '< 1.3.4' 11 | # Ruby 3.4.0 has removed some libraries from the standard library. 12 | gem 'bigdecimal' 13 | gem 'logger' 14 | gem 'benchmark' 15 | gem 'mutex_m' -------------------------------------------------------------------------------- /apps/example/src/screens/Main/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from "react-native"; 2 | 3 | const styles = StyleSheet.create({ 4 | container: { 5 | flexShrink: 0, 6 | }, 7 | contentContainer: { 8 | paddingVertical: 20, 9 | flex: 1, 10 | }, 11 | leftAction: { width: 50, height: 50, backgroundColor: "crimson" }, 12 | rightAction: { width: 50, height: 50, backgroundColor: "purple" }, 13 | swipeable: { 14 | height: 50, 15 | marginBottom: 20, 16 | backgroundColor: "papayawhip", 17 | alignItems: "center", 18 | }, 19 | }); 20 | 21 | export default styles; 22 | -------------------------------------------------------------------------------- /package/src/index.web.tsx: -------------------------------------------------------------------------------- 1 | import { AFFINITY_CALCULATION_STRATEGY } from "./enums"; 2 | import AdvancedTextInputMaskListener from "./web/AdvancedTextInputMaskListener"; 3 | import useMaskedTextInputListener from "./web/hooks/useMaskedTextInputListener"; 4 | import MaskedTextInput from "./web/views/MaskedTextInput"; 5 | 6 | import type { MaskedTextInputProps } from "./types"; 7 | 8 | export type { MaskedTextInputProps }; 9 | 10 | export { AFFINITY_CALCULATION_STRATEGY }; 11 | 12 | export { 13 | AdvancedTextInputMaskListener, 14 | MaskedTextInput, 15 | useMaskedTextInputListener, 16 | }; 17 | -------------------------------------------------------------------------------- /apps/example/src/screens/RNTextInput/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { KeyboardAwareScrollView } from "react-native-keyboard-controller"; 3 | 4 | import BaseTextInput from "../../components/BaseTextInput"; 5 | 6 | import styles from "./styles"; 7 | 8 | const RNTextInput = () => { 9 | return ( 10 | 14 | 15 | 16 | ); 17 | }; 18 | 19 | export default RNTextInput; 20 | -------------------------------------------------------------------------------- /apps/example/src/components/BaseTextInput/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from "react-native"; 2 | 3 | export const FOCUSED_BORDER_COLOR = "#2a41cb"; 4 | export const DEFAULT_BORDER_COLOR = "#767676"; 5 | export const PLACEHOLDER_COLOR = "#767676"; 6 | export const DEFAULT_BACKGROUND_COLOR = "#ffffff"; 7 | export const PRESSED_BACKGROUND_COLOR = "#ebeded"; 8 | 9 | export default StyleSheet.create({ 10 | input: { 11 | borderWidth: 1, 12 | paddingHorizontal: 16, 13 | borderRadius: 8, 14 | marginBottom: 16, 15 | justifyContent: "center", 16 | color: "#000", 17 | width: "90%", 18 | height: 50, 19 | }, 20 | }); 21 | -------------------------------------------------------------------------------- /apps/example/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext { 3 | buildToolsVersion = "36.0.0" 4 | minSdkVersion = 24 5 | compileSdkVersion = 36 6 | targetSdkVersion = 36 7 | ndkVersion = "27.1.12297006" 8 | kotlinVersion = "2.2.20" 9 | } 10 | repositories { 11 | google() 12 | mavenCentral() 13 | } 14 | dependencies { 15 | classpath("com.android.tools.build:gradle") 16 | classpath("com.facebook.react:react-native-gradle-plugin") 17 | classpath("org.jetbrains.kotlin:kotlin-gradle-plugin") 18 | } 19 | } 20 | 21 | apply plugin: "com.facebook.react.rootproject" 22 | -------------------------------------------------------------------------------- /apps/example/src/screens/IBAN/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { KeyboardAwareScrollView } from "react-native-keyboard-controller"; 3 | 4 | import TextInput from "../../components/TextInput"; 5 | 6 | import styles from "./styles"; 7 | 8 | const IBAN = () => { 9 | return ( 10 | 14 | 18 | 19 | ); 20 | }; 21 | 22 | export default IBAN; 23 | -------------------------------------------------------------------------------- /package/ios/extentions/Notations.swift: -------------------------------------------------------------------------------- 1 | import ForkInputMask 2 | import Foundation 3 | 4 | extension Dictionary where Key == String, Value == Any { 5 | func toNotation() -> Notation? { 6 | guard let characterString = self["character"] as? String, 7 | let character = characterString.first, 8 | let characterSetString = self["characterSet"] as? String, 9 | let isOptional = self["isOptional"] as? Bool 10 | else { 11 | return nil 12 | } 13 | 14 | let characterSet = CharacterSet(charactersIn: characterSetString) 15 | return Notation(character: character, characterSet: characterSet, isOptional: isOptional) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.github/workflows/verify-android.yml: -------------------------------------------------------------------------------- 1 | name: 📱 Validate Android 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - ".github/workflows/verify-android.yml" 9 | - "package/android/**" 10 | pull_request: 11 | branches: 12 | - main 13 | paths: 14 | - ".github/workflows/verify-android.yml" 15 | - "package/android/**" 16 | 17 | jobs: 18 | ktlint: 19 | runs-on: ubuntu-latest 20 | name: 🔎 Kotlin Lint 21 | steps: 22 | - uses: actions/checkout@v4 23 | - uses: touchlab-lab/ktlint-action-setup@1.0.0 24 | with: 25 | ktlint_version: 1.3.1 26 | - run: ktlint "package/android/src/**/*.kt" 27 | -------------------------------------------------------------------------------- /apps/example/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { GestureHandlerRootView } from "react-native-gesture-handler"; 3 | import { KeyboardProvider } from "react-native-keyboard-controller"; 4 | import { 5 | SafeAreaProvider, 6 | initialWindowMetrics, 7 | } from "react-native-safe-area-context"; 8 | 9 | import Navigation from "./navigation"; 10 | 11 | export default function App() { 12 | return ( 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /package/android/src/main/java/com/maskedtextinput/mappers/AffinityCalculationStrategyMapper.kt: -------------------------------------------------------------------------------- 1 | package com.maskedtextinput.mappers 2 | 3 | import com.redmadrobot.inputmask.helper.AffinityCalculationStrategy 4 | 5 | class AffinityCalculationStrategyMapper { 6 | fun fromInt(value: Int): AffinityCalculationStrategy = 7 | when (value) { 8 | 0 -> AffinityCalculationStrategy.WHOLE_STRING 9 | 1 -> AffinityCalculationStrategy.PREFIX 10 | 2 -> AffinityCalculationStrategy.CAPACITY 11 | 3 -> AffinityCalculationStrategy.EXTRACTED_VALUE_CAPACITY 12 | else -> throw IllegalArgumentException("Invalid value for AffinityCalculationStrategy: $value") 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /package/src/web/model/CaretString.ts: -------------------------------------------------------------------------------- 1 | import type { CaretGravity } from "./types"; 2 | 3 | class CaretString { 4 | string: string; 5 | caretPosition: number; 6 | caretGravity: CaretGravity; 7 | 8 | constructor( 9 | string: string, 10 | caretPosition: number, 11 | caretGravity: CaretGravity, 12 | ) { 13 | this.string = string; 14 | this.caretPosition = caretPosition; 15 | this.caretGravity = caretGravity; 16 | } 17 | 18 | reversed(): CaretString { 19 | return new CaretString( 20 | this.string.split("").reverse().join(""), 21 | this.string.length - this.caretPosition, 22 | this.caretGravity, 23 | ); 24 | } 25 | } 26 | 27 | export default CaretString; 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "" 5 | labels: Feature 6 | assignees: IvanIhnatsiuk 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /apps/example/src/screens/Main/index.tsx: -------------------------------------------------------------------------------- 1 | import { useNavigation } from "@react-navigation/native"; 2 | import React from "react"; 3 | import { ScrollView } from "react-native"; 4 | 5 | import MenuItem from "../../components/MenuItem"; 6 | 7 | import { MENU_ITEMS } from "./constants"; 8 | import styles from "./styles"; 9 | 10 | const Main = () => { 11 | const { navigate } = useNavigation(); 12 | 13 | return ( 14 | 18 | {MENU_ITEMS.map((item) => ( 19 | 20 | ))} 21 | 22 | ); 23 | }; 24 | 25 | export default Main; 26 | -------------------------------------------------------------------------------- /package/android/src/main/java/com/maskedtextinput/MaskedTextInputPackage.kt: -------------------------------------------------------------------------------- 1 | package com.maskedtextinput 2 | 3 | import com.facebook.react.ReactPackage 4 | import com.facebook.react.bridge.NativeModule 5 | import com.facebook.react.bridge.ReactApplicationContext 6 | import com.facebook.react.uimanager.ViewManager 7 | import com.maskedtextinput.managers.AdvancedTextInputMaskDecoratorViewManager 8 | 9 | class MaskedTextInputPackage : ReactPackage { 10 | override fun createNativeModules(reactContext: ReactApplicationContext): List = emptyList() 11 | 12 | override fun createViewManagers(reactContext: ReactApplicationContext): List> = 13 | listOf(AdvancedTextInputMaskDecoratorViewManager(reactContext)) 14 | } 15 | -------------------------------------------------------------------------------- /apps/example/src/screens/ControlledInput/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { KeyboardAwareScrollView } from "react-native-keyboard-controller"; 3 | 4 | import TextInput from "../../components/TextInput"; 5 | 6 | import styles from "./styles"; 7 | 8 | const ControlledInput = () => { 9 | return ( 10 | 14 | 21 | 22 | ); 23 | }; 24 | 25 | export default ControlledInput; 26 | -------------------------------------------------------------------------------- /.github/workflows/build-android.yml: -------------------------------------------------------------------------------- 1 | name: 🤖 Build Android 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - ".github/workflows/build-android.yml" 9 | - "package/android/**" 10 | - "apps/example/android/**" 11 | - "yarn.lock" 12 | - "apps/example/yarn.lock" 13 | pull_request: 14 | paths: 15 | - ".github/workflows/build-android.yml" 16 | - "package/android/**" 17 | - "apps/example/android/**" 18 | - "yarn.lock" 19 | - "apps/example/yarn.lock" 20 | 21 | jobs: 22 | build-android: 23 | runs-on: ubuntu-latest 24 | steps: 25 | - name: Checkout 26 | uses: actions/checkout@v4 27 | 28 | - name: Build example for Android 29 | uses: ./.github/actions/build-android 30 | -------------------------------------------------------------------------------- /package/ios/extentions/AffinityCalculationStrategy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AffinityCalculationStrategy.swift 3 | // react-native-advanced-input-mask 4 | // 5 | // Created by Ivan Ignathuk on 08/10/2024. 6 | // 7 | 8 | import ForkInputMask 9 | import Foundation 10 | 11 | extension AffinityCalculationStrategy { 12 | static func forNumber(number: NSNumber?) -> AffinityCalculationStrategy { 13 | switch number { 14 | case 0: 15 | return AffinityCalculationStrategy.wholeString 16 | case 1: 17 | return AffinityCalculationStrategy.prefix 18 | case 2: 19 | return AffinityCalculationStrategy.capacity 20 | case 3: 21 | return AffinityCalculationStrategy.extractedValueCapacity 22 | default: 23 | return AffinityCalculationStrategy.wholeString 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.github/workflows/build-web.yml: -------------------------------------------------------------------------------- 1 | name: 🌐 Build Web 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - ".github/workflows/build-web.yml" 9 | - "package/src/**" 10 | - "apps/example/**" 11 | - "yarn.lock" 12 | - "apps/example/yarn.lock" 13 | pull_request: 14 | paths: 15 | - ".github/workflows/build-web.yml" 16 | - "package/src/**" 17 | - "apps/example/**" 18 | - "yarn.lock" 19 | - "apps/example/yarn.lock" 20 | 21 | jobs: 22 | build-web: 23 | runs-on: ubuntu-latest 24 | steps: 25 | - name: Checkout 26 | uses: actions/checkout@v4 27 | 28 | - name: Setup 29 | uses: ./.github/actions/setup 30 | 31 | - name: Build example for Web 32 | run: cd apps/example && yarn build:web 33 | -------------------------------------------------------------------------------- /package/android/src/newarch/java/com/maskedtextinput/AdvancedTextInputMaskDecoratorViewManagerSpec.kt: -------------------------------------------------------------------------------- 1 | package com.maskedtextinput 2 | 3 | import android.view.View 4 | import com.facebook.react.uimanager.SimpleViewManager 5 | import com.facebook.react.uimanager.ViewManagerDelegate 6 | import com.facebook.react.viewmanagers.AdvancedTextInputMaskDecoratorViewManagerDelegate 7 | import com.facebook.react.viewmanagers.AdvancedTextInputMaskDecoratorViewManagerInterface 8 | 9 | abstract class AdvancedTextInputMaskDecoratorViewManagerSpec : 10 | SimpleViewManager(), 11 | AdvancedTextInputMaskDecoratorViewManagerInterface { 12 | private val mDelegate: ViewManagerDelegate = AdvancedTextInputMaskDecoratorViewManagerDelegate(this) 13 | 14 | override fun getDelegate(): ViewManagerDelegate? = mDelegate 15 | } 16 | -------------------------------------------------------------------------------- /package/android/src/main/java/com/maskedtextinput/mappers/NotationMapper.kt: -------------------------------------------------------------------------------- 1 | package com.maskedtextinput.mappers 2 | 3 | import com.facebook.react.bridge.ReadableArray 4 | import com.redmadrobot.inputmask.model.Notation 5 | 6 | class NotationMapper { 7 | fun fromReadableArray(readableArray: ReadableArray): List { 8 | val list = mutableListOf() 9 | for (i in 0 until readableArray.size()) { 10 | val map = readableArray.getMap(i) 11 | if (map != null) { 12 | val char = map.getString("character")?.first() 13 | val characterSet = map.getString("characterSet") 14 | val isOptional = map.getBoolean("isOptional") 15 | if (char != null && characterSet != null) { 16 | list.add(Notation(char, characterSet, isOptional)) 17 | } 18 | } 19 | } 20 | return list 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | release: 5 | types: [created] 6 | workflow_dispatch: 7 | 8 | jobs: 9 | publish: 10 | defaults: 11 | run: 12 | working-directory: package 13 | runs-on: ubuntu-latest 14 | permissions: 15 | contents: read 16 | id-token: write 17 | steps: 18 | - uses: actions/checkout@v4 19 | 20 | # Setup .npmrc file to publish to npm 21 | - uses: actions/setup-node@v4 22 | with: 23 | node-version: "18.x" 24 | registry-url: "https://registry.npmjs.org" 25 | cache: yarn 26 | - run: yarn install --frozen-lockfile 27 | 28 | - name: Publish package 29 | working-directory: package 30 | run: yarn prepack && npm publish --provenance 31 | env: 32 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 33 | -------------------------------------------------------------------------------- /apps/example/src/screens/ValidationRegEx/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { KeyboardAwareScrollView } from "react-native-keyboard-controller"; 3 | 4 | import TextInput from "../../components/TextInput"; 5 | 6 | import styles from "./styles"; 7 | 8 | const ValidationRegex = () => { 9 | return ( 10 | 14 | 23 | 24 | ); 25 | }; 26 | 27 | export default ValidationRegex; 28 | -------------------------------------------------------------------------------- /apps/example/src/components/MenuItem/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from "react-native"; 2 | 3 | export default StyleSheet.create({ 4 | container: { 5 | overflow: "hidden", 6 | backgroundColor: "#fff", 7 | borderRadius: 12, 8 | shadowColor: "#000", 9 | shadowOffset: { 10 | width: 15, 11 | height: 15, 12 | }, 13 | shadowRadius: 15, 14 | shadowOpacity: 0.1, 15 | boxShadow: "0 0 10px rgba(0, 0, 0, 0.1)", 16 | marginVertical: 8, 17 | elevation: 20, 18 | marginHorizontal: 16, 19 | }, 20 | contentContainer: { 21 | padding: 16, 22 | justifyContent: "space-between", 23 | flexDirection: "row", 24 | alignItems: "center", 25 | }, 26 | title: { 27 | fontSize: 16, 28 | color: "#333", 29 | fontWeight: "600", 30 | }, 31 | emoji: { 32 | color: "black", 33 | fontSize: 20, 34 | }, 35 | }); 36 | -------------------------------------------------------------------------------- /.github/actions/setup/action.yml: -------------------------------------------------------------------------------- 1 | name: Setup 2 | description: Setup Node.js and install dependencies 3 | 4 | runs: 5 | using: composite 6 | steps: 7 | - name: Setup Node.js 8 | uses: actions/setup-node@v4 9 | with: 10 | node-version-file: .nvmrc 11 | 12 | - name: Cache dependencies 13 | id: yarn-cache 14 | uses: actions/cache@v4 15 | with: 16 | path: | 17 | **/node_modules 18 | .yarn/install-state.gz 19 | key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }}-${{ hashFiles('**/package.json', '!node_modules/**') }} 20 | restore-keys: | 21 | ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }} 22 | ${{ runner.os }}-yarn- 23 | 24 | - name: Install dependencies 25 | if: steps.yarn-cache.outputs.cache-hit != 'true' 26 | run: yarn install --immutable 27 | shell: bash 28 | -------------------------------------------------------------------------------- /apps/example/metro.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | const { makeMetroConfig } = require("@rnx-kit/metro-config"); 4 | 5 | const root = path.resolve(__dirname, "../.."); 6 | const pack = require("../../package/package.json"); 7 | const modules = Object.keys(pack.peerDependencies); 8 | 9 | const extraConfig = { 10 | watchFolders: [root], 11 | transformer: { 12 | getTransformOptions: async () => ({ 13 | transform: { 14 | experimentalImportSupport: false, 15 | inlineRequires: true, 16 | }, 17 | }), 18 | }, 19 | resolver: { 20 | unstable_enableSymlinks: true, 21 | extraNodeModules: modules.reduce((acc, name) => { 22 | acc[name] = path.join(__dirname, "node_modules", name); 23 | 24 | return acc; 25 | }, {}), 26 | }, 27 | }; 28 | 29 | const metroConfig = makeMetroConfig(extraConfig); 30 | 31 | module.exports = metroConfig; 32 | -------------------------------------------------------------------------------- /apps/example/babel.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('@babel/core').TransformOptions} */ 2 | module.exports = function (api) { 3 | api.cache(true); 4 | 5 | return { 6 | presets: ["module:@react-native/babel-preset"], 7 | overrides: [ 8 | { 9 | exclude: /\/node_modules\//, 10 | plugins: [ 11 | [ 12 | "module-resolver", 13 | { 14 | extensions: [".tsx", ".ts", ".js", ".json"], 15 | alias: { 16 | "react-native-advanced-input-mask": "../../package/src", 17 | }, 18 | }, 19 | ], 20 | ], 21 | }, 22 | ], 23 | plugins: [ 24 | [ 25 | "babel-plugin-react-compiler", 26 | { 27 | target: "19", 28 | }, 29 | ], 30 | "@babel/plugin-proposal-export-namespace-from", 31 | "react-native-worklets/plugin", 32 | ], 33 | }; 34 | }; 35 | -------------------------------------------------------------------------------- /package/android/src/main/java/com/maskedtextinput/listeners/MaskedTextValueListener.kt: -------------------------------------------------------------------------------- 1 | package com.maskedtextinput.listeners 2 | 3 | import com.redmadrobot.inputmask.MaskedTextChangedListener 4 | 5 | class MaskedTextValueListener( 6 | private val onChangeText: (Boolean, String, String, String) -> Unit, 7 | ) : MaskedTextChangedListener.ValueListener { 8 | private var previousFormattedText = "" 9 | private var previousExtractedText = "" 10 | 11 | override fun onTextChanged( 12 | maskFilled: Boolean, 13 | extractedValue: String, 14 | formattedValue: String, 15 | tailPlaceholder: String, 16 | ) { 17 | if (previousFormattedText != formattedValue || previousExtractedText !== extractedValue) { 18 | previousFormattedText = formattedValue 19 | previousExtractedText = extractedValue 20 | onChangeText(maskFilled, extractedValue, formattedValue, tailPlaceholder) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /apps/example/src/components/Button/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { type FC, memo, useMemo } from "react"; 2 | import { type StyleProp, Text, View, type ViewStyle } from "react-native"; 3 | 4 | import Touchable from "../replicas/touchables"; 5 | 6 | import styles from "./styles"; 7 | 8 | import type { TouchableCrossPlatformProps } from "../replicas/touchables/types"; 9 | 10 | type Props = TouchableCrossPlatformProps & { 11 | title: string; 12 | style?: StyleProp; 13 | }; 14 | 15 | const Button: FC = ({ title, style, ...rest }) => { 16 | const containerStyle: StyleProp = useMemo( 17 | () => [styles.container, style], 18 | [style], 19 | ); 20 | 21 | return ( 22 | 23 | 24 | {title} 25 | 26 | 27 | ); 28 | }; 29 | 30 | export default memo(Button); 31 | -------------------------------------------------------------------------------- /package/src/web/model/constants.ts: -------------------------------------------------------------------------------- 1 | import { StateName, type StateType } from "./types"; 2 | 3 | export const OPTIONAL_STATE_TYPES: Record = { 4 | numeric: { 5 | regex: /^\d$/, 6 | name: StateName.numeric, 7 | typeString: "[9]", 8 | }, 9 | literal: { 10 | regex: /^[A-Za-z]$/, 11 | name: StateName.literal, 12 | typeString: "[a]", 13 | }, 14 | alphaNumeric: { 15 | regex: /^[A-Za-z0-9]$/, 16 | name: StateName.alphaNumeric, 17 | typeString: "[-]", 18 | }, 19 | }; 20 | 21 | export const FIXED_STATE_TYPES: Record = { 22 | literal: { 23 | regex: /^[A-Za-z]$/, 24 | name: StateName.literal, 25 | typeString: "[A]", 26 | }, 27 | numeric: { 28 | regex: /^\d$/, 29 | name: StateName.numeric, 30 | typeString: "[0]", 31 | }, 32 | alphaNumeric: { 33 | regex: /^[A-Za-z0-9]$/, 34 | name: StateName.alphaNumeric, 35 | typeString: "[_]", 36 | }, 37 | }; 38 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "paths": { 4 | "react-native-advanced-input-mask": ["./package/src"] 5 | }, 6 | "composite": true, 7 | "rootDir": ".", 8 | "baseUrl": ".", 9 | "allowUnreachableCode": false, 10 | "allowUnusedLabels": false, 11 | "esModuleInterop": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "jsx": "react", 14 | "lib": ["esnext", "dom"], 15 | "module": "esnext", 16 | "moduleResolution": "bundler", 17 | "noFallthroughCasesInSwitch": true, 18 | "noImplicitReturns": true, 19 | "noImplicitUseStrict": false, 20 | "noStrictGenericChecks": false, 21 | "noUncheckedIndexedAccess": true, 22 | "noUnusedLocals": true, 23 | "noUnusedParameters": true, 24 | "resolveJsonModule": true, 25 | "skipLibCheck": true, 26 | "strict": true, 27 | "target": "esnext", 28 | "verbatimModuleSyntax": true 29 | }, 30 | "exclude": ["./package/*/lib"] 31 | } 32 | -------------------------------------------------------------------------------- /.github/workflows/build-ios.yml: -------------------------------------------------------------------------------- 1 | name: 🍏 Build iOS 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - ".github/workflows/build-ios.yml" 9 | - "./github/actions/build-ios/**" 10 | - "package/react-native-advanced-input-mask.podspec" 11 | - "package/ios/**" 12 | - "apps/example/ios/**" 13 | - "yarn.lock" 14 | - "apps/example/yarn.lock" 15 | pull_request: 16 | branches: 17 | - main 18 | paths: 19 | - ".github/workflows/build-ios.yml" 20 | - ".github/actions/build-ios/**" 21 | - "package/react-native-advanced-input-mask.podspec" 22 | - "package/ios/**" 23 | - "apps/example/ios/**" 24 | - "yarn.lock" 25 | - "example/yarn.lock" 26 | 27 | jobs: 28 | build-ios: 29 | runs-on: macos-15 30 | steps: 31 | - name: Checkout 32 | uses: actions/checkout@v4 33 | - name: Run iOS Build Action 34 | uses: ./.github/actions/build-ios 35 | -------------------------------------------------------------------------------- /package/android/src/main/java/com/maskedtextinput/events/ChangeTextEvent.kt: -------------------------------------------------------------------------------- 1 | package com.maskedtextinput.events 2 | 3 | import com.facebook.react.bridge.Arguments 4 | import com.facebook.react.bridge.WritableMap 5 | import com.facebook.react.uimanager.events.Event 6 | 7 | class ChangeTextEvent( 8 | surfaceId: Int, 9 | viewTag: Int, 10 | private val extractedText: String, 11 | private val formattedText: String, 12 | private val tailPlaceholder: String, 13 | private val complete: Boolean, 14 | ) : Event(surfaceId, viewTag) { 15 | override fun getEventName(): String = EventNames.CHANGE_TEXT_EVENT 16 | 17 | override fun getCoalescingKey(): Short = 0 18 | 19 | override fun getEventData(): WritableMap? = 20 | Arguments.createMap().apply { 21 | putString("extracted", extractedText) 22 | putString("formatted", formattedText) 23 | putString("tailPlaceholder", tailPlaceholder) 24 | putBoolean("complete", complete) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /apps/example/ios/Podfile: -------------------------------------------------------------------------------- 1 | # Resolve react_native_pods.rb with node to allow for hoisting 2 | require Pod::Executable.execute_command('node', ['-p', 3 | 'require.resolve( 4 | "react-native/scripts/react_native_pods.rb", 5 | {paths: [process.argv[1]]}, 6 | )', __dir__]).strip 7 | 8 | platform :ios, min_ios_version_supported 9 | prepare_react_native_project! 10 | 11 | target 'MaskedTextInputExample' do 12 | config = use_native_modules! 13 | 14 | use_react_native!( 15 | :path => config[:reactNativePath], 16 | # An absolute path to your application root. 17 | :app_path => "#{Pod::Config.instance.installation_root}/.." 18 | ) 19 | 20 | post_install do |installer| 21 | # https://github.com/facebook/react-native/blob/main/packages/react-native/scripts/react_native_pods.rb#L197-L202 22 | react_native_post_install( 23 | installer, 24 | config[:reactNativePath], 25 | :mac_catalyst_enabled => false, 26 | # :ccache_enabled => true 27 | ) 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /apps/example/src/components/MenuItem/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, useCallback } from "react"; 2 | import { Text, View } from "react-native"; 3 | 4 | import Touchable from "../replicas/touchables"; 5 | 6 | import styles from "./styles"; 7 | 8 | export type Props = { 9 | title: string; 10 | onPress: (info?: T) => void; 11 | info?: T; 12 | testId: string; 13 | emoji: string; 14 | }; 15 | 16 | const MenuItem = (props: Props) => { 17 | const { title, onPress, info, testId, emoji } = props; 18 | 19 | const handlePress = useCallback(() => { 20 | onPress(info); 21 | }, [info, onPress]); 22 | 23 | return ( 24 | 25 | 30 | {title} 31 | {emoji} 32 | 33 | 34 | ); 35 | }; 36 | 37 | export default memo(MenuItem); 38 | -------------------------------------------------------------------------------- /apps/example/src/screens/CustomNotations/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { KeyboardAwareScrollView } from "react-native-keyboard-controller"; 3 | 4 | import TextInput from "../../components/TextInput"; 5 | 6 | import styles from "./styles"; 7 | 8 | const alphaNumericChars = 9 | "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; 10 | 11 | const customNotations = [ 12 | { 13 | character: "$", 14 | characterSet: alphaNumericChars, 15 | isOptional: false, 16 | }, 17 | ]; 18 | 19 | const CustomNotations = () => { 20 | return ( 21 | 25 | 32 | 33 | ); 34 | }; 35 | 36 | export default CustomNotations; 37 | -------------------------------------------------------------------------------- /apps/example/src/screens/Date/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { KeyboardAwareScrollView } from "react-native-keyboard-controller"; 3 | 4 | import TextInput from "../../components/TextInput"; 5 | 6 | import styles from "./styles"; 7 | 8 | const affineFormats = ["[00]{/}[00]{/}[00]"]; 9 | 10 | const DateScreen = () => { 11 | const defaultValue = React.useMemo(() => new Date().toISOString(), []); 12 | 13 | return ( 14 | 18 | 29 | 30 | ); 31 | }; 32 | 33 | export default DateScreen; 34 | -------------------------------------------------------------------------------- /package/src/web/model/state/FixedState.ts: -------------------------------------------------------------------------------- 1 | import State from "./State"; 2 | 3 | import type { Next } from "../types"; 4 | 5 | class FixedState extends State { 6 | ownCharacter: string; 7 | 8 | constructor(child: State, ownCharacter: string) { 9 | super(child); 10 | this.ownCharacter = ownCharacter; 11 | } 12 | 13 | accept: (char: string) => Next = (char) => 14 | char === this.ownCharacter 15 | ? { 16 | state: this.nextState(), 17 | pass: true, 18 | insert: char, 19 | value: char, 20 | } 21 | : { 22 | state: this.nextState(), 23 | insert: this.ownCharacter, 24 | value: this.ownCharacter, 25 | pass: false, 26 | }; 27 | 28 | autocomplete: () => Next = () => ({ 29 | state: this.nextState(), 30 | insert: this.ownCharacter, 31 | value: this.ownCharacter, 32 | pass: false, 33 | }); 34 | 35 | toString = () => 36 | `{${this.ownCharacter}} -> ${this.child?.toString() ?? "null"} `; 37 | } 38 | 39 | export default FixedState; 40 | -------------------------------------------------------------------------------- /package/src/web/model/types.ts: -------------------------------------------------------------------------------- 1 | import type CaretString from "./CaretString"; 2 | import type State from "./state/State"; 3 | 4 | export type Next = { 5 | state: State; 6 | insert: string | null; 7 | pass: boolean; 8 | value: string | null; 9 | }; 10 | 11 | export type StateType = { 12 | name: StateName; 13 | regex: RegExp; 14 | typeString: string; 15 | }; 16 | 17 | export type Ellipsis = { 18 | name: "ellipsis"; 19 | inheritedType: StateType; 20 | typeString: string; 21 | }; 22 | 23 | export type MaskResult = { 24 | formattedText: CaretString; 25 | extractedValue: string; 26 | affinity: number; 27 | complete: boolean; 28 | tailPlaceholder: string; 29 | reversed(): MaskResult; 30 | }; 31 | 32 | export interface CaretGravity { 33 | autocomplete: boolean; 34 | autoskip: boolean; 35 | type: CaretGravityType; 36 | } 37 | 38 | export const enum CaretGravityType { 39 | Forward, 40 | Backward, 41 | } 42 | 43 | export const enum StateName { 44 | literal = "literal", 45 | numeric = "numeric", 46 | alphaNumeric = "alphaNumeric", 47 | } 48 | -------------------------------------------------------------------------------- /package/src/web/model/state/FreeState.ts: -------------------------------------------------------------------------------- 1 | import State from "./State"; 2 | 3 | import type { Next } from "../types"; 4 | 5 | class FreeState extends State { 6 | ownCharacter: string; 7 | 8 | constructor(child: State, ownCharacter: string) { 9 | super(child); 10 | this.ownCharacter = ownCharacter; 11 | } 12 | 13 | accept: (char: string) => Next | null = (char: string): Next | null => { 14 | return this.ownCharacter === char 15 | ? { 16 | state: this.nextState(), 17 | insert: char, 18 | pass: true, 19 | value: null, 20 | } 21 | : { 22 | state: this.nextState(), 23 | insert: this.ownCharacter, 24 | pass: false, 25 | value: null, 26 | }; 27 | }; 28 | 29 | autocomplete: () => Next | null = () => ({ 30 | state: this.nextState(), 31 | insert: this.ownCharacter, 32 | pass: false, 33 | value: null, 34 | }); 35 | 36 | toString: () => string = () => 37 | `${this.ownCharacter} -> ${this.child ? this.child.toString() : "null"}`; 38 | } 39 | 40 | export default FreeState; 41 | -------------------------------------------------------------------------------- /package/ios/AdvancedTextInputMaskDecoratorViewManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AdvancedTextInputMaskDecoratorViewManager.swift 3 | // react-native-advanced-input-mask 4 | // 5 | // Created by Ivan Ignathuk on 05/10/2024. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | @objc(AdvancedTextInputMaskDecoratorViewManager) 12 | class AdvancedTextInputMaskDecoratorViewManager: RCTViewManager { 13 | override func view() -> UIView! { 14 | AdvancedTextInputMaskDecoratorView() 15 | } 16 | 17 | override static func requiresMainQueueSetup() -> Bool { 18 | true 19 | } 20 | 21 | @objc(setText:text:autocomplete:) 22 | func setText(_ reactTag: NSNumber, text: NSString, autocomplete: Bool) { 23 | bridge.uiManager.addUIBlock { _, viewRegistry in 24 | guard let view = viewRegistry?[reactTag] as? AdvancedTextInputMaskDecoratorView else { 25 | if RCT_DEBUG == 1 { 26 | print("Invalid view returned from registry, expecting ContainerView") 27 | } 28 | return 29 | } 30 | 31 | view.setMaskedText(text: text, autocomplete: autocomplete) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.github/actions/build-android/action.yml: -------------------------------------------------------------------------------- 1 | name: "Build iOS" 2 | description: "Builds the iOS app" 3 | inputs: 4 | cache-key-prefix: 5 | description: "Prefix for cache key" 6 | required: false 7 | default: "gradle" 8 | build-command: 9 | description: "Android build job" 10 | required: false 11 | default: "yarn build:android" 12 | 13 | runs: 14 | using: "composite" 15 | steps: 16 | - name: Setup 17 | uses: ./.github/actions/setup 18 | 19 | - name: Setup JDK 17 20 | uses: actions/setup-java@v4 21 | with: 22 | distribution: "microsoft" 23 | java-version: "17" 24 | 25 | - name: Restore Gradle cache 26 | uses: actions/cache@v4 27 | with: 28 | path: | 29 | ~/.gradle/caches 30 | ~/.gradle/wrapper 31 | key: ${{ runner.os }}-${{ inputs.cache-key-prefix }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} 32 | restore-keys: | 33 | ${{ runner.os }}-${{ inputs.cache-key-prefix }}- 34 | 35 | - name: Build Android apk 36 | run: ${{ inputs.build-command }} 37 | shell: bash 38 | -------------------------------------------------------------------------------- /apps/example/src/screens/AllowedKeys/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Text } from "react-native"; 3 | import { KeyboardAwareScrollView } from "react-native-keyboard-controller"; 4 | 5 | import BaseTextInput from "../../components/BaseTextInput"; 6 | import TextInput from "../../components/TextInput"; 7 | 8 | import styles from "./styles"; 9 | 10 | const CustomNotations = () => { 11 | const [allowedKeys, setAllowedKeys] = React.useState("1234567890"); 12 | 13 | return ( 14 | 18 | Set allowed keys 19 | 25 | 30 | 31 | ); 32 | }; 33 | 34 | export default CustomNotations; 35 | -------------------------------------------------------------------------------- /apps/example/ios/MaskedTextInputExample/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images": [ 3 | { 4 | "idiom": "iphone", 5 | "scale": "2x", 6 | "size": "20x20" 7 | }, 8 | { 9 | "idiom": "iphone", 10 | "scale": "3x", 11 | "size": "20x20" 12 | }, 13 | { 14 | "idiom": "iphone", 15 | "scale": "2x", 16 | "size": "29x29" 17 | }, 18 | { 19 | "idiom": "iphone", 20 | "scale": "3x", 21 | "size": "29x29" 22 | }, 23 | { 24 | "idiom": "iphone", 25 | "scale": "2x", 26 | "size": "40x40" 27 | }, 28 | { 29 | "idiom": "iphone", 30 | "scale": "3x", 31 | "size": "40x40" 32 | }, 33 | { 34 | "idiom": "iphone", 35 | "scale": "2x", 36 | "size": "60x60" 37 | }, 38 | { 39 | "idiom": "iphone", 40 | "scale": "3x", 41 | "size": "60x60" 42 | }, 43 | { 44 | "idiom": "ios-marketing", 45 | "scale": "1x", 46 | "size": "1024x1024" 47 | } 48 | ], 49 | "info": { 50 | "author": "xcode", 51 | "version": 1 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 IvanIhnatsiuk 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | -------------------------------------------------------------------------------- /apps/example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 13 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /apps/example/android/app/src/main/java/com/maskedtextinputexample/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.maskedtextinputexample 2 | 3 | import android.os.Bundle 4 | import com.facebook.react.ReactActivity 5 | import com.facebook.react.ReactActivityDelegate 6 | import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled 7 | import com.facebook.react.defaults.DefaultReactActivityDelegate 8 | 9 | class MainActivity : ReactActivity() { 10 | /** 11 | * Returns the name of the main component registered from JavaScript. This is used to schedule 12 | * rendering of the component. 13 | */ 14 | override fun getMainComponentName(): String = "MaskedTextInputExample" 15 | 16 | /** 17 | * Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate] 18 | * which allows you to enable New Architecture with a single boolean flags [fabricEnabled] 19 | */ 20 | override fun createReactActivityDelegate(): ReactActivityDelegate = DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled) 21 | 22 | override fun onCreate(savedInstanceState: Bundle?) { 23 | super.onCreate(null) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /package/ios/MaskedTextInputManager.mm: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import 4 | 5 | @interface RCT_EXTERN_MODULE (AdvancedTextInputMaskDecoratorViewManager, RCTViewManager) 6 | RCT_EXPORT_VIEW_PROPERTY(primaryMaskFormat, NSString) 7 | RCT_EXPORT_VIEW_PROPERTY(customNotations, NSArray) 8 | RCT_EXPORT_VIEW_PROPERTY(allowedKeys, NSString) 9 | RCT_EXPORT_VIEW_PROPERTY(autocomplete, BOOL) 10 | RCT_EXPORT_VIEW_PROPERTY(autoSkip, BOOL) 11 | RCT_EXPORT_VIEW_PROPERTY(isRTL, BOOL) 12 | RCT_EXPORT_VIEW_PROPERTY(autocompleteOnFocus, BOOL) 13 | RCT_EXPORT_VIEW_PROPERTY(allowSuggestions, BOOL) 14 | RCT_EXPORT_VIEW_PROPERTY(defaultValue, NSString) 15 | RCT_EXPORT_VIEW_PROPERTY(value, NSString) 16 | RCT_EXPORT_VIEW_PROPERTY(validationRegex, NSString) 17 | RCT_EXPORT_VIEW_PROPERTY(affinityFormat, NSArray) 18 | 19 | RCT_EXPORT_VIEW_PROPERTY(onAdvancedMaskTextChange, RCTDirectEventBlock); 20 | 21 | RCT_EXTERN_METHOD(setText 22 | : (nonnull NSNumber *)reactTag text 23 | : (nonnull NSString *)text autocomplete 24 | : (nonnull BOOL)autocomplete); 25 | 26 | @end 27 | -------------------------------------------------------------------------------- /apps/example/ios/MaskedTextInputExample/PrivacyInfo.xcprivacy: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPrivacyAccessedAPITypes 6 | 7 | 8 | NSPrivacyAccessedAPIType 9 | NSPrivacyAccessedAPICategoryFileTimestamp 10 | NSPrivacyAccessedAPITypeReasons 11 | 12 | C617.1 13 | 14 | 15 | 16 | NSPrivacyAccessedAPIType 17 | NSPrivacyAccessedAPICategoryUserDefaults 18 | NSPrivacyAccessedAPITypeReasons 19 | 20 | CA92.1 21 | 22 | 23 | 24 | NSPrivacyAccessedAPIType 25 | NSPrivacyAccessedAPICategorySystemBootTime 26 | NSPrivacyAccessedAPITypeReasons 27 | 28 | 35F9.1 29 | 30 | 31 | 32 | NSPrivacyCollectedDataTypes 33 | 34 | NSPrivacyTracking 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## 📜 Description 2 | 3 | 4 | 5 | ## 💡 Motivation and Context 6 | 7 | 8 | 9 | 10 | ## 📢 Changelog 11 | 12 | 13 | 14 | 15 | ### JS 16 | 17 | - 18 | - 19 | 20 | ### iOS 21 | 22 | - 23 | - 24 | 25 | ### Android 26 | 27 | - 28 | - 29 | 30 | ## 🤔 How Has This Been Tested? 31 | 32 | 33 | 34 | 35 | 36 | ## 📸 Screenshots (if appropriate): 37 | 38 | 39 | 40 | 41 | ## 📝 Checklist 42 | 43 | - [ ] CI successfully passed 44 | - [ ] I added new mocks and corresponding unit-tests if library API was changed 45 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "" 5 | labels: bug 6 | assignees: IvanIhnatsiuk 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **Code snippet** 13 | Add your code snippet where error has been occurred. 14 | 15 | **Repo for reproducing** 16 | I would be highly appreciate if you can provide repository for reproducing your issue. It can significantly reduce the time for discovering and fixing the problem. 17 | 18 | **To Reproduce** 19 | Steps to reproduce the behavior: 20 | 21 | **Expected behavior** 22 | A clear and concise description of what you expected to happen. 23 | 24 | **Screenshots** 25 | If applicable, add screenshots to help explain your problem. 26 | 27 | **Smartphone (please complete the following information):** 28 | 29 | - Desktop OS: [e.g. Windows 10, MacOS 10.15.5] 30 | - Device: [e.g. iPhone8] 31 | - OS: [e.g. iOS 10.0] 32 | - RN version: [e.g. 0.68.2] 33 | - RN architecture: [e.g. old/new or paper/fabric] 34 | - JS engine: [e.g. JSC, Hermes, v8] 35 | - Library version: [e.g. 1.2.0] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # XDE 6 | .expo/ 7 | 8 | # VSCode 9 | .vscode/ 10 | jsconfig.json 11 | 12 | # Xcode 13 | # 14 | build/ 15 | *.pbxuser 16 | !default.pbxuser 17 | *.mode1v3 18 | !default.mode1v3 19 | *.mode2v3 20 | !default.mode2v3 21 | *.perspectivev3 22 | !default.perspectivev3 23 | xcuserdata 24 | *.xccheckout 25 | *.moved-aside 26 | DerivedData 27 | *.hmap 28 | *.ipa 29 | *.xcuserstate 30 | project.xcworkspace 31 | .xcode.env.local 32 | **/.xcode.env.local 33 | 34 | # Android/IJ 35 | # 36 | .classpath 37 | .cxx 38 | .gradle 39 | .idea 40 | .project 41 | .settings 42 | .kotlin 43 | local.properties 44 | android.iml 45 | .kotlin/ 46 | 47 | # Cocoapods 48 | # 49 | apps/example/ios/Pods 50 | 51 | # Ruby 52 | apps/example/vendor/ 53 | 54 | # node.js 55 | # 56 | node_modules/ 57 | npm-debug.log 58 | yarn-debug.log 59 | yarn-error.log 60 | 61 | # BUCK 62 | buck-out/ 63 | \.buckd/ 64 | android/app/libs 65 | android/keystores/debug.keystore 66 | 67 | # Yarn 68 | .yarn/* 69 | !.yarn/patches 70 | !.yarn/plugins 71 | !.yarn/releases 72 | !.yarn/sdks 73 | !.yarn/versions 74 | 75 | # Expo 76 | .expo/ 77 | 78 | # Turborepo 79 | .turbo/ 80 | 81 | # generated by bob 82 | lib/ 83 | 84 | #web 85 | 86 | apps/example/dist/ -------------------------------------------------------------------------------- /package/src/web/helper/CaretStringIterator.ts: -------------------------------------------------------------------------------- 1 | import { CaretGravityType } from "../model/types"; 2 | 3 | import type CaretString from "../model/CaretString"; 4 | 5 | class CaretStringIterator { 6 | protected caretString: CaretString; 7 | protected currentIndex: number; 8 | 9 | constructor(caretString: CaretString, currentIndex: number = 0) { 10 | this.caretString = caretString; 11 | this.currentIndex = currentIndex; 12 | } 13 | 14 | insertionAffectsCaret(): boolean { 15 | const { caretGravity, caretPosition } = this.caretString; 16 | 17 | const isBackwardGravity = caretGravity.type === CaretGravityType.Backward; 18 | 19 | return isBackwardGravity 20 | ? this.currentIndex <= caretPosition 21 | : this.currentIndex < caretPosition; 22 | } 23 | 24 | deletionAffectsCaret: () => boolean = () => 25 | this.currentIndex < this.caretString.caretPosition; 26 | 27 | next(): string | null { 28 | const { string } = this.caretString; 29 | 30 | if (this.currentIndex >= string.length) { 31 | return null; 32 | } 33 | 34 | const char = string.charAt(this.currentIndex); 35 | 36 | this.currentIndex += 1; 37 | 38 | return char; 39 | } 40 | } 41 | 42 | export default CaretStringIterator; 43 | -------------------------------------------------------------------------------- /apps/example/src/screens/Main/constants.ts: -------------------------------------------------------------------------------- 1 | import ScreenNames from "../../navigation/screenNames"; 2 | 3 | import type { Props as MenuItemProps } from "../../components/MenuItem"; 4 | 5 | export const MENU_ITEMS: Omit, "onPress">[] = [ 6 | { 7 | title: "Phone Input", 8 | info: ScreenNames.PhoneInput, 9 | testId: "phone-input", 10 | emoji: "📞", 11 | }, 12 | { 13 | title: "Date Input", 14 | info: ScreenNames.Date, 15 | testId: "date-input", 16 | emoji: "📅", 17 | }, 18 | { 19 | title: "Custom notations", 20 | info: ScreenNames.CustomNotations, 21 | testId: "custom-notations", 22 | emoji: "🧩", 23 | }, 24 | { 25 | title: "IBAN Input", 26 | info: ScreenNames.IBAN, 27 | testId: "iban-input", 28 | emoji: "💳", 29 | }, 30 | { 31 | title: "Allowed keys", 32 | info: ScreenNames.AllowedKeys, 33 | testId: "allowed-keys", 34 | emoji: "🔑", 35 | }, 36 | { 37 | title: "Controlled Input", 38 | info: ScreenNames.ControlledInput, 39 | testId: "controlled-text-input", 40 | emoji: "🕹", 41 | }, 42 | { 43 | title: "Validation Regex", 44 | info: ScreenNames.ValidationRegex, 45 | testId: "validation-regex", 46 | emoji: "🧪", 47 | }, 48 | ]; 49 | -------------------------------------------------------------------------------- /package/ios/AdvancedtextInputMaskDecoratorView.h: -------------------------------------------------------------------------------- 1 | // 2 | // AdvancedtextInputMaskDecoratorView.h 3 | // Pods 4 | // 5 | // Created by Ivan Ignathuk on 23/10/2024. 6 | // 7 | 8 | #pragma once 9 | 10 | // This guard prevent this file to be compiled in the old architecture. 11 | #ifdef RCT_NEW_ARCH_ENABLED 12 | #import 13 | #import 14 | #import 15 | #import "AdvancedTextInputViewContainer.h" 16 | 17 | NS_ASSUME_NONNULL_BEGIN 18 | 19 | @interface AdvancedTextInputMaskDecoratorView 20 | : RCTViewComponentView 21 | @end 22 | 23 | namespace facebook { 24 | namespace react { 25 | // In order to compare these structs we need to add the == operator for each 26 | // TODO: 27 | // https://github.com/reactwg/react-native-new-architecture/discussions/91#discussioncomment-4426469 28 | bool operator==( 29 | const AdvancedTextInputMaskDecoratorViewCustomNotationsStruct &a, 30 | const AdvancedTextInputMaskDecoratorViewCustomNotationsStruct &b) 31 | { 32 | return b.character == a.character && b.characterSet == a.characterSet && 33 | b.isOptional == a.isOptional; 34 | } 35 | } // namespace react 36 | } // namespace facebook 37 | 38 | NS_ASSUME_NONNULL_END 39 | 40 | #endif /* RCT_NEW_ARCH_ENABLED */ 41 | -------------------------------------------------------------------------------- /package/ios/AdvancedInputMaskDelegateWrapper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AdvancedInputMaskDelegateWrapper.swift 3 | // react-native-advanced-input-mask 4 | // 5 | // Created by Ivan Ignathuk on 01/11/2024. 6 | // 7 | 8 | import ForkInputMask 9 | import Foundation 10 | import UIKit 11 | 12 | class AdvancedInputMaskDelegateWrapper: NSObject, UITextFieldDelegate { 13 | weak var textFieldDelegate: UITextFieldDelegate? 14 | 15 | init(textFieldDelegate: UITextFieldDelegate?) { 16 | self.textFieldDelegate = textFieldDelegate 17 | super.init() 18 | } 19 | 20 | func textFieldDidEndEditing(_ textField: UITextField, reason _: UITextField.DidEndEditingReason) { 21 | // since reason method doesn't exist in RN delegate, but we need this event to properly blur the input 22 | textFieldDelegate?.textFieldDidEndEditing?(textField) 23 | } 24 | 25 | // MARK: call forwarding 26 | 27 | override func responds(to aSelector: Selector!) -> Bool { 28 | if super.responds(to: aSelector) { 29 | return true 30 | } 31 | return textFieldDelegate?.responds(to: aSelector) ?? false 32 | } 33 | 34 | override func forwardingTarget(for aSelector: Selector!) -> Any? { 35 | if textFieldDelegate?.responds(to: aSelector) ?? false { 36 | return textFieldDelegate 37 | } 38 | return super.forwardingTarget(for: aSelector) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /package/ios/RNConversions.h: -------------------------------------------------------------------------------- 1 | // 2 | // RNConversions.h 3 | // Pods 4 | // 5 | // Created by Ivan Ignathuk on 25/10/2024. 6 | // 7 | 8 | #ifdef __cplusplus 9 | #import 10 | 11 | inline NSArray *convertCustomNotations( 12 | std::vector 13 | customNotationsArray) 14 | { 15 | NSMutableArray *customNotations = [NSMutableArray arrayWithCapacity:customNotationsArray.size()]; 16 | 17 | for (auto customNotation : customNotationsArray) { 18 | NSDictionary *notation = @{ 19 | @"character" : RCTNSStringFromString(customNotation.character), 20 | @"characterSet" : RCTNSStringFromString(customNotation.characterSet), 21 | @"isOptional" : @(customNotation.isOptional), 22 | }; 23 | [customNotations addObject:notation]; 24 | } 25 | 26 | return customNotations; 27 | } 28 | 29 | inline NSArray *convertAffinityFormat(std::vector affinityFormatArray) 30 | { 31 | NSMutableArray *affinityFormats = 32 | [NSMutableArray arrayWithCapacity:affinityFormatArray.size()]; 33 | 34 | for (auto &affinityFormat : affinityFormatArray) { 35 | [affinityFormats addObject:RCTNSStringFromString(affinityFormat)]; 36 | } 37 | 38 | return affinityFormats; 39 | } 40 | 41 | #endif 42 | -------------------------------------------------------------------------------- /package/src/web/model/state/OptionalValueState.ts: -------------------------------------------------------------------------------- 1 | import { getCharacterTypeString } from "../utils"; 2 | 3 | import State from "./State"; 4 | 5 | import type { Notation } from "../../../types"; 6 | import type { Next, StateType } from "../types"; 7 | 8 | class OptionalValueState extends State { 9 | stateType: StateType | Notation; 10 | 11 | constructor(child: State, stateType: StateType | Notation) { 12 | super(child); 13 | this.stateType = stateType; 14 | } 15 | 16 | private accepts(character: string): boolean { 17 | if (this.stateType) { 18 | if ("name" in this.stateType) { 19 | return this.stateType.regex.test(character); 20 | } 21 | 22 | return this.stateType.characterSet.includes(character); 23 | } 24 | 25 | return false; 26 | } 27 | 28 | accept: (character: string) => Next = (character) => 29 | this.accepts(character) 30 | ? { 31 | state: this.nextState(), 32 | insert: character, 33 | pass: true, 34 | value: character, 35 | } 36 | : { state: this.nextState(), insert: null, pass: false, value: null }; 37 | 38 | toString: () => string = () => { 39 | const typeStr = getCharacterTypeString(this.stateType); 40 | 41 | return `${typeStr} -> ${this.child?.toString() ?? "null"}`; 42 | }; 43 | } 44 | 45 | export default OptionalValueState; 46 | -------------------------------------------------------------------------------- /package/ios/AdvancedTextInputViewContainer.h: -------------------------------------------------------------------------------- 1 | // 2 | // Header.h 3 | // Pods 4 | // 5 | // Created by Ivan Ignathuk on 23/10/2024. 6 | // 7 | 8 | @class UITraitCollection; 9 | @class NSDictionary; 10 | @class NSString; 11 | @class NSCoder; 12 | 13 | #import 14 | 15 | @protocol AdvancedTextInputViewContainerDelegate 16 | - (void)onAdvancedMaskTextChangeWithEventData:(NSDictionary *)eventData; 17 | @end 18 | 19 | @interface AdvancedTextInputViewDecoratorView : RCTView 20 | @property (nonatomic, weak) id _Nullable delegate; 21 | - (void)setPrimaryMaskFormat:(NSString *_Nonnull)primaryMaskFormat; 22 | - (void)setAutocomplete:(BOOL)autocomplete; 23 | - (void)setAutocompleteOnFocus:(BOOL)autocompleteOnFocus; 24 | - (void)setAllowSuggestions:(BOOL)allowSuggestions; 25 | - (void)setAllowedKeys:(NSString *_Nonnull)allowedKeys; 26 | - (void)setCustomNotations:(NSArray *)customNotations; 27 | - (void)setAutoSkip:(BOOL)autoSkip; 28 | - (void)setIsRTL:(BOOL)isRTL; 29 | - (void)setDefaultValue:(NSString *)defaultValue; 30 | - (void)setValue:(NSString *)value; 31 | - (void)setAffinityFormat:(NSArray *)affinityFormat; 32 | - (void)setAffinityCalculationStrategy:(NSInteger)affinityCalculationStrategy; 33 | - (void)setValidationRegex:(NSString *)validationRegex; 34 | - (void)cleanup; 35 | - (void)setMaskedText:(NSString *_Nonnull)text autocomplete:(BOOL)autocomplete; 36 | @end 37 | -------------------------------------------------------------------------------- /apps/example/src/components/replicas/touchables/types/index.ts: -------------------------------------------------------------------------------- 1 | import type { ComponentType } from "react"; 2 | import type { 3 | ColorValue, 4 | Falsy, 5 | Insets, 6 | RecursiveArray, 7 | RegisteredStyle, 8 | StyleProp, 9 | ViewStyle, 10 | } from "react-native"; 11 | import type { GenericTouchableProps as TouchableProps } from "react-native-gesture-handler/lib/typescript/components/touchables/GenericTouchableProps"; 12 | import type { AnimatedStyle } from "react-native-reanimated"; 13 | 14 | export type GenericTouchableProps = { 15 | hitSlop?: Insets; 16 | disabled?: boolean; 17 | style?: 18 | | (ViewStyle[] & 19 | RecursiveArray>) 20 | | StyleProp 21 | | (AnimatedStyle & StyleProp); 22 | onPress?: () => void; 23 | onPressIn?: () => void; 24 | } & TouchableProps; 25 | 26 | export type TouchableHighlightProps = GenericTouchableProps & { 27 | overlayColor?: ColorValue; 28 | }; 29 | 30 | // For cross-platform usage we have to specify both platform-specific properties 31 | export type TouchableCrossPlatformProps = TouchableHighlightProps & 32 | TouchableNativeFeedbackProps; 33 | 34 | export type TouchableComponentType = ComponentType; 35 | export type TouchableNativeFeedbackProps = GenericTouchableProps & { 36 | background?: ColorValue; 37 | duration?: number; 38 | borderless?: boolean; 39 | rippleRadius?: number; 40 | }; 41 | -------------------------------------------------------------------------------- /apps/example/ios/MaskedTextInputExample/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import React 3 | import React_RCTAppDelegate 4 | import ReactAppDependencyProvider 5 | 6 | @main 7 | class AppDelegate: UIResponder, UIApplicationDelegate { 8 | var window: UIWindow? 9 | 10 | var reactNativeDelegate: ReactNativeDelegate? 11 | var reactNativeFactory: RCTReactNativeFactory? 12 | 13 | func application( 14 | _ application: UIApplication, 15 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil 16 | ) -> Bool { 17 | let delegate = ReactNativeDelegate() 18 | let factory = RCTReactNativeFactory(delegate: delegate) 19 | delegate.dependencyProvider = RCTAppDependencyProvider() 20 | 21 | reactNativeDelegate = delegate 22 | reactNativeFactory = factory 23 | 24 | window = UIWindow(frame: UIScreen.main.bounds) 25 | 26 | factory.startReactNative( 27 | withModuleName: "MaskedTextInputExample", 28 | in: window, 29 | launchOptions: launchOptions 30 | ) 31 | 32 | return true 33 | } 34 | } 35 | 36 | class ReactNativeDelegate: RCTDefaultReactNativeFactoryDelegate { 37 | override func sourceURL(for bridge: RCTBridge) -> URL? { 38 | self.bundleURL() 39 | } 40 | 41 | override func bundleURL() -> URL? { 42 | #if DEBUG 43 | RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index") 44 | #else 45 | Bundle.main.url(forResource: "main", withExtension: "jsbundle") 46 | #endif 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /.github/workflows/verify-ios.yml: -------------------------------------------------------------------------------- 1 | name: ✨ Validate iOS 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - ".github/workflows/verify-ios.yml" 9 | - "package/ios/**" 10 | pull_request: 11 | branches: 12 | - main 13 | paths: 14 | - ".github/workflows/verify-ios.yml" 15 | - "package/ios/**" 16 | 17 | jobs: 18 | swift-lint: 19 | runs-on: ubuntu-latest 20 | name: 🔎 Swift Lint 21 | steps: 22 | - uses: actions/checkout@v4 23 | - name: Run SwiftLint GitHub Action (--strict) 24 | uses: norio-nomura/action-swiftlint@master 25 | with: 26 | args: --strict 27 | env: 28 | WORKING_DIRECTORY: package/ios 29 | format: 30 | runs-on: macOS-15 31 | name: 📚 Swift Format 32 | defaults: 33 | run: 34 | working-directory: package/ios 35 | steps: 36 | - uses: actions/checkout@v4 37 | 38 | - name: Install SwiftFormat 39 | run: brew install swiftformat 40 | 41 | - name: Format Swift code 42 | run: swiftformat --verbose . 43 | 44 | - name: Verify that the formatted code hasn't been changed 45 | run: git diff --exit-code HEAD 46 | objc-lint: 47 | runs-on: macos-15 48 | name: 🔎 ObjC Lint 49 | steps: 50 | - uses: actions/checkout@v4 51 | 52 | - name: Install dependencies 53 | run: yarn install --frozen-lockfile 54 | 55 | - name: Verify formatting 56 | run: yarn lint-clang 57 | -------------------------------------------------------------------------------- /apps/example/android/app/src/main/java/com/maskedtextinputexample/MainApplication.kt: -------------------------------------------------------------------------------- 1 | package com.maskedtextinputexample 2 | 3 | import android.app.Application 4 | import com.facebook.react.PackageList 5 | import com.facebook.react.ReactApplication 6 | import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative 7 | import com.facebook.react.ReactHost 8 | import com.facebook.react.ReactNativeHost 9 | import com.facebook.react.ReactPackage 10 | import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost 11 | import com.facebook.react.defaults.DefaultReactNativeHost 12 | 13 | class MainApplication : 14 | Application(), 15 | ReactApplication { 16 | override val reactNativeHost: ReactNativeHost = 17 | object : DefaultReactNativeHost(this) { 18 | override fun getPackages(): List = 19 | PackageList(this).packages.apply { 20 | // Packages that cannot be autolinked yet can be added manually here, for example: 21 | // add(MyReactNativePackage()) 22 | } 23 | 24 | override fun getJSMainModuleName(): String = "index" 25 | 26 | override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG 27 | 28 | override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED 29 | override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED 30 | } 31 | 32 | override val reactHost: ReactHost 33 | get() = getDefaultReactHost(applicationContext, reactNativeHost) 34 | 35 | override fun onCreate() { 36 | super.onCreate() 37 | loadReactNative(this) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /apps/example/src/screens/Phone/index.tsx: -------------------------------------------------------------------------------- 1 | import { useNavigation } from "@react-navigation/native"; 2 | import * as React from "react"; 3 | import { KeyboardAwareScrollView } from "react-native-keyboard-controller"; 4 | 5 | import Button from "../../components/Button"; 6 | import TextInput from "../../components/TextInput"; 7 | import ScreenNames from "../../navigation/screenNames"; 8 | 9 | import styles from "./styles"; 10 | 11 | const Phone = () => { 12 | const { reset, navigate } = useNavigation(); 13 | 14 | const navigateToRNTextInputWithReset = React.useCallback(() => { 15 | reset({ index: 0, routes: [{ name: ScreenNames.RNTextInput }] }); 16 | }, [reset]); 17 | 18 | const navigateToRNTextInput = React.useCallback(() => { 19 | navigate(ScreenNames.RNTextInput); 20 | }, [navigate]); 21 | 22 | return ( 23 | 27 | 35 |