currency-fomatter 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/.eslintignore ADDED
@@ -0,0 +1,2 @@
1
+ node_modules
2
+ dist
package/.eslintrc ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "root": true,
3
+ "extends": [
4
+ "eslint:recommended",
5
+ "plugin:react/recommended",
6
+ "plugin:@typescript-eslint/eslint-recommended",
7
+ "plugin:@typescript-eslint/recommended"
8
+ ],
9
+ "parser": "@typescript-eslint/parser",
10
+ "plugins": [
11
+ "@typescript-eslint",
12
+ "react",
13
+ "react-hooks"
14
+ ],
15
+ "rules": {
16
+ "react-hooks/rules-of-hooks": "error",
17
+ "react-hooks/exhaustive-deps": "warn",
18
+ "@typescript-eslint/no-non-null-assertion": "off",
19
+ "@typescript-eslint/ban-ts-comment": "off",
20
+ "@typescript-eslint/no-explicit-any": "off"
21
+ },
22
+ "settings": {
23
+ "react": {
24
+ "version": "detect"
25
+ }
26
+ },
27
+ "env": {
28
+ "browser": true,
29
+ "node": true
30
+ },
31
+ "globals": {
32
+ "JSX": true
33
+ }
34
+ }
package/README.md ADDED
@@ -0,0 +1 @@
1
+ # currency-fomatter
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "currency-fomatter",
3
+ "version": "1.0.0",
4
+ "description": "",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "build": "tsc",
8
+ "test": "echo \"Error: no test specified\" && exit 1",
9
+ "lint": "eslint \"{**/*,*}.{js,ts,jsx,tsx}\""
10
+ },
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/nguyenduyhoang35/currency-fomatter.git"
14
+ },
15
+ "keywords": [],
16
+ "author": "HoangNguyen",
17
+ "license": "ISC",
18
+ "bugs": {
19
+ "url": "https://github.com/nguyenduyhoang35/currency-fomatter/issues"
20
+ },
21
+ "homepage": "https://github.com/nguyenduyhoang35/currency-fomatter#readme",
22
+ "devDependencies": {
23
+ "@types/react": "^18.2.7",
24
+ "@typescript-eslint/eslint-plugin": "^5.59.7",
25
+ "@typescript-eslint/parser": "^5.59.7",
26
+ "eslint": "^8.41.0",
27
+ "eslint-plugin-react": "^7.32.2",
28
+ "eslint-plugin-react-hooks": "^4.6.0",
29
+ "react": "^18.2.0",
30
+ "react-dom": "^18.2.0",
31
+ "typescript": "^5.0.4"
32
+ },
33
+ "dependencies": {
34
+ "@types/node": "^20.2.3",
35
+ "tslib": "^2.5.2"
36
+ }
37
+ }
@@ -0,0 +1,854 @@
1
+ //@flow
2
+ import PropTypes from "prop-types";
3
+ import React, { Component } from "react";
4
+
5
+ import {
6
+ noop,
7
+ returnTrue,
8
+ charIsNumber,
9
+ escapeRegExp,
10
+ fixLeadingZero,
11
+ splitString,
12
+ limitToScale,
13
+ roundToPrecision,
14
+ omit,
15
+ setCaretPosition,
16
+ thousandGroupSpacing,
17
+ } from "./utils";
18
+
19
+ const propTypes = {
20
+ thousandSeparator: PropTypes.oneOfType([PropTypes.string, PropTypes.oneOf([true])]),
21
+ thousandSpacing: PropTypes.oneOf(["2", "2s", "3", "4"]),
22
+ decimalSeparator: PropTypes.string,
23
+ decimalScale: PropTypes.number,
24
+ fixedDecimalScale: PropTypes.bool,
25
+ displayType: PropTypes.oneOf(["input", "text"]),
26
+ prefix: PropTypes.string,
27
+ suffix: PropTypes.string,
28
+ format: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
29
+ removeFormatting: PropTypes.func,
30
+ mask: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]),
31
+ value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
32
+ isNumericString: PropTypes.bool,
33
+ customInput: PropTypes.func,
34
+ allowNegative: PropTypes.bool,
35
+ onValueChange: PropTypes.func,
36
+ onKeyDown: PropTypes.func,
37
+ onMouseUp: PropTypes.func,
38
+ onChange: PropTypes.func,
39
+ onFocus: PropTypes.func,
40
+ onBlur: PropTypes.func,
41
+ type: PropTypes.oneOf(["text", "tel"]),
42
+ isAllowed: PropTypes.func,
43
+ renderText: PropTypes.func,
44
+ };
45
+
46
+ const defaultProps = {
47
+ displayType: "input",
48
+ decimalSeparator: ".",
49
+ thousandSpacing: "3",
50
+ fixedDecimalScale: false,
51
+ prefix: "",
52
+ suffix: "",
53
+ allowNegative: true,
54
+ isNumericString: false,
55
+ type: "text",
56
+ onValueChange: noop,
57
+ onChange: noop,
58
+ onKeyDown: noop,
59
+ onMouseUp: noop,
60
+ onFocus: noop,
61
+ onBlur: noop,
62
+ isAllowed: returnTrue,
63
+ };
64
+
65
+ type MyProps = {
66
+ value?: any;
67
+ format: any;
68
+ decimalScale: number;
69
+ decimalSeparator: string;
70
+ thousandSpacing: "2" | "2s" | "3" | "4";
71
+ thousandSeparator: string | boolean;
72
+ mask: string | string[];
73
+ allowNegative: boolean;
74
+ prefix: string;
75
+ suffix: string;
76
+ removeFormatting: any;
77
+ fixedDecimalScale: boolean;
78
+ isNumericString: boolean;
79
+ isAllowed: any;
80
+ onValueChange: any;
81
+ onChange: any;
82
+ onKeyDown: any;
83
+ onMouseUp: any;
84
+ onFocus: any;
85
+ onBlur: any;
86
+ type: "text" | "tel";
87
+ displayType: "input" | "text";
88
+ customInput: any;
89
+ renderText: any;
90
+ };
91
+ type MyState = { value?: string; numAsString?: any };
92
+
93
+ class CurrencyFormat extends Component<MyProps, MyState> {
94
+ state: {
95
+ value?: string;
96
+ numAsString?: any;
97
+ };
98
+ static defaultProps: Object;
99
+ constructor(props: MyProps) {
100
+ super(props);
101
+
102
+ //validate props
103
+ this.validateProps();
104
+
105
+ const formattedValue = this.formatValueProp();
106
+
107
+ this.state = {
108
+ value: formattedValue,
109
+ numAsString: this.removeFormatting(formattedValue),
110
+ };
111
+
112
+ this.onChange = this.onChange.bind(this);
113
+ this.onKeyDown = this.onKeyDown.bind(this);
114
+ this.onMouseUp = this.onMouseUp.bind(this);
115
+ this.onFocus = this.onFocus.bind(this);
116
+ this.onBlur = this.onBlur.bind(this);
117
+ }
118
+
119
+ componentDidUpdate(prevProps: Object) {
120
+ this.updateValueIfRequired(prevProps);
121
+ }
122
+
123
+ updateValueIfRequired(prevProps: Object) {
124
+ const { props, state } = this;
125
+
126
+ if (prevProps !== props) {
127
+ //validate props
128
+ this.validateProps();
129
+
130
+ const stateValue = state.value;
131
+
132
+ const lastNumStr = state.numAsString || "";
133
+
134
+ const formattedValue = props.value === undefined ? this.formatNumString(lastNumStr) : this.formatValueProp();
135
+
136
+ if (formattedValue !== stateValue) {
137
+ this.setState({
138
+ value: formattedValue,
139
+ numAsString: this.removeFormatting(formattedValue),
140
+ });
141
+ }
142
+ }
143
+ }
144
+
145
+ /** Misc methods **/
146
+ getFloatString(num = "") {
147
+ const { decimalSeparator } = this.getSeparators();
148
+ const numRegex = this.getNumberRegex(true);
149
+
150
+ //remove negation for regex check
151
+ const hasNegation = num[0] === "-";
152
+ if (hasNegation) num = num.replace("-", "");
153
+
154
+ num = (num.match(numRegex) || []).join("").replace(decimalSeparator, ".");
155
+
156
+ //remove extra decimals
157
+ const firstDecimalIndex = num.indexOf(".");
158
+
159
+ if (firstDecimalIndex !== -1) {
160
+ num = `${num.substring(0, firstDecimalIndex)}.${num
161
+ .substring(firstDecimalIndex + 1, num.length)
162
+ .replace(new RegExp(escapeRegExp(decimalSeparator), "g"), "")}`;
163
+ }
164
+
165
+ //add negation back
166
+ if (hasNegation) num = "-" + num;
167
+
168
+ return num;
169
+ }
170
+
171
+ //returned regex assumes decimalSeparator is as per prop
172
+ getNumberRegex(g: boolean, ignoreDecimalSeparator?: boolean) {
173
+ const { format, decimalScale } = this.props;
174
+ const { decimalSeparator } = this.getSeparators();
175
+ return new RegExp(
176
+ "\\d" +
177
+ (decimalSeparator && decimalScale !== 0 && !ignoreDecimalSeparator && !format
178
+ ? "|" + escapeRegExp(decimalSeparator)
179
+ : ""),
180
+ g ? "g" : undefined,
181
+ );
182
+ }
183
+
184
+ getSeparators() {
185
+ const { decimalSeparator, thousandSpacing } = this.props;
186
+ let { thousandSeparator } = this.props;
187
+
188
+ if (thousandSeparator === true) {
189
+ thousandSeparator = ",";
190
+ }
191
+
192
+ return {
193
+ decimalSeparator,
194
+ thousandSeparator,
195
+ thousandSpacing,
196
+ };
197
+ }
198
+
199
+ getMaskAtIndex(index: number) {
200
+ const { mask = " " } = this.props;
201
+ if (typeof mask === "string") {
202
+ return mask;
203
+ }
204
+
205
+ return mask[index] || " ";
206
+ }
207
+
208
+ validateProps() {
209
+ const { mask } = this.props;
210
+
211
+ //validate decimalSeparator and thousandSeparator
212
+ const { decimalSeparator, thousandSeparator } = this.getSeparators();
213
+
214
+ if (decimalSeparator === thousandSeparator) {
215
+ throw new Error(`
216
+ Decimal separator can\'t be same as thousand separator.\n
217
+ thousandSeparator: ${thousandSeparator} (thousandSeparator = {true} is same as thousandSeparator = ",")
218
+ decimalSeparator: ${decimalSeparator} (default value for decimalSeparator is .)
219
+ `);
220
+ }
221
+
222
+ //validate mask
223
+ if (mask) {
224
+ const maskAsStr = mask === "string" ? mask : mask.toString();
225
+ if (maskAsStr.match(/\d/g)) {
226
+ throw new Error(`
227
+ Mask ${mask} should not contain numeric character;
228
+ `);
229
+ }
230
+ }
231
+ }
232
+
233
+ splitDecimal(numStr: string) {
234
+ const { allowNegative } = this.props;
235
+ const hasNagation = numStr[0] === "-";
236
+ const addNegation = hasNagation && allowNegative;
237
+ numStr = numStr.replace("-", "");
238
+
239
+ const parts = numStr.split(".");
240
+ const beforeDecimal = parts[0];
241
+ const afterDecimal = parts[1] || "";
242
+
243
+ return {
244
+ beforeDecimal,
245
+ afterDecimal,
246
+ hasNagation,
247
+ addNegation,
248
+ };
249
+ }
250
+
251
+ /** Misc methods end **/
252
+
253
+ /** caret specific methods **/
254
+ setPatchedCaretPosition(el: any, caretPos: number, currentValue: string) {
255
+ /* setting caret position within timeout of 0ms is required for mobile chrome,
256
+ otherwise browser resets the caret position after we set it
257
+ We are also setting it without timeout so that in normal browser we don't see the flickering */
258
+ setCaretPosition(el, caretPos);
259
+ setTimeout(() => {
260
+ if (el.value === currentValue) setCaretPosition(el, caretPos);
261
+ }, 0);
262
+ }
263
+
264
+ /* This keeps the caret within typing area so people can't type in between prefix or suffix */
265
+ correctCaretPosition(value: string, caretPos: number, direction?: string) {
266
+ const { prefix, suffix, format } = this.props;
267
+
268
+ //in case of format as number limit between prefix and suffix
269
+ if (!format) {
270
+ const hasNegation = value[0] === "-";
271
+ return Math.min(Math.max(caretPos, prefix.length + (hasNegation ? 1 : 0)), value.length - suffix.length);
272
+ }
273
+
274
+ //in case if custom format method don't do anything
275
+ if (typeof format === "function") return caretPos;
276
+
277
+ /* in case format is string find the closest # position from the caret position */
278
+
279
+ //in case the caretPos have input value on it don't do anything
280
+ if (format[caretPos] === "#" && charIsNumber(value[caretPos])) return caretPos;
281
+
282
+ //if caretPos is just after input value don't do anything
283
+ if (format[caretPos - 1] === "#" && charIsNumber(value[caretPos - 1])) return caretPos;
284
+
285
+ //find the nearest caret position
286
+ const firstHashPosition = format.indexOf("#");
287
+ const lastHashPosition = format.lastIndexOf("#");
288
+
289
+ //limit the cursor between the first # position and the last # position
290
+ caretPos = Math.min(Math.max(caretPos, firstHashPosition), lastHashPosition + 1);
291
+
292
+ const nextPos = format.substring(caretPos, format.length).indexOf("#");
293
+ let caretLeftBound = caretPos;
294
+ const caretRightBoud = caretPos + (nextPos === -1 ? 0 : nextPos);
295
+
296
+ //get the position where the last number is present
297
+ while (
298
+ caretLeftBound > firstHashPosition &&
299
+ (format[caretLeftBound] !== "#" || !charIsNumber(value[caretLeftBound]))
300
+ ) {
301
+ caretLeftBound -= 1;
302
+ }
303
+
304
+ const goToLeft =
305
+ !charIsNumber(value[caretRightBoud]) ||
306
+ (direction === "left" && caretPos !== firstHashPosition) ||
307
+ caretPos - caretLeftBound < caretRightBoud - caretPos;
308
+
309
+ return goToLeft ? caretLeftBound + 1 : caretRightBoud;
310
+ }
311
+
312
+ getCaretPosition(inputValue: string, formattedValue: string, caretPos: number) {
313
+ const { format } = this.props;
314
+ const stateValue = this.state.value;
315
+ const numRegex = this.getNumberRegex(true);
316
+ const inputNumber = (inputValue.match(numRegex) || []).join("");
317
+ const formattedNumber = (formattedValue.match(numRegex) || []).join("");
318
+ let j, i;
319
+
320
+ j = 0;
321
+
322
+ for (i = 0; i < caretPos; i++) {
323
+ const currentInputChar = inputValue[i] || "";
324
+ const currentFormatChar = formattedValue[j] || "";
325
+ //no need to increase new cursor position if formatted value does not have those characters
326
+ //case inputValue = 1a23 and formattedValue = 123
327
+ if (!currentInputChar.match(numRegex) && currentInputChar !== currentFormatChar) continue;
328
+
329
+ //When we are striping out leading zeros maintain the new cursor position
330
+ //Case inputValue = 00023 and formattedValue = 23;
331
+ if (
332
+ currentInputChar === "0" &&
333
+ currentFormatChar.match(numRegex) &&
334
+ currentFormatChar !== "0" &&
335
+ inputNumber.length !== formattedNumber.length
336
+ )
337
+ continue;
338
+
339
+ //we are not using currentFormatChar because j can change here
340
+ while (currentInputChar !== formattedValue[j] && j < formattedValue.length) j++;
341
+ j++;
342
+ }
343
+
344
+ if (typeof format === "string" && !stateValue) {
345
+ //set it to the maximum value so it goes after the last number
346
+ j = formattedValue.length;
347
+ }
348
+
349
+ //correct caret position if its outside of editable area
350
+ j = this.correctCaretPosition(formattedValue, j);
351
+
352
+ return j;
353
+ }
354
+ /** caret specific methods ends **/
355
+
356
+ /** methods to remove formattting **/
357
+ removePrefixAndSuffix(val: string) {
358
+ const { format, prefix, suffix } = this.props;
359
+
360
+ //remove prefix and suffix
361
+ if (!format && val) {
362
+ const isNegative = val[0] === "-";
363
+
364
+ //remove negation sign
365
+ if (isNegative) val = val.substring(1, val.length);
366
+
367
+ //remove prefix
368
+ val = prefix && val.indexOf(prefix) === 0 ? val.substring(prefix.length, val.length) : val;
369
+
370
+ //remove suffix
371
+ const suffixLastIndex = val.lastIndexOf(suffix);
372
+ val =
373
+ suffix && suffixLastIndex !== -1 && suffixLastIndex === val.length - suffix.length
374
+ ? val.substring(0, suffixLastIndex)
375
+ : val;
376
+
377
+ //add negation sign back
378
+ if (isNegative) val = "-" + val;
379
+ }
380
+
381
+ return val;
382
+ }
383
+
384
+ removePatternFormatting(val: string) {
385
+ const { format } = this.props;
386
+ const formatArray = format.split("#").filter((str: string) => str !== "");
387
+ let start = 0;
388
+ let numStr = "";
389
+
390
+ for (let i = 0, ln = formatArray.length; i <= ln; i++) {
391
+ const part = formatArray[i] || "";
392
+
393
+ //if i is the last fragment take the index of end of the value
394
+ //For case like +1 (911) 911 91 91 having pattern +1 (###) ### ## ##
395
+ const index = i === ln ? val.length : val.indexOf(part, start);
396
+
397
+ /* in any case if we don't find the pattern part in the value assume the val as numeric string
398
+ This will be also in case if user has started typing, in any other case it will not be -1
399
+ unless wrong prop value is provided */
400
+ if (index === -1) {
401
+ numStr = val;
402
+ break;
403
+ } else {
404
+ numStr += val.substring(start, index);
405
+ start = index + part.length;
406
+ }
407
+ }
408
+
409
+ return (numStr.match(/\d/g) || []).join("");
410
+ }
411
+
412
+ removeFormatting(val: string) {
413
+ const { format, removeFormatting } = this.props;
414
+ if (!val) return val;
415
+
416
+ if (!format) {
417
+ val = this.removePrefixAndSuffix(val);
418
+ val = this.getFloatString(val);
419
+ } else if (typeof format === "string") {
420
+ val = this.removePatternFormatting(val);
421
+ } else if (typeof removeFormatting === "function") {
422
+ //condition need to be handled if format method is provide,
423
+ val = removeFormatting(val);
424
+ } else {
425
+ val = (val.match(/\d/g) || []).join("");
426
+ }
427
+ return val;
428
+ }
429
+ /** methods to remove formattting end **/
430
+
431
+ /*** format specific methods start ***/
432
+ /**
433
+ * Format when # based string is provided
434
+ * @param {string} numStr Numeric String
435
+ * @return {string} formatted Value
436
+ */
437
+ formatWithPattern(numStr: string) {
438
+ const { format } = this.props;
439
+ let hashCount = 0;
440
+ const formattedNumberAry = format.split("");
441
+ for (let i = 0, ln = format.length; i < ln; i++) {
442
+ if (format[i] === "#") {
443
+ formattedNumberAry[i] = numStr[hashCount] || this.getMaskAtIndex(hashCount);
444
+ hashCount += 1;
445
+ }
446
+ }
447
+ return formattedNumberAry.join("");
448
+ }
449
+ /**
450
+ * Format the given string according to thousand separator and thousand spacing
451
+ * @param {*} beforeDecimal
452
+ * @param {*} thousandSeparator
453
+ * @param {*} thousandSpacing
454
+ */
455
+ formatThousand(beforeDecimal: any, thousandSeparator: string | boolean, thousandSpacing: "2" | "2s" | "3" | "4") {
456
+ let digitalGroup;
457
+ switch (thousandSpacing) {
458
+ case thousandGroupSpacing.two:
459
+ digitalGroup = /(\d)(?=(\d{2})+(?!\d))/g;
460
+ break;
461
+ case thousandGroupSpacing.twoScaled:
462
+ digitalGroup = /(\d)(?=(((\d{2})+)(\d{1})(?!\d)))/g;
463
+ break;
464
+ case thousandGroupSpacing.four:
465
+ digitalGroup = /(\d)(?=(\d{4})+(?!\d))/g;
466
+ break;
467
+ default:
468
+ digitalGroup = /(\d)(?=(\d{3})+(?!\d))/g;
469
+ }
470
+
471
+ return beforeDecimal.replace(digitalGroup, "$1" + thousandSeparator);
472
+ }
473
+ /**
474
+ * @param {string} numStr Numeric string/floatString] It always have decimalSeparator as .
475
+ * @return {string} formatted Value
476
+ */
477
+ formatAsNumber(numStr: string) {
478
+ const { decimalScale, fixedDecimalScale, prefix, suffix } = this.props;
479
+ const { thousandSeparator, decimalSeparator, thousandSpacing } = this.getSeparators();
480
+
481
+ const hasDecimalSeparator = numStr.indexOf(".") !== -1 || (decimalScale && fixedDecimalScale);
482
+ let { beforeDecimal, afterDecimal, addNegation } = this.splitDecimal(numStr); // eslint-disable-line prefer-const
483
+
484
+ //apply decimal precision if its defined
485
+ if (decimalScale !== undefined) afterDecimal = limitToScale(afterDecimal, decimalScale, fixedDecimalScale);
486
+
487
+ if (thousandSeparator) {
488
+ beforeDecimal = this.formatThousand(beforeDecimal, thousandSeparator, thousandSpacing);
489
+ }
490
+
491
+ //add prefix and suffix
492
+ if (prefix) beforeDecimal = prefix + beforeDecimal;
493
+ if (suffix) afterDecimal = afterDecimal + suffix;
494
+
495
+ //restore negation sign
496
+ if (addNegation) beforeDecimal = "-" + beforeDecimal;
497
+
498
+ numStr = beforeDecimal + ((hasDecimalSeparator && decimalSeparator) || "") + afterDecimal;
499
+
500
+ return numStr;
501
+ }
502
+
503
+ formatNumString(value = "") {
504
+ const { format } = this.props;
505
+ let formattedValue = value;
506
+
507
+ if (value === "") {
508
+ formattedValue = "";
509
+ } else if (value === "-" && !format) {
510
+ formattedValue = "-";
511
+ value = "";
512
+ } else if (typeof format === "string") {
513
+ formattedValue = this.formatWithPattern(formattedValue);
514
+ } else if (typeof format === "function") {
515
+ formattedValue = format(formattedValue);
516
+ } else {
517
+ formattedValue = this.formatAsNumber(formattedValue);
518
+ }
519
+
520
+ return formattedValue;
521
+ }
522
+
523
+ formatValueProp() {
524
+ const { format, decimalScale, fixedDecimalScale } = this.props;
525
+ let { value, isNumericString } = this.props;
526
+
527
+ // if value is not defined return empty string
528
+ if (value === undefined) return "";
529
+
530
+ if (typeof value === "number") {
531
+ value = value.toString();
532
+ isNumericString = true;
533
+ }
534
+
535
+ //round the number based on decimalScale
536
+ //format only if non formatted value is provided
537
+ if (isNumericString && !format && typeof decimalScale === "number") {
538
+ value = roundToPrecision(value, decimalScale, fixedDecimalScale);
539
+ }
540
+
541
+ const formattedValue = isNumericString ? this.formatNumString(value) : this.formatInput(value);
542
+
543
+ return formattedValue;
544
+ }
545
+
546
+ formatNegation(value = "") {
547
+ const { allowNegative } = this.props;
548
+ const negationRegex = new RegExp("(-)");
549
+ const doubleNegationRegex = new RegExp("(-)(.)*(-)");
550
+
551
+ // Check number has '-' value
552
+ const hasNegation = negationRegex.test(value);
553
+
554
+ // Check number has 2 or more '-' values
555
+ const removeNegation = doubleNegationRegex.test(value);
556
+
557
+ //remove negation
558
+ value = value.replace(/-/g, "");
559
+
560
+ if (hasNegation && !removeNegation && allowNegative) {
561
+ value = "-" + value;
562
+ }
563
+
564
+ return value;
565
+ }
566
+
567
+ formatInput(value = "") {
568
+ const { format } = this.props;
569
+
570
+ //format negation only if we are formatting as number
571
+ if (!format) {
572
+ value = this.formatNegation(value);
573
+ }
574
+
575
+ //remove formatting from number
576
+ value = this.removeFormatting(value);
577
+
578
+ return this.formatNumString(value);
579
+ }
580
+
581
+ /*** format specific methods end ***/
582
+ isCharacterAFormat(caretPos: number, value: string) {
583
+ const { format, prefix, suffix, decimalScale, fixedDecimalScale } = this.props;
584
+ const { decimalSeparator } = this.getSeparators();
585
+
586
+ //check within format pattern
587
+ if (typeof format === "string" && format[caretPos] !== "#") return true;
588
+
589
+ //check in number format
590
+ if (
591
+ !format &&
592
+ (caretPos < prefix.length ||
593
+ caretPos >= value.length - suffix.length ||
594
+ (decimalScale && fixedDecimalScale && value[caretPos] === decimalSeparator))
595
+ ) {
596
+ return true;
597
+ }
598
+
599
+ return false;
600
+ }
601
+
602
+ checkIfFormatGotDeleted(start: number, end: number, value: string) {
603
+ for (let i = start; i < end; i++) {
604
+ if (this.isCharacterAFormat(i, value)) return true;
605
+ }
606
+ return false;
607
+ }
608
+
609
+ /**
610
+ * This will check if any formatting got removed by the delete or backspace and reset the value
611
+ * It will also work as fallback if android chome keyDown handler does not work
612
+ **/
613
+ correctInputValue(caretPos: number, lastValue: string, value: string) {
614
+ const { format } = this.props;
615
+ const lastNumStr = this.state.numAsString || "";
616
+
617
+ //don't do anyhting if something got added, or if value is empty string (when whole input is cleared)
618
+ if (value.length >= lastValue.length || !value.length) {
619
+ return value;
620
+ }
621
+
622
+ const start = caretPos;
623
+ const lastValueParts = splitString(lastValue, caretPos);
624
+ const newValueParts = splitString(value, caretPos);
625
+ const deletedIndex = lastValueParts[1].lastIndexOf(newValueParts[1]);
626
+ const diff = deletedIndex !== -1 ? lastValueParts[1].substring(0, deletedIndex) : "";
627
+ const end = start + diff.length;
628
+
629
+ //if format got deleted reset the value to last value
630
+ if (this.checkIfFormatGotDeleted(start, end, lastValue)) {
631
+ value = lastValue;
632
+ }
633
+
634
+ //for numbers check if beforeDecimal got deleted and there is nothing after decimal,
635
+ //clear all numbers in such case while keeping the - sign
636
+ if (!format) {
637
+ const numericString = this.removeFormatting(value);
638
+ let { beforeDecimal, afterDecimal, addNegation } = this.splitDecimal(numericString); // eslint-disable-line prefer-const
639
+
640
+ //clear only if something got deleted
641
+ if (numericString.length < lastNumStr.length && beforeDecimal === "" && !parseFloat(afterDecimal)) {
642
+ return addNegation ? "-" : "";
643
+ }
644
+ }
645
+
646
+ return value;
647
+ }
648
+
649
+ onChange(e: React.FormEvent<HTMLInputElement>) {
650
+ e.persist();
651
+ const el: any = e.target;
652
+ let inputValue = el.value;
653
+ const { state, props } = this;
654
+ const { isAllowed } = props;
655
+ const lastValue = state.value || "";
656
+
657
+ /*Max of selectionStart and selectionEnd is taken for the patch of pixel and other mobile device caret bug*/
658
+ const currentCaretPosition = Math.max(el.selectionStart, el.selectionEnd);
659
+
660
+ inputValue = this.correctInputValue(currentCaretPosition, lastValue, inputValue);
661
+
662
+ let formattedValue = this.formatInput(inputValue) || "";
663
+ const numAsString = this.removeFormatting(formattedValue);
664
+
665
+ const valueObj = {
666
+ formattedValue,
667
+ value: numAsString,
668
+ floatValue: parseFloat(numAsString),
669
+ };
670
+
671
+ if (!isAllowed(valueObj)) {
672
+ formattedValue = lastValue;
673
+ }
674
+
675
+ //set the value imperatively, this is required for IE fix
676
+ el.value = formattedValue;
677
+
678
+ //get the caret position
679
+ const caretPos = this.getCaretPosition(inputValue, formattedValue, currentCaretPosition);
680
+
681
+ //set caret position
682
+ this.setPatchedCaretPosition(el, caretPos, formattedValue);
683
+
684
+ //change the state
685
+ if (formattedValue !== lastValue) {
686
+ this.setState({ value: formattedValue, numAsString }, () => {
687
+ props.onValueChange(valueObj);
688
+ props.onChange(e);
689
+ });
690
+ } else {
691
+ props.onChange(e);
692
+ }
693
+ }
694
+
695
+ onBlur(e: React.FormEvent<HTMLInputElement>) {
696
+ const { props, state } = this;
697
+ const { format, onBlur } = props;
698
+ let { numAsString } = state;
699
+ const lastValue = state.value;
700
+ if (!format) {
701
+ numAsString = fixLeadingZero(numAsString);
702
+ const formattedValue = this.formatNumString(numAsString);
703
+ const valueObj = {
704
+ formattedValue,
705
+ value: numAsString,
706
+ floatValue: parseFloat(numAsString),
707
+ };
708
+
709
+ //change the state
710
+ if (formattedValue !== lastValue) {
711
+ // the event needs to be persisted because its properties can be accessed in an asynchronous way
712
+ e.persist();
713
+ this.setState({ value: formattedValue, numAsString }, () => {
714
+ props.onValueChange(valueObj);
715
+ onBlur(e);
716
+ });
717
+ return;
718
+ }
719
+ }
720
+ onBlur(e);
721
+ }
722
+
723
+ onKeyDown(e: any) {
724
+ const el: any = e.target;
725
+ const { key } = e;
726
+ const { selectionEnd, value } = el;
727
+ const { selectionStart } = el;
728
+ let expectedCaretPosition;
729
+ const { decimalScale, fixedDecimalScale, prefix, suffix, format, onKeyDown } = this.props;
730
+ const ignoreDecimalSeparator = decimalScale !== undefined && fixedDecimalScale;
731
+ const numRegex = this.getNumberRegex(false, ignoreDecimalSeparator);
732
+ const negativeRegex = new RegExp("-");
733
+ const isPatternFormat = typeof format === "string";
734
+
735
+ //Handle backspace and delete against non numerical/decimal characters or arrow keys
736
+ if (key === "ArrowLeft" || key === "Backspace") {
737
+ expectedCaretPosition = selectionStart - 1;
738
+ } else if (key === "ArrowRight") {
739
+ expectedCaretPosition = selectionStart + 1;
740
+ } else if (key === "Delete") {
741
+ expectedCaretPosition = selectionStart;
742
+ }
743
+
744
+ //if expectedCaretPosition is not set it means we don't want to Handle keyDown
745
+ //also if multiple characters are selected don't handle
746
+ if (expectedCaretPosition === undefined || selectionStart !== selectionEnd) {
747
+ onKeyDown(e);
748
+ return;
749
+ }
750
+
751
+ let newCaretPosition = expectedCaretPosition;
752
+ const leftBound = isPatternFormat ? format.indexOf("#") : prefix.length;
753
+ const rightBound = isPatternFormat ? format.lastIndexOf("#") + 1 : value.length - suffix.length;
754
+
755
+ if (key === "ArrowLeft" || key === "ArrowRight") {
756
+ const direction = key === "ArrowLeft" ? "left" : "right";
757
+ newCaretPosition = this.correctCaretPosition(value, expectedCaretPosition, direction);
758
+ } else if (
759
+ key === "Delete" &&
760
+ !numRegex.test(value[expectedCaretPosition]) &&
761
+ !negativeRegex.test(value[expectedCaretPosition])
762
+ ) {
763
+ while (!numRegex.test(value[newCaretPosition]) && newCaretPosition < rightBound) newCaretPosition++;
764
+ } else if (
765
+ key === "Backspace" &&
766
+ !numRegex.test(value[expectedCaretPosition]) &&
767
+ !negativeRegex.test(value[expectedCaretPosition])
768
+ ) {
769
+ while (!numRegex.test(value[newCaretPosition - 1]) && newCaretPosition > leftBound) {
770
+ newCaretPosition--;
771
+ }
772
+ newCaretPosition = this.correctCaretPosition(value, newCaretPosition, "left");
773
+ }
774
+
775
+ if (
776
+ newCaretPosition !== expectedCaretPosition ||
777
+ expectedCaretPosition < leftBound ||
778
+ expectedCaretPosition > rightBound
779
+ ) {
780
+ e.preventDefault();
781
+ this.setPatchedCaretPosition(el, newCaretPosition, value);
782
+ }
783
+
784
+ /* NOTE: this is just required for unit test as we need to get the newCaretPosition,
785
+ Remove this when you find different solution */
786
+ if (e.isUnitTestRun) {
787
+ this.setPatchedCaretPosition(el, newCaretPosition, value);
788
+ }
789
+
790
+ this.props.onKeyDown(e);
791
+ }
792
+
793
+ /** required to handle the caret position when click anywhere within the input **/
794
+ onMouseUp(e: React.FormEvent<HTMLInputElement>) {
795
+ const el: any = e.target;
796
+ const { selectionStart, selectionEnd, value } = el;
797
+
798
+ if (selectionStart === selectionEnd) {
799
+ const caretPostion = this.correctCaretPosition(value, selectionStart);
800
+ if (caretPostion !== selectionStart) {
801
+ this.setPatchedCaretPosition(el, caretPostion, value);
802
+ }
803
+ }
804
+
805
+ this.props.onMouseUp(e);
806
+ }
807
+
808
+ onFocus(e: React.FormEvent<HTMLInputElement>) {
809
+ // Workaround Chrome and Safari bug https://bugs.chromium.org/p/chromium/issues/detail?id=779328
810
+ // (onFocus event target selectionStart is always 0 before setTimeout)
811
+ e.persist();
812
+ setTimeout(() => {
813
+ const el: any = e.target;
814
+ const { selectionStart, value } = el;
815
+
816
+ const caretPosition = this.correctCaretPosition(value, selectionStart);
817
+ if (caretPosition !== selectionStart) {
818
+ this.setPatchedCaretPosition(el, caretPosition, value);
819
+ }
820
+
821
+ this.props.onFocus(e);
822
+ });
823
+ }
824
+
825
+ render() {
826
+ const { type, displayType, customInput, renderText } = this.props;
827
+ const { value } = this.state;
828
+
829
+ const otherProps = omit(this.props, propTypes);
830
+
831
+ const inputProps = Object.assign({}, otherProps, {
832
+ type,
833
+ value,
834
+ onChange: this.onChange,
835
+ onKeyDown: this.onKeyDown,
836
+ onMouseUp: this.onMouseUp,
837
+ onFocus: this.onFocus,
838
+ onBlur: this.onBlur,
839
+ });
840
+
841
+ if (displayType === "text") {
842
+ return renderText ? renderText(value) || null : <span {...otherProps}>{value}</span>;
843
+ } else if (customInput) {
844
+ const CustomInput = customInput;
845
+ return <CustomInput {...inputProps} />;
846
+ }
847
+
848
+ return <input {...inputProps} />;
849
+ }
850
+ }
851
+
852
+ CurrencyFormat.defaultProps = defaultProps;
853
+
854
+ module.exports = CurrencyFormat;
@@ -0,0 +1,105 @@
1
+ /* eslint-disable no-useless-escape */
2
+ /* eslint-disable @typescript-eslint/no-empty-function */
3
+ /* eslint-disable no-self-assign */
4
+ //@flow
5
+
6
+ // basic noop function
7
+ export function noop() { }
8
+ export function returnTrue() { return true; }
9
+
10
+ export function charIsNumber(char?: string) {
11
+ return !!(char || '').match(/\d/);
12
+ }
13
+
14
+ export function escapeRegExp(str: string) {
15
+ return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
16
+ }
17
+
18
+ export function fixLeadingZero(numStr?: string) {
19
+ if (!numStr) return numStr;
20
+ const isNegative = numStr[0] === '-';
21
+ if (isNegative) numStr = numStr.substring(1, numStr.length);
22
+ const parts = numStr.split('.');
23
+ const beforeDecimal = parts[0].replace(/^0+/, '') || '0';
24
+ const afterDecimal = parts[1] || '';
25
+
26
+ return `${isNegative ? '-' : ''}${beforeDecimal}${afterDecimal ? `.${afterDecimal}` : ''}`;
27
+ }
28
+
29
+ export function splitString(str: string, index: number) {
30
+ return [str.substring(0, index), str.substring(index)]
31
+ }
32
+
33
+ /**
34
+ * limit decimal numbers to given scale
35
+ * Not used .fixedTo because that will break with big numbers
36
+ */
37
+ export function limitToScale(numStr: string, scale: number, fixedDecimalScale: boolean) {
38
+ let str = ''
39
+ const filler = fixedDecimalScale ? '0' : '';
40
+ for (let i = 0; i <= scale - 1; i++) {
41
+ str += numStr[i] || filler;
42
+ }
43
+ return str;
44
+ }
45
+
46
+ /**
47
+ * This method is required to round prop value to given scale.
48
+ * Not used .round or .fixedTo because that will break with big numbers
49
+ */
50
+ export function roundToPrecision(numStr: string, scale: number, fixedDecimalScale: boolean) {
51
+ const numberParts = numStr.split('.');
52
+ const roundedDecimalParts = parseFloat(`0.${numberParts[1] || '0'}`).toFixed(scale).split('.');
53
+ const intPart = numberParts[0].split('').reverse().reduce((roundedStr, current, idx) => {
54
+ if (roundedStr.length > idx) {
55
+ return (Number(roundedStr[0]) + Number(current)).toString() + roundedStr.substring(1, roundedStr.length);
56
+ }
57
+ return current + roundedStr;
58
+ }, roundedDecimalParts[0]);
59
+
60
+ const decimalPart = limitToScale(roundedDecimalParts[1] || '', (numberParts[1] || '').length, fixedDecimalScale);
61
+
62
+ return intPart + (decimalPart ? '.' + decimalPart : '');
63
+ }
64
+
65
+
66
+ export function omit(obj: any, keyMaps: any) {
67
+ const filteredObj: any = {};
68
+ Object.keys(obj).forEach((key) => {
69
+ if (!keyMaps[key]) filteredObj[key] = obj[key]
70
+ });
71
+ return filteredObj;
72
+ }
73
+
74
+ /** set the caret positon in an input field **/
75
+ export function setCaretPosition(el: any, caretPos: number) {
76
+ el.value = el.value;
77
+ // ^ this is used to not only get "focus", but
78
+ // to make sure we don't have it everything -selected-
79
+ // (it causes an issue in chrome, and having it doesn't hurt any other browser)
80
+ if (el !== null) {
81
+ if (el.createTextRange) {
82
+ const range = el.createTextRange();
83
+ range.move('character', caretPos);
84
+ range.select();
85
+ return true;
86
+ }
87
+ // (el.selectionStart === 0 added for Firefox bug)
88
+ if (el.selectionStart || el.selectionStart === 0) {
89
+ el.focus();
90
+ el.setSelectionRange(caretPos, caretPos);
91
+ return true;
92
+ }
93
+
94
+ // fail city, fortunately this never happens (as far as I've tested) :)
95
+ el.focus();
96
+ return false;
97
+ }
98
+ }
99
+
100
+ export const thousandGroupSpacing = {
101
+ two: '2',
102
+ twoScaled: '2s',
103
+ three: '3',
104
+ four: '4'
105
+ };
@@ -0,0 +1,26 @@
1
+ import React, { useState } from 'react'
2
+
3
+ type Props = {
4
+ value?: number
5
+ }
6
+ const MyCounter = ({ value = 0 }: Props) => {
7
+ const [counter, setCounter] = useState(value);
8
+
9
+ const onMinus = () => {
10
+ setCounter((prev) => prev - 1)
11
+ };
12
+
13
+ const onPlus = () => {
14
+ setCounter((prev) => prev + 1)
15
+ };
16
+
17
+ return (
18
+ <div>
19
+ <h1>Counter: {counter}</h1>
20
+ <button onClick={onMinus}>-</button>
21
+ <button onClick={onPlus}>+</button>
22
+ </div>
23
+ )
24
+ }
25
+
26
+ export default MyCounter
package/src/index.tsx ADDED
@@ -0,0 +1,3 @@
1
+ import MyCounter from './components/test'
2
+
3
+ export { MyCounter }
package/tsconfig.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "include": ["src"],
3
+ "exclude": [
4
+ "dist",
5
+ "node_modules"
6
+ ],
7
+ "compilerOptions": {
8
+ "module": "esnext",
9
+ "lib": ["dom", "esnext"],
10
+ "importHelpers": true,
11
+ "declaration": true,
12
+ "sourceMap": true,
13
+ "rootDir": "./src",
14
+ "outDir": "./dist/esm",
15
+ "strict": true,
16
+ "noImplicitReturns": false,
17
+ "noFallthroughCasesInSwitch": true,
18
+ "noUnusedLocals": true,
19
+ "noUnusedParameters": true,
20
+ "moduleResolution": "node",
21
+ "jsx": "react",
22
+ "esModuleInterop": true,
23
+ "skipLibCheck": true,
24
+ "forceConsistentCasingInFileNames": true,
25
+ }
26
+ }