react-input-mask-format 2.0.0 → 2.2.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/README.md CHANGED
@@ -4,10 +4,26 @@
4
4
  [![npm downloads](https://img.shields.io/npm/dm/react-input-mask-format.svg)](https://www.npmjs.com/package/react-input-mask-format)
5
5
  [![CI](https://github.com/Temirtator/react-input-mask-format/actions/workflows/ci.yml/badge.svg)](https://github.com/Temirtator/react-input-mask-format/actions/workflows/ci.yml)
6
6
 
7
- Input masking component for React. Made with attention to UX.
7
+ General-purpose input masking for React — dates, phone numbers, cards, custom
8
+ tokens, and a growing set of ready-made country packs (Kazakhstan ships first).
9
+ Made with attention to UX.
8
10
 
9
11
  A maintained fork of [react-input-mask](https://github.com/sanniassin/react-input-mask).
10
12
 
13
+ ## What's new in 2.2
14
+
15
+ - `useMask` hook — mask your own `<input>` with no wrapper component, see [useMask hook](#usemask-hook)
16
+ - `react-input-mask-format/number` — numeric & currency masking, a lightweight `react-number-format` alternative, see [Numeric & currency](#numeric--currency--react-input-mask-formatnumber)
17
+
18
+ ### 2.1
19
+
20
+ - `transform` prop (`uppercase` / `lowercase` / custom) — see [transform](#transform)
21
+ - Custom tokens via RegExp `formatChars` + shipped `extendedFormatChars` (`A`, `Я`, `#`)
22
+ - `react-input-mask-format/presets` — `card` + Kazakhstan pack (phone, IIN, BIN, IBAN, plate, postal)
23
+ - `react-input-mask-format/validators` — `isValidIin`, `isValidBin`, `isValidKzIban`, `luhn`
24
+
25
+ All additive — no migration needed.
26
+
11
27
  ## Why this fork
12
28
 
13
29
  The original `react-input-mask` has been unmaintained since 2021. This fork is:
@@ -45,6 +61,8 @@ function DateInput(props) {
45
61
  | Name | Type | Default | Description |
46
62
  | :---------------------------------------------------------: | :--------------------------------: | :-----: | :--- |
47
63
  | **[`mask`](#mask)** | `{String\|Array<String, RegExp>}` | | Mask format |
64
+ | **[`transform`](#transform)** | `{"uppercase"\|"lowercase"\|Function}` | | Normalize each entered character before it's tested against the mask |
65
+ | **[`formatChars`](#custom-tokens-formatchars-and-extendedformatchars)** | `{Object<String, RegExp>}` | | Custom mask tokens beyond the default `9`, `a`, `*` |
48
66
  | **[`maskPlaceholder`](#maskplaceholder)** | `{String}` | `_` | Placeholder to cover unfilled parts of the mask |
49
67
  | **[`alwaysShowMask`](#alwaysshowmask)** | `{Boolean}` | `false` | Whether mask prefix and placeholder should be displayed when input is empty and has no focus |
50
68
  | **[`beforeMaskedStateChange`](#beforemaskedstatechange)** | `{Function}` | | Function to modify value and selection before applying mask |
@@ -79,6 +97,151 @@ const mask = [firstLetter, digit, letter, " ", digit, letter, digit];
79
97
  return <InputMask mask={mask} />;
80
98
  ```
81
99
 
100
+ ### `transform`
101
+
102
+ Normalize every entered character. Runs before the mask test, so an uppercase-only
103
+ class accepts lowercase typing; runs before `beforeMaskedStateChange`.
104
+
105
+ ```jsx
106
+ <InputMask mask="aaaaaa" transform="uppercase" /> // "abc" → "ABC"
107
+ <InputMask mask={[/[A-Z]/, /[A-Z]/]} transform="uppercase" />
108
+ <InputMask mask="9a9a" transform={(char, position) => char} />
109
+ ```
110
+
111
+ Values: `"uppercase"`, `"lowercase"`, or `(char, position) => char` (must be pure).
112
+
113
+ ### Custom tokens (`formatChars`) and `extendedFormatChars`
114
+
115
+ Define your own mask tokens by mapping a character to a `RegExp`:
116
+
117
+ ```jsx
118
+ import InputMask, { extendedFormatChars } from "react-input-mask-format";
119
+
120
+ // roll your own
121
+ <InputMask mask="ww-ww" formatChars={{ w: /[a-z]/ }} />
122
+
123
+ // or use the shipped extended set: A (uppercase), Я (Cyrillic incl. Kazakh), # (hex)
124
+ <InputMask mask="AAA-###" formatChars={extendedFormatChars} />
125
+ ```
126
+
127
+ The default tokens (`9`, `a`, `*`) are unchanged. Passing string values (`{ w: "[a-z]" }`)
128
+ still works but is deprecated — pass `RegExp` values.
129
+
130
+ ## Presets
131
+
132
+ Ready-made mask configs. Import what you need and spread it — tree-shakeable, and the
133
+ core bundle carries none of it.
134
+
135
+ ```jsx
136
+ import InputMask from "react-input-mask-format";
137
+ import { kzPhone, kzIban } from "react-input-mask-format/presets";
138
+
139
+ <InputMask {...kzPhone} value={phone} onChange={onChange} />
140
+ <InputMask {...kzIban} value={iban} onChange={onChange} />
141
+ ```
142
+
143
+ Presets are a country-agnostic system; Kazakhstan is the first (flagship) country pack —
144
+ PRs adding other countries are welcome.
145
+
146
+ | export | mask | example |
147
+ | --- | --- | --- |
148
+ | `card` | `9999 9999 9999 9999` | `4242 4242 4242 4242` |
149
+ | `kzPhone` | `+7 (799) 999-99-99` | `+7 (701) 234-56-78` |
150
+ | `kzIin` | `999999999999` | `901010123458` |
151
+ | `kzBin` | `999999999999` | `150340004984` |
152
+ | `kzIban` | `KZ99 999* **** **** ****` (uppercase) | `KZ86 125K ZT50 0410 0100` |
153
+ | `kzPlate` | `123 ABC 02` (letters `ABCEHKMNOPTXY`, uppercase) | `123 ABC 02` |
154
+ | `kzPostal` | `999999` | `050000` |
155
+
156
+ ## Validators
157
+
158
+ Optional checksum validators, separate entry point, zero-deps. Accept raw or formatted
159
+ input; return `false` on empty/invalid.
160
+
161
+ ```jsx
162
+ import { isValidIin, isValidBin, isValidKzIban, luhn } from "react-input-mask-format/validators";
163
+
164
+ isValidIin("901010123458"); // KZ IIN/BIN mod-11 checksum
165
+ isValidKzIban("KZ86 125K ZT50 0410 0100"); // ISO 7064 MOD-97
166
+ luhn("4242 4242 4242 4242"); // card Luhn
167
+ ```
168
+
169
+ ## `useMask` hook
170
+
171
+ Mask your own `<input>` with no wrapper component. `useMask` returns a ref
172
+ callback:
173
+
174
+ ```tsx
175
+ import { useMask } from "react-input-mask-format";
176
+
177
+ function Phone() {
178
+ const ref = useMask({ mask: "+7 (999) 999-99-99", maskPlaceholder: "_" });
179
+ return <input ref={ref} name="phone" />;
180
+ }
181
+ ```
182
+
183
+ `useMask` is **uncontrolled**: attach it to an input you don't drive with a
184
+ React `value` prop, and read the masked value through your own `onChange` (it
185
+ fires with the masked value) or on form submit. For a controlled component,
186
+ use `<InputMask>`.
187
+
188
+ Options: `mask`, `maskPlaceholder`, `alwaysShowMask`, `formatChars`,
189
+ `transform`, `beforeMaskedStateChange`.
190
+
191
+ ## Numeric & currency — `react-input-mask-format/number`
192
+
193
+ A separate, tree-shakeable entry for formatting numbers, currency, and
194
+ percentages. Prop names match `react-number-format`, so migration is mostly a
195
+ find-and-replace of the import.
196
+
197
+ ```tsx
198
+ import { NumberFormat } from "react-input-mask-format/number";
199
+
200
+ function Amount() {
201
+ const [value, setValue] = React.useState<number>();
202
+ return (
203
+ <NumberFormat
204
+ value={value ?? ""}
205
+ onValueChange={({ floatValue }) => setValue(floatValue)}
206
+ thousandSeparator="," decimalScale={2} fixedDecimalScale prefix="$ "
207
+ allowNegative
208
+ />
209
+ );
210
+ }
211
+ ```
212
+
213
+ `onValueChange` receives `{ value, formattedValue, floatValue }`:
214
+
215
+ ```txt
216
+ typing "1234.5" → value "1234.5" formattedValue "$ 1,234.50" floatValue 1234.5
217
+ ```
218
+
219
+ Options: `thousandSeparator` (`true` → `,`, or a custom string),
220
+ `decimalSeparator` (default `.`), `decimalScale` (truncates, no rounding),
221
+ `fixedDecimalScale`, `prefix`, `suffix`, `allowNegative` (default `true`),
222
+ `allowLeadingZeros`, and `isAllowed(values) => boolean` (reject an edit, e.g.
223
+ for min/max).
224
+
225
+ There is also a ref hook and the pure helpers:
226
+
227
+ ```tsx
228
+ import { useNumberFormat, formatNumber, parseNumber } from "react-input-mask-format/number";
229
+
230
+ const ref = useNumberFormat({ thousandSeparator: " ", decimalScale: 2, prefix: "$ " });
231
+ // <input ref={ref} />
232
+
233
+ formatNumber(1234.5, { thousandSeparator: ",", decimalScale: 2, fixedDecimalScale: true }); // "1,234.50"
234
+ ```
235
+
236
+ ### Migrating from react-number-format
237
+
238
+ `NumberFormat`'s props (`thousandSeparator`, `decimalSeparator`, `decimalScale`,
239
+ `fixedDecimalScale`, `prefix`, `suffix`, `allowNegative`, `allowLeadingZeros`,
240
+ `isAllowed`, `onValueChange`) mirror `react-number-format`'s `NumericFormat`.
241
+ The main differences: `decimalScale` **truncates** rather than rounds while
242
+ typing, and this package ships a single `NumberFormat` (no separate
243
+ `PatternFormat` — use the mask engine / `<InputMask>` for pattern masks).
244
+
82
245
  ### `maskPlaceholder`
83
246
 
84
247
  ```jsx
@@ -0,0 +1,42 @@
1
+ // src/utils/input.ts
2
+ function setInputSelection(input, start, end) {
3
+ if (end === void 0) {
4
+ end = start;
5
+ }
6
+ input.setSelectionRange(start, end);
7
+ }
8
+ function getInputSelection(input) {
9
+ const start = input.selectionStart;
10
+ const end = input.selectionEnd;
11
+ return {
12
+ start,
13
+ end,
14
+ length: (end != null ? end : 0) - (start != null ? start : 0)
15
+ };
16
+ }
17
+ function isInputFocused(input) {
18
+ const inputDocument = input.ownerDocument;
19
+ return inputDocument.hasFocus() && inputDocument.activeElement === input;
20
+ }
21
+
22
+ // src/set-native-value.ts
23
+ function setNativeValue(input, value) {
24
+ const descriptor = Object.getOwnPropertyDescriptor(
25
+ HTMLInputElement.prototype,
26
+ "value"
27
+ );
28
+ const setter = descriptor && descriptor.set;
29
+ if (setter) {
30
+ setter.call(input, value);
31
+ } else {
32
+ input.value = value;
33
+ }
34
+ }
35
+
36
+ export {
37
+ setInputSelection,
38
+ getInputSelection,
39
+ isInputFocused,
40
+ setNativeValue
41
+ };
42
+ //# sourceMappingURL=chunk-CEZXZ7IJ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/utils/input.ts","../src/set-native-value.ts"],"sourcesContent":["import type { Selection } from \"../types\";\n\nexport function setInputSelection(\n input: HTMLInputElement,\n start: number,\n end?: number\n): void {\n if (end === undefined) {\n end = start;\n }\n input.setSelectionRange(start, end);\n}\n\nexport function getInputSelection(input: HTMLInputElement): Required<Selection> {\n const start = input.selectionStart;\n const end = input.selectionEnd;\n\n return {\n start,\n end,\n length: (end ?? 0) - (start ?? 0)\n };\n}\n\nexport function isInputFocused(input: HTMLInputElement): boolean {\n const inputDocument = input.ownerDocument;\n return inputDocument.hasFocus() && inputDocument.activeElement === input;\n}\n","// Write to input.value through the native prototype setter so React's\n// internal value tracker stays stale. The user's keystroke `input` event then\n// bubbles into React's root listener, which sees masked value != stale tracker\n// and fires the consumer's onChange with the masked value. Using the tracked\n// (React-wrapped) setter instead would suppress that onChange.\nexport function setNativeValue(input: HTMLInputElement, value: string): void {\n const descriptor = Object.getOwnPropertyDescriptor(\n HTMLInputElement.prototype,\n \"value\"\n );\n const setter = descriptor && descriptor.set;\n if (setter) {\n setter.call(input, value);\n } else {\n input.value = value;\n }\n}\n"],"mappings":";AAEO,SAAS,kBACd,OACA,OACA,KACM;AACN,MAAI,QAAQ,QAAW;AACrB,UAAM;AAAA,EACR;AACA,QAAM,kBAAkB,OAAO,GAAG;AACpC;AAEO,SAAS,kBAAkB,OAA8C;AAC9E,QAAM,QAAQ,MAAM;AACpB,QAAM,MAAM,MAAM;AAElB,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,SAAS,oBAAO,MAAM,wBAAS;AAAA,EACjC;AACF;AAEO,SAAS,eAAe,OAAkC;AAC/D,QAAM,gBAAgB,MAAM;AAC5B,SAAO,cAAc,SAAS,KAAK,cAAc,kBAAkB;AACrE;;;ACtBO,SAAS,eAAe,OAAyB,OAAqB;AAC3E,QAAM,aAAa,OAAO;AAAA,IACxB,iBAAiB;AAAA,IACjB;AAAA,EACF;AACA,QAAM,SAAS,cAAc,WAAW;AACxC,MAAI,QAAQ;AACV,WAAO,KAAK,OAAO,KAAK;AAAA,EAC1B,OAAO;AACL,UAAM,QAAQ;AAAA,EAChB;AACF;","names":[]}
package/dist/index.cjs CHANGED
@@ -30,10 +30,13 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.tsx
31
31
  var src_exports = {};
32
32
  __export(src_exports, {
33
- default: () => src_default
33
+ default: () => src_default,
34
+ defaultFormatChars: () => defaultFormatChars,
35
+ extendedFormatChars: () => extendedFormatChars,
36
+ useMask: () => useMask
34
37
  });
35
38
  module.exports = __toCommonJS(src_exports);
36
- var import_react2 = __toESM(require("react"), 1);
39
+ var import_react3 = __toESM(require("react"), 1);
37
40
 
38
41
  // src/hooks.ts
39
42
  var import_react = require("react");
@@ -265,6 +268,12 @@ var defaultFormatChars = {
265
268
  a: /[A-Za-z]/,
266
269
  "*": /[A-Za-z0-9]/
267
270
  };
271
+ var extendedFormatChars = {
272
+ ...defaultFormatChars,
273
+ A: /[A-Z]/,
274
+ "\u042F": /[А-Яа-яЁёӘәҒғҚқҢңӨөҰұҮүҺһІі]/,
275
+ "#": /[0-9A-Fa-f]/
276
+ };
268
277
 
269
278
  // src/validate-props.ts
270
279
  function warnDev(condition, message) {
@@ -491,13 +500,14 @@ var MaskUtils = class {
491
500
  if (position >= mask.length) {
492
501
  return value;
493
502
  }
494
- const isAllowed = this.isCharacterAllowedAtPosition(character, position);
503
+ const transformedCharacter = this.transform && this.isPositionEditable(position) ? this.transform(character, position) : character;
504
+ const isAllowed = this.isCharacterAllowedAtPosition(transformedCharacter, position);
495
505
  const isEditable = this.isPositionEditable(position);
496
506
  const nextEditablePosition = this.getRightEditablePosition(position);
497
- const isNextPlaceholder = maskPlaceholder && nextEditablePosition ? character === maskPlaceholder[nextEditablePosition] : null;
507
+ const isNextPlaceholder = maskPlaceholder && nextEditablePosition ? transformedCharacter === maskPlaceholder[nextEditablePosition] : null;
498
508
  const valueBefore = value.slice(0, position);
499
509
  if (isAllowed || !isEditable) {
500
- const insertedCharacter = isAllowed ? character : mask[position];
510
+ const insertedCharacter = isAllowed ? transformedCharacter : mask[position];
501
511
  value = valueBefore + insertedCharacter;
502
512
  }
503
513
  if (!isAllowed && !isEditable && !isNextPlaceholder) {
@@ -592,6 +602,7 @@ var MaskUtils = class {
592
602
  };
593
603
  };
594
604
  this.maskOptions = parseMask(options);
605
+ this.transform = options.transform;
595
606
  }
596
607
  };
597
608
 
@@ -609,15 +620,22 @@ function resolveMaskPlaceholder(maskPlaceholder, maskChar) {
609
620
  }
610
621
  return maskChar;
611
622
  }
612
- function toRegExpFormatChars(formatChars) {
623
+ function normalizeFormatChars(formatChars) {
613
624
  if (!formatChars) {
614
- return void 0;
625
+ return { formatChars: void 0, hasLegacyString: false };
615
626
  }
616
627
  const result = {};
628
+ let hasLegacyString = false;
617
629
  for (const key of Object.keys(formatChars)) {
618
- result[key] = new RegExp(formatChars[key]);
630
+ const value = formatChars[key];
631
+ if (typeof value === "string") {
632
+ hasLegacyString = true;
633
+ result[key] = new RegExp(value);
634
+ } else {
635
+ result[key] = value;
636
+ }
619
637
  }
620
- return result;
638
+ return { formatChars: result, hasLegacyString };
621
639
  }
622
640
  function createBeforeMaskedStateChangeAdapter(beforeMaskedValueChange, maskOptions) {
623
641
  return ({ previousState, currentState, nextState }) => {
@@ -631,14 +649,243 @@ function createBeforeMaskedStateChangeAdapter(beforeMaskedValueChange, maskOptio
631
649
  };
632
650
  }
633
651
 
652
+ // src/utils/transform.ts
653
+ function resolveTransform(transform) {
654
+ if (!transform) {
655
+ return void 0;
656
+ }
657
+ if (transform === "uppercase") {
658
+ return (char) => char.toUpperCase();
659
+ }
660
+ if (transform === "lowercase") {
661
+ return (char) => char.toLowerCase();
662
+ }
663
+ if (typeof transform === "function") {
664
+ return transform;
665
+ }
666
+ if (process.env.NODE_ENV !== "production") {
667
+ console.error(
668
+ `react-input-mask-format: invalid transform "${String(
669
+ transform
670
+ )}", expected "uppercase" | "lowercase" | (char, position) => char`
671
+ );
672
+ }
673
+ return void 0;
674
+ }
675
+
676
+ // src/use-mask.ts
677
+ var import_react2 = require("react");
678
+
679
+ // src/set-native-value.ts
680
+ function setNativeValue(input, value) {
681
+ const descriptor = Object.getOwnPropertyDescriptor(
682
+ HTMLInputElement.prototype,
683
+ "value"
684
+ );
685
+ const setter = descriptor && descriptor.set;
686
+ if (setter) {
687
+ setter.call(input, value);
688
+ } else {
689
+ input.value = value;
690
+ }
691
+ }
692
+
693
+ // src/bind-mask.ts
694
+ var IS_JSDOM = typeof navigator !== "undefined" && navigator.userAgent.includes("jsdom");
695
+ function createMaskController(maskUtils, options) {
696
+ let input = null;
697
+ let lastValue = "";
698
+ let lastSelection = { start: null, end: null };
699
+ function getInputState() {
700
+ return { value: input.value, selection: getInputSelection(input) };
701
+ }
702
+ function getLastInputState() {
703
+ return { value: lastValue, selection: lastSelection };
704
+ }
705
+ function setInputState({ value, selection }) {
706
+ setNativeValue(input, value);
707
+ lastValue = value;
708
+ if (input && isInputFocused(input) && selection.start !== null && selection.end !== null) {
709
+ setInputSelection(input, selection.start, selection.end);
710
+ lastSelection = getInputSelection(input);
711
+ } else {
712
+ lastSelection = selection;
713
+ }
714
+ if (IS_JSDOM) {
715
+ const resyncValue = value;
716
+ const resyncSelection = selection;
717
+ queueMicrotask(() => {
718
+ if (!input || input.value !== resyncValue) return;
719
+ input.value = resyncValue;
720
+ if (isInputFocused(input) && resyncSelection.start !== null && resyncSelection.end !== null) {
721
+ setInputSelection(input, resyncSelection.start, resyncSelection.end);
722
+ }
723
+ });
724
+ }
725
+ }
726
+ function handleInput() {
727
+ const currentState = getInputState();
728
+ const previousState = getLastInputState();
729
+ let nextState = maskUtils.processChange(currentState, previousState);
730
+ if (options.beforeMaskedStateChange) {
731
+ nextState = options.beforeMaskedStateChange({ currentState, previousState, nextState });
732
+ }
733
+ setInputState(nextState);
734
+ }
735
+ function handleFocus() {
736
+ const currentValue = getInputState().value;
737
+ if (!maskUtils.isValueFilled(currentValue)) {
738
+ const newValue = maskUtils.formatValue(currentValue);
739
+ const newSelection = maskUtils.getDefaultSelectionForValue(newValue);
740
+ let nextState = { value: newValue, selection: newSelection };
741
+ if (options.beforeMaskedStateChange) {
742
+ nextState = options.beforeMaskedStateChange({ currentState: getInputState(), nextState });
743
+ }
744
+ setInputState(nextState);
745
+ defer(() => {
746
+ if (input && isInputFocused(input)) {
747
+ setInputSelection(input, lastSelection.start, lastSelection.end);
748
+ }
749
+ });
750
+ }
751
+ }
752
+ function handleBlur() {
753
+ const lastValueOnBlur = getLastInputState().value;
754
+ if (!options.alwaysShowMask && maskUtils.isValueEmpty(lastValueOnBlur)) {
755
+ let nextState = { value: "", selection: { start: null, end: null } };
756
+ if (options.beforeMaskedStateChange) {
757
+ nextState = options.beforeMaskedStateChange({ currentState: getInputState(), nextState });
758
+ }
759
+ setInputState(nextState);
760
+ }
761
+ }
762
+ function handleMouseDown(event) {
763
+ if (!input) return;
764
+ const { value } = getInputState();
765
+ const inputDocument = getElementDocument(input);
766
+ if (!isInputFocused(input) && !maskUtils.isValueFilled(value)) {
767
+ const mouseDownX = event.clientX;
768
+ const mouseDownY = event.clientY;
769
+ const mouseDownTime = (/* @__PURE__ */ new Date()).getTime();
770
+ const mouseUpHandler = (mouseUpEvent) => {
771
+ inputDocument.removeEventListener("mouseup", mouseUpHandler);
772
+ if (!input || !isInputFocused(input)) return;
773
+ const deltaX = Math.abs(mouseUpEvent.clientX - mouseDownX);
774
+ const deltaY = Math.abs(mouseUpEvent.clientY - mouseDownY);
775
+ const axisDelta = Math.max(deltaX, deltaY);
776
+ const timeDelta = (/* @__PURE__ */ new Date()).getTime() - mouseDownTime;
777
+ if (axisDelta <= 10 && timeDelta <= 200 || axisDelta <= 5 && timeDelta <= 300) {
778
+ const newSelection = maskUtils.getDefaultSelectionForValue(lastValue);
779
+ if (newSelection.start !== null && newSelection.end !== null) {
780
+ setInputSelection(input, newSelection.start, newSelection.end);
781
+ lastSelection = getInputSelection(input);
782
+ }
783
+ }
784
+ };
785
+ inputDocument.addEventListener("mouseup", mouseUpHandler);
786
+ }
787
+ }
788
+ function bind(el) {
789
+ input = el;
790
+ lastValue = el.value;
791
+ lastSelection = getInputSelection(el);
792
+ const formatted = maskUtils.formatValue(el.value);
793
+ if (!maskUtils.isValueEmpty(formatted) || options.alwaysShowMask) {
794
+ setNativeValue(el, formatted);
795
+ lastValue = formatted;
796
+ }
797
+ el.addEventListener("input", handleInput);
798
+ el.addEventListener("focus", handleFocus);
799
+ el.addEventListener("blur", handleBlur);
800
+ el.addEventListener("mousedown", handleMouseDown);
801
+ }
802
+ function unbind() {
803
+ if (input) {
804
+ input.removeEventListener("input", handleInput);
805
+ input.removeEventListener("focus", handleFocus);
806
+ input.removeEventListener("blur", handleBlur);
807
+ input.removeEventListener("mousedown", handleMouseDown);
808
+ }
809
+ input = null;
810
+ }
811
+ function update(nextMaskUtils, nextOptions) {
812
+ maskUtils = nextMaskUtils;
813
+ options = nextOptions;
814
+ }
815
+ return { bind, unbind, update };
816
+ }
817
+
818
+ // src/use-mask.ts
819
+ function useMask(options) {
820
+ const {
821
+ mask,
822
+ maskPlaceholder: maskPlaceholderProp,
823
+ alwaysShowMask = false,
824
+ formatChars,
825
+ transform,
826
+ beforeMaskedStateChange: beforeMaskedStateChangeProp,
827
+ beforeMaskedValueChange
828
+ } = options;
829
+ if (beforeMaskedValueChange !== void 0) {
830
+ warnDeprecatedOnce(
831
+ "beforeMaskedValueChange",
832
+ "beforeMaskedValueChange is deprecated, use beforeMaskedStateChange."
833
+ );
834
+ }
835
+ const resolvedPlaceholder = resolveMaskPlaceholder(maskPlaceholderProp, void 0);
836
+ const maskPlaceholder = resolvedPlaceholder === void 0 ? "_" : resolvedPlaceholder;
837
+ const { formatChars: normalizedFormatChars, hasLegacyString } = normalizeFormatChars(formatChars);
838
+ if (hasLegacyString) {
839
+ warnDeprecatedOnce(
840
+ "formatChars",
841
+ "string values in formatChars are deprecated, pass RegExp values instead."
842
+ );
843
+ }
844
+ const maskUtils = new MaskUtils({
845
+ mask: mask != null ? mask : null,
846
+ maskPlaceholder,
847
+ formatChars: normalizedFormatChars,
848
+ transform: resolveTransform(transform)
849
+ });
850
+ let beforeMaskedStateChange = beforeMaskedStateChangeProp;
851
+ if (!beforeMaskedStateChange && beforeMaskedValueChange) {
852
+ const v2MaskOptions = {
853
+ mask: mask != null ? mask : void 0,
854
+ maskChar: maskPlaceholder,
855
+ alwaysShowMask,
856
+ formatChars: formatChars != null ? formatChars : { "9": "[0-9]", a: "[A-Za-z]", "*": "[A-Za-z0-9]" },
857
+ permanents: maskUtils.maskOptions.permanents
858
+ };
859
+ beforeMaskedStateChange = createBeforeMaskedStateChangeAdapter(beforeMaskedValueChange, v2MaskOptions);
860
+ }
861
+ const controllerOptions = { alwaysShowMask, beforeMaskedStateChange };
862
+ const controllerRef = (0, import_react2.useRef)(null);
863
+ (0, import_react2.useEffect)(() => {
864
+ var _a;
865
+ (_a = controllerRef.current) == null ? void 0 : _a.update(maskUtils, controllerOptions);
866
+ });
867
+ return (0, import_react2.useCallback)((el) => {
868
+ var _a;
869
+ if (el) {
870
+ const controller = createMaskController(maskUtils, controllerOptions);
871
+ controller.bind(el);
872
+ controllerRef.current = controller;
873
+ } else {
874
+ (_a = controllerRef.current) == null ? void 0 : _a.unbind();
875
+ controllerRef.current = null;
876
+ }
877
+ }, []);
878
+ }
879
+
634
880
  // src/index.tsx
635
- var InputMask = (0, import_react2.forwardRef)(function InputMask2(props, forwardedRef) {
881
+ var InputMask = (0, import_react3.forwardRef)(function InputMask2(props, forwardedRef) {
636
882
  const {
637
883
  alwaysShowMask = false,
638
884
  children,
639
885
  mask,
640
886
  maskPlaceholder: maskPlaceholderProp,
641
887
  beforeMaskedStateChange: beforeMaskedStateChangeProp,
888
+ transform,
642
889
  maskChar,
643
890
  formatChars,
644
891
  beforeMaskedValueChange,
@@ -647,9 +894,6 @@ var InputMask = (0, import_react2.forwardRef)(function InputMask2(props, forward
647
894
  if (maskChar !== void 0) {
648
895
  warnDeprecatedOnce("maskChar", "maskChar is deprecated, use maskPlaceholder instead. See migration guide: https://github.com/Temirtator/react-input-mask-format#migrating-from-react-input-mask");
649
896
  }
650
- if (formatChars !== void 0) {
651
- warnDeprecatedOnce("formatChars", "formatChars is deprecated, prefer an array mask with RegExps. See migration guide: https://github.com/Temirtator/react-input-mask-format#migrating-from-react-input-mask");
652
- }
653
897
  if (beforeMaskedValueChange !== void 0) {
654
898
  warnDeprecatedOnce("beforeMaskedValueChange", "beforeMaskedValueChange is deprecated, use beforeMaskedStateChange. See migration guide: https://github.com/Temirtator/react-input-mask-format#migrating-from-react-input-mask");
655
899
  }
@@ -657,10 +901,18 @@ var InputMask = (0, import_react2.forwardRef)(function InputMask2(props, forward
657
901
  const maskPlaceholder = resolvedPlaceholder === void 0 ? "_" : resolvedPlaceholder;
658
902
  validateMaxLength(props);
659
903
  validateMaskPlaceholder({ ...props, maskPlaceholder });
904
+ const { formatChars: normalizedFormatChars, hasLegacyString } = normalizeFormatChars(formatChars);
905
+ if (hasLegacyString) {
906
+ warnDeprecatedOnce(
907
+ "formatChars",
908
+ 'string values in formatChars are deprecated, pass RegExp values instead (e.g. { "#": /[0-9]/ }). See migration guide: https://github.com/Temirtator/react-input-mask-format#migrating-from-react-input-mask'
909
+ );
910
+ }
660
911
  const maskUtils = new MaskUtils({
661
912
  mask,
662
913
  maskPlaceholder,
663
- formatChars: toRegExpFormatChars(formatChars)
914
+ formatChars: normalizedFormatChars,
915
+ transform: resolveTransform(transform)
664
916
  });
665
917
  let beforeMaskedStateChange = beforeMaskedStateChangeProp;
666
918
  if (!beforeMaskedStateChange && beforeMaskedValueChange) {
@@ -809,7 +1061,7 @@ var InputMask = (0, import_react2.forwardRef)(function InputMask2(props, forward
809
1061
  renderValue = newValue;
810
1062
  }
811
1063
  const lastSelection = getLastInputState().selection;
812
- (0, import_react2.useLayoutEffect)(() => {
1064
+ (0, import_react3.useLayoutEffect)(() => {
813
1065
  if (isMasked && isControlled) {
814
1066
  setInputState({
815
1067
  ...getLastInputState(),
@@ -817,7 +1069,7 @@ var InputMask = (0, import_react2.forwardRef)(function InputMask2(props, forward
817
1069
  });
818
1070
  }
819
1071
  });
820
- (0, import_react2.useLayoutEffect)(() => {
1072
+ (0, import_react3.useLayoutEffect)(() => {
821
1073
  if (!isMasked) {
822
1074
  return;
823
1075
  }
@@ -874,10 +1126,16 @@ var InputMask = (0, import_react2.forwardRef)(function InputMask2(props, forward
874
1126
  };
875
1127
  if (children) {
876
1128
  validateChildren(props, children);
877
- return import_react2.default.cloneElement(children, inputProps);
1129
+ return import_react3.default.cloneElement(children, inputProps);
878
1130
  }
879
- return /* @__PURE__ */ import_react2.default.createElement("input", { ...inputProps });
1131
+ return /* @__PURE__ */ import_react3.default.createElement("input", { ...inputProps });
880
1132
  });
881
1133
  InputMask.displayName = "InputMask";
882
1134
  var src_default = InputMask;
1135
+ // Annotate the CommonJS export names for ESM import in node:
1136
+ 0 && (module.exports = {
1137
+ defaultFormatChars,
1138
+ extendedFormatChars,
1139
+ useMask
1140
+ });
883
1141
  //# sourceMappingURL=index.cjs.map