210 lines
7.6 KiB
JavaScript
210 lines
7.6 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: '',
|
|
selected: this.multiple ? (Array.isArray(this.value) ? [...this.value] : []) : this.value
|
|
};
|
|
},
|
|
|
|
watch: {
|
|
value(newVal) {
|
|
this.selected = this.multiple ? (Array.isArray(newVal) ? [...newVal] : []) : newVal;
|
|
},
|
|
open(isOpen) {
|
|
if (!isOpen) {
|
|
this.searchQuery = ''; // Reset search on close
|
|
return;
|
|
}
|
|
this.$nextTick(() => {
|
|
if (this.searchable && this.$refs.searchInput) {
|
|
this.$refs.searchInput.focus();
|
|
}
|
|
if (this.$refs.optionsList) {
|
|
this.$refs.optionsList.scrollTop = 0;
|
|
}
|
|
});
|
|
}
|
|
},
|
|
|
|
mounted() {
|
|
document.addEventListener('click', this.onClickOutside);
|
|
document.addEventListener('tt-select-open', this.handleGlobalOpen);
|
|
},
|
|
|
|
beforeDestroy() {
|
|
document.removeEventListener('click', this.onClickOutside);
|
|
document.removeEventListener('tt-select-open', this.handleGlobalOpen);
|
|
},
|
|
|
|
computed: {
|
|
normalizedOptions() {
|
|
return this.options.map(opt =>
|
|
(opt !== null && typeof opt === 'object')
|
|
? {value: opt.value, text: opt.text, disabled: !!opt.disabled}
|
|
: {value: opt, text: String(opt), disabled: false}
|
|
);
|
|
},
|
|
|
|
filteredOptions() {
|
|
const displayLimit = 100;
|
|
|
|
if (!this.searchable || !this.searchQuery) {
|
|
return this.normalizedOptions.slice(0, displayLimit);
|
|
}
|
|
|
|
const q = this.searchQuery.toLowerCase();
|
|
return this.normalizedOptions
|
|
.filter(option => option.text.toLowerCase().includes(q))
|
|
.slice(0, displayLimit);
|
|
},
|
|
|
|
displayText() {
|
|
const addSuffix = text => text ? `${text}${this.suffix ? ` ${this.suffix}` : ''}` : '';
|
|
|
|
if (!this.multiple) {
|
|
const opt = this.normalizedOptions.find(o => o.value === this.selected);
|
|
return opt ? addSuffix(opt.text) : 'Auswählen...';
|
|
}
|
|
|
|
const count = Array.isArray(this.selected) ? this.selected.length : 0;
|
|
if (count === 0) return 'Auswählen...';
|
|
if (count === 1) {
|
|
const opt = this.normalizedOptions.find(o => o.value === this.selected[0]);
|
|
return opt ? addSuffix(opt.text) : '';
|
|
}
|
|
return `${count} ausgewählt`;
|
|
},
|
|
|
|
hasSelection() {
|
|
if (this.multiple) {
|
|
return Array.isArray(this.selected) && this.selected.length > 0;
|
|
}
|
|
return this.selected !== null && this.selected !== undefined && this.selected !== '';
|
|
}
|
|
},
|
|
|
|
methods: {
|
|
toggleDropdown() {
|
|
if (this.disabled) return;
|
|
if (!this.open) {
|
|
this.$emit('focus');
|
|
// Dispatch event that this select is opening
|
|
document.dispatchEvent(new CustomEvent('tt-select-open', { detail: { id: this._uid } }));
|
|
}
|
|
this.open = !this.open;
|
|
},
|
|
|
|
onClickOutside(e) {
|
|
if (this.$el && !this.$el.contains(e.target)) {
|
|
this.open = false;
|
|
}
|
|
},
|
|
|
|
handleGlobalOpen(event) {
|
|
if (event.detail.id !== this._uid) {
|
|
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 newSelection = Array.isArray(this.selected) ? [...this.selected] : [];
|
|
const index = newSelection.indexOf(opt.value);
|
|
if (index > -1) {
|
|
newSelection.splice(index, 1);
|
|
} else {
|
|
newSelection.push(opt.value);
|
|
}
|
|
this.selected = newSelection;
|
|
} else {
|
|
this.selected = opt.value;
|
|
this.open = false; // Close on selection for single mode
|
|
}
|
|
this.$emit('input', this.selected);
|
|
},
|
|
|
|
handleEnter() {
|
|
if (this.searchQuery && this.filteredOptions.length === 1) {
|
|
this.selectOption(this.filteredOptions[0]);
|
|
if (!this.multiple) this.open = false;
|
|
}
|
|
}
|
|
},
|
|
|
|
template: `
|
|
<div class="form-group" :class="[{ 'row': row }, 'tt-select-modern']">
|
|
<label v-if="label" :for="'tt-select-' + _uid"
|
|
:class="['col-form-label', { 'col-sm-4': row, 'col-form-label-sm': sm && row }]">
|
|
{{ label }}
|
|
</label>
|
|
|
|
<div :class="{ 'col-sm-8 p-0': row }">
|
|
<div class="tt-select-container">
|
|
<button type="button" class="tt-select-trigger"
|
|
:id="'tt-select-' + _uid"
|
|
:class="{ 'sm': sm, 'open': open }"
|
|
:disabled="disabled"
|
|
@click.stop="toggleDropdown">
|
|
<span class="trigger-text" :class="{'placeholder': !hasSelection}">{{ displayText }}</span>
|
|
<i class="fas fa-chevron-down trigger-arrow"></i>
|
|
</button>
|
|
|
|
<div class="tt-select-dropdown" :class="{ show: open }" @click.stop>
|
|
<div class="dropdown-header">
|
|
<span>{{ label || 'Auswählen' }}</span>
|
|
<button type="button" class="close-btn" @click="toggleDropdown">×</button>
|
|
</div>
|
|
|
|
<div v-if="searchable" class="search-input-wrapper">
|
|
<input ref="searchInput" type="text" class="search-input"
|
|
v-model="searchQuery" :disabled="disabled" placeholder="Suchen..."
|
|
@keydown.enter.prevent="handleEnter">
|
|
</div>
|
|
|
|
<ul class="options-list" ref="optionsList">
|
|
<li v-if="!filteredOptions.length" class="no-results">
|
|
Keine Ergebnisse gefunden.
|
|
</li>
|
|
<li v-for="opt in filteredOptions" :key="opt.value"
|
|
class="option-item"
|
|
:class="{ 'selected': isSelected(opt), 'disabled': opt.disabled }"
|
|
@click="selectOption(opt)">
|
|
<div class="option-label">
|
|
<input :type="multiple ? 'checkbox' : 'radio'"
|
|
:name="'option-' + _uid"
|
|
:value="opt.value"
|
|
:checked="isSelected(opt)"
|
|
:disabled="opt.disabled">
|
|
<span>{{ opt.text }}<span v-if="suffix"> {{ suffix }}</span></span>
|
|
</div>
|
|
<i v-if="isSelected(opt)" class="fas fa-check option-checkmark"></i>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`
|
|
}); |