@salve-software/react-native-nitro-input-mask 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/NitroInputMask.podspec +31 -0
- package/README.md +181 -0
- package/android/CMakeLists.txt +32 -0
- package/android/build.gradle +148 -0
- package/android/fix-prefab.gradle +51 -0
- package/android/gradle.properties +5 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/cpp/cpp-adapter.cpp +9 -0
- package/android/src/main/java/com/nitroinputmask/NitroInputMaskContext.kt +7 -0
- package/android/src/main/java/com/nitroinputmask/NitroInputMaskModule.kt +108 -0
- package/android/src/main/java/com/nitroinputmask/NitroInputMaskPackage.kt +29 -0
- package/android/src/main/java/com/nitroinputmask/NitroInputMaskServiceModule.kt +17 -0
- package/android/src/main/java/com/nitroinputmask/NitroInputMaskTextWatcher.kt +127 -0
- package/android/src/main/java/com/nitroinputmask/engine/CursorEngine.kt +30 -0
- package/android/src/main/java/com/nitroinputmask/engine/MaskEngine.kt +261 -0
- package/android/src/main/java/com/nitroinputmask/engine/MaskEngineFactory.kt +18 -0
- package/android/src/main/java/com/nitroinputmask/engine/MaskEngineProtocol.kt +8 -0
- package/android/src/main/java/com/nitroinputmask/masks/CreditCardMaskEngine.kt +47 -0
- package/android/src/main/java/com/nitroinputmask/masks/CustomMaskEngine.kt +16 -0
- package/android/src/main/java/com/nitroinputmask/masks/DatetimeMaskEngine.kt +38 -0
- package/android/src/main/java/com/nitroinputmask/masks/MoneyMaskEngine.kt +66 -0
- package/ios/Bridge.h +8 -0
- package/ios/NitroInputMaskDelegateProxy.swift +194 -0
- package/ios/NitroInputMaskModule.swift +114 -0
- package/ios/NitroInputMaskServiceModule.swift +11 -0
- package/ios/engine/CursorEngine.swift +52 -0
- package/ios/engine/MaskEngine.swift +221 -0
- package/ios/engine/MaskEngineFactory.swift +16 -0
- package/ios/engine/MaskEngineProtocol.swift +7 -0
- package/ios/masks/CreditCardMaskEngine.swift +57 -0
- package/ios/masks/CustomMaskEngine.swift +16 -0
- package/ios/masks/DatetimeMaskEngine.swift +46 -0
- package/ios/masks/MoneyMaskEngine.swift +66 -0
- package/lib/commonjs/classes/NitroInputMaskService/NitroInputMaskService.class.js +74 -0
- package/lib/commonjs/classes/NitroInputMaskService/NitroInputMaskService.class.js.map +1 -0
- package/lib/commonjs/classes/NitroInputMaskService/index.js +13 -0
- package/lib/commonjs/classes/NitroInputMaskService/index.js.map +1 -0
- package/lib/commonjs/classes/NitroInputMaskService/types/IApplyMaskProps.js +6 -0
- package/lib/commonjs/classes/NitroInputMaskService/types/IApplyMaskProps.js.map +1 -0
- package/lib/commonjs/classes/NitroInputMaskService/types/index.js +2 -0
- package/lib/commonjs/classes/NitroInputMaskService/types/index.js.map +1 -0
- package/lib/commonjs/classes/index.js +17 -0
- package/lib/commonjs/classes/index.js.map +1 -0
- package/lib/commonjs/components/NitroInputMask/index.js +41 -0
- package/lib/commonjs/components/NitroInputMask/index.js.map +1 -0
- package/lib/commonjs/components/NitroInputMask/types/NitroInputMaskProps.js +6 -0
- package/lib/commonjs/components/NitroInputMask/types/NitroInputMaskProps.js.map +1 -0
- package/lib/commonjs/components/NitroInputMask/types/index.js +2 -0
- package/lib/commonjs/components/NitroInputMask/types/index.js.map +1 -0
- package/lib/commonjs/components/index.js +17 -0
- package/lib/commonjs/components/index.js.map +1 -0
- package/lib/commonjs/index.js +39 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/nitro-module.js +17 -0
- package/lib/commonjs/nitro-module.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/specs/nitro-input-mask-service.nitro.js +6 -0
- package/lib/commonjs/specs/nitro-input-mask-service.nitro.js.map +1 -0
- package/lib/commonjs/specs/nitro-input-mask.nitro.js +6 -0
- package/lib/commonjs/specs/nitro-input-mask.nitro.js.map +1 -0
- package/lib/commonjs/types/CreditCardMaskOptions.js +2 -0
- package/lib/commonjs/types/CreditCardMaskOptions.js.map +1 -0
- package/lib/commonjs/types/CustomMaskOptions.js +2 -0
- package/lib/commonjs/types/CustomMaskOptions.js.map +1 -0
- package/lib/commonjs/types/DatetimeMaskOptions.js +2 -0
- package/lib/commonjs/types/DatetimeMaskOptions.js.map +1 -0
- package/lib/commonjs/types/MaskConfig.js +6 -0
- package/lib/commonjs/types/MaskConfig.js.map +1 -0
- package/lib/commonjs/types/MoneyMaskOptions.js +2 -0
- package/lib/commonjs/types/MoneyMaskOptions.js.map +1 -0
- package/lib/commonjs/types/index.js +2 -0
- package/lib/commonjs/types/index.js.map +1 -0
- package/lib/module/classes/NitroInputMaskService/NitroInputMaskService.class.js +70 -0
- package/lib/module/classes/NitroInputMaskService/NitroInputMaskService.class.js.map +1 -0
- package/lib/module/classes/NitroInputMaskService/index.js +4 -0
- package/lib/module/classes/NitroInputMaskService/index.js.map +1 -0
- package/lib/module/classes/NitroInputMaskService/types/IApplyMaskProps.js +4 -0
- package/lib/module/classes/NitroInputMaskService/types/IApplyMaskProps.js.map +1 -0
- package/lib/module/classes/NitroInputMaskService/types/index.js +2 -0
- package/lib/module/classes/NitroInputMaskService/types/index.js.map +1 -0
- package/lib/module/classes/index.js +4 -0
- package/lib/module/classes/index.js.map +1 -0
- package/lib/module/components/NitroInputMask/index.js +35 -0
- package/lib/module/components/NitroInputMask/index.js.map +1 -0
- package/lib/module/components/NitroInputMask/types/NitroInputMaskProps.js +4 -0
- package/lib/module/components/NitroInputMask/types/NitroInputMaskProps.js.map +1 -0
- package/lib/module/components/NitroInputMask/types/index.js +2 -0
- package/lib/module/components/NitroInputMask/types/index.js.map +1 -0
- package/lib/module/components/index.js +4 -0
- package/lib/module/components/index.js.map +1 -0
- package/lib/module/index.js +6 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/nitro-module.js +12 -0
- package/lib/module/nitro-module.js.map +1 -0
- package/lib/module/specs/nitro-input-mask-service.nitro.js +4 -0
- package/lib/module/specs/nitro-input-mask-service.nitro.js.map +1 -0
- package/lib/module/specs/nitro-input-mask.nitro.js +4 -0
- package/lib/module/specs/nitro-input-mask.nitro.js.map +1 -0
- package/lib/module/types/CreditCardMaskOptions.js +2 -0
- package/lib/module/types/CreditCardMaskOptions.js.map +1 -0
- package/lib/module/types/CustomMaskOptions.js +2 -0
- package/lib/module/types/CustomMaskOptions.js.map +1 -0
- package/lib/module/types/DatetimeMaskOptions.js +2 -0
- package/lib/module/types/DatetimeMaskOptions.js.map +1 -0
- package/lib/module/types/MaskConfig.js +4 -0
- package/lib/module/types/MaskConfig.js.map +1 -0
- package/lib/module/types/MoneyMaskOptions.js +2 -0
- package/lib/module/types/MoneyMaskOptions.js.map +1 -0
- package/lib/module/types/index.js +2 -0
- package/lib/module/types/index.js.map +1 -0
- package/lib/typescript/src/classes/NitroInputMaskService/NitroInputMaskService.class.d.ts +50 -0
- package/lib/typescript/src/classes/NitroInputMaskService/NitroInputMaskService.class.d.ts.map +1 -0
- package/lib/typescript/src/classes/NitroInputMaskService/index.d.ts +3 -0
- package/lib/typescript/src/classes/NitroInputMaskService/index.d.ts.map +1 -0
- package/lib/typescript/src/classes/NitroInputMaskService/types/IApplyMaskProps.d.ts +5 -0
- package/lib/typescript/src/classes/NitroInputMaskService/types/IApplyMaskProps.d.ts.map +1 -0
- package/lib/typescript/src/classes/NitroInputMaskService/types/index.d.ts +2 -0
- package/lib/typescript/src/classes/NitroInputMaskService/types/index.d.ts.map +1 -0
- package/lib/typescript/src/classes/index.d.ts +2 -0
- package/lib/typescript/src/classes/index.d.ts.map +1 -0
- package/lib/typescript/src/components/NitroInputMask/index.d.ts +5 -0
- package/lib/typescript/src/components/NitroInputMask/index.d.ts.map +1 -0
- package/lib/typescript/src/components/NitroInputMask/types/NitroInputMaskProps.d.ts +4 -0
- package/lib/typescript/src/components/NitroInputMask/types/NitroInputMaskProps.d.ts.map +1 -0
- package/lib/typescript/src/components/NitroInputMask/types/index.d.ts +2 -0
- package/lib/typescript/src/components/NitroInputMask/types/index.d.ts.map +1 -0
- package/lib/typescript/src/components/index.d.ts +2 -0
- package/lib/typescript/src/components/index.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +4 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/nitro-module.d.ts +5 -0
- package/lib/typescript/src/nitro-module.d.ts.map +1 -0
- package/lib/typescript/src/specs/nitro-input-mask-service.nitro.d.ts +9 -0
- package/lib/typescript/src/specs/nitro-input-mask-service.nitro.d.ts.map +1 -0
- package/lib/typescript/src/specs/nitro-input-mask.nitro.d.ts +23 -0
- package/lib/typescript/src/specs/nitro-input-mask.nitro.d.ts.map +1 -0
- package/lib/typescript/src/types/CreditCardMaskOptions.d.ts +5 -0
- package/lib/typescript/src/types/CreditCardMaskOptions.d.ts.map +1 -0
- package/lib/typescript/src/types/CustomMaskOptions.d.ts +4 -0
- package/lib/typescript/src/types/CustomMaskOptions.d.ts.map +1 -0
- package/lib/typescript/src/types/DatetimeMaskOptions.d.ts +4 -0
- package/lib/typescript/src/types/DatetimeMaskOptions.d.ts.map +1 -0
- package/lib/typescript/src/types/MaskConfig.d.ts +18 -0
- package/lib/typescript/src/types/MaskConfig.d.ts.map +1 -0
- package/lib/typescript/src/types/MoneyMaskOptions.d.ts +9 -0
- package/lib/typescript/src/types/MoneyMaskOptions.d.ts.map +1 -0
- package/lib/typescript/src/types/index.d.ts +6 -0
- package/lib/typescript/src/types/index.d.ts.map +1 -0
- package/nitro.json +40 -0
- package/nitrogen/generated/.gitattributes +1 -0
- package/nitrogen/generated/android/NitroInputMask+autolinking.cmake +83 -0
- package/nitrogen/generated/android/NitroInputMask+autolinking.gradle +27 -0
- package/nitrogen/generated/android/NitroInputMaskOnLoad.cpp +70 -0
- package/nitrogen/generated/android/NitroInputMaskOnLoad.hpp +34 -0
- package/nitrogen/generated/android/c++/JHybridNitroInputMaskServiceSpec.cpp +57 -0
- package/nitrogen/generated/android/c++/JHybridNitroInputMaskServiceSpec.hpp +63 -0
- package/nitrogen/generated/android/c++/JHybridNitroInputMaskSpec.cpp +68 -0
- package/nitrogen/generated/android/c++/JHybridNitroInputMaskSpec.hpp +66 -0
- package/nitrogen/generated/android/c++/JNitroMaskOptions.hpp +94 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroinputmask/HybridNitroInputMaskServiceSpec.kt +54 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroinputmask/HybridNitroInputMaskSpec.kt +66 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroinputmask/NitroInputMaskOnLoad.kt +35 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroinputmask/NitroMaskOptions.kt +96 -0
- package/nitrogen/generated/ios/NitroInputMask+autolinking.rb +62 -0
- package/nitrogen/generated/ios/NitroInputMask-Swift-Cxx-Bridge.cpp +50 -0
- package/nitrogen/generated/ios/NitroInputMask-Swift-Cxx-Bridge.hpp +124 -0
- package/nitrogen/generated/ios/NitroInputMask-Swift-Cxx-Umbrella.hpp +53 -0
- package/nitrogen/generated/ios/NitroInputMaskAutolinking.mm +41 -0
- package/nitrogen/generated/ios/NitroInputMaskAutolinking.swift +38 -0
- package/nitrogen/generated/ios/c++/HybridNitroInputMaskServiceSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridNitroInputMaskServiceSpecSwift.hpp +85 -0
- package/nitrogen/generated/ios/c++/HybridNitroInputMaskSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridNitroInputMaskSpecSwift.hpp +101 -0
- package/nitrogen/generated/ios/swift/HybridNitroInputMaskServiceSpec.swift +55 -0
- package/nitrogen/generated/ios/swift/HybridNitroInputMaskServiceSpec_cxx.swift +138 -0
- package/nitrogen/generated/ios/swift/HybridNitroInputMaskSpec.swift +58 -0
- package/nitrogen/generated/ios/swift/HybridNitroInputMaskSpec_cxx.swift +170 -0
- package/nitrogen/generated/ios/swift/NitroMaskOptions.swift +204 -0
- package/nitrogen/generated/shared/c++/HybridNitroInputMaskServiceSpec.cpp +21 -0
- package/nitrogen/generated/shared/c++/HybridNitroInputMaskServiceSpec.hpp +64 -0
- package/nitrogen/generated/shared/c++/HybridNitroInputMaskSpec.cpp +24 -0
- package/nitrogen/generated/shared/c++/HybridNitroInputMaskSpec.hpp +67 -0
- package/nitrogen/generated/shared/c++/NitroMaskOptions.hpp +120 -0
- package/package.json +136 -0
- package/src/classes/NitroInputMaskService/NitroInputMaskService.class.ts +70 -0
- package/src/classes/NitroInputMaskService/index.ts +2 -0
- package/src/classes/NitroInputMaskService/types/IApplyMaskProps.ts +3 -0
- package/src/classes/NitroInputMaskService/types/index.ts +1 -0
- package/src/classes/index.ts +1 -0
- package/src/components/NitroInputMask/index.tsx +41 -0
- package/src/components/NitroInputMask/types/NitroInputMaskProps.ts +4 -0
- package/src/components/NitroInputMask/types/index.ts +1 -0
- package/src/components/index.ts +1 -0
- package/src/index.ts +3 -0
- package/src/nitro-module.ts +14 -0
- package/src/specs/nitro-input-mask-service.nitro.ts +6 -0
- package/src/specs/nitro-input-mask.nitro.ts +25 -0
- package/src/types/CreditCardMaskOptions.ts +4 -0
- package/src/types/CustomMaskOptions.ts +3 -0
- package/src/types/DatetimeMaskOptions.ts +3 -0
- package/src/types/MaskConfig.ts +10 -0
- package/src/types/MoneyMaskOptions.ts +8 -0
- package/src/types/index.ts +5 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
package com.nitroinputmask
|
|
2
|
+
|
|
3
|
+
import android.text.Editable
|
|
4
|
+
import android.text.TextWatcher
|
|
5
|
+
import android.widget.EditText
|
|
6
|
+
import com.nitroinputmask.engine.CursorEngine
|
|
7
|
+
import com.nitroinputmask.engine.MaskEngineProtocol
|
|
8
|
+
import java.lang.ref.WeakReference
|
|
9
|
+
|
|
10
|
+
// Weak reference avoids EditText → TextWatcher → EditText retain cycle
|
|
11
|
+
internal class NitroInputMaskTextWatcher(var engine: MaskEngineProtocol, editText: EditText) : TextWatcher {
|
|
12
|
+
private val editTextRef = WeakReference(editText)
|
|
13
|
+
var isProgrammatic = false
|
|
14
|
+
private var prevLength = 0
|
|
15
|
+
private var isDeletion = false
|
|
16
|
+
private var changeStart = 0
|
|
17
|
+
private var prevMasked = ""
|
|
18
|
+
private var prevCursorEnd = 0
|
|
19
|
+
|
|
20
|
+
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
|
|
21
|
+
prevLength = s?.length ?: 0
|
|
22
|
+
isDeletion = after < count
|
|
23
|
+
prevMasked = s?.toString() ?: ""
|
|
24
|
+
prevCursorEnd = start + count
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
|
|
28
|
+
changeStart = start
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
override fun afterTextChanged(s: Editable?) {
|
|
32
|
+
if (isProgrammatic) return
|
|
33
|
+
val editText = editTextRef.get() ?: return
|
|
34
|
+
val input = s?.toString()?.replace("\n", "") ?: ""
|
|
35
|
+
|
|
36
|
+
var (masked, _) = engine.apply(input)
|
|
37
|
+
|
|
38
|
+
// Separator deletion: masking re-produces the same text because the separator
|
|
39
|
+
// is always re-inserted. If there is data, block the deletion (text unchanged,
|
|
40
|
+
// cursor moves to just before the separator). If there is no data, clear the field.
|
|
41
|
+
var isSeparatorDeletion = false
|
|
42
|
+
if (isDeletion && masked == prevMasked) {
|
|
43
|
+
val (_, raw) = engine.apply(input)
|
|
44
|
+
if (raw.isEmpty()) {
|
|
45
|
+
masked = ""
|
|
46
|
+
} else {
|
|
47
|
+
isSeparatorDeletion = true
|
|
48
|
+
// masked stays == prevMasked; cursor will move to changeStart below
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
val cursorPos = when {
|
|
53
|
+
engine.wantsTrailingCursor -> when {
|
|
54
|
+
isSeparatorDeletion -> changeStart
|
|
55
|
+
isDeletion -> {
|
|
56
|
+
val digitsAfterDeleted = prevMasked.drop(prevCursorEnd).count { it.isDigit() }
|
|
57
|
+
if (digitsAfterDeleted == 0) {
|
|
58
|
+
masked.length
|
|
59
|
+
} else {
|
|
60
|
+
positionAfterDigits(prevMasked.take(changeStart).count { it.isDigit() }, masked)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
else -> {
|
|
64
|
+
val digitsAfter = countDigits(prevMasked, prevCursorEnd)
|
|
65
|
+
cursorPositionWithDigitsAfter(digitsAfter, masked)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
isDeletion -> minOf(changeStart, masked.length)
|
|
69
|
+
else -> {
|
|
70
|
+
val expMask = engine.expandedMask
|
|
71
|
+
if (expMask != null) {
|
|
72
|
+
val raw = CursorEngine.offsetAfterInsertion(prevMasked, masked, expMask, changeStart)
|
|
73
|
+
skipLeadingLiterals(raw, masked, expMask)
|
|
74
|
+
} else {
|
|
75
|
+
masked.length
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (masked == s?.toString()) {
|
|
81
|
+
editText.setSelection(cursorPos)
|
|
82
|
+
} else {
|
|
83
|
+
isProgrammatic = true
|
|
84
|
+
s?.replace(0, s.length, masked)
|
|
85
|
+
isProgrammatic = false
|
|
86
|
+
editText.setSelection(minOf(cursorPos, editText.text.length))
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private fun positionAfterDigits(count: Int, text: String): Int {
|
|
91
|
+
if (count == 0) return text.indexOfFirst { it.isDigit() }.takeIf { it >= 0 } ?: 0
|
|
92
|
+
var found = 0
|
|
93
|
+
for (i in text.indices) {
|
|
94
|
+
if (text[i].isDigit()) {
|
|
95
|
+
found++
|
|
96
|
+
if (found == count) return i + 1
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return text.length
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private fun skipLeadingLiterals(offset: Int, masked: String, mask: String): Int {
|
|
103
|
+
val dataTokens = setOf('9', 'A', '*')
|
|
104
|
+
var idx = offset
|
|
105
|
+
while (idx < mask.length && mask[idx] !in dataTokens) idx++
|
|
106
|
+
return minOf(idx, masked.length)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private fun countDigits(text: String, from: Int): Int {
|
|
110
|
+
if (from >= text.length) return 0
|
|
111
|
+
return text.substring(from).count { it.isDigit() }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private fun cursorPositionWithDigitsAfter(count: Int, text: String): Int {
|
|
115
|
+
if (count == 0) return text.length
|
|
116
|
+
var digitsFromEnd = 0
|
|
117
|
+
var pos = text.length
|
|
118
|
+
while (pos > 0) {
|
|
119
|
+
pos--
|
|
120
|
+
if (text[pos].isDigit()) {
|
|
121
|
+
digitsFromEnd++
|
|
122
|
+
if (digitsFromEnd == count) return pos
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return 0
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
package com.nitroinputmask.engine
|
|
2
|
+
|
|
3
|
+
object CursorEngine {
|
|
4
|
+
fun offsetAtEnd(masked: String, mask: String): Int {
|
|
5
|
+
for (i in masked.length - 1 downTo 0) {
|
|
6
|
+
if (i < mask.length && (mask[i] == '9' || mask[i] == 'A' || mask[i] == '*')) {
|
|
7
|
+
return i + 1
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
return masked.length
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
fun offsetAfterInsertion(oldMasked: String, newMasked: String, mask: String, at: Int): Int {
|
|
14
|
+
var dataRankBefore = 0
|
|
15
|
+
for (i in 0 until minOf(at, mask.length)) {
|
|
16
|
+
val m = mask[i]
|
|
17
|
+
if (m == '9' || m == 'A' || m == '*') dataRankBefore++
|
|
18
|
+
}
|
|
19
|
+
val targetRank = dataRankBefore + 1
|
|
20
|
+
var dataCount = 0
|
|
21
|
+
for (i in 0 until minOf(newMasked.length, mask.length)) {
|
|
22
|
+
val m = mask[i]
|
|
23
|
+
if (m == '9' || m == 'A' || m == '*') {
|
|
24
|
+
dataCount++
|
|
25
|
+
if (dataCount == targetRank) return i + 1
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return newMasked.length
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
package com.nitroinputmask.engine
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Slot — private internal representation of each compiled mask position
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
|
|
7
|
+
internal sealed class Slot {
|
|
8
|
+
object Digit : Slot()
|
|
9
|
+
object Letter : Slot()
|
|
10
|
+
object Any : Slot()
|
|
11
|
+
data class Literal(val char: Char) : Slot()
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A slot that belongs to an inline `[from-to]` range.
|
|
15
|
+
*
|
|
16
|
+
* @param blockName the range content string, e.g. "1-12" (used as the
|
|
17
|
+
* partial-tracking key; two ranges with identical bounds
|
|
18
|
+
* will share partial state — acceptable trade-off)
|
|
19
|
+
* @param position 0-based index of this slot within the range block
|
|
20
|
+
* @param length total character width (= max(from.digits, to.digits))
|
|
21
|
+
* @param from inclusive lower bound of the valid range
|
|
22
|
+
* @param to inclusive upper bound of the valid range
|
|
23
|
+
*/
|
|
24
|
+
data class RangeSlot(
|
|
25
|
+
val blockName: String,
|
|
26
|
+
val length: Int,
|
|
27
|
+
val from: Int,
|
|
28
|
+
val to: Int
|
|
29
|
+
) : Slot()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// CompiledMask — result of compile(); shared between MaskEngine and callers
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
internal data class CompiledMask(
|
|
37
|
+
internal val slots: List<Slot>,
|
|
38
|
+
/** Expanded mask string used by CursorEngine (range positions become '9'). */
|
|
39
|
+
val expandedMask: String
|
|
40
|
+
) {
|
|
41
|
+
val isEmpty: Boolean get() = slots.isEmpty()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// MaskEngine
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
object MaskEngine {
|
|
49
|
+
|
|
50
|
+
// -------------------------------------------------------------------------
|
|
51
|
+
// compile
|
|
52
|
+
// -------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Compiles a mask string into a [CompiledMask].
|
|
56
|
+
*
|
|
57
|
+
* Supported tokens:
|
|
58
|
+
* - `9` — any digit
|
|
59
|
+
* - `A` — any letter
|
|
60
|
+
* - `*` — any alphanumeric
|
|
61
|
+
* - `[from-to]` — integer range, e.g. `[1-12]`, `[1-31]`, `[0-23]`
|
|
62
|
+
* - any other character — literal
|
|
63
|
+
*
|
|
64
|
+
* Note: two `[from-to]` blocks with identical bounds share the same
|
|
65
|
+
* `blockName` key and therefore share partial-tracking state during
|
|
66
|
+
* [apply]. This is an acceptable trade-off; prefer distinct bounds when
|
|
67
|
+
* designing date masks (e.g. `[1-12]/[1-31]/9999`).
|
|
68
|
+
*/
|
|
69
|
+
internal fun compile(mask: String): CompiledMask {
|
|
70
|
+
val slots = mutableListOf<Slot>()
|
|
71
|
+
val expanded = StringBuilder()
|
|
72
|
+
var i = 0
|
|
73
|
+
|
|
74
|
+
while (i < mask.length) {
|
|
75
|
+
if (mask[i] == '[') {
|
|
76
|
+
val closeIdx = mask.indexOf(']', i)
|
|
77
|
+
if (closeIdx == -1) {
|
|
78
|
+
// Malformed — treat '[' as a literal and advance past it.
|
|
79
|
+
slots.add(Slot.Literal('['))
|
|
80
|
+
expanded.append('[')
|
|
81
|
+
i++
|
|
82
|
+
continue
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
val content = mask.substring(i + 1, closeIdx) // e.g. "1-12"
|
|
86
|
+
val dashIdx = content.indexOf('-')
|
|
87
|
+
val from = if (dashIdx > 0) content.substring(0, dashIdx).toIntOrNull() else null
|
|
88
|
+
val to = if (dashIdx > 0) content.substring(dashIdx + 1).toIntOrNull() else null
|
|
89
|
+
if (from != null && to != null) {
|
|
90
|
+
val length = maxOf(from.toString().length, to.toString().length)
|
|
91
|
+
repeat(length) {
|
|
92
|
+
slots.add(Slot.RangeSlot(content, length, from, to))
|
|
93
|
+
expanded.append('9')
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
// Malformed range — emit the entire "[...]" as individual literals.
|
|
98
|
+
val literal = "[$content]"
|
|
99
|
+
for (c in literal) { slots.add(Slot.Literal(c)); expanded.append(c) }
|
|
100
|
+
}
|
|
101
|
+
i = closeIdx + 1
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
when (val ch = mask[i]) {
|
|
105
|
+
'9' -> { slots.add(Slot.Digit); expanded.append('9') }
|
|
106
|
+
'A' -> { slots.add(Slot.Letter); expanded.append('A') }
|
|
107
|
+
'*' -> { slots.add(Slot.Any); expanded.append('*') }
|
|
108
|
+
else -> { slots.add(Slot.Literal(ch)); expanded.append(ch) }
|
|
109
|
+
}
|
|
110
|
+
i++
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return CompiledMask(slots, expanded.toString())
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// -------------------------------------------------------------------------
|
|
118
|
+
// apply
|
|
119
|
+
// -------------------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
internal fun apply(input: String, compiled: CompiledMask): Pair<String, String> {
|
|
122
|
+
if (compiled.slots.isEmpty()) return Pair(input, input)
|
|
123
|
+
|
|
124
|
+
val masked = StringBuilder()
|
|
125
|
+
val raw = StringBuilder()
|
|
126
|
+
val slots = compiled.slots
|
|
127
|
+
|
|
128
|
+
// Accumulate per-block digit strings so we can validate range prefixes
|
|
129
|
+
val blockPartials = mutableMapOf<String, StringBuilder>()
|
|
130
|
+
|
|
131
|
+
var ii = 0 // index into input characters
|
|
132
|
+
var si = 0 // index into slots
|
|
133
|
+
|
|
134
|
+
while (si < slots.size && ii < input.length) {
|
|
135
|
+
val slot = slots[si]
|
|
136
|
+
val c = input[ii]
|
|
137
|
+
|
|
138
|
+
when (slot) {
|
|
139
|
+
is Slot.Digit -> {
|
|
140
|
+
if (c.isDigit()) {
|
|
141
|
+
masked.append(c); raw.append(c); ii++; si++
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
ii++
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
is Slot.Letter -> {
|
|
149
|
+
if (c.isLetter()) {
|
|
150
|
+
masked.append(c); raw.append(c); ii++; si++
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
ii++
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
is Slot.Any -> {
|
|
158
|
+
if (c.isLetterOrDigit()) {
|
|
159
|
+
masked.append(c); raw.append(c); ii++; si++
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
ii++
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
is Slot.Literal -> {
|
|
167
|
+
masked.append(slot.char)
|
|
168
|
+
if (c == slot.char) ii++
|
|
169
|
+
si++
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
is Slot.RangeSlot -> {
|
|
173
|
+
if (c.isDigit()) {
|
|
174
|
+
val partial = blockPartials.getOrPut(slot.blockName) { StringBuilder() }
|
|
175
|
+
val candidate = partial.toString() + c
|
|
176
|
+
|
|
177
|
+
if (isValidPrefix(candidate, slot.from, slot.to, slot.length)) {
|
|
178
|
+
partial.append(c)
|
|
179
|
+
masked.append(c)
|
|
180
|
+
raw.append(c)
|
|
181
|
+
ii++
|
|
182
|
+
si++
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
// Invalid digit for this range position — skip the input char
|
|
186
|
+
ii++
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
// Non-digit where a range digit is expected — skip
|
|
191
|
+
ii++
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Only emit trailing literals if some input was actually consumed.
|
|
198
|
+
if (ii > 0) while (si < slots.size) {
|
|
199
|
+
val slot = slots[si]
|
|
200
|
+
|
|
201
|
+
if (slot is Slot.Literal) {
|
|
202
|
+
masked.append(slot.char)
|
|
203
|
+
si++
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
break
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return Pair(masked.toString(), raw.toString())
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// -------------------------------------------------------------------------
|
|
214
|
+
// extractRaw
|
|
215
|
+
// -------------------------------------------------------------------------
|
|
216
|
+
|
|
217
|
+
internal fun extractRaw(text: String, compiled: CompiledMask): String {
|
|
218
|
+
return extractRaw(text, compiled.expandedMask)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private fun extractRaw(text: String, expandedMask: String): String {
|
|
222
|
+
val raw = StringBuilder()
|
|
223
|
+
val textChars = text.toList()
|
|
224
|
+
val maskChars = expandedMask.toList()
|
|
225
|
+
var ti = 0
|
|
226
|
+
var mi = 0
|
|
227
|
+
|
|
228
|
+
while (ti < textChars.size && mi < maskChars.size) {
|
|
229
|
+
val m = maskChars[mi]
|
|
230
|
+
if (m == '9' || m == 'A' || m == '*') {
|
|
231
|
+
raw.append(textChars[ti])
|
|
232
|
+
ti++
|
|
233
|
+
mi++
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
if (textChars[ti] == m) ti++
|
|
237
|
+
mi++
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return raw.toString()
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// -------------------------------------------------------------------------
|
|
245
|
+
// isValidPrefix
|
|
246
|
+
// -------------------------------------------------------------------------
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Returns true if [partial] (the digits entered so far for a range block) is
|
|
250
|
+
* still consistent with producing a final value inside [from]..[to].
|
|
251
|
+
*
|
|
252
|
+
* After [partial] the remaining positions can be filled with '0'..'9', so we
|
|
253
|
+
* test whether the optimistic interval [min, max] overlaps [from, to].
|
|
254
|
+
*/
|
|
255
|
+
private fun isValidPrefix(partial: String, from: Int, to: Int, length: Int): Boolean {
|
|
256
|
+
val remaining = length - partial.length
|
|
257
|
+
val partialMin = (partial + "0".repeat(remaining)).toIntOrNull() ?: return false
|
|
258
|
+
val partialMax = (partial + "9".repeat(remaining)).toIntOrNull() ?: return false
|
|
259
|
+
return partialMin <= to && partialMax >= from
|
|
260
|
+
}
|
|
261
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
package com.nitroinputmask.engine
|
|
2
|
+
|
|
3
|
+
import com.margelo.nitro.nitroinputmask.NitroMaskOptions
|
|
4
|
+
import com.nitroinputmask.masks.CreditCardMaskEngine
|
|
5
|
+
import com.nitroinputmask.masks.CustomMaskEngine
|
|
6
|
+
import com.nitroinputmask.masks.DatetimeMaskEngine
|
|
7
|
+
import com.nitroinputmask.masks.MoneyMaskEngine
|
|
8
|
+
|
|
9
|
+
internal object MaskEngineFactory {
|
|
10
|
+
fun build(maskType: String, options: NitroMaskOptions): MaskEngineProtocol {
|
|
11
|
+
return when (maskType) {
|
|
12
|
+
"money" -> MoneyMaskEngine(options)
|
|
13
|
+
"datetime" -> DatetimeMaskEngine(options.format ?: "DD/MM/YYYY")
|
|
14
|
+
"credit-card" -> CreditCardMaskEngine(options)
|
|
15
|
+
else -> CustomMaskEngine(options.mask ?: "") // "custom"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
package com.nitroinputmask.engine
|
|
2
|
+
|
|
3
|
+
interface MaskEngineProtocol {
|
|
4
|
+
fun apply(input: String): Pair<String, String>
|
|
5
|
+
val wantsTrailingCursor: Boolean
|
|
6
|
+
/** Expanded mask string for cursor positioning. Null means put cursor at end. */
|
|
7
|
+
val expandedMask: String?
|
|
8
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
package com.nitroinputmask.masks
|
|
2
|
+
|
|
3
|
+
import com.margelo.nitro.nitroinputmask.NitroMaskOptions
|
|
4
|
+
import com.nitroinputmask.engine.CompiledMask
|
|
5
|
+
import com.nitroinputmask.engine.MaskEngine
|
|
6
|
+
import com.nitroinputmask.engine.MaskEngineProtocol
|
|
7
|
+
|
|
8
|
+
internal class CreditCardMaskEngine(options: NitroMaskOptions) : MaskEngineProtocol {
|
|
9
|
+
private val compiled: CompiledMask
|
|
10
|
+
private val obfuscated: Boolean = options.obfuscated ?: false
|
|
11
|
+
|
|
12
|
+
init {
|
|
13
|
+
val mask = when (options.issuer) {
|
|
14
|
+
"amex" -> "9999 999999 99999"
|
|
15
|
+
"diners" -> "9999 999999 9999"
|
|
16
|
+
else -> "9999 9999 9999 9999" // visa-or-mastercard (default)
|
|
17
|
+
}
|
|
18
|
+
compiled = MaskEngine.compile(mask)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
override fun apply(input: String): Pair<String, String> {
|
|
22
|
+
val (masked, raw) = MaskEngine.apply(input, compiled)
|
|
23
|
+
|
|
24
|
+
if (obfuscated && masked.isNotEmpty()) {
|
|
25
|
+
return Pair(obfuscate(masked), raw)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return Pair(masked, raw)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
override val wantsTrailingCursor: Boolean = false
|
|
32
|
+
override val expandedMask: String? = compiled.expandedMask
|
|
33
|
+
|
|
34
|
+
private fun obfuscate(masked: String): String {
|
|
35
|
+
val lastSpaceIdx = masked.lastIndexOf(' ')
|
|
36
|
+
if (lastSpaceIdx == -1) return masked // single group — don't obfuscate
|
|
37
|
+
|
|
38
|
+
val prefix = masked.substring(0, lastSpaceIdx + 1)
|
|
39
|
+
val lastGroup = masked.substring(lastSpaceIdx + 1)
|
|
40
|
+
|
|
41
|
+
val obfuscatedPrefix = prefix.map { c ->
|
|
42
|
+
if (c.isDigit()) '*' else c
|
|
43
|
+
}.joinToString("")
|
|
44
|
+
|
|
45
|
+
return obfuscatedPrefix + lastGroup
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
package com.nitroinputmask.masks
|
|
2
|
+
|
|
3
|
+
import com.nitroinputmask.engine.CompiledMask
|
|
4
|
+
import com.nitroinputmask.engine.MaskEngine
|
|
5
|
+
import com.nitroinputmask.engine.MaskEngineProtocol
|
|
6
|
+
|
|
7
|
+
internal class CustomMaskEngine(mask: String) : MaskEngineProtocol {
|
|
8
|
+
private val compiled: CompiledMask = MaskEngine.compile(mask)
|
|
9
|
+
|
|
10
|
+
override fun apply(input: String): Pair<String, String> {
|
|
11
|
+
return MaskEngine.apply(input, compiled)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
override val wantsTrailingCursor: Boolean = false
|
|
15
|
+
override val expandedMask: String? = compiled.expandedMask
|
|
16
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
package com.nitroinputmask.masks
|
|
2
|
+
|
|
3
|
+
import com.nitroinputmask.engine.CompiledMask
|
|
4
|
+
import com.nitroinputmask.engine.MaskEngine
|
|
5
|
+
import com.nitroinputmask.engine.MaskEngineProtocol
|
|
6
|
+
|
|
7
|
+
internal class DatetimeMaskEngine(format: String) : MaskEngineProtocol {
|
|
8
|
+
private val compiled: CompiledMask = MaskEngine.compile(formatToPattern(format))
|
|
9
|
+
|
|
10
|
+
override fun apply(input: String): Pair<String, String> {
|
|
11
|
+
return MaskEngine.apply(input, compiled)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
override val wantsTrailingCursor: Boolean = false
|
|
15
|
+
override val expandedMask: String? = compiled.expandedMask
|
|
16
|
+
|
|
17
|
+
companion object {
|
|
18
|
+
private fun formatToPattern(format: String): String {
|
|
19
|
+
val sb = StringBuilder()
|
|
20
|
+
var i = 0
|
|
21
|
+
|
|
22
|
+
while (i < format.length) {
|
|
23
|
+
when {
|
|
24
|
+
format.startsWith("YYYY", i) -> { sb.append("9999"); i += 4 }
|
|
25
|
+
format.startsWith("MM", i) -> { sb.append("[1-12]"); i += 2 }
|
|
26
|
+
format.startsWith("DD", i) -> { sb.append("[1-31]"); i += 2 }
|
|
27
|
+
format.startsWith("HH", i) -> { sb.append("[0-23]"); i += 2 }
|
|
28
|
+
format.startsWith("hh", i) -> { sb.append("[1-12]"); i += 2 }
|
|
29
|
+
format.startsWith("mm", i) -> { sb.append("[0-59]"); i += 2 }
|
|
30
|
+
format.startsWith("ss", i) -> { sb.append("[0-59]"); i += 2 }
|
|
31
|
+
else -> { sb.append(format[i]); i++ }
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return sb.toString()
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
package com.nitroinputmask.masks
|
|
2
|
+
|
|
3
|
+
import com.margelo.nitro.nitroinputmask.NitroMaskOptions
|
|
4
|
+
import com.nitroinputmask.engine.MaskEngineProtocol
|
|
5
|
+
|
|
6
|
+
internal class MoneyMaskEngine(options: NitroMaskOptions) : MaskEngineProtocol {
|
|
7
|
+
private val precision: Int = (options.precision?.toInt()) ?: 2
|
|
8
|
+
private val separator: String = options.separator ?: ","
|
|
9
|
+
private val delimiter: String = options.delimiter ?: "."
|
|
10
|
+
private val unit: String = options.unit ?: ""
|
|
11
|
+
private val suffixUnit: String = options.suffixUnit ?: ""
|
|
12
|
+
private val zeroCents: Boolean = options.zeroCents ?: false
|
|
13
|
+
|
|
14
|
+
override fun apply(input: String): Pair<String, String> {
|
|
15
|
+
val digits = input.filter { it.isDigit() }
|
|
16
|
+
|
|
17
|
+
if (digits.isEmpty()) {
|
|
18
|
+
return Pair("", "")
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
val effectivePrecision = if (zeroCents) 0 else precision
|
|
22
|
+
|
|
23
|
+
// Pad on the left so we have at least (precision+1) digits
|
|
24
|
+
val minLength = effectivePrecision + 1
|
|
25
|
+
val padded = digits.padStart(maxOf(minLength, digits.length), '0')
|
|
26
|
+
|
|
27
|
+
// Split integer and decimal parts
|
|
28
|
+
var intPart: String
|
|
29
|
+
val centPart: String
|
|
30
|
+
if (effectivePrecision > 0) {
|
|
31
|
+
intPart = padded.dropLast(effectivePrecision)
|
|
32
|
+
centPart = padded.takeLast(effectivePrecision)
|
|
33
|
+
} else {
|
|
34
|
+
intPart = padded
|
|
35
|
+
centPart = ""
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Remove leading zeros from intPart, keep at least "0"
|
|
39
|
+
while (intPart.length > 1 && intPart.startsWith('0')) {
|
|
40
|
+
intPart = intPart.drop(1)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Insert delimiter every 3 digits from right
|
|
44
|
+
val sb = StringBuilder()
|
|
45
|
+
val total = intPart.length
|
|
46
|
+
for (i in intPart.indices) {
|
|
47
|
+
sb.append(intPart[i])
|
|
48
|
+
val remaining = total - i - 1
|
|
49
|
+
if (remaining > 0 && remaining % 3 == 0) {
|
|
50
|
+
sb.append(delimiter)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Build result
|
|
55
|
+
var masked = unit + sb.toString()
|
|
56
|
+
if (effectivePrecision > 0) {
|
|
57
|
+
masked += separator + centPart
|
|
58
|
+
}
|
|
59
|
+
masked += suffixUnit
|
|
60
|
+
|
|
61
|
+
return Pair(masked, digits)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
override val wantsTrailingCursor: Boolean = true
|
|
65
|
+
override val expandedMask: String? = null
|
|
66
|
+
}
|