@alfalab/core-components-number-input 1.3.0 → 2.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/Component.desktop.d.ts +1 -1
- package/Component.desktop.js +9 -1
- package/Component.mobile.d.ts +1 -1
- package/Component.mobile.js +9 -1
- package/Component.responsive.d.ts +1 -1
- package/Component.responsive.js +14 -1
- package/components/number-input/Component.d.ts +21 -46
- package/components/number-input/Component.js +87 -84
- package/components/number-input/index.css +30 -0
- package/components/number-input/index.js +8 -0
- package/components/steppers/Component.d.ts +14 -0
- package/components/steppers/Component.js +33 -0
- package/components/steppers/index.css +32 -0
- package/components/steppers/index.d.ts +1 -0
- package/components/steppers/index.js +15 -0
- package/cssm/Component.desktop.d.ts +1 -1
- package/cssm/Component.desktop.js +11 -1
- package/cssm/Component.mobile.d.ts +1 -1
- package/cssm/Component.mobile.js +11 -1
- package/cssm/Component.responsive.d.ts +1 -1
- package/cssm/Component.responsive.js +16 -1
- package/cssm/components/number-input/Component.d.ts +21 -46
- package/cssm/components/number-input/Component.js +87 -84
- package/cssm/components/number-input/index.js +10 -0
- package/cssm/components/number-input/index.module.css +29 -0
- package/cssm/components/steppers/Component.d.ts +14 -0
- package/cssm/components/steppers/Component.js +32 -0
- package/cssm/components/steppers/index.d.ts +1 -0
- package/cssm/components/steppers/index.js +16 -0
- package/cssm/components/steppers/index.module.css +31 -0
- package/cssm/desktop/index.js +10 -0
- package/cssm/index.js +11 -0
- package/cssm/mobile/index.js +10 -0
- package/cssm/utils.d.ts +19 -9
- package/cssm/utils.js +244 -21
- package/desktop/index.js +8 -0
- package/esm/Component.desktop.d.ts +1 -1
- package/esm/Component.desktop.js +9 -1
- package/esm/Component.mobile.d.ts +1 -1
- package/esm/Component.mobile.js +9 -1
- package/esm/Component.responsive.d.ts +1 -1
- package/esm/Component.responsive.js +15 -2
- package/esm/components/number-input/Component.d.ts +21 -46
- package/esm/components/number-input/Component.js +88 -86
- package/esm/components/number-input/index.css +30 -0
- package/esm/components/number-input/index.js +8 -0
- package/esm/components/steppers/Component.d.ts +14 -0
- package/esm/components/steppers/Component.js +24 -0
- package/esm/components/steppers/index.css +32 -0
- package/esm/components/steppers/index.d.ts +1 -0
- package/esm/components/steppers/index.js +7 -0
- package/esm/desktop/index.js +8 -0
- package/esm/index.js +9 -0
- package/esm/mobile/index.js +8 -0
- package/esm/utils.d.ts +19 -9
- package/esm/utils.js +236 -19
- package/index.js +9 -0
- package/mobile/index.js +8 -0
- package/modern/Component.desktop.d.ts +1 -1
- package/modern/Component.desktop.js +9 -1
- package/modern/Component.mobile.d.ts +1 -1
- package/modern/Component.mobile.js +9 -1
- package/modern/Component.responsive.d.ts +1 -1
- package/modern/Component.responsive.js +13 -1
- package/modern/components/number-input/Component.d.ts +21 -46
- package/modern/components/number-input/Component.js +84 -86
- package/modern/components/number-input/index.css +30 -0
- package/modern/components/number-input/index.js +8 -0
- package/modern/components/steppers/Component.d.ts +14 -0
- package/modern/components/steppers/Component.js +23 -0
- package/modern/components/steppers/index.css +32 -0
- package/modern/components/steppers/index.d.ts +1 -0
- package/modern/components/steppers/index.js +7 -0
- package/modern/desktop/index.js +8 -0
- package/modern/index.js +9 -0
- package/modern/mobile/index.js +8 -0
- package/modern/utils.d.ts +19 -9
- package/modern/utils.js +223 -18
- package/package.json +9 -2
- package/src/Component.desktop.tsx +2 -2
- package/src/Component.mobile.tsx +2 -2
- package/src/Component.responsive.tsx +16 -2
- package/src/components/number-input/Component.tsx +195 -129
- package/src/components/number-input/index.module.css +18 -0
- package/src/components/steppers/Component.tsx +64 -0
- package/src/components/steppers/index.module.css +21 -0
- package/src/components/steppers/index.ts +1 -0
- package/src/utils.ts +302 -24
- package/utils.d.ts +19 -9
- package/utils.js +244 -21
|
@@ -1,99 +1,97 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, { forwardRef, useMemo, useState, useEffect } from 'react';
|
|
2
2
|
import mergeRefs from 'react-merge-refs';
|
|
3
|
-
import {
|
|
3
|
+
import { maskitoTransform } from '@maskito/core';
|
|
4
|
+
import { useMaskito } from '@maskito/react';
|
|
5
|
+
import cn from 'classnames';
|
|
6
|
+
import { fnUtils, os } from '@alfalab/core-components-shared/modern';
|
|
7
|
+
import { createMaskOptions, parseNumber, stringifyNumberWithoutExp, MAX_DIGITS, MIN_SAFE_INTEGER, MAX_SAFE_INTEGER, MINUS_SIGN } from '../../utils.js';
|
|
8
|
+
import { Steppers } from '../steppers/Component.js';
|
|
9
|
+
import '@alfalab/core-components-icon-button/modern';
|
|
10
|
+
import '@alfalab/icons-glyph/MinusMIcon';
|
|
11
|
+
import '@alfalab/icons-glyph/PlusMediumMIcon';
|
|
4
12
|
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
const styles = {"steppers":"number-input__steppers_1g1qu","steppersFocused":"number-input__steppersFocused_1g1qu","steppersDisable":"number-input__steppersDisable_1g1qu","s":"number-input__s_1g1qu","m":"number-input__m_1g1qu"};
|
|
14
|
+
require('./index.css')
|
|
15
|
+
|
|
16
|
+
const NumberInput = forwardRef(({ value: propValue, onChange, separator = ',', fractionLength = MAX_DIGITS, defaultValue, Input, min: minProp, max: maxProp, rightAddons, dataTestId, disabled, onBlur, onFocus, view, step: stepProp, size = 's', disableUserInput, clear: clearProp, ...restProps }, ref) => {
|
|
17
|
+
const min = Math.max(MIN_SAFE_INTEGER, minProp ?? MIN_SAFE_INTEGER);
|
|
18
|
+
const max = Math.min(MAX_SAFE_INTEGER, maxProp ?? MAX_SAFE_INTEGER);
|
|
19
|
+
const withStepper = stepProp !== undefined;
|
|
20
|
+
const maskOptions = useMemo(() => createMaskOptions({
|
|
21
|
+
separator,
|
|
22
|
+
fractionLength: withStepper ? 0 : fractionLength,
|
|
23
|
+
min,
|
|
24
|
+
max,
|
|
25
|
+
}), [separator, fractionLength, min, max, withStepper]);
|
|
26
|
+
const [focused, setFocused] = useState(false);
|
|
27
|
+
const [value, setValue] = useState(() => {
|
|
28
|
+
if (defaultValue == null) {
|
|
29
|
+
return withStepper ? fnUtils.clamp(0, min, max).toString() : '';
|
|
14
30
|
}
|
|
15
|
-
return
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
});
|
|
30
|
-
};
|
|
31
|
-
const handleChange = (event) => {
|
|
32
|
-
const input = event.target;
|
|
33
|
-
const newValue = input.value.replace(createSeparatorsRegExp(), separator);
|
|
34
|
-
const allowedValue = getAllowedValue({
|
|
35
|
-
value: newValue,
|
|
36
|
-
fractionLength,
|
|
37
|
-
allowSigns,
|
|
38
|
-
separator,
|
|
39
|
-
});
|
|
40
|
-
if (onChange) {
|
|
41
|
-
onChange(event, {
|
|
42
|
-
value: getNumberValueFromStr(allowedValue),
|
|
43
|
-
valueString: allowedValue,
|
|
31
|
+
return fnUtils
|
|
32
|
+
.clamp(parseNumber(maskitoTransform(defaultValue.toString(), maskOptions)), min, max)
|
|
33
|
+
.toString();
|
|
34
|
+
});
|
|
35
|
+
const maskRef = useMaskito({ options: maskOptions });
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (propValue !== undefined) {
|
|
38
|
+
setValue((prev) => {
|
|
39
|
+
const parsedNumber = parseNumber(propValue);
|
|
40
|
+
if (parsedNumber !== parseNumber(prev)) {
|
|
41
|
+
return maskitoTransform(stringifyNumberWithoutExp(parsedNumber), maskOptions);
|
|
42
|
+
}
|
|
43
|
+
return prev;
|
|
44
44
|
});
|
|
45
45
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const handleKeyDown = (event) => {
|
|
52
|
-
const disallowedSymbols = /[/|?!@#$%^&*()_=A-Za-zА-Яа-яЁё ]/;
|
|
53
|
-
const oneKeyPress = !event.altKey && !event.metaKey && !event.ctrlKey;
|
|
54
|
-
const target = event.target;
|
|
55
|
-
// Запрещаем вводить неразрешенные символы за исключением комбинаций клавиш
|
|
56
|
-
if (oneKeyPress && event.key.length === 1 && disallowedSymbols.test(event.key)) {
|
|
57
|
-
return event.preventDefault();
|
|
58
|
-
}
|
|
59
|
-
const val = target.value;
|
|
60
|
-
const hasSeparator = (val.match(createSeparatorsRegExp()) || []).length > 0;
|
|
61
|
-
// Запрещаем вводить второй сепаратор
|
|
62
|
-
if (hasSeparator && SEPARATORS.some((s) => s === event.key)) {
|
|
63
|
-
return event.preventDefault();
|
|
64
|
-
}
|
|
65
|
-
// Запрещаем вводить лишний знак
|
|
66
|
-
if ((!allowSigns || SIGNS.some((s) => s === val[0])) &&
|
|
67
|
-
SIGNS.some((s) => s === event.key)) {
|
|
68
|
-
return event.preventDefault();
|
|
69
|
-
}
|
|
70
|
-
const selectionStart = target.selectionStart || 0;
|
|
71
|
-
// Запрещаем вводить цифры в дробную часть, если кол-во цифр больше fractionLength
|
|
72
|
-
if (hasSeparator &&
|
|
73
|
-
fractionLength &&
|
|
74
|
-
event.key.length === 1 &&
|
|
75
|
-
selectionStart > val.indexOf(separator) &&
|
|
76
|
-
val.split(separator)[1].length >= fractionLength) {
|
|
77
|
-
return event.preventDefault();
|
|
78
|
-
}
|
|
79
|
-
return undefined;
|
|
46
|
+
}, [maskOptions, propValue, separator]);
|
|
47
|
+
const getMaxLength = (valueString) => {
|
|
48
|
+
const hasSeparator = valueString?.includes(separator);
|
|
49
|
+
const hasSigns = valueString?.startsWith(MINUS_SIGN);
|
|
50
|
+
return MAX_DIGITS + (hasSeparator ? 1 : 0) + (hasSigns ? 1 : 0);
|
|
80
51
|
};
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
52
|
+
const getStep = () => Math.round(stepProp ?? 1);
|
|
53
|
+
const changeValue = (event, newValue) => {
|
|
54
|
+
const isNaNValue = Number.isNaN(newValue);
|
|
55
|
+
const valueString = event?.target.value ?? newValue?.toString() ?? '';
|
|
56
|
+
setValue(valueString);
|
|
57
|
+
if (valueString === '' || !isNaNValue) {
|
|
58
|
+
onChange?.(event, {
|
|
59
|
+
value: isNaNValue ? null : newValue,
|
|
87
60
|
});
|
|
88
61
|
}
|
|
89
|
-
|
|
90
|
-
|
|
62
|
+
};
|
|
63
|
+
const handleChange = (event) => {
|
|
64
|
+
const valueString = event.target.value;
|
|
65
|
+
changeValue(event, parseNumber(valueString));
|
|
66
|
+
};
|
|
67
|
+
const handleIncrement = () => {
|
|
68
|
+
const parsed = parseNumber(value);
|
|
69
|
+
const nextValue = maskitoTransform((Number.isNaN(parsed) ? min : parsed + getStep()).toString(), maskOptions);
|
|
70
|
+
changeValue(null, parseNumber(nextValue));
|
|
71
|
+
};
|
|
72
|
+
const handleDecrement = () => {
|
|
73
|
+
const parsed = parseNumber(value);
|
|
74
|
+
const nextValue = maskitoTransform((Number.isNaN(parsed) ? max : parsed - getStep()).toString(), maskOptions);
|
|
75
|
+
changeValue(null, parseNumber(nextValue));
|
|
76
|
+
};
|
|
77
|
+
const handleFocus = (e) => {
|
|
78
|
+
onFocus?.(e);
|
|
79
|
+
if (!disableUserInput) {
|
|
80
|
+
setFocused(true);
|
|
91
81
|
}
|
|
92
|
-
if (onBlur)
|
|
93
|
-
onBlur(event);
|
|
94
82
|
};
|
|
95
|
-
const
|
|
96
|
-
|
|
83
|
+
const handleBlur = (e) => {
|
|
84
|
+
onBlur?.(e);
|
|
85
|
+
setFocused(false);
|
|
86
|
+
};
|
|
87
|
+
return (React.createElement(Input, { maxLength: getMaxLength(value), ...restProps,
|
|
88
|
+
// В iOS в цифровой клавиатуре невозможно ввести минус.
|
|
89
|
+
inputMode: min < 0 && os.isIOS() ? 'text' : 'decimal', ref: mergeRefs([ref, maskRef]), value: value, onInput: handleChange, dataTestId: dataTestId, disabled: disabled, onFocus: handleFocus, onBlur: handleBlur, size: size, disableUserInput: disableUserInput, clear: clearProp && /\d/.test(value), rightAddons: withStepper ? (React.createElement(React.Fragment, null,
|
|
90
|
+
rightAddons,
|
|
91
|
+
React.createElement(Steppers, { dataTestId: dataTestId, disabled: disabled, value: parseNumber(value), min: min, max: max, className: cn(styles.steppers, styles[size], {
|
|
92
|
+
[styles.steppersFocused]: focused,
|
|
93
|
+
[styles.steppersDisable]: disabled,
|
|
94
|
+
}), onIncrement: handleIncrement, onDecrement: handleDecrement }))) : (rightAddons) }));
|
|
97
95
|
});
|
|
98
96
|
|
|
99
97
|
export { NumberInput };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/* hash: ppwj9 */
|
|
2
|
+
:root {
|
|
3
|
+
} /* deprecated */ :root {
|
|
4
|
+
--color-light-neutral-0: #fff;
|
|
5
|
+
--color-light-neutral-translucent-200: rgba(30, 43, 68, 0.08); /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */
|
|
6
|
+
} :root { /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */
|
|
7
|
+
} :root {
|
|
8
|
+
} :root {
|
|
9
|
+
|
|
10
|
+
/* Hard */
|
|
11
|
+
|
|
12
|
+
/* Up */
|
|
13
|
+
|
|
14
|
+
/* Hard up */
|
|
15
|
+
} :root {
|
|
16
|
+
} :root {
|
|
17
|
+
--gap-2xs-neg: -4px;
|
|
18
|
+
--gap-xs-neg: -8px;
|
|
19
|
+
} :root {
|
|
20
|
+
} :root {
|
|
21
|
+
} .number-input__steppers_1g1qu {
|
|
22
|
+
background-color: var(--color-light-neutral-0);
|
|
23
|
+
} .number-input__steppersFocused_1g1qu,
|
|
24
|
+
.number-input__steppersDisable_1g1qu {
|
|
25
|
+
background-color: var(--color-light-neutral-translucent-200);
|
|
26
|
+
} .number-input__s_1g1qu {
|
|
27
|
+
margin-right: var(--gap-xs-neg);
|
|
28
|
+
} .number-input__m_1g1qu {
|
|
29
|
+
margin-right: var(--gap-2xs-neg);
|
|
30
|
+
}
|
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
export { NumberInput } from './Component.js';
|
|
2
2
|
import 'react';
|
|
3
3
|
import 'react-merge-refs';
|
|
4
|
+
import '@maskito/core';
|
|
5
|
+
import '@maskito/react';
|
|
6
|
+
import 'classnames';
|
|
7
|
+
import '@alfalab/core-components-shared/modern';
|
|
4
8
|
import '../../utils.js';
|
|
9
|
+
import '../steppers/Component.js';
|
|
10
|
+
import '@alfalab/core-components-icon-button/modern';
|
|
11
|
+
import '@alfalab/icons-glyph/MinusMIcon';
|
|
12
|
+
import '@alfalab/icons-glyph/PlusMediumMIcon';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/// <reference types="react" />
|
|
2
|
+
import React from 'react';
|
|
3
|
+
type SteppersProps = {
|
|
4
|
+
value: number;
|
|
5
|
+
min: number;
|
|
6
|
+
max: number;
|
|
7
|
+
className?: string;
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
onIncrement: () => void;
|
|
10
|
+
onDecrement: () => void;
|
|
11
|
+
dataTestId?: string;
|
|
12
|
+
};
|
|
13
|
+
declare const Steppers: React.FC<SteppersProps>;
|
|
14
|
+
export { SteppersProps, Steppers };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import cn from 'classnames';
|
|
3
|
+
import { IconButton } from '@alfalab/core-components-icon-button/modern';
|
|
4
|
+
import { getDataTestId } from '@alfalab/core-components-shared/modern';
|
|
5
|
+
import { MinusMIcon } from '@alfalab/icons-glyph/MinusMIcon';
|
|
6
|
+
import { PlusMediumMIcon } from '@alfalab/icons-glyph/PlusMediumMIcon';
|
|
7
|
+
|
|
8
|
+
const styles = {"component":"number-input__component_1k8eq","separator":"number-input__separator_1k8eq","button":"number-input__button_1k8eq"};
|
|
9
|
+
require('./index.css')
|
|
10
|
+
|
|
11
|
+
function preventDefault(e) {
|
|
12
|
+
e.preventDefault();
|
|
13
|
+
}
|
|
14
|
+
const Steppers = ({ className, onIncrement, onDecrement, value, min, max, disabled, dataTestId, }) => {
|
|
15
|
+
const decButtonDisabled = disabled || value <= min;
|
|
16
|
+
const incButtonDisabled = disabled || value >= max;
|
|
17
|
+
return (React.createElement("div", { className: cn(styles.component, className) },
|
|
18
|
+
React.createElement(IconButton, { disabled: decButtonDisabled, className: styles.button, icon: React.createElement(MinusMIcon, null), "aria-label": '\u0443\u043C\u0435\u043D\u044C\u0448\u0438\u0442\u044C', onMouseDown: preventDefault, onClick: onDecrement, dataTestId: getDataTestId(dataTestId, 'decrement-button'), view: 'secondary' }),
|
|
19
|
+
React.createElement("div", { className: styles.separator }),
|
|
20
|
+
React.createElement(IconButton, { disabled: incButtonDisabled, className: styles.button, icon: React.createElement(PlusMediumMIcon, null), "aria-label": '\u0443\u0432\u0435\u043B\u0438\u0447\u0438\u0442\u044C', onMouseDown: preventDefault, onClick: onIncrement, dataTestId: getDataTestId(dataTestId, 'increment-button'), view: 'secondary' })));
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export { Steppers };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/* hash: 1x7ch */
|
|
2
|
+
:root {
|
|
3
|
+
} /* deprecated */ :root {
|
|
4
|
+
--color-light-neutral-translucent-300: rgba(15, 25, 55, 0.1); /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */
|
|
5
|
+
} :root { /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */
|
|
6
|
+
} :root {
|
|
7
|
+
} :root {
|
|
8
|
+
|
|
9
|
+
/* Hard */
|
|
10
|
+
|
|
11
|
+
/* Up */
|
|
12
|
+
|
|
13
|
+
/* Hard up */
|
|
14
|
+
} :root {
|
|
15
|
+
} :root {
|
|
16
|
+
} :root {
|
|
17
|
+
} :root {
|
|
18
|
+
} .number-input__component_1k8eq {
|
|
19
|
+
display: flex;
|
|
20
|
+
flex-flow: row nowrap;
|
|
21
|
+
align-items: center;
|
|
22
|
+
border-radius: 5px;
|
|
23
|
+
transition: background-color 0.2s ease;
|
|
24
|
+
overflow: visible;
|
|
25
|
+
} .number-input__separator_1k8eq {
|
|
26
|
+
height: 18px;
|
|
27
|
+
width: 1px;
|
|
28
|
+
background-color: var(--color-light-neutral-translucent-300);
|
|
29
|
+
} .number-input__button_1k8eq {
|
|
30
|
+
width: 40px;
|
|
31
|
+
height: 40px;
|
|
32
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./Component";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { Steppers } from './Component.js';
|
|
2
|
+
import 'react';
|
|
3
|
+
import 'classnames';
|
|
4
|
+
import '@alfalab/core-components-icon-button/modern';
|
|
5
|
+
import '@alfalab/core-components-shared/modern';
|
|
6
|
+
import '@alfalab/icons-glyph/MinusMIcon';
|
|
7
|
+
import '@alfalab/icons-glyph/PlusMediumMIcon';
|
package/modern/desktop/index.js
CHANGED
|
@@ -3,4 +3,12 @@ import 'react';
|
|
|
3
3
|
import '@alfalab/core-components-input/modern/desktop';
|
|
4
4
|
import '../components/number-input/Component.js';
|
|
5
5
|
import 'react-merge-refs';
|
|
6
|
+
import '@maskito/core';
|
|
7
|
+
import '@maskito/react';
|
|
8
|
+
import 'classnames';
|
|
9
|
+
import '@alfalab/core-components-shared/modern';
|
|
6
10
|
import '../utils.js';
|
|
11
|
+
import '../components/steppers/Component.js';
|
|
12
|
+
import '@alfalab/core-components-icon-button/modern';
|
|
13
|
+
import '@alfalab/icons-glyph/MinusMIcon';
|
|
14
|
+
import '@alfalab/icons-glyph/PlusMediumMIcon';
|
package/modern/index.js
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
export { NumberInputResponsive as NumberInput } from './Component.responsive.js';
|
|
2
2
|
import 'react';
|
|
3
3
|
import '@alfalab/core-components-input/modern';
|
|
4
|
+
import '@alfalab/core-components-mq/modern';
|
|
4
5
|
import './components/number-input/Component.js';
|
|
5
6
|
import 'react-merge-refs';
|
|
7
|
+
import '@maskito/core';
|
|
8
|
+
import '@maskito/react';
|
|
9
|
+
import 'classnames';
|
|
10
|
+
import '@alfalab/core-components-shared/modern';
|
|
6
11
|
import './utils.js';
|
|
12
|
+
import './components/steppers/Component.js';
|
|
13
|
+
import '@alfalab/core-components-icon-button/modern';
|
|
14
|
+
import '@alfalab/icons-glyph/MinusMIcon';
|
|
15
|
+
import '@alfalab/icons-glyph/PlusMediumMIcon';
|
package/modern/mobile/index.js
CHANGED
|
@@ -3,4 +3,12 @@ import 'react';
|
|
|
3
3
|
import '@alfalab/core-components-input/modern/mobile';
|
|
4
4
|
import '../components/number-input/Component.js';
|
|
5
5
|
import 'react-merge-refs';
|
|
6
|
+
import '@maskito/core';
|
|
7
|
+
import '@maskito/react';
|
|
8
|
+
import 'classnames';
|
|
9
|
+
import '@alfalab/core-components-shared/modern';
|
|
6
10
|
import '../utils.js';
|
|
11
|
+
import '../components/steppers/Component.js';
|
|
12
|
+
import '@alfalab/core-components-icon-button/modern';
|
|
13
|
+
import '@alfalab/icons-glyph/MinusMIcon';
|
|
14
|
+
import '@alfalab/icons-glyph/PlusMediumMIcon';
|
package/modern/utils.d.ts
CHANGED
|
@@ -1,13 +1,23 @@
|
|
|
1
|
-
|
|
1
|
+
import { MaskitoOptions, MaskitoPlugin } from '@maskito/core';
|
|
2
|
+
declare const MINUS_SIGN = "-";
|
|
2
3
|
declare const SEPARATORS: string[];
|
|
3
|
-
declare
|
|
4
|
+
declare const MAX_SAFE_INTEGER: number;
|
|
5
|
+
declare const MIN_SAFE_INTEGER: number;
|
|
6
|
+
declare const MAX_DIGITS = 15;
|
|
7
|
+
declare function parseNumber(value?: string | number | null): number;
|
|
4
8
|
/**
|
|
5
|
-
*
|
|
9
|
+
* Преобразовать число в строку с заменой экспоненты на десятичную дробь
|
|
6
10
|
*/
|
|
7
|
-
declare
|
|
8
|
-
|
|
9
|
-
fractionLength?: number | undefined;
|
|
11
|
+
declare function stringifyNumberWithoutExp(value: number): string;
|
|
12
|
+
declare function createMaskOptions({ separator, fractionLength, min, max, }: {
|
|
10
13
|
separator: string;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
+
fractionLength: number;
|
|
15
|
+
min: number;
|
|
16
|
+
max: number;
|
|
17
|
+
}): MaskitoOptions;
|
|
18
|
+
declare function createMinMaxPlugin({ min, max }: {
|
|
19
|
+
min: number;
|
|
20
|
+
max: number;
|
|
21
|
+
}): MaskitoPlugin;
|
|
22
|
+
declare function createNotEmptyPartsPlugin(separator: string): MaskitoPlugin;
|
|
23
|
+
export { MINUS_SIGN, SEPARATORS, MAX_SAFE_INTEGER, MIN_SAFE_INTEGER, MAX_DIGITS, parseNumber, stringifyNumberWithoutExp, createMaskOptions, createMinMaxPlugin, createNotEmptyPartsPlugin };
|
package/modern/utils.js
CHANGED
|
@@ -1,28 +1,233 @@
|
|
|
1
|
-
|
|
1
|
+
import { maskitoTransform } from '@maskito/core';
|
|
2
|
+
import { fnUtils } from '@alfalab/core-components-shared/modern';
|
|
3
|
+
|
|
4
|
+
/* eslint-disable no-param-reassign */
|
|
5
|
+
const MINUS_SIGN = '-';
|
|
2
6
|
const SEPARATORS = [',', '.'];
|
|
3
|
-
|
|
4
|
-
|
|
7
|
+
const MAX_SAFE_INTEGER = 2 ** 53 - 1;
|
|
8
|
+
const MIN_SAFE_INTEGER = -MAX_SAFE_INTEGER;
|
|
9
|
+
const MAX_DIGITS = 15; // с 16 уже упираемся в MAX_SAFE_INTEGER
|
|
10
|
+
function parseNumber(value = '') {
|
|
11
|
+
if (typeof value === 'number')
|
|
12
|
+
return value;
|
|
13
|
+
const pseudoSeparatorsRegExp = new RegExp(`[${SEPARATORS.join('')}]`, 'gi');
|
|
14
|
+
return value
|
|
15
|
+
? parseFloat(value
|
|
16
|
+
.replace(new RegExp(`[^${MINUS_SIGN}${SEPARATORS.join('')}0-9]`, 'gi'), '')
|
|
17
|
+
.replace(pseudoSeparatorsRegExp, '.'))
|
|
18
|
+
: NaN;
|
|
5
19
|
}
|
|
6
|
-
|
|
7
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Преобразовать число в строку с заменой экспоненты на десятичную дробь
|
|
22
|
+
*/
|
|
23
|
+
function stringifyNumberWithoutExp(value) {
|
|
24
|
+
const valueString = String(value);
|
|
25
|
+
const [numberPart, expPart] = valueString.split('e-');
|
|
26
|
+
let valueWithoutExp = valueString;
|
|
27
|
+
if (expPart) {
|
|
28
|
+
const [, fractionalPart] = numberPart.split('.');
|
|
29
|
+
const decimalDigits = Number(expPart) + (fractionalPart?.length || 0);
|
|
30
|
+
valueWithoutExp = value.toFixed(decimalDigits);
|
|
31
|
+
}
|
|
32
|
+
return valueWithoutExp;
|
|
33
|
+
}
|
|
34
|
+
const getNumberRegExp = (min, fractionLength) => {
|
|
35
|
+
let reStr = '[0-9]*';
|
|
36
|
+
if (min < 0) {
|
|
37
|
+
reStr = `(\\${MINUS_SIGN})?${reStr}`;
|
|
38
|
+
}
|
|
8
39
|
if (fractionLength !== 0) {
|
|
9
|
-
reStr = `${reStr}[${SEPARATORS.map((s) => `\\${s}`).join('')}]?[0-9]{0,${fractionLength ||
|
|
40
|
+
reStr = `${reStr}[${SEPARATORS.map((s) => `\\${s}`).join('')}]?[0-9]{0,${fractionLength || MAX_DIGITS}}`;
|
|
10
41
|
}
|
|
11
42
|
return new RegExp(`^${reStr}$`);
|
|
12
43
|
};
|
|
44
|
+
function createMaskOptions({ separator, fractionLength, min, max, }) {
|
|
45
|
+
return {
|
|
46
|
+
mask: getNumberRegExp(min, fractionLength),
|
|
47
|
+
preprocessors: [
|
|
48
|
+
createPseudoSeparatorPreprocessor(separator),
|
|
49
|
+
createNotEmptyIntegerPartPreprocessor({ separator, fractionLength }),
|
|
50
|
+
createZeroFractionLengthPreprocessor(fractionLength, separator),
|
|
51
|
+
createRepeatedSeparatorPreprocessor(separator),
|
|
52
|
+
],
|
|
53
|
+
postprocessors: [
|
|
54
|
+
createLeadingZeroesValidationPostprocessor(separator),
|
|
55
|
+
createMinMaxPostprocessor({ min, max, separator }),
|
|
56
|
+
],
|
|
57
|
+
plugins: [createNotEmptyPartsPlugin(separator), createMinMaxPlugin({ min, max })],
|
|
58
|
+
};
|
|
59
|
+
}
|
|
13
60
|
/**
|
|
14
|
-
*
|
|
61
|
+
* Заполняет целочисленную часть при вводе separator.
|
|
62
|
+
* @example Type , => 0,
|
|
15
63
|
*/
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
64
|
+
function createNotEmptyIntegerPartPreprocessor({ separator, fractionLength, }) {
|
|
65
|
+
const startWithDecimalSepRegExp = new RegExp(`^\\D*\\${separator}`);
|
|
66
|
+
return ({ elementState, data }) => {
|
|
67
|
+
const { value, selection } = elementState;
|
|
68
|
+
const [from] = selection;
|
|
69
|
+
if (fractionLength <= 0 ||
|
|
70
|
+
value.includes(separator) ||
|
|
71
|
+
!data.match(startWithDecimalSepRegExp)) {
|
|
72
|
+
return { elementState, data };
|
|
73
|
+
}
|
|
74
|
+
const digitsBeforeCursor = value.slice(0, from).match(/\d+/);
|
|
75
|
+
return {
|
|
76
|
+
elementState,
|
|
77
|
+
data: digitsBeforeCursor ? data : `0${data}`,
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Не позволяет вводить невалидный разделитель.
|
|
83
|
+
*/
|
|
84
|
+
function createPseudoSeparatorPreprocessor(separator) {
|
|
85
|
+
const pseudoSeparatorsRegExp = new RegExp(`[${SEPARATORS.join('')}]`, 'gi');
|
|
86
|
+
return ({ elementState, data }) => {
|
|
87
|
+
const { value, selection } = elementState;
|
|
88
|
+
return {
|
|
89
|
+
elementState: {
|
|
90
|
+
selection,
|
|
91
|
+
value: value.replace(pseudoSeparatorsRegExp, separator),
|
|
92
|
+
},
|
|
93
|
+
data: data.replace(pseudoSeparatorsRegExp, separator),
|
|
94
|
+
};
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Помогает верно обрезать значения при вставке, если fractionLength===0
|
|
99
|
+
* @example paste 123,123 -> 123
|
|
100
|
+
*/
|
|
101
|
+
function createZeroFractionLengthPreprocessor(fractionLength, separator) {
|
|
102
|
+
if (fractionLength > 0) {
|
|
103
|
+
return (state) => state;
|
|
21
104
|
}
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
105
|
+
const decimalPartRegExp = new RegExp(`\\${separator}.*$`, 'g');
|
|
106
|
+
return ({ elementState, data }) => {
|
|
107
|
+
const { value, selection } = elementState;
|
|
108
|
+
const [from, to] = selection;
|
|
109
|
+
const newValue = value.replace(decimalPartRegExp, '');
|
|
110
|
+
return {
|
|
111
|
+
elementState: {
|
|
112
|
+
selection: [Math.min(from, newValue.length), Math.min(to, newValue.length)],
|
|
113
|
+
value: newValue,
|
|
114
|
+
},
|
|
115
|
+
data: data.replace(decimalPartRegExp, ''),
|
|
116
|
+
};
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Запрещает вводить второй сепаратор
|
|
121
|
+
*/
|
|
122
|
+
function createRepeatedSeparatorPreprocessor(separator) {
|
|
123
|
+
return ({ elementState, data }) => {
|
|
124
|
+
const { value, selection } = elementState;
|
|
125
|
+
const [from, to] = selection;
|
|
126
|
+
return {
|
|
127
|
+
elementState,
|
|
128
|
+
data: !value.includes(separator) || value.slice(from, to + 1).includes(separator)
|
|
129
|
+
? data
|
|
130
|
+
: data.replace(new RegExp(`\\${separator}`), ''),
|
|
131
|
+
};
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Удаляет лишние нули в начале целой части.
|
|
136
|
+
* @example 0,|00005 => Backspace => |5
|
|
137
|
+
* @example -0,|00005 => Backspace => -|5
|
|
138
|
+
* @example "000000" => 0|
|
|
139
|
+
* @example 0| => Type "5" => 5|
|
|
140
|
+
*/
|
|
141
|
+
function createLeadingZeroesValidationPostprocessor(separator) {
|
|
142
|
+
const trimLeadingZeroes = (value) => value
|
|
143
|
+
.replace(new RegExp('^(\\D+)?0+(?=0)'), '$1')
|
|
144
|
+
.replace(new RegExp('^(\\D+)?0+(?=[1-9])'), '$1');
|
|
145
|
+
const countTrimmedZeroesBefore = (value, index) => {
|
|
146
|
+
const valueBefore = value.slice(0, index);
|
|
147
|
+
const followedByZero = value.slice(index).startsWith('0');
|
|
148
|
+
return (valueBefore.length - trimLeadingZeroes(valueBefore).length + (followedByZero ? 1 : 0));
|
|
149
|
+
};
|
|
150
|
+
return ({ value, selection }) => {
|
|
151
|
+
const [from, to] = selection;
|
|
152
|
+
const hasSeparator = value.includes(separator);
|
|
153
|
+
const [integerPart, decimalPart = ''] = value.split(separator);
|
|
154
|
+
const zeroTrimmedIntegerPart = trimLeadingZeroes(integerPart);
|
|
155
|
+
if (integerPart === zeroTrimmedIntegerPart) {
|
|
156
|
+
return { value, selection };
|
|
157
|
+
}
|
|
158
|
+
const newFrom = from - countTrimmedZeroesBefore(value, from);
|
|
159
|
+
const newTo = to - countTrimmedZeroesBefore(value, to);
|
|
160
|
+
return {
|
|
161
|
+
value: zeroTrimmedIntegerPart + (hasSeparator ? separator : '') + decimalPart,
|
|
162
|
+
selection: [Math.max(newFrom, 0), Math.max(newTo, 0)],
|
|
163
|
+
};
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Валидирует значение с учетом min max значений.
|
|
168
|
+
* Работает совместно с createMinMaxPlugin
|
|
169
|
+
*/
|
|
170
|
+
function createMinMaxPostprocessor({ min, max, separator, }) {
|
|
171
|
+
return ({ value, selection }) => {
|
|
172
|
+
const parsedNumber = parseNumber(value);
|
|
173
|
+
const limitedValue =
|
|
174
|
+
/**
|
|
175
|
+
* Здесь невозможно ограничить нижнюю границу, если пользователь вводит положительное число.
|
|
176
|
+
* То же самое для верхней границы и отрицательного числа.
|
|
177
|
+
* Если min=5, то без этого условия не получится ввести 40, похожая ситуация и с отрицательным max
|
|
178
|
+
*/
|
|
179
|
+
parsedNumber > 0 ? Math.min(parsedNumber, max) : Math.max(parsedNumber, min);
|
|
180
|
+
if (!Number.isNaN(parsedNumber) && limitedValue !== parsedNumber) {
|
|
181
|
+
const newValue = `${limitedValue}`.replace('.', separator);
|
|
182
|
+
return {
|
|
183
|
+
value: newValue,
|
|
184
|
+
selection: [newValue.length, newValue.length],
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
value,
|
|
189
|
+
selection,
|
|
190
|
+
};
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
function createMinMaxPlugin({ min, max }) {
|
|
194
|
+
return (element, options) => {
|
|
195
|
+
const listener = () => {
|
|
196
|
+
const parsedNumber = parseNumber(element.value);
|
|
197
|
+
const clampedNumber = fnUtils.clamp(parsedNumber, min, max);
|
|
198
|
+
if (!Number.isNaN(parsedNumber) && parsedNumber !== clampedNumber) {
|
|
199
|
+
element.value = maskitoTransform(stringifyNumberWithoutExp(clampedNumber), options);
|
|
200
|
+
element.dispatchEvent(new Event('input', { bubbles: true }));
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
const evListenerOptions = { capture: true };
|
|
204
|
+
element.addEventListener('blur', listener, evListenerOptions);
|
|
205
|
+
return () => element.removeEventListener('blur', listener, evListenerOptions);
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
function createNotEmptyPartsPlugin(separator) {
|
|
209
|
+
return (element) => {
|
|
210
|
+
const listener = () => {
|
|
211
|
+
const newValue = element.value
|
|
212
|
+
// 0,9000000 -> 0,9
|
|
213
|
+
.replace(new RegExp(`(\\${separator}\\d*?)(0+$)`), '$1')
|
|
214
|
+
// ,2 => 0,2
|
|
215
|
+
.replace(new RegExp(`^(\\D+)?\\${separator}`), `$10${separator}`)
|
|
216
|
+
// 0, -> 0
|
|
217
|
+
.replace(new RegExp(`\\${separator}$`), '')
|
|
218
|
+
// -0 -> 0
|
|
219
|
+
.replace(new RegExp(`^${MINUS_SIGN}0$`), '0')
|
|
220
|
+
// - -> ''
|
|
221
|
+
.replace(new RegExp(`^${MINUS_SIGN}$`), '');
|
|
222
|
+
if (newValue !== element.value) {
|
|
223
|
+
element.value = newValue;
|
|
224
|
+
element.dispatchEvent(new Event('input', { bubbles: true }));
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
const evListenerOptions = { capture: true };
|
|
228
|
+
element.addEventListener('blur', listener, evListenerOptions);
|
|
229
|
+
return () => element.removeEventListener('blur', listener, evListenerOptions);
|
|
230
|
+
};
|
|
231
|
+
}
|
|
27
232
|
|
|
28
|
-
export { SEPARATORS,
|
|
233
|
+
export { MAX_DIGITS, MAX_SAFE_INTEGER, MINUS_SIGN, MIN_SAFE_INTEGER, SEPARATORS, createMaskOptions, createMinMaxPlugin, createNotEmptyPartsPlugin, parseNumber, stringifyNumberWithoutExp };
|