react-input-mask-format 2.1.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
@@ -10,7 +10,12 @@ Made with attention to UX.
10
10
 
11
11
  A maintained fork of [react-input-mask](https://github.com/sanniassin/react-input-mask).
12
12
 
13
- ## What's new in 2.1
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
14
19
 
15
20
  - `transform` prop (`uppercase` / `lowercase` / custom) — see [transform](#transform)
16
21
  - Custom tokens via RegExp `formatChars` + shipped `extendedFormatChars` (`A`, `Я`, `#`)
@@ -161,6 +166,82 @@ isValidKzIban("KZ86 125K ZT50 0410 0100"); // ISO 7064 MOD-97
161
166
  luhn("4242 4242 4242 4242"); // card Luhn
162
167
  ```
163
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
+
164
245
  ### `maskPlaceholder`
165
246
 
166
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
@@ -32,10 +32,11 @@ var src_exports = {};
32
32
  __export(src_exports, {
33
33
  default: () => src_default,
34
34
  defaultFormatChars: () => defaultFormatChars,
35
- extendedFormatChars: () => extendedFormatChars
35
+ extendedFormatChars: () => extendedFormatChars,
36
+ useMask: () => useMask
36
37
  });
37
38
  module.exports = __toCommonJS(src_exports);
38
- var import_react2 = __toESM(require("react"), 1);
39
+ var import_react3 = __toESM(require("react"), 1);
39
40
 
40
41
  // src/hooks.ts
41
42
  var import_react = require("react");
@@ -672,8 +673,212 @@ function resolveTransform(transform) {
672
673
  return void 0;
673
674
  }
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
+
675
880
  // src/index.tsx
676
- var InputMask = (0, import_react2.forwardRef)(function InputMask2(props, forwardedRef) {
881
+ var InputMask = (0, import_react3.forwardRef)(function InputMask2(props, forwardedRef) {
677
882
  const {
678
883
  alwaysShowMask = false,
679
884
  children,
@@ -856,7 +1061,7 @@ var InputMask = (0, import_react2.forwardRef)(function InputMask2(props, forward
856
1061
  renderValue = newValue;
857
1062
  }
858
1063
  const lastSelection = getLastInputState().selection;
859
- (0, import_react2.useLayoutEffect)(() => {
1064
+ (0, import_react3.useLayoutEffect)(() => {
860
1065
  if (isMasked && isControlled) {
861
1066
  setInputState({
862
1067
  ...getLastInputState(),
@@ -864,7 +1069,7 @@ var InputMask = (0, import_react2.forwardRef)(function InputMask2(props, forward
864
1069
  });
865
1070
  }
866
1071
  });
867
- (0, import_react2.useLayoutEffect)(() => {
1072
+ (0, import_react3.useLayoutEffect)(() => {
868
1073
  if (!isMasked) {
869
1074
  return;
870
1075
  }
@@ -921,15 +1126,16 @@ var InputMask = (0, import_react2.forwardRef)(function InputMask2(props, forward
921
1126
  };
922
1127
  if (children) {
923
1128
  validateChildren(props, children);
924
- return import_react2.default.cloneElement(children, inputProps);
1129
+ return import_react3.default.cloneElement(children, inputProps);
925
1130
  }
926
- return /* @__PURE__ */ import_react2.default.createElement("input", { ...inputProps });
1131
+ return /* @__PURE__ */ import_react3.default.createElement("input", { ...inputProps });
927
1132
  });
928
1133
  InputMask.displayName = "InputMask";
929
1134
  var src_default = InputMask;
930
1135
  // Annotate the CommonJS export names for ESM import in node:
931
1136
  0 && (module.exports = {
932
1137
  defaultFormatChars,
933
- extendedFormatChars
1138
+ extendedFormatChars,
1139
+ useMask
934
1140
  });
935
1141
  //# sourceMappingURL=index.cjs.map