@milaboratories/uikit 2.10.45 → 2.11.0

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.
Files changed (41) hide show
  1. package/.turbo/turbo-build.log +19 -19
  2. package/.turbo/turbo-formatter$colon$check.log +2 -2
  3. package/.turbo/turbo-linter$colon$check.log +2 -2
  4. package/.turbo/turbo-types$colon$check.log +1 -1
  5. package/CHANGELOG.md +18 -0
  6. package/dist/components/PlNumberField/PlNumberField.js.map +1 -1
  7. package/dist/components/PlNumberField/PlNumberField.vue.d.ts +45 -75
  8. package/dist/components/PlNumberField/PlNumberField.vue.d.ts.map +1 -1
  9. package/dist/components/PlNumberField/PlNumberField.vue2.js +129 -121
  10. package/dist/components/PlNumberField/PlNumberField.vue2.js.map +1 -1
  11. package/dist/components/PlNumberField/__test__/PlNumberField.spec.d.ts.map +1 -0
  12. package/dist/components/PlNumberField/__test__/parseNumber.spec.d.ts +2 -0
  13. package/dist/components/PlNumberField/__test__/parseNumber.spec.d.ts.map +1 -0
  14. package/dist/components/PlNumberField/parseNumber.d.ts +56 -7
  15. package/dist/components/PlNumberField/parseNumber.d.ts.map +1 -1
  16. package/dist/components/PlNumberField/parseNumber.js +40 -56
  17. package/dist/components/PlNumberField/parseNumber.js.map +1 -1
  18. package/dist/components/PlNumberField/pl-number-field.css +1 -1
  19. package/dist/components/PlSearchField/PlSearchField.js.map +1 -1
  20. package/dist/components/PlSearchField/PlSearchField.style.js.map +1 -1
  21. package/dist/components/PlSearchField/PlSearchField.vue.d.ts +20 -32
  22. package/dist/components/PlSearchField/PlSearchField.vue.d.ts.map +1 -1
  23. package/dist/components/PlSearchField/PlSearchField.vue2.js +4 -2
  24. package/dist/components/PlSearchField/PlSearchField.vue2.js.map +1 -1
  25. package/dist/components/PlTextField/PlTextField.js.map +1 -1
  26. package/dist/components/PlTextField/PlTextField.vue.d.ts +46 -118
  27. package/dist/components/PlTextField/PlTextField.vue.d.ts.map +1 -1
  28. package/dist/components/PlTextField/PlTextField.vue2.js +61 -58
  29. package/dist/components/PlTextField/PlTextField.vue2.js.map +1 -1
  30. package/package.json +5 -5
  31. package/src/components/PlNumberField/PlNumberField.vue +151 -143
  32. package/src/components/PlNumberField/__test__/PlNumberField.spec.ts +296 -0
  33. package/src/components/PlNumberField/__test__/parseNumber.spec.ts +204 -0
  34. package/src/components/PlNumberField/parseNumber.ts +125 -98
  35. package/src/components/PlNumberField/pl-number-field.scss +17 -4
  36. package/src/components/PlSearchField/PlSearchField.vue +8 -4
  37. package/src/components/PlTextField/PlTextField.vue +37 -49
  38. package/src/components/PlTextField/__tests__/TextField.spec.ts +2 -2
  39. package/dist/components/PlNumberField/__tests__/PlNumberField.spec.d.ts.map +0 -1
  40. package/src/components/PlNumberField/__tests__/PlNumberField.spec.ts +0 -182
  41. /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.45",
3
+ "version": "2.11.0",
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
- "@milaboratories/helpers": "1.13.7",
35
- "@platforma-sdk/model": "1.59.3"
34
+ "@platforma-sdk/model": "1.60.0",
35
+ "@milaboratories/helpers": "1.14.0"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@vitest/coverage-istanbul": "^4.0.18",
@@ -40,9 +40,9 @@
40
40
  "svgo": "^3.3.2",
41
41
  "typescript": "~5.9.3",
42
42
  "vitest": "^4.0.18",
43
+ "@milaboratories/ts-builder": "1.3.0",
43
44
  "@milaboratories/build-configs": "1.5.2",
44
- "@milaboratories/ts-configs": "1.2.2",
45
- "@milaboratories/ts-builder": "1.3.0"
45
+ "@milaboratories/ts-configs": "1.2.2"
46
46
  },
47
47
  "scripts": {
48
48
  "dev": "ts-builder serve --target browser-lib --build-config ./build.browser-lib.config.js",
@@ -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 setup lang="ts">
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 { parseNumber } from "./parseNumber";
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
- /** Input is disabled if true */
32
- disabled?: boolean;
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
- /** If false - remove buttons on the right */
44
- useIncrementButtons?: boolean;
45
- /** If true - changes do not apply immediately, they apply only by removing focus from the input (by click enter or by click outside) */
46
- updateOnEnterOrClickOutside?: boolean;
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
- function modelToString(v: number | undefined) {
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
- watch(modelValue, (n) => {
97
- const r = parsedResult.value;
98
- if (r.error || n !== r.value) {
99
- resetCachedValue();
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
- const inputValue = computed({
104
- get() {
105
- return cachedValue.value ?? modelToString(modelValue.value);
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
- cachedValue.value = r.cleanInput;
104
+ const result = tryParseNumber(input.value);
105
+ if (result.value !== undefined) {
106
+ modelValue.value = result.value as V;
107
+ }
108
+ }
111
109
 
112
- if (r.error || props.updateOnEnterOrClickOutside) {
113
- inputRef.value!.value = r.cleanInput;
114
- } else {
115
- modelValue.value = r.value;
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
- function applyChanges() {
123
- if (parsedResult.value.error === undefined) {
124
- modelValue.value = parsedResult.value.value;
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 errors = computed(() => {
129
- let ers: string[] = [];
131
+ const error = computed(() => {
132
+ if (props.errorMessage) return props.errorMessage;
130
133
 
131
- if (props.errorMessage) {
132
- ers.push(props.errorMessage);
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
- const r = parsedResult.value;
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
- ers = [...ers];
147
-
148
- return ers.join(" ");
144
+ return undefined;
149
145
  });
150
146
 
151
- const isIncrementDisabled = computed(() => {
152
- const r = parsedResult.value;
147
+ const canShowClearable = computed(
148
+ () =>
149
+ props.clearable &&
150
+ (modelValue.value !== undefined || displayText.value.trim() !== "") &&
151
+ !props.disabled,
152
+ );
153
153
 
154
- if (props.maxValue !== undefined && r.value !== undefined) {
155
- return r.value >= props.maxValue;
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
- return false;
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
- const r = parsedResult.value;
163
-
164
- if (props.minValue !== undefined && r.value !== undefined) {
165
- return r.value <= props.minValue;
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
- const r = parsedResult.value;
175
-
176
- const parsedValue = r.value;
177
-
178
- if (!isIncrementDisabled.value) {
179
- let nV;
180
- if (parsedValue === undefined) {
181
- nV = props.minValue ? props.minValue : 0;
182
- } else {
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
- const r = parsedResult.value;
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
- const parsedValue = r.value;
211
+ modelValue.value = (props.minValue !== undefined ? Math.max(props.minValue, nV) : nV) as V;
212
+ }
194
213
 
195
- if (!isDecrementDisabled.value) {
196
- let nV;
197
- if (parsedValue === undefined) {
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 handleKeyPress(e: { code: string; preventDefault(): void }) {
208
- if (props.updateOnEnterOrClickOutside) {
209
- if (e.code === "Escape") {
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
- inputValue.value = String(modelValue.value); // to make .1 => 0.1, 10.00 => 10, remove leading zeros etc
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.useIncrementButtons && e.code === "ArrowUp") {
233
+ if (!props.disableSteps && e.code === "ArrowUp") {
227
234
  increment();
228
235
  }
229
236
 
230
- if (props.useIncrementButtons && e.code === "ArrowDown") {
237
+ if (!props.disableSteps && e.code === "ArrowDown") {
231
238
  decrement();
232
239
  }
233
240
  }
234
241
 
235
- // https://stackoverflow.com/questions/880512/prevent-text-selection-after-double-click#:~:text=If%20you%20encounter%20a%20situation,none%3B%20to%20the%20summary%20element.
236
- // this prevents selecting of more than input content in some cases,
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: !!errors.trim(), disabled: disabled }"
253
+ :class="{ error: !!error, disabled: disabled }"
249
254
  class="pl-number-field d-flex-column"
250
- @keydown="handleKeyPress($event)"
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
- v-model="inputValue"
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
- @focusin="focused = true"
273
- @focusout="
274
- focused = false;
275
- applyChanges();
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="useIncrementButtons"
288
+ v-if="!props.disableSteps"
281
289
  class="pl-number-field__icons d-flex-column"
282
- @mousedown="onMousedown"
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="errors.trim()" class="pl-number-field__error">
327
- {{ errors }}
334
+ <div v-if="error" class="pl-number-field__error">
335
+ {{ error }}
328
336
  </div>
329
337
  </div>
330
338
  </template>