├── .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 |
40 |
45 |
46 | );
47 | };
48 |
49 | export default Phone;
50 |
--------------------------------------------------------------------------------
/package/android/src/main/java/com/maskedtextinput/transformation/CustomTransformationMethod.kt:
--------------------------------------------------------------------------------
1 | package com.maskedtextinput.transformation
2 |
3 | import android.text.method.TransformationMethod
4 | import android.view.View
5 |
6 | class CustomTransformationMethod(
7 | private val transformationString: String,
8 | private val transformationChar: Char,
9 | ) : TransformationMethod {
10 | override fun getTransformation(
11 | source: CharSequence,
12 | view: View,
13 | ): CharSequence = CardCharSequence(source)
14 |
15 | inner class CardCharSequence(
16 | private val source: CharSequence,
17 | ) : CharSequence {
18 | override val length: Int
19 | get() = source.length
20 |
21 | override fun get(index: Int): Char =
22 | if (index < transformationString.length && transformationString[index] == transformationChar) {
23 | transformationChar
24 | } else {
25 | source[index]
26 | }
27 |
28 | override fun subSequence(
29 | startIndex: Int,
30 | endIndex: Int,
31 | ): CharSequence {
32 | val result = StringBuilder()
33 | for (i in startIndex until endIndex) {
34 | result.append(
35 | if (transformationString[i] == transformationChar) {
36 | transformationChar
37 | } else {
38 | source[i]
39 | },
40 | )
41 | }
42 | return result
43 | }
44 | }
45 |
46 | override fun onFocusChanged(
47 | view: View,
48 | sourceText: CharSequence,
49 | focused: Boolean,
50 | direction: Int,
51 | previouslyFocusedRect: android.graphics.Rect?,
52 | ) {
53 | // No action needed
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/package/src/web/helper/RTLMask.ts:
--------------------------------------------------------------------------------
1 | import Mask from "./Mask";
2 | import RTLCaretStringIterator from "./RTLCaretStringIterator";
3 |
4 | import type CaretStringIterator from "./CaretStringIterator";
5 | import type { Notation } from "../../types";
6 | import type CaretString from "../model/CaretString";
7 | import type { MaskResult } from "../model/types";
8 |
9 | export default class RTLMask extends Mask {
10 | private static rtlCache: Map = new Map();
11 |
12 | constructor(format: string, customNotations: Notation[]) {
13 | super(RTLMask.reversedFormat(format), customNotations);
14 | }
15 |
16 | static getOrCreate(format: string, customNotations: Notation[]): RTLMask {
17 | const key = RTLMask.reversedFormat(format);
18 | const cachedMask = RTLMask.rtlCache.get(key);
19 |
20 | if (!cachedMask) {
21 | const newMask = new RTLMask(format, customNotations);
22 |
23 | RTLMask.rtlCache.set(key, newMask);
24 |
25 | return newMask;
26 | }
27 |
28 | return cachedMask;
29 | }
30 |
31 | apply(text: CaretString): MaskResult {
32 | return super.apply(text.reversed()).reversed();
33 | }
34 |
35 | makeIterator(text: CaretString): CaretStringIterator {
36 | return new RTLCaretStringIterator(text);
37 | }
38 |
39 | private static reversedFormat(format: string): string {
40 | const mapped = format.split("").reduceRight((acc, char) => {
41 | switch (char) {
42 | case "[":
43 | return acc + "]";
44 | case "]":
45 | return acc + "[";
46 | case "{":
47 | return acc + "}";
48 | case "}":
49 | return acc + "{";
50 | default:
51 | return acc + char;
52 | }
53 | }, "");
54 |
55 | return mapped;
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/apps/example/ios/MaskedTextInputExample/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleDisplayName
8 | MaskedTextInputExample
9 | CFBundleExecutable
10 | $(EXECUTABLE_NAME)
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | APPL
19 | CFBundleShortVersionString
20 | $(MARKETING_VERSION)
21 | CFBundleSignature
22 | ????
23 | CFBundleVersion
24 | $(CURRENT_PROJECT_VERSION)
25 | LSRequiresIPhoneOS
26 |
27 | NSAppTransportSecurity
28 |
29 | NSAllowsArbitraryLoads
30 |
31 | NSAllowsLocalNetworking
32 |
33 |
34 | NSLocationWhenInUseUsageDescription
35 |
36 | RCTNewArchEnabled
37 |
38 | UILaunchStoryboardName
39 | LaunchScreen
40 | UIRequiredDeviceCapabilities
41 |
42 | arm64
43 |
44 | UISupportedInterfaceOrientations
45 |
46 | UIInterfaceOrientationPortrait
47 | UIInterfaceOrientationLandscapeLeft
48 | UIInterfaceOrientationLandscapeRight
49 |
50 | UIViewControllerBasedStatusBarAppearance
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/apps/example/src/components/replicas/touchables/TouchableHighlight/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo, useMemo } from "react";
2 | import { StyleSheet } from "react-native";
3 | import { Gesture, GestureDetector } from "react-native-gesture-handler";
4 | import Reanimated, {
5 | runOnJS,
6 | useAnimatedStyle,
7 | useSharedValue,
8 | withTiming,
9 | } from "react-native-reanimated";
10 |
11 | import type { TouchableHighlightProps } from "../types";
12 | import type { FC } from "react";
13 |
14 | const noop = () => {};
15 |
16 | const TouchableHighlight: FC = ({
17 | children,
18 | disabled,
19 | onPress = noop,
20 | style,
21 | overlayColor = "rgba(0,0,0,0.1)",
22 | ...rest
23 | }) => {
24 | const opacity = useSharedValue(0);
25 |
26 | const gesture = useMemo(
27 | () =>
28 | Gesture.Tap()
29 | .onBegin((event) => {
30 | console.log(event.numberOfPointers);
31 | opacity.set(withTiming(1, { duration: 150 }));
32 | })
33 | .onEnd(() => runOnJS(onPress)())
34 | .onFinalize(() => {
35 | opacity.set(withTiming(0, { duration: 450 }));
36 | })
37 | .enabled(!disabled),
38 | [disabled, opacity, onPress],
39 | );
40 |
41 | const highlight = useAnimatedStyle(
42 | () => ({
43 | opacity: opacity.get(),
44 | backgroundColor: overlayColor,
45 | }),
46 | [overlayColor],
47 | );
48 |
49 | const highlightStyle = useMemo(
50 | () => [StyleSheet.absoluteFillObject, highlight],
51 | [highlight],
52 | );
53 |
54 | return (
55 |
56 |
57 | {children}
58 |
59 |
60 |
61 | );
62 | };
63 |
64 | export default memo(TouchableHighlight);
65 |
--------------------------------------------------------------------------------
/package/src/web/helper/affinityCalculationStrategy.ts:
--------------------------------------------------------------------------------
1 | import { AFFINITY_CALCULATION_STRATEGY } from "../../enums";
2 |
3 | import type { Mask } from "./Mask";
4 | import type CaretString from "../model/CaretString";
5 |
6 | /**
7 | * Calculates an affinity score based on the specified strategy.
8 | */
9 | export function calculateAffinityOfMask(
10 | strategy: AFFINITY_CALCULATION_STRATEGY,
11 | mask: Mask,
12 | text: CaretString,
13 | ): number {
14 | switch (strategy) {
15 | case AFFINITY_CALCULATION_STRATEGY.WHOLE_STRING: {
16 | const result = mask.apply(text);
17 |
18 | return result.affinity;
19 | }
20 | case AFFINITY_CALCULATION_STRATEGY.PREFIX: {
21 | const result = mask.apply(text);
22 |
23 | return prefixIntersection(result.formattedText.string, text.string)
24 | .length;
25 | }
26 | case AFFINITY_CALCULATION_STRATEGY.CAPACITY: {
27 | if (text.string.length > mask.totalTextLength()) {
28 | return Number.MIN_SAFE_INTEGER;
29 | }
30 |
31 | return text.string.length - mask.totalTextLength();
32 | }
33 | case AFFINITY_CALCULATION_STRATEGY.EXTRACTED_VALUE_CAPACITY: {
34 | const result = mask.apply(text);
35 | const extractedValueLength = result.extractedValue.length;
36 |
37 | if (extractedValueLength > mask.totalValueLength()) {
38 | return Number.MIN_SAFE_INTEGER;
39 | }
40 |
41 | return extractedValueLength - mask.totalValueLength();
42 | }
43 | default:
44 | return 0;
45 | }
46 | }
47 |
48 | /**
49 | * Finds common prefix between two strings.
50 | */
51 | function prefixIntersection(str1: string, str2: string): string {
52 | if (!str1 || !str2) {
53 | return "";
54 | }
55 | let endIndex = 0;
56 |
57 | while (endIndex < str1.length && endIndex < str2.length) {
58 | if (str1[endIndex] !== str2[endIndex]) {
59 | break;
60 | }
61 | endIndex++;
62 | }
63 |
64 | return str1.substring(0, endIndex);
65 | }
66 |
--------------------------------------------------------------------------------
/.github/actions/build-ios/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: "example-ios"
8 | pod-install-command:
9 | description: "Custom pod install command"
10 | required: false
11 | default: "pod install"
12 |
13 | runs:
14 | using: "composite"
15 | steps:
16 | - name: Setup Xcode
17 | uses: maxim-lobanov/setup-xcode@v1
18 | with:
19 | xcode-version: "16.4"
20 |
21 | - name: Setup
22 | uses: ./.github/actions/setup
23 |
24 | - name: Install Ccache
25 | uses: hendrikmuhs/ccache-action@v1.2
26 | with:
27 | max-size: 1.5G
28 | key: ${{ runner.os }}-ccache-${{ inputs.cache-key-prefix }}
29 | create-symlink: true
30 |
31 | - name: Setup ccache behavior
32 | run: |
33 | cd apps/example/ios
34 | echo "CCACHE_SLOPPINESS=clang_index_store,file_stat_matches,include_file_ctime,include_file_mtime,ivfsoverlay,pch_defines,modules,system_headers,time_macros" >> $GITHUB_ENV
35 | echo "CCACHE_FILECLONE=true" >> $GITHUB_ENV
36 | echo "CCACHE_DEPEND=true" >> $GITHUB_ENV
37 | echo "CCACHE_INODECACHE=true" >> $GITHUB_ENV
38 | shell: bash
39 |
40 | - name: Restore Pods cache
41 | uses: actions/cache@v4
42 | with:
43 | path: |
44 | apps/example/ios/Pods
45 | ~/Library/Caches/CocoaPods
46 | ~/.cocoapods
47 | key: ${{ runner.os }}-pods-${{ inputs.cache-key-prefix }}-${{ hashFiles('**/Podfile.lock') }}
48 | restore-keys: |
49 | ${{ runner.os }}-pods-${{ inputs.cache-key-prefix }}
50 |
51 | - name: Install Pods
52 | run: cd apps/example/ios && export USE_CCACHE=1 && ${{ inputs.pod-install-command }}
53 | shell: bash
54 |
55 | - name: Install xcpretty
56 | run: cd apps/example/ios && gem install xcpretty
57 | shell: bash
58 |
59 | - name: Build example for iOS
60 | run: cd apps/example && yarn build:ios
61 | shell: bash
62 |
--------------------------------------------------------------------------------
/apps/example/android/app/src/main/res/drawable/rn_edit_text_material.xml:
--------------------------------------------------------------------------------
1 |
2 |
16 |
21 |
22 |
23 |
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/package/src/web/model/state/ValueState.ts:
--------------------------------------------------------------------------------
1 | import { getCharacterTypeString } from "../utils";
2 |
3 | import State from "./State";
4 |
5 | import type { Notation } from "../../../types";
6 | import type { Ellipsis, Next, StateType } from "../types";
7 |
8 | class ValueState extends State {
9 | stateType: StateType | Ellipsis | Notation;
10 |
11 | constructor(
12 | child: State | null,
13 | valueState: StateType | Ellipsis | Notation,
14 | ) {
15 | super(child);
16 | this.stateType = valueState;
17 | }
18 |
19 | private accepts(character: string): boolean {
20 | if ("name" in this.stateType) {
21 | if (this.stateType.name === "ellipsis") {
22 | return this.checkEllipsis(this.stateType.inheritedType, character);
23 | }
24 |
25 | return this.stateType.regex.test(character);
26 | }
27 |
28 | return this.stateType.characterSet.includes(character);
29 | }
30 |
31 | private checkEllipsis(
32 | stateType: StateType | Ellipsis | Notation,
33 | character: string,
34 | ): boolean {
35 | if ("name" in stateType) {
36 | if (stateType.name === "ellipsis") {
37 | this.checkEllipsis(stateType.inheritedType, character);
38 | } else {
39 | return stateType.regex.test(character);
40 | }
41 | }
42 |
43 | return (stateType as Notation).characterSet.includes(character);
44 | }
45 |
46 | accept: (character: string) => Next | null = (character) =>
47 | this.accepts(character)
48 | ? {
49 | state: this.nextState(),
50 | insert: character,
51 | pass: true,
52 | value: character,
53 | }
54 | : null;
55 |
56 | get isElliptical(): boolean {
57 | return "name" in this.stateType && this.stateType.name === "ellipsis";
58 | }
59 |
60 | nextState: () => State = () => (this.isElliptical ? this : this.child!);
61 |
62 | toString: () => string = () => {
63 | const typeStr = getCharacterTypeString(this.stateType);
64 |
65 | return `${typeStr} -> ${this.child?.toString() ?? "null"}`;
66 | };
67 | }
68 |
69 | export default ValueState;
70 |
--------------------------------------------------------------------------------
/apps/example/src/navigation/Root/index.tsx:
--------------------------------------------------------------------------------
1 | import { createNativeStackNavigator } from "@react-navigation/native-stack";
2 |
3 | import AllowedKeys from "../../screens/AllowedKeys";
4 | import ControlledInput from "../../screens/ControlledInput";
5 | import CustomNotations from "../../screens/CustomNotations";
6 | import DateScreen from "../../screens/Date";
7 | import IBAN from "../../screens/IBAN";
8 | import Main from "../../screens/Main";
9 | import Phone from "../../screens/Phone";
10 | import RNTextInput from "../../screens/RNTextInput";
11 | import ValidationRegex from "../../screens/ValidationRegEx";
12 | import ScreenNames from "../screenNames";
13 |
14 | const RootStack = createNativeStackNavigator({
15 | initialRouteName: ScreenNames.Main,
16 | screenOptions: {
17 | contentStyle: {
18 | backgroundColor: "#fafafa",
19 | },
20 | },
21 | screens: {
22 | [ScreenNames.Main]: {
23 | screen: Main,
24 | },
25 | [ScreenNames.RNTextInput]: {
26 | screen: RNTextInput,
27 | },
28 | [ScreenNames.Date]: {
29 | options: {
30 | title: "Date Input 📅",
31 | },
32 | screen: DateScreen,
33 | },
34 | [ScreenNames.PhoneInput]: {
35 | options: {
36 | title: "Phone Input 📞",
37 | },
38 | screen: Phone,
39 | },
40 | [ScreenNames.IBAN]: {
41 | options: {
42 | title: "IBAN Input 💳",
43 | },
44 | screen: IBAN,
45 | },
46 | [ScreenNames.AllowedKeys]: {
47 | options: {
48 | title: "Allowed keys 🔑",
49 | },
50 | screen: AllowedKeys,
51 | },
52 | [ScreenNames.CustomNotations]: {
53 | options: {
54 | title: "Custom notations 🧩",
55 | },
56 | screen: CustomNotations,
57 | },
58 | [ScreenNames.ControlledInput]: {
59 | options: {
60 | title: "Controlled Input 🕹",
61 | },
62 | screen: ControlledInput,
63 | },
64 | [ScreenNames.ValidationRegex]: {
65 | options: {
66 | title: "Validation Regex 🧪",
67 | },
68 | screen: ValidationRegex,
69 | },
70 | },
71 | });
72 |
73 | export default RootStack;
74 |
--------------------------------------------------------------------------------
/package/src/web/views/MaskedTextInput/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { forwardRef, memo, useImperativeHandle, useRef } from "react";
2 | import { TextInput } from "react-native";
3 |
4 | import useMaskedTextInputListener from "../../hooks/useMaskedTextInputListener";
5 |
6 | import type { MaskedTextInputProps } from "../../../types";
7 |
8 | const MaskedTextInput = forwardRef(
9 | (
10 | {
11 | affinityCalculationStrategy,
12 | affinityFormat,
13 | autocomplete = true,
14 | autoSkip = false,
15 | customNotations,
16 | isRTL = false,
17 | mask,
18 | autoCapitalize = "words",
19 | allowedKeys,
20 | defaultValue,
21 | onChange,
22 | onChangeText,
23 | onTailPlaceholderChange,
24 | onFocus,
25 | renderTextInputComponent,
26 | validationRegex,
27 | ...rest
28 | },
29 | ref,
30 | ) => {
31 | const InputComponent = renderTextInputComponent ?? TextInput;
32 |
33 | const inputRef = useRef(null);
34 |
35 | const {
36 | defaultValue: maskedDefaultValue,
37 | handleFocus,
38 | handleOnChange,
39 | setTextField,
40 | } = useMaskedTextInputListener({
41 | mask,
42 | affinityFormat,
43 | affinityCalculationStrategy,
44 | customNotations,
45 | allowedKeys,
46 | autocomplete,
47 | autoSkip,
48 | isRTL,
49 | onChange,
50 | onChangeText,
51 | onTailPlaceholderChange,
52 | onFocus,
53 | validationRegex,
54 | defaultValue,
55 | });
56 |
57 | useImperativeHandle(
58 | ref,
59 | () => {
60 | setTextField(inputRef.current as unknown as HTMLInputElement);
61 |
62 | return inputRef.current;
63 | },
64 | [setTextField, inputRef],
65 | );
66 |
67 | return (
68 |
76 | );
77 | },
78 | );
79 |
80 | export default memo(MaskedTextInput);
81 |
--------------------------------------------------------------------------------
/apps/example/android/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | # Default value: -Xmx512m -XX:MaxMetaspaceSize=256m
13 | org.gradle.jvmargs=-Xmx4608m
14 |
15 | # When configured, Gradle will run in incubating parallel mode.
16 | # This option should only be used with decoupled projects. More details, visit
17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
18 | # org.gradle.parallel=true
19 |
20 | # AndroidX package structure to make it clearer which packages are bundled with the
21 | # Android operating system, and which are packaged with your app's APK
22 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
23 | android.useAndroidX=true
24 | # Automatically convert third-party libraries to use AndroidX
25 | android.enableJetifier=true
26 |
27 | # Use this property to specify which architecture you want to build.
28 | # You can also override it from the CLI using
29 | # ./gradlew -PreactNativeArchitectures=x86_64
30 | reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
31 |
32 | # Use this property to enable support to the new architecture.
33 | # This will allow you to use TurboModules and the Fabric render in
34 | # your application. You should enable this flag either if you want
35 | # to write custom TurboModules/Fabric components OR use libraries that
36 | # are providing them.
37 | newArchEnabled=true
38 |
39 | # Use this property to enable or disable the Hermes JS engine.
40 | # If set to false, you will be using JSC instead.
41 | hermesEnabled=true
42 |
43 | # Use this property to enable edge-to-edge display support.
44 | # This allows your app to draw behind system bars for an immersive UI.
45 | # Note: Only works with ReactActivity and should not be used with custom Activity.
46 | edgeToEdgeEnabled=true
--------------------------------------------------------------------------------
/package/src/native/specs/AdvancedTextInputMaskDecoratorViewNativeComponent.ts:
--------------------------------------------------------------------------------
1 | import codegenNativeCommands from "react-native/Libraries/Utilities/codegenNativeCommands";
2 | import codegenNativeComponent from "react-native/Libraries/Utilities/codegenNativeComponent";
3 |
4 | import type { HostComponent } from "react-native";
5 | import type { ViewProps } from "react-native/Libraries/Components/View/ViewPropTypes";
6 | import type {
7 | DirectEventHandler,
8 | Int32,
9 | } from "react-native/Libraries/Types/CodegenTypes";
10 |
11 | export type OnAdvancedMaskTextChange = Readonly<{
12 | extracted: string;
13 | formatted: string;
14 | tailPlaceholder: string;
15 | complete: boolean;
16 | }>;
17 |
18 | type CustomTransformation = {
19 | transformationChar: string;
20 | transformationString: string;
21 | };
22 |
23 | type Notation = {
24 | /**
25 | * A symbol in format string.
26 | */
27 | character: string;
28 | /**
29 | * An associated character set of acceptable input characters.
30 | */
31 | characterSet: string;
32 | /**
33 | * Is it an optional symbol or mandatory?
34 | */
35 | isOptional: boolean;
36 | };
37 |
38 | export interface NativeProps extends ViewProps {
39 | affinityCalculationStrategy?: Int32;
40 | affinityFormat?: ReadonlyArray;
41 | allowSuggestions?: boolean;
42 | autocomplete?: boolean;
43 | autocompleteOnFocus?: boolean;
44 | autoSkip?: boolean;
45 | customNotations?: ReadonlyArray;
46 | customTransformation?: Readonly;
47 | allowedKeys?: string;
48 | defaultValue?: string;
49 | isRTL?: boolean;
50 | onAdvancedMaskTextChange?: DirectEventHandler;
51 | primaryMaskFormat: string;
52 | value?: string;
53 | validationRegex?: string;
54 | }
55 |
56 | export interface NativeCommands {
57 | setText: (
58 | viewRef: React.ElementRef>,
59 | text: string,
60 | autocomplete: boolean,
61 | ) => void;
62 | }
63 |
64 | export const Commands: NativeCommands = codegenNativeCommands({
65 | supportedCommands: ["setText"],
66 | });
67 |
68 | export default codegenNativeComponent(
69 | "AdvancedTextInputMaskDecoratorView",
70 | ) as HostComponent;
71 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-native-advanced-input-mask-monorepo",
3 | "private": true,
4 | "description": "Text input mask for React Native on iOS, Android and web. Synchronous and easy formatting without hustle",
5 | "scripts": {
6 | "test": "jest",
7 | "typecheck": "tsc --noEmit",
8 | "lint": "eslint \"**/*.{js,ts,tsx}\"",
9 | "clean": "del-cli android/build apps/example/android/build apps/example/android/app/build example/ios/build lib",
10 | "release": "release-it",
11 | "lint-clang": "sh clang-format.sh",
12 | "format": "prettier '**/*' --ignore-unknown --write"
13 | },
14 | "devDependencies": {
15 | "@commitlint/config-conventional": "^17.0.2",
16 | "@evilmartians/lefthook": "^1.5.0",
17 | "@react-native/eslint-config": "0.80.0",
18 | "@release-it/conventional-changelog": "^5.0.0",
19 | "@types/jest": "^28.1.2",
20 | "@types/react": "19.1.0",
21 | "@types/react-native-web": "0.19.2",
22 | "@typescript-eslint/eslint-plugin": "^8.26.1",
23 | "@typescript-eslint/parser": "^8.26.1",
24 | "clang-format": "^1.8.0",
25 | "commitlint": "^17.0.2",
26 | "del-cli": "^5.0.0",
27 | "eslint": "^8.4.1",
28 | "eslint-config-prettier": "^8.5.0",
29 | "eslint-import-resolver-typescript": "^3.8.4",
30 | "eslint-plugin-eslint-comments": "^3.2.0",
31 | "eslint-plugin-ft-flow": "^3.0.11",
32 | "eslint-plugin-import": "^2.31.0",
33 | "eslint-plugin-jest": "^28.11.0",
34 | "eslint-plugin-prettier": "^4.2.1",
35 | "eslint-plugin-react-compiler": "19.1.0-rc.2",
36 | "eslint-plugin-react-perf": "^3.3.3",
37 | "jest": "^28.1.1",
38 | "prettier": "2.8.8",
39 | "release-it": "^15.0.0",
40 | "typescript": "^5.8.2"
41 | },
42 | "resolutions": {
43 | "@types/react": "19.1.0",
44 | "react-dom": "19.1.0"
45 | },
46 | "workspaces": {
47 | "packages": [
48 | "apps/example",
49 | "package"
50 | ]
51 | },
52 | "packageManager": "yarn@4.6.0",
53 | "engines": {
54 | "node": ">= 20.0.0"
55 | },
56 | "jest": {
57 | "preset": "react-native",
58 | "modulePathIgnorePatterns": [
59 | "/apps/example/node_modules",
60 | "/package/lib/"
61 | ]
62 | },
63 | "commitlint": {
64 | "extends": [
65 | "@commitlint/config-conventional"
66 | ]
67 | },
68 | "prettier": {
69 | "quoteProps": "consistent",
70 | "trailingComma": "all"
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/package/react-native-advanced-input-mask.podspec:
--------------------------------------------------------------------------------
1 | require "json"
2 |
3 | package = JSON.parse(File.read(File.join(__dir__, "package.json")))
4 | folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32'
5 |
6 | new_arch_enabled = ENV['RCT_NEW_ARCH_ENABLED'] == '1'
7 |
8 | Pod::Spec.new do |s|
9 | s.name = "react-native-advanced-input-mask"
10 | s.version = package["version"]
11 | s.summary = package["description"]
12 | s.homepage = package["homepage"]
13 | s.license = package["license"]
14 | s.authors = package["author"]
15 |
16 | s.platforms = { :ios => "13.0" }
17 | s.source = { :git => "https://github.com/IvanIhnatsiuk/react-native-advanced-input-mask.git", :tag => "#{s.version}" }
18 |
19 | s.source_files = "ios/**/*.{h,m,mm,swift}"
20 | s.public_header_files = "ios/**/*.h"
21 |
22 | s.dependency "ForkInputMask", "~> 7.3.2"
23 |
24 |
25 | if new_arch_enabled then
26 | s.pod_target_xcconfig = {
27 | # This is handy when we want to detect if new arch is enabled in Swift code
28 | # and can be used like:
29 | # #if ADVANCE_INPUT_MASK_NEW_ARCH_ENABLED
30 | # // do sth when new arch is enabled
31 | # #else
32 | # // do sth when old arch is enabled
33 | # #endif
34 | "OTHER_SWIFT_FLAGS" => "-DADVANCE_INPUT_MASK_NEW_ARCH_ENABLED"
35 | }
36 | end
37 |
38 |
39 | # Use install_modules_dependencies helper to install the dependencies if React Native version >=0.71.0.
40 | # See https://github.com/facebook/react-native/blob/febf6b7f33fdb4904669f99d795eba4c0f95d7bf/scripts/cocoapods/new_architecture.rb#L79.
41 | if respond_to?(:install_modules_dependencies, true)
42 | install_modules_dependencies(s)
43 | else
44 | s.dependency "React-Core"
45 |
46 | # Don't install the dependencies when we run `pod install` in the old architecture.
47 | if ENV['RCT_NEW_ARCH_ENABLED'] == '1' then
48 | s.compiler_flags = folly_compiler_flags + " -DRCT_NEW_ARCH_ENABLED=1"
49 | s.pod_target_xcconfig = {
50 | "HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/boost\"",
51 | "OTHER_CPLUSPLUSFLAGS" => "-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1",
52 | "CLANG_CXX_LANGUAGE_STANDARD" => "c++17"
53 | }
54 | s.dependency "React-Codegen"
55 | s.dependency "RCT-Folly"
56 | s.dependency "RCTRequired"
57 | s.dependency "RCTTypeSafety"
58 | s.dependency "ReactCommon/turbomodule/core"
59 | end
60 | end
61 | end
62 |
--------------------------------------------------------------------------------
/package/android/src/oldarch/java/com/maskedtextinput/AdvancedTextInputMaskDecoratorViewManagerSpec.kt:
--------------------------------------------------------------------------------
1 | package com.maskedtextinput
2 |
3 | import android.view.View
4 | import com.facebook.react.bridge.ReadableArray
5 | import com.facebook.react.bridge.ReadableMap
6 | import com.facebook.react.uimanager.SimpleViewManager
7 |
8 | abstract class AdvancedTextInputMaskDecoratorViewManagerSpec : SimpleViewManager() {
9 | abstract fun setPrimaryMaskFormat(
10 | view: T,
11 | mask: String?,
12 | )
13 |
14 | abstract fun setCustomNotations(
15 | view: T,
16 | customNotation: ReadableArray?,
17 | )
18 |
19 | abstract fun setDefaultValue(
20 | view: T,
21 | defaultValue: String?,
22 | )
23 |
24 | abstract fun setValue(
25 | view: T,
26 | value: String?,
27 | )
28 |
29 | abstract fun setAffinityCalculationStrategy(
30 | view: T,
31 | affinityCalculationStrategy: Int,
32 | )
33 |
34 | abstract fun setAffinityFormat(
35 | view: T,
36 | affinityFormat: ReadableArray?,
37 | )
38 |
39 | abstract fun setIsRTL(
40 | view: T,
41 | isRTL: Boolean = false,
42 | )
43 |
44 | abstract fun setAutoSkip(
45 | view: T,
46 | autoSkip: Boolean = false,
47 | )
48 |
49 | abstract fun setAutocomplete(
50 | view: T,
51 | autocomplete: Boolean = false,
52 | )
53 |
54 | abstract fun setCustomTransformation(
55 | view: T,
56 | customTransformation: ReadableMap? = null,
57 | )
58 |
59 | abstract fun setAllowedKeys(
60 | view: T,
61 | allowedKeys: String? = null,
62 | )
63 |
64 | abstract fun setAllowSuggestions(
65 | view: T?,
66 | value: Boolean,
67 | )
68 |
69 | abstract fun setAutocompleteOnFocus(
70 | view: T,
71 | value: Boolean,
72 | )
73 |
74 | abstract fun setValidationRegex(
75 | view: T,
76 | validationRegex: String?,
77 | )
78 |
79 | abstract fun setText(
80 | view: T,
81 | text: String?,
82 | autocomplete: Boolean,
83 | )
84 |
85 | override fun receiveCommand(
86 | root: T,
87 | commandId: String?,
88 | args: ReadableArray?,
89 | ) {
90 | super.receiveCommand(root, commandId, args)
91 | when (commandId) {
92 | SET_TEXT -> {
93 | val text = args?.getString(0)
94 | val autocomplete = args?.getBoolean(1)
95 | if (text != null && autocomplete != null) {
96 | this.setText(root, text, autocomplete)
97 | }
98 | }
99 | }
100 | }
101 |
102 | companion object {
103 | const val SET_TEXT = "setText"
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/.github/workflows/ios-e2e-test.yml:
--------------------------------------------------------------------------------
1 | name: 🛠 iOS e2e tests
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | paths:
8 | - ".github/workflows/ios-e2e-test.yml"
9 | - "package/src/native/**"
10 | - "package/ios/**"
11 | - "yarn.lock"
12 | - "apps/example/yarn.lock"
13 | - "e2e/**"
14 | pull_request:
15 | branches:
16 | - main
17 | paths:
18 | - ".github/workflows/ios-e2e-test.yml"
19 | - "package/src/native/**"
20 | - "package/ios/**"
21 | - "yarn.lock"
22 | - "apps/example/yarn.lock"
23 | - "e2e/**"
24 |
25 | jobs:
26 | build-ios:
27 | runs-on: macOS-15
28 | steps:
29 | - name: Checkout
30 | uses: actions/checkout@v4
31 | - name: Build iOS app
32 | uses: ./.github/actions/build-ios
33 | - name: Upload iOS app
34 | uses: actions/upload-artifact@v4
35 | with:
36 | name: ios-app
37 | path: apps/example/ios/build/Build/Products/Release-iphonesimulator/MaskedTextInputExample.app/**
38 | e2e-test:
39 | name: "⚙️ Automated test cases"
40 | runs-on: macOS-15
41 | needs: build-ios
42 | steps:
43 | - name: Checkout
44 | uses: actions/checkout@v4
45 |
46 | - name: Setup Xcode
47 | uses: maxim-lobanov/setup-xcode@v1
48 | with:
49 | xcode-version: "16.2"
50 | - uses: actions/checkout@v4
51 | - name: Download ios app
52 | uses: actions/download-artifact@v4
53 | with:
54 | name: ios-app
55 | path: apps/example/ios/build/Build/Products/Release-iphonesimulator/MaskedTextInputExample.app/
56 |
57 | - name: Install Maestro CLI
58 | run: |
59 | brew tap mobile-dev-inc/tap
60 | brew install maestro
61 |
62 | - name: Install app on simulator
63 | run: |
64 | UDID=$(xcrun simctl list devices | grep "iPhone 16 Pro (" | grep -oE "[0-9A-F-]{36}" | head -n 1)
65 | open -a Simulator --args -CurrentDeviceUDID $UDID
66 | xcrun simctl bootstatus "$UDID"
67 | osascript -e 'tell application "Simulator" to activate'
68 | xcrun simctl install "$UDID" apps/example/ios/build/Build/Products/Release-iphonesimulator/MaskedTextInputExample.app
69 |
70 | - name: Run E2E tests
71 | run: |
72 | export MAESTRO_DRIVER_STARTUP_TIMEOUT=600000
73 | maestro test e2e/.maestro/* --format html ./reports/debug --debug-output ./reports/debug --flatten-debug-output
74 |
75 | - name: Upload test report
76 | if: always()
77 | uses: actions/upload-artifact@v4
78 | with:
79 | path: ./reports/debug
80 | name: e2e-report-ios
81 |
--------------------------------------------------------------------------------
/package/ios/NotifyingAdvancedTexInputMaskListener.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NotifyingAdvancedTexInputMaskListener.swift
3 | // react-native-advanced-input-mask
4 | //
5 | // Created by Ivan Ignathuk on 16/10/2024.
6 | //
7 |
8 | import ForkInputMask
9 | import Foundation
10 | import UIKit
11 |
12 | class NotifyingAdvancedTexInputMaskListener: MaskedTextInputListener {
13 | var allowedKeys = ""
14 | var validationRegex: NSRegularExpression?
15 |
16 | init(primaryFormat: String,
17 | autocomplete: Bool,
18 | autocompleteOnFocus: Bool,
19 | autoskip: Bool,
20 | rightToLeft: Bool,
21 | affineFormats: [String],
22 | affinityCalculationStrategy: AffinityCalculationStrategy,
23 | customNotations: [Notation],
24 | onMaskedTextChangedCallback: (
25 | (_ textInput: UITextInput,
26 | _ value: String,
27 | _ complete: Bool,
28 | _ tailPlaceholder: String) -> Void)?,
29 | allowSuggestions: Bool,
30 | allowedKeys: String,
31 | validationRegex: NSRegularExpression?)
32 | {
33 | self.allowedKeys = allowedKeys
34 | self.validationRegex = validationRegex
35 | super.init(
36 | primaryFormat: primaryFormat,
37 | autocomplete: autocomplete,
38 | autocompleteOnFocus: autocompleteOnFocus,
39 | autoskip: autoskip,
40 | rightToLeft: rightToLeft,
41 | affineFormats: affineFormats,
42 | affinityCalculationStrategy: affinityCalculationStrategy,
43 | customNotations: customNotations,
44 | onMaskedTextChangedCallback: onMaskedTextChangedCallback,
45 | allowSuggestions: allowSuggestions
46 | )
47 | }
48 |
49 | private func isValid(_ text: String) -> Bool {
50 | guard let validationRegex = validationRegex else {
51 | return true
52 | }
53 |
54 | let range = NSRange(location: 0, length: text.utf16.count)
55 |
56 | return validationRegex.firstMatch(in: text, options: [], range: range) != nil
57 | }
58 |
59 | override func textField(
60 | _ textField: UITextField,
61 | shouldChangeCharactersIn range: NSRange,
62 | replacementString string: String
63 | ) -> Bool {
64 | let newText: String = allowedKeys.isEmpty
65 | ? string
66 | : String(string.filter { allowedKeys.contains($0) })
67 | let nextTextFieldText = ((textField.text ?? "") as NSString).replacingCharacters(in: range, with: newText)
68 |
69 | if !isValid(nextTextFieldText) {
70 | return false
71 | }
72 |
73 | defer {
74 | NotificationCenter.default.post(name: UITextField.textDidChangeNotification, object: textField)
75 | textField.sendActions(for: .editingChanged)
76 | }
77 |
78 | return super.textField(textField, shouldChangeCharactersIn: range, replacementString: newText)
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/apps/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-native-advanced-input-mask-example",
3 | "version": "0.0.1",
4 | "private": true,
5 | "scripts": {
6 | "android": "react-native run-android",
7 | "ios": "react-native run-ios",
8 | "web": "./node_modules/.bin/webpack-dev-server",
9 | "start": "react-native start",
10 | "build:android": "cd android && ./gradlew assembleRelease --build-cache",
11 | "build:ios": "cd ios && xcodebuild -workspace MaskedTextInputExample.xcworkspace -scheme MaskedTextInputExample -configuration Release -destination 'platform=iOS Simulator,name=iPhone 16 Pro' -sdk iphonesimulator -derivedDataPath build -UseModernBuildSystem=YES CC=clang CPLUSPLUS=clang++ LD=clang LDPLUSPLUS=clang++ | xcpretty",
12 | "build:web": "./node_modules/.bin/webpack"
13 | },
14 | "resolutions": {
15 | "react": "19.1.0",
16 | "react-dom": "19.1.0"
17 | },
18 | "dependencies": {
19 | "@babel/plugin-proposal-export-namespace-from": "^7.18.9",
20 | "@react-navigation/native": "7.1.9",
21 | "@react-navigation/native-stack": "7.3.13",
22 | "babel-plugin-react-compiler": "19.1.0-rc.2",
23 | "react": "19.1.0",
24 | "react-dom": "19.1.0",
25 | "react-native": "0.81.4",
26 | "react-native-advanced-input-mask": "*",
27 | "react-native-edge-to-edge": "^1.7.0",
28 | "react-native-gesture-handler": "^2.28.0",
29 | "react-native-keyboard-controller": "^1.18.6",
30 | "react-native-reanimated": "4.1.0",
31 | "react-native-safe-area-context": "5.6.1",
32 | "react-native-screens": "^4.15.4",
33 | "react-native-web": "0.21.1",
34 | "react-native-worklets": "^0.5.1"
35 | },
36 | "devDependencies": {
37 | "@babel/core": "^7.27.1",
38 | "@babel/plugin-proposal-class-properties": "^7.18.6",
39 | "@babel/preset-env": "^7.27.2",
40 | "@babel/preset-react": "^7.27.1",
41 | "@babel/runtime": "^7.27.1",
42 | "@react-native-community/cli": "20.0.2",
43 | "@react-native-community/cli-platform-android": "20.0.2",
44 | "@react-native-community/cli-platform-ios": "20.0.2",
45 | "@react-native/babel-preset": "0.81.4",
46 | "@react-native/eslint-config": "0.81.4",
47 | "@react-native/metro-config": "0.81.4",
48 | "@react-native/typescript-config": "0.81.4",
49 | "@rnx-kit/metro-config": "^2.1.2",
50 | "@types/react-dom": "^19",
51 | "@types/react-native-web": "0.19.1",
52 | "babel-loader": "^9.2.1",
53 | "babel-plugin-module-resolver": "^5.0.0",
54 | "babel-plugin-react-native-web": "0.21.1",
55 | "pod-install": "^0.1.0",
56 | "react-scripts": "^5.0.1",
57 | "ts-loader": "^9.5.2",
58 | "url-loader": "^4.1.1",
59 | "webpack": "^5.97.1",
60 | "webpack-cli": "^6.0.1",
61 | "webpack-dev-server": "^5.2.0"
62 | },
63 | "engines": {
64 | "node": ">=20"
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/package/ios/.clang-format:
--------------------------------------------------------------------------------
1 | ---
2 | AccessModifierOffset: -1
3 | AlignAfterOpenBracket: AlwaysBreak
4 | AlignConsecutiveAssignments: false
5 | AlignConsecutiveDeclarations: false
6 | AlignEscapedNewlinesLeft: true
7 | AlignOperands: false
8 | AlignTrailingComments: false
9 | AllowAllParametersOfDeclarationOnNextLine: false
10 | AllowShortBlocksOnASingleLine: false
11 | AllowShortCaseLabelsOnASingleLine: false
12 | AllowShortFunctionsOnASingleLine: Empty
13 | AllowShortIfStatementsOnASingleLine: false
14 | AllowShortLoopsOnASingleLine: false
15 | AlwaysBreakAfterReturnType: None
16 | AlwaysBreakBeforeMultilineStrings: true
17 | AlwaysBreakTemplateDeclarations: true
18 | BinPackArguments: false
19 | BinPackParameters: false
20 | BraceWrapping:
21 | AfterClass: false
22 | AfterControlStatement: false
23 | AfterEnum: false
24 | AfterFunction: false
25 | AfterNamespace: false
26 | AfterObjCDeclaration: false
27 | AfterStruct: false
28 | AfterUnion: false
29 | BeforeCatch: false
30 | BeforeElse: false
31 | IndentBraces: false
32 | BreakBeforeBinaryOperators: None
33 | BreakBeforeBraces: Attach
34 | BreakBeforeTernaryOperators: true
35 | BreakConstructorInitializersBeforeComma: false
36 | BreakAfterJavaFieldAnnotations: false
37 | BreakStringLiterals: false
38 | ColumnLimit: 80
39 | CommentPragmas: "^ IWYU pragma:"
40 | ConstructorInitializerAllOnOneLineOrOnePerLine: true
41 | ConstructorInitializerIndentWidth: 4
42 | ContinuationIndentWidth: 4
43 | Cpp11BracedListStyle: true
44 | DerivePointerAlignment: false
45 | DisableFormat: false
46 | ForEachMacros: [FOR_EACH_RANGE, FOR_EACH]
47 | IncludeCategories:
48 | - Regex: '^<.*\.h(pp)?>'
49 | Priority: 1
50 | - Regex: "^<.*"
51 | Priority: 2
52 | - Regex: ".*"
53 | Priority: 3
54 | IndentCaseLabels: true
55 | IndentWidth: 2
56 | IndentWrappedFunctionNames: false
57 | KeepEmptyLinesAtTheStartOfBlocks: false
58 | MacroBlockBegin: ""
59 | MacroBlockEnd: ""
60 | MaxEmptyLinesToKeep: 1
61 | NamespaceIndentation: None
62 | ObjCBlockIndentWidth: 2
63 | ObjCSpaceAfterProperty: true
64 | ObjCSpaceBeforeProtocolList: true
65 | PenaltyBreakBeforeFirstCallParameter: 1
66 | PenaltyBreakComment: 300
67 | PenaltyBreakFirstLessLess: 120
68 | PenaltyBreakString: 1000
69 | PenaltyExcessCharacter: 1000000
70 | PenaltyReturnTypeOnItsOwnLine: 200
71 | PointerAlignment: Right
72 | ReflowComments: true
73 | SortIncludes: true
74 | SpaceAfterCStyleCast: false
75 | SpaceBeforeAssignmentOperators: true
76 | SpaceBeforeParens: ControlStatements
77 | SpaceInEmptyParentheses: false
78 | SpacesBeforeTrailingComments: 1
79 | SpacesInAngles: false
80 | SpacesInContainerLiterals: true
81 | SpacesInCStyleCastParentheses: false
82 | SpacesInParentheses: false
83 | SpacesInSquareBrackets: false
84 | Standard: Cpp11
85 | TabWidth: 8
86 | UseTab: Never
87 | ---
88 | Language: ObjC
89 | ColumnLimit: 100
90 | BreakBeforeBraces: WebKit
91 |
--------------------------------------------------------------------------------
/apps/example/src/components/BaseTextInput/index.tsx:
--------------------------------------------------------------------------------
1 | import { forwardRef, memo, useCallback, useMemo } from "react";
2 | import React from "react";
3 | import {
4 | type BlurEvent,
5 | type FocusEvent,
6 | TextInput,
7 | type TextInputProps,
8 | } from "react-native";
9 | import Reanimated, {
10 | interpolateColor,
11 | useAnimatedStyle,
12 | useSharedValue,
13 | withTiming,
14 | } from "react-native-reanimated";
15 |
16 | import styles, {
17 | DEFAULT_BACKGROUND_COLOR,
18 | DEFAULT_BORDER_COLOR,
19 | FOCUSED_BORDER_COLOR,
20 | PLACEHOLDER_COLOR,
21 | PRESSED_BACKGROUND_COLOR,
22 | } from "./styles";
23 |
24 | const AnimatedTextInputView = Reanimated.createAnimatedComponent(TextInput);
25 |
26 | const BaseTextInput = forwardRef(
27 | ({ onBlur, onFocus, style, ...rest }, ref) => {
28 | const progress = useSharedValue(0);
29 | const backgroundColorProgress = useSharedValue(0);
30 |
31 | const animateTo = useCallback(
32 | (focused: boolean) => {
33 | progress.set(withTiming(focused ? 1 : 0, { duration: 350 }));
34 | },
35 | [progress],
36 | );
37 |
38 | const handleFocus = useCallback(
39 | (e: FocusEvent) => {
40 | animateTo(true);
41 | onFocus?.(e);
42 | },
43 | [onFocus, animateTo],
44 | );
45 |
46 | const handleBlur = useCallback(
47 | (e: BlurEvent) => {
48 | animateTo(false);
49 | onBlur?.(e);
50 | },
51 | [onBlur, animateTo],
52 | );
53 |
54 | const handlePressIn = useCallback(() => {
55 | if (progress.get() === 0) {
56 | backgroundColorProgress.set(withTiming(1, { duration: 150 }));
57 | }
58 | }, [backgroundColorProgress, progress]);
59 |
60 | const handlePressOut = useCallback(() => {
61 | backgroundColorProgress.set(withTiming(0, { duration: 450 }));
62 | }, [backgroundColorProgress]);
63 |
64 | const animatedStyle = useAnimatedStyle(
65 | () => ({
66 | backgroundColor: interpolateColor(
67 | backgroundColorProgress.get(),
68 | [0, 1],
69 | [DEFAULT_BACKGROUND_COLOR, PRESSED_BACKGROUND_COLOR],
70 | ),
71 | borderColor: interpolateColor(
72 | progress.get(),
73 | [0, 1],
74 | [DEFAULT_BORDER_COLOR, FOCUSED_BORDER_COLOR],
75 | ),
76 | }),
77 | [backgroundColorProgress],
78 | );
79 |
80 | const inputStyle = useMemo(
81 | () => [styles.input, style, animatedStyle],
82 | [style, animatedStyle],
83 | );
84 |
85 | return (
86 |
96 | );
97 | },
98 | );
99 |
100 | export default memo(BaseTextInput);
101 |
--------------------------------------------------------------------------------
/apps/example/README.md:
--------------------------------------------------------------------------------
1 | This is a new [**React Native**](https://reactnative.dev) project, bootstrapped using [`@react-native-community/cli`](https://github.com/react-native-community/cli).
2 |
3 | # Getting Started
4 |
5 | > **Note**: Make sure you have completed the [React Native - Environment Setup](https://reactnative.dev/docs/environment-setup) instructions till "Creating a new application" step, before proceeding.
6 |
7 | ## Step 1: Start the Metro Server
8 |
9 | First, you will need to start **Metro**, the JavaScript _bundler_ that ships _with_ React Native.
10 |
11 | To start Metro, run the following command from the _root_ of your React Native project:
12 |
13 | ```bash
14 | # using npm
15 | npm start
16 |
17 | # OR using Yarn
18 | yarn start
19 | ```
20 |
21 | ## Step 2: Start your Application
22 |
23 | Let Metro Bundler run in its _own_ terminal. Open a _new_ terminal from the _root_ of your React Native project. Run the following command to start your _Android_ or _iOS_ app:
24 |
25 | ### For Android
26 |
27 | ```bash
28 | # using npm
29 | npm run android
30 |
31 | # OR using Yarn
32 | yarn android
33 | ```
34 |
35 | ### For iOS
36 |
37 | ```bash
38 | # using npm
39 | npm run ios
40 |
41 | # OR using Yarn
42 | yarn ios
43 | ```
44 |
45 | If everything is set up _correctly_, you should see your new app running in your _Android Emulator_ or _iOS Simulator_ shortly provided you have set up your emulator/simulator correctly.
46 |
47 | This is one way to run your app — you can also run it directly from within Android Studio and Xcode respectively.
48 |
49 | ## Step 3: Modifying your App
50 |
51 | Now that you have successfully run the app, let's modify it.
52 |
53 | 1. Open `App.tsx` in your text editor of choice and edit some lines.
54 | 2. For **Android**: Press the R key twice or select **"Reload"** from the **Developer Menu** (Ctrl + M (on Window and Linux) or Cmd ⌘ + M (on macOS)) to see your changes!
55 |
56 | For **iOS**: Hit Cmd ⌘ + R in your iOS Simulator to reload the app and see your changes!
57 |
58 | ## Congratulations! :tada:
59 |
60 | You've successfully run and modified your React Native App. :partying_face:
61 |
62 | ### Now what?
63 |
64 | - If you want to add this new React Native code to an existing application, check out the [Integration guide](https://reactnative.dev/docs/integration-with-existing-apps).
65 | - If you're curious to learn more about React Native, check out the [Introduction to React Native](https://reactnative.dev/docs/getting-started).
66 |
67 | # Troubleshooting
68 |
69 | If you can't get this to work, see the [Troubleshooting](https://reactnative.dev/docs/troubleshooting) page.
70 |
71 | # Learn More
72 |
73 | To learn more about React Native, take a look at the following resources:
74 |
75 | - [React Native Website](https://reactnative.dev) - learn more about React Native.
76 | - [Getting Started](https://reactnative.dev/docs/environment-setup) - an **overview** of React Native and how setup your environment.
77 | - [Learn the Basics](https://reactnative.dev/docs/getting-started) - a **guided tour** of the React Native **basics**.
78 | - [Blog](https://reactnative.dev/blog) - read the latest official React Native **Blog** posts.
79 | - [`@facebook/react-native`](https://github.com/facebook/react-native) - the Open Source; GitHub **repository** for React Native.
80 |
--------------------------------------------------------------------------------
/.github/workflows/size-diff-calculation.yml:
--------------------------------------------------------------------------------
1 | name: 🧪 Size calculation
2 |
3 | on:
4 | pull_request:
5 | paths-ignore:
6 | - "example/**"
7 | - "WebExample/**"
8 | - "gifs/**"
9 |
10 | permissions:
11 | contents: read
12 | pull-requests: write
13 |
14 | jobs:
15 | calculate-sizes:
16 | name: 🔬 Calculate package sizes
17 | runs-on: ubuntu-latest
18 | steps:
19 | - name: Checkout to target branch
20 | uses: actions/checkout@v4
21 | with:
22 | ref: ${{ github.event.pull_request.base.ref }}
23 |
24 | - uses: actions/setup-node@v4
25 | with:
26 | node-version: 18.19.0
27 | cache: yarn
28 |
29 | - name: Install dependencies
30 | run: yarn install --frozen-lockfile
31 |
32 | - name: Calculate size of package (target)
33 | id: old-size
34 | run: echo "OLD_SIZE=$(node scripts/size.js)" >> "$GITHUB_OUTPUT"
35 |
36 | - uses: actions/checkout@v4
37 |
38 | - name: Install dependencies
39 | run: yarn install --frozen-lockfile
40 |
41 | - name: Calculate size of package (current)
42 | id: new-size
43 | run: echo "NEW_SIZE=$(node scripts/size.js)" >> "$GITHUB_OUTPUT"
44 |
45 | - name: Calculate difference
46 | id: diff
47 | env:
48 | NEW_SIZE: ${{ steps.new-size.outputs.NEW_SIZE }}
49 | OLD_SIZE: ${{ steps.old-size.outputs.OLD_SIZE }}
50 | run: |
51 | echo "DIFF=$((NEW_SIZE - OLD_SIZE))" >> "$GITHUB_OUTPUT"
52 | echo "SIGN=$([ $((NEW_SIZE - OLD_SIZE)) -gt 0 ] && echo "📈" || echo "📉")" >> "$GITHUB_OUTPUT"
53 |
54 | - name: Create size data files
55 | run: |
56 | echo "${{ steps.new-size.outputs.NEW_SIZE }}" > new-size.txt
57 | echo "${{ steps.old-size.outputs.OLD_SIZE }}" > old-size.txt
58 | echo "${{ steps.diff.outputs.DIFF }}" > diff.txt
59 | echo "${{ steps.diff.outputs.SIGN }}" > sign.txt
60 | echo "${{ github.event.pull_request.number }}" > pr-number.txt
61 |
62 | - name: Upload size data
63 | uses: actions/upload-artifact@v4
64 | with:
65 | name: size-data
66 | path: |
67 | new-size.txt
68 | old-size.txt
69 | diff.txt
70 | sign.txt
71 | pr-number.txt
72 | retention-days: 1
73 |
74 | - name: Read size data
75 | id: size-data
76 | run: |
77 | echo "NEW_SIZE=$(cat new-size.txt)" >> "$GITHUB_OUTPUT"
78 | echo "OLD_SIZE=$(cat old-size.txt)" >> "$GITHUB_OUTPUT"
79 | echo "DIFF=$(cat diff.txt)" >> "$GITHUB_OUTPUT"
80 | echo "SIGN=$(cat sign.txt)" >> "$GITHUB_OUTPUT"
81 | echo "PR_NUMBER=$(cat pr-number.txt)" >> "$GITHUB_OUTPUT"
82 |
83 | - name: 📝 Create size diff comment
84 | uses: marocchino/sticky-pull-request-comment@v2
85 | with:
86 | recreate: true
87 | number: ${{ steps.size-data.outputs.PR_NUMBER }}
88 | message: |
89 | ### 📊 Package size report
90 | | Current size | Target Size | Difference |
91 | | ------------- | ------------- | ------------------------ |
92 | | ${{ steps.size-data.outputs.NEW_SIZE }} bytes | ${{ steps.size-data.outputs.OLD_SIZE }} bytes | ${{ steps.size-data.outputs.DIFF }} bytes ${{ steps.size-data.outputs.SIGN }} |
93 |
--------------------------------------------------------------------------------
/package/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-native-advanced-input-mask",
3 | "version": "1.4.6",
4 | "description": "Text input mask for React Native on iOS, Android and web. Synchronous and easy formatting without hustle",
5 | "main": "lib/commonjs/index",
6 | "module": "lib/module/index",
7 | "types": "lib/typescript/src/index.d.ts",
8 | "react-native": "src/index",
9 | "source": "src/index",
10 | "files": [
11 | "src",
12 | "lib",
13 | "android",
14 | "ios",
15 | "cpp",
16 | "react-native-advanced-input-mask.podspec",
17 | "jest",
18 | "!ios/build",
19 | "!android/build",
20 | "!android/gradle",
21 | "!android/gradlew",
22 | "!android/gradlew.bat",
23 | "!android/local.properties",
24 | "!android/.editorconfig",
25 | "!lib/typescript/WebExample",
26 | "!ios/.swiftlint.yml",
27 | "!ios/.swiftformat",
28 | "!ios/.clang-format",
29 | "!**/__tests__",
30 | "!**/__fixtures__",
31 | "!**/__mocks__",
32 | "!**/.*"
33 | ],
34 | "repository": {
35 | "type": "git",
36 | "url": "git+https://github.com/IvanIhnatsiuk/react-native-advanced-input-mask.git"
37 | },
38 | "author": "IvanIhnatsiuk (https://github.com/IvanIhnatsiuk)",
39 | "license": "MIT",
40 | "bugs": {
41 | "url": "https://github.com/IvanIhnatsiuk/react-native-advanced-input-mask/issues"
42 | },
43 | "homepage": "https://github.com/IvanIhnatsiuk/react-native-advanced-input-mask#readme",
44 | "publishConfig": {
45 | "registry": "https://registry.npmjs.org/"
46 | },
47 | "scripts": {
48 | "prepare": "bob build",
49 | "prepack": "cp ../README.md ./README.md && cp ../LICENSE ./LICENSE"
50 | },
51 | "keywords": [
52 | "react-native",
53 | "ios",
54 | "android",
55 | "web",
56 | "text-input",
57 | "masking",
58 | "input",
59 | "react-native-component",
60 | "form",
61 | "validation",
62 | "formatting",
63 | "phone input",
64 | "credit card input",
65 | "date input",
66 | "time input",
67 | "mask"
68 | ],
69 | "devDependencies": {
70 | "@babel/core": "^7.26.7",
71 | "@react-native/eslint-config": "0.79.0",
72 | "@types/jest": "^28.1.2",
73 | "@types/react": "19.1.0",
74 | "jest": "^28.1.1",
75 | "react": "19.0.0",
76 | "react-dom": "19.0.0",
77 | "react-native": "0.80.0",
78 | "react-native-builder-bob": "^0.20.0",
79 | "react-native-web": "0.20.0",
80 | "typescript": "^5.0.2"
81 | },
82 | "peerDependencies": {
83 | "react": "*",
84 | "react-native": "*"
85 | },
86 | "eslintIgnore": [
87 | "node_modules/",
88 | "lib/"
89 | ],
90 | "react-native-builder-bob": {
91 | "source": "src",
92 | "output": "lib",
93 | "targets": [
94 | "commonjs",
95 | "module",
96 | [
97 | "typescript",
98 | {
99 | "project": "tsconfig.build.json"
100 | }
101 | ]
102 | ]
103 | },
104 | "codegenConfig": {
105 | "name": "maskedtextinput",
106 | "type": "all",
107 | "jsSrcsDir": "./src/native/specs",
108 | "android": {
109 | "javaPackageName": "com.maskedtextinput"
110 | },
111 | "ios": {
112 | "componentProvider": {
113 | "AdvancedTextInputMaskDecoratorView": "AdvancedTextInputMaskDecoratorView"
114 | }
115 | }
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/package/android/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | // Buildscript is evaluated before everything else so we can't use getExtOrDefault
3 | def kotlin_version = rootProject.ext.has("kotlinVersion") ? rootProject.ext.get("kotlinVersion") : project.properties["MaskedTextInput_kotlinVersion"]
4 |
5 | repositories {
6 | google()
7 | mavenCentral()
8 | }
9 |
10 | dependencies {
11 | classpath "com.android.tools.build:gradle:7.2.1"
12 | // noinspection DifferentKotlinGradleVersion
13 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
14 | }
15 | }
16 |
17 | def isNewArchitectureEnabled() {
18 | return rootProject.hasProperty("newArchEnabled") && rootProject.getProperty("newArchEnabled") == "true"
19 | }
20 |
21 | apply plugin: "com.android.library"
22 | apply plugin: "kotlin-android"
23 |
24 | if (isNewArchitectureEnabled()) {
25 | apply plugin: "com.facebook.react"
26 | }
27 |
28 | def getExtOrDefault(name) {
29 | return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties["MaskedTextInput_" + name]
30 | }
31 |
32 | def getExtOrIntegerDefault(name) {
33 | return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["MaskedTextInput_" + name]).toInteger()
34 | }
35 |
36 | def supportsNamespace() {
37 | def parsed = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION.tokenize('.')
38 | def major = parsed[0].toInteger()
39 | def minor = parsed[1].toInteger()
40 |
41 | // Namespace support was added in 7.3.0
42 | return (major == 7 && minor >= 3) || major >= 8
43 | }
44 |
45 | android {
46 | if (supportsNamespace()) {
47 | namespace "com.maskedtextinput"
48 |
49 | sourceSets {
50 | main {
51 | manifest.srcFile "src/main/AndroidManifestNew.xml"
52 | }
53 | }
54 | }
55 |
56 | compileSdkVersion getExtOrIntegerDefault("compileSdkVersion")
57 |
58 | defaultConfig {
59 | minSdkVersion getExtOrIntegerDefault("minSdkVersion")
60 | targetSdkVersion getExtOrIntegerDefault("targetSdkVersion")
61 |
62 | }
63 |
64 | buildTypes {
65 | release {
66 | minifyEnabled false
67 | }
68 | }
69 |
70 | lintOptions {
71 | disable "GradleCompatible"
72 | }
73 |
74 | compileOptions {
75 | sourceCompatibility JavaVersion.VERSION_1_8
76 | targetCompatibility JavaVersion.VERSION_1_8
77 | }
78 |
79 | sourceSets {
80 | main {
81 | if (isNewArchitectureEnabled()) {
82 | java.srcDirs += [
83 | "src/newarch",
84 | // This is needed to build Kotlin project with NewArch enabled
85 | "${project.buildDir}/generated/source/codegen/java"
86 | ]
87 | } else {
88 | java.srcDirs += ["src/oldarch"]
89 | }
90 | }
91 | }
92 | }
93 |
94 | repositories {
95 | mavenCentral()
96 | google()
97 | maven { url 'https://jitpack.io' }
98 | }
99 |
100 | def kotlin_version = getExtOrDefault("kotlinVersion")
101 |
102 | dependencies {
103 | // For < 0.71, this will be from the local maven repo
104 | // For > 0.71, this will be replaced by `com.facebook.react:react-android:$version` by react gradle plugin
105 | //noinspection GradleDynamicVersion
106 | implementation "com.facebook.react:react-native:+"
107 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
108 | implementation 'com.github.IvanIhnatsiuk:input-mask-android:7.2.5'
109 | }
110 |
111 |
--------------------------------------------------------------------------------
/apps/example/android/gradlew.bat:
--------------------------------------------------------------------------------
1 | @REM Copyright (c) Meta Platforms, Inc. and affiliates.
2 | @REM
3 | @REM This source code is licensed under the MIT license found in the
4 | @REM LICENSE file in the root directory of this source tree.
5 |
6 | @rem
7 | @rem Copyright 2015 the original author or authors.
8 | @rem
9 | @rem Licensed under the Apache License, Version 2.0 (the "License");
10 | @rem you may not use this file except in compliance with the License.
11 | @rem You may obtain a copy of the License at
12 | @rem
13 | @rem https://www.apache.org/licenses/LICENSE-2.0
14 | @rem
15 | @rem Unless required by applicable law or agreed to in writing, software
16 | @rem distributed under the License is distributed on an "AS IS" BASIS,
17 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18 | @rem See the License for the specific language governing permissions and
19 | @rem limitations under the License.
20 | @rem
21 | @rem SPDX-License-Identifier: Apache-2.0
22 | @rem
23 |
24 | @if "%DEBUG%"=="" @echo off
25 | @rem ##########################################################################
26 | @rem
27 | @rem Gradle startup script for Windows
28 | @rem
29 | @rem ##########################################################################
30 |
31 | @rem Set local scope for the variables with windows NT shell
32 | if "%OS%"=="Windows_NT" setlocal
33 |
34 | set DIRNAME=%~dp0
35 | if "%DIRNAME%"=="" set DIRNAME=.
36 | @rem This is normally unused
37 | set APP_BASE_NAME=%~n0
38 | set APP_HOME=%DIRNAME%
39 |
40 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
41 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
42 |
43 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
44 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
45 |
46 | @rem Find java.exe
47 | if defined JAVA_HOME goto findJavaFromJavaHome
48 |
49 | set JAVA_EXE=java.exe
50 | %JAVA_EXE% -version >NUL 2>&1
51 | if %ERRORLEVEL% equ 0 goto execute
52 |
53 | echo. 1>&2
54 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
55 | echo. 1>&2
56 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
57 | echo location of your Java installation. 1>&2
58 |
59 | goto fail
60 |
61 | :findJavaFromJavaHome
62 | set JAVA_HOME=%JAVA_HOME:"=%
63 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
64 |
65 | if exist "%JAVA_EXE%" goto execute
66 |
67 | echo. 1>&2
68 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
69 | echo. 1>&2
70 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
71 | echo location of your Java installation. 1>&2
72 |
73 | goto fail
74 |
75 | :execute
76 | @rem Setup the command line
77 |
78 | set CLASSPATH=
79 |
80 |
81 | @rem Execute Gradle
82 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
83 |
84 | :end
85 | @rem End local scope for the variables with windows NT shell
86 | if %ERRORLEVEL% equ 0 goto mainEnd
87 |
88 | :fail
89 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
90 | rem the _cmd.exe /c_ return code!
91 | set EXIT_CODE=%ERRORLEVEL%
92 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
93 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
94 | exit /b %EXIT_CODE%
95 |
96 | :mainEnd
97 | if "%OS%"=="Windows_NT" endlocal
98 |
99 | :omega
--------------------------------------------------------------------------------
/apps/example/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GEM
2 | remote: https://rubygems.org/
3 | specs:
4 | CFPropertyList (3.0.7)
5 | base64
6 | nkf
7 | rexml
8 | activesupport (7.1.5.1)
9 | base64
10 | benchmark (>= 0.3)
11 | bigdecimal
12 | concurrent-ruby (~> 1.0, >= 1.0.2)
13 | connection_pool (>= 2.2.5)
14 | drb
15 | i18n (>= 1.6, < 2)
16 | logger (>= 1.4.2)
17 | minitest (>= 5.1)
18 | mutex_m
19 | securerandom (>= 0.3)
20 | tzinfo (~> 2.0)
21 | addressable (2.8.7)
22 | public_suffix (>= 2.0.2, < 7.0)
23 | algoliasearch (1.27.5)
24 | httpclient (~> 2.8, >= 2.8.3)
25 | json (>= 1.5.1)
26 | atomos (0.1.3)
27 | base64 (0.2.0)
28 | benchmark (0.4.0)
29 | bigdecimal (3.1.9)
30 | claide (1.1.0)
31 | cocoapods (1.15.2)
32 | addressable (~> 2.8)
33 | claide (>= 1.0.2, < 2.0)
34 | cocoapods-core (= 1.15.2)
35 | cocoapods-deintegrate (>= 1.0.3, < 2.0)
36 | cocoapods-downloader (>= 2.1, < 3.0)
37 | cocoapods-plugins (>= 1.0.0, < 2.0)
38 | cocoapods-search (>= 1.0.0, < 2.0)
39 | cocoapods-trunk (>= 1.6.0, < 2.0)
40 | cocoapods-try (>= 1.1.0, < 2.0)
41 | colored2 (~> 3.1)
42 | escape (~> 0.0.4)
43 | fourflusher (>= 2.3.0, < 3.0)
44 | gh_inspector (~> 1.0)
45 | molinillo (~> 0.8.0)
46 | nap (~> 1.0)
47 | ruby-macho (>= 2.3.0, < 3.0)
48 | xcodeproj (>= 1.23.0, < 2.0)
49 | cocoapods-core (1.15.2)
50 | activesupport (>= 5.0, < 8)
51 | addressable (~> 2.8)
52 | algoliasearch (~> 1.0)
53 | concurrent-ruby (~> 1.1)
54 | fuzzy_match (~> 2.0.4)
55 | nap (~> 1.0)
56 | netrc (~> 0.11)
57 | public_suffix (~> 4.0)
58 | typhoeus (~> 1.0)
59 | cocoapods-deintegrate (1.0.5)
60 | cocoapods-downloader (2.1)
61 | cocoapods-plugins (1.0.0)
62 | nap
63 | cocoapods-search (1.0.1)
64 | cocoapods-trunk (1.6.0)
65 | nap (>= 0.8, < 2.0)
66 | netrc (~> 0.11)
67 | cocoapods-try (1.2.0)
68 | colored2 (3.1.2)
69 | concurrent-ruby (1.3.3)
70 | connection_pool (2.5.0)
71 | drb (2.2.1)
72 | escape (0.0.4)
73 | ethon (0.16.0)
74 | ffi (>= 1.15.0)
75 | ffi (1.17.1)
76 | fourflusher (2.3.1)
77 | fuzzy_match (2.0.4)
78 | gh_inspector (1.1.3)
79 | httpclient (2.8.3)
80 | i18n (1.14.6)
81 | concurrent-ruby (~> 1.0)
82 | json (2.9.1)
83 | logger (1.6.5)
84 | minitest (5.25.4)
85 | molinillo (0.8.0)
86 | mutex_m (0.3.0)
87 | nanaimo (0.3.0)
88 | nap (1.1.0)
89 | netrc (0.11.0)
90 | nkf (0.2.0)
91 | public_suffix (4.0.7)
92 | rexml (3.4.0)
93 | ruby-macho (2.5.1)
94 | securerandom (0.3.2)
95 | typhoeus (1.4.1)
96 | ethon (>= 0.9.0)
97 | tzinfo (2.0.6)
98 | concurrent-ruby (~> 1.0)
99 | xcodeproj (1.25.1)
100 | CFPropertyList (>= 2.3.3, < 4.0)
101 | atomos (~> 0.1.3)
102 | claide (>= 1.0.2, < 2.0)
103 | colored2 (~> 3.1)
104 | nanaimo (~> 0.3.0)
105 | rexml (>= 3.3.6, < 4.0)
106 |
107 | PLATFORMS
108 | ruby
109 |
110 | DEPENDENCIES
111 | activesupport (>= 6.1.7.5, != 7.1.0)
112 | benchmark
113 | bigdecimal
114 | cocoapods (>= 1.13, != 1.15.1, != 1.15.0)
115 | concurrent-ruby (< 1.3.4)
116 | logger
117 | mutex_m
118 | xcodeproj (< 1.26.0)
119 |
120 | RUBY VERSION
121 | ruby 2.7.5p203
122 |
123 | BUNDLED WITH
124 | 2.4.22
125 |
--------------------------------------------------------------------------------
/apps/example/src/components/TextInput/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { type FC, memo } from "react";
2 | import {
3 | type NativeSyntheticEvent,
4 | Text,
5 | type TextInputFocusEventData,
6 | } from "react-native";
7 | import {
8 | MaskedTextInput,
9 | type MaskedTextInputProps,
10 | } from "react-native-advanced-input-mask";
11 |
12 | import BaseTextInput from "../BaseTextInput";
13 | import Button from "../Button";
14 |
15 | import styles from "./styles";
16 |
17 | import type { MaskedTextInputRef } from "package/src/types";
18 |
19 | type Props = MaskedTextInputProps & {
20 | controlled?: boolean;
21 | initialValue?: string;
22 | };
23 |
24 | const TextInput: FC = (props) => {
25 | const {
26 | controlled = false,
27 | initialValue,
28 | onChangeText,
29 | onFocus,
30 | onBlur,
31 | style,
32 | defaultValue,
33 | ...rest
34 | } = props;
35 |
36 | const inputRef = React.useRef(null);
37 |
38 | const [textState, setTextState] = React.useState({
39 | extracted: initialValue || defaultValue,
40 | formatted: initialValue || defaultValue,
41 | });
42 |
43 | const [focused, setFocused] = React.useState(false);
44 |
45 | const handleTextChange = React.useCallback(
46 | (
47 | formatted: string,
48 | extracted: string,
49 | tailPlaceholder: string,
50 | complete: boolean,
51 | ) => {
52 | onChangeText?.(formatted, extracted, tailPlaceholder, complete);
53 | setTextState({ extracted, formatted });
54 | },
55 | [onChangeText],
56 | );
57 |
58 | const handleFocus = React.useCallback(
59 | (e: NativeSyntheticEvent) => {
60 | setFocused(true);
61 | onFocus?.(e);
62 | },
63 | [onFocus],
64 | );
65 |
66 | const handleBlur = React.useCallback(
67 | (e: NativeSyntheticEvent) => {
68 | setFocused(false);
69 | onBlur?.(e);
70 | },
71 | [onBlur],
72 | );
73 |
74 | const handleClearTextButtonPress = React.useCallback(() => {
75 | if (!controlled) {
76 | inputRef.current?.clear();
77 | }
78 | setTextState({ extracted: "", formatted: "" });
79 | }, [controlled]);
80 |
81 | const handleSetText = React.useCallback(() => {
82 | inputRef.current?.setText("999999", false);
83 | }, []);
84 |
85 | const handleFocusButtonPress = React.useCallback(() => {
86 | inputRef.current?.focus();
87 | }, []);
88 |
89 | return (
90 | <>
91 | extracted value {textState.extracted}
92 | formatted value {textState.formatted}
93 | focused {focused ? "Yes" : "No"}
94 |
105 |
110 |
115 |
120 | >
121 | );
122 | };
123 |
124 | export default memo(TextInput);
125 |
--------------------------------------------------------------------------------
/apps/example/ios/MaskedTextInputExample.xcodeproj/xcshareddata/xcschemes/MaskedTextInputExample.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
33 |
39 |
40 |
41 |
42 |
43 |
53 |
55 |
61 |
62 |
63 |
64 |
70 |
72 |
78 |
79 |
80 |
81 |
83 |
84 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/apps/example/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require("path");
2 |
3 | const webpack = require("webpack");
4 | const appDirectory = path.resolve(__dirname, ".");
5 |
6 | const pack = require("../../package/package.json");
7 | const modules = Object.keys(pack.peerDependencies);
8 |
9 | const babelLoaderConfiguration = {
10 | test: /\.(js|jsx|ts|tsx)$/,
11 | include: [
12 | path.resolve(appDirectory, "index.web.js"),
13 | path.resolve(appDirectory, "src"),
14 | path.resolve(__dirname, "../../package/src"),
15 | path.resolve(__dirname, "node_modules/react-native-gesture-handler"),
16 | path.resolve(__dirname, "node_modules/react-native-reanimated"),
17 | ],
18 | use: {
19 | loader: "babel-loader",
20 | options: {
21 | configFile: true,
22 | cacheDirectory: true,
23 | presets: [
24 | ["@babel/preset-env", { loose: true }],
25 | "@babel/preset-react",
26 | "@babel/preset-typescript",
27 | ],
28 | plugins: [
29 | ["@babel/plugin-transform-class-properties", { loose: true }],
30 | ["@babel/plugin-transform-private-methods", { loose: true }],
31 | ["@babel/plugin-transform-private-property-in-object", { loose: true }],
32 | "@babel/plugin-transform-flow-strip-types",
33 | ],
34 | },
35 | },
36 | };
37 |
38 | const imageLoaderConfiguration = {
39 | test: /\.(gif|jpe?g|png|svg)$/,
40 | use: {
41 | loader: "url-loader",
42 | options: {
43 | name: "[name].[ext]",
44 | esModule: false,
45 | },
46 | },
47 | };
48 |
49 | module.exports = (_, defaults) => ({
50 | devServer: {
51 | open: true,
52 | },
53 | mode: "development",
54 | entry: [path.resolve(appDirectory, "index.web.js")],
55 | output: {
56 | filename: "bundle.web.js",
57 | path: path.resolve(appDirectory, "dist"),
58 | },
59 |
60 | plugins: [
61 | new webpack.EnvironmentPlugin({ JEST_WORKER_ID: null }),
62 | new webpack.DefinePlugin({
63 | process: {
64 | env: {},
65 | platform: JSON.stringify("web"),
66 | },
67 | }),
68 | new webpack.DefinePlugin({
69 | __DEV__: process.env.NODE_ENV !== "production",
70 | }),
71 | new webpack.ContextReplacementPlugin(
72 | /react-native-worklets[/\\]lib/,
73 | path.resolve(__dirname, "src"),
74 | ),
75 | ],
76 |
77 | module: {
78 | ...defaults.module,
79 | rules: [
80 | {
81 | test: /\.m?js$/,
82 | resolve: {
83 | fullySpecified: false,
84 | },
85 | },
86 | babelLoaderConfiguration,
87 | imageLoaderConfiguration,
88 | {
89 | test: /\.(js|ts|tsx)$/,
90 | use: "babel-loader",
91 | exclude:
92 | /node_modules\/(?!react-native-|@react-native|react-native-advanced-input-mask)/,
93 | },
94 | ],
95 | },
96 |
97 | resolve: {
98 | ...defaults.resolve,
99 | symlinks: true,
100 | alias: {
101 | ...modules.reduce((acc, name) => {
102 | acc[name] = path.join(__dirname, "node_modules", name);
103 |
104 | return acc;
105 | }, {}),
106 | "react-native-advanced-input-mask": path.resolve(
107 | __dirname,
108 | "../../package/src",
109 | ),
110 | "react-native": "react-native-web",
111 | "react-native-reanimated": path.resolve(
112 | __dirname,
113 | "node_modules/react-native-reanimated",
114 | ),
115 | "react-native-gesture-handler": path.resolve(
116 | __dirname,
117 | "node_modules/react-native-gesture-handler",
118 | ),
119 | },
120 | extensions: [
121 | ".web.js",
122 | ".web.tsx",
123 | ".web.ts",
124 | ".js",
125 | ".jsx",
126 | ".json",
127 | ".ts",
128 | ".tsx",
129 | ],
130 | },
131 | });
132 |
--------------------------------------------------------------------------------
/.github/workflows/android-e2e-test.yml:
--------------------------------------------------------------------------------
1 | name: 🛠 Android e2e tests
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | paths:
8 | - ".github/workflows/ios-e2e-test.yml"
9 | - "package/src/native/**"
10 | - "package/ios/**"
11 | - "yarn.lock"
12 | - "apps/example/yarn.lock"
13 | - "e2e/**"
14 | pull_request:
15 | branches:
16 | - main
17 | paths:
18 | - ".github/workflows/ios-e2e-test.yml"
19 | - "package/src/native/**"
20 | - "package/ios/**"
21 | - "yarn.lock"
22 | - "apps/example/yarn.lock"
23 | - "e2e/**"
24 |
25 | jobs:
26 | build-android:
27 | runs-on: ubuntu-latest
28 | steps:
29 | - name: Checkout
30 | uses: actions/checkout@v4
31 |
32 | - name: Build example for Android
33 | uses: ./.github/actions/build-android
34 | with:
35 | build-command: yarn build:android -PreactNativeArchitectures=x86_64
36 |
37 | - name: Upload build artifacts
38 | uses: actions/upload-artifact@v4
39 | with:
40 | name: android-apk
41 | path: apps/example/android/app/build/outputs/apk/release
42 | if-no-files-found: error
43 | e2e-test:
44 | name: "⚙️ Automated test cases"
45 | runs-on: ubuntu-latest
46 | needs: build-android
47 | steps:
48 | - name: Checkout
49 | uses: actions/checkout@v4
50 |
51 | - name: Gradle cache
52 | uses: gradle/actions/setup-gradle@v3
53 |
54 | - name: Download apk
55 | uses: actions/download-artifact@v4
56 | with:
57 | name: android-apk
58 | path: apps/example/android/app/build/outputs/apk/release
59 |
60 | - name: Install Maestro
61 | run: |
62 | curl -fsSL "https://get.maestro.mobile.dev" | bash
63 | echo "$HOME/.maestro/bin" >> $GITHUB_PATH
64 | export MAESTRO_DRIVER_STARTUP_TIMEOUT=600000
65 |
66 | - name: Enable KVM group perms
67 | run: |
68 | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
69 | sudo udevadm control --reload-rules
70 | sudo udevadm trigger --name-match=kvm
71 |
72 | - name: AVD cache
73 | uses: actions/cache@v4
74 | id: avd-cache
75 | with:
76 | path: |
77 | ~/.android/avd/*
78 | ~/.android/adb*
79 | key: avd-cache
80 |
81 | - name: Create AVD and generate snapshot for caching
82 | uses: reactivecircus/android-emulator-runner@v2.33.0
83 | with:
84 | api-level: 35
85 | ram-size: 4096M
86 | disk-size: 10G
87 | force-avd-creation: false
88 | profile: pixel_6a
89 | emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
90 | disable-animations: false
91 | arch: x86_64
92 | script: echo "Generated AVD snapshot for caching."
93 |
94 | - name: Run tests
95 | uses: reactivecircus/android-emulator-runner@v2.33.0
96 | with:
97 | api-level: 35
98 | force-avd-creation: false
99 | disable-animations: true
100 | profile: pixel_6a
101 | arch: x86_64
102 | ram-size: 4096M
103 | disk-size: 10G
104 | emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
105 | script: |
106 | adb install -r -d -t apps/example/android/app/build/outputs/apk/release/app-release.apk
107 | mkdir -p ./reports/debug
108 | maestro test e2e/.maestro/* --format html ./reports/debug --debug-output ./reports/debug --flatten-debug-output
109 |
110 | - name: Upload test report
111 | if: always()
112 | uses: actions/upload-artifact@v4
113 | with:
114 | path: ./reports/debug
115 | name: e2e-report-android
116 |
--------------------------------------------------------------------------------
/package/src/types.ts:
--------------------------------------------------------------------------------
1 | import type { AFFINITY_CALCULATION_STRATEGY } from "./enums";
2 | import type { ElementType, JSX } from "react";
3 | import type { TextInputProps } from "react-native";
4 |
5 | export type Notation = {
6 | /**
7 | * A symbol in format string.
8 | */
9 | character: string;
10 | /**
11 | * An associated character set of acceptable input characters.
12 | */
13 | characterSet: string;
14 | /**
15 | * Is it an optional symbol or mandatory?
16 | */
17 | isOptional: boolean;
18 | };
19 |
20 | type CustomTransformation = {
21 | transformationChar: string;
22 | transformationString: string;
23 | };
24 |
25 | export type MaskedTextInputOwnProps = {
26 | /**
27 | * The mask format to be applied to the text input.
28 | * @example "[0000] [0000] [0000] [0000]"
29 | */
30 | mask: string;
31 | /**
32 | * Custom notations to be used in the mask format.
33 | * Each notation should be an object containing:
34 | * - character: The character to be replaced in the mask.
35 | * - characterSet: The set of characters that can replace the notation character.
36 | * - isOptional: Whether the notation character is optional.
37 | */
38 | customNotations?: Notation[];
39 | /**
40 | * Callback function to be called when the text changes.
41 | * @param formattedValue The formatted value.
42 | * @param extractedValue The extracted value.
43 | * @param tailPlaceholder tail placeholder value.
44 | * @param complete a boolean flag indicating that the extracted value is complete
45 | */
46 | onChangeText?: (
47 | formattedValue: string,
48 | extractedValue: string,
49 | tailPlaceholder: string,
50 | complete: boolean,
51 | ) => void;
52 | /**
53 | * Callback function to be called when tail placeholder changes.
54 | * @param tailPlaceholder The tail placeholder.
55 | */
56 | onTailPlaceholderChange?: (tailPlaceholder: string) => void;
57 | /**
58 | * An array of strings representing the affinity format.
59 | * Used to determine the best mask format based on the input.
60 | */
61 | affinityFormat?: string[];
62 | /**
63 | * Whether to enable autocomplete for the text input.
64 | * Default is true.
65 | */
66 | autocomplete?: boolean;
67 | /**
68 | * Whether to automatically skip to the next input field when the current one is filled.
69 | * @default false.
70 | */
71 | autoSkip?: boolean;
72 | /**
73 | * Whether the text input should support right-to-left (RTL) text direction.
74 | * @default false.
75 | */
76 | isRTL?: boolean;
77 | /**
78 | * The strategy to use for affinity calculation.
79 | * Determines how the best mask format is selected based on the input.
80 | */
81 | affinityCalculationStrategy?: AFFINITY_CALCULATION_STRATEGY;
82 | /**
83 | * Custom transformation to be applied to the text input.
84 | * Defines how the input text should be transformed.
85 | */
86 | customTransformation?: CustomTransformation;
87 | /**
88 | * A string representing all symbols that can be entered in the text input.
89 | * For example: "0123456789".
90 | */
91 | allowedKeys?: string;
92 | defaultValue?: string;
93 | value?: string;
94 | // IOS only props
95 | /**
96 | * Whether to allow suggestions for the text input on iOS.
97 | * @default false.
98 | */
99 | allowSuggestions?: boolean;
100 | autocompleteOnFocus?: boolean;
101 | renderTextInputComponent?:
102 | | ElementType
103 | | ((props: TextInputProps) => JSX.Element);
104 | /**
105 | * A validation regex that runs before applying the mask.
106 | *
107 | * ⚠️ Use this only when absolutely necessary. Prefer using `allowedKeys` with `affinityFormat` instead.
108 | */
109 | validationRegex?: string;
110 | };
111 |
112 | export type MaskedTextInputRef = {
113 | setText: (text: string, autocomplete?: boolean) => void;
114 | clear: () => void;
115 | blur: () => void;
116 | isFocused: () => boolean;
117 | focus: () => void;
118 | setNativeProps: (props: object) => void;
119 | setSelection: (start: number, end: number) => void;
120 | };
121 |
122 | export type MaskedTextInputProps = Omit &
123 | MaskedTextInputOwnProps;
124 |
--------------------------------------------------------------------------------
/package/android/src/main/java/com/maskedtextinput/listeners/ReactMaskedTextChangeListener.kt:
--------------------------------------------------------------------------------
1 | package com.maskedtextinput.listeners
2 |
3 | import android.text.Editable
4 | import android.view.View
5 | import com.facebook.react.views.textinput.ReactEditText
6 | import com.redmadrobot.inputmask.MaskedTextChangedListener
7 | import com.redmadrobot.inputmask.helper.AffinityCalculationStrategy
8 | import com.redmadrobot.inputmask.model.Notation
9 |
10 | class ReactMaskedTextChangeListener(
11 | primaryFormat: String,
12 | affineFormats: List,
13 | customNotations: List,
14 | affinityCalculationStrategy: AffinityCalculationStrategy,
15 | autocomplete: Boolean,
16 | autoSkip: Boolean,
17 | val field: ReactEditText,
18 | rightToLeft: Boolean,
19 | valueListener: MaskedTextValueListener,
20 | var allowedKeys: String?,
21 | private val focusChangeListener: View.OnFocusChangeListener,
22 | var autocompleteOnFocus: Boolean,
23 | var validationRegex: Regex?,
24 | ) : MaskedTextChangedListener(
25 | primaryFormat = primaryFormat,
26 | affineFormats = affineFormats,
27 | customNotations = customNotations,
28 | affinityCalculationStrategy = affinityCalculationStrategy,
29 | autocomplete = autocomplete,
30 | autoskip = autoSkip,
31 | field = field,
32 | rightToLeft = rightToLeft,
33 | valueListener = valueListener,
34 | ) {
35 | private var cursorPosition = 0
36 | private var prevText = ""
37 |
38 | override fun onTextChanged(
39 | text: CharSequence,
40 | cursorPosition: Int,
41 | before: Int,
42 | count: Int,
43 | ) {
44 | if (prevText == field.text.toString()) return
45 |
46 | val newText = allowedKeys?.run { text.filter { it in this } } ?: text
47 | if (!isValidText(text.toString())) {
48 | this.cursorPosition = cursorPosition
49 | return
50 | }
51 |
52 | super.onTextChanged(newText, cursorPosition, before, count)
53 | }
54 |
55 | override fun afterTextChanged(edit: Editable?) {
56 | if (prevText == field.text.toString()) return
57 |
58 | val stringText = edit.toString()
59 | if (!isValidText(stringText)) {
60 | field.setText(prevText)
61 | field.setSelection(cursorPosition)
62 | return
63 | }
64 |
65 | prevText = stringText
66 | super.afterTextChanged(edit)
67 | }
68 |
69 | private fun isValidText(text: String): Boolean = this.validationRegex?.matches(text) ?: true
70 |
71 | override fun onFocusChange(
72 | view: View?,
73 | hasFocus: Boolean,
74 | ) {
75 | if (autocompleteOnFocus) {
76 | val prevAutocomplete = this.autocomplete
77 | this.autocomplete = autocompleteOnFocus
78 | super.onFocusChange(view, hasFocus)
79 | this.autocomplete = prevAutocomplete
80 | }
81 | focusChangeListener.onFocusChange(view, hasFocus)
82 | }
83 |
84 | companion object {
85 | fun installOn(
86 | primaryFormat: String,
87 | affineFormats: List,
88 | customNotations: List,
89 | affinityCalculationStrategy: AffinityCalculationStrategy,
90 | autocomplete: Boolean,
91 | autoSkip: Boolean,
92 | field: ReactEditText,
93 | rightToLeft: Boolean,
94 | valueListener: MaskedTextValueListener,
95 | allowedKeys: String?,
96 | autocompleteOnFocus: Boolean,
97 | validationRegex: Regex?,
98 | ): ReactMaskedTextChangeListener {
99 | val listener =
100 | ReactMaskedTextChangeListener(
101 | primaryFormat = primaryFormat,
102 | affineFormats = affineFormats,
103 | customNotations = customNotations,
104 | affinityCalculationStrategy = affinityCalculationStrategy,
105 | autocomplete = autocomplete,
106 | autoSkip = autoSkip,
107 | field = field,
108 | rightToLeft = rightToLeft,
109 | focusChangeListener = field.onFocusChangeListener,
110 | valueListener = valueListener,
111 | allowedKeys = allowedKeys,
112 | validationRegex = validationRegex,
113 | autocompleteOnFocus = autocompleteOnFocus,
114 | )
115 | field.addTextChangedListener(listener)
116 | field.onFocusChangeListener = listener
117 |
118 | return listener
119 | }
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/apps/example/ios/MaskedTextInputExample/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
24 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/package/src/native/views/MaskedTextInput/index.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | forwardRef,
3 | memo,
4 | useCallback,
5 | useImperativeHandle,
6 | useRef,
7 | } from "react";
8 | import { StyleSheet, TextInput } from "react-native";
9 |
10 | import { IS_FABRIC } from "../../architecture";
11 | import AdvancedTextInputMaskDecoratorViewNativeComponent from "../../specs/AdvancedTextInputMaskDecoratorViewNativeComponent";
12 | import { Commands } from "../../specs/AdvancedTextInputMaskDecoratorViewNativeComponent";
13 |
14 | import type { MaskedTextInputProps, MaskedTextInputRef } from "../../../types";
15 | import type {
16 | NativeCommands,
17 | NativeProps,
18 | } from "../../specs/AdvancedTextInputMaskDecoratorViewNativeComponent";
19 | import type { Component } from "react";
20 | import type { NativeSyntheticEvent } from "react-native";
21 |
22 | const styles = StyleSheet.create({
23 | displayNone: {
24 | display: "none",
25 | },
26 | farAway: {
27 | position: "absolute",
28 | top: 1e8,
29 | left: 1e8,
30 | },
31 | });
32 |
33 | const MaskedTextInput = forwardRef(
34 | (
35 | {
36 | affinityCalculationStrategy,
37 | affinityFormat,
38 | allowSuggestions,
39 | allowedKeys,
40 | autocomplete,
41 | autocompleteOnFocus,
42 | autoSkip,
43 | customNotations,
44 | customTransformation,
45 | defaultValue,
46 | isRTL,
47 | mask,
48 | autoCapitalize = "words",
49 | value,
50 | onChangeText,
51 | onTailPlaceholderChange,
52 | renderTextInputComponent,
53 | validationRegex,
54 | ...rest
55 | },
56 | ref,
57 | ) => {
58 | const inputRef = useRef(null);
59 | const maskedViewDecoratorRef = useRef<
60 | Component & NativeCommands
61 | >(null);
62 | const InputComponent = renderTextInputComponent ?? TextInput;
63 |
64 | useImperativeHandle(ref, () => {
65 | return {
66 | isFocused: () => !!inputRef.current?.isFocused(),
67 | blur: () => {
68 | inputRef.current?.blur();
69 | },
70 | focus: () => {
71 | inputRef.current?.focus();
72 | },
73 | setNativeProps: (props: object) => {
74 | inputRef.current?.setNativeProps(props);
75 | },
76 | clear: () => {
77 | if (maskedViewDecoratorRef.current) {
78 | // @ts-expect-error the type is correct
79 | Commands.setText(maskedViewDecoratorRef.current, "", false);
80 | }
81 | },
82 | setSelection: (start: number, end: number) => {
83 | inputRef.current?.setSelection(start, end);
84 | },
85 | setText: (text: string, autoComplete?: boolean) => {
86 | if (maskedViewDecoratorRef.current) {
87 | Commands.setText(
88 | // @ts-expect-error the type is correct
89 | maskedViewDecoratorRef.current,
90 | text,
91 | !!autoComplete,
92 | );
93 | }
94 | },
95 | };
96 | });
97 |
98 | const onAdvancedMaskTextChangeCallback = useCallback(
99 | ({
100 | nativeEvent: { extracted, formatted, tailPlaceholder, complete },
101 | }: NativeSyntheticEvent<{
102 | extracted: string;
103 | formatted: string;
104 | tailPlaceholder: string;
105 | complete: boolean;
106 | }>) => {
107 | onChangeText?.(formatted, extracted, tailPlaceholder, complete);
108 | onTailPlaceholderChange?.(tailPlaceholder);
109 | },
110 | [onChangeText, onTailPlaceholderChange],
111 | );
112 |
113 | return (
114 | <>
115 |
120 |
140 | >
141 | );
142 | },
143 | );
144 |
145 | export default memo(MaskedTextInput);
146 |
--------------------------------------------------------------------------------
/apps/example/android/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: "com.android.application"
2 | apply plugin: "org.jetbrains.kotlin.android"
3 | apply plugin: "com.facebook.react"
4 |
5 | /**
6 | * This is the configuration block to customize your React Native Android app.
7 | * By default you don't need to apply any configuration, just uncomment the lines you need.
8 | */
9 | react {
10 | /* Folders */
11 | // The root of your project, i.e. where "package.json" lives. Default is '../..'
12 | // root = file("../../")
13 | // The folder where the react-native NPM package is. Default is ../../node_modules/react-native
14 | // reactNativeDir = file("../../node_modules/react-native")
15 | // The folder where the react-native Codegen package is. Default is ../../node_modules/@react-native/codegen
16 | // codegenDir = file("../../node_modules/@react-native/codegen")
17 | // The cli.js file which is the React Native CLI entrypoint. Default is ../../node_modules/react-native/cli.js
18 | // cliFile = file("../../node_modules/react-native/cli.js")
19 |
20 | /* Variants */
21 | // The list of variants to that are debuggable. For those we're going to
22 | // skip the bundling of the JS bundle and the assets. By default is just 'debug'.
23 | // If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants.
24 | // debuggableVariants = ["liteDebug", "prodDebug"]
25 |
26 | /* Bundling */
27 | // A list containing the node command and its flags. Default is just 'node'.
28 | // nodeExecutableAndArgs = ["node"]
29 | //
30 | // The command to run when bundling. By default is 'bundle'
31 | // bundleCommand = "ram-bundle"
32 | //
33 | // The path to the CLI configuration file. Default is empty.
34 | // bundleConfig = file(../rn-cli.config.js)
35 | //
36 | // The name of the generated asset file containing your JS bundle
37 | // bundleAssetName = "MyApplication.android.bundle"
38 | //
39 | // The entry file for bundle generation. Default is 'index.android.js' or 'index.js'
40 | // entryFile = file("../js/MyApplication.android.js")
41 | //
42 | // A list of extra flags to pass to the 'bundle' commands.
43 | // See https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle
44 | // extraPackagerArgs = []
45 |
46 | /* Hermes Commands */
47 | // The hermes compiler command to run. By default it is 'hermesc'
48 | // hermesCommand = "$rootDir/my-custom-hermesc/bin/hermesc"
49 | //
50 | // The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map"
51 | // hermesFlags = ["-O", "-output-source-map"]
52 |
53 | /* Autolinking */
54 | autolinkLibrariesWithApp()
55 | }
56 |
57 | /**
58 | * Set this to true to Run Proguard on Release builds to minify the Java bytecode.
59 | */
60 | def enableProguardInReleaseBuilds = false
61 |
62 | /**
63 | * The preferred build flavor of JavaScriptCore (JSC)
64 | *
65 | * For example, to use the international variant, you can use:
66 | * `def jscFlavor = io.github.react-native-community:jsc-android-intl:2026004.+`
67 | *
68 | * The international variant includes ICU i18n library and necessary data
69 | * allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that
70 | * give correct results when using with locales other than en-US. Note that
71 | * this variant is about 6MiB larger per architecture than default.
72 | */
73 | def jscFlavor = 'io.github.react-native-community:jsc-android:2026004.+'
74 |
75 | android {
76 | ndkVersion rootProject.ext.ndkVersion
77 | buildToolsVersion rootProject.ext.buildToolsVersion
78 | compileSdk rootProject.ext.compileSdkVersion
79 |
80 | namespace "com.maskedtextinputexample"
81 | defaultConfig {
82 | applicationId "com.maskedtextinputexample"
83 | minSdkVersion rootProject.ext.minSdkVersion
84 | targetSdkVersion rootProject.ext.targetSdkVersion
85 | versionCode 1
86 | versionName "1.0"
87 | }
88 | signingConfigs {
89 | debug {
90 | storeFile file('debug.keystore')
91 | storePassword 'android'
92 | keyAlias 'androiddebugkey'
93 | keyPassword 'android'
94 | }
95 | }
96 | buildTypes {
97 | debug {
98 | signingConfig signingConfigs.debug
99 | }
100 | release {
101 | // Caution! In production, you need to generate your own keystore file.
102 | // see https://reactnative.dev/docs/signed-apk-android.
103 | signingConfig signingConfigs.debug
104 | minifyEnabled enableProguardInReleaseBuilds
105 | proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
106 | }
107 | }
108 | }
109 |
110 | dependencies {
111 | // The version of react-native is set by the React Native Gradle Plugin
112 | implementation("com.facebook.react:react-android")
113 |
114 | if (hermesEnabled.toBoolean()) {
115 | implementation("com.facebook.react:hermes-android")
116 | } else {
117 | implementation jscFlavor
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | Contributions are always welcome, no matter how large or small!
4 |
5 | We want this community to be friendly and respectful to each other. Please follow it in all your interactions with the project. Before contributing, please read the [code of conduct](./CODE_OF_CONDUCT.md).
6 |
7 | ## Development workflow
8 |
9 | This project is a monorepo managed using [Yarn workspaces](https://yarnpkg.com/features/workspaces). It contains the following packages:
10 |
11 | - The library package in the root directory.
12 | - An example app in the `example/` directory.
13 |
14 | To get started with the project, run `yarn` in the root directory to install the required dependencies for each package:
15 |
16 | ```sh
17 | yarn
18 | ```
19 |
20 | > Since the project relies on Yarn workspaces, you cannot use [`npm`](https://github.com/npm/cli) for development.
21 |
22 | The [example app](/example/) demonstrates usage of the library. You need to run it to test any changes you make.
23 |
24 | It is configured to use the local version of the library, so any changes you make to the library's source code will be reflected in the example app. Changes to the library's JavaScript code will be reflected in the example app without a rebuild, but native code changes will require a rebuild of the example app.
25 |
26 | If you want to use Android Studio or XCode to edit the native code, you can open the `example/android` or `example/ios` directories respectively in those editors. To edit the Objective-C or Swift files, open `example/ios/MaskedTextInputExample.xcworkspace` in XCode and find the source files at `Pods > Development Pods > react-native-advanced-input-mask`.
27 |
28 | To edit the Java or Kotlin files, open `example/android` in Android studio and find the source files at `react-native-advanced-input-mask` under `Android`.
29 |
30 | You can use various commands from the root directory to work with the project.
31 |
32 | To start the packager:
33 |
34 | ```sh
35 | yarn example start
36 | ```
37 |
38 | To run the example app on Android:
39 |
40 | ```sh
41 | yarn example android
42 | ```
43 |
44 | To run the example app on iOS:
45 |
46 | ```sh
47 | yarn example ios
48 | ```
49 |
50 | Make sure your code passes TypeScript and ESLint. Run the following to verify:
51 |
52 | ```sh
53 | yarn typecheck
54 | yarn lint
55 | ```
56 |
57 | To fix formatting errors, run the following:
58 |
59 | ```sh
60 | yarn lint --fix
61 | ```
62 |
63 | Remember to add tests for your change if possible. Run the unit tests by:
64 |
65 | ```sh
66 | yarn test
67 | ```
68 |
69 | ### Commit message convention
70 |
71 | We follow the [conventional commits specification](https://www.conventionalcommits.org/en) for our commit messages:
72 |
73 | - `fix`: bug fixes, e.g. fix crash due to deprecated method.
74 | - `feat`: new features, e.g. add new method to the module.
75 | - `refactor`: code refactor, e.g. migrate from class components to hooks.
76 | - `docs`: changes into documentation, e.g. add usage example for the module..
77 | - `test`: adding or updating tests, e.g. add integration tests using detox.
78 | - `chore`: tooling changes, e.g. change CI config.
79 |
80 | Our pre-commit hooks verify that your commit message matches this format when committing.
81 |
82 | ### Linting and tests
83 |
84 | [ESLint](https://eslint.org/), [Prettier](https://prettier.io/), [TypeScript](https://www.typescriptlang.org/)
85 |
86 | We use [TypeScript](https://www.typescriptlang.org/) for type checking, [ESLint](https://eslint.org/) with [Prettier](https://prettier.io/) for linting and formatting the code, and [Jest](https://jestjs.io/) for testing.
87 |
88 | Our pre-commit hooks verify that the linter and tests pass when committing.
89 |
90 | ### Publishing to npm
91 |
92 | We use [release-it](https://github.com/release-it/release-it) to make it easier to publish new versions. It handles common tasks like bumping version based on semver, creating tags and releases etc.
93 |
94 | To publish new versions, run the following:
95 |
96 | ```sh
97 | yarn release
98 | ```
99 |
100 | ### Scripts
101 |
102 | The `package.json` file contains various scripts for common tasks:
103 |
104 | - `yarn`: setup project by installing dependencies.
105 | - `yarn typecheck`: type-check files with TypeScript.
106 | - `yarn lint`: lint files with ESLint.
107 | - `yarn test`: run unit tests with Jest.
108 | - `yarn example start`: start the Metro server for the example app.
109 | - `yarn example android`: run the example app on Android.
110 | - `yarn example ios`: run the example app on iOS.
111 |
112 | ### Sending a pull request
113 |
114 | > **Working on your first pull request?** You can learn how from this _free_ series: [How to Contribute to an Open Source Project on GitHub](https://app.egghead.io/playlists/how-to-contribute-to-an-open-source-project-on-github).
115 |
116 | When you're sending a pull request:
117 |
118 | - Prefer small pull requests focused on one change.
119 | - Verify that linters and tests are passing.
120 | - Review the documentation to make sure it looks good.
121 | - Follow the pull request template when opening a pull request.
122 | - For pull requests that change the API or implementation, discuss with maintainers first by opening an issue.
123 |
--------------------------------------------------------------------------------
/package/src/web/hooks/useMaskedTextInputListener/index.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2 |
3 | import MaskedTextChangedListener from "../../AdvancedTextInputMaskListener";
4 | import CaretString from "../../model/CaretString";
5 | import { CaretGravityType } from "../../model/types";
6 |
7 | import type { Props } from "./types";
8 | import type {
9 | NativeSyntheticEvent,
10 | TextInputChangeEventData,
11 | TextInputFocusEventData,
12 | } from "react-native";
13 |
14 | const useMaskedTextInputListener = ({
15 | autocompleteOnFocus,
16 | mask,
17 | affinityFormat,
18 | affinityCalculationStrategy,
19 | customNotations,
20 | allowedKeys = "",
21 | autocomplete = true,
22 | autoSkip = false,
23 | isRTL = false,
24 | onChange,
25 | onChangeText,
26 | onTailPlaceholderChange,
27 | onFocus,
28 | defaultValue,
29 | validationRegex,
30 | }: Props) => {
31 | const prevDispatchedPayload = useRef<{
32 | extracted: string | null;
33 | formatted: string | null;
34 | }>({ extracted: null, formatted: null });
35 |
36 | const isInitialMount = useRef(true);
37 |
38 | const [listener] = useState(
39 | () =>
40 | new MaskedTextChangedListener(
41 | mask,
42 | affinityFormat,
43 | customNotations,
44 | affinityCalculationStrategy,
45 | autocomplete,
46 | autoSkip,
47 | isRTL,
48 | allowedKeys,
49 | validationRegex,
50 | autocompleteOnFocus,
51 | defaultValue,
52 | ),
53 | );
54 |
55 | useEffect(() => {
56 | if (isInitialMount.current) {
57 | isInitialMount.current = false;
58 |
59 | return;
60 | }
61 |
62 | if (listener.affineFormats !== affinityFormat && affinityFormat) {
63 | // eslint-disable-next-line react-compiler/react-compiler
64 | listener.affineFormats = affinityFormat;
65 | }
66 |
67 | if (listener.customNotations !== customNotations && customNotations) {
68 | listener.customNotations = customNotations;
69 | }
70 |
71 | if (
72 | listener.affinityCalculationStrategy !== affinityCalculationStrategy &&
73 | affinityCalculationStrategy !== undefined
74 | ) {
75 | listener.affinityCalculationStrategy = affinityCalculationStrategy;
76 | }
77 |
78 | if (listener.autocomplete !== autocomplete) {
79 | listener.autocomplete = autocomplete;
80 | }
81 |
82 | if (listener.autoskip !== autoSkip) {
83 | listener.autoskip = autoSkip;
84 | }
85 |
86 | if (listener.primaryFormat !== mask) {
87 | listener.primaryFormat = mask;
88 | }
89 |
90 | if (listener.allowedKeys !== allowedKeys) {
91 | listener.setAllowedKeys(allowedKeys);
92 | }
93 |
94 | if (listener.rightToLeft !== isRTL) {
95 | listener.rightToLeft = isRTL;
96 | }
97 |
98 | if (listener.getValidationRegex() !== validationRegex) {
99 | listener.setValidationRegex(validationRegex);
100 | }
101 |
102 | if (listener.autocompleteOnFocus !== autocompleteOnFocus) {
103 | listener.autocompleteOnFocus = autocompleteOnFocus;
104 | }
105 |
106 | if (defaultValue !== listener.defaultValue) {
107 | listener.setText(defaultValue ?? "", false);
108 | }
109 | }, [
110 | autocompleteOnFocus,
111 | validationRegex,
112 | affinityFormat,
113 | customNotations,
114 | mask,
115 | isRTL,
116 | autoSkip,
117 | autocomplete,
118 | affinityCalculationStrategy,
119 | allowedKeys,
120 | listener,
121 | defaultValue,
122 | ]);
123 |
124 | const handleOnChange = useCallback(
125 | (e: NativeSyntheticEvent) => {
126 | const { tailPlaceholder, formattedText, extractedValue, complete } =
127 | listener.handleTextChange(e);
128 |
129 | const formattedValue = formattedText.string;
130 |
131 | if (
132 | prevDispatchedPayload.current.extracted === extractedValue &&
133 | prevDispatchedPayload.current.formatted === formattedValue
134 | ) {
135 | return;
136 | }
137 |
138 | onChange?.(e);
139 | onChangeText?.(formattedValue, extractedValue, tailPlaceholder, complete);
140 | onTailPlaceholderChange?.(tailPlaceholder);
141 |
142 | prevDispatchedPayload.current = {
143 | extracted: extractedValue,
144 | formatted: formattedValue,
145 | };
146 | },
147 | [listener, onChange, onChangeText, onTailPlaceholderChange],
148 | );
149 |
150 | const handleFocus = useCallback(
151 | (e: NativeSyntheticEvent) => {
152 | listener.handleFocus(e);
153 | onFocus?.(e);
154 | },
155 | [listener, onFocus],
156 | );
157 |
158 | const defaultValueResult = useMemo(
159 | () =>
160 | defaultValue
161 | ? listener.primaryMask.apply(
162 | new CaretString(defaultValue, defaultValue.length, {
163 | autocomplete: true,
164 | autoskip: false,
165 | type: CaretGravityType.Forward,
166 | }),
167 | ).formattedText.string
168 | : undefined,
169 | [defaultValue, listener.primaryMask],
170 | );
171 |
172 | return {
173 | setTextField: listener.setTextField,
174 | handleOnChange,
175 | handleFocus,
176 | listener,
177 | defaultValue: defaultValueResult,
178 | inputRef: listener.textField,
179 | };
180 | };
181 |
182 | export default useMaskedTextInputListener;
183 |
--------------------------------------------------------------------------------