@modul/mbui 0.0.1-beta-pv-50534-fc27fe54 → 0.0.1-beta-pv-50560-03649539

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@modul/mbui",
3
- "version": "0.0.1-beta-pv-50534-fc27fe54",
3
+ "version": "0.0.1-beta-pv-50560-03649539",
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 = '&nbsp;',
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,3 @@
1
+ import AmountFormat from './AmountFormat'
2
+
3
+ export { AmountFormat }
@@ -0,0 +1,123 @@
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
+ ({ onChange, precision = 4, value, ...props }: IAmountInput, ref: ForwardedRef<HTMLInputElement>) => {
24
+ const innerRef = useForwardedRef(ref)
25
+ //Состояния для отслеживания текущего значения и позиции курсора
26
+ const [viewValue, setViewValue] = useState<string>('')
27
+ const [startPos, setStartPos] = useState<number | null>(null)
28
+
29
+ //Функция для обработки и форматирования входного значения
30
+ const parseValue = useCallback(
31
+ (val: string | number | undefined): { viewValue: string; startPos: number | null } => {
32
+ const el = innerRef.current
33
+ const cleanedValue = cleanValue(val === undefined || val === null ? '' : val.toString(), false)
34
+ const clean = trimValidLength(cleanedValue, '.', precision)
35
+ const parsedValue = parseFloatOrNull(clean)
36
+ const formattedValue = parsedValue ? accounting.formatNumber(parsedValue, precision, ' ') : ''
37
+
38
+ // Инициализация стартовой позиции для курсора
39
+ let parsedStartPos = el?.selectionStart || null
40
+
41
+ // Вычисление новой позиции курсора при изменении значения
42
+ if (parsedValue && el && formattedValue.length !== clean.length) {
43
+ parsedStartPos = calculateStart(el.selectionStart, clean, formattedValue)
44
+ }
45
+
46
+ // Формирование отформатированного значения для отображения
47
+ let parsedViewValue = formattedValue
48
+
49
+ // Обработка случая, когда значение равно нулю или пусто
50
+ if (!parsedValue) {
51
+ if (!/^0[,.]?0*/.test(clean)) {
52
+ parsedViewValue = ''
53
+ } else {
54
+ parsedViewValue = clean.replace('.', ',')
55
+ if (parsedViewValue === '0' || parsedViewValue === '0,' || parsedViewValue === '0,0') {
56
+ parsedViewValue = `0,${[...Array(precision)].map(() => '0').join('')}`
57
+ parsedStartPos = calculateStart(el.selectionStart, clean, parsedViewValue)
58
+ }
59
+ }
60
+ }
61
+
62
+ // Возвращение отформатированного значения и позиции курсора
63
+ return { viewValue: parsedViewValue, startPos: parsedStartPos }
64
+ },
65
+ [innerRef, precision]
66
+ )
67
+
68
+ //для обновления значений при изменении входного значения
69
+ useEffect(() => {
70
+ const { viewValue: parsedViewValue, startPos: parsedStartPos } = parseValue(value)
71
+ setViewValue(parsedViewValue)
72
+ setStartPos(parsedStartPos)
73
+ }, [value, parseValue])
74
+
75
+ // для установки позиции курсора при изменении startPos
76
+ useEffect(() => {
77
+ if (startPos !== null && innerRef.current) {
78
+ innerRef.current.setSelectionRange(startPos, startPos)
79
+ }
80
+ }, [startPos, innerRef])
81
+
82
+ const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
83
+ const el = innerRef.current
84
+ const isDeletingKey = e.key === 'Backspace'
85
+ const inputValue = el?.value || ''
86
+ const prevCharValue = inputValue[el.selectionStart - 1]
87
+
88
+ // Проверка на удаление пробела или десятичного разделителя
89
+ const isDeletingWhiteSpace = isDeletingKey && el.selectionStart >= 2 && /\s/g.test(prevCharValue)
90
+ const isDeletingDecimalSeparator = isDeletingKey && prevCharValue === ','
91
+
92
+ // Вычисление новой позиции курсора после удаления
93
+ const newStartPos = isDeletingDecimalSeparator || isDeletingWhiteSpace ? el.selectionStart - 1 : null
94
+
95
+ // Установка новой позиции курсора и вызов onChange
96
+ if (newStartPos !== startPos) {
97
+ setStartPos(newStartPos)
98
+ }
99
+
100
+ onChange?.(viewValue, e)
101
+ }
102
+
103
+ const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
104
+ const inputValue = e.target.value
105
+ const { viewValue: parsedViewValue, startPos: parsedStartPos } = parseValue(inputValue)
106
+ setViewValue(parsedViewValue)
107
+ setStartPos(parsedStartPos)
108
+ onChange?.(parsedViewValue, e)
109
+ }
110
+
111
+ return (
112
+ <input
113
+ {...props}
114
+ ref={innerRef}
115
+ value={viewValue}
116
+ onKeyDown={handleKeyDown}
117
+ onChange={handleChange}
118
+ />
119
+ )
120
+ }
121
+ )
122
+
123
+ 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,3 @@
1
+ import AmountInput from './AmountInput'
2
+
3
+ export { AmountInput }
@@ -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,13 @@
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
+ return <span className="currency-iso">{symbol}</span>
11
+ }
12
+
13
+ export default CurrencySymbol
@@ -0,0 +1,7 @@
1
+ export enum currencySymbols {
2
+ USD = '$',
3
+ EUR = '€',
4
+ CNY = '¥',
5
+ RUB = '₽',
6
+ RUR = '₽',
7
+ }
@@ -0,0 +1,3 @@
1
+ import CurrencySymbol from './CurrencySymbol'
2
+
3
+ export { CurrencySymbol }
@@ -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, type = 'text', ...props }: IInputProps, ref: ForwardedRef<HTMLInputElement>) => {
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}
@@ -1,4 +1,3 @@
1
1
  import Input from './Input'
2
- import InputClearable from './InputClearable'
3
2
 
4
- export { Input, InputClearable }
3
+ export { Input }
@@ -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)[] | NumberConstructor
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, InputClearable } from './Base/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, InputClearable }
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