Merge branch 'tt-position-manager/add-drag-drop' into 'master'
added drag and drop See merge request fronk/thetool!1670
This commit is contained in:
@@ -67,3 +67,21 @@
|
||||
padding: 0.3rem 0.6rem; /* Small button padding */
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.positions-manager.ctrl-pressed .position-row {
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.positions-manager.ctrl-pressed .position-row:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.position-row.dragging {
|
||||
opacity: 0.5;
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
/* Style for the drop indicator */
|
||||
.drop-indicator {
|
||||
border-top: 2px solid #007bff !important;
|
||||
}
|
||||
|
||||
@@ -37,30 +37,33 @@ Vue.component('tt-resolver', {
|
||||
|
||||
Vue.component('tt-positions-manager', {
|
||||
props: {
|
||||
value: {type: [Array, String], required: false},
|
||||
config: {type: Object, required: true},
|
||||
groupMode: {type: Boolean, default: false},
|
||||
value: {type: [Array, String], required: false},
|
||||
config: {type: Object, required: true},
|
||||
groupMode: {type: Boolean, default: false},
|
||||
submitLoading: {type: Boolean, default: false}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
window: window,
|
||||
positions: this.value,
|
||||
formData: {},
|
||||
groupName: '',
|
||||
selectedIndex: null,
|
||||
window: window,
|
||||
positions: this.value,
|
||||
formData: {},
|
||||
groupName: '',
|
||||
selectedIndex: null,
|
||||
editingGroupName: null,
|
||||
tempGroupName: '',
|
||||
// New state for instant editing
|
||||
tempGroupName: '',
|
||||
editingCell: {
|
||||
groupName: null, // For grouped positions
|
||||
index: null, // Index within the group or flat array
|
||||
key: null // Field key being edited
|
||||
}
|
||||
groupName: null,
|
||||
index: null,
|
||||
key: null
|
||||
},
|
||||
// --- New properties for Drag and Drop ---
|
||||
ctrlPressed: false, // Is the Ctrl key currently pressed?
|
||||
draggingItem: null, // The actual position object being dragged
|
||||
dragOverItem: null // The position object the mouse is currently over
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div class="positions-manager">
|
||||
<div class="positions-manager" :class="{'ctrl-pressed': ctrlPressed}">
|
||||
<template v-if="config['header']">
|
||||
<h4 class="text-center">{{ config["header"] }}</h4>
|
||||
</template>
|
||||
@@ -144,7 +147,6 @@ Vue.component('tt-positions-manager', {
|
||||
<tt-input label="Gruppenname" v-model="groupName" sm/>
|
||||
<tt-button @click="addGroup" sm text="Gruppe hinzufügen" additional-class="btn-primary"/>
|
||||
</div>
|
||||
|
||||
<table class="table table-striped table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -153,9 +155,13 @@ Vue.component('tt-positions-manager', {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
<template v-for="(group, groupName) in positionsToRender">
|
||||
<tr v-if="groupMode">
|
||||
<tr v-if="groupMode"
|
||||
class="group-header-row"
|
||||
@dragover.prevent="dragOverItem = groupName"
|
||||
@dragleave="dragOverItem = null"
|
||||
@drop="onDrop(groupName)"
|
||||
:class="{'drop-indicator': dragOverItem === groupName}">
|
||||
<td :colspan="Object.keys(config.fields).length + 1">
|
||||
<div class="d-flex justify-content-center align-items-center">
|
||||
<template v-if="editingGroupName === groupName">
|
||||
@@ -173,26 +179,38 @@ Vue.component('tt-positions-manager', {
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-for="(position, index) in group" :key="groupMode ? groupName + index : index">
|
||||
<tr v-for="position in group"
|
||||
:key="positions.indexOf(position)"
|
||||
class="position-row"
|
||||
:class="{
|
||||
'dragging': draggingItem === position,
|
||||
'drop-indicator': dragOverItem === position
|
||||
}"
|
||||
:draggable="ctrlPressed"
|
||||
@dragstart="onDragStart(position, $event)"
|
||||
@dragend="onDragEnd"
|
||||
@dragover.prevent="dragOverItem = position"
|
||||
@dragleave="dragOverItem = null"
|
||||
@drop="onDrop(position)">
|
||||
|
||||
<td v-for="(field, key) in config.fields"
|
||||
:class="{'editable-cell': field.editableInTable && (field.type === 'input' || field.type === 'textarea')}"
|
||||
@click="startCellEdit(groupName, index, key, field)">
|
||||
|
||||
<template v-if="isCellEditing(groupName, index, key)">
|
||||
@click="startCellEdit(groupName, positions.indexOf(position), key, field)">
|
||||
<template v-if="isCellEditing(groupName, positions.indexOf(position), key)">
|
||||
<input
|
||||
v-if="field.type === 'input'"
|
||||
:type="field.inputType || 'text'"
|
||||
v-model="position[key]"
|
||||
@blur="saveCellEdit(position, key, groupName, index)"
|
||||
@keyup.enter="saveCellEdit(position, key, groupName, index)"
|
||||
@blur="saveCellEdit(position, key)"
|
||||
@keyup.enter="saveCellEdit(position, key)"
|
||||
class="form-control form-control-sm"
|
||||
autofocus
|
||||
/>
|
||||
<textarea
|
||||
v-else-if="field.type === 'textarea'"
|
||||
v-model="position[key]"
|
||||
@blur="saveCellEdit(position, key, groupName, index)"
|
||||
@keyup.enter.prevent="saveCellEdit(position, key, groupName, index)"
|
||||
@blur="saveCellEdit(position, key)"
|
||||
@keyup.enter.prevent="saveCellEdit(position, key)"
|
||||
class="form-control form-control-sm"
|
||||
rows="1"
|
||||
autofocus
|
||||
@@ -200,7 +218,7 @@ Vue.component('tt-positions-manager', {
|
||||
</template>
|
||||
<template v-else>
|
||||
<tt-resolver
|
||||
:key="index + key + position[key]"
|
||||
:key="positions.indexOf(position) + key + position[key]"
|
||||
v-if="position[key] && (field.customFieldReference || field.type === 'autocomplete')"
|
||||
:autocomplete="!field.customFieldReference && field.type === 'autocomplete'"
|
||||
:reference="field.customFieldReference || field.apiUrl"
|
||||
@@ -211,21 +229,73 @@ Vue.component('tt-positions-manager', {
|
||||
</template>
|
||||
</td>
|
||||
<td class="d-flex justify-content-end">
|
||||
<select v-if="groupMode" v-model="position._group" @change="$set(position, '_group', $event.target.value)">
|
||||
<option v-for="group in allGroups" :value="group">{{ group }}</option>
|
||||
<select v-if="groupMode" :value="position._group || 'Keine Gruppe'" @change="$set(position, '_group', $event.target.value)">
|
||||
<option v-for="g in allGroups" :value="g">{{ g }}</option>
|
||||
</select>
|
||||
<tt-button @click="editEntry(getActualIndex(position, groupName, index))" sm additional-class="btn-primary" icon="fa fa-edit"/>
|
||||
<tt-button @click="deleteEntry(getActualIndex(position, groupName, index))" sm additional-class="btn-danger" icon="fa fa-trash"/>
|
||||
<tt-button @click="editEntry(position)" sm additional-class="btn-primary" icon="fa fa-edit"/>
|
||||
<tt-button @click="deleteEntry(position)" sm additional-class="btn-danger" icon="fa fa-trash"/>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
`,
|
||||
methods: {
|
||||
methods: {
|
||||
// --- NEW DRAG AND DROP METHODS ---
|
||||
handleKeyDown(event) {
|
||||
if (event.key === 'Control' || event.key === 'Meta') { // Meta is for MacOS Command key
|
||||
this.ctrlPressed = true;
|
||||
}
|
||||
},
|
||||
handleKeyUp(event) {
|
||||
if (event.key === 'Control' || event.key === 'Meta') {
|
||||
this.ctrlPressed = false;
|
||||
}
|
||||
},
|
||||
onDragStart(position, event) {
|
||||
this.draggingItem = position;
|
||||
// Necessary for Firefox to enable drag
|
||||
event.dataTransfer.setData('text/plain', '');
|
||||
event.dataTransfer.effectAllowed = 'move';
|
||||
},
|
||||
onDrop(targetItemOrGroup) {
|
||||
if (!this.draggingItem) return;
|
||||
|
||||
const draggedIndex = this.positions.indexOf(this.draggingItem);
|
||||
// Remove item from its original position
|
||||
const itemToMove = this.positions.splice(draggedIndex, 1)[0];
|
||||
|
||||
let targetIndex;
|
||||
let newGroup;
|
||||
|
||||
if (typeof targetItemOrGroup === 'string') {
|
||||
// Dropped on a group header
|
||||
newGroup = targetItemOrGroup;
|
||||
const firstItemOfGroup = this.positions.find(p => (p._group || 'Keine Gruppe') === newGroup);
|
||||
targetIndex = firstItemOfGroup ? this.positions.indexOf(firstItemOfGroup) : this.positions.length;
|
||||
} else {
|
||||
// Dropped on another position item
|
||||
newGroup = targetItemOrGroup._group || 'Keine Gruppe';
|
||||
targetIndex = this.positions.indexOf(targetItemOrGroup);
|
||||
}
|
||||
|
||||
// Update group and insert at the new position
|
||||
this.$set(itemToMove, '_group', newGroup);
|
||||
this.positions.splice(targetIndex, 0, itemToMove);
|
||||
|
||||
this.$emit('input', this.positions);
|
||||
this.cleanupDrag();
|
||||
},
|
||||
onDragEnd() {
|
||||
this.cleanupDrag();
|
||||
},
|
||||
cleanupDrag() {
|
||||
this.draggingItem = null;
|
||||
this.dragOverItem = null;
|
||||
},
|
||||
|
||||
// --- EXISTING METHODS (some are simplified) ---
|
||||
updateField(key, value) {
|
||||
this.$set(this.formData, key, value);
|
||||
},
|
||||
@@ -287,7 +357,6 @@ Vue.component('tt-positions-manager', {
|
||||
}
|
||||
|
||||
if (this.tempGroupName !== oldGroupName) {
|
||||
// Update all positions with the old group name to the new group name
|
||||
this.positions.forEach(position => {
|
||||
if (position._group === oldGroupName) {
|
||||
this.$set(position, '_group', this.tempGroupName);
|
||||
@@ -305,14 +374,11 @@ Vue.component('tt-positions-manager', {
|
||||
},
|
||||
deleteGroup(groupName) {
|
||||
if (confirm(`Möchten Sie die Gruppe "${groupName}" wirklich löschen? Alle Einträge werden in "Keine Gruppe" verschoben.`)) {
|
||||
// Move all items from this group to "Keine Gruppe" and remove empty group entries
|
||||
this.positions = this.positions.filter(position => {
|
||||
if (position._group === groupName) {
|
||||
// If it's just a group header (only has _group property), remove it
|
||||
if (Object.keys(position).length === 1) {
|
||||
return false;
|
||||
}
|
||||
// Otherwise, move to "Keine Gruppe"
|
||||
this.$set(position, '_group', 'Keine Gruppe');
|
||||
}
|
||||
return true;
|
||||
@@ -320,34 +386,15 @@ Vue.component('tt-positions-manager', {
|
||||
this.$emit('input', this.positions);
|
||||
}
|
||||
},
|
||||
getActualIndex(position, groupName, groupIndex) {
|
||||
// Find the actual index in the positions array
|
||||
if (!this.groupMode) return groupIndex;
|
||||
async editEntry(position) { // Simplified to take position object
|
||||
const index = this.positions.indexOf(position);
|
||||
if (index === -1) return;
|
||||
|
||||
let actualIndex = 0;
|
||||
let currentGroupIndex = 0;
|
||||
|
||||
for (let i = 0; i < this.positions.length; i++) {
|
||||
const pos = this.positions[i];
|
||||
const posGroup = pos._group ?? 'Keine Gruppe';
|
||||
|
||||
if (posGroup === groupName && Object.keys(pos).length > 1) {
|
||||
if (currentGroupIndex === groupIndex) {
|
||||
return i;
|
||||
}
|
||||
currentGroupIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
return groupIndex; // Fallback
|
||||
},
|
||||
async editEntry(index) {
|
||||
this.selectedIndex = index;
|
||||
for (const [key, field] of Object.entries(this.config.fields)) {
|
||||
if (field.type === 'autocomplete' && field.emitDisplayValue) {
|
||||
await this.$nextTick();
|
||||
if (this.positions[index][key + '_text'] && this.$refs['autocomplete-' + key][0]) {
|
||||
console.log('inhere');
|
||||
this.$refs['autocomplete-' + key][0].displayValue = this.positions[index][key + '_text'];
|
||||
this.$set(this.formData, key, this.positions[index][key]);
|
||||
}
|
||||
@@ -355,14 +402,16 @@ Vue.component('tt-positions-manager', {
|
||||
}
|
||||
this.formData = {...this.positions[index]};
|
||||
|
||||
// Scroll to the form container
|
||||
this.$nextTick(() => {
|
||||
this.$refs.formContainer.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
});
|
||||
},
|
||||
deleteEntry(index) {
|
||||
this.positions.splice(index, 1);
|
||||
this.$emit('input', this.positions);
|
||||
deleteEntry(position) { // Simplified to take position object
|
||||
const index = this.positions.indexOf(position);
|
||||
if (index > -1) {
|
||||
this.positions.splice(index, 1);
|
||||
this.$emit('input', this.positions);
|
||||
}
|
||||
},
|
||||
resetForm() {
|
||||
this.formData = {};
|
||||
@@ -383,7 +432,6 @@ Vue.component('tt-positions-manager', {
|
||||
if (field.inputType === 'date') return moment(value).format('DD.MM.YYYY');
|
||||
return value;
|
||||
},
|
||||
// New methods for instant editing
|
||||
startCellEdit(groupName, index, key, field) {
|
||||
if (!field.editableInTable || !(field.type === 'input' || field.type === 'textarea')) return;
|
||||
this.editingCell = { groupName, index, key };
|
||||
@@ -394,20 +442,19 @@ Vue.component('tt-positions-manager', {
|
||||
}
|
||||
});
|
||||
},
|
||||
saveCellEdit(position, key, groupName, index) {
|
||||
saveCellEdit(position, key) {
|
||||
this.editingCell = { groupName: null, index: null, key: null };
|
||||
// Ensure data is updated in the original positions array
|
||||
const actualIndex = this.getActualIndex(position, groupName, index);
|
||||
this.$set(this.positions, actualIndex, position);
|
||||
// Since `position` is an object from the array, it's already reactive.
|
||||
// We just emit the change.
|
||||
this.$emit('input', this.positions);
|
||||
},
|
||||
isCellEditing(groupName, index, key) {
|
||||
return this.editingCell.groupName === groupName &&
|
||||
const currentGroup = (this.positions[index] && this.positions[index]._group) || 'Keine Gruppe';
|
||||
return this.editingCell.groupName === currentGroup &&
|
||||
this.editingCell.index === index &&
|
||||
this.editingCell.key === key;
|
||||
}
|
||||
},
|
||||
//TODO: cleanup
|
||||
created() {
|
||||
if (this.config["customMethods"]) Object.assign(this, this.config.customMethods);
|
||||
if (!this.positions) this.positions = [];
|
||||
@@ -422,14 +469,39 @@ Vue.component('tt-positions-manager', {
|
||||
},
|
||||
mounted() {
|
||||
this.resetForm();
|
||||
// Add listeners for the Ctrl key
|
||||
window.addEventListener('keydown', this.handleKeyDown);
|
||||
window.addEventListener('keyup', this.handleKeyUp);
|
||||
window.addEventListener('blur', () => { this.ctrlPressed = false; }); // Reset if window loses focus
|
||||
},
|
||||
beforeDestroy() {
|
||||
// Important: remove listeners to prevent memory leaks
|
||||
window.removeEventListener('keydown', this.handleKeyDown);
|
||||
window.removeEventListener('keyup', this.handleKeyUp);
|
||||
},
|
||||
computed: {
|
||||
groupedPositions() {
|
||||
const groups = {};
|
||||
// Ensure there's a default group for items without one
|
||||
if (this.positions.some(p => !p._group && Object.keys(p).length > 1)) {
|
||||
groups['Keine Gruppe'] = [];
|
||||
}
|
||||
|
||||
// Get all unique group names first to preserve order
|
||||
const groupNames = [...new Set(this.positions.map(p => p._group).filter(Boolean))];
|
||||
groupNames.forEach(name => {
|
||||
groups[name] = [];
|
||||
});
|
||||
|
||||
for (const position of this.positions) {
|
||||
const group = position._group ?? 'Keine Gruppe';
|
||||
if (!groups[group]) groups[group] = [];
|
||||
if (Object.keys(position).length !== 1) groups[group].push(position);
|
||||
// Ignore empty group placeholders
|
||||
if (Object.keys(position).length === 1 && position._group) continue;
|
||||
|
||||
const groupName = position._group || 'Keine Gruppe';
|
||||
if (!groups[groupName]) {
|
||||
groups[groupName] = [];
|
||||
}
|
||||
groups[groupName].push(position);
|
||||
}
|
||||
return groups;
|
||||
},
|
||||
@@ -437,15 +509,15 @@ Vue.component('tt-positions-manager', {
|
||||
return this.groupMode ? this.groupedPositions : {'': this.positions};
|
||||
},
|
||||
allGroups() {
|
||||
return Object.keys(this.groupedPositions);
|
||||
return ['Keine Gruppe', ...Object.keys(this.groupedPositions).filter(g => g !== 'Keine Gruppe')];
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
watch: {
|
||||
value: {
|
||||
handler() {
|
||||
this.positions = this.value;
|
||||
handler(newValue) {
|
||||
this.positions = newValue;
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user