edvoyui-component-library-test-flight 0.0.150 → 0.0.151
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/dist/EUIButton.vue.d.ts.map +1 -0
- package/dist/EUIButtonGroup.vue.d.ts.map +1 -0
- package/dist/library-vue-ts.cjs.js +141 -54
- package/dist/library-vue-ts.css +1 -1
- package/dist/library-vue-ts.es.js +14231 -12463
- package/dist/library-vue-ts.umd.js +145 -58
- package/dist/searchTagSelect/EUISearchTagSelect.vue.d.ts +5 -0
- package/dist/searchTagSelect/EUISearchTagSelect.vue.d.ts.map +1 -0
- package/dist/searchTagSelect/SearchInput.vue.d.ts +5 -0
- package/dist/searchTagSelect/SearchInput.vue.d.ts.map +1 -0
- package/dist/telephone/EUITelephone.vue.d.ts.map +1 -0
- package/package.json +3 -2
- package/src/assets/svg/CheckTick.vue +21 -0
- package/src/components/HelloWorld.vue +48 -2
- package/src/components/checkbox/EUICheckbox.vue +11 -4
- package/src/components/index.ts +1 -0
- package/src/components/searchTagSelect/EUISearchTagSelect.vue +637 -0
- package/src/components/searchTagSelect/SearchInput.vue +167 -0
- package/src/components/tag/EUITag.vue +46 -6
- package/dist/EUITelephone.vue.d.ts.map +0 -1
- package/dist/button/EUIButton.vue.d.ts.map +0 -1
- package/dist/button/EUIButtonGroup.vue.d.ts.map +0 -1
- /package/dist/{button/EUIButton.vue.d.ts → EUIButton.vue.d.ts} +0 -0
- /package/dist/{button/EUIButtonGroup.vue.d.ts → EUIButtonGroup.vue.d.ts} +0 -0
- /package/dist/{EUITelephone.vue.d.ts → telephone/EUITelephone.vue.d.ts} +0 -0
|
@@ -0,0 +1,637 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div ref="componentRef" class="relative w-full">
|
|
3
|
+
<slot v-if="$slots.label" name="label">
|
|
4
|
+
<label
|
|
5
|
+
v-if="label"
|
|
6
|
+
for="search-input"
|
|
7
|
+
class="block text-sm font-medium text-gray-700"
|
|
8
|
+
>
|
|
9
|
+
{{ label }} <span v-if="required" class="text-red-500">*</span>
|
|
10
|
+
</label>
|
|
11
|
+
</slot>
|
|
12
|
+
<Popper
|
|
13
|
+
:class="[`popper-defaultstyle w-full ${popperClasses}`]"
|
|
14
|
+
v-bind="$attrs"
|
|
15
|
+
:show="showDropDown"
|
|
16
|
+
arrow
|
|
17
|
+
closeDelay="100"
|
|
18
|
+
:offsetSkid="offsetSkid"
|
|
19
|
+
:offsetDistance="offsetDistance"
|
|
20
|
+
:placement="placement"
|
|
21
|
+
:interactive="true"
|
|
22
|
+
:auto-hide="true"
|
|
23
|
+
@open:popper="handleOpen"
|
|
24
|
+
@close:popper="handleClose"
|
|
25
|
+
>
|
|
26
|
+
<SearchInput
|
|
27
|
+
v-if="showDropDown || selectedValues.length === 0"
|
|
28
|
+
ref="searchInputRef"
|
|
29
|
+
id="search-input"
|
|
30
|
+
v-model:model-value="internalSearch"
|
|
31
|
+
input-type="defaultInput"
|
|
32
|
+
:icon="SearchBigZoomIn"
|
|
33
|
+
:placeholder="placeholder"
|
|
34
|
+
:disabled="disabled"
|
|
35
|
+
@focus="handleSearchFocus"
|
|
36
|
+
@click="handleOpen"
|
|
37
|
+
/>
|
|
38
|
+
<button
|
|
39
|
+
v-else-if="!showDropDown && selectedValues.length > 0"
|
|
40
|
+
type="button"
|
|
41
|
+
class="flex items-center justify-between w-full h-10 gap-2 px-4 py-2 text-left bg-white border border-gray-200 border-solid cursor-pointer search-textstyle rounded-3xl"
|
|
42
|
+
@click="handleOpen"
|
|
43
|
+
>
|
|
44
|
+
<component
|
|
45
|
+
v-if="searchIcon"
|
|
46
|
+
:is="SearchBigZoomIn"
|
|
47
|
+
class="flex-shrink-0 w-5 h-5 text-gray-500"
|
|
48
|
+
/>
|
|
49
|
+
<div class="flex-1 min-w-0">
|
|
50
|
+
<span
|
|
51
|
+
v-if="selectType === 'single'"
|
|
52
|
+
class="text-sm font-semibold text-current truncate"
|
|
53
|
+
>
|
|
54
|
+
{{ startCase(selectedValues[0][itemText] || selectedValues[0]) }}
|
|
55
|
+
</span>
|
|
56
|
+
<template v-else-if="selectType === 'multiple' && showCount">
|
|
57
|
+
<div
|
|
58
|
+
class="inline-flex items-center gap-2 py-1 pl-2 pr-1 text-xs font-semibold text-current bg-gray-100 rounded-3xl"
|
|
59
|
+
>
|
|
60
|
+
Selected Cities
|
|
61
|
+
<span class="show-count"> {{ selectedValues.length }}</span>
|
|
62
|
+
</div>
|
|
63
|
+
</template>
|
|
64
|
+
<span v-else class="text-sm font-normal text-gray-400">
|
|
65
|
+
{{ placeholder }}
|
|
66
|
+
</span>
|
|
67
|
+
</div>
|
|
68
|
+
<div class="inset-y-0 m-auto right-2.5 flex items-center gap-2">
|
|
69
|
+
<XMarkIcon
|
|
70
|
+
v-if="selectedValues.length > 0 && clearable"
|
|
71
|
+
class="w-4 h-4 text-gray-300 cursor-pointer hover:text-gray-500"
|
|
72
|
+
@click.stop="handleClearAll"
|
|
73
|
+
/>
|
|
74
|
+
<ChevronDownStroke
|
|
75
|
+
v-else
|
|
76
|
+
:class="[
|
|
77
|
+
'w-5 h-5 transform transition duration-200 ease-in-out',
|
|
78
|
+
showDropDown
|
|
79
|
+
? 'text-gray-800 rotate-180'
|
|
80
|
+
: 'text-gray-300 rotate-0',
|
|
81
|
+
]"
|
|
82
|
+
/>
|
|
83
|
+
</div>
|
|
84
|
+
</button>
|
|
85
|
+
<template #content>
|
|
86
|
+
<div
|
|
87
|
+
v-if="showDropDown"
|
|
88
|
+
class="w-full p-4 overflow-y-auto text-sm bg-white border-2 border-gray-100 border-solid shadow-lg rounded-3xl max-h-72 scrollbar--hide"
|
|
89
|
+
>
|
|
90
|
+
<h3
|
|
91
|
+
class="sticky z-10 p-2 mb-2 -mx-px -mt-3 font-semibold text-gray-500 bg-white text-sm/4 -top-4"
|
|
92
|
+
>
|
|
93
|
+
Search Results
|
|
94
|
+
</h3>
|
|
95
|
+
<ul
|
|
96
|
+
ref="options"
|
|
97
|
+
tabindex="-1"
|
|
98
|
+
role="listbox"
|
|
99
|
+
aria-labelledby="listbox-label"
|
|
100
|
+
aria-activedescendant="listbox-option-3"
|
|
101
|
+
class="space-y-0.5"
|
|
102
|
+
>
|
|
103
|
+
<li v-if="filteredItems.length === 0 && !hideNoData">
|
|
104
|
+
<slot name="no-items">
|
|
105
|
+
<div
|
|
106
|
+
v-if="internalSearch.length > 0"
|
|
107
|
+
class="px-2 py-4 mb-6 text-sm font-medium text-center text-gray-500 min-h-32"
|
|
108
|
+
>
|
|
109
|
+
No results found for "<strong>{{ internalSearch }}</strong
|
|
110
|
+
>"
|
|
111
|
+
</div>
|
|
112
|
+
</slot>
|
|
113
|
+
</li>
|
|
114
|
+
<li
|
|
115
|
+
v-for="(item, i) in filteredItems"
|
|
116
|
+
v-else
|
|
117
|
+
:key="`items-${i}`"
|
|
118
|
+
role="option"
|
|
119
|
+
:class="[
|
|
120
|
+
'transition-all duration-100 ease-in-out',
|
|
121
|
+
selectType === 'single' ? 'cursor-pointer' : '',
|
|
122
|
+
]"
|
|
123
|
+
@click="
|
|
124
|
+
selectType === 'single'
|
|
125
|
+
? selected(item)
|
|
126
|
+
? handleDeselect(item)
|
|
127
|
+
: handleSelect(item)
|
|
128
|
+
: null
|
|
129
|
+
"
|
|
130
|
+
@click.stop
|
|
131
|
+
>
|
|
132
|
+
<!-- Single Select Item -->
|
|
133
|
+
<template v-if="selectType === 'single'">
|
|
134
|
+
<slot name="item" :item="item">
|
|
135
|
+
<div
|
|
136
|
+
:class="[
|
|
137
|
+
selected(item)
|
|
138
|
+
? 'bg-purple-50 text-purple-600'
|
|
139
|
+
: 'text-gray-600 hover:bg-gray-50 hover:text-gray-800',
|
|
140
|
+
'relative py-2 px-4 flex items-center capitalize text-sm transition-colors duration-75 rounded',
|
|
141
|
+
]"
|
|
142
|
+
>
|
|
143
|
+
<span
|
|
144
|
+
:class="[
|
|
145
|
+
selected(item) ? 'font-semibold mr-5' : 'font-medium',
|
|
146
|
+
'block tracking-tight',
|
|
147
|
+
]"
|
|
148
|
+
>
|
|
149
|
+
{{ startCase(item[itemText] || item) }}
|
|
150
|
+
</span>
|
|
151
|
+
<span
|
|
152
|
+
v-if="selected(item)"
|
|
153
|
+
class="absolute inset-y-0 right-0 flex items-center pr-4"
|
|
154
|
+
>
|
|
155
|
+
<CheckTick
|
|
156
|
+
class="text-current size-5"
|
|
157
|
+
aria-hidden="true"
|
|
158
|
+
/>
|
|
159
|
+
</span>
|
|
160
|
+
</div>
|
|
161
|
+
</slot>
|
|
162
|
+
</template>
|
|
163
|
+
|
|
164
|
+
<!-- Multiple Select Item -->
|
|
165
|
+
<template v-else-if="selectType === 'multiple'">
|
|
166
|
+
<slot name="item" :item="item">
|
|
167
|
+
<EUICheckbox
|
|
168
|
+
:model-value="selected(item)"
|
|
169
|
+
name="checkbox"
|
|
170
|
+
:class="[
|
|
171
|
+
selected(item)
|
|
172
|
+
? 'text-gray-800 hover:bg-purple-50 hover:text-purple-600'
|
|
173
|
+
: 'text-gray-600 hover:bg-gray-50 hover:text-gray-800',
|
|
174
|
+
'relative flex items-center capitalize text-sm transition-colors duration-75 w-full multiple-select rounded-md font-medium [&_label]:w-full [&_label]:p-2.5',
|
|
175
|
+
]"
|
|
176
|
+
@update:model-value="
|
|
177
|
+
(checked) => handleCheckboxChange(item, checked)
|
|
178
|
+
"
|
|
179
|
+
>
|
|
180
|
+
<template #label>
|
|
181
|
+
<span class="ms-2 first-letter:capitalize">{{
|
|
182
|
+
startCase(item[itemText] || item)
|
|
183
|
+
}}</span>
|
|
184
|
+
</template>
|
|
185
|
+
</EUICheckbox>
|
|
186
|
+
</slot>
|
|
187
|
+
</template>
|
|
188
|
+
</li>
|
|
189
|
+
</ul>
|
|
190
|
+
</div>
|
|
191
|
+
</template>
|
|
192
|
+
</Popper>
|
|
193
|
+
<div v-if="errors && errors.length > 0 && !showDropDown" class="error-msg">
|
|
194
|
+
{{ typeof errors[0] === "object" ? errors[0].$message : errors[0] }}
|
|
195
|
+
</div>
|
|
196
|
+
<slot v-if="$slots.labelhint" name="labelhint"></slot>
|
|
197
|
+
<div
|
|
198
|
+
v-if="selectType === 'multiple' && selectedValues.length > 0 && showTags"
|
|
199
|
+
class="px-0.5 my-2 inline-flex gap-x-3 gap-y-2 items-center flex-wrap"
|
|
200
|
+
>
|
|
201
|
+
<slot
|
|
202
|
+
v-for="(item, index) in selectedValues"
|
|
203
|
+
name="selection"
|
|
204
|
+
:item="item"
|
|
205
|
+
:clear="() => handleDeselect(item)"
|
|
206
|
+
:key="`selected-values-${index}`"
|
|
207
|
+
>
|
|
208
|
+
<EUITag
|
|
209
|
+
class="capitalize"
|
|
210
|
+
:disabled="disabled"
|
|
211
|
+
closeIcon
|
|
212
|
+
size="md"
|
|
213
|
+
color="secondary"
|
|
214
|
+
@remove="handleTagRemove(item)"
|
|
215
|
+
>
|
|
216
|
+
{{ startCase(item[itemText] || item) }}
|
|
217
|
+
</EUITag>
|
|
218
|
+
</slot>
|
|
219
|
+
</div>
|
|
220
|
+
<div
|
|
221
|
+
class="absolute top-0 px-2 text-xs text-white rounded right-6"
|
|
222
|
+
:class="[
|
|
223
|
+
{ 'bg-gray-400': tagColor === 'None' },
|
|
224
|
+
{ 'bg-green-400': tagColor === 'Success' },
|
|
225
|
+
{ 'bg-red-400': tagColor === 'Error' },
|
|
226
|
+
]"
|
|
227
|
+
v-if="tag"
|
|
228
|
+
>
|
|
229
|
+
{{ tag }}
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
</template>
|
|
233
|
+
|
|
234
|
+
<script setup lang="ts">
|
|
235
|
+
import { computed, ref, watch, nextTick, onMounted, onUnmounted } from "vue";
|
|
236
|
+
import { startCase } from "lodash";
|
|
237
|
+
import { ErrorObject } from "../../utils/types";
|
|
238
|
+
import SearchInput from "./SearchInput.vue";
|
|
239
|
+
import SearchBigZoomIn from "../../assets/svg/SearchBigZoomIn.vue";
|
|
240
|
+
import ChevronDownStroke from "../../assets/svg/ChevronDownStroke.vue";
|
|
241
|
+
import EUICheckbox from "../checkbox/EUICheckbox.vue";
|
|
242
|
+
import EUITag from "../tag/EUITag.vue";
|
|
243
|
+
import Popper from "vue3-popper";
|
|
244
|
+
import CheckTick from "../../assets/svg/CheckTick.vue";
|
|
245
|
+
|
|
246
|
+
// Props
|
|
247
|
+
interface Props {
|
|
248
|
+
modelValue?: any;
|
|
249
|
+
items?: any[];
|
|
250
|
+
label?: string;
|
|
251
|
+
placement?:
|
|
252
|
+
| "auto"
|
|
253
|
+
| "auto-start"
|
|
254
|
+
| "auto-end"
|
|
255
|
+
| "top"
|
|
256
|
+
| "top-start"
|
|
257
|
+
| "top-end"
|
|
258
|
+
| "bottom"
|
|
259
|
+
| "bottom-start"
|
|
260
|
+
| "bottom-end"
|
|
261
|
+
| "right"
|
|
262
|
+
| "right-start"
|
|
263
|
+
| "right-end"
|
|
264
|
+
| "left"
|
|
265
|
+
| "left-start"
|
|
266
|
+
| "left-end";
|
|
267
|
+
itemText?: string;
|
|
268
|
+
placeholder?: string;
|
|
269
|
+
offsetSkid?: string;
|
|
270
|
+
offsetDistance?: string;
|
|
271
|
+
errors?: Array<string | ErrorObject>;
|
|
272
|
+
selectType?: "single" | "multiple";
|
|
273
|
+
filterFunction?: Function;
|
|
274
|
+
searchable?: boolean;
|
|
275
|
+
backendPagination?: boolean;
|
|
276
|
+
hideNoData?: boolean;
|
|
277
|
+
disabled?: boolean;
|
|
278
|
+
tag?: string;
|
|
279
|
+
tagColor?: "None" | "Success" | "Error";
|
|
280
|
+
required?: boolean;
|
|
281
|
+
classes?: string;
|
|
282
|
+
searchIcon?: boolean;
|
|
283
|
+
showTags?: boolean;
|
|
284
|
+
clearable?: boolean;
|
|
285
|
+
ignoreDeselect?: boolean;
|
|
286
|
+
popperClasses?: string;
|
|
287
|
+
showCount?: boolean;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
291
|
+
items: () => [],
|
|
292
|
+
itemText: "name",
|
|
293
|
+
placeholder: "Please Select",
|
|
294
|
+
offsetDistance: "4",
|
|
295
|
+
offsetSkid: "0",
|
|
296
|
+
filterFunction: () => {},
|
|
297
|
+
selectType: "single",
|
|
298
|
+
searchable: true,
|
|
299
|
+
backendPagination: false,
|
|
300
|
+
hideNoData: false,
|
|
301
|
+
disabled: false,
|
|
302
|
+
tag: "",
|
|
303
|
+
tagColor: "None",
|
|
304
|
+
required: false,
|
|
305
|
+
classes: "",
|
|
306
|
+
searchIcon: false,
|
|
307
|
+
showTags: true,
|
|
308
|
+
clearable: true,
|
|
309
|
+
placement: "bottom-start",
|
|
310
|
+
ignoreDeselect: false,
|
|
311
|
+
popperClasses: "",
|
|
312
|
+
showCount: false,
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// Emits
|
|
316
|
+
const emit = defineEmits<{
|
|
317
|
+
"update:modelValue": [value: any];
|
|
318
|
+
blur: [];
|
|
319
|
+
search: [value: string];
|
|
320
|
+
create: [value: string];
|
|
321
|
+
"on-change": [value: any];
|
|
322
|
+
"on-deselect": [value: any];
|
|
323
|
+
toggle: [value?: any];
|
|
324
|
+
}>();
|
|
325
|
+
|
|
326
|
+
// Refs
|
|
327
|
+
const internalSearch = ref("");
|
|
328
|
+
const showDropDown = ref(false);
|
|
329
|
+
const options = ref<HTMLElement>();
|
|
330
|
+
const searchInputRef = ref<any>();
|
|
331
|
+
const componentRef = ref<HTMLElement>();
|
|
332
|
+
const isOpening = ref(false);
|
|
333
|
+
|
|
334
|
+
// Computed properties
|
|
335
|
+
const selectedValues = computed({
|
|
336
|
+
get(): any {
|
|
337
|
+
const value: any = props.modelValue;
|
|
338
|
+
if (!value) return [];
|
|
339
|
+
return [].concat(value);
|
|
340
|
+
},
|
|
341
|
+
set(value: any) {
|
|
342
|
+
emit("update:modelValue", value);
|
|
343
|
+
},
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
const selected = computed(() => {
|
|
347
|
+
return (item: any) => {
|
|
348
|
+
if (typeof item === "object")
|
|
349
|
+
return Boolean(
|
|
350
|
+
selectedValues.value.find(
|
|
351
|
+
(x: any) => x[props.itemText] === item[props.itemText]
|
|
352
|
+
)
|
|
353
|
+
);
|
|
354
|
+
if (typeof item === "string") return selectedValues.value.includes(item);
|
|
355
|
+
};
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
export type SelectItemKey =
|
|
359
|
+
| string
|
|
360
|
+
| (string | number)[]
|
|
361
|
+
| ((item: object, fallback?: any) => any);
|
|
362
|
+
|
|
363
|
+
function getNestedValue(
|
|
364
|
+
obj: any,
|
|
365
|
+
path: (string | number)[],
|
|
366
|
+
fallback?: any
|
|
367
|
+
): any {
|
|
368
|
+
const last = path.length - 1;
|
|
369
|
+
|
|
370
|
+
if (last < 0) return obj === undefined ? fallback : obj;
|
|
371
|
+
|
|
372
|
+
for (let i = 0; i < last; i++) {
|
|
373
|
+
if (obj == null) {
|
|
374
|
+
return fallback;
|
|
375
|
+
}
|
|
376
|
+
obj = obj[path[i]];
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (obj == null) return fallback;
|
|
380
|
+
|
|
381
|
+
return obj[path[last]] === undefined ? fallback : obj[path[last]];
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function getObjectValueByPath(obj: any, path: string, fallback?: any): any {
|
|
385
|
+
if (obj == null || !path || typeof path !== "string") return fallback;
|
|
386
|
+
if (obj[path] !== undefined) return obj[path];
|
|
387
|
+
path = path.replace(/\[(\w+)\]/g, ".$1"); // convert indexes to properties
|
|
388
|
+
path = path.replace(/^\./, ""); // strip a leading dot
|
|
389
|
+
return getNestedValue(obj, path.split("."), fallback);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function getPropertyFromItem(
|
|
393
|
+
item: object,
|
|
394
|
+
property: SelectItemKey,
|
|
395
|
+
fallback?: any
|
|
396
|
+
): any {
|
|
397
|
+
if (property == null) return item === undefined ? fallback : item;
|
|
398
|
+
|
|
399
|
+
if (item !== Object(item)) return fallback === undefined ? item : fallback;
|
|
400
|
+
|
|
401
|
+
if (typeof property === "string")
|
|
402
|
+
return getObjectValueByPath(item, property, fallback);
|
|
403
|
+
|
|
404
|
+
if (Array.isArray(property)) return getNestedValue(item, property, fallback);
|
|
405
|
+
|
|
406
|
+
if (typeof property !== "function") return fallback;
|
|
407
|
+
|
|
408
|
+
const value = property(item, fallback);
|
|
409
|
+
|
|
410
|
+
return typeof value === "undefined" ? fallback : value;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const filteredItems = computed(() => {
|
|
414
|
+
let items = props.items?.slice();
|
|
415
|
+
const search = internalSearch.value.trim().toLowerCase();
|
|
416
|
+
|
|
417
|
+
if (internalSearch.value !== "") {
|
|
418
|
+
if (search === "") return [];
|
|
419
|
+
if (props.backendPagination) return props.items;
|
|
420
|
+
items = props.items.filter((option: any) => {
|
|
421
|
+
const value = getPropertyFromItem(option, props.itemText);
|
|
422
|
+
const text = value != null ? String(value).toLowerCase() : "";
|
|
423
|
+
return text.includes(search);
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
return items;
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
const handleDeselect = (item: any) => {
|
|
430
|
+
// console.log("called with:", item, selectedValues.value);
|
|
431
|
+
if (props.ignoreDeselect) return;
|
|
432
|
+
let index = -1;
|
|
433
|
+
if (typeof item === "object" && item[props.itemText]) {
|
|
434
|
+
index = selectedValues.value.findIndex(
|
|
435
|
+
(x: any) => x[props.itemText] === item[props.itemText]
|
|
436
|
+
);
|
|
437
|
+
} else {
|
|
438
|
+
index = selectedValues.value.indexOf(item);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if (index > -1) {
|
|
442
|
+
const newValues = [...selectedValues.value];
|
|
443
|
+
newValues.splice(index, 1);
|
|
444
|
+
emit("update:modelValue", newValues);
|
|
445
|
+
nextTick(() => {
|
|
446
|
+
if (selectedValues.value.length !== newValues.length) {
|
|
447
|
+
selectedValues.value = newValues;
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (!props.selectType || props.selectType === "single") {
|
|
453
|
+
handleClose();
|
|
454
|
+
}
|
|
455
|
+
emit("on-deselect", item);
|
|
456
|
+
emit("toggle");
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
const handleClearAll = () => {
|
|
460
|
+
emit("update:modelValue", []);
|
|
461
|
+
emit("toggle");
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
const handleTagRemove = (item: any) => {
|
|
465
|
+
handleDeselect(item);
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
const handleCheckboxChange = (item: any, checked: boolean) => {
|
|
469
|
+
if (checked) {
|
|
470
|
+
handleSelect(item);
|
|
471
|
+
} else {
|
|
472
|
+
handleDeselect(item);
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
const handleUpdate = (items: any) => {
|
|
477
|
+
if (typeof items === "object") {
|
|
478
|
+
if (Array.isArray(items)) {
|
|
479
|
+
if (items.length == 0) {
|
|
480
|
+
emit("update:modelValue", "");
|
|
481
|
+
} else {
|
|
482
|
+
emit("update:modelValue", items);
|
|
483
|
+
}
|
|
484
|
+
} else {
|
|
485
|
+
emit("update:modelValue", items);
|
|
486
|
+
}
|
|
487
|
+
} else if (items.length > 0) {
|
|
488
|
+
emit("update:modelValue", items);
|
|
489
|
+
} else {
|
|
490
|
+
emit("update:modelValue", "");
|
|
491
|
+
}
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
const handleSelect = (item: any) => {
|
|
495
|
+
let newSelection: any;
|
|
496
|
+
|
|
497
|
+
if (props.selectType === "multiple" && typeof item === "object") {
|
|
498
|
+
const findObj = selectedValues.value.find(
|
|
499
|
+
(x: any) => x[props.itemText] === item[props.itemText]
|
|
500
|
+
);
|
|
501
|
+
if (findObj) {
|
|
502
|
+
newSelection = selectedValues.value.filter(
|
|
503
|
+
(x: any) => x[props.itemText] !== item[props.itemText]
|
|
504
|
+
);
|
|
505
|
+
} else {
|
|
506
|
+
newSelection = selectedValues.value.concat(item);
|
|
507
|
+
}
|
|
508
|
+
emit("on-change", [...newSelection]);
|
|
509
|
+
} else if (props.selectType === "multiple" && typeof item === "string") {
|
|
510
|
+
if (selectedValues.value.includes(item)) {
|
|
511
|
+
return handleDeselect(item);
|
|
512
|
+
}
|
|
513
|
+
newSelection = selectedValues.value.concat(item);
|
|
514
|
+
emit("on-change", [...newSelection]);
|
|
515
|
+
} else {
|
|
516
|
+
// Single select
|
|
517
|
+
newSelection = item;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Only clear search for single select, keep it for multiple select
|
|
521
|
+
if (props.selectType === "single") {
|
|
522
|
+
internalSearch.value = "";
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
handleUpdate(newSelection);
|
|
526
|
+
if (!props.selectType || props.selectType === "single") {
|
|
527
|
+
handleClose();
|
|
528
|
+
}
|
|
529
|
+
emit("toggle", newSelection);
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
const handleOpen = () => {
|
|
533
|
+
isOpening.value = true;
|
|
534
|
+
|
|
535
|
+
// Only clear search for single select, preserve it for multiple select
|
|
536
|
+
if (props.selectType === "single") {
|
|
537
|
+
internalSearch.value = "";
|
|
538
|
+
}
|
|
539
|
+
showDropDown.value = true;
|
|
540
|
+
|
|
541
|
+
nextTick(() => {
|
|
542
|
+
setTimeout(() => {
|
|
543
|
+
internalSearch.value = "";
|
|
544
|
+
isOpening.value = false;
|
|
545
|
+
if (searchInputRef.value?.searchInput) {
|
|
546
|
+
searchInputRef.value.searchInput.focus();
|
|
547
|
+
} else if (searchInputRef.value?.$el) {
|
|
548
|
+
const inputElement = searchInputRef.value.$el.querySelector("input");
|
|
549
|
+
if (inputElement) {
|
|
550
|
+
inputElement.focus();
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}, 50);
|
|
554
|
+
});
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
const handleClose = () => {
|
|
558
|
+
if (showDropDown.value) {
|
|
559
|
+
emit("blur");
|
|
560
|
+
}
|
|
561
|
+
showDropDown.value = false;
|
|
562
|
+
|
|
563
|
+
// Only clear search for single select, keep it for multiple select
|
|
564
|
+
if (props.selectType === "single") {
|
|
565
|
+
internalSearch.value = "";
|
|
566
|
+
}
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
const handleSearchFocus = () => {
|
|
570
|
+
internalSearch.value = "";
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
// Document click handler for outside clicks
|
|
574
|
+
const handleDocumentClick = (event: MouseEvent) => {
|
|
575
|
+
// Don't close if we're currently opening
|
|
576
|
+
if (isOpening.value) return;
|
|
577
|
+
|
|
578
|
+
const target = event.target as Node;
|
|
579
|
+
|
|
580
|
+
// Check if click is outside the component and popper
|
|
581
|
+
if (showDropDown.value && componentRef.value) {
|
|
582
|
+
// Check if click is outside the main component
|
|
583
|
+
const isOutsideComponent = !componentRef.value.contains(target);
|
|
584
|
+
|
|
585
|
+
// Check if click is outside any popper content
|
|
586
|
+
const popperElements = document.querySelectorAll(
|
|
587
|
+
'[data-popper-placement], .popper-defaultstyle, [role="tooltip"]'
|
|
588
|
+
);
|
|
589
|
+
const isOutsidePopper = Array.from(popperElements).every(
|
|
590
|
+
(popper) => !popper.contains(target)
|
|
591
|
+
);
|
|
592
|
+
|
|
593
|
+
if (isOutsideComponent && isOutsidePopper) {
|
|
594
|
+
handleClose();
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
// Lifecycle hooks
|
|
600
|
+
onMounted(() => {
|
|
601
|
+
document.addEventListener("click", handleDocumentClick);
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
onUnmounted(() => {
|
|
605
|
+
document.removeEventListener("click", handleDocumentClick);
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
// Watchers
|
|
609
|
+
watch(internalSearch, (value: string) => {
|
|
610
|
+
emit("search", value);
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
watch(
|
|
614
|
+
() => props.modelValue,
|
|
615
|
+
(newValue, oldValue) => {
|
|
616
|
+
console.log("changed from:", oldValue, "to:", newValue);
|
|
617
|
+
},
|
|
618
|
+
{ deep: true }
|
|
619
|
+
);
|
|
620
|
+
</script>
|
|
621
|
+
|
|
622
|
+
<style lang="scss">
|
|
623
|
+
.popper-defaultstyle {
|
|
624
|
+
border: 0px solid transparent !important;
|
|
625
|
+
margin: 0px !important;
|
|
626
|
+
@apply w-full;
|
|
627
|
+
|
|
628
|
+
& > div.popper {
|
|
629
|
+
width: 100% !important;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
.show-count {
|
|
634
|
+
min-width: 20px;
|
|
635
|
+
@apply h-5 inline-flex items-center justify-center text-xss font-semibold bg-white rounded-xl;
|
|
636
|
+
}
|
|
637
|
+
</style>
|