ketekny-ui-kit 1.0.139 → 1.0.140
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 +1 -1
- package/src/ui/kSelect.vue +54 -3
package/package.json
CHANGED
package/src/ui/kSelect.vue
CHANGED
|
@@ -17,13 +17,14 @@
|
|
|
17
17
|
:placeholder="placeholder"
|
|
18
18
|
class="block pr-10 truncate text-gray-700 data-[placeholder]:text-gray-400 dark:text-slate-100 dark:data-[placeholder]:text-slate-500"
|
|
19
19
|
/>
|
|
20
|
-
<div v-else class="flex
|
|
20
|
+
<div v-else ref="selectedContainer" class="flex items-center gap-0.5 pr-10 overflow-hidden whitespace-nowrap">
|
|
21
21
|
<span v-if="selectedOptions.length === 0" class="text-gray-400 dark:text-slate-500">
|
|
22
22
|
{{ placeholder }}
|
|
23
23
|
</span>
|
|
24
24
|
<kChip
|
|
25
|
-
v-for="option in
|
|
25
|
+
v-for="option in visibleSelectedOptions"
|
|
26
26
|
:key="option[optionValue]"
|
|
27
|
+
class="shrink-0"
|
|
27
28
|
size="small"
|
|
28
29
|
>
|
|
29
30
|
<span>{{ option[optionLabel] }}</span>
|
|
@@ -38,6 +39,9 @@
|
|
|
38
39
|
<X class="w-3 h-3" />
|
|
39
40
|
</button>
|
|
40
41
|
</kChip>
|
|
42
|
+
<span v-if="hiddenSelectedCount > 0" class="text-xs text-gray-500 shrink-0 dark:text-slate-400">
|
|
43
|
+
+{{ hiddenSelectedCount }}...
|
|
44
|
+
</span>
|
|
41
45
|
</div>
|
|
42
46
|
|
|
43
47
|
<button
|
|
@@ -112,6 +116,14 @@
|
|
|
112
116
|
</SelectPortal>
|
|
113
117
|
</SelectRoot>
|
|
114
118
|
|
|
119
|
+
<div ref="chipMeasureContainer" class="fixed invisible flex items-center gap-0.5 pointer-events-none" aria-hidden="true">
|
|
120
|
+
<kChip v-for="option in selectedOptions" :key="option[optionValue]" size="small">
|
|
121
|
+
<span>{{ option[optionLabel] }}</span>
|
|
122
|
+
<X class="w-3 h-3 ml-1" />
|
|
123
|
+
</kChip>
|
|
124
|
+
<span ref="overflowLabelMeasure" class="text-xs">+{{ selectedOptions.length }}...</span>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
115
127
|
<div class="mt-1 text-sm text-red-500 dark:text-rose-400" v-if="hasError && error !== true && error !== ''">
|
|
116
128
|
{{ error }}
|
|
117
129
|
</div>
|
|
@@ -123,7 +135,7 @@
|
|
|
123
135
|
</template>
|
|
124
136
|
|
|
125
137
|
<script setup>
|
|
126
|
-
import { computed, nextTick, ref, watch } from "vue";
|
|
138
|
+
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue";
|
|
127
139
|
import {
|
|
128
140
|
SelectContent,
|
|
129
141
|
SelectItem,
|
|
@@ -161,6 +173,11 @@ const open = ref(false);
|
|
|
161
173
|
const searchQuery = ref("");
|
|
162
174
|
const searchInput = ref(null);
|
|
163
175
|
const viewportRef = ref(null);
|
|
176
|
+
const selectedContainer = ref(null);
|
|
177
|
+
const chipMeasureContainer = ref(null);
|
|
178
|
+
const overflowLabelMeasure = ref(null);
|
|
179
|
+
const visibleSelectedCount = ref(0);
|
|
180
|
+
let selectedContainerObserver;
|
|
164
181
|
const generatedId = `select-${Math.random().toString(36).substr(2, 9)}`;
|
|
165
182
|
const defaultStyle = "w-full px-3 py-2 border rounded-lg transition shadow-sm focus-visible:outline-none text-gray-700 focus-visible:ring-2 focus-visible:ring-primary/20 focus-visible:border-primary bg-white placeholder-gray-400 dark:bg-slate-800 dark:text-slate-100 dark:border-slate-600 dark:placeholder-slate-500";
|
|
166
183
|
const errorStyle = "border-red-500 focus-visible:ring focus-visible:ring-red-300 dark:border-rose-500 dark:focus-visible:ring-rose-500/20";
|
|
@@ -213,6 +230,40 @@ const selectedOptions = computed(() => {
|
|
|
213
230
|
.map((value) => props.options.find((option) => option?.[props.optionValue] === value))
|
|
214
231
|
.filter(Boolean);
|
|
215
232
|
});
|
|
233
|
+
const visibleSelectedOptions = computed(() => selectedOptions.value.slice(0, visibleSelectedCount.value));
|
|
234
|
+
const hiddenSelectedCount = computed(() => selectedOptions.value.length - visibleSelectedOptions.value.length);
|
|
235
|
+
|
|
236
|
+
function updateVisibleSelectedCount() {
|
|
237
|
+
if (!props.multiple || !selectedContainer.value || !chipMeasureContainer.value) return;
|
|
238
|
+
|
|
239
|
+
const chipElements = Array.from(chipMeasureContainer.value.children).slice(0, selectedOptions.value.length);
|
|
240
|
+
const availableWidth = selectedContainer.value.clientWidth;
|
|
241
|
+
const overflowLabelWidth = overflowLabelMeasure.value?.offsetWidth ?? 0;
|
|
242
|
+
const gap = 2;
|
|
243
|
+
let usedWidth = 0;
|
|
244
|
+
let count = 0;
|
|
245
|
+
|
|
246
|
+
for (const chip of chipElements) {
|
|
247
|
+
const nextWidth = usedWidth + (count > 0 ? gap : 0) + chip.offsetWidth;
|
|
248
|
+
const remainingAfterNext = selectedOptions.value.length - count - 1;
|
|
249
|
+
const reservedWidth = remainingAfterNext > 0 ? gap + overflowLabelWidth : 0;
|
|
250
|
+
if (nextWidth + reservedWidth > availableWidth) break;
|
|
251
|
+
usedWidth = nextWidth;
|
|
252
|
+
count += 1;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
visibleSelectedCount.value = count;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
onMounted(() => {
|
|
259
|
+
selectedContainerObserver = new ResizeObserver(updateVisibleSelectedCount);
|
|
260
|
+
if (selectedContainer.value) selectedContainerObserver.observe(selectedContainer.value);
|
|
261
|
+
nextTick(updateVisibleSelectedCount);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
onBeforeUnmount(() => selectedContainerObserver?.disconnect());
|
|
265
|
+
|
|
266
|
+
watch(selectedOptions, () => nextTick(updateVisibleSelectedCount), { deep: true });
|
|
216
267
|
|
|
217
268
|
const internalValue = computed({
|
|
218
269
|
get() {
|