vueless 0.0.765 → 0.0.767
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
|
@@ -112,7 +112,7 @@ const { getDataTest, moneyInputAttrs } = useUI<Config>(defaultConfig);
|
|
|
112
112
|
<UInput
|
|
113
113
|
:id="elementId"
|
|
114
114
|
ref="moneyInputRef"
|
|
115
|
-
|
|
115
|
+
:model-value="formattedValue"
|
|
116
116
|
:size="size"
|
|
117
117
|
:label="localLabel"
|
|
118
118
|
:label-align="labelAlign"
|
|
@@ -1,22 +1,29 @@
|
|
|
1
|
-
import { onMounted, nextTick, ref, onBeforeUnmount, toValue, watch } from "vue";
|
|
1
|
+
import { onMounted, nextTick, ref, onBeforeUnmount, toValue, watch, computed, readonly } from "vue";
|
|
2
2
|
|
|
3
3
|
import { getRawValue, getFormattedValue } from "./utilFormat.ts";
|
|
4
4
|
|
|
5
5
|
import type { FormatOptions } from "./types.ts";
|
|
6
6
|
|
|
7
|
+
const digitSet = ["1", "2", "3", "4", "5", "6", "7", "9", "0"];
|
|
8
|
+
const rawDecimalMark = ".";
|
|
9
|
+
const comma = ",";
|
|
10
|
+
const arrowKeys = ["ArrowUp", "ArrowRight", "ArrowDown", "ArrowLeft"];
|
|
11
|
+
const minus = "-";
|
|
12
|
+
|
|
7
13
|
export default function useFormatCurrency(
|
|
8
14
|
elementId: string = "",
|
|
9
|
-
|
|
15
|
+
formatOptions: (() => FormatOptions) | FormatOptions,
|
|
10
16
|
) {
|
|
11
|
-
let prevValue = "";
|
|
12
17
|
let inputElement: HTMLInputElement | null = null;
|
|
13
18
|
|
|
14
19
|
const formattedValue = ref("");
|
|
15
20
|
const rawValue = ref("");
|
|
21
|
+
const prevValue = ref("");
|
|
22
|
+
|
|
23
|
+
const options = computed(() => toValue(formatOptions));
|
|
16
24
|
|
|
17
|
-
// update value according to updated options
|
|
18
25
|
watch(
|
|
19
|
-
() =>
|
|
26
|
+
() => options,
|
|
20
27
|
() => setValue(formattedValue.value),
|
|
21
28
|
{ deep: true },
|
|
22
29
|
);
|
|
@@ -26,7 +33,7 @@ export default function useFormatCurrency(
|
|
|
26
33
|
|
|
27
34
|
if (inputElement) {
|
|
28
35
|
inputElement.addEventListener("input", onInput);
|
|
29
|
-
|
|
36
|
+
inputElement.addEventListener("keydown", onKeydown);
|
|
30
37
|
}
|
|
31
38
|
});
|
|
32
39
|
|
|
@@ -36,61 +43,196 @@ export default function useFormatCurrency(
|
|
|
36
43
|
}
|
|
37
44
|
});
|
|
38
45
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Set input value manually.
|
|
48
|
+
* @param {Intl.StringNumericLiteral} value
|
|
49
|
+
* @returns {void}
|
|
50
|
+
*/
|
|
51
|
+
function setValue(value: string) {
|
|
52
|
+
const newFormattedValue = getFormattedValue(value, options.value);
|
|
42
53
|
|
|
43
|
-
formattedValue.value =
|
|
44
|
-
rawValue.value = getRawValue(
|
|
54
|
+
formattedValue.value = newFormattedValue;
|
|
55
|
+
rawValue.value = getRawValue(newFormattedValue, options.value);
|
|
45
56
|
|
|
46
|
-
prevValue = formattedValue.value;
|
|
57
|
+
prevValue.value = formattedValue.value;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function onKeydown(event: KeyboardEvent) {
|
|
61
|
+
if (!event.target || !inputElement) return;
|
|
62
|
+
|
|
63
|
+
const cursorStart = inputElement.selectionStart || 0;
|
|
64
|
+
const cursorEnd = inputElement.selectionEnd || 0;
|
|
65
|
+
const isEndOfValue = cursorEnd === formattedValue.value.length;
|
|
66
|
+
const isKeyCombination = event.ctrlKey || event.shiftKey || event.metaKey || event.altKey;
|
|
67
|
+
const isSelection = cursorEnd !== cursorStart;
|
|
68
|
+
|
|
69
|
+
if (event.key === "Backspace" && !isSelection) {
|
|
70
|
+
const charToRemove = inputElement.value[cursorStart - 1];
|
|
71
|
+
const isFormatChar = [
|
|
72
|
+
options.value.thousandsSeparator,
|
|
73
|
+
options.value.prefix,
|
|
74
|
+
options.value.decimalSeparator,
|
|
75
|
+
].includes(charToRemove);
|
|
76
|
+
|
|
77
|
+
// Skip unremovable character and put cursor one step back.
|
|
78
|
+
if (isFormatChar && !inputElement.value.endsWith(options.value.decimalSeparator)) {
|
|
79
|
+
event.preventDefault();
|
|
80
|
+
|
|
81
|
+
inputElement.setSelectionRange(cursorStart - 1, cursorEnd - 1);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const endsWithDecimal = formattedValue.value.endsWith(options.value.decimalSeparator);
|
|
88
|
+
const includesDecimalMark =
|
|
89
|
+
formattedValue.value.includes(options.value.decimalSeparator) && !endsWithDecimal;
|
|
90
|
+
const isCharKey = !arrowKeys.includes(event.key) && !isKeyCombination;
|
|
91
|
+
|
|
92
|
+
if ((event.key === comma || event.key === rawDecimalMark) && endsWithDecimal) {
|
|
93
|
+
event.preventDefault();
|
|
94
|
+
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (isEndOfValue && includesDecimalMark && isCharKey && !isSelection) {
|
|
99
|
+
const fraction = prevValue.value.split(options.value.decimalSeparator).at(-1) || "";
|
|
100
|
+
|
|
101
|
+
if (fraction.length >= options.value.maxFractionDigits) {
|
|
102
|
+
event.preventDefault();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
47
107
|
}
|
|
48
108
|
|
|
49
109
|
async function onInput(event: Event) {
|
|
50
|
-
if (!event.target) return;
|
|
110
|
+
if (!event.target || !inputElement) return;
|
|
111
|
+
|
|
112
|
+
await nextTick();
|
|
113
|
+
|
|
114
|
+
const cursorStart = inputElement.selectionStart || 0;
|
|
115
|
+
const cursorEnd = inputElement.selectionEnd || 0;
|
|
116
|
+
|
|
117
|
+
const input = event.target as HTMLInputElement;
|
|
118
|
+
|
|
119
|
+
let value = input.value || "";
|
|
120
|
+
|
|
121
|
+
const prevCursorPosition = cursorEnd - 1;
|
|
122
|
+
const eventData = (event as InputEvent).data || "";
|
|
123
|
+
|
|
124
|
+
if (value === minus) {
|
|
125
|
+
formattedValue.value = minus;
|
|
126
|
+
rawValue.value = minus;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (!value || value.startsWith(`${options.value.decimalSeparator}0`)) {
|
|
130
|
+
formattedValue.value = options.value.prefix;
|
|
131
|
+
rawValue.value = "";
|
|
132
|
+
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Replace dot with decimal separator
|
|
137
|
+
if (eventData === rawDecimalMark || eventData === comma) {
|
|
138
|
+
value = [
|
|
139
|
+
...prevValue.value.slice(0, prevCursorPosition),
|
|
140
|
+
options.value.decimalSeparator,
|
|
141
|
+
...prevValue.value.slice(prevCursorPosition),
|
|
142
|
+
].join("");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (value.split(options.value.decimalSeparator).length > 2) {
|
|
146
|
+
value = value.split("").with(value.lastIndexOf(options.value.decimalSeparator), "").join("");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (value.endsWith(options.value.decimalSeparator)) {
|
|
150
|
+
formattedValue.value = value;
|
|
151
|
+
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const newRawValue = getRawValue(value, options.value);
|
|
156
|
+
const isNumericValue = eventData && digitSet.includes(eventData);
|
|
157
|
+
const isMinus = cursorEnd === 1 && cursorStart === 1 && eventData === minus;
|
|
158
|
+
const isDoubleMinus = isMinus && prevValue.value.startsWith(minus);
|
|
159
|
+
const isMinusWithin = newRawValue.includes(minus) && !newRawValue.startsWith(minus);
|
|
51
160
|
|
|
52
|
-
|
|
53
|
-
if (!inputElement) return;
|
|
161
|
+
const isReservedSymbol = eventData !== rawDecimalMark && eventData !== comma;
|
|
54
162
|
|
|
55
|
-
|
|
56
|
-
|
|
163
|
+
if (
|
|
164
|
+
(!isNumericValue && isReservedSymbol && !isMinus && eventData.length === 1) ||
|
|
165
|
+
isDoubleMinus ||
|
|
166
|
+
isMinusWithin
|
|
167
|
+
) {
|
|
168
|
+
inputElement.value = formattedValue.value;
|
|
57
169
|
|
|
58
|
-
|
|
59
|
-
const input = event.target as HTMLInputElement;
|
|
60
|
-
const value = input.value || "";
|
|
170
|
+
await nextTick();
|
|
61
171
|
|
|
62
|
-
|
|
172
|
+
inputElement.setSelectionRange(cursorStart, cursorEnd);
|
|
63
173
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
.filter((value: string) => value === toValue(options).thousandsSeparator).length;
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
67
176
|
|
|
68
|
-
|
|
69
|
-
.split("")
|
|
70
|
-
.filter((value) => value === toValue(options).thousandsSeparator).length;
|
|
177
|
+
const newFormattedValue = getFormattedValue(newRawValue, options.value);
|
|
71
178
|
|
|
72
|
-
|
|
73
|
-
|
|
179
|
+
if (Number.isNaN(newFormattedValue) || newFormattedValue.includes("NaN")) {
|
|
180
|
+
inputElement.value = prevValue.value;
|
|
74
181
|
|
|
75
|
-
|
|
76
|
-
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
77
184
|
|
|
78
|
-
|
|
79
|
-
|
|
185
|
+
formattedValue.value = newFormattedValue;
|
|
186
|
+
rawValue.value = getRawValue(newFormattedValue, options.value);
|
|
80
187
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
188
|
+
inputElement.value = formattedValue.value;
|
|
189
|
+
|
|
190
|
+
await setInputCursor(newFormattedValue, inputElement, cursorStart, cursorEnd);
|
|
191
|
+
|
|
192
|
+
prevValue.value = formattedValue.value;
|
|
193
|
+
}
|
|
85
194
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
195
|
+
async function setInputCursor(
|
|
196
|
+
newValue: string,
|
|
197
|
+
inputElement: HTMLInputElement,
|
|
198
|
+
prevCursorStart: number | null,
|
|
199
|
+
prevCursorEnd: number | null,
|
|
200
|
+
) {
|
|
201
|
+
const hasValueInputValue = prevCursorStart === 1 && prevCursorEnd === 1;
|
|
90
202
|
|
|
91
|
-
|
|
92
|
-
|
|
203
|
+
const currentValueOffsetLength = newValue
|
|
204
|
+
.split("")
|
|
205
|
+
.filter((value: string) => value === options.value.thousandsSeparator).length;
|
|
206
|
+
|
|
207
|
+
const prevValueOffsetLength = prevValue.value
|
|
208
|
+
.split("")
|
|
209
|
+
.filter((value) => value === options.value.thousandsSeparator).length;
|
|
210
|
+
|
|
211
|
+
const prefixLength = options.value.prefix.length;
|
|
212
|
+
const offset = currentValueOffsetLength - prevValueOffsetLength;
|
|
213
|
+
|
|
214
|
+
await nextTick();
|
|
215
|
+
|
|
216
|
+
if (offset < 0 && inputElement) {
|
|
217
|
+
inputElement.setSelectionRange(prevCursorStart, prevCursorEnd);
|
|
218
|
+
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (newValue.length === prevCursorEnd || !prevCursorStart || !prevCursorEnd) return;
|
|
223
|
+
|
|
224
|
+
let newCursorStart = prevCursorStart;
|
|
225
|
+
let newCursorEnd = prevCursorEnd;
|
|
226
|
+
|
|
227
|
+
if (hasValueInputValue && prefixLength) {
|
|
228
|
+
newCursorStart += prefixLength;
|
|
229
|
+
newCursorEnd += prefixLength;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (inputElement) {
|
|
233
|
+
inputElement.setSelectionRange(newCursorStart + offset, newCursorEnd + offset);
|
|
234
|
+
}
|
|
93
235
|
}
|
|
94
236
|
|
|
95
|
-
return { rawValue, formattedValue, setValue };
|
|
237
|
+
return { rawValue: readonly(rawValue), formattedValue: readonly(formattedValue), setValue };
|
|
96
238
|
}
|
|
@@ -1,81 +1,30 @@
|
|
|
1
1
|
import type { FormatOptions } from "./types.ts";
|
|
2
2
|
|
|
3
|
-
const isNumberValueRegExp = /^[\d,.\s-]+$/;
|
|
4
3
|
const rawDecimalMark = ".";
|
|
5
|
-
const minus = "-";
|
|
6
4
|
|
|
7
|
-
export function getRawValue(
|
|
5
|
+
export function getRawValue(
|
|
6
|
+
value: string,
|
|
7
|
+
options: Pick<FormatOptions, "prefix" | "decimalSeparator" | "thousandsSeparator">,
|
|
8
|
+
): Intl.StringNumericLiteral {
|
|
8
9
|
const { thousandsSeparator, decimalSeparator, prefix } = options;
|
|
9
10
|
|
|
10
|
-
value =
|
|
11
|
-
? String(value).replace(decimalSeparator, "")
|
|
12
|
-
: String(value);
|
|
11
|
+
value = value.endsWith(decimalSeparator) ? value.replace(decimalSeparator, "") : value;
|
|
13
12
|
|
|
14
13
|
const rawValueWithPrefix = value
|
|
15
14
|
.replaceAll(thousandsSeparator, "")
|
|
16
|
-
.replaceAll(" ", "")
|
|
17
15
|
.replace(decimalSeparator, rawDecimalMark);
|
|
18
16
|
|
|
19
|
-
return rawValueWithPrefix.replace(prefix, "");
|
|
17
|
+
return rawValueWithPrefix.replace(prefix, "") as Intl.StringNumericLiteral;
|
|
20
18
|
}
|
|
21
19
|
|
|
22
|
-
export function getFormattedValue(value: string
|
|
23
|
-
const {
|
|
24
|
-
thousandsSeparator,
|
|
25
|
-
decimalSeparator,
|
|
26
|
-
minFractionDigits,
|
|
27
|
-
maxFractionDigits,
|
|
28
|
-
prefix,
|
|
29
|
-
positiveOnly,
|
|
30
|
-
} = options;
|
|
20
|
+
export function getFormattedValue(value: string, options: FormatOptions): string {
|
|
21
|
+
const { thousandsSeparator, decimalSeparator, prefix, positiveOnly } = options;
|
|
31
22
|
|
|
32
|
-
const
|
|
33
|
-
const
|
|
23
|
+
const minFractionDigits = Math.abs(options.minFractionDigits);
|
|
24
|
+
const maxFractionDigits = Math.abs(options.maxFractionDigits);
|
|
34
25
|
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
// slice to first decimal mark
|
|
39
|
-
value = String(value)
|
|
40
|
-
.replaceAll(rawDecimalMark, decimalSeparator)
|
|
41
|
-
.split(decimalSeparator)
|
|
42
|
-
.slice(0, 2)
|
|
43
|
-
.map((value: string, index: number) =>
|
|
44
|
-
index ? value.replaceAll(thousandsSeparator, "") : value,
|
|
45
|
-
)
|
|
46
|
-
.join(decimalSeparator);
|
|
47
|
-
|
|
48
|
-
value = String(value)
|
|
49
|
-
.replace(invalidValuesRegExp, "")
|
|
50
|
-
.replace(doubleValueRegExp, "$1")
|
|
51
|
-
.replaceAll(decimalSeparator, rawDecimalMark)
|
|
52
|
-
.trim();
|
|
53
|
-
|
|
54
|
-
const isNumber = isNumberValueRegExp.test(value);
|
|
55
|
-
const isFloat = value.endsWith(rawDecimalMark) || value.endsWith(".0");
|
|
56
|
-
const isMinus = value === minus;
|
|
57
|
-
|
|
58
|
-
if (isMinus && positiveOnly) {
|
|
59
|
-
value = "";
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
if (value.includes(minus)) {
|
|
63
|
-
let isFirstMinus = value.startsWith(minus);
|
|
64
|
-
|
|
65
|
-
value = value.replaceAll(minus, (match) => {
|
|
66
|
-
if (isFirstMinus) {
|
|
67
|
-
isFirstMinus = false;
|
|
68
|
-
|
|
69
|
-
return match;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return "";
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (!value || !isNumber || isFloat || isMinus) {
|
|
77
|
-
return `${prefix}${value.replaceAll(rawDecimalMark, decimalSeparator)}`;
|
|
78
|
-
}
|
|
26
|
+
const isValidMinFractionDigits = minFractionDigits <= maxFractionDigits;
|
|
27
|
+
const actualMinFractionDigit = isValidMinFractionDigits ? minFractionDigits : maxFractionDigits;
|
|
79
28
|
|
|
80
29
|
const intlNumberOptions: Intl.NumberFormatOptions = {
|
|
81
30
|
minimumFractionDigits: actualMinFractionDigit,
|
|
@@ -89,31 +38,12 @@ export function getFormattedValue(value: string | number, options: FormatOptions
|
|
|
89
38
|
|
|
90
39
|
const intlNumber = new Intl.NumberFormat("en-US", intlNumberOptions);
|
|
91
40
|
|
|
92
|
-
const rawValue = getRawValue(value, {
|
|
93
|
-
decimalSeparator,
|
|
94
|
-
thousandsSeparator,
|
|
95
|
-
prefix,
|
|
96
|
-
minFractionDigits: 0,
|
|
97
|
-
maxFractionDigits: 2,
|
|
98
|
-
positiveOnly: false,
|
|
99
|
-
});
|
|
100
|
-
|
|
101
41
|
const formattedValue = intlNumber
|
|
102
|
-
.formatToParts(
|
|
42
|
+
.formatToParts(value as Intl.StringNumericLiteral)
|
|
103
43
|
.map((part) => {
|
|
104
44
|
if (part.type === "group") part.value = thousandsSeparator;
|
|
105
45
|
if (part.type === "decimal") part.value = decimalSeparator;
|
|
106
46
|
|
|
107
|
-
if (part.type === "fraction") {
|
|
108
|
-
const fraction = rawValue.split(rawDecimalMark).at(-1) || "";
|
|
109
|
-
const formattedFraction = fraction
|
|
110
|
-
.split("")
|
|
111
|
-
.slice(actualMinFractionDigit, maxFractionDigits)
|
|
112
|
-
.join("");
|
|
113
|
-
|
|
114
|
-
part.value = formattedFraction;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
47
|
return part;
|
|
118
48
|
});
|
|
119
49
|
|