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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vueless",
3
- "version": "0.0.765",
3
+ "version": "0.0.767",
4
4
  "license": "MIT",
5
5
  "description": "Vue Styleless UI Component Library, powered by Tailwind CSS.",
6
6
  "keywords": [
@@ -112,7 +112,7 @@ const { getDataTest, moneyInputAttrs } = useUI<Config>(defaultConfig);
112
112
  <UInput
113
113
  :id="elementId"
114
114
  ref="moneyInputRef"
115
- v-model="formattedValue"
115
+ :model-value="formattedValue"
116
116
  :size="size"
117
117
  :label="localLabel"
118
118
  :label-align="labelAlign"
@@ -17,7 +17,7 @@ export interface Props {
17
17
  /**
18
18
  * Input value.
19
19
  */
20
- modelValue?: number | string;
20
+ modelValue?: string;
21
21
  /**
22
22
  * Input label.
23
23
  */
@@ -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
- options: (() => FormatOptions) | FormatOptions,
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
- () => toValue(options),
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
- onInput(formattedValue.value as unknown as InputEvent);
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
- // Use to set input value manually
40
- function setValue(value: string | number) {
41
- const localFormattedValue = getFormattedValue(value, toValue(options));
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 = localFormattedValue;
44
- rawValue.value = getRawValue(localFormattedValue, toValue(options));
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
- await nextTick(async () => {
53
- if (!inputElement) return;
161
+ const isReservedSymbol = eventData !== rawDecimalMark && eventData !== comma;
54
162
 
55
- let cursorStart = inputElement.selectionStart;
56
- let cursorEnd = inputElement.selectionEnd;
163
+ if (
164
+ (!isNumericValue && isReservedSymbol && !isMinus && eventData.length === 1) ||
165
+ isDoubleMinus ||
166
+ isMinusWithin
167
+ ) {
168
+ inputElement.value = formattedValue.value;
57
169
 
58
- const hasValueInputValue = cursorEnd === 1 && cursorStart === 1;
59
- const input = event.target as HTMLInputElement;
60
- const value = input.value || "";
170
+ await nextTick();
61
171
 
62
- const localFormattedValue = getFormattedValue(value, toValue(options));
172
+ inputElement.setSelectionRange(cursorStart, cursorEnd);
63
173
 
64
- const currentValueOffsetLength = localFormattedValue
65
- .split("")
66
- .filter((value: string) => value === toValue(options).thousandsSeparator).length;
174
+ return;
175
+ }
67
176
 
68
- const prevValueOffsetLength = prevValue
69
- .split("")
70
- .filter((value) => value === toValue(options).thousandsSeparator).length;
177
+ const newFormattedValue = getFormattedValue(newRawValue, options.value);
71
178
 
72
- const prefixLength = toValue(options).prefix.length;
73
- const offset = currentValueOffsetLength - prevValueOffsetLength;
179
+ if (Number.isNaN(newFormattedValue) || newFormattedValue.includes("NaN")) {
180
+ inputElement.value = prevValue.value;
74
181
 
75
- formattedValue.value = localFormattedValue || toValue(options).prefix;
76
- rawValue.value = getRawValue(localFormattedValue, toValue(options));
182
+ return;
183
+ }
77
184
 
78
- await nextTick(() => {
79
- if (localFormattedValue.length === cursorEnd || !cursorStart || !cursorEnd) return;
185
+ formattedValue.value = newFormattedValue;
186
+ rawValue.value = getRawValue(newFormattedValue, options.value);
80
187
 
81
- if (hasValueInputValue && prefixLength) {
82
- cursorStart += prefixLength;
83
- cursorEnd += prefixLength;
84
- }
188
+ inputElement.value = formattedValue.value;
189
+
190
+ await setInputCursor(newFormattedValue, inputElement, cursorStart, cursorEnd);
191
+
192
+ prevValue.value = formattedValue.value;
193
+ }
85
194
 
86
- if (inputElement) {
87
- inputElement.setSelectionRange(cursorStart + offset, cursorEnd + offset);
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
- prevValue = formattedValue.value;
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(value: string | number, options: FormatOptions): string {
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 = String(value).endsWith(decimalSeparator)
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 | number, options: FormatOptions): 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 invalidValuesRegExp = new RegExp("[^\\d,\\d.\\s-" + decimalSeparator + "]", "g");
33
- const doubleValueRegExp = new RegExp("([,\\.\\s\\-" + decimalSeparator + "])+", "g");
23
+ const minFractionDigits = Math.abs(options.minFractionDigits);
24
+ const maxFractionDigits = Math.abs(options.maxFractionDigits);
34
25
 
35
- const actualMinFractionDigit =
36
- minFractionDigits <= maxFractionDigits ? minFractionDigits : maxFractionDigits;
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((rawValue || 0) as unknown as number)
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