242 lines
8.3 KiB
JavaScript
242 lines
8.3 KiB
JavaScript
Vue.component('tt-select', {
|
|
props: {
|
|
options: { type: Array, required: true },
|
|
label: { type: String, default: null },
|
|
required: { type: Boolean, default: false },
|
|
value: { type: [String, Number, Array], default: null },
|
|
disabled: { type: Boolean, default: false },
|
|
suffix: { type: String, default: null },
|
|
sm: { type: Boolean, default: false },
|
|
row: { type: Boolean, default: false },
|
|
multiple: { type: Boolean, default: false },
|
|
searchable: { type: Boolean, default: true }
|
|
},
|
|
|
|
data() {
|
|
return {
|
|
open: false,
|
|
searchQuery: '',
|
|
// internal model: array if multiple, else primitive
|
|
selected: this.multiple
|
|
? (Array.isArray(this.value) ? [...this.value] : [])
|
|
: this.value
|
|
};
|
|
},
|
|
|
|
watch: {
|
|
// keep internal model in sync with external v-model
|
|
value(newVal) {
|
|
this.selected = this.multiple
|
|
? (Array.isArray(newVal) ? [...newVal] : [])
|
|
: newVal;
|
|
},
|
|
|
|
// Enhancement: Handle focus and scroll on open
|
|
open(isOpen) {
|
|
if (isOpen) {
|
|
this.$nextTick(() => {
|
|
if (this.$refs.dropdownMenu) {
|
|
// 2. Scroll to the top when opened
|
|
this.$refs.dropdownMenu.scrollTop = 0;
|
|
}
|
|
if (this.searchable && this.$refs.searchInput) {
|
|
// 1. Focus the input when opened
|
|
this.$refs.searchInput.focus();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
},
|
|
|
|
mounted() {
|
|
document.addEventListener('click', this.onClickOutside);
|
|
},
|
|
|
|
beforeDestroy() {
|
|
document.removeEventListener('click', this.onClickOutside);
|
|
},
|
|
|
|
computed: {
|
|
// normalize all options to { value, text, disabled }
|
|
normalizedOptions() {
|
|
return this.options.map(opt => {
|
|
if (opt !== null && typeof opt === 'object') {
|
|
return {
|
|
value: opt.value,
|
|
text: opt.text,
|
|
disabled: !!opt.disabled
|
|
};
|
|
}
|
|
return {
|
|
value: opt,
|
|
text: String(opt),
|
|
disabled: false
|
|
};
|
|
});
|
|
},
|
|
|
|
// filter by searchQuery
|
|
filteredOptions() {
|
|
if (!this.searchable || !this.searchQuery) {
|
|
return this.normalizedOptions;
|
|
}
|
|
const q = this.searchQuery.toLowerCase();
|
|
return this.normalizedOptions.filter(o =>
|
|
o.text.toLowerCase().includes(q)
|
|
);
|
|
},
|
|
|
|
// what to show on the button
|
|
displayText() {
|
|
if (this.multiple) {
|
|
const arr = Array.isArray(this.selected) ? this.selected : [];
|
|
const count = arr.length;
|
|
if (count === 0) return '';
|
|
if (count === 1) {
|
|
const o = this.normalizedOptions.find(x => x.value === arr[0]);
|
|
return o
|
|
? o.text + (this.suffix ? ' ' + this.suffix : '')
|
|
: '';
|
|
}
|
|
// more than one selected
|
|
return `${count} ausgewählt`;
|
|
}
|
|
// single select
|
|
const sel = this.normalizedOptions.find(o => o.value === this.selected);
|
|
return sel
|
|
? sel.text + (this.suffix ? ' ' + this.suffix : '')
|
|
: '';
|
|
}
|
|
},
|
|
|
|
methods: {
|
|
toggleDropdown() {
|
|
if (this.disabled) return;
|
|
this.open = !this.open;
|
|
},
|
|
|
|
onClickOutside(e) {
|
|
if (!this.$el.contains(e.target)) {
|
|
this.open = false;
|
|
}
|
|
},
|
|
|
|
isSelected(opt) {
|
|
return this.multiple
|
|
? Array.isArray(this.selected) && this.selected.includes(opt.value)
|
|
: this.selected === opt.value;
|
|
},
|
|
|
|
selectOption(opt) {
|
|
if (opt.disabled) return;
|
|
if (this.multiple) {
|
|
const arr = Array.isArray(this.selected) ? [...this.selected] : [];
|
|
const idx = arr.indexOf(opt.value);
|
|
if (idx > -1) arr.splice(idx, 1);
|
|
else arr.push(opt.value);
|
|
|
|
this.selected = arr;
|
|
this.$emit('input', arr);
|
|
} else {
|
|
this.selected = opt.value;
|
|
this.$emit('input', opt.value);
|
|
this.open = false;
|
|
}
|
|
},
|
|
|
|
// Enhancement: Handle Enter key press on search input
|
|
handleEnter() {
|
|
// 4. If search is active and only one result is left, select it
|
|
if (this.searchQuery && this.filteredOptions.length === 1) {
|
|
this.selectOption(this.filteredOptions[0]);
|
|
// Ensure dropdown closes for both single and multi-select mode
|
|
this.open = false;
|
|
}
|
|
}
|
|
},
|
|
|
|
template: `
|
|
<div class="form-group" :class="{ row: row }">
|
|
<label v-if="label"
|
|
:for="label"
|
|
:class="{
|
|
'col-form-label': row,
|
|
'col-sm-4': row,
|
|
'col-form-label-sm': sm && row
|
|
}">
|
|
{{ label }}
|
|
</label>
|
|
|
|
<div :class="row ? 'col-sm-8 p-0' : ''">
|
|
<div class="dropdown" :class="{ show: open }">
|
|
<button type="button"
|
|
class="btn btn-outline-secondary form-control"
|
|
:class="{ 'form-control-sm': sm, 'btn-sm': sm }"
|
|
:disabled="disabled"
|
|
@click.stop="toggleDropdown"
|
|
style="display:flex; align-items:center;">
|
|
<span style="
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
flex: 1;
|
|
text-align: left;
|
|
">
|
|
{{ displayText || 'Auswählen...' }}
|
|
</span>
|
|
<i class="fas fa-chevron-down ml-2"></i>
|
|
</button>
|
|
|
|
<div class="dropdown-menu p-2"
|
|
ref="dropdownMenu"
|
|
:class="{ show: open }"
|
|
style="
|
|
min-width: 100%;
|
|
max-width: 400px;
|
|
max-height: 300px;
|
|
overflow-y: auto;
|
|
">
|
|
<input v-if="searchable"
|
|
ref="searchInput"
|
|
type="text"
|
|
class="form-control mb-2"
|
|
v-model="searchQuery"
|
|
:disabled="disabled"
|
|
placeholder="Suchen..."
|
|
@keydown.enter.prevent="handleEnter">
|
|
|
|
<div v-if="!filteredOptions.length && searchQuery" class="dropdown-item disabled text-muted">
|
|
Keine Ergebnisse gefunden.
|
|
</div>
|
|
|
|
<div v-for="opt in filteredOptions"
|
|
:key="opt.value"
|
|
class="dropdown-item px-2 py-1"
|
|
:class="{ disabled: opt.disabled }"
|
|
@click.stop>
|
|
<label class="d-flex align-items-center m-0 w-100" style="cursor: pointer;">
|
|
<input v-if="multiple"
|
|
type="checkbox"
|
|
class="form-check-input mr-2"
|
|
:checked="isSelected(opt)"
|
|
:disabled="opt.disabled"
|
|
@change.prevent="selectOption(opt)">
|
|
<input v-else
|
|
type="radio"
|
|
class="form-check-input mr-2"
|
|
:name="label"
|
|
:value="opt.value"
|
|
:checked="isSelected(opt)"
|
|
:disabled="opt.disabled"
|
|
@change.prevent="selectOption(opt)">
|
|
<span>
|
|
{{ opt.text }}<span v-if="suffix"> {{ suffix }}</span>
|
|
</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`
|
|
}); |