@v1nt1248/3nclient-lib 0.1.79 → 0.1.81

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
@@ -2,7 +2,7 @@
2
2
  "name": "@v1nt1248/3nclient-lib",
3
3
  "license": "AGPL-3.0-or-later",
4
4
  "author": "v1nt1248",
5
- "version": "0.1.79",
5
+ "version": "0.1.81",
6
6
  "description": "Library for 3NWeb clients",
7
7
  "type": "module",
8
8
  "files": [
@@ -20,6 +20,8 @@ export interface Ui3nAutocompleteProps<T extends Ui3nAutocompleteOptionBase> {
20
20
  itemTitle?: keyof T;
21
21
  itemValue?: keyof T;
22
22
  modelValue: T[] | Array<T[keyof T]>;
23
+ addNewValue?: boolean;
24
+ newValueValidator?: (value: string) => boolean;
23
25
  multiple?: boolean;
24
26
  noDataText?: string;
25
27
  placeholder?: string;
@@ -30,6 +32,7 @@ export interface Ui3nAutocompleteEmits<T extends Ui3nAutocompleteOptionBase> {
30
32
  (event: 'update:modelValue', value: T[] | Array<T[keyof T]>): void;
31
33
  (event: 'update:search', value: string): void;
32
34
  (event: 'update:focused', value: boolean): void;
35
+ (event: 'valid:new-value', value: boolean): void;
33
36
  }
34
37
 
35
38
  export interface Ui3nAutocompleteSlots<T extends Ui3nAutocompleteOptionBase> {
@@ -22,7 +22,6 @@
22
22
  import Ui3nChip from '../ui3n-chip/ui3n-chip.vue';
23
23
  import Ui3nMenu from '../ui3n-menu/ui3n-menu.vue';
24
24
  import Ui3nHtml from '@/directives/ui3n-html';
25
- import Ui3nClickOutside from '@/directives/ui3n-click-outside';
26
25
  import { markSearch, getRandomId } from '@/utils';
27
26
  import type { Nullable } from '@/components/types';
28
27
  import {
@@ -33,12 +32,10 @@
33
32
  } from './types';
34
33
 
35
34
  const vUi3nHtml = Ui3nHtml;
36
- const vUi3nClickOutside = Ui3nClickOutside;
37
35
 
38
36
  const props = withDefaults(defineProps<Ui3nAutocompleteProps<T>>(), {
39
37
  clearOnSelect: false,
40
38
  filterKeys: () => [],
41
- filterMode: 'every',
42
39
  hideSelected: false,
43
40
  items: () => [],
44
41
  itemTitle: 'name',
@@ -55,6 +52,7 @@
55
52
  const inputEl = ref<Nullable<HTMLInputElement>>(null);
56
53
  const menuBodyEl = ref<Nullable<HTMLDivElement>>(null);
57
54
  const activeItemsIndex = ref<Nullable<number>>(null);
55
+ const isNewValueValid = ref(true);
58
56
 
59
57
  const ids = computed(() => props.returnObject
60
58
  ? (props.modelValue as T[]).map(item => item.id)
@@ -99,9 +97,13 @@
99
97
  }
100
98
 
101
99
  function onBlur() {
102
- emits('update:focused', false)
100
+ emits('update:focused', false);
103
101
  activeItemsIndex.value = null;
104
- query.value = '';
102
+
103
+ setTimeout(() => {
104
+ isMenuOpen.value = false;
105
+ query.value = '';
106
+ }, 150);
105
107
  }
106
108
 
107
109
  function onItemClick(item: T) {
@@ -165,7 +167,7 @@
165
167
  }
166
168
 
167
169
  function handlePressingEscOrTabKeys() {
168
- emits('update:focused', false)
170
+ emits('update:focused', false);
169
171
  isMenuOpen.value = false;
170
172
  activeItemsIndex.value = null;
171
173
  query.value = '';
@@ -174,21 +176,35 @@
174
176
  }
175
177
 
176
178
  function handlePressingEnterKey() {
177
- if (activeItemsIndex.value === null) {
179
+ if (activeItemsIndex.value === null && !isMenuOpen.value) {
178
180
  isMenuOpen.value = true;
179
181
  activeItemsIndex.value = 0;
180
182
  menuBodyEl.value && menuBodyEl.value.focus();
181
183
  return;
182
184
  }
183
185
 
184
- const item = filteredItems.value[activeItemsIndex.value!];
185
- onItemClick(item);
186
- activeItemsIndex.value = null;
186
+ if (activeItemsIndex.value === null && isMenuOpen.value && query.value) {
187
+ isNewValueValid.value = !props.newValueValidator ? true : props.newValueValidator(query.value);
188
+ emits('valid:new-value', isNewValueValid.value);
189
+ if (isNewValueValid.value) {
190
+ const item = {
191
+ id: getRandomId(6),
192
+ [props.itemTitle]: query.value,
193
+ [props.itemValue]: query.value,
194
+ };
195
+ onItemClick(item as unknown as T);
196
+ }
197
+ return;
198
+ }
199
+
200
+ if (activeItemsIndex.value !== null) {
201
+ const item = filteredItems.value[activeItemsIndex.value!];
202
+ onItemClick(item);
203
+ activeItemsIndex.value = null;
204
+ }
187
205
  }
188
206
 
189
207
  function onKeydown(event: KeyboardEvent, key: 'down' | 'up' | 'esc' | 'enter' | 'tab') {
190
- if (isEmpty(filteredItems.value)) return;
191
-
192
208
  switch (key) {
193
209
  case 'down':
194
210
  event.preventDefault();
@@ -212,16 +228,16 @@
212
228
  }
213
229
  }
214
230
 
215
- function onClickOutside() {
216
- handlePressingEscOrTabKeys();
217
- }
218
-
219
231
  watch(
220
232
  () => size(filteredItems.value),
221
233
  (val, oVal) => {
222
234
  if (val !== oVal) {
223
235
  activeItemsIndex.value = null;
224
236
  }
237
+
238
+ if (val && !isNewValueValid.value) {
239
+ isNewValueValid.value = true;
240
+ }
225
241
  },
226
242
  );
227
243
  </script>
@@ -252,7 +268,8 @@
252
268
  ref="inputEl"
253
269
  v-model="query"
254
270
  type="text"
255
- :class="$style.input"
271
+ :class="[$style.input, !isNewValueValid && $style.inputWithError]"
272
+ :placeholder="isEmpty(modelValue) && placeholder ? placeholder : ''"
256
273
  :disabled="disabled"
257
274
  @input="onInput"
258
275
  @focusin="emits('update:focused', true)"
@@ -266,7 +283,7 @@
266
283
  </div>
267
284
 
268
285
  <template #menu>
269
- <div ref="menuBodyEl" v-ui3n-click-outside="onClickOutside" :class="$style.body">
286
+ <div ref="menuBodyEl" :class="$style.body">
270
287
  <template v-if="size(filteredItems)">
271
288
  <template v-for="(item, index) in filteredItems" :key="item.id">
272
289
  <div
@@ -335,6 +352,7 @@
335
352
  line-height: var(--font-16);
336
353
  font-weight: 400;
337
354
  color: var(--color-text-control-primary-default);
355
+ background-color: transparent;
338
356
  border: none;
339
357
  outline: none;
340
358
 
@@ -345,7 +363,12 @@
345
363
  &:hover,
346
364
  &:focus {
347
365
  height: calc(var(--autocomplete-min-height) - 2px);
348
- border: 1px solid var(--color-border-control-accent-focused);
366
+
367
+ &:not(.inputWithError) {
368
+ border: 1px solid var(--color-border-control-accent-focused);
369
+ }
370
+
371
+ border: 1px solid var(--error-content-default);
349
372
  }
350
373
  }
351
374