diff --git a/public/plugins/vue/tt-components/css/tt-position-manager.css b/public/plugins/vue/tt-components/css/tt-position-manager.css
index d9b702ee8..8b7bd4090 100644
--- a/public/plugins/vue/tt-components/css/tt-position-manager.css
+++ b/public/plugins/vue/tt-components/css/tt-position-manager.css
@@ -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;
+}
diff --git a/public/plugins/vue/tt-components/tt-position-manager.js b/public/plugins/vue/tt-components/tt-position-manager.js
index 50d676a3c..4910c2c05 100644
--- a/public/plugins/vue/tt-components/tt-position-manager.js
+++ b/public/plugins/vue/tt-components/tt-position-manager.js
@@ -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: `
-
+
{{ config["header"] }}
@@ -144,7 +147,6 @@ Vue.component('tt-positions-manager', {
-
`,
- 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
}
}
-});
\ No newline at end of file
+})