@modul/mbui 0.0.1-beta-pv-50534-fc27fe54 → 0.0.1-beta-pv-50560-3e5e7e6f
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/package.json +3 -1
- package/src/Base/AmountFormat/AmountFormat.tsx +46 -0
- package/src/Base/AmountFormat/index.ts +3 -0
- package/src/Base/AmountInput/AmountInput.tsx +127 -0
- package/src/Base/AmountInput/hooks.ts +16 -0
- package/src/Base/AmountInput/index.ts +3 -0
- package/src/Base/AmountInput/types.ts +8 -0
- package/src/Base/AmountInput/utils.ts +26 -0
- package/src/Base/CurrencySymbol/CurrencySymbol.tsx +15 -0
- package/src/Base/CurrencySymbol/enums.ts +7 -0
- package/src/Base/CurrencySymbol/index.ts +3 -0
- package/src/Base/Input/Input.tsx +1 -2
- package/src/Base/Input/index.ts +1 -2
- package/src/Base/Input/types.ts +1 -1
- package/src/index.ts +4 -2
- package/src/Base/Input/InputClearable.tsx +0 -41
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@modul/mbui",
|
|
3
|
-
"version": "0.0.1-beta-pv-
|
|
3
|
+
"version": "0.0.1-beta-pv-50560-3e5e7e6f",
|
|
4
4
|
"packageManager": "yarn@3.5.1",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"scripts": {
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
"src"
|
|
14
14
|
],
|
|
15
15
|
"dependencies": {
|
|
16
|
+
"accounting": "0.4.1",
|
|
17
|
+
"modul-helpers": "0.1.108",
|
|
16
18
|
"react-datepicker": "4.16.0",
|
|
17
19
|
"react-imask": "7.1.3"
|
|
18
20
|
},
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { validateHelper } from 'modul-helpers'
|
|
3
|
+
import accounting from 'accounting'
|
|
4
|
+
import { CurrencySymbol } from '../CurrencySymbol'
|
|
5
|
+
|
|
6
|
+
interface AmountFormatProps {
|
|
7
|
+
value: string | number
|
|
8
|
+
currency?: string
|
|
9
|
+
precision?: number
|
|
10
|
+
def?: string
|
|
11
|
+
className?: string
|
|
12
|
+
whiteSpace?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
//FIXME: def
|
|
16
|
+
const AmountFormat: React.FC<AmountFormatProps> = ({
|
|
17
|
+
value,
|
|
18
|
+
currency = 'RUR',
|
|
19
|
+
precision = 2,
|
|
20
|
+
def = '',
|
|
21
|
+
className = '',
|
|
22
|
+
whiteSpace = ' ',
|
|
23
|
+
}) => {
|
|
24
|
+
const clearValue = (value: string) => (value.replace ? value.replace(/[^0-9.,]+/g, '').replace(',', '.') : value)
|
|
25
|
+
|
|
26
|
+
const formattedValue = React.useMemo(() => {
|
|
27
|
+
const numericValue = parseFloat(clearValue(String(value)))
|
|
28
|
+
return !isNaN(numericValue) ? accounting.formatNumber(numericValue, precision, whiteSpace) : def
|
|
29
|
+
}, [value, precision, whiteSpace, def])
|
|
30
|
+
|
|
31
|
+
//FIXME: возможно просто вернуть null
|
|
32
|
+
if (validateHelper.isEmpty(value)) {
|
|
33
|
+
return null
|
|
34
|
+
// return <span className={className}>{def}</span>
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
//FIXME: dangerouslySetInnerHTML ???
|
|
38
|
+
return (
|
|
39
|
+
<span className={className}>
|
|
40
|
+
{formattedValue}
|
|
41
|
+
{currency && <CurrencySymbol value={currency} />}
|
|
42
|
+
</span>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export default AmountFormat
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import React, { useState, useEffect, forwardRef, ForwardedRef, ChangeEvent, KeyboardEvent, useCallback } from 'react'
|
|
2
|
+
import accounting from 'accounting'
|
|
3
|
+
import { numberHelper } from 'modul-helpers'
|
|
4
|
+
import { calculateStart, cleanValue, parseFloatOrNull } from './utils'
|
|
5
|
+
import { useForwardedRef } from './hooks'
|
|
6
|
+
import { CombinedChangeEventHandler } from './types'
|
|
7
|
+
|
|
8
|
+
const { trimValidLength } = numberHelper
|
|
9
|
+
|
|
10
|
+
accounting.settings = {
|
|
11
|
+
number: {
|
|
12
|
+
decimal: ',',
|
|
13
|
+
},
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface IAmountInput extends React.InputHTMLAttributes<HTMLInputElement> {
|
|
17
|
+
value?: string | number
|
|
18
|
+
precision?: number
|
|
19
|
+
onChange?: CombinedChangeEventHandler
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const AmountInput = forwardRef<HTMLInputElement, IAmountInput>(
|
|
23
|
+
(
|
|
24
|
+
{ onChange, precision = 2, value, type = 'text', ...props }: IAmountInput,
|
|
25
|
+
ref: ForwardedRef<HTMLInputElement>
|
|
26
|
+
) => {
|
|
27
|
+
const innerRef = useForwardedRef(ref)
|
|
28
|
+
//Состояния для отслеживания текущего значения и позиции курсора
|
|
29
|
+
const [viewValue, setViewValue] = useState<string>('')
|
|
30
|
+
const [startPos, setStartPos] = useState<number | null>(null)
|
|
31
|
+
|
|
32
|
+
//Функция для обработки и форматирования входного значения
|
|
33
|
+
const parseValue = useCallback(
|
|
34
|
+
(val: string | number | undefined): { viewValue: string; startPos: number | null } => {
|
|
35
|
+
const el = innerRef.current
|
|
36
|
+
const cleanedValue = cleanValue(val === undefined || val === null ? '' : val.toString(), false)
|
|
37
|
+
const clean = trimValidLength(cleanedValue, '.', precision)
|
|
38
|
+
const parsedValue = parseFloatOrNull(clean)
|
|
39
|
+
const formattedValue = parsedValue ? accounting.formatNumber(parsedValue, precision, ' ') : ''
|
|
40
|
+
|
|
41
|
+
// Инициализация стартовой позиции для курсора
|
|
42
|
+
let parsedStartPos = el?.selectionStart || null
|
|
43
|
+
|
|
44
|
+
// Вычисление новой позиции курсора при изменении значения
|
|
45
|
+
if (parsedValue && el && formattedValue.length !== clean.length) {
|
|
46
|
+
parsedStartPos = calculateStart(el.selectionStart, clean, formattedValue)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Формирование отформатированного значения для отображения
|
|
50
|
+
let parsedViewValue = formattedValue
|
|
51
|
+
|
|
52
|
+
// Обработка случая, когда значение равно нулю или пусто
|
|
53
|
+
if (!parsedValue) {
|
|
54
|
+
if (!/^0[,.]?0*/.test(clean)) {
|
|
55
|
+
parsedViewValue = ''
|
|
56
|
+
} else {
|
|
57
|
+
parsedViewValue = clean.replace('.', ',')
|
|
58
|
+
if (parsedViewValue === '0' || parsedViewValue === '0,' || parsedViewValue === '0,0') {
|
|
59
|
+
parsedViewValue = `0,${[...Array(precision)].map(() => '0').join('')}`
|
|
60
|
+
parsedStartPos = calculateStart(el.selectionStart, clean, parsedViewValue)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Возвращение отформатированного значения и позиции курсора
|
|
66
|
+
return { viewValue: parsedViewValue, startPos: parsedStartPos }
|
|
67
|
+
},
|
|
68
|
+
[innerRef, precision]
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
//для обновления значений при изменении входного значения
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
const { viewValue: parsedViewValue, startPos: parsedStartPos } = parseValue(value)
|
|
74
|
+
setViewValue(parsedViewValue)
|
|
75
|
+
setStartPos(parsedStartPos)
|
|
76
|
+
}, [value, parseValue])
|
|
77
|
+
|
|
78
|
+
// для установки позиции курсора при изменении startPos
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
if (startPos !== null && innerRef.current) {
|
|
81
|
+
innerRef.current.setSelectionRange(startPos, startPos)
|
|
82
|
+
}
|
|
83
|
+
}, [startPos, innerRef])
|
|
84
|
+
|
|
85
|
+
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
|
|
86
|
+
const el = innerRef.current
|
|
87
|
+
const isDeletingKey = e.key === 'Backspace'
|
|
88
|
+
const inputValue = el?.value || ''
|
|
89
|
+
const prevCharValue = inputValue[el.selectionStart - 1]
|
|
90
|
+
|
|
91
|
+
// Проверка на удаление пробела или десятичного разделителя
|
|
92
|
+
const isDeletingWhiteSpace = isDeletingKey && el.selectionStart >= 2 && /\s/g.test(prevCharValue)
|
|
93
|
+
const isDeletingDecimalSeparator = isDeletingKey && prevCharValue === ','
|
|
94
|
+
|
|
95
|
+
// Вычисление новой позиции курсора после удаления
|
|
96
|
+
const newStartPos = isDeletingDecimalSeparator || isDeletingWhiteSpace ? el.selectionStart - 1 : null
|
|
97
|
+
|
|
98
|
+
// Установка новой позиции курсора и вызов onChange
|
|
99
|
+
if (newStartPos !== startPos) {
|
|
100
|
+
setStartPos(newStartPos)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
onChange?.(viewValue, e)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
|
|
107
|
+
const inputValue = e.target.value
|
|
108
|
+
const { viewValue: parsedViewValue, startPos: parsedStartPos } = parseValue(inputValue)
|
|
109
|
+
setViewValue(parsedViewValue)
|
|
110
|
+
setStartPos(parsedStartPos)
|
|
111
|
+
onChange?.(parsedViewValue, e)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<input
|
|
116
|
+
type={type}
|
|
117
|
+
ref={innerRef}
|
|
118
|
+
value={viewValue}
|
|
119
|
+
onKeyDown={handleKeyDown}
|
|
120
|
+
onChange={handleChange}
|
|
121
|
+
{...props}
|
|
122
|
+
/>
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
export default AmountInput
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import React, { useEffect } from 'react'
|
|
2
|
+
|
|
3
|
+
export function useForwardedRef<T>(ref: React.ForwardedRef<T>) {
|
|
4
|
+
const innerRef = React.useRef<T>(null)
|
|
5
|
+
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
if (!ref) return
|
|
8
|
+
if (typeof ref === 'function') {
|
|
9
|
+
ref(innerRef.current)
|
|
10
|
+
} else {
|
|
11
|
+
ref.current = innerRef.current
|
|
12
|
+
}
|
|
13
|
+
}, [ref])
|
|
14
|
+
|
|
15
|
+
return innerRef
|
|
16
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { ChangeEvent, KeyboardEvent, ChangeEventHandler } from 'react'
|
|
2
|
+
|
|
3
|
+
type CustomChangeEventHandler = (
|
|
4
|
+
value: string,
|
|
5
|
+
e?: ChangeEvent<HTMLInputElement> | KeyboardEvent<HTMLInputElement>
|
|
6
|
+
) => void
|
|
7
|
+
|
|
8
|
+
export type CombinedChangeEventHandler = ChangeEventHandler<HTMLInputElement> & CustomChangeEventHandler
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export function cleanValue(val, ignoreSpace = true) {
|
|
2
|
+
let res = ignoreSpace ? val.replace(/[^0-9.,]+/g, '') : val.replace(/[^0-9., ]+/g, '')
|
|
3
|
+
res = res.replace(/,/g, '.').replace('-', '')
|
|
4
|
+
const dotPos = res.indexOf('.')
|
|
5
|
+
if (dotPos > -1) {
|
|
6
|
+
const output = res.split('.')
|
|
7
|
+
res = output.shift() + '.' + output.join('')
|
|
8
|
+
}
|
|
9
|
+
return res
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function calculateStart(start, originValue, formattedValue) {
|
|
13
|
+
const substr = originValue.substring(0, start).replace(/ /g, '')
|
|
14
|
+
let regex = '^'
|
|
15
|
+
|
|
16
|
+
for (let i = 0, len = substr.length; i < len; i++) {
|
|
17
|
+
regex += substr[i] + ' {0,1}'
|
|
18
|
+
}
|
|
19
|
+
const match = new RegExp(regex).exec(formattedValue)
|
|
20
|
+
return (match && match[0].length) || 0
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function parseFloatOrNull(val) {
|
|
24
|
+
const result = parseFloat(cleanValue(val))
|
|
25
|
+
return isNaN(result) ? null : result
|
|
26
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { currencySymbols } from './enums'
|
|
3
|
+
|
|
4
|
+
interface CurrencySymbolProps {
|
|
5
|
+
value: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const CurrencySymbol: React.FC<CurrencySymbolProps> = ({ value }) => {
|
|
9
|
+
const symbol = currencySymbols[value] || value
|
|
10
|
+
const currencyClass = currencySymbols[value] ? 'currency-iso' : ''
|
|
11
|
+
|
|
12
|
+
return <span className={currencyClass}>{symbol}</span>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default CurrencySymbol
|
package/src/Base/Input/Input.tsx
CHANGED
|
@@ -3,7 +3,7 @@ import { IMaskInput } from 'react-imask'
|
|
|
3
3
|
import { IInputProps } from './types'
|
|
4
4
|
|
|
5
5
|
const Input = React.forwardRef<HTMLInputElement, IInputProps>(
|
|
6
|
-
({ onChange,
|
|
6
|
+
({ onChange, ...props }: IInputProps, ref: ForwardedRef<HTMLInputElement>) => {
|
|
7
7
|
const handleChange = (event: ChangeEvent<HTMLInputElement>): void => {
|
|
8
8
|
const val = event.target.value
|
|
9
9
|
onChange?.(val, event)
|
|
@@ -11,7 +11,6 @@ const Input = React.forwardRef<HTMLInputElement, IInputProps>(
|
|
|
11
11
|
|
|
12
12
|
return (
|
|
13
13
|
<IMaskInput
|
|
14
|
-
type={type}
|
|
15
14
|
ref={ref}
|
|
16
15
|
{...props}
|
|
17
16
|
onChange={handleChange}
|
package/src/Base/Input/index.ts
CHANGED
package/src/Base/Input/types.ts
CHANGED
|
@@ -6,5 +6,5 @@ type CombinedChangeEventHandler = ChangeEventHandler<HTMLInputElement> & CustomC
|
|
|
6
6
|
|
|
7
7
|
export interface IInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
|
8
8
|
onChange?: CombinedChangeEventHandler
|
|
9
|
-
mask?: string | RegExp | (string | RegExp)[]
|
|
9
|
+
mask?: string | RegExp | (string | RegExp)[]
|
|
10
10
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Button } from './Base/Buttons'
|
|
2
2
|
import { TextLink } from './Base/Links'
|
|
3
3
|
import { DatePicker } from './DatePicker'
|
|
4
|
-
import { Input
|
|
4
|
+
import { Input } from './Base/Input'
|
|
5
|
+
import { AmountFormat } from './Base/AmountFormat'
|
|
6
|
+
import { AmountInput } from './Base/AmountInput'
|
|
5
7
|
|
|
6
|
-
export { Button, TextLink, DatePicker, Input,
|
|
8
|
+
export { Button, TextLink, DatePicker, Input, AmountFormat, AmountInput }
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import React, { ChangeEvent, ForwardedRef, useState } from 'react'
|
|
2
|
-
import { IMaskInput } from 'react-imask'
|
|
3
|
-
import { IInputProps } from './types'
|
|
4
|
-
|
|
5
|
-
const InputClearable = React.forwardRef<HTMLInputElement, IInputProps>(
|
|
6
|
-
({ onChange, value = '', type = 'text', ...props }: IInputProps, ref: ForwardedRef<HTMLInputElement>) => {
|
|
7
|
-
const [inputValue, setInputValue] = useState(value)
|
|
8
|
-
|
|
9
|
-
const handleChange = (event: ChangeEvent<HTMLInputElement>): void => {
|
|
10
|
-
const val = event.target.value
|
|
11
|
-
setInputValue(val)
|
|
12
|
-
onChange?.(val, event)
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const handleClearClick = (): void => {
|
|
16
|
-
setInputValue('')
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
//TODO: fix style
|
|
20
|
-
return (
|
|
21
|
-
<div className="input_wrap_clear">
|
|
22
|
-
<IMaskInput
|
|
23
|
-
type={type}
|
|
24
|
-
ref={ref}
|
|
25
|
-
{...props}
|
|
26
|
-
value={inputValue}
|
|
27
|
-
onChange={handleChange}
|
|
28
|
-
/>
|
|
29
|
-
{inputValue && (
|
|
30
|
-
<a
|
|
31
|
-
className="input_clear icon-cancel"
|
|
32
|
-
onClick={handleClearClick}
|
|
33
|
-
style={{ visibility: 'initial' }}
|
|
34
|
-
/>
|
|
35
|
-
)}
|
|
36
|
-
</div>
|
|
37
|
-
)
|
|
38
|
-
}
|
|
39
|
-
)
|
|
40
|
-
|
|
41
|
-
export default InputClearable
|