@v1nt1248/3nclient-lib 0.1.76 → 0.1.78
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 +2 -1
- package/dist/components/index.d.ts +3 -1
- package/dist/components/ui3n-autocomplete/types.d.ts +44 -0
- package/dist/components/ui3n-autocomplete/ui3n-autocomplete.vue.d.ts +18 -0
- package/dist/components/ui3n-menu/types.d.ts +2 -0
- package/dist/components/ui3n-menu/ui3n-menu.vue.d.ts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/style.css +1 -1
- package/dist/ui3n-components.d.ts +2 -0
- package/dist/ui3n-components.js +6787 -6544
- package/dist/ui3n-plugins.js +1 -1
- package/package.json +1 -1
- package/src/components/index.ts +12 -0
- package/src/components/ui3n-autocomplete/types.ts +40 -0
- package/src/components/ui3n-autocomplete/ui3n-autocomplete.vue +390 -0
- package/src/components/ui3n-chip/ui3n-chip.vue +5 -1
- package/src/components/ui3n-dialog/ui3n-dialog.vue +0 -1
- package/src/components/ui3n-menu/types.ts +2 -0
- package/src/components/ui3n-menu/ui3n-menu.vue +18 -1
package/dist/ui3n-plugins.js
CHANGED
|
@@ -3662,7 +3662,7 @@ const Wh = ["id"], Oh = /* @__PURE__ */ ZI({
|
|
|
3662
3662
|
], 2)) : JA("", !0);
|
|
3663
3663
|
};
|
|
3664
3664
|
}
|
|
3665
|
-
}), mh = "
|
|
3665
|
+
}), mh = "_overlay_zorcd_1", Xh = "_dialog_zorcd_11", jh = "_draggable_zorcd_30", vh = "_title_zorcd_34", Ph = "_icon_zorcd_58", zh = "_dragHandleIcon_zorcd_62", _h = "_closeBtn_zorcd_68", $h = "_content_zorcd_75", AU = "_actions_zorcd_80", IU = "_bothBtns_zorcd_88", QU = {
|
|
3666
3666
|
overlay: mh,
|
|
3667
3667
|
dialog: Xh,
|
|
3668
3668
|
draggable: jh,
|
package/package.json
CHANGED
package/src/components/index.ts
CHANGED
|
@@ -83,6 +83,13 @@ import type {
|
|
|
83
83
|
} from './ui3n-radio-group/types';
|
|
84
84
|
import Ui3nEditable from './ui3n-editable/ui3n-editable.vue';
|
|
85
85
|
import type { Ui3nEditableProps, Ui3nEditableEmits } from './ui3n-editable/types';
|
|
86
|
+
import Ui3nAutocomplete from './ui3n-autocomplete/ui3n-autocomplete.vue';
|
|
87
|
+
import type {
|
|
88
|
+
Ui3nAutocompleteProps,
|
|
89
|
+
Ui3nAutocompleteEmits,
|
|
90
|
+
Ui3nAutocompleteSlots,
|
|
91
|
+
Ui3nAutocompleteOptionBase,
|
|
92
|
+
} from './ui3n-autocomplete/types';
|
|
86
93
|
|
|
87
94
|
export {
|
|
88
95
|
Ui3nIcon,
|
|
@@ -188,4 +195,9 @@ export {
|
|
|
188
195
|
Ui3nEditable,
|
|
189
196
|
Ui3nEditableProps,
|
|
190
197
|
Ui3nEditableEmits,
|
|
198
|
+
Ui3nAutocomplete,
|
|
199
|
+
Ui3nAutocompleteProps,
|
|
200
|
+
Ui3nAutocompleteEmits,
|
|
201
|
+
Ui3nAutocompleteSlots,
|
|
202
|
+
Ui3nAutocompleteOptionBase,
|
|
191
203
|
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { VNode } from 'vue';
|
|
2
|
+
|
|
3
|
+
export interface Ui3nAutocompleteOptionBase {
|
|
4
|
+
id: string;
|
|
5
|
+
name?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type Ui3nAutocompleteValue<T extends Ui3nAutocompleteOptionBase> = {
|
|
9
|
+
[Property in keyof T]: T[Property];
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export interface Ui3nAutocompleteProps<T extends Ui3nAutocompleteOptionBase> {
|
|
13
|
+
chips?: boolean;
|
|
14
|
+
clearOnSelect?: boolean;
|
|
15
|
+
customFilter?: (value: T, query: string) => boolean;
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
filterKeys?: string[];
|
|
18
|
+
hideSelected?: boolean;
|
|
19
|
+
items: T[];
|
|
20
|
+
itemTitle?: keyof T;
|
|
21
|
+
itemValue?: keyof T;
|
|
22
|
+
modelValue: T[] | Array<T[keyof T]>;
|
|
23
|
+
multiple?: boolean;
|
|
24
|
+
noDataText?: string;
|
|
25
|
+
placeholder?: string;
|
|
26
|
+
returnObject?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface Ui3nAutocompleteEmits<T extends Ui3nAutocompleteOptionBase> {
|
|
30
|
+
(event: 'update:modelValue', value: T[] | Array<T[keyof T]>): void;
|
|
31
|
+
(event: 'update:search', value: string): void;
|
|
32
|
+
(event: 'update:focused', value: boolean): void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface Ui3nAutocompleteSlots<T extends Ui3nAutocompleteOptionBase> {
|
|
36
|
+
item: (props: { item: T; index: number; query?: string }) => VNode;
|
|
37
|
+
chip: (props: { item: T | T[keyof T]; index: number }) => VNode;
|
|
38
|
+
noDataText: () => VNode;
|
|
39
|
+
selection: (props: { value: T[] | Array<T[keyof T]> }) => VNode;
|
|
40
|
+
}
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Copyright (C) 2025 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 Ui3nAutocompleteOptionBase">
|
|
18
|
+
import { computed, ref, watch } from 'vue';
|
|
19
|
+
import cloneDeep from 'lodash/cloneDeep';
|
|
20
|
+
import isEmpty from 'lodash/isEmpty';
|
|
21
|
+
import size from 'lodash/size';
|
|
22
|
+
import Ui3nChip from '../ui3n-chip/ui3n-chip.vue';
|
|
23
|
+
import Ui3nMenu from '../ui3n-menu/ui3n-menu.vue';
|
|
24
|
+
import Ui3nHtml from '../../directives/ui3n-html';
|
|
25
|
+
import { markSearch, getRandomId } from '@/utils';
|
|
26
|
+
import type { Nullable } from '@/components/types';
|
|
27
|
+
import {
|
|
28
|
+
Ui3nAutocompleteOptionBase,
|
|
29
|
+
Ui3nAutocompleteProps,
|
|
30
|
+
Ui3nAutocompleteEmits,
|
|
31
|
+
Ui3nAutocompleteSlots,
|
|
32
|
+
} from './types';
|
|
33
|
+
|
|
34
|
+
const vUi3nHtml = Ui3nHtml;
|
|
35
|
+
|
|
36
|
+
const props = withDefaults(defineProps<Ui3nAutocompleteProps<T>>(), {
|
|
37
|
+
clearOnSelect: false,
|
|
38
|
+
filterKeys: () => [],
|
|
39
|
+
filterMode: 'every',
|
|
40
|
+
hideSelected: false,
|
|
41
|
+
items: () => [],
|
|
42
|
+
itemTitle: 'name',
|
|
43
|
+
itemValue: 'id',
|
|
44
|
+
});
|
|
45
|
+
const emits = defineEmits<Ui3nAutocompleteEmits<T>>();
|
|
46
|
+
defineSlots<Ui3nAutocompleteSlots<T>>();
|
|
47
|
+
|
|
48
|
+
const componentId = `auto-${getRandomId(4)}`;
|
|
49
|
+
|
|
50
|
+
const query = ref('');
|
|
51
|
+
const isMenuOpen = ref(false);
|
|
52
|
+
const activatorEl = ref<Nullable<HTMLDivElement>>(null);
|
|
53
|
+
const inputEl = ref<Nullable<HTMLInputElement>>(null);
|
|
54
|
+
const menuBodyEl = ref<Nullable<HTMLDivElement>>(null);
|
|
55
|
+
const activeItemsIndex = ref<Nullable<number>>(null);
|
|
56
|
+
|
|
57
|
+
const ids = computed(() => props.returnObject
|
|
58
|
+
? (props.modelValue as T[]).map(item => item.id)
|
|
59
|
+
: (props.modelValue as Array<T[keyof T]>));
|
|
60
|
+
|
|
61
|
+
const displayValue = computed(() => {
|
|
62
|
+
if (props.chips) return '';
|
|
63
|
+
|
|
64
|
+
if (props.returnObject) {
|
|
65
|
+
return (props.modelValue as T[]).map(v => v[props.itemTitle] || '?').join(', ');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return props.modelValue.join(', ');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const filteredItems = computed(() => {
|
|
72
|
+
if (props.customFilter) {
|
|
73
|
+
return props.items
|
|
74
|
+
.filter(item => props.customFilter!(item, query.value))
|
|
75
|
+
.filter(item => {
|
|
76
|
+
if (!props.hideSelected) return true;
|
|
77
|
+
|
|
78
|
+
return props.returnObject
|
|
79
|
+
? !(ids.value as string[]).includes(item.id as string)
|
|
80
|
+
: !(ids.value as Array<T[keyof T]>).includes(item as T[keyof T]);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return props.items
|
|
85
|
+
.filter(item => (item[props.itemTitle] as string).toLowerCase().includes(query.value.toLowerCase()))
|
|
86
|
+
.filter(item => {
|
|
87
|
+
if (!props.hideSelected) return true;
|
|
88
|
+
|
|
89
|
+
return props.returnObject
|
|
90
|
+
? !(ids.value as string[]).includes(item.id as string)
|
|
91
|
+
: !(ids.value as Array<T[keyof T]>).includes(item as T[keyof T]);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
function onInput() {
|
|
96
|
+
emits('update:search', query.value);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function onBlur() {
|
|
100
|
+
emits('update:focused', false)
|
|
101
|
+
activeItemsIndex.value = null;
|
|
102
|
+
query.value = '';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function onItemClick(item: T) {
|
|
106
|
+
if (props.disabled) return;
|
|
107
|
+
|
|
108
|
+
if (!props.multiple) {
|
|
109
|
+
const newValue = props.returnObject ? [item] : [item[props.itemValue]];
|
|
110
|
+
emits('update:modelValue', newValue);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const updatedValue = cloneDeep(props.modelValue);
|
|
115
|
+
const index = props.returnObject
|
|
116
|
+
? (props.modelValue as T[]).findIndex(v => v.id === item.id)
|
|
117
|
+
: props.modelValue.findIndex(v => v === item[props.itemValue]);
|
|
118
|
+
|
|
119
|
+
if (index > -1) {
|
|
120
|
+
updatedValue.splice(index, 1);
|
|
121
|
+
} else {
|
|
122
|
+
// @ts-ignore
|
|
123
|
+
updatedValue.push(props.returnObject ? item : item[props.itemValue]);
|
|
124
|
+
}
|
|
125
|
+
emits('update:modelValue', updatedValue);
|
|
126
|
+
|
|
127
|
+
props.clearOnSelect && (query.value = '');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function onChipClose(item: T | T[keyof T]) {
|
|
131
|
+
if (props.disabled) return;
|
|
132
|
+
|
|
133
|
+
const index = props.returnObject
|
|
134
|
+
? (props.modelValue as T[]).findIndex(v => v.id === (item as T).id)
|
|
135
|
+
: (props.modelValue as Array<T[keyof T]>).findIndex(v => v === item);
|
|
136
|
+
const updatedModelValue = cloneDeep(props.modelValue);
|
|
137
|
+
updatedModelValue.splice(index, 1);
|
|
138
|
+
emits('update:modelValue', updatedModelValue);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function handlePressingDownKey() {
|
|
142
|
+
if (activeItemsIndex.value === null) {
|
|
143
|
+
activeItemsIndex.value = 0;
|
|
144
|
+
menuBodyEl.value && menuBodyEl.value.focus();
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (activeItemsIndex.value >= 0 && activeItemsIndex.value < size(filteredItems.value) - 1) {
|
|
149
|
+
activeItemsIndex.value += 1;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function handlePressingUpKey() {
|
|
154
|
+
if (activeItemsIndex.value === null) {
|
|
155
|
+
activeItemsIndex.value = size(filteredItems.value) - 1;
|
|
156
|
+
menuBodyEl.value && menuBodyEl.value.focus();
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (activeItemsIndex.value > 0 && activeItemsIndex.value < size(filteredItems.value)) {
|
|
161
|
+
activeItemsIndex.value -= 1;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function handlePressingEscOrTabKeys() {
|
|
166
|
+
emits('update:focused', false)
|
|
167
|
+
isMenuOpen.value = false;
|
|
168
|
+
activeItemsIndex.value = null;
|
|
169
|
+
query.value = '';
|
|
170
|
+
inputEl.value && inputEl.value.blur();
|
|
171
|
+
activatorEl.value && activatorEl.value.blur();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function handlePressingEnterKey() {
|
|
175
|
+
if (activeItemsIndex.value === null) {
|
|
176
|
+
isMenuOpen.value = true;
|
|
177
|
+
activeItemsIndex.value = 0;
|
|
178
|
+
menuBodyEl.value && menuBodyEl.value.focus();
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const item = filteredItems.value[activeItemsIndex.value!];
|
|
183
|
+
onItemClick(item);
|
|
184
|
+
activeItemsIndex.value = null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function onKeydown(event: KeyboardEvent, key: 'down' | 'up' | 'esc' | 'enter' | 'tab') {
|
|
188
|
+
if (isEmpty(filteredItems.value)) return;
|
|
189
|
+
|
|
190
|
+
switch (key) {
|
|
191
|
+
case 'down':
|
|
192
|
+
event.preventDefault();
|
|
193
|
+
event.stopPropagation();
|
|
194
|
+
handlePressingDownKey();
|
|
195
|
+
break;
|
|
196
|
+
case 'up':
|
|
197
|
+
event.preventDefault();
|
|
198
|
+
event.stopPropagation();
|
|
199
|
+
handlePressingUpKey();
|
|
200
|
+
break;
|
|
201
|
+
case 'esc':
|
|
202
|
+
case 'tab':
|
|
203
|
+
handlePressingEscOrTabKeys();
|
|
204
|
+
break;
|
|
205
|
+
case 'enter':
|
|
206
|
+
event.preventDefault();
|
|
207
|
+
event.stopPropagation();
|
|
208
|
+
handlePressingEnterKey();
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
watch(
|
|
214
|
+
() => size(filteredItems.value),
|
|
215
|
+
(val, oVal) => {
|
|
216
|
+
if (val !== oVal) {
|
|
217
|
+
activeItemsIndex.value = null;
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
);
|
|
221
|
+
</script>
|
|
222
|
+
|
|
223
|
+
<template>
|
|
224
|
+
<div :class="$style.ui3nAutocomplete">
|
|
225
|
+
<ui3n-menu v-model="isMenuOpen" :offset-x="2" :offset-y="4" :disabled="disabled">
|
|
226
|
+
<div ref="activatorEl" :class="$style.trigger">
|
|
227
|
+
<template v-if="chips">
|
|
228
|
+
<template v-for="(item, index) in modelValue" :key="item.id">
|
|
229
|
+
<slot name="chip" :item="item" :index="index">
|
|
230
|
+
<ui3n-chip
|
|
231
|
+
round
|
|
232
|
+
closeable
|
|
233
|
+
@close="onChipClose(item)"
|
|
234
|
+
>
|
|
235
|
+
{{ returnObject ? (item as T)[props.itemTitle] : item }}
|
|
236
|
+
</ui3n-chip>
|
|
237
|
+
</slot>
|
|
238
|
+
</template>
|
|
239
|
+
</template>
|
|
240
|
+
|
|
241
|
+
<template v-else>
|
|
242
|
+
<div :class="$style.displayValue">{{ displayValue }}</div>
|
|
243
|
+
</template>
|
|
244
|
+
|
|
245
|
+
<input
|
|
246
|
+
ref="inputEl"
|
|
247
|
+
v-model="query"
|
|
248
|
+
type="text"
|
|
249
|
+
:class="$style.input"
|
|
250
|
+
:disabled="disabled"
|
|
251
|
+
@input="onInput"
|
|
252
|
+
@focusin="emits('update:focused', true)"
|
|
253
|
+
@focusout="onBlur"
|
|
254
|
+
@keydown.down="onKeydown($event, 'down')"
|
|
255
|
+
@keydown.up="onKeydown($event, 'up')"
|
|
256
|
+
@keydown.esc="onKeydown($event, 'esc')"
|
|
257
|
+
@keydown.enter="onKeydown($event, 'enter')"
|
|
258
|
+
@keydown.tab="onKeydown($event, 'tab')"
|
|
259
|
+
/>
|
|
260
|
+
</div>
|
|
261
|
+
|
|
262
|
+
<template #menu>
|
|
263
|
+
<div ref="menuBodyEl" :class="$style.body">
|
|
264
|
+
<template v-if="size(filteredItems)">
|
|
265
|
+
<template v-for="(item, index) in filteredItems" :key="item.id">
|
|
266
|
+
<div
|
|
267
|
+
:id="`${componentId}-${index}`"
|
|
268
|
+
:class="[
|
|
269
|
+
$style.item,
|
|
270
|
+
activeItemsIndex === index && $style.itemSelected,
|
|
271
|
+
disabled && $style.itemDisabled
|
|
272
|
+
]"
|
|
273
|
+
@click.stop.prevent="onItemClick(item)"
|
|
274
|
+
>
|
|
275
|
+
<slot name="item" :item="item" :index="index" :query="query">
|
|
276
|
+
<span v-ui3n-html="markSearch(item[props.itemTitle] as string, query)" />
|
|
277
|
+
</slot>
|
|
278
|
+
</div>
|
|
279
|
+
</template>
|
|
280
|
+
</template>
|
|
281
|
+
|
|
282
|
+
<div v-if="!size(filteredItems) && noDataText" :class="$style.noData">
|
|
283
|
+
<slot name="noDataText">
|
|
284
|
+
{{ noDataText }}
|
|
285
|
+
</slot>
|
|
286
|
+
</div>
|
|
287
|
+
</div>
|
|
288
|
+
</template>
|
|
289
|
+
</ui3n-menu>
|
|
290
|
+
</div>
|
|
291
|
+
</template>
|
|
292
|
+
|
|
293
|
+
<style lang="scss" module>
|
|
294
|
+
.ui3nAutocomplete {
|
|
295
|
+
--autocomplete-min-height: var(--spacing-ml);
|
|
296
|
+
|
|
297
|
+
position: relative;
|
|
298
|
+
width: 100%;
|
|
299
|
+
min-height: var(--autocomplete-min-height);
|
|
300
|
+
|
|
301
|
+
:global(.match-search) {
|
|
302
|
+
font-weight: 600;
|
|
303
|
+
color: var(--color-text-control-accent-default) !important;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
.trigger {
|
|
308
|
+
display: flex;
|
|
309
|
+
width: 100%;
|
|
310
|
+
flex-wrap: wrap;
|
|
311
|
+
justify-content: flex-start;
|
|
312
|
+
align-items: flex-start;
|
|
313
|
+
gap: var(--spacing-xs);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
.displayValue {
|
|
317
|
+
font-size: var(--font-12);
|
|
318
|
+
line-height: var(--autocomplete-min-height);
|
|
319
|
+
font-weight: 400;
|
|
320
|
+
color: var(--color-text-control-primary-default);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
.input {
|
|
324
|
+
border-radius: var(--spacing-xs);
|
|
325
|
+
padding: 0 var(--spacing-s);
|
|
326
|
+
flex-grow: 1;
|
|
327
|
+
height: var(--autocomplete-min-height);
|
|
328
|
+
font-size: var(--font-12);
|
|
329
|
+
line-height: var(--font-16);
|
|
330
|
+
font-weight: 400;
|
|
331
|
+
color: var(--color-text-control-primary-default);
|
|
332
|
+
border: none;
|
|
333
|
+
outline: none;
|
|
334
|
+
|
|
335
|
+
&::placeholder {
|
|
336
|
+
color: var(--color-text-control-secondary-default);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
&:hover,
|
|
340
|
+
&:focus {
|
|
341
|
+
height: calc(var(--autocomplete-min-height) - 2px);
|
|
342
|
+
border: 1px solid var(--color-border-control-accent-focused);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
.body {
|
|
347
|
+
position: relative;
|
|
348
|
+
background-color: var(--color-bg-control-secondary-default);
|
|
349
|
+
padding: var(--spacing-xs);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
.item {
|
|
353
|
+
display: flex;
|
|
354
|
+
min-height: var(--spacing-l);
|
|
355
|
+
justify-content: flex-start;
|
|
356
|
+
align-items: center;
|
|
357
|
+
padding: var(--spacing-xs) var(--spacing-s);
|
|
358
|
+
font-size: var(--font-13);
|
|
359
|
+
line-height: var(--font-16);
|
|
360
|
+
font-weight: 500;
|
|
361
|
+
color: var(--color-text-control-primary-default);
|
|
362
|
+
border-radius: var(--spacing-xs);
|
|
363
|
+
user-select: none;
|
|
364
|
+
cursor: pointer;
|
|
365
|
+
|
|
366
|
+
&.itemSelected,
|
|
367
|
+
&:hover {
|
|
368
|
+
background-color: var(--color-bg-control-primary-hover);
|
|
369
|
+
color: var(--color-text-control-accent-default);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
&.itemDisabled {
|
|
373
|
+
opacity: 0.7;
|
|
374
|
+
pointer-events: none;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
.noData {
|
|
379
|
+
display: flex;
|
|
380
|
+
min-height: var(--spacing-l);
|
|
381
|
+
justify-content: flex-start;
|
|
382
|
+
align-items: center;
|
|
383
|
+
padding: var(--spacing-xs) var(--spacing-s);
|
|
384
|
+
font-size: var(--font-13);
|
|
385
|
+
line-height: var(--font-16);
|
|
386
|
+
font-weight: 500;
|
|
387
|
+
color: var(--color-text-control-primary-default);
|
|
388
|
+
border-radius: var(--spacing-xs);
|
|
389
|
+
}
|
|
390
|
+
</style>
|
|
@@ -42,6 +42,10 @@ const mainCssClasses = computed(() => {
|
|
|
42
42
|
|
|
43
43
|
return val;
|
|
44
44
|
});
|
|
45
|
+
|
|
46
|
+
function onClick() {
|
|
47
|
+
emits('close');
|
|
48
|
+
}
|
|
45
49
|
</script>
|
|
46
50
|
|
|
47
51
|
<template>
|
|
@@ -62,7 +66,7 @@ const mainCssClasses = computed(() => {
|
|
|
62
66
|
color="transparent"
|
|
63
67
|
icon="round-close"
|
|
64
68
|
icon-color="var(--color-icon-control-primary-default)"
|
|
65
|
-
@click="
|
|
69
|
+
@click.stop.prevent="onClick"
|
|
66
70
|
/>
|
|
67
71
|
</div>
|
|
68
72
|
</template>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { VNode } from 'vue';
|
|
2
2
|
|
|
3
3
|
export interface Ui3nMenuProps {
|
|
4
|
+
modelValue?: boolean;
|
|
4
5
|
positionStrategy?: 'absolute' | 'fixed';
|
|
5
6
|
offsetX?: number;
|
|
6
7
|
offsetY?: number;
|
|
@@ -15,6 +16,7 @@ export interface Ui3nMenuEmits {
|
|
|
15
16
|
(ev: 'close'): void;
|
|
16
17
|
(ev: 'closed'): void;
|
|
17
18
|
(ev: 'click-outside'): void;
|
|
19
|
+
(ev: 'update:modelValue', value: boolean): void;
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
export interface Ui3nMenuSlots {
|
|
@@ -49,11 +49,13 @@
|
|
|
49
49
|
|
|
50
50
|
isShow.value = !isShow.value;
|
|
51
51
|
isShow.value ? emits('open') : emits('close');
|
|
52
|
+
emits('update:modelValue', isShow.value);
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
function onContentClick() {
|
|
55
56
|
isShow.value = false;
|
|
56
57
|
emits('close');
|
|
58
|
+
emits('update:modelValue', isShow.value);
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
function onClickOutside() {
|
|
@@ -61,6 +63,7 @@
|
|
|
61
63
|
if (props.closeOnClickOutside) {
|
|
62
64
|
isShow.value = false;
|
|
63
65
|
emits('close');
|
|
66
|
+
emits('update:modelValue', isShow.value);
|
|
64
67
|
}
|
|
65
68
|
}
|
|
66
69
|
|
|
@@ -70,13 +73,22 @@
|
|
|
70
73
|
val ? emits('opened') : emits('closed');
|
|
71
74
|
},
|
|
72
75
|
);
|
|
76
|
+
|
|
77
|
+
watch(
|
|
78
|
+
() => props.modelValue,
|
|
79
|
+
val => {
|
|
80
|
+
if (isShow.value !== val) {
|
|
81
|
+
isShow.value = val;
|
|
82
|
+
}
|
|
83
|
+
}, { immediate: true },
|
|
84
|
+
);
|
|
73
85
|
</script>
|
|
74
86
|
|
|
75
87
|
<template>
|
|
76
88
|
<div ref="menuElement" :class="$style.ui3nMenu">
|
|
77
89
|
<div
|
|
78
90
|
ref="menuTriggerElement"
|
|
79
|
-
:class="$style.ui3nMenuTrigger"
|
|
91
|
+
:class="[$style.ui3nMenuTrigger, disabled && $style.ui3nMenuTriggerDisabled]"
|
|
80
92
|
@click.stop="toggleMenu"
|
|
81
93
|
>
|
|
82
94
|
<slot />
|
|
@@ -109,6 +121,11 @@
|
|
|
109
121
|
.ui3nMenuTrigger {
|
|
110
122
|
position: relative;
|
|
111
123
|
max-width: max-content;
|
|
124
|
+
|
|
125
|
+
&.ui3nMenuTriggerDisabled {
|
|
126
|
+
pointer-events: none;
|
|
127
|
+
cursor: default;
|
|
128
|
+
}
|
|
112
129
|
}
|
|
113
130
|
|
|
114
131
|
.ui3nMenuContent {
|