@v1nt1248/3nclient-lib 0.3.39 → 0.3.41

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.3.39",
5
+ "version": "0.3.41",
6
6
  "description": "Library for 3NWeb clients",
7
7
  "type": "module",
8
8
  "files": [
@@ -26,6 +26,7 @@
26
26
  items: () => [],
27
27
  itemTitle: 'name',
28
28
  itemValue: 'id',
29
+ addNewValue: false,
29
30
  });
30
31
  const emits = defineEmits<Ui3nAutocompleteEmits<T>>();
31
32
  defineSlots<Ui3nAutocompleteSlots<T>>();
@@ -40,6 +41,7 @@
40
41
  const activeItemsIndex = ref<Nullable<number>>(null);
41
42
  const isLastChipHighlighted = ref(false);
42
43
  const isNewValueValid = ref(true);
44
+ const selectedFromMenu = ref(false);
43
45
 
44
46
  const ids = computed(() =>
45
47
  props.returnObject ? (props.modelValue as T[]).map(item => item.id) : (props.modelValue as Array<T[keyof T]>),
@@ -89,12 +91,60 @@
89
91
  activeItemsIndex.value = null;
90
92
  isLastChipHighlighted.value = false;
91
93
 
94
+ if (!selectedFromMenu.value) {
95
+ tryAddNewValue();
96
+ }
97
+
92
98
  setTimeout(() => {
93
99
  isMenuOpen.value = false;
94
100
  query.value = '';
101
+ selectedFromMenu.value = false;
95
102
  }, 150);
96
103
  }
97
104
 
105
+ function tryAddNewValue(): boolean {
106
+ if (!props.addNewValue || props.disabled) return false;
107
+
108
+ const text = query.value.trim();
109
+ if (!text) return false;
110
+
111
+ isNewValueValid.value = !props.newValueValidator ? true : props.newValueValidator(text);
112
+ emits('valid:new-value', isNewValueValid.value);
113
+ if (!isNewValueValid.value) return false;
114
+
115
+ const item = {
116
+ id: getRandomId(6),
117
+ [props.itemTitle]: text,
118
+ [props.itemValue]: text,
119
+ } as unknown as T;
120
+
121
+ if (!props.multiple) {
122
+ const newValue = props.returnObject ? [item] : [item[props.itemValue]];
123
+ emits('update:modelValue', newValue);
124
+ } else {
125
+ const alreadyPresent = props.returnObject
126
+ ? (props.modelValue as T[]).some(
127
+ v => v[props.itemValue] === item[props.itemValue] || v[props.itemTitle] === item[props.itemTitle],
128
+ )
129
+ : props.modelValue.some(v => v === item[props.itemValue]);
130
+
131
+ if (!alreadyPresent) {
132
+ const updatedValue = cloneDeep(props.modelValue);
133
+ // @ts-ignore
134
+ updatedValue.push(props.returnObject ? item : item[props.itemValue]);
135
+ emits('update:modelValue', updatedValue);
136
+ }
137
+ }
138
+
139
+ isNewValueValid.value = true;
140
+ props.clearOnSelect && (query.value = '');
141
+ return true;
142
+ }
143
+
144
+ function onItemMouseDown() {
145
+ selectedFromMenu.value = true;
146
+ }
147
+
98
148
  function onItemClick(item: T) {
99
149
  if (props.disabled) return;
100
150
 
@@ -155,7 +205,7 @@
155
205
  }
156
206
  }
157
207
 
158
- function handlePressingEscOrTabKeys() {
208
+ function handlePressingEscKey() {
159
209
  emits('update:focused', false);
160
210
  isMenuOpen.value = false;
161
211
  activeItemsIndex.value = null;
@@ -164,34 +214,35 @@
164
214
  activatorEl.value && activatorEl.value.blur();
165
215
  }
166
216
 
167
- function handlePressingEnterKey() {
168
- if (activeItemsIndex.value === null && !isMenuOpen.value) {
169
- isMenuOpen.value = true;
170
- activeItemsIndex.value = 0;
171
- menuBodyEl.value && menuBodyEl.value.focus({ preventScroll: true });
172
- return;
217
+ function handlePressingTabKey() {
218
+ if (props.addNewValue) {
219
+ tryAddNewValue();
173
220
  }
174
221
 
175
- if (activeItemsIndex.value === null && isMenuOpen.value && query.value) {
176
- isNewValueValid.value = !props.newValueValidator ? true : props.newValueValidator(query.value);
177
- emits('valid:new-value', isNewValueValid.value);
178
- if (isNewValueValid.value) {
179
- const item = {
180
- id: getRandomId(6),
181
- [props.itemTitle]: query.value,
182
- [props.itemValue]: query.value,
183
- };
184
- onItemClick(item as unknown as T);
185
- }
186
- props.clearOnSelect && (query.value = '');
187
- return;
188
- }
222
+ emits('update:focused', false);
223
+ isMenuOpen.value = false;
224
+ activeItemsIndex.value = null;
225
+ query.value = '';
226
+ }
189
227
 
228
+ function handlePressingEnterKey() {
190
229
  if (activeItemsIndex.value !== null) {
191
230
  const item = filteredItems.value[activeItemsIndex.value!];
192
231
  onItemClick(item);
193
232
  activeItemsIndex.value = null;
194
233
  props.clearOnSelect && (query.value = '');
234
+ return;
235
+ }
236
+
237
+ if (props.addNewValue && query.value.trim()) {
238
+ tryAddNewValue();
239
+ return;
240
+ }
241
+
242
+ if (!isMenuOpen.value) {
243
+ isMenuOpen.value = true;
244
+ activeItemsIndex.value = 0;
245
+ menuBodyEl.value && menuBodyEl.value.focus({ preventScroll: true });
195
246
  }
196
247
  }
197
248
 
@@ -229,8 +280,11 @@
229
280
  }
230
281
 
231
282
  case 'esc':
283
+ handlePressingEscKey();
284
+ break;
285
+
232
286
  case 'tab':
233
- handlePressingEscOrTabKeys();
287
+ handlePressingTabKey();
234
288
  break;
235
289
 
236
290
  case 'enter': {
@@ -251,6 +305,7 @@
251
305
  isMenuOpen.value = false;
252
306
  activeItemsIndex.value = null;
253
307
  isNewValueValid.value = true;
308
+ selectedFromMenu.value = false;
254
309
 
255
310
  emits('update:modelValue', [] as unknown as T[] & Array<T[keyof T]>);
256
311
  emits('update:search', '');
@@ -323,6 +378,7 @@
323
378
 
324
379
  <ui3n-menu
325
380
  v-model="isMenuOpen"
381
+ width="100%"
326
382
  :lock-scroll="lockScroll"
327
383
  :offset-x="2"
328
384
  :offset-y="4"
@@ -413,6 +469,7 @@
413
469
  activeItemsIndex === index && $style.itemSelected,
414
470
  disabled && $style.itemDisabled,
415
471
  ]"
472
+ @mousedown="onItemMouseDown"
416
473
  @click.stop.prevent="onItemClick(item)"
417
474
  >
418
475
  <slot
@@ -24,6 +24,11 @@ export interface Ui3nMenuProps {
24
24
  * Root element id
25
25
  */
26
26
  id?: string;
27
+ /**
28
+ * Root element width
29
+ * @default 'max-content'
30
+ */
31
+ width?: string | number;
27
32
  /**
28
33
  * Menu open state
29
34
  */
@@ -16,6 +16,7 @@
16
16
  closeOnClick: true,
17
17
  closeOnClickOutside: true,
18
18
  disabled: false,
19
+ width: 'max-content',
19
20
  });
20
21
  const emits = defineEmits<Ui3nMenuEmits>();
21
22
  defineSlots<Ui3nMenuSlots>();
@@ -90,6 +91,13 @@
90
91
  };
91
92
  });
92
93
 
94
+ const rootStyles = computed(() => {
95
+ const numericWidth = !isNaN(Number(props.width));
96
+ const widthStr = numericWidth ? `${props.width}px` : String(props.width);
97
+
98
+ return { width: widthStr };
99
+ });
100
+
93
101
  function preventScrollEvent(e: Event) {
94
102
  e.preventDefault();
95
103
  }
@@ -262,6 +270,7 @@
262
270
  ref="menu-element"
263
271
  :id="id"
264
272
  :class="$style.ui3nMenu"
273
+ :style="rootStyles"
265
274
  @click="emits('click', $event)"
266
275
  >
267
276
  <div
@@ -292,7 +301,6 @@
292
301
  --ui3n-menu-content-bg: var(--color-bg-control-secondary-default);
293
302
 
294
303
  position: relative;
295
- width: max-content;
296
304
  display: inline-block;
297
305
  overflow: visible;
298
306
  }
@@ -259,6 +259,7 @@
259
259
 
260
260
  <ui3n-menu
261
261
  v-model="isMenuOpen"
262
+ width="100%"
262
263
  :offset-x="-3"
263
264
  :offset-y="4"
264
265
  position-autoupdate
@@ -399,8 +400,6 @@
399
400
  }
400
401
 
401
402
  .menu {
402
- width: 100%;
403
- max-width: 100% !important;
404
403
  border-radius: var(--ui3n-selector-border-radius);
405
404
 
406
405
  & > div {