Rml workorder/v2 need improvements
This commit is contained in:
@@ -1,50 +1,42 @@
|
||||
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 }
|
||||
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
|
||||
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;
|
||||
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();
|
||||
}
|
||||
});
|
||||
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;
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -57,55 +49,42 @@ Vue.component('tt-select', {
|
||||
},
|
||||
|
||||
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)
|
||||
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}
|
||||
);
|
||||
},
|
||||
|
||||
// what to show on the button
|
||||
filteredOptions() {
|
||||
if (!this.searchable || !this.searchQuery) return this.normalizedOptions;
|
||||
const q = this.searchQuery.toLowerCase();
|
||||
return this.normalizedOptions.filter(o => o.text.toLowerCase().includes(q));
|
||||
},
|
||||
|
||||
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`;
|
||||
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...';
|
||||
}
|
||||
// single select
|
||||
const sel = this.normalizedOptions.find(o => o.value === this.selected);
|
||||
return sel
|
||||
? sel.text + (this.suffix ? ' ' + this.suffix : '')
|
||||
: '';
|
||||
|
||||
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 !== '';
|
||||
}
|
||||
},
|
||||
|
||||
@@ -116,7 +95,7 @@ Vue.component('tt-select', {
|
||||
},
|
||||
|
||||
onClickOutside(e) {
|
||||
if (!this.$el.contains(e.target)) {
|
||||
if (this.$el && !this.$el.contains(e.target)) {
|
||||
this.open = false;
|
||||
}
|
||||
},
|
||||
@@ -129,111 +108,80 @@ Vue.component('tt-select', {
|
||||
|
||||
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);
|
||||
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.$emit('input', opt.value);
|
||||
this.open = false;
|
||||
this.open = false; // Close on selection for single mode
|
||||
}
|
||||
this.$emit('input', this.selected);
|
||||
},
|
||||
|
||||
// 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;
|
||||
if (!this.multiple) 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
|
||||
}">
|
||||
<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="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 }"
|
||||
<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"
|
||||
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>
|
||||
@click.stop="toggleDropdown">
|
||||
<span class="trigger-text" :class="{'placeholder': !hasSelection}">{{ displayText }}</span>
|
||||
<i class="fas fa-chevron-down trigger-arrow"></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 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-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 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>
|
||||
|
||||
Reference in New Issue
Block a user