offer fix
This commit is contained in:
@@ -23,16 +23,16 @@
|
||||
}
|
||||
|
||||
.positions-manager .form-container .button-wrapper {
|
||||
align-self: flex-end; /* Align button container at the bottom */
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
.positions-manager .form-container [class*="tt-input"],
|
||||
.positions-manager .form-container [class*="tt-checkbox"] {
|
||||
flex: 1 1; /* Flexible width with a minimum of 200px */
|
||||
flex: 1 1;
|
||||
}
|
||||
|
||||
.positions-manager .form-container .btn {
|
||||
align-self: flex-end; /* Align button at the bottom of the form area */
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
.positions-manager table {
|
||||
@@ -51,11 +51,11 @@
|
||||
.positions-manager table th,
|
||||
.positions-manager table td {
|
||||
padding: 8px;
|
||||
vertical-align: middle; /* Vertically center text */
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.positions-manager table tbody tr:nth-child(even) {
|
||||
background-color: #f9f9f9; /* Alternate row color */
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.positions-manager table .btn {
|
||||
@@ -64,24 +64,26 @@
|
||||
|
||||
.positions-manager .form-container .btn-sm,
|
||||
.positions-manager table .btn.btn-sm {
|
||||
padding: 0.3rem 0.6rem; /* Small button padding */
|
||||
padding: 0.3rem 0.6rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.positions-manager.ctrl-pressed .position-row {
|
||||
.positions-manager.ctrl-pressed .position-row,
|
||||
.positions-manager.ctrl-pressed .group-header-row {
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.positions-manager.ctrl-pressed .position-row:active {
|
||||
.positions-manager.ctrl-pressed .position-row:active,
|
||||
.positions-manager.ctrl-pressed .group-header-row:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.position-row.dragging {
|
||||
.position-row.dragging,
|
||||
.group-header-row.dragging {
|
||||
opacity: 0.5;
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
/* Style for the drop indicator */
|
||||
.drop-indicator {
|
||||
border-top: 2px solid #007bff !important;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user