vueless 0.0.766 → 0.0.768

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.766",
3
+ "version": "0.0.768",
4
4
  "license": "MIT",
5
5
  "description": "Vue Styleless UI Component Library, powered by Tailwind CSS.",
6
6
  "keywords": [
@@ -56,19 +56,21 @@ watch(
56
56
  () => props.modelValue,
57
57
  () => {
58
58
  if (String(localValue.value) !== String(rawValue.value)) {
59
- setValue(localValue.value);
59
+ setValue(String(localValue.value));
60
60
  }
61
61
  },
62
62
  );
63
63
 
64
64
  onMounted(() => {
65
65
  if (localValue.value) {
66
- setValue(localValue.value);
66
+ setValue(String(localValue.value));
67
67
  }
68
68
  });
69
69
 
70
70
  function onKeyup(event: KeyboardEvent) {
71
- localValue.value = rawValue.value || "";
71
+ const numberValue = rawValue.value ? parseFloat(rawValue.value) : "";
72
+
73
+ localValue.value = props.valueType === "number" ? numberValue : rawValue.value || "";
72
74
 
73
75
  nextTick(() => emit("keyup", event));
74
76
  }
@@ -112,7 +114,7 @@ const { getDataTest, moneyInputAttrs } = useUI<Config>(defaultConfig);
112
114
  <UInput
113
115
  :id="elementId"
114
116
  ref="moneyInputRef"
115
- v-model="formattedValue"
117
+ :model-value="formattedValue"
116
118
  :size="size"
117
119
  :label="localLabel"
118
120
  :label-align="labelAlign"
@@ -9,6 +9,7 @@ export default /*tw*/ {
9
9
  thousandsSeparator: " ",
10
10
  labelAlign: "topInside",
11
11
  prefix: "",
12
+ valueType: "number",
12
13
  positiveOnly: false,
13
14
  readonly: false,
14
15
  disabled: false,
@@ -17,7 +17,13 @@ export interface Props {
17
17
  /**
18
18
  * Input value.
19
19
  */
20
- modelValue?: number | string;
20
+ modelValue?: string | number;
21
+
22
+ /**
23
+ * Set type of the modelValue.
24
+ */
25
+ valueType?: "string" | "number";
26
+
21
27
  /**
22
28
  * Input label.
23
29
  */
@@ -1,22 +1,28 @@
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 minus = "-";
11
+
7
12
  export default function useFormatCurrency(
8
13
  elementId: string = "",
9
- options: (() => FormatOptions) | FormatOptions,
14
+ formatOptions: (() => FormatOptions) | FormatOptions,
10
15
  ) {
11
- let prevValue = "";
12
16
  let inputElement: HTMLInputElement | null = null;
13
17
 
14
18
  const formattedValue = ref("");
15
19
  const rawValue = ref("");
20
+ const prevValue = ref("");
21
+
22
+ const options = computed(() => toValue(formatOptions));
16
23
 
17
- // update value according to updated options
18
24
  watch(
19
- () => toValue(options),
25
+ () => options,
20
26
  () => setValue(formattedValue.value),
21
27
  { deep: true },
22
28
  );
@@ -26,7 +32,7 @@ export default function useFormatCurrency(
26
32
 
27
33
  if (inputElement) {
28
34
  inputElement.addEventListener("input", onInput);
29
- onInput(formattedValue.value as unknown as InputEvent);
35
+ inputElement.addEventListener("keydown", onKeydown);
30
36
  }
31
37
  });
32
38
 
@@ -36,61 +42,193 @@ export default function useFormatCurrency(
36
42
  }
37
43
  });
38
44
 
39
- // Use to set input value manually
40
- function setValue(value: string | number) {
41
- const localFormattedValue = getFormattedValue(value, toValue(options));
45
+ /**
46
+ * Set input value manually.
47
+ * @param {Intl.StringNumericLiteral} value
48
+ * @returns {void}
49
+ */
50
+ function setValue(value: string) {
51
+ const newFormattedValue = getFormattedValue(value, options.value);
52
+
53
+ formattedValue.value = newFormattedValue;
54
+ rawValue.value = getRawValue(newFormattedValue, options.value);
55
+
56
+ prevValue.value = formattedValue.value;
57
+ }
58
+
59
+ function onKeydown(event: KeyboardEvent) {
60
+ if (!event.target || !inputElement) return;
61
+
62
+ const cursorStart = inputElement.selectionStart || 0;
63
+ const cursorEnd = inputElement.selectionEnd || 0;
64
+ const isSelection = cursorEnd !== cursorStart;
42
65
 
43
- formattedValue.value = localFormattedValue;
44
- rawValue.value = getRawValue(localFormattedValue, toValue(options));
66
+ if (event.key === "Backspace" && !isSelection) {
67
+ const charToRemove = inputElement.value[cursorStart - 1];
68
+ const isFormatChar = [
69
+ options.value.thousandsSeparator,
70
+ options.value.prefix,
71
+ options.value.decimalSeparator,
72
+ ].includes(charToRemove);
45
73
 
46
- prevValue = formattedValue.value;
74
+ // Skip unremovable character and put cursor one step back.
75
+ if (isFormatChar && !inputElement.value.endsWith(options.value.decimalSeparator)) {
76
+ event.preventDefault();
77
+
78
+ inputElement.setSelectionRange(cursorStart - 1, cursorEnd - 1);
79
+ }
80
+
81
+ return;
82
+ }
83
+
84
+ const endsWithDecimal = formattedValue.value.endsWith(options.value.decimalSeparator);
85
+
86
+ if ((event.key === comma || event.key === rawDecimalMark) && endsWithDecimal) {
87
+ event.preventDefault();
88
+
89
+ return;
90
+ }
47
91
  }
48
92
 
49
93
  async function onInput(event: Event) {
50
- if (!event.target) return;
94
+ if (!event.target || !inputElement) return;
51
95
 
52
- await nextTick(async () => {
53
- if (!inputElement) return;
96
+ await nextTick();
54
97
 
55
- let cursorStart = inputElement.selectionStart;
56
- let cursorEnd = inputElement.selectionEnd;
98
+ const cursorStart = inputElement.selectionStart || 0;
99
+ const cursorEnd = inputElement.selectionEnd || 0;
57
100
 
58
- const hasValueInputValue = cursorEnd === 1 && cursorStart === 1;
59
- const input = event.target as HTMLInputElement;
60
- const value = input.value || "";
101
+ const input = event.target as HTMLInputElement;
61
102
 
62
- const localFormattedValue = getFormattedValue(value, toValue(options));
103
+ let value = input.value || "";
63
104
 
64
- const currentValueOffsetLength = localFormattedValue
65
- .split("")
66
- .filter((value: string) => value === toValue(options).thousandsSeparator).length;
105
+ const prevCursorPosition = cursorEnd - 1;
106
+ const eventData = (event as InputEvent).data || "";
67
107
 
68
- const prevValueOffsetLength = prevValue
69
- .split("")
70
- .filter((value) => value === toValue(options).thousandsSeparator).length;
108
+ if (value === minus) {
109
+ formattedValue.value = minus;
110
+ rawValue.value = minus;
111
+ }
112
+
113
+ if (!value || value.startsWith(`${options.value.decimalSeparator}0`)) {
114
+ formattedValue.value = options.value.prefix;
115
+ rawValue.value = "";
116
+
117
+ return;
118
+ }
71
119
 
72
- const prefixLength = toValue(options).prefix.length;
73
- const offset = currentValueOffsetLength - prevValueOffsetLength;
120
+ // Replace dot with decimal separator
121
+ if (eventData === rawDecimalMark || eventData === comma) {
122
+ value = [
123
+ ...prevValue.value.slice(0, prevCursorPosition),
124
+ options.value.decimalSeparator,
125
+ ...prevValue.value.slice(prevCursorPosition),
126
+ ].join("");
127
+ }
128
+
129
+ if (value.split(options.value.decimalSeparator).length > 2) {
130
+ value = value.split("").with(value.lastIndexOf(options.value.decimalSeparator), "").join("");
131
+ }
132
+
133
+ const decimalSeparatorIndex = value.indexOf(options.value.decimalSeparator);
134
+ const newRawValue = getRawValue(value, options.value);
135
+
136
+ const isEventDataDecimal =
137
+ value.endsWith(options.value.decimalSeparator) ||
138
+ value.endsWith(`${options.value.decimalSeparator}0`);
139
+
140
+ if (
141
+ isEventDataDecimal &&
142
+ cursorStart > decimalSeparatorIndex &&
143
+ !options.value.minFractionDigits
144
+ ) {
145
+ formattedValue.value = value;
146
+
147
+ rawValue.value = newRawValue;
148
+
149
+ return;
150
+ }
74
151
 
75
- formattedValue.value = localFormattedValue || toValue(options).prefix;
76
- rawValue.value = getRawValue(localFormattedValue, toValue(options));
152
+ const isNumericValue = eventData && digitSet.includes(eventData);
153
+ const isMinus = cursorEnd === 1 && cursorStart === 1 && eventData === minus;
154
+ const isDoubleMinus = isMinus && prevValue.value.startsWith(minus);
155
+ const isMinusWithin = newRawValue.includes(minus) && !newRawValue.startsWith(minus);
77
156
 
78
- await nextTick(() => {
79
- if (localFormattedValue.length === cursorEnd || !cursorStart || !cursorEnd) return;
157
+ const isReservedSymbol = eventData !== rawDecimalMark && eventData !== comma;
80
158
 
81
- if (hasValueInputValue && prefixLength) {
82
- cursorStart += prefixLength;
83
- cursorEnd += prefixLength;
84
- }
159
+ if (
160
+ (!isNumericValue && isReservedSymbol && !isMinus && eventData.length === 1) ||
161
+ isDoubleMinus ||
162
+ isMinusWithin
163
+ ) {
164
+ inputElement.value = formattedValue.value;
85
165
 
86
- if (inputElement) {
87
- inputElement.setSelectionRange(cursorStart + offset, cursorEnd + offset);
88
- }
89
- });
166
+ await nextTick();
90
167
 
91
- prevValue = formattedValue.value;
92
- });
168
+ inputElement.setSelectionRange(cursorStart, cursorEnd);
169
+
170
+ return;
171
+ }
172
+
173
+ const newFormattedValue = getFormattedValue(newRawValue, options.value);
174
+
175
+ if (Number.isNaN(newFormattedValue) || newFormattedValue.includes("NaN")) {
176
+ inputElement.value = prevValue.value;
177
+
178
+ return;
179
+ }
180
+
181
+ formattedValue.value = newFormattedValue;
182
+ rawValue.value = getRawValue(newFormattedValue, options.value);
183
+
184
+ inputElement.value = formattedValue.value;
185
+
186
+ await setInputCursor(newFormattedValue, inputElement, cursorStart, cursorEnd);
187
+
188
+ prevValue.value = formattedValue.value;
189
+ }
190
+
191
+ async function setInputCursor(
192
+ newValue: string,
193
+ inputElement: HTMLInputElement,
194
+ prevCursorStart: number | null,
195
+ prevCursorEnd: number | null,
196
+ ) {
197
+ const hasValueInputValue = prevCursorStart === 1 && prevCursorEnd === 1;
198
+
199
+ const currentValueOffsetLength = newValue
200
+ .split("")
201
+ .filter((value: string) => value === options.value.thousandsSeparator).length;
202
+
203
+ const prevValueOffsetLength = prevValue.value
204
+ .split("")
205
+ .filter((value) => value === options.value.thousandsSeparator).length;
206
+
207
+ const prefixLength = options.value.prefix.length;
208
+ const offset = currentValueOffsetLength - prevValueOffsetLength;
209
+
210
+ await nextTick();
211
+
212
+ if (offset < 0 && inputElement) {
213
+ inputElement.setSelectionRange(prevCursorStart, prevCursorEnd);
214
+
215
+ return;
216
+ }
217
+
218
+ if (newValue.length === prevCursorEnd || !prevCursorStart || !prevCursorEnd) return;
219
+
220
+ let newCursorStart = prevCursorStart;
221
+ let newCursorEnd = prevCursorEnd;
222
+
223
+ if (hasValueInputValue && prefixLength) {
224
+ newCursorStart += prefixLength;
225
+ newCursorEnd += prefixLength;
226
+ }
227
+
228
+ if (inputElement) {
229
+ inputElement.setSelectionRange(newCursorStart + offset, newCursorEnd + offset);
230
+ }
93
231
  }
94
232
 
95
- return { rawValue, formattedValue, setValue };
233
+ return { rawValue: readonly(rawValue), formattedValue: readonly(formattedValue), setValue };
96
234
  }
@@ -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,25 +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
- part.value = part.value.padEnd(maxFractionDigits, "0");
109
- }
110
-
111
47
  return part;
112
48
  });
113
49