@milaboratories/uikit 2.3.25 → 2.3.27
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/.turbo/turbo-build.log +22 -21
- package/.turbo/turbo-type-check.log +1 -1
- package/CHANGELOG.md +13 -0
- package/dist/components/DataTable/TableComponent.vue.js +1 -1
- package/dist/components/PlFileDialog/Local.vue.js +7 -7
- package/dist/components/PlFileInput/PlFileInput.vue.js +7 -7
- package/dist/components/PlNumberField/PlNumberField.vue.d.ts +47 -24
- package/dist/components/PlNumberField/PlNumberField.vue.d.ts.map +1 -1
- package/dist/components/PlNumberField/PlNumberField.vue.js +97 -109
- package/dist/components/PlNumberField/PlNumberField.vue.js.map +1 -1
- package/dist/components/PlNumberField/parseNumber.d.ts +12 -0
- package/dist/components/PlNumberField/parseNumber.d.ts.map +1 -0
- package/dist/components/PlNumberField/parseNumber.js +77 -0
- package/dist/components/PlNumberField/parseNumber.js.map +1 -0
- package/dist/components/PlSlideModal/PlPureSlideModal.vue.js +1 -1
- package/dist/lib/model/common/dist/index.js +34 -34
- package/dist/lib/model/common/dist/index.js.map +1 -1
- package/dist/sdk/model/dist/index.js +15 -15
- package/dist/sdk/model/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/components/PlNumberField/PlNumberField.vue +89 -80
- package/src/components/PlNumberField/__tests__/PlNumberField.spec.ts +66 -9
- package/src/components/PlNumberField/parseNumber.ts +121 -0
- package/src/components/PlNumberField/pl-number-field.scss +9 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@milaboratories/uikit",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.27",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"d3-axis": "^3.0.0",
|
|
32
32
|
"resize-observer-polyfill": "^1.5.1",
|
|
33
33
|
"@milaboratories/helpers": "^1.6.19",
|
|
34
|
-
"@platforma-sdk/model": "^1.42.
|
|
34
|
+
"@platforma-sdk/model": "^1.42.4"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@vitejs/plugin-vue": "^5.2.3",
|
|
@@ -41,9 +41,9 @@
|
|
|
41
41
|
"vue-tsc": "^2.2.10",
|
|
42
42
|
"yarpm": "^1.2.0",
|
|
43
43
|
"svgo": "^3.3.2",
|
|
44
|
-
"@milaboratories/eslint-config": "^1.0.4",
|
|
45
44
|
"@milaboratories/ts-configs": "1.0.5",
|
|
46
|
-
"@milaboratories/build-configs": "1.0.5"
|
|
45
|
+
"@milaboratories/build-configs": "1.0.5",
|
|
46
|
+
"@milaboratories/eslint-config": "^1.0.4"
|
|
47
47
|
},
|
|
48
48
|
"scripts": {
|
|
49
49
|
"dev": "vite",
|
|
@@ -1,11 +1,32 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Number input field with increment/decrement buttons, validation, and min/max constraints.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* <PlNumberField v-model="price" :step="0.01" :min-value="0" label="Price" />
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* <PlNumberField
|
|
10
|
+
* v-model="evenNumber"
|
|
11
|
+
* :validate="(v) => v % 2 !== 0 ? 'Number must be even' : undefined"
|
|
12
|
+
* :update-on-enter-or-click-outside="true"
|
|
13
|
+
* label="Even Number"
|
|
14
|
+
* />
|
|
15
|
+
*/
|
|
16
|
+
export default {
|
|
17
|
+
name: 'PlNumberField',
|
|
18
|
+
};
|
|
19
|
+
</script>
|
|
20
|
+
|
|
1
21
|
<script setup lang="ts">
|
|
2
22
|
import './pl-number-field.scss';
|
|
3
23
|
import DoubleContour from '../../utils/DoubleContour.vue';
|
|
4
24
|
import { useLabelNotch } from '../../utils/useLabelNotch';
|
|
5
25
|
import { computed, ref, useSlots, watch } from 'vue';
|
|
6
26
|
import { PlTooltip } from '../PlTooltip';
|
|
27
|
+
import { parseNumber } from './parseNumber';
|
|
7
28
|
|
|
8
|
-
|
|
29
|
+
const props = withDefaults(defineProps<{
|
|
9
30
|
/** Input is disabled if true */
|
|
10
31
|
disabled?: boolean;
|
|
11
32
|
/** Label on the top border of the field, empty by default */
|
|
@@ -26,9 +47,7 @@ type NumberInputProps = {
|
|
|
26
47
|
errorMessage?: string;
|
|
27
48
|
/** Additional validity check for input value that must return an error text if failed */
|
|
28
49
|
validate?: (v: number) => string | undefined;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const props = withDefaults(defineProps<NumberInputProps>(), {
|
|
50
|
+
}>(), {
|
|
32
51
|
step: 1,
|
|
33
52
|
label: undefined,
|
|
34
53
|
placeholder: undefined,
|
|
@@ -42,93 +61,71 @@ const props = withDefaults(defineProps<NumberInputProps>(), {
|
|
|
42
61
|
|
|
43
62
|
const modelValue = defineModel<number | undefined>({ required: true });
|
|
44
63
|
|
|
45
|
-
const root = ref<HTMLElement>();
|
|
46
64
|
const slots = useSlots();
|
|
47
|
-
const input = ref<HTMLInputElement>();
|
|
48
65
|
|
|
49
|
-
|
|
66
|
+
const rootRef = ref<HTMLElement>();
|
|
67
|
+
const inputRef = ref<HTMLInputElement>();
|
|
68
|
+
|
|
69
|
+
useLabelNotch(rootRef);
|
|
50
70
|
|
|
51
71
|
function modelToString(v: number | undefined) {
|
|
52
72
|
return v === undefined ? '' : String(+v); // (+v) to avoid staying in input non-number values if they are provided in model
|
|
53
73
|
}
|
|
54
74
|
|
|
55
|
-
|
|
56
|
-
return v === '.' || v === ',' || v === '-';
|
|
57
|
-
}
|
|
58
|
-
function stringToModel(v: string) {
|
|
59
|
-
if (v === '') {
|
|
60
|
-
return undefined;
|
|
61
|
-
}
|
|
62
|
-
if (isPartial(v)) {
|
|
63
|
-
return 0;
|
|
64
|
-
}
|
|
65
|
-
let forParsing = v;
|
|
66
|
-
forParsing = forParsing.replace(',', '.');
|
|
67
|
-
forParsing = forParsing.replace('−', '-'); // minus, replacing for the case of input the whole copied value
|
|
68
|
-
forParsing = forParsing.replace('–', '-'); // dash, replacing for the case of input the whole copied value
|
|
69
|
-
forParsing = forParsing.replace('+', '');
|
|
70
|
-
return parseFloat(forParsing);
|
|
71
|
-
}
|
|
75
|
+
const parsedResult = computed(() => parseNumber(props, inputValue.value));
|
|
72
76
|
|
|
73
|
-
const
|
|
74
|
-
const innerNumberValue = computed(() => stringToModel(innerTextValue.value));
|
|
77
|
+
const cachedValue = ref<string | undefined>(undefined);
|
|
75
78
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
+
const resetCachedValue = () => cachedValue.value = undefined;
|
|
80
|
+
|
|
81
|
+
watch(modelValue, (n) => {
|
|
82
|
+
const r = parsedResult.value;
|
|
83
|
+
if (r.error || n !== r.value) {
|
|
84
|
+
resetCachedValue();
|
|
79
85
|
}
|
|
80
86
|
});
|
|
81
87
|
|
|
82
|
-
const NUMBER_REGEX = /^[-−–+]?(\d+)?[\\.,]?(\d+)?$/; // parseFloat works without errors on strings with multiple dots, or letters in value
|
|
83
88
|
const inputValue = computed({
|
|
84
89
|
get() {
|
|
85
|
-
return
|
|
90
|
+
return cachedValue.value ?? modelToString(modelValue.value);
|
|
86
91
|
},
|
|
87
92
|
set(nextValue: string) {
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
) {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
} else if (input.value) {
|
|
98
|
-
input.value.value = innerTextValue.value;
|
|
93
|
+
const r = parseNumber(props, nextValue);
|
|
94
|
+
|
|
95
|
+
cachedValue.value = r.cleanInput;
|
|
96
|
+
|
|
97
|
+
if (r.error || props.updateOnEnterOrClickOutside) {
|
|
98
|
+
inputRef.value!.value = r.cleanInput;
|
|
99
|
+
} else {
|
|
100
|
+
modelValue.value = r.value;
|
|
99
101
|
}
|
|
100
102
|
},
|
|
101
103
|
});
|
|
104
|
+
|
|
102
105
|
const focused = ref(false);
|
|
103
106
|
|
|
104
107
|
function applyChanges() {
|
|
105
|
-
if (
|
|
106
|
-
modelValue.value =
|
|
107
|
-
return;
|
|
108
|
+
if (parsedResult.value.error === undefined) {
|
|
109
|
+
modelValue.value = parsedResult.value.value;
|
|
108
110
|
}
|
|
109
|
-
modelValue.value = innerNumberValue.value;
|
|
110
111
|
}
|
|
111
112
|
|
|
112
113
|
const errors = computed(() => {
|
|
113
114
|
let ers: string[] = [];
|
|
115
|
+
|
|
114
116
|
if (props.errorMessage) {
|
|
115
117
|
ers.push(props.errorMessage);
|
|
116
118
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
119
|
+
|
|
120
|
+
const r = parsedResult.value;
|
|
121
|
+
|
|
122
|
+
if (r.error) {
|
|
123
|
+
ers.push(r.error.message);
|
|
124
|
+
} else if (props.validate && r.value !== undefined) {
|
|
125
|
+
const error = props.validate(r.value);
|
|
122
126
|
if (error) {
|
|
123
127
|
ers.push(error);
|
|
124
128
|
}
|
|
125
|
-
} else {
|
|
126
|
-
if (props.minValue !== undefined && parsedValue !== undefined && parsedValue < props.minValue) {
|
|
127
|
-
ers.push(`Value must be higher than ${props.minValue}`);
|
|
128
|
-
}
|
|
129
|
-
if (props.maxValue !== undefined && parsedValue !== undefined && parsedValue > props.maxValue) {
|
|
130
|
-
ers.push(`Value must be less than ${props.maxValue}`);
|
|
131
|
-
}
|
|
132
129
|
}
|
|
133
130
|
|
|
134
131
|
ers = [...ers];
|
|
@@ -137,18 +134,22 @@ const errors = computed(() => {
|
|
|
137
134
|
});
|
|
138
135
|
|
|
139
136
|
const isIncrementDisabled = computed(() => {
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
137
|
+
const r = parsedResult.value;
|
|
138
|
+
|
|
139
|
+
if (props.maxValue !== undefined && r.value !== undefined) {
|
|
140
|
+
return r.value >= props.maxValue;
|
|
143
141
|
}
|
|
142
|
+
|
|
144
143
|
return false;
|
|
145
144
|
});
|
|
146
145
|
|
|
147
146
|
const isDecrementDisabled = computed(() => {
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
147
|
+
const r = parsedResult.value;
|
|
148
|
+
|
|
149
|
+
if (props.minValue !== undefined && r.value !== undefined) {
|
|
150
|
+
return r.value <= props.minValue;
|
|
151
151
|
}
|
|
152
|
+
|
|
152
153
|
return false;
|
|
153
154
|
});
|
|
154
155
|
|
|
@@ -157,7 +158,10 @@ const multiplier = computed(() =>
|
|
|
157
158
|
);
|
|
158
159
|
|
|
159
160
|
function increment() {
|
|
160
|
-
const
|
|
161
|
+
const r = parsedResult.value;
|
|
162
|
+
|
|
163
|
+
const parsedValue = r.value;
|
|
164
|
+
|
|
161
165
|
if (!isIncrementDisabled.value) {
|
|
162
166
|
let nV;
|
|
163
167
|
if (parsedValue === undefined) {
|
|
@@ -172,7 +176,10 @@ function increment() {
|
|
|
172
176
|
}
|
|
173
177
|
|
|
174
178
|
function decrement() {
|
|
175
|
-
const
|
|
179
|
+
const r = parsedResult.value;
|
|
180
|
+
|
|
181
|
+
const parsedValue = r.value;
|
|
182
|
+
|
|
176
183
|
if (!isDecrementDisabled.value) {
|
|
177
184
|
let nV;
|
|
178
185
|
if (parsedValue === undefined) {
|
|
@@ -189,24 +196,26 @@ function decrement() {
|
|
|
189
196
|
function handleKeyPress(e: { code: string; preventDefault(): void }) {
|
|
190
197
|
if (props.updateOnEnterOrClickOutside) {
|
|
191
198
|
if (e.code === 'Escape') {
|
|
192
|
-
|
|
193
|
-
|
|
199
|
+
inputValue.value = modelToString(modelValue.value);
|
|
200
|
+
inputRef.value?.blur();
|
|
194
201
|
}
|
|
195
202
|
if (e.code === 'Enter') {
|
|
196
|
-
|
|
203
|
+
inputRef.value?.blur();
|
|
197
204
|
}
|
|
198
205
|
}
|
|
199
206
|
|
|
200
207
|
if (e.code === 'Enter') {
|
|
201
|
-
|
|
208
|
+
inputValue.value = String(modelValue.value); // to make .1 => 0.1, 10.00 => 10, remove leading zeros etc
|
|
202
209
|
}
|
|
203
210
|
|
|
204
211
|
if (['ArrowDown', 'ArrowUp'].includes(e.code)) {
|
|
205
212
|
e.preventDefault();
|
|
206
213
|
}
|
|
214
|
+
|
|
207
215
|
if (props.useIncrementButtons && e.code === 'ArrowUp') {
|
|
208
216
|
increment();
|
|
209
217
|
}
|
|
218
|
+
|
|
210
219
|
if (props.useIncrementButtons && e.code === 'ArrowDown') {
|
|
211
220
|
decrement();
|
|
212
221
|
}
|
|
@@ -224,15 +233,15 @@ const onMousedown = (ev: MouseEvent) => {
|
|
|
224
233
|
|
|
225
234
|
<template>
|
|
226
235
|
<div
|
|
227
|
-
ref="
|
|
236
|
+
ref="rootRef"
|
|
228
237
|
:class="{ error: !!errors.trim(), disabled: disabled }"
|
|
229
|
-
class="
|
|
238
|
+
class="pl-number-field d-flex-column"
|
|
230
239
|
@keydown="handleKeyPress($event)"
|
|
231
240
|
>
|
|
232
|
-
<div class="
|
|
233
|
-
<DoubleContour class="
|
|
241
|
+
<div class="pl-number-field__main-wrapper d-flex">
|
|
242
|
+
<DoubleContour class="pl-number-field__contour"/>
|
|
234
243
|
<div
|
|
235
|
-
class="
|
|
244
|
+
class="pl-number-field__wrapper flex-grow d-flex flex-align-center"
|
|
236
245
|
:class="{withoutArrows: !useIncrementButtons}"
|
|
237
246
|
>
|
|
238
247
|
<label v-if="label" class="text-description">
|
|
@@ -244,7 +253,7 @@ const onMousedown = (ev: MouseEvent) => {
|
|
|
244
253
|
</PlTooltip>
|
|
245
254
|
</label>
|
|
246
255
|
<input
|
|
247
|
-
ref="
|
|
256
|
+
ref="inputRef"
|
|
248
257
|
v-model="inputValue"
|
|
249
258
|
:disabled="disabled"
|
|
250
259
|
:placeholder="placeholder"
|
|
@@ -253,10 +262,10 @@ const onMousedown = (ev: MouseEvent) => {
|
|
|
253
262
|
@focusout="focused = false; applyChanges()"
|
|
254
263
|
/>
|
|
255
264
|
</div>
|
|
256
|
-
<div v-if="useIncrementButtons" class="
|
|
265
|
+
<div v-if="useIncrementButtons" class="pl-number-field__icons d-flex-column" @mousedown="onMousedown">
|
|
257
266
|
<div
|
|
258
267
|
:class="{ disabled: isIncrementDisabled }"
|
|
259
|
-
class="
|
|
268
|
+
class="pl-number-field__icon d-flex flex-justify-center uc-pointer flex-grow flex-align-center"
|
|
260
269
|
@click="increment"
|
|
261
270
|
>
|
|
262
271
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
|
|
@@ -270,7 +279,7 @@ const onMousedown = (ev: MouseEvent) => {
|
|
|
270
279
|
</div>
|
|
271
280
|
<div
|
|
272
281
|
:class="{ disabled: isDecrementDisabled }"
|
|
273
|
-
class="
|
|
282
|
+
class="pl-number-field__icon d-flex flex-justify-center uc-pointer flex-grow flex-align-center"
|
|
274
283
|
@click="decrement"
|
|
275
284
|
>
|
|
276
285
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
|
|
@@ -284,7 +293,7 @@ const onMousedown = (ev: MouseEvent) => {
|
|
|
284
293
|
</div>
|
|
285
294
|
</div>
|
|
286
295
|
</div>
|
|
287
|
-
<div v-if="errors.trim()" class="
|
|
296
|
+
<div v-if="errors.trim()" class="pl-number-field__error">
|
|
288
297
|
{{ errors }}
|
|
289
298
|
</div>
|
|
290
299
|
</div>
|
|
@@ -29,7 +29,7 @@ describe('NumberInput.vue', () => {
|
|
|
29
29
|
step: 2,
|
|
30
30
|
},
|
|
31
31
|
});
|
|
32
|
-
const incrementButton = wrapper.find('.
|
|
32
|
+
const incrementButton = wrapper.find('.pl-number-field__icons div:first-child');
|
|
33
33
|
await incrementButton.trigger('click');
|
|
34
34
|
expect(wrapper.vm.modelValue).toEqual(12);
|
|
35
35
|
});
|
|
@@ -41,7 +41,7 @@ describe('NumberInput.vue', () => {
|
|
|
41
41
|
step: 1,
|
|
42
42
|
},
|
|
43
43
|
});
|
|
44
|
-
const decrementButton = wrapper.find('.
|
|
44
|
+
const decrementButton = wrapper.find('.pl-number-field__icons div:last-child');
|
|
45
45
|
await decrementButton.trigger('click');
|
|
46
46
|
expect(wrapper.vm.modelValue).toEqual(9);
|
|
47
47
|
});
|
|
@@ -53,7 +53,7 @@ describe('NumberInput.vue', () => {
|
|
|
53
53
|
maxValue: 10,
|
|
54
54
|
},
|
|
55
55
|
});
|
|
56
|
-
const incrementButton = wrapper.find('.
|
|
56
|
+
const incrementButton = wrapper.find('.pl-number-field__icons div:first-child');
|
|
57
57
|
expect(incrementButton.classes()).toContain('disabled');
|
|
58
58
|
});
|
|
59
59
|
|
|
@@ -64,7 +64,7 @@ describe('NumberInput.vue', () => {
|
|
|
64
64
|
minValue: 1,
|
|
65
65
|
},
|
|
66
66
|
});
|
|
67
|
-
const decrementButton = wrapper.find('.
|
|
67
|
+
const decrementButton = wrapper.find('.pl-number-field__icons div:last-child');
|
|
68
68
|
expect(decrementButton.classes()).toContain('disabled');
|
|
69
69
|
});
|
|
70
70
|
|
|
@@ -76,8 +76,8 @@ describe('NumberInput.vue', () => {
|
|
|
76
76
|
errorMessage: 'Custom error message',
|
|
77
77
|
},
|
|
78
78
|
});
|
|
79
|
-
expect(wrapper.find('.
|
|
80
|
-
expect(wrapper.find('.
|
|
79
|
+
expect(wrapper.find('.pl-number-field__error').text()).toContain('Custom error message');
|
|
80
|
+
expect(wrapper.find('.pl-number-field__error').text()).toContain('Value must be higher than 10');
|
|
81
81
|
});
|
|
82
82
|
|
|
83
83
|
it('validates and updates the computedValue when the user types in the input field', async () => {
|
|
@@ -89,7 +89,7 @@ describe('NumberInput.vue', () => {
|
|
|
89
89
|
const input = wrapper.find('input');
|
|
90
90
|
await input.setValue('1.1.1');
|
|
91
91
|
await input.trigger('focusout');
|
|
92
|
-
expect(wrapper.vm.modelValue).toEqual(
|
|
92
|
+
expect(wrapper.vm.modelValue).toEqual(1.1);
|
|
93
93
|
|
|
94
94
|
await input.setValue('15');
|
|
95
95
|
await input.trigger('focusout');
|
|
@@ -97,16 +97,46 @@ describe('NumberInput.vue', () => {
|
|
|
97
97
|
|
|
98
98
|
await input.setValue('.');
|
|
99
99
|
await input.trigger('focusout');
|
|
100
|
-
expect(wrapper.vm.modelValue).toEqual(
|
|
100
|
+
expect(wrapper.vm.modelValue).toEqual(15);
|
|
101
|
+
|
|
102
|
+
await input.setValue('..');
|
|
103
|
+
await input.trigger('focusout');
|
|
104
|
+
expect(wrapper.vm.modelValue).toEqual(15);
|
|
105
|
+
expect(input.element.value).toEqual('.');
|
|
106
|
+
|
|
107
|
+
await input.setValue(',,');
|
|
108
|
+
await input.trigger('focusout');
|
|
109
|
+
expect(wrapper.vm.modelValue).toEqual(15);
|
|
110
|
+
expect(input.element.value).toEqual('.');
|
|
111
|
+
|
|
112
|
+
await input.setValue('-');
|
|
113
|
+
await input.trigger('focusout');
|
|
114
|
+
expect(wrapper.vm.modelValue).toEqual(15);
|
|
115
|
+
expect(input.element.value).toEqual('-');
|
|
116
|
+
|
|
117
|
+
await input.setValue('-a');
|
|
118
|
+
await input.trigger('focusout');
|
|
119
|
+
expect(wrapper.vm.modelValue).toEqual(15);
|
|
120
|
+
expect(input.element.value).toEqual('-');
|
|
121
|
+
|
|
122
|
+
await input.setValue('-1');
|
|
123
|
+
await input.trigger('focusout');
|
|
124
|
+
expect(wrapper.vm.modelValue).toEqual(-1);
|
|
125
|
+
expect(input.element.value).toEqual('-1');
|
|
101
126
|
|
|
102
127
|
await input.setValue(',');
|
|
103
128
|
await input.trigger('focusout');
|
|
104
|
-
expect(wrapper.vm.modelValue).toEqual(
|
|
129
|
+
expect(wrapper.vm.modelValue).toEqual(-1);
|
|
105
130
|
|
|
106
131
|
await input.setValue('1,1');
|
|
107
132
|
await input.trigger('focusout');
|
|
108
133
|
expect(wrapper.vm.modelValue).toEqual(1.1);
|
|
109
134
|
expect(input.element.value).toEqual('1.1');
|
|
135
|
+
|
|
136
|
+
await input.setValue('-1.1');
|
|
137
|
+
await input.trigger('focusout');
|
|
138
|
+
expect(wrapper.vm.modelValue).toEqual(-1.1);
|
|
139
|
+
expect(input.element.value).toEqual('-1.1');
|
|
110
140
|
});
|
|
111
141
|
|
|
112
142
|
it('update model with undefined when input is cleared', async () => {
|
|
@@ -120,4 +150,31 @@ describe('NumberInput.vue', () => {
|
|
|
120
150
|
await input.trigger('focusout');
|
|
121
151
|
expect(wrapper.vm.modelValue).toEqual(undefined);
|
|
122
152
|
});
|
|
153
|
+
|
|
154
|
+
it('external modelValue change', async () => {
|
|
155
|
+
const wrapper = mount(PlNumberField, {
|
|
156
|
+
props: {
|
|
157
|
+
modelValue: 10,
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const input = wrapper.find('input');
|
|
162
|
+
await input.trigger('focusout');
|
|
163
|
+
expect(wrapper.vm.modelValue).toEqual(10);
|
|
164
|
+
expect(input.element.value).toEqual('10');
|
|
165
|
+
|
|
166
|
+
await input.setValue('');
|
|
167
|
+
await input.trigger('focusout');
|
|
168
|
+
expect(wrapper.vm.modelValue).toEqual(undefined);
|
|
169
|
+
|
|
170
|
+
wrapper.setProps({ modelValue: 1 });
|
|
171
|
+
|
|
172
|
+
await input.trigger('focusout');
|
|
173
|
+
expect(wrapper.vm.modelValue).toEqual(1);
|
|
174
|
+
expect(input.element.value).toEqual('1');
|
|
175
|
+
|
|
176
|
+
await input.setValue(10);
|
|
177
|
+
expect(wrapper.vm.modelValue).toEqual(10);
|
|
178
|
+
expect(input.element.value).toEqual('10');
|
|
179
|
+
});
|
|
123
180
|
});
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
type ParseResult = {
|
|
2
|
+
error?: Error;
|
|
3
|
+
value?: number;
|
|
4
|
+
cleanInput: string;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
const NUMBER_REGEX = /^[-−–+]?(\d+)?[.,]?(\d+)?$/; // parseFloat works without errors on strings with multiple dots, or letters in value
|
|
8
|
+
|
|
9
|
+
function isPartial(v: string) {
|
|
10
|
+
return v === '.' || v === ',' || v === '-';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function clearNumericValue(v: string) {
|
|
14
|
+
v = v.trim();
|
|
15
|
+
v = v.replace(',', '.');
|
|
16
|
+
v = v.replace('−', '-'); // minus, replacing for the case of input the whole copied value
|
|
17
|
+
v = v.replace('–', '-'); // dash, replacing for the case of input the whole copied value
|
|
18
|
+
v = v.replace('+', '');
|
|
19
|
+
return v;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function stringToNumber(v: string) {
|
|
23
|
+
return parseFloat(clearNumericValue(v));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function clearInput(v: string): string {
|
|
27
|
+
v = v.trim();
|
|
28
|
+
|
|
29
|
+
if (isPartial(v)) {
|
|
30
|
+
return v;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (/^-[^0-9.]/.test(v)) {
|
|
34
|
+
return '-';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const match = v.match(/^(.*)[.,][^0-9].*$/);
|
|
38
|
+
if (match) {
|
|
39
|
+
return match[1] + '.';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (v.match(NUMBER_REGEX)) {
|
|
43
|
+
return clearNumericValue(v);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const n = stringToNumber(v);
|
|
47
|
+
|
|
48
|
+
return isNaN(n) ? '' : String(+n);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function parseNumber(props: {
|
|
52
|
+
minValue?: number;
|
|
53
|
+
maxValue?: number;
|
|
54
|
+
validate?: (v: number) => string | undefined;
|
|
55
|
+
}, str: string): ParseResult {
|
|
56
|
+
str = str.trim();
|
|
57
|
+
|
|
58
|
+
const cleanInput = clearInput(str);
|
|
59
|
+
|
|
60
|
+
if (str === '') {
|
|
61
|
+
return {
|
|
62
|
+
value: undefined,
|
|
63
|
+
cleanInput,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!str.match(NUMBER_REGEX)) {
|
|
68
|
+
return {
|
|
69
|
+
error: Error('Value is not a number'),
|
|
70
|
+
cleanInput,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (isPartial(str)) {
|
|
75
|
+
return {
|
|
76
|
+
error: Error('Enter a number'),
|
|
77
|
+
cleanInput,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const value = stringToNumber(str);
|
|
82
|
+
|
|
83
|
+
if (isNaN(value)) {
|
|
84
|
+
return {
|
|
85
|
+
error: Error('Value is not a number'),
|
|
86
|
+
cleanInput,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (props.minValue !== undefined && value < props.minValue) {
|
|
91
|
+
return {
|
|
92
|
+
error: Error(`Value must be higher than ${props.minValue}`),
|
|
93
|
+
value,
|
|
94
|
+
cleanInput,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (props.maxValue !== undefined && value > props.maxValue) {
|
|
99
|
+
return {
|
|
100
|
+
error: Error(`Value must be less than ${props.maxValue}`),
|
|
101
|
+
value,
|
|
102
|
+
cleanInput,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (props.validate) {
|
|
107
|
+
const error = props.validate(value);
|
|
108
|
+
if (error) {
|
|
109
|
+
return {
|
|
110
|
+
error: Error(error),
|
|
111
|
+
value,
|
|
112
|
+
cleanInput,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
value,
|
|
119
|
+
cleanInput,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
.
|
|
1
|
+
.pl-number-field {
|
|
2
2
|
--contour-color: var(--txt-01);
|
|
3
3
|
--contour-border-width: 1px;
|
|
4
4
|
--options-bg: #fff;
|
|
@@ -65,6 +65,14 @@
|
|
|
65
65
|
color: var(--color-hint);
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
&__error {
|
|
69
|
+
margin-top: 3px;
|
|
70
|
+
color: var(--txt-error);
|
|
71
|
+
font-size: 12px;
|
|
72
|
+
font-weight: 500;
|
|
73
|
+
line-height: 16px;
|
|
74
|
+
}
|
|
75
|
+
|
|
68
76
|
input {
|
|
69
77
|
outline: none;
|
|
70
78
|
border: none;
|