@milaboratories/uikit 2.10.44 → 2.10.46
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 +15 -15
- package/.turbo/turbo-formatter$colon$check.log +2 -2
- package/.turbo/turbo-linter$colon$check.log +2 -2
- package/.turbo/turbo-types$colon$check.log +1 -1
- package/CHANGELOG.md +12 -0
- package/dist/components/PlNumberField/PlNumberField.js.map +1 -1
- package/dist/components/PlNumberField/PlNumberField.vue.d.ts +45 -75
- package/dist/components/PlNumberField/PlNumberField.vue.d.ts.map +1 -1
- package/dist/components/PlNumberField/PlNumberField.vue2.js +129 -121
- package/dist/components/PlNumberField/PlNumberField.vue2.js.map +1 -1
- package/dist/components/PlNumberField/__test__/PlNumberField.spec.d.ts.map +1 -0
- package/dist/components/PlNumberField/__test__/parseNumber.spec.d.ts +2 -0
- package/dist/components/PlNumberField/__test__/parseNumber.spec.d.ts.map +1 -0
- package/dist/components/PlNumberField/parseNumber.d.ts +56 -7
- package/dist/components/PlNumberField/parseNumber.d.ts.map +1 -1
- package/dist/components/PlNumberField/parseNumber.js +40 -56
- package/dist/components/PlNumberField/parseNumber.js.map +1 -1
- package/dist/components/PlNumberField/pl-number-field.css +1 -1
- package/dist/components/PlSearchField/PlSearchField.js.map +1 -1
- package/dist/components/PlSearchField/PlSearchField.style.js.map +1 -1
- package/dist/components/PlSearchField/PlSearchField.vue.d.ts +20 -32
- package/dist/components/PlSearchField/PlSearchField.vue.d.ts.map +1 -1
- package/dist/components/PlSearchField/PlSearchField.vue2.js +4 -2
- package/dist/components/PlSearchField/PlSearchField.vue2.js.map +1 -1
- package/dist/components/PlTextField/PlTextField.js.map +1 -1
- package/dist/components/PlTextField/PlTextField.vue.d.ts +46 -118
- package/dist/components/PlTextField/PlTextField.vue.d.ts.map +1 -1
- package/dist/components/PlTextField/PlTextField.vue2.js +61 -58
- package/dist/components/PlTextField/PlTextField.vue2.js.map +1 -1
- package/package.json +5 -5
- package/src/components/PlAutocomplete/__tests__/PlAutocomplete.spec.ts +18 -12
- package/src/components/PlAutocompleteMulti/__tests__/PlAutocompleteMulti.spec.ts +16 -12
- package/src/components/PlDropdown/__tests__/PlDropdown.spec.ts +2 -4
- package/src/components/PlDropdownMulti/__tests__/PlDropdownMulti.spec.ts +4 -6
- package/src/components/PlDropdownMultiRef/__tests__/PlDropdownMultiRef.spec.ts +4 -7
- package/src/components/PlDropdownRef/__tests__/PlDropdownRef.spec.ts +2 -6
- package/src/components/PlNumberField/PlNumberField.vue +151 -143
- package/src/components/PlNumberField/__test__/PlNumberField.spec.ts +296 -0
- package/src/components/PlNumberField/__test__/parseNumber.spec.ts +204 -0
- package/src/components/PlNumberField/parseNumber.ts +125 -98
- package/src/components/PlNumberField/pl-number-field.scss +17 -4
- package/src/components/PlSearchField/PlSearchField.vue +8 -4
- package/src/components/PlTextField/PlTextField.vue +37 -49
- package/src/components/PlTextField/__tests__/TextField.spec.ts +2 -2
- package/dist/components/PlNumberField/__tests__/PlNumberField.spec.d.ts.map +0 -1
- package/src/components/PlNumberField/__tests__/PlNumberField.spec.ts +0 -182
- /package/dist/components/PlNumberField/{__tests__ → __test__}/PlNumberField.spec.d.ts +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@milaboratories/uikit",
|
|
3
|
-
"version": "2.10.
|
|
3
|
+
"version": "2.10.46",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -31,8 +31,8 @@
|
|
|
31
31
|
"resize-observer-polyfill": "^1.5.1",
|
|
32
32
|
"sortablejs": "^1.15.6",
|
|
33
33
|
"vue": "^3.5.24",
|
|
34
|
-
"@
|
|
35
|
-
"@
|
|
34
|
+
"@platforma-sdk/model": "1.59.3",
|
|
35
|
+
"@milaboratories/helpers": "1.13.7"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@vitest/coverage-istanbul": "^4.0.18",
|
|
@@ -41,8 +41,8 @@
|
|
|
41
41
|
"typescript": "~5.9.3",
|
|
42
42
|
"vitest": "^4.0.18",
|
|
43
43
|
"@milaboratories/ts-builder": "1.3.0",
|
|
44
|
-
"@milaboratories/
|
|
45
|
-
"@milaboratories/
|
|
44
|
+
"@milaboratories/ts-configs": "1.2.2",
|
|
45
|
+
"@milaboratories/build-configs": "1.5.2"
|
|
46
46
|
},
|
|
47
47
|
"scripts": {
|
|
48
48
|
"dev": "ts-builder serve --target browser-lib --build-config ./build.browser-lib.config.js",
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
2
|
|
|
3
|
-
import { mount } from "@vue/test-utils";
|
|
3
|
+
import { mount, flushPromises } from "@vue/test-utils";
|
|
4
4
|
import PlAutocomplete from "../PlAutocomplete.vue";
|
|
5
|
-
import { delay } from "@milaboratories/helpers";
|
|
6
5
|
|
|
7
6
|
describe("PlAutocomplete", () => {
|
|
7
|
+
beforeEach(() => vi.useFakeTimers());
|
|
8
|
+
afterEach(() => vi.useRealTimers());
|
|
9
|
+
|
|
8
10
|
it("modelValue", async () => {
|
|
9
11
|
const options = [
|
|
10
12
|
{ text: "Option 1", value: 1 },
|
|
@@ -20,21 +22,25 @@ describe("PlAutocomplete", () => {
|
|
|
20
22
|
},
|
|
21
23
|
});
|
|
22
24
|
|
|
23
|
-
|
|
25
|
+
// Flush initial watch (immediate: true) that fires on mount
|
|
26
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
27
|
+
await flushPromises();
|
|
28
|
+
|
|
24
29
|
await wrapper.find(".pl-autocomplete__envelope").trigger("click");
|
|
25
30
|
await wrapper.find("input").trigger("focus");
|
|
26
31
|
await wrapper.find("input").setValue("option");
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
...document.body.querySelectorAll(".dropdown-list-item"),
|
|
31
|
-
] as HTMLElement[];
|
|
32
|
+
// Flush 300ms debounce + optionsSearch promise
|
|
33
|
+
await vi.advanceTimersByTimeAsync(300);
|
|
34
|
+
await flushPromises();
|
|
32
35
|
|
|
33
|
-
|
|
36
|
+
const getOptions = () =>
|
|
37
|
+
[...document.body.querySelectorAll(".dropdown-list-item")] as HTMLElement[];
|
|
34
38
|
|
|
35
|
-
|
|
39
|
+
expect(getOptions().length).toBe(2);
|
|
36
40
|
|
|
37
|
-
|
|
41
|
+
getOptions()[1].click();
|
|
42
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
43
|
+
await flushPromises();
|
|
38
44
|
|
|
39
45
|
expect(wrapper.props("modelValue")).toBe(2);
|
|
40
46
|
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
2
|
|
|
3
|
-
import { mount } from "@vue/test-utils";
|
|
3
|
+
import { mount, flushPromises } from "@vue/test-utils";
|
|
4
4
|
import PlAutocompleteMulti from "../PlAutocompleteMulti.vue";
|
|
5
|
-
import { delay } from "@milaboratories/helpers";
|
|
6
5
|
|
|
7
6
|
describe("PlAutocompleteMulti", () => {
|
|
7
|
+
beforeEach(() => vi.useFakeTimers());
|
|
8
|
+
afterEach(() => vi.useRealTimers());
|
|
9
|
+
|
|
8
10
|
it("modelValue", async () => {
|
|
9
11
|
const wrapper = mount(PlAutocompleteMulti, {
|
|
10
12
|
props: {
|
|
@@ -20,22 +22,24 @@ describe("PlAutocompleteMulti", () => {
|
|
|
20
22
|
},
|
|
21
23
|
});
|
|
22
24
|
|
|
23
|
-
|
|
25
|
+
// Flush initial watch (immediate: true) that fires on mount
|
|
26
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
27
|
+
await flushPromises();
|
|
28
|
+
|
|
24
29
|
await wrapper.find(".pl-autocomplete-multi__envelope").trigger("click");
|
|
25
30
|
await wrapper.find("input").trigger("focus");
|
|
31
|
+
// Flush debounce (0ms) + optionsSearch promise
|
|
32
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
33
|
+
await flushPromises();
|
|
26
34
|
|
|
27
35
|
const getOptions = () =>
|
|
28
36
|
[...document.body.querySelectorAll(".dropdown-list-item")] as HTMLElement[];
|
|
29
37
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const options = getOptions();
|
|
33
|
-
|
|
34
|
-
expect(options.length).toBe(2);
|
|
35
|
-
|
|
36
|
-
options[1].click();
|
|
38
|
+
expect(getOptions().length).toBe(2);
|
|
37
39
|
|
|
38
|
-
|
|
40
|
+
getOptions()[1].click();
|
|
41
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
42
|
+
await flushPromises();
|
|
39
43
|
|
|
40
44
|
expect(wrapper.props("modelValue")).toEqual([1, 2]);
|
|
41
45
|
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
2
|
|
|
3
|
-
import { mount } from "@vue/test-utils";
|
|
3
|
+
import { mount, flushPromises } from "@vue/test-utils";
|
|
4
4
|
import PlDropdown from "../PlDropdown.vue";
|
|
5
|
-
import { delay } from "@milaboratories/helpers";
|
|
6
5
|
|
|
7
6
|
describe("PlDropdown", () => {
|
|
8
7
|
it("modelValue", async () => {
|
|
@@ -24,8 +23,7 @@ describe("PlDropdown", () => {
|
|
|
24
23
|
expect(options.length).toBe(2);
|
|
25
24
|
|
|
26
25
|
options[1].click();
|
|
27
|
-
|
|
28
|
-
await delay(20);
|
|
26
|
+
await flushPromises();
|
|
29
27
|
|
|
30
28
|
expect(wrapper.props("modelValue")).toBe(2);
|
|
31
29
|
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
2
|
|
|
3
|
-
import { mount } from "@vue/test-utils";
|
|
4
|
-
import
|
|
5
|
-
import { delay } from "@milaboratories/helpers";
|
|
3
|
+
import { mount, flushPromises } from "@vue/test-utils";
|
|
4
|
+
import PlDropdownMulti from "../PlDropdownMulti.vue";
|
|
6
5
|
|
|
7
6
|
describe("PlDropdownMulti", () => {
|
|
8
7
|
it("modelValue", async () => {
|
|
9
|
-
const wrapper = mount(
|
|
8
|
+
const wrapper = mount(PlDropdownMulti, {
|
|
10
9
|
props: {
|
|
11
10
|
modelValue: [1],
|
|
12
11
|
"onUpdate:modelValue": (e) => wrapper.setProps({ modelValue: e }),
|
|
@@ -27,8 +26,7 @@ describe("PlDropdownMulti", () => {
|
|
|
27
26
|
expect(options.length).toBe(2);
|
|
28
27
|
|
|
29
28
|
options[1].click();
|
|
30
|
-
|
|
31
|
-
await delay(20);
|
|
29
|
+
await flushPromises();
|
|
32
30
|
|
|
33
31
|
expect(wrapper.props("modelValue")).toEqual([1, 2]);
|
|
34
32
|
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
2
|
|
|
3
|
-
import { mount } from "@vue/test-utils";
|
|
4
|
-
import
|
|
5
|
-
import { delay } from "@milaboratories/helpers";
|
|
3
|
+
import { mount, flushPromises } from "@vue/test-utils";
|
|
4
|
+
import PlDropdownMultiRef from "../PlDropdownMultiRef.vue";
|
|
6
5
|
|
|
7
6
|
describe("PlDropdownMultiRef", () => {
|
|
8
7
|
it("modelValue", async () => {
|
|
9
|
-
const wrapper = mount(
|
|
8
|
+
const wrapper = mount(PlDropdownMultiRef, {
|
|
10
9
|
props: {
|
|
11
10
|
modelValue: [
|
|
12
11
|
{
|
|
@@ -46,10 +45,8 @@ describe("PlDropdownMultiRef", () => {
|
|
|
46
45
|
|
|
47
46
|
expect(options.length).toBe(2);
|
|
48
47
|
|
|
49
|
-
// console.log(wrapper.props('modelValue'), 'mv');
|
|
50
48
|
options[0].click();
|
|
51
|
-
|
|
52
|
-
await delay(20);
|
|
49
|
+
await flushPromises();
|
|
53
50
|
|
|
54
51
|
expect(wrapper.props("modelValue")).toEqual([
|
|
55
52
|
{
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
2
|
|
|
3
|
-
import { mount } from "@vue/test-utils";
|
|
3
|
+
import { mount, flushPromises } from "@vue/test-utils";
|
|
4
4
|
import PlDropdownRef from "../PlDropdownRef.vue";
|
|
5
|
-
import { delay } from "@milaboratories/helpers";
|
|
6
5
|
|
|
7
6
|
describe("PlDropdownRef", () => {
|
|
8
7
|
it("modelValue", async () => {
|
|
@@ -41,11 +40,8 @@ describe("PlDropdownRef", () => {
|
|
|
41
40
|
|
|
42
41
|
expect(options.length).toBe(2);
|
|
43
42
|
|
|
44
|
-
expect(options.length).toBe(2);
|
|
45
|
-
|
|
46
43
|
options[1].click();
|
|
47
|
-
|
|
48
|
-
await delay(20);
|
|
44
|
+
await flushPromises();
|
|
49
45
|
|
|
50
46
|
expect(wrapper.props("modelValue")).toStrictEqual({
|
|
51
47
|
__isRef: true as const,
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
* <PlNumberField
|
|
10
10
|
* v-model="evenNumber"
|
|
11
11
|
* :validate="(v) => v % 2 !== 0 ? 'Number must be even' : undefined"
|
|
12
|
-
* :update-on-enter-or-click-outside="true"
|
|
13
12
|
* label="Even Number"
|
|
14
13
|
* />
|
|
15
14
|
*/
|
|
@@ -18,18 +17,35 @@ export default {
|
|
|
18
17
|
};
|
|
19
18
|
</script>
|
|
20
19
|
|
|
21
|
-
<script
|
|
20
|
+
<script
|
|
21
|
+
setup
|
|
22
|
+
lang="ts"
|
|
23
|
+
generic="
|
|
24
|
+
R extends true | false,
|
|
25
|
+
V extends undefined | number,
|
|
26
|
+
C extends Exclude<V, R extends true ? undefined : never>
|
|
27
|
+
"
|
|
28
|
+
>
|
|
22
29
|
import "./pl-number-field.scss";
|
|
23
30
|
import DoubleContour from "../../utils/DoubleContour.vue";
|
|
24
31
|
import { useLabelNotch } from "../../utils/useLabelNotch";
|
|
25
32
|
import { computed, ref, useSlots, watch } from "vue";
|
|
26
33
|
import { PlTooltip } from "../PlTooltip";
|
|
27
|
-
import {
|
|
34
|
+
import { PlIcon16 } from "../PlIcon16";
|
|
35
|
+
import { tryParseNumber, numberToDecimalString, validateNumber } from "./parseNumber";
|
|
36
|
+
|
|
37
|
+
const modelValue = defineModel<V>({ required: true });
|
|
38
|
+
|
|
39
|
+
const emit = defineEmits<{
|
|
40
|
+
blur: [value: V];
|
|
41
|
+
focus: [value: V];
|
|
42
|
+
enter: [value: V];
|
|
43
|
+
}>();
|
|
28
44
|
|
|
29
45
|
const props = withDefaults(
|
|
30
46
|
defineProps<{
|
|
31
|
-
/**
|
|
32
|
-
|
|
47
|
+
/** If `true`, the field is required and will show an error if left empty. */
|
|
48
|
+
required?: R;
|
|
33
49
|
/** Label on the top border of the field, empty by default */
|
|
34
50
|
label?: string;
|
|
35
51
|
/** Input placeholder, empty by default */
|
|
@@ -40,14 +56,16 @@ const props = withDefaults(
|
|
|
40
56
|
minValue?: number;
|
|
41
57
|
/** If defined - show an error if value is higher */
|
|
42
58
|
maxValue?: number;
|
|
43
|
-
/**
|
|
44
|
-
|
|
45
|
-
/** If true -
|
|
46
|
-
|
|
59
|
+
/** Input is disabled if true */
|
|
60
|
+
disabled?: boolean;
|
|
61
|
+
/** If true - remove buttons on the right */
|
|
62
|
+
disableSteps?: boolean;
|
|
47
63
|
/** Error message that shows always when it's provided, without other checks */
|
|
48
64
|
errorMessage?: string;
|
|
49
65
|
/** Additional validity check for input value that must return an error text if failed */
|
|
50
66
|
validate?: (v: number) => string | undefined;
|
|
67
|
+
/** If `true`, shows a clear button that resets value to `undefined`. If a function, calls it to get the reset value. */
|
|
68
|
+
clearable?: (R extends true ? never : boolean) | (() => C);
|
|
51
69
|
/** Makes some of corners not rounded */
|
|
52
70
|
groupPosition?:
|
|
53
71
|
| "top"
|
|
@@ -60,201 +78,185 @@ const props = withDefaults(
|
|
|
60
78
|
| "bottom-right"
|
|
61
79
|
| "middle";
|
|
62
80
|
}>(),
|
|
63
|
-
{
|
|
64
|
-
step: 1,
|
|
65
|
-
label: undefined,
|
|
66
|
-
placeholder: undefined,
|
|
67
|
-
minValue: undefined,
|
|
68
|
-
maxValue: undefined,
|
|
69
|
-
useIncrementButtons: true,
|
|
70
|
-
updateOnEnter: false,
|
|
71
|
-
errorMessage: undefined,
|
|
72
|
-
validate: undefined,
|
|
73
|
-
groupPosition: undefined,
|
|
74
|
-
},
|
|
81
|
+
{ step: 1 },
|
|
75
82
|
);
|
|
76
83
|
|
|
77
|
-
const modelValue = defineModel<number | undefined>({ required: true });
|
|
78
|
-
|
|
79
84
|
const slots = useSlots();
|
|
80
85
|
|
|
81
86
|
const rootRef = ref<HTMLElement>();
|
|
82
|
-
const inputRef = ref<HTMLInputElement>();
|
|
83
87
|
|
|
84
88
|
useLabelNotch(rootRef);
|
|
85
89
|
|
|
86
|
-
|
|
87
|
-
return v === undefined ? "" : String(+v); // (+v) to avoid staying in input non-number values if they are provided in model
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const parsedResult = computed(() => parseNumber(props, inputValue.value));
|
|
91
|
-
|
|
92
|
-
const cachedValue = ref<string | undefined>(undefined);
|
|
93
|
-
|
|
94
|
-
const resetCachedValue = () => (cachedValue.value = undefined);
|
|
90
|
+
const displayText = ref(numberToDecimalString(modelValue.value));
|
|
95
91
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
92
|
+
// Sync display when model changes externally (parent, increment/decrement).
|
|
93
|
+
// Skip if the current input already represents the same value.
|
|
94
|
+
watch(modelValue, (newVal) => {
|
|
95
|
+
const parsed = tryParseNumber(displayText.value);
|
|
96
|
+
if (parsed.value === newVal) return;
|
|
97
|
+
displayText.value = numberToDecimalString(newVal);
|
|
101
98
|
});
|
|
102
99
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
},
|
|
107
|
-
set(nextValue: string) {
|
|
108
|
-
const r = parseNumber(props, nextValue);
|
|
100
|
+
function handleInput(event: Event) {
|
|
101
|
+
const input = event.target as HTMLInputElement;
|
|
102
|
+
displayText.value = input.value;
|
|
109
103
|
|
|
110
|
-
|
|
104
|
+
const result = tryParseNumber(input.value);
|
|
105
|
+
if (result.value !== undefined) {
|
|
106
|
+
modelValue.value = result.value as V;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
111
109
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
110
|
+
// On Enter or blur: if parseable, replace display with canonical decimal string.
|
|
111
|
+
// Converts exponential (1e-5) to plain form (0.00001).
|
|
112
|
+
function commitValue() {
|
|
113
|
+
const text = displayText.value.trim();
|
|
114
|
+
const result = tryParseNumber(text);
|
|
115
|
+
|
|
116
|
+
// Empty or partial (-, ., -.) → clear display and reset model for non-required fields
|
|
117
|
+
if (text === "" || (result.value === undefined && result.error === undefined)) {
|
|
118
|
+
displayText.value = "";
|
|
119
|
+
if (!props.required) {
|
|
120
|
+
modelValue.value = undefined as V;
|
|
116
121
|
}
|
|
117
|
-
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const focused = ref(false);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
121
124
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
+
if (result.value !== undefined) {
|
|
126
|
+
modelValue.value = result.value as V;
|
|
127
|
+
displayText.value = numberToDecimalString(result.value);
|
|
125
128
|
}
|
|
126
129
|
}
|
|
127
130
|
|
|
128
|
-
const
|
|
129
|
-
|
|
131
|
+
const error = computed(() => {
|
|
132
|
+
if (props.errorMessage) return props.errorMessage;
|
|
130
133
|
|
|
131
|
-
|
|
132
|
-
|
|
134
|
+
const result = tryParseNumber(displayText.value);
|
|
135
|
+
if (result.error) return result.error;
|
|
136
|
+
if (result.value !== undefined) {
|
|
137
|
+
return validateNumber(result.value, props);
|
|
133
138
|
}
|
|
134
139
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
if (r.error) {
|
|
138
|
-
ers.push(r.error.message);
|
|
139
|
-
} else if (props.validate && r.value !== undefined) {
|
|
140
|
-
const error = props.validate(r.value);
|
|
141
|
-
if (error) {
|
|
142
|
-
ers.push(error);
|
|
143
|
-
}
|
|
140
|
+
if (props.required && displayText.value.trim() === "") {
|
|
141
|
+
return "Value is required";
|
|
144
142
|
}
|
|
145
143
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
return ers.join(" ");
|
|
144
|
+
return undefined;
|
|
149
145
|
});
|
|
150
146
|
|
|
151
|
-
const
|
|
152
|
-
|
|
147
|
+
const canShowClearable = computed(
|
|
148
|
+
() =>
|
|
149
|
+
props.clearable &&
|
|
150
|
+
(modelValue.value !== undefined || displayText.value.trim() !== "") &&
|
|
151
|
+
!props.disabled,
|
|
152
|
+
);
|
|
153
153
|
|
|
154
|
-
|
|
155
|
-
|
|
154
|
+
function clear() {
|
|
155
|
+
if (typeof props.clearable === "function") {
|
|
156
|
+
modelValue.value = props.clearable();
|
|
157
|
+
displayText.value = numberToDecimalString(modelValue.value);
|
|
158
|
+
} else {
|
|
159
|
+
modelValue.value = undefined as V;
|
|
160
|
+
displayText.value = "";
|
|
156
161
|
}
|
|
162
|
+
}
|
|
157
163
|
|
|
158
|
-
|
|
164
|
+
const isIncrementDisabled = computed(() => {
|
|
165
|
+
if (error.value) return true;
|
|
166
|
+
return (
|
|
167
|
+
props.maxValue !== undefined &&
|
|
168
|
+
modelValue.value !== undefined &&
|
|
169
|
+
modelValue.value >= props.maxValue
|
|
170
|
+
);
|
|
159
171
|
});
|
|
160
172
|
|
|
161
173
|
const isDecrementDisabled = computed(() => {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
return false;
|
|
174
|
+
if (error.value) return true;
|
|
175
|
+
return (
|
|
176
|
+
props.minValue !== undefined &&
|
|
177
|
+
modelValue.value !== undefined &&
|
|
178
|
+
modelValue.value <= props.minValue
|
|
179
|
+
);
|
|
169
180
|
});
|
|
170
181
|
|
|
171
182
|
const multiplier = computed(() => 10 ** (props.step.toString().split(".").at(1)?.length ?? 0));
|
|
172
183
|
|
|
173
184
|
function increment() {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
nV =
|
|
184
|
-
((parsedValue || 0) * multiplier.value + props.step * multiplier.value) / multiplier.value;
|
|
185
|
-
}
|
|
186
|
-
modelValue.value = props.maxValue !== undefined ? Math.min(props.maxValue, nV) : nV;
|
|
185
|
+
if (isIncrementDisabled.value) return;
|
|
186
|
+
|
|
187
|
+
let nV: number;
|
|
188
|
+
if (modelValue.value === undefined) {
|
|
189
|
+
nV = props.minValue ?? 0;
|
|
190
|
+
} else {
|
|
191
|
+
nV =
|
|
192
|
+
((modelValue.value || 0) * multiplier.value + props.step * multiplier.value) /
|
|
193
|
+
multiplier.value;
|
|
187
194
|
}
|
|
195
|
+
|
|
196
|
+
modelValue.value = (props.maxValue !== undefined ? Math.min(props.maxValue, nV) : nV) as V;
|
|
188
197
|
}
|
|
189
198
|
|
|
190
199
|
function decrement() {
|
|
191
|
-
|
|
200
|
+
if (isDecrementDisabled.value) return;
|
|
201
|
+
|
|
202
|
+
let nV: number;
|
|
203
|
+
if (modelValue.value === undefined) {
|
|
204
|
+
nV = 0;
|
|
205
|
+
} else {
|
|
206
|
+
nV =
|
|
207
|
+
((modelValue.value || 0) * multiplier.value - props.step * multiplier.value) /
|
|
208
|
+
multiplier.value;
|
|
209
|
+
}
|
|
192
210
|
|
|
193
|
-
|
|
211
|
+
modelValue.value = (props.minValue !== undefined ? Math.max(props.minValue, nV) : nV) as V;
|
|
212
|
+
}
|
|
194
213
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
nV = 0;
|
|
199
|
-
} else {
|
|
200
|
-
nV =
|
|
201
|
-
((parsedValue || 0) * multiplier.value - props.step * multiplier.value) / multiplier.value;
|
|
202
|
-
}
|
|
203
|
-
modelValue.value = props.minValue !== undefined ? Math.max(props.minValue, nV) : nV;
|
|
204
|
-
}
|
|
214
|
+
function handleBlur() {
|
|
215
|
+
commitValue();
|
|
216
|
+
emit("blur", modelValue.value);
|
|
205
217
|
}
|
|
206
218
|
|
|
207
|
-
function
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
inputValue.value = modelToString(modelValue.value);
|
|
211
|
-
inputRef.value?.blur();
|
|
212
|
-
}
|
|
213
|
-
if (e.code === "Enter") {
|
|
214
|
-
inputRef.value?.blur();
|
|
215
|
-
}
|
|
216
|
-
}
|
|
219
|
+
function handleFocus() {
|
|
220
|
+
emit("focus", modelValue.value);
|
|
221
|
+
}
|
|
217
222
|
|
|
223
|
+
function handleKeyDown(e: KeyboardEvent) {
|
|
218
224
|
if (e.code === "Enter") {
|
|
219
|
-
|
|
225
|
+
commitValue();
|
|
226
|
+
emit("enter", modelValue.value);
|
|
220
227
|
}
|
|
221
228
|
|
|
222
229
|
if (["ArrowDown", "ArrowUp"].includes(e.code)) {
|
|
223
230
|
e.preventDefault();
|
|
224
231
|
}
|
|
225
232
|
|
|
226
|
-
if (props.
|
|
233
|
+
if (!props.disableSteps && e.code === "ArrowUp") {
|
|
227
234
|
increment();
|
|
228
235
|
}
|
|
229
236
|
|
|
230
|
-
if (props.
|
|
237
|
+
if (!props.disableSteps && e.code === "ArrowDown") {
|
|
231
238
|
decrement();
|
|
232
239
|
}
|
|
233
240
|
}
|
|
234
241
|
|
|
235
|
-
//
|
|
236
|
-
|
|
237
|
-
// but also disable selecting input content by double-click (useful feature)
|
|
238
|
-
const onMousedown = (ev: MouseEvent) => {
|
|
242
|
+
// Prevent selecting beyond input content on triple-click etc.
|
|
243
|
+
function handleMousedown(ev: MouseEvent) {
|
|
239
244
|
if (ev.detail > 1) {
|
|
240
245
|
ev.preventDefault();
|
|
241
246
|
}
|
|
242
|
-
}
|
|
247
|
+
}
|
|
243
248
|
</script>
|
|
244
249
|
|
|
245
250
|
<template>
|
|
246
251
|
<div
|
|
247
252
|
ref="rootRef"
|
|
248
|
-
:class="{ error: !!
|
|
253
|
+
:class="{ error: !!error, disabled: disabled }"
|
|
249
254
|
class="pl-number-field d-flex-column"
|
|
250
|
-
@keydown="
|
|
255
|
+
@keydown="handleKeyDown"
|
|
251
256
|
>
|
|
252
257
|
<div class="pl-number-field__main-wrapper d-flex">
|
|
253
258
|
<DoubleContour class="pl-number-field__contour" :group-position="groupPosition" />
|
|
254
|
-
<div
|
|
255
|
-
class="pl-number-field__wrapper flex-grow d-flex flex-align-center"
|
|
256
|
-
:class="{ withoutArrows: !useIncrementButtons }"
|
|
257
|
-
>
|
|
259
|
+
<div class="pl-number-field__wrapper flex-grow d-flex flex-align-center">
|
|
258
260
|
<label v-if="label" class="text-description">
|
|
259
261
|
{{ label }}
|
|
260
262
|
<PlTooltip v-if="slots.tooltip" class="info" position="top">
|
|
@@ -265,21 +267,27 @@ const onMousedown = (ev: MouseEvent) => {
|
|
|
265
267
|
</label>
|
|
266
268
|
<input
|
|
267
269
|
ref="inputRef"
|
|
268
|
-
|
|
270
|
+
type="text"
|
|
271
|
+
inputmode="numeric"
|
|
272
|
+
:value="displayText"
|
|
269
273
|
:disabled="disabled"
|
|
270
274
|
:placeholder="placeholder"
|
|
271
275
|
class="text-s flex-grow"
|
|
272
|
-
@
|
|
273
|
-
@focusout="
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
276
|
+
@input="handleInput"
|
|
277
|
+
@focusout="handleBlur"
|
|
278
|
+
@focusin="handleFocus"
|
|
279
|
+
/>
|
|
280
|
+
<PlIcon16
|
|
281
|
+
v-if="canShowClearable"
|
|
282
|
+
class="pl-number-field__clearable"
|
|
283
|
+
name="delete-clear"
|
|
284
|
+
@click.stop="clear"
|
|
277
285
|
/>
|
|
278
286
|
</div>
|
|
279
287
|
<div
|
|
280
|
-
v-if="
|
|
288
|
+
v-if="!props.disableSteps"
|
|
281
289
|
class="pl-number-field__icons d-flex-column"
|
|
282
|
-
@mousedown="
|
|
290
|
+
@mousedown="handleMousedown"
|
|
283
291
|
>
|
|
284
292
|
<div
|
|
285
293
|
:class="{ disabled: isIncrementDisabled }"
|
|
@@ -323,8 +331,8 @@ const onMousedown = (ev: MouseEvent) => {
|
|
|
323
331
|
</div>
|
|
324
332
|
</div>
|
|
325
333
|
</div>
|
|
326
|
-
<div v-if="
|
|
327
|
-
{{
|
|
334
|
+
<div v-if="error" class="pl-number-field__error">
|
|
335
|
+
{{ error }}
|
|
328
336
|
</div>
|
|
329
337
|
</div>
|
|
330
338
|
</template>
|