diff --git a/src/components/base/NumberInputBase.ts b/src/components/base/NumberInputBase.ts new file mode 100644 index 00000000..c7e6ea79 --- /dev/null +++ b/src/components/base/NumberInputBase.ts @@ -0,0 +1,123 @@ +import { watch } from 'vue'; + +import { useI18n } from '@/locales/helpers.ts'; +import { type CommonNumberInputProps, useCommonNumberInputBase } from '@/components/base/CommonNumberInputBase.ts'; + +import { isNumber, replaceAll, removeAll } from '@/lib/common.ts'; + +export interface NumberInputProps extends CommonNumberInputProps { + minValue?: number; + maxValue?: number; + maxDecimalCount?: number; +} + +export interface NumberInputEmits { + (e: 'update:modelValue', value: number): void; +} + +export function useNumberInputBase(props: NumberInputProps, emit: NumberInputEmits) { + const { + getCurrentDecimalSeparator, + getCurrentDigitGroupingSymbol + } = useI18n(); + + const { + currentValue, + onKeyUpDown, + onPaste + } = useCommonNumberInputBase(props, props.maxDecimalCount ?? -1, getFormattedValue(props.modelValue), parseNumber, getFormattedValue, getValidFormattedValue); + + function parseNumber(value: string): number { + if (!value) { + return 0; + } + + const decimalSeparator = getCurrentDecimalSeparator(); + + let finalValue = ''; + for (let i = 0; i < value.length; i++) { + if (!('0' <= value[i] && value[i] <= '9') && value[i] !== '-' && value[i] !== decimalSeparator) { + break; + } + + finalValue += value[i]; + } + + if (decimalSeparator !== '.') { + finalValue = replaceAll(finalValue, decimalSeparator, '.'); + } + + return parseFloat(finalValue); + } + + function getValidFormattedValue(value: number, textualValue: string): string { + if (isNumber(props.minValue) && value < props.minValue) { + return getFormattedValue(props.minValue); + } + + if (isNumber(props.maxValue) && value > props.maxValue) { + return getFormattedValue(props.maxValue); + } + + const decimalSeparator = getCurrentDecimalSeparator(); + const digitGroupingSymbol = getCurrentDigitGroupingSymbol(); + return replaceAll(removeAll(textualValue, digitGroupingSymbol), '.', decimalSeparator); + } + + function getFormattedValue(value: number): string { + if (!Number.isNaN(value) && Number.isFinite(value)) { + const decimalSeparator = getCurrentDecimalSeparator(); + + if (isNumber(props.maxDecimalCount) && props.maxDecimalCount >= 0) { + return replaceAll(value.toFixed(props.maxDecimalCount), '.', decimalSeparator); + } else { + return replaceAll(value.toString(), '.', decimalSeparator); + } + } + + return '0'; + } + + watch(() => props.modelValue, (newValue) => { + const numericCurrentValue = parseNumber(currentValue.value); + + if (newValue !== numericCurrentValue) { + const newStringValue = getFormattedValue(newValue); + + if (!(newStringValue === '0' && currentValue.value === '')) { + currentValue.value = newStringValue; + } + } + }); + + watch(currentValue, (newValue) => { + let finalValue = ''; + + if (newValue) { + const decimalSeparator = getCurrentDecimalSeparator(); + + for (let i = 0; i < newValue.length; i++) { + if (!('0' <= newValue[i] && newValue[i] <= '9') && newValue[i] !== '-' && newValue[i] !== decimalSeparator) { + break; + } + + finalValue += newValue[i]; + } + } + + if (finalValue !== newValue) { + currentValue.value = finalValue; + } else { + const value: number = parseNumber(finalValue); + emit('update:modelValue', value); + } + }); + + return { + // states + currentValue, + // functions + onKeyUpDown, + onPaste + } +} diff --git a/src/components/desktop/NumberInput.vue b/src/components/desktop/NumberInput.vue new file mode 100644 index 00000000..1abc4e13 --- /dev/null +++ b/src/components/desktop/NumberInput.vue @@ -0,0 +1,35 @@ + + + diff --git a/src/components/mobile/ListNumberInput.vue b/src/components/mobile/ListNumberInput.vue new file mode 100644 index 00000000..baab66de --- /dev/null +++ b/src/components/mobile/ListNumberInput.vue @@ -0,0 +1,26 @@ + + + diff --git a/src/consts/exchange_rate.ts b/src/consts/exchange_rate.ts new file mode 100644 index 00000000..f2dd6db7 --- /dev/null +++ b/src/consts/exchange_rate.ts @@ -0,0 +1,2 @@ +export const USER_CUSTOM_EXCHANGE_RATE_MIN_VALUE: number = 0; +export const USER_CUSTOM_EXCHANGE_RATE_MAX_VALUE: number = 999999999.9999; diff --git a/src/desktop-main.ts b/src/desktop-main.ts index c511b461..086157f5 100644 --- a/src/desktop-main.ts +++ b/src/desktop-main.ts @@ -77,6 +77,7 @@ import MapView from '@/components/common/MapView.vue'; import ItemIcon from '@/components/desktop/ItemIcon.vue'; import BtnVerticalGroup from '@/components/desktop/BtnVerticalGroup.vue'; +import NumberInput from '@/components/desktop/NumberInput.vue'; import AmountInput from '@/components/desktop/AmountInput.vue'; import LanguageSelect from '@/components/desktop/LanguageSelect.vue'; import LanguageSelectButton from '@/components/desktop/LanguageSelectButton.vue'; @@ -453,6 +454,7 @@ app.component('MapView', MapView); app.component('ItemIcon', ItemIcon); app.component('BtnVerticalGroup', BtnVerticalGroup); +app.component('NumberInput', NumberInput); app.component('AmountInput', AmountInput); app.component('LanguageSelect', LanguageSelect); app.component('LanguageSelectButton', LanguageSelectButton); diff --git a/src/mobile-main.ts b/src/mobile-main.ts index a1e0588e..1bcb73d2 100644 --- a/src/mobile-main.ts +++ b/src/mobile-main.ts @@ -95,6 +95,7 @@ import DateSelectionSheet from '@/components/mobile/DateSelectionSheet.vue'; import DateRangeSelectionSheet from '@/components/mobile/DateRangeSelectionSheet.vue'; import MonthSelectionSheet from '@/components/mobile/MonthSelectionSheet.vue'; import MonthRangeSelectionSheet from '@/components/mobile/MonthRangeSelectionSheet.vue'; +import ListNumberInput from '@/components/mobile/ListNumberInput.vue'; import ListItemSelectionSheet from '@/components/mobile/ListItemSelectionSheet.vue'; import ListItemSelectionPopup from '@/components/mobile/ListItemSelectionPopup.vue'; import TwoColumnListItemSelectionSheet from '@/components/mobile/TwoColumnListItemSelectionSheet.vue'; @@ -181,6 +182,7 @@ app.component('DateSelectionSheet', DateSelectionSheet); app.component('DateRangeSelectionSheet', DateRangeSelectionSheet); app.component('MonthSelectionSheet', MonthSelectionSheet); app.component('MonthRangeSelectionSheet', MonthRangeSelectionSheet); +app.component('ListNumberInput', ListNumberInput); app.component('ListItemSelectionSheet', ListItemSelectionSheet); app.component('ListItemSelectionPopup', ListItemSelectionPopup); app.component('TwoColumnListItemSelectionSheet', TwoColumnListItemSelectionSheet); diff --git a/src/views/desktop/exchangerates/list/dialogs/UpdateDialog.vue b/src/views/desktop/exchangerates/list/dialogs/UpdateDialog.vue index d785e713..8a1d39ed 100644 --- a/src/views/desktop/exchangerates/list/dialogs/UpdateDialog.vue +++ b/src/views/desktop/exchangerates/list/dialogs/UpdateDialog.vue @@ -11,11 +11,13 @@ - @@ -28,11 +30,13 @@ - @@ -68,6 +72,11 @@ import { useI18n } from '@/locales/helpers.ts'; import { useUserStore } from '@/stores/user.ts'; import { useExchangeRatesStore } from '@/stores/exchangeRates.ts'; +import { + USER_CUSTOM_EXCHANGE_RATE_MAX_VALUE, + USER_CUSTOM_EXCHANGE_RATE_MIN_VALUE +} from '@/consts/exchange_rate.ts'; + import { mdiSwapVertical } from '@mdi/js'; diff --git a/src/views/mobile/exchangerates/UpdatePage.vue b/src/views/mobile/exchangerates/UpdatePage.vue index 87140fbc..dd417dc4 100644 --- a/src/views/mobile/exchangerates/UpdatePage.vue +++ b/src/views/mobile/exchangerates/UpdatePage.vue @@ -10,13 +10,17 @@ - + - + ();