offer fix

This commit is contained in:
2025-10-16 08:12:54 +02:00
parent 7a85e5821b
commit e6d9260441
7 changed files with 1165 additions and 1184 deletions
@@ -56,10 +56,10 @@ Vue.component('tt-positions-manager', {
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
ctrlPressed: false,
draggingItem: null,
dragOverItem: null,
draggingGroup: null
}
},
template: `
@@ -127,7 +127,6 @@ Vue.component('tt-positions-manager', {
/>
<slot :name="key + '-prepend'" v-bind:field="field" v-bind:value="formData[key]"/>
</div>
</slot>
</template>
</template>
@@ -138,11 +137,8 @@ Vue.component('tt-positions-manager', {
:additional-class="selectedIndex === null ? 'btn-primary' : 'btn-success'"
:text="selectedIndex === null ? 'Hinzufügen' : 'Aktualisieren'"/>
</div>
<slot name="form-actions-append"></slot>
</div>
<div class="form-container" v-if="groupMode">
<tt-input label="Gruppenname" v-model="groupName" sm/>
<tt-button @click="addGroup" sm text="Gruppe hinzufügen" additional-class="btn-primary"/>
@@ -158,10 +154,16 @@ Vue.component('tt-positions-manager', {
<template v-for="(group, groupName) in positionsToRender">
<tr v-if="groupMode"
class="group-header-row"
:class="{
'dragging': draggingGroup === groupName,
'drop-indicator': dragOverItem === groupName && draggingGroup && draggingGroup !== groupName
}"
:draggable="ctrlPressed"
@dragstart="onGroupDragStart(groupName, $event)"
@dragend="onDragEnd"
@dragover.prevent="dragOverItem = groupName"
@dragleave="dragOverItem = null"
@drop="onDrop(groupName)"
:class="{'drop-indicator': dragOverItem === groupName}">
@drop.stop="onDrop(groupName)">
<td :colspan="Object.keys(config.fields).length + 1">
<div class="d-flex justify-content-center align-items-center">
<template v-if="editingGroupName === groupName">
@@ -184,15 +186,14 @@ Vue.component('tt-positions-manager', {
class="position-row"
:class="{
'dragging': draggingItem === position,
'drop-indicator': dragOverItem === position
'drop-indicator': dragOverItem === position && draggingItem
}"
:draggable="ctrlPressed"
@dragstart="onDragStart(position, $event)"
@dragend="onDragEnd"
@dragover.prevent="dragOverItem = position"
@dragleave="dragOverItem = null"
@drop="onDrop(position)">
@drop.stop="onDrop(position)">
<td v-for="(field, key) in config.fields"
:class="{'editable-cell': field.editableInTable && (field.type === 'input' || field.type === 'textarea')}"
@click="startCellEdit(groupName, positions.indexOf(position), key, field)">
@@ -242,49 +243,57 @@ Vue.component('tt-positions-manager', {
</div>
`,
methods: {
// --- NEW DRAG AND DROP METHODS ---
handleKeyDown(event) {
if (event.key === 'Control' || event.key === 'Meta') { // Meta is for MacOS Command key
this.ctrlPressed = true;
}
if (event.key === 'Control' || event.key === 'Meta') this.ctrlPressed = true;
},
handleKeyUp(event) {
if (event.key === 'Control' || event.key === 'Meta') {
this.ctrlPressed = false;
}
if (event.key === 'Control' || event.key === 'Meta') this.ctrlPressed = false;
},
onDragStart(position, event) {
this.draggingItem = position;
// Necessary for Firefox to enable drag
this.draggingGroup = null;
event.dataTransfer.setData('text/plain', '');
event.dataTransfer.effectAllowed = 'move';
},
onGroupDragStart(groupName, event) {
this.draggingGroup = groupName;
this.draggingItem = null;
event.dataTransfer.setData('text/plain', '');
event.dataTransfer.effectAllowed = 'move';
},
onDrop(targetItemOrGroup) {
if (!this.draggingItem) return;
if (this.draggingItem) {
const draggedIndex = this.positions.indexOf(this.draggingItem);
const itemToMove = this.positions.splice(draggedIndex, 1)[0];
let targetIndex, newGroup;
if (typeof targetItemOrGroup === 'string') {
newGroup = targetItemOrGroup;
const firstItemOfGroup = this.positions.find(p => (p._group || 'Keine Gruppe') === newGroup);
targetIndex = firstItemOfGroup ? this.positions.indexOf(firstItemOfGroup) : this.positions.length;
} else {
newGroup = targetItemOrGroup._group || 'Keine Gruppe';
targetIndex = this.positions.indexOf(targetItemOrGroup);
}
this.$set(itemToMove, '_group', newGroup);
this.positions.splice(targetIndex, 0, itemToMove);
this.$emit('input', this.positions);
} else if (this.draggingGroup) {
const targetGroupName = typeof targetItemOrGroup === 'string'
? targetItemOrGroup
: (targetItemOrGroup._group || 'Keine Gruppe');
if (this.draggingGroup === targetGroupName) return this.cleanupDrag();
const draggedIndex = this.positions.indexOf(this.draggingItem);
// Remove item from its original position
const itemToMove = this.positions.splice(draggedIndex, 1)[0];
const groupOrder = [...this.allGroups];
const fromIndex = groupOrder.indexOf(this.draggingGroup);
const [movedGroup] = groupOrder.splice(fromIndex, 1);
const toIndex = groupOrder.indexOf(targetGroupName);
groupOrder.splice(toIndex, 0, movedGroup);
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);
const newPositions = groupOrder.flatMap(groupName =>
this.positions.filter(p => (p._group || 'Keine Gruppe') === groupName)
);
this.$emit('input', newPositions);
}
// 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() {
@@ -293,9 +302,8 @@ Vue.component('tt-positions-manager', {
cleanupDrag() {
this.draggingItem = null;
this.dragOverItem = null;
this.draggingGroup = null;
},
// --- EXISTING METHODS (some are simplified) ---
updateField(key, value) {
this.$set(this.formData, key, value);
},
@@ -376,9 +384,7 @@ Vue.component('tt-positions-manager', {
if (confirm(`Möchten Sie die Gruppe "${groupName}" wirklich löschen? Alle Einträge werden in "Keine Gruppe" verschoben.`)) {
this.positions = this.positions.filter(position => {
if (position._group === groupName) {
if (Object.keys(position).length === 1) {
return false;
}
if (Object.keys(position).length === 1) return false;
this.$set(position, '_group', 'Keine Gruppe');
}
return true;
@@ -386,7 +392,7 @@ Vue.component('tt-positions-manager', {
this.$emit('input', this.positions);
}
},
async editEntry(position) { // Simplified to take position object
async editEntry(position) {
const index = this.positions.indexOf(position);
if (index === -1) return;
@@ -406,7 +412,7 @@ Vue.component('tt-positions-manager', {
this.$refs.formContainer.scrollIntoView({ behavior: 'smooth', block: 'start' });
});
},
deleteEntry(position) { // Simplified to take position object
deleteEntry(position) {
const index = this.positions.indexOf(position);
if (index > -1) {
this.positions.splice(index, 1);
@@ -437,15 +443,11 @@ Vue.component('tt-positions-manager', {
this.editingCell = { groupName, index, key };
this.$nextTick(() => {
const inputElement = this.$el.querySelector(`.editable-cell input[type="${field.type}"], .editable-cell textarea`);
if (inputElement) {
inputElement.focus();
}
if (inputElement) inputElement.focus();
});
},
saveCellEdit(position, key) {
this.editingCell = { groupName: null, index: null, key: null };
// 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) {
@@ -469,38 +471,28 @@ 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
window.addEventListener('blur', () => { this.ctrlPressed = false; });
},
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] = [];
});
groupNames.forEach(name => groups[name] = []);
for (const position of this.positions) {
// Ignore empty group placeholders
if (Object.keys(position).length === 1 && position._group) continue;
const groupName = position._group || 'Keine Gruppe';
if (!groups[groupName]) {
groups[groupName] = [];
}
if (!groups[groupName]) groups[groupName] = [];
groups[groupName].push(position);
}
return groups;
@@ -509,7 +501,16 @@ Vue.component('tt-positions-manager', {
return this.groupMode ? this.groupedPositions : {'': this.positions};
},
allGroups() {
return ['Keine Gruppe', ...Object.keys(this.groupedPositions).filter(g => g !== 'Keine Gruppe')];
const groupOrder = [];
const seenGroups = new Set();
for (const pos of this.positions) {
const groupName = pos._group || 'Keine Gruppe';
if (!seenGroups.has(groupName)) {
seenGroups.add(groupName);
groupOrder.push(groupName);
}
}
return groupOrder;
}
},
watch: {
@@ -520,4 +521,4 @@ Vue.component('tt-positions-manager', {
deep: true
}
}
})
})