@v1nt1248/3nclient-lib 0.2.95 → 0.2.97
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/README.md +1 -0
- package/dist/components/index.d.ts +3 -1
- package/dist/components/ui3n-menu/types.d.ts +1 -0
- package/dist/components/ui3n-selector/types.d.ts +45 -0
- package/dist/components/ui3n-selector/ui3n-selector.vue.d.ts +16 -0
- package/dist/index.d.ts +2 -0
- package/dist/style.css +1 -1
- package/dist/types/components.types.d.ts +38 -3
- package/dist/ui3n-components.d.ts +2 -0
- package/dist/ui3n-components.js +1140 -916
- package/package.json +2 -2
- package/src/components/index.ts +14 -0
- package/src/components/ui3n-autocomplete/ui3n-autocomplete.vue +0 -1
- package/src/components/ui3n-menu/types.ts +1 -0
- package/src/components/ui3n-menu/ui3n-menu.vue +15 -22
- package/src/components/ui3n-selector/types.ts +54 -0
- package/src/components/ui3n-selector/ui3n-selector.vue +486 -0
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.2.
|
|
5
|
+
"version": "0.2.97",
|
|
6
6
|
"description": "Library for 3NWeb clients",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"files": [
|
|
@@ -107,7 +107,7 @@
|
|
|
107
107
|
"vue-eslint-parser": "10.4.0",
|
|
108
108
|
"vue-tsc": "3.2.5"
|
|
109
109
|
},
|
|
110
|
-
"packageManager": "pnpm@10.
|
|
110
|
+
"packageManager": "pnpm@10.32.1",
|
|
111
111
|
"pnpm": {
|
|
112
112
|
"peerDependencyRules": {
|
|
113
113
|
"ignoreMissing": [
|
package/src/components/index.ts
CHANGED
|
@@ -104,6 +104,14 @@ import type {
|
|
|
104
104
|
} from './ui3n-autocomplete/types';
|
|
105
105
|
import Ui3nSlider from './ui3n-slider/ui3n-slider.vue';
|
|
106
106
|
import type { UI3nSliderProps, UI3nSliderEmits } from './ui3n-slider/types';
|
|
107
|
+
import Ui3nSelector from './ui3n-selector/ui3n-selector.vue';
|
|
108
|
+
import type {
|
|
109
|
+
Ui3nSelectorOptionBase,
|
|
110
|
+
Ui3nSelectorProps,
|
|
111
|
+
Ui3nSelectorEmits,
|
|
112
|
+
Ui3nSelectorSlots,
|
|
113
|
+
Ui3nSelectorItemDisplayingFunction,
|
|
114
|
+
} from './ui3n-selector/types';
|
|
107
115
|
|
|
108
116
|
export {
|
|
109
117
|
Ui3nIcon,
|
|
@@ -219,4 +227,10 @@ export {
|
|
|
219
227
|
Ui3nSlider,
|
|
220
228
|
UI3nSliderProps,
|
|
221
229
|
UI3nSliderEmits,
|
|
230
|
+
Ui3nSelector,
|
|
231
|
+
Ui3nSelectorProps,
|
|
232
|
+
Ui3nSelectorEmits,
|
|
233
|
+
Ui3nSelectorSlots,
|
|
234
|
+
Ui3nSelectorOptionBase,
|
|
235
|
+
Ui3nSelectorItemDisplayingFunction,
|
|
222
236
|
};
|
|
@@ -55,19 +55,12 @@
|
|
|
55
55
|
|
|
56
56
|
middleware.push(shift());
|
|
57
57
|
|
|
58
|
-
const {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
menuContentElement,
|
|
65
|
-
{
|
|
66
|
-
placement: 'bottom-start',
|
|
67
|
-
strategy: props.positionStrategy,
|
|
68
|
-
middleware,
|
|
69
|
-
whileElementsMounted: props.positionAutoupdate || props.positionStrategy === 'fixed' ? autoUpdate : undefined,
|
|
70
|
-
});
|
|
58
|
+
const { floatingStyles, isPositioned, update } = useFloating(usedTriggerElement, menuContentElement, {
|
|
59
|
+
placement: 'bottom-start',
|
|
60
|
+
strategy: props.positionStrategy,
|
|
61
|
+
middleware,
|
|
62
|
+
whileElementsMounted: props.positionAutoupdate || props.positionStrategy === 'fixed' ? autoUpdate : undefined,
|
|
63
|
+
});
|
|
71
64
|
|
|
72
65
|
function toggleMenu() {
|
|
73
66
|
if (props.disabled) {
|
|
@@ -118,14 +111,11 @@
|
|
|
118
111
|
},
|
|
119
112
|
);
|
|
120
113
|
|
|
121
|
-
watch(
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
127
|
-
},
|
|
128
|
-
);
|
|
114
|
+
watch([() => props.offsetX, () => props.offsetY], ([offsetX, offsetY], [oldOffsetX, oldOffsetY]) => {
|
|
115
|
+
if (offsetX !== oldOffsetX || offsetY !== oldOffsetY) {
|
|
116
|
+
update();
|
|
117
|
+
}
|
|
118
|
+
});
|
|
129
119
|
|
|
130
120
|
defineExpose<Ui3nMenuExpose>({
|
|
131
121
|
isPositioned,
|
|
@@ -150,7 +140,10 @@
|
|
|
150
140
|
v-if="isShow"
|
|
151
141
|
ref="menu-content-element"
|
|
152
142
|
v-click-outside="onClickOutside"
|
|
153
|
-
:style="
|
|
143
|
+
:style="{
|
|
144
|
+
...floatingStyles,
|
|
145
|
+
...(contentStyles && { ...contentStyles }),
|
|
146
|
+
}"
|
|
154
147
|
:class="$style.ui3nMenuContent"
|
|
155
148
|
v-on="closeOnClick ? { click: onContentClick } : {}"
|
|
156
149
|
>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import type { VNode } from 'vue';
|
|
3
|
+
import type { PrepareComponentEmits, Nullable } from '../../types';
|
|
4
|
+
|
|
5
|
+
export interface Ui3nSelectorOptionBase {
|
|
6
|
+
id: string;
|
|
7
|
+
[key: string]: any;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type Ui3nSelectorItemDisplayingFunction<T extends Ui3nSelectorOptionBase> = (v: T) => string;
|
|
11
|
+
|
|
12
|
+
export type Ui3nSelectorValue<T extends Ui3nSelectorOptionBase> = T | T[keyof T] | null;
|
|
13
|
+
|
|
14
|
+
export interface Ui3nSelectorProps<T extends Ui3nSelectorOptionBase> {
|
|
15
|
+
modelValue: Ui3nSelectorValue<T>;
|
|
16
|
+
label?: string;
|
|
17
|
+
placeholder?: string;
|
|
18
|
+
items?: T[];
|
|
19
|
+
itemValue?: keyof T;
|
|
20
|
+
itemDisplay?: keyof T | Ui3nSelectorItemDisplayingFunction<T>;
|
|
21
|
+
customFilter?: (value: Ui3nSelectorValue<T>, query: string) => boolean;
|
|
22
|
+
returnObject?: boolean;
|
|
23
|
+
clearable?: boolean;
|
|
24
|
+
withSearch?: boolean;
|
|
25
|
+
hideSelected?: boolean;
|
|
26
|
+
noDataText?: string;
|
|
27
|
+
disabled?: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface Ui3nSelectorEventsMap<T extends Ui3nSelectorOptionBase> {
|
|
31
|
+
'update:modelValue': Ui3nSelectorValue<T>;
|
|
32
|
+
'list:open': undefined;
|
|
33
|
+
'list:close': undefined;
|
|
34
|
+
focus: undefined;
|
|
35
|
+
blur: undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type Ui3nSelectorEvents = keyof Ui3nSelectorEventsMap<any>;
|
|
39
|
+
|
|
40
|
+
// export interface Ui3nSelectorEmits<T extends Ui3nSelectorOptionBase> {
|
|
41
|
+
// (event: 'update:modelValue', value: T | T[keyof T] | null): void;
|
|
42
|
+
// (event: 'list:open'): void;
|
|
43
|
+
// (event: 'list:close'): void;
|
|
44
|
+
// (event: 'focus'): void;
|
|
45
|
+
// (event: 'blur'): void;
|
|
46
|
+
// }
|
|
47
|
+
|
|
48
|
+
export type Ui3nSelectorEmits<T extends Ui3nSelectorOptionBase> = PrepareComponentEmits<Ui3nSelectorEventsMap<T>>;
|
|
49
|
+
|
|
50
|
+
export interface Ui3nSelectorSlots<T extends Ui3nSelectorOptionBase> {
|
|
51
|
+
noDataText?: () => VNode;
|
|
52
|
+
item?: (props: { item: T; index: number; activeIndex: Nullable<number>; query?: string }) => VNode;
|
|
53
|
+
selection?: (props: { value: Ui3nSelectorValue<T>; selectedItem: T | undefined }) => VNode;
|
|
54
|
+
}
|
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Copyright (C) 2026 3NSoft Inc.
|
|
3
|
+
|
|
4
|
+
This program is free software: you can redistribute it and/or modify it under
|
|
5
|
+
the terms of the GNU General Public License as published by the Free Software
|
|
6
|
+
Foundation, either version 3 of the License, or (at your option) any later
|
|
7
|
+
version.
|
|
8
|
+
|
|
9
|
+
This program is distributed in the hope that it will be useful, but
|
|
10
|
+
WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
|
12
|
+
See the GNU General Public License for more details.
|
|
13
|
+
|
|
14
|
+
You should have received a copy of the GNU General Public License along with
|
|
15
|
+
this program. If not, see <http://www.gnu.org/licenses/>.
|
|
16
|
+
-->
|
|
17
|
+
<script setup lang="ts" generic="T extends Ui3nSelectorOptionBase">
|
|
18
|
+
import { ref, computed, type ComputedRef, useTemplateRef, watch } from 'vue';
|
|
19
|
+
import isEmpty from 'lodash/isEmpty';
|
|
20
|
+
import size from 'lodash/size';
|
|
21
|
+
import { getRandomId } from '@/utils';
|
|
22
|
+
import type { Nullable } from '@/types';
|
|
23
|
+
import type {
|
|
24
|
+
Ui3nSelectorOptionBase,
|
|
25
|
+
Ui3nSelectorProps,
|
|
26
|
+
Ui3nSelectorEmits,
|
|
27
|
+
Ui3nSelectorSlots,
|
|
28
|
+
Ui3nSelectorItemDisplayingFunction,
|
|
29
|
+
} from './types';
|
|
30
|
+
import Ui3nButton from '../ui3n-button/ui3n-button.vue';
|
|
31
|
+
import Ui3nIcon from '../ui3n-icon/ui3n-icon.vue';
|
|
32
|
+
import Ui3nMenu from '../ui3n-menu/ui3n-menu.vue';
|
|
33
|
+
|
|
34
|
+
const props = withDefaults(defineProps<Ui3nSelectorProps<T>>(), {
|
|
35
|
+
label: '',
|
|
36
|
+
items: () => [] as T[],
|
|
37
|
+
itemValue: 'id',
|
|
38
|
+
itemDisplay: 'id' as keyof T,
|
|
39
|
+
clearable: false,
|
|
40
|
+
withSearch: false,
|
|
41
|
+
returnObject: false,
|
|
42
|
+
noDataText: '',
|
|
43
|
+
});
|
|
44
|
+
const emits = defineEmits<Ui3nSelectorEmits<T>>();
|
|
45
|
+
defineSlots<Ui3nSelectorSlots<T>>();
|
|
46
|
+
|
|
47
|
+
const componentId = `select-${getRandomId(4)}`;
|
|
48
|
+
|
|
49
|
+
const activatorEl = useTemplateRef<HTMLDivElement>('activator');
|
|
50
|
+
const bodyEl = useTemplateRef<HTMLDivElement>('body');
|
|
51
|
+
const isMenuOpen = ref(false);
|
|
52
|
+
const isFocused = ref(false);
|
|
53
|
+
const activeItemIndex = ref<Nullable<number>>(null);
|
|
54
|
+
|
|
55
|
+
const query = ref('');
|
|
56
|
+
const processedQuery = computed(() => query.value.toLowerCase());
|
|
57
|
+
|
|
58
|
+
const filteredItems = computed(() => {
|
|
59
|
+
return props.items
|
|
60
|
+
.filter(item => {
|
|
61
|
+
if (props.customFilter) {
|
|
62
|
+
return props.customFilter(item, query.value);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const itemDisplayingVal = getItemTitle(item, true);
|
|
66
|
+
return itemDisplayingVal.includes(processedQuery.value);
|
|
67
|
+
})
|
|
68
|
+
.filter(item => {
|
|
69
|
+
if (!props.hideSelected) {
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return props.returnObject
|
|
74
|
+
? (props.modelValue as T).id !== item.id
|
|
75
|
+
: (props.modelValue as T[keyof T]) !== item[props.itemValue];
|
|
76
|
+
});
|
|
77
|
+
}) as ComputedRef<T[]>;
|
|
78
|
+
|
|
79
|
+
const displayingSelection = computed(() => {
|
|
80
|
+
if (!props.modelValue) {
|
|
81
|
+
return '';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (props.returnObject && typeof props.modelValue === 'object') {
|
|
85
|
+
const selectedItem = props.items.find(item => item.id === (props.modelValue as T).id);
|
|
86
|
+
return selectedItem ? getItemTitle(selectedItem) : JSON.stringify(props.modelValue);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (props.returnObject && typeof props.returnObject !== 'object') {
|
|
90
|
+
return JSON.stringify(props.modelValue);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const selectedItem = props.items.find(item => item[props.itemValue] === props.modelValue);
|
|
94
|
+
return selectedItem ? getItemTitle(selectedItem) : props.modelValue;
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
function getItemTitle(value: T | T[keyof T], withProcessing?: boolean): string {
|
|
98
|
+
if (props.returnObject || typeof value === 'object') {
|
|
99
|
+
const isItemDisplayFunction = typeof props.itemDisplay === 'function';
|
|
100
|
+
const val = isItemDisplayFunction
|
|
101
|
+
? (props.itemDisplay as Ui3nSelectorItemDisplayingFunction<T>)(value as T)
|
|
102
|
+
: value
|
|
103
|
+
? ((value as T)[props.itemDisplay as keyof T] as string)
|
|
104
|
+
: '';
|
|
105
|
+
return withProcessing ? val.toLowerCase() : val;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return withProcessing
|
|
109
|
+
? value
|
|
110
|
+
? (value as T[keyof T] as string).toLowerCase()
|
|
111
|
+
: ''
|
|
112
|
+
: value
|
|
113
|
+
? (value as T[keyof T] as string)
|
|
114
|
+
: '';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function onFocus() {
|
|
118
|
+
isFocused.value = true;
|
|
119
|
+
emits('focus');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function onBlur() {
|
|
123
|
+
setTimeout(() => {
|
|
124
|
+
isFocused.value = false;
|
|
125
|
+
emits('blur');
|
|
126
|
+
}, 100);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function onItemClick(item: T) {
|
|
130
|
+
if (props.disabled) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const newValue = props.returnObject ? item : item[props.itemValue];
|
|
135
|
+
emits('update:modelValue', newValue);
|
|
136
|
+
query.value = '';
|
|
137
|
+
isMenuOpen.value = false;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function resetValue() {
|
|
141
|
+
emits('update:modelValue', null);
|
|
142
|
+
isMenuOpen.value = false;
|
|
143
|
+
activeItemIndex.value = null;
|
|
144
|
+
query.value = '';
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function handlePressingDownKey() {
|
|
148
|
+
if (activeItemIndex.value === null) {
|
|
149
|
+
activeItemIndex.value = 0;
|
|
150
|
+
bodyEl.value && bodyEl.value.focus();
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (activeItemIndex.value >= 0 && activeItemIndex.value < size(filteredItems.value) - 1) {
|
|
155
|
+
activeItemIndex.value += 1;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function handlePressingUpKey() {
|
|
160
|
+
if (activeItemIndex.value === null) {
|
|
161
|
+
activeItemIndex.value = size(filteredItems.value) - 1;
|
|
162
|
+
bodyEl.value && bodyEl.value.focus();
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (activeItemIndex.value > 0 && activeItemIndex.value < size(filteredItems.value)) {
|
|
167
|
+
activeItemIndex.value -= 1;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function handlePressingEscOrTabKeys() {
|
|
172
|
+
emits('blur');
|
|
173
|
+
isMenuOpen.value = false;
|
|
174
|
+
bodyEl.value!.blur();
|
|
175
|
+
activatorEl.value && activatorEl.value.blur();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function handlePressingEnterKey() {
|
|
179
|
+
if (activeItemIndex.value === null && !isMenuOpen.value) {
|
|
180
|
+
isMenuOpen.value = true;
|
|
181
|
+
bodyEl.value && bodyEl.value.focus();
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (activeItemIndex.value === null && isMenuOpen.value) {
|
|
186
|
+
isMenuOpen.value = false;
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (activeItemIndex.value !== null) {
|
|
191
|
+
const item = filteredItems.value[activeItemIndex.value!];
|
|
192
|
+
onItemClick(item);
|
|
193
|
+
activeItemIndex.value = null;
|
|
194
|
+
query.value = '';
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function onKeydown(event: KeyboardEvent, key: 'down' | 'up' | 'esc' | 'enter' | 'tab') {
|
|
199
|
+
switch (key) {
|
|
200
|
+
case 'down':
|
|
201
|
+
event.preventDefault();
|
|
202
|
+
event.stopPropagation();
|
|
203
|
+
handlePressingDownKey();
|
|
204
|
+
break;
|
|
205
|
+
case 'up':
|
|
206
|
+
event.preventDefault();
|
|
207
|
+
event.stopPropagation();
|
|
208
|
+
handlePressingUpKey();
|
|
209
|
+
break;
|
|
210
|
+
case 'esc':
|
|
211
|
+
case 'tab':
|
|
212
|
+
handlePressingEscOrTabKeys();
|
|
213
|
+
break;
|
|
214
|
+
case 'enter':
|
|
215
|
+
event.preventDefault();
|
|
216
|
+
event.stopPropagation();
|
|
217
|
+
handlePressingEnterKey();
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
watch(
|
|
223
|
+
() => isMenuOpen.value,
|
|
224
|
+
(val, oVal) => {
|
|
225
|
+
if (val !== oVal && val) {
|
|
226
|
+
if (props.modelValue) {
|
|
227
|
+
const selectedItemIndex = filteredItems.value.findIndex(item => {
|
|
228
|
+
if (typeof props.modelValue === 'object') {
|
|
229
|
+
return item.id === (props.modelValue as T).id;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return item[props.itemValue] === props.modelValue;
|
|
233
|
+
});
|
|
234
|
+
activeItemIndex.value = selectedItemIndex >= 0 ? selectedItemIndex : 0;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
emits('list:open');
|
|
238
|
+
} else if (val !== oVal && !val) {
|
|
239
|
+
activeItemIndex.value = null;
|
|
240
|
+
query.value = '';
|
|
241
|
+
emits('list:close');
|
|
242
|
+
}
|
|
243
|
+
},
|
|
244
|
+
);
|
|
245
|
+
</script>
|
|
246
|
+
|
|
247
|
+
<template>
|
|
248
|
+
<div :class="$style.ui3nSelector">
|
|
249
|
+
<label
|
|
250
|
+
v-if="label"
|
|
251
|
+
:class="$style.label"
|
|
252
|
+
>
|
|
253
|
+
{{ label }}
|
|
254
|
+
</label>
|
|
255
|
+
|
|
256
|
+
<ui3n-menu
|
|
257
|
+
v-model="isMenuOpen"
|
|
258
|
+
:offset-x="-3"
|
|
259
|
+
:offset-y="4"
|
|
260
|
+
position-autoupdate
|
|
261
|
+
:content-border-radius="8"
|
|
262
|
+
:content-styles="{ width: '100%' }"
|
|
263
|
+
:class="$style.menu"
|
|
264
|
+
:disabled="disabled"
|
|
265
|
+
>
|
|
266
|
+
<div
|
|
267
|
+
ref="activator"
|
|
268
|
+
tabindex="0"
|
|
269
|
+
:class="[
|
|
270
|
+
$style.trigger,
|
|
271
|
+
isFocused && $style.triggerFocused,
|
|
272
|
+
clearable && modelValue && $style.clearable,
|
|
273
|
+
placeholder && !modelValue && !disabled && $style.placeholder,
|
|
274
|
+
disabled && $style.triggerDisabled,
|
|
275
|
+
]"
|
|
276
|
+
@focusin="onFocus"
|
|
277
|
+
@focusout="onBlur"
|
|
278
|
+
@keydown.down="onKeydown($event, 'down')"
|
|
279
|
+
@keydown.up="onKeydown($event, 'up')"
|
|
280
|
+
@keydown.esc="onKeydown($event, 'esc')"
|
|
281
|
+
@keydown.enter="onKeydown($event, 'enter')"
|
|
282
|
+
@keydown.tab="onKeydown($event, 'tab')"
|
|
283
|
+
>
|
|
284
|
+
<slot
|
|
285
|
+
name="selection"
|
|
286
|
+
:value="modelValue"
|
|
287
|
+
:selected-item="activeItemIndex !== null ? filteredItems[activeItemIndex] : undefined"
|
|
288
|
+
>
|
|
289
|
+
{{ displayingSelection || placeholder || '' }}
|
|
290
|
+
</slot>
|
|
291
|
+
|
|
292
|
+
<ui3n-button
|
|
293
|
+
v-if="clearable && modelValue"
|
|
294
|
+
type="icon"
|
|
295
|
+
size="small"
|
|
296
|
+
color="transparent"
|
|
297
|
+
icon="round-close"
|
|
298
|
+
icon-size="16"
|
|
299
|
+
icon-color="var(--color-icon-block-primary-default)"
|
|
300
|
+
:disabled="disabled"
|
|
301
|
+
:class="$style.clearBtn"
|
|
302
|
+
@click.stop.prevent="resetValue"
|
|
303
|
+
/>
|
|
304
|
+
|
|
305
|
+
<ui3n-icon
|
|
306
|
+
icon="outline-arrow-drop-down"
|
|
307
|
+
color="var(--color-icon-block-primary-default)"
|
|
308
|
+
size="16"
|
|
309
|
+
:vertical-flip="isMenuOpen"
|
|
310
|
+
:class="$style.menuIcon"
|
|
311
|
+
/>
|
|
312
|
+
</div>
|
|
313
|
+
|
|
314
|
+
<template #menu>
|
|
315
|
+
<div
|
|
316
|
+
v-if="!isEmpty(filteredItems) || (isEmpty(filteredItems) && noDataText)"
|
|
317
|
+
ref="body"
|
|
318
|
+
:class="$style.body"
|
|
319
|
+
>
|
|
320
|
+
<template v-if="!isEmpty(filteredItems)">
|
|
321
|
+
<template
|
|
322
|
+
v-for="(item, index) in filteredItems"
|
|
323
|
+
:key="item.id"
|
|
324
|
+
>
|
|
325
|
+
<div
|
|
326
|
+
:id="`${componentId}-${index}`"
|
|
327
|
+
:class="[
|
|
328
|
+
$style.item,
|
|
329
|
+
activeItemIndex === index && $style.itemSelected,
|
|
330
|
+
disabled && $style.itemDisabled,
|
|
331
|
+
]"
|
|
332
|
+
@click.stop.prevent="onItemClick(item)"
|
|
333
|
+
>
|
|
334
|
+
<slot
|
|
335
|
+
name="item"
|
|
336
|
+
:item="item"
|
|
337
|
+
:index="index"
|
|
338
|
+
:active-index="activeItemIndex"
|
|
339
|
+
:query="query"
|
|
340
|
+
>
|
|
341
|
+
{{ getItemTitle(item) }}
|
|
342
|
+
</slot>
|
|
343
|
+
</div>
|
|
344
|
+
</template>
|
|
345
|
+
</template>
|
|
346
|
+
|
|
347
|
+
<div
|
|
348
|
+
v-if="isEmpty(filteredItems) && noDataText"
|
|
349
|
+
:class="$style.noData"
|
|
350
|
+
>
|
|
351
|
+
<slot name="noDataText">
|
|
352
|
+
{{ noDataText }}
|
|
353
|
+
</slot>
|
|
354
|
+
</div>
|
|
355
|
+
</div>
|
|
356
|
+
</template>
|
|
357
|
+
</ui3n-menu>
|
|
358
|
+
</div>
|
|
359
|
+
</template>
|
|
360
|
+
|
|
361
|
+
<style lang="scss" module>
|
|
362
|
+
@use '@/assets/styles/mixins' as mixins;
|
|
363
|
+
|
|
364
|
+
.ui3nSelector {
|
|
365
|
+
--selector-activator-height: 32px;
|
|
366
|
+
|
|
367
|
+
position: relative;
|
|
368
|
+
width: 100%;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
.label {
|
|
372
|
+
display: block;
|
|
373
|
+
width: 100%;
|
|
374
|
+
font-size: var(--font-12);
|
|
375
|
+
font-weight: 500;
|
|
376
|
+
line-height: var(--font-16);
|
|
377
|
+
color: var(--color-text-control-primary-default);
|
|
378
|
+
margin-bottom: var(--spacing-xs);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
.menu {
|
|
382
|
+
width: 100%;
|
|
383
|
+
max-width: 100% !important;
|
|
384
|
+
|
|
385
|
+
& > div {
|
|
386
|
+
max-width: 100% !important;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
.trigger {
|
|
391
|
+
display: flex;
|
|
392
|
+
width: 100%;
|
|
393
|
+
height: var(--selector-activator-height);
|
|
394
|
+
padding: 0 var(--spacing-ml) 0 var(--spacing-s);
|
|
395
|
+
justify-content: flex-start;
|
|
396
|
+
align-items: center;
|
|
397
|
+
background-color: var(--color-bg-control-secondary-default);
|
|
398
|
+
border-radius: var(--spacing-xs);
|
|
399
|
+
color: var(--color-text-control-primary-default);
|
|
400
|
+
font-size: var(--font-13);
|
|
401
|
+
font-weight: 400;
|
|
402
|
+
line-height: var(--font-16);
|
|
403
|
+
transition: all 0.2s ease-in-out;
|
|
404
|
+
cursor: pointer;
|
|
405
|
+
@include mixins.text-overflow-ellipsis();
|
|
406
|
+
|
|
407
|
+
&.placeholder {
|
|
408
|
+
color: var(--color-text-control-secondary-default);
|
|
409
|
+
font-style: italic;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
&.triggerDisabled {
|
|
413
|
+
background-color: var(--color-bg-control-secondary-disabled);
|
|
414
|
+
color: var(--color-text-control-secondary-disabled);
|
|
415
|
+
cursor: default;
|
|
416
|
+
pointer-events: none;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
.menuIcon {
|
|
420
|
+
position: absolute;
|
|
421
|
+
top: 8px;
|
|
422
|
+
right: 4px;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
&.clearable {
|
|
426
|
+
padding-right: calc(var(--spacing-xxl) + var(--spacing-xs));
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
.clearBtn {
|
|
430
|
+
position: absolute;
|
|
431
|
+
top: var(--spacing-xs);
|
|
432
|
+
right: var(--spacing-ml);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
&.triggerFocused {
|
|
436
|
+
background-color: var(--color-bg-control-secondary-focused);
|
|
437
|
+
outline: 1px solid var(--color-border-control-accent-focused);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
.body {
|
|
442
|
+
position: relative;
|
|
443
|
+
width: 100%;
|
|
444
|
+
background-color: var(--color-bg-block-primary-default);
|
|
445
|
+
padding: var(--spacing-xs);
|
|
446
|
+
|
|
447
|
+
.noData {
|
|
448
|
+
display: flex;
|
|
449
|
+
min-height: var(--spacing-l);
|
|
450
|
+
justify-content: flex-start;
|
|
451
|
+
align-items: center;
|
|
452
|
+
padding: var(--spacing-xs) var(--spacing-s);
|
|
453
|
+
font-size: var(--font-13);
|
|
454
|
+
line-height: var(--font-16);
|
|
455
|
+
font-weight: 500;
|
|
456
|
+
color: var(--color-text-control-primary-default);
|
|
457
|
+
border-radius: var(--spacing-xs);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
.item {
|
|
461
|
+
display: flex;
|
|
462
|
+
min-height: var(--spacing-l);
|
|
463
|
+
justify-content: flex-start;
|
|
464
|
+
align-items: center;
|
|
465
|
+
padding: var(--spacing-xs) var(--spacing-s);
|
|
466
|
+
font-size: var(--font-13);
|
|
467
|
+
line-height: var(--font-16);
|
|
468
|
+
font-weight: 500;
|
|
469
|
+
color: var(--color-text-control-primary-default);
|
|
470
|
+
border-radius: var(--spacing-xs);
|
|
471
|
+
user-select: none;
|
|
472
|
+
cursor: pointer;
|
|
473
|
+
|
|
474
|
+
&.itemSelected,
|
|
475
|
+
&:hover {
|
|
476
|
+
background-color: var(--color-bg-control-primary-hover);
|
|
477
|
+
color: var(--color-text-control-accent-default);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
&.itemDisabled {
|
|
481
|
+
opacity: 0.7;
|
|
482
|
+
pointer-events: none;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
</style>
|