From 951842c824fd108f61f84fda217ee3e32b96a85e Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Wed, 8 Apr 2026 15:09:47 +0200 Subject: [PATCH] feat(Dialog): enhance dialog functionality with resizing and improved positioning - Added support for dialog resizing, allowing users to adjust the size of non-modal dialogs. - Implemented dynamic positioning for dialogs based on user-defined coordinates or calculated defaults. - Updated Vuex store to manage dialog dimensions and positions effectively. - Enhanced the BaseDialog component with resize handles and improved drag-and-drop functionality. - Updated localization strings to include new resize functionality for better user guidance. --- frontend/src/components/BaseDialog.vue | 133 ++++++++++++++- frontend/src/components/DialogManager.vue | 191 +++++++++++++++++++--- frontend/src/i18n/locales/de.json | 3 +- frontend/src/store.js | 31 +++- 4 files changed, 325 insertions(+), 33 deletions(-) diff --git a/frontend/src/components/BaseDialog.vue b/frontend/src/components/BaseDialog.vue index 67539462..229552ea 100644 --- a/frontend/src/components/BaseDialog.vue +++ b/frontend/src/components/BaseDialog.vue @@ -5,6 +5,7 @@ @click.self="handleOverlayClick" >

{{ title }}

-
+
+ +
@@ -116,6 +126,12 @@ export default { type: Boolean, default: true }, + + /** Nicht-modale Dialoge: Ecke zum Vergrößern/Verkleinern */ + resizable: { + type: Boolean, + default: true + }, // Schließen bei Overlay-Klick (nur bei modalen Dialogen) closeOnOverlay: { @@ -129,7 +145,11 @@ export default { localPosition: { ...this.position }, isDragging: false, dragStartX: 0, - dragStartY: 0 + dragStartY: 0, + localWidth: null, + localHeight: null, + isResizing: false, + resizeStart: null, }; }, @@ -158,6 +178,14 @@ export default { if (!this.isModal) { style.left = `${this.localPosition.x}px`; style.top = `${this.localPosition.y}px`; + if (this.localWidth != null) { + style.width = `${this.localWidth}px`; + style.maxWidth = 'none'; + } + if (this.localHeight != null) { + style.height = `${this.localHeight}px`; + style.maxHeight = 'none'; + } } return style; @@ -188,7 +216,11 @@ export default { startDrag(event) { if (!this.isDraggable) return; - + if (event.button !== 0) return; + if (event.target.closest('button, a, input, select, textarea, [contenteditable="true"]')) { + return; + } + this.isDragging = true; this.dragStartX = event.clientX - this.localPosition.x; this.dragStartY = event.clientY - this.localPosition.y; @@ -201,9 +233,20 @@ export default { onDrag(event) { if (!this.isDragging) return; - - this.localPosition.x = event.clientX - this.dragStartX; - this.localPosition.y = event.clientY - this.dragStartY; + + let nx = event.clientX - this.dragStartX; + let ny = event.clientY - this.dragStartY; + const el = this.$refs.dialogContainer; + if (el && typeof window !== 'undefined') { + const w = el.offsetWidth; + const h = el.offsetHeight; + const pad = 8; + nx = Math.min(Math.max(pad, nx), window.innerWidth - w - pad); + ny = Math.min(Math.max(pad, ny), window.innerHeight - h - pad); + } + + this.localPosition.x = nx; + this.localPosition.y = ny; this.$emit('update:position', { ...this.localPosition }); }, @@ -212,7 +255,48 @@ export default { this.isDragging = false; document.removeEventListener('mousemove', this.onDrag); document.removeEventListener('mouseup', this.stopDrag); - } + }, + + startResize(event) { + if (this.isModal || !this.resizable) return; + if (event.button !== 0) return; + const el = this.$refs.dialogContainer; + if (!el) return; + const rect = el.getBoundingClientRect(); + this.isResizing = true; + this.resizeStart = { + mouseX: event.clientX, + mouseY: event.clientY, + width: rect.width, + height: rect.height, + }; + document.addEventListener('mousemove', this.onResizeMove); + document.addEventListener('mouseup', this.stopResize); + event.preventDefault(); + }, + + onResizeMove(event) { + if (!this.isResizing || !this.resizeStart || typeof window === 'undefined') return; + const dx = event.clientX - this.resizeStart.mouseX; + const dy = event.clientY - this.resizeStart.mouseY; + let w = this.resizeStart.width + dx; + let h = this.resizeStart.height + dy; + const minW = 260; + const minH = 160; + const maxW = window.innerWidth - this.localPosition.x - 8; + const maxH = window.innerHeight - this.localPosition.y - 8; + w = Math.min(Math.max(minW, w), maxW); + h = Math.min(Math.max(minH, h), maxH); + this.localWidth = w; + this.localHeight = h; + }, + + stopResize() { + this.isResizing = false; + this.resizeStart = null; + document.removeEventListener('mousemove', this.onResizeMove); + document.removeEventListener('mouseup', this.stopResize); + }, }, watch: { @@ -228,6 +312,9 @@ export default { if (this.isDragging) { this.stopDrag(); } + if (this.isResizing) { + this.stopResize(); + } } }; @@ -265,6 +352,7 @@ export default { /* Dialog Container */ .dialog-container { + position: relative; background: white; border-radius: 8px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2); @@ -283,6 +371,8 @@ export default { /* Non-Modal Dialog Container */ .non-modal-dialog .dialog-container { position: absolute; + min-width: 260px; + min-height: 160px; } /* Größen */ @@ -321,7 +411,34 @@ export default { } .dialog-header.draggable { - cursor: move; + cursor: grab; +} + +.dialog-header.draggable:active { + cursor: grabbing; +} + +.dialog-resize-handle { + position: absolute; + right: 0; + bottom: 0; + width: 16px; + height: 16px; + cursor: nwse-resize; + background: linear-gradient( + 135deg, + transparent 50%, + rgba(0, 0, 0, 0.12) 50% + ); + border-bottom-right-radius: 8px; +} + +.dialog-resize-handle:hover { + background: linear-gradient( + 135deg, + transparent 45%, + rgba(47, 122, 95, 0.35) 45% + ); } .dialog-title { diff --git a/frontend/src/components/DialogManager.vue b/frontend/src/components/DialogManager.vue index 3cc3bc18..e6edad35 100644 --- a/frontend/src/components/DialogManager.vue +++ b/frontend/src/components/DialogManager.vue @@ -21,11 +21,7 @@ v-for="dialog in activeDialogs" :key="dialog.id" class="dialog-window" - :style="{ - left: dialog.position.x + 'px', - top: dialog.position.y + 'px', - zIndex: dialog.zIndex - }" + :style="dialogWindowStyle(dialog)" @mousedown="bringToFront(dialog.id)" >
@@ -39,14 +35,19 @@ @action="handleHeaderAction" />
-
- - +
+ +
+
@@ -69,6 +70,9 @@ const InternalTournamentStats = defineAsyncComponent(() => import('./tournament/InternalTournamentStats.vue'), ); +const MIN_DIALOG_W = 280; +const MIN_DIALOG_H = 200; + export default { components: { MatchReportDialog, @@ -84,11 +88,42 @@ export default { InternalTournamentStats, }, name: 'DialogManager', + data() { + return { + dragSession: null, + resizeSession: null, + }; + }, computed: { ...mapGetters(['dialogs', 'minimizedDialogs', 'activeDialogs']) }, methods: { - ...mapActions(['closeDialog', 'minimizeDialog', 'restoreDialog', 'bringDialogToFront']), + ...mapActions(['closeDialog', 'minimizeDialog', 'restoreDialog', 'bringDialogToFront', 'updateDialogBounds']), + + dialogWindowStyle(dialog) { + const s = { + left: `${dialog.position.x}px`, + top: `${dialog.position.y}px`, + zIndex: dialog.zIndex, + }; + if (dialog.width != null && Number.isFinite(Number(dialog.width))) { + s.width = `${Number(dialog.width)}px`; + } + if (dialog.height != null && Number.isFinite(Number(dialog.height))) { + s.height = `${Number(dialog.height)}px`; + } + return s; + }, + + approxDialogWidth(dialog) { + if (dialog.width != null && Number.isFinite(Number(dialog.width))) return Number(dialog.width); + return Math.round(window.innerWidth * 0.9); + }, + + approxDialogHeight(dialog) { + if (dialog.height != null && Number.isFinite(Number(dialog.height))) return Number(dialog.height); + return Math.round(window.innerHeight * 0.9); + }, getDialogComponent(componentName) { const components = { @@ -113,9 +148,91 @@ export default { }, startDrag(dialogId, event) { - // Drag & Drop deaktiviert, da Dialog jetzt fest positioniert ist - // Dialog wird immer zentriert angezeigt - this.bringToFront(dialogId); + if (event.button !== 0) return; + if (event.target.closest('button, a, input, select, textarea, [contenteditable="true"]')) { + return; + } + const dialog = this.dialogs.find((d) => d.id === dialogId); + if (!dialog) return; + this.bringDialogToFront(dialogId); + this.dragSession = { + dialogId, + startX: event.clientX, + startY: event.clientY, + origX: dialog.position.x, + origY: dialog.position.y, + }; + document.addEventListener('mousemove', this.onDragMove); + document.addEventListener('mouseup', this.onDragEnd); + event.preventDefault(); + }, + + onDragMove(event) { + if (!this.dragSession) return; + const { dialogId, startX, startY, origX, origY } = this.dragSession; + const dialog = this.dialogs.find((d) => d.id === dialogId); + if (!dialog) return; + let nx = origX + event.clientX - startX; + let ny = origY + event.clientY - startY; + const w = this.approxDialogWidth(dialog); + const h = this.approxDialogHeight(dialog); + const pad = 8; + nx = Math.min(Math.max(pad, nx), window.innerWidth - w - pad); + ny = Math.min(Math.max(pad, ny), window.innerHeight - h - pad); + this.updateDialogBounds({ dialogId, position: { x: nx, y: ny } }); + }, + + onDragEnd() { + this.dragSession = null; + document.removeEventListener('mousemove', this.onDragMove); + document.removeEventListener('mouseup', this.onDragEnd); + }, + + startResize(dialogId, event) { + if (event.button !== 0) return; + this.bringDialogToFront(dialogId); + const dialog = this.dialogs.find((d) => d.id === dialogId); + if (!dialog) return; + const el = event.currentTarget.closest('.dialog-window'); + if (!el) return; + const rect = el.getBoundingClientRect(); + this.resizeSession = { + dialogId, + startX: event.clientX, + startY: event.clientY, + origW: rect.width, + origH: rect.height, + }; + document.addEventListener('mousemove', this.onResizeMove); + document.addEventListener('mouseup', this.onResizeEnd); + event.preventDefault(); + }, + + onResizeMove(event) { + if (!this.resizeSession) return; + const { dialogId, startX, startY, origW, origH } = this.resizeSession; + const dialog = this.dialogs.find((d) => d.id === dialogId); + if (!dialog) return; + let w = origW + event.clientX - startX; + let h = origH + event.clientY - startY; + const maxW = window.innerWidth - dialog.position.x - 8; + const maxH = window.innerHeight - dialog.position.y - 8; + w = Math.min(Math.max(MIN_DIALOG_W, w), maxW); + h = Math.min(Math.max(MIN_DIALOG_H, h), maxH); + this.updateDialogBounds({ dialogId, width: w, height: h }); + let nx = dialog.position.x; + let ny = dialog.position.y; + if (nx + w > window.innerWidth - 8) nx = Math.max(8, window.innerWidth - w - 8); + if (ny + h > window.innerHeight - 8) ny = Math.max(8, window.innerHeight - h - 8); + if (nx !== dialog.position.x || ny !== dialog.position.y) { + this.updateDialogBounds({ dialogId, position: { x: nx, y: ny } }); + } + }, + + onResizeEnd() { + this.resizeSession = null; + document.removeEventListener('mousemove', this.onResizeMove); + document.removeEventListener('mouseup', this.onResizeEnd); }, handleHeaderAction(action) { @@ -187,8 +304,9 @@ export default { }, beforeUnmount() { - // Event-Listener entfernen window.removeEventListener('message', this.handlePostMessage); + if (this.dragSession) this.onDragEnd(); + if (this.resizeSession) this.onResizeEnd(); } }; @@ -206,8 +324,11 @@ export default { .dialog-window { position: absolute; - width: 90vw; - height: 90vh; + box-sizing: border-box; + min-width: 280px; + min-height: 200px; + width: min(90vw, calc(100vw - 32px)); + height: min(90vh, calc(100vh - 32px)); background: white; border-radius: 8px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2); @@ -215,8 +336,6 @@ export default { display: flex; flex-direction: column; overflow: hidden; - left: 5vw !important; - top: 5vh !important; } .dialog-header { @@ -226,8 +345,13 @@ export default { display: flex; justify-content: space-between; align-items: center; - cursor: move; + cursor: grab; user-select: none; + flex-shrink: 0; +} + +.dialog-header:active { + cursor: grabbing; } .dialog-title { @@ -276,6 +400,31 @@ export default { flex: 1; padding: 0; overflow-y: auto; + min-height: 0; +} + +.dialog-resize-handle { + position: absolute; + right: 0; + bottom: 0; + width: 16px; + height: 16px; + cursor: nwse-resize; + background: linear-gradient( + 135deg, + transparent 50%, + rgba(0, 0, 0, 0.1) 50% + ); + border-bottom-right-radius: 8px; + z-index: 2; +} + +.dialog-resize-handle:hover { + background: linear-gradient( + 135deg, + transparent 45%, + rgba(47, 122, 95, 0.35) 45% + ); } .minimized-dialogs { @@ -321,10 +470,8 @@ export default { /* Responsive Design */ @media (max-width: 768px) { .dialog-window { - width: 95vw; - height: 95vh; - left: 2.5vw !important; - top: 2.5vh !important; + max-width: calc(100vw - 16px); + max-height: calc(100vh - 16px); } .minimized-dialogs { diff --git a/frontend/src/i18n/locales/de.json b/frontend/src/i18n/locales/de.json index b1acb459..fb038b62 100644 --- a/frontend/src/i18n/locales/de.json +++ b/frontend/src/i18n/locales/de.json @@ -2078,7 +2078,8 @@ }, "baseDialog": { "minimize": "Minimieren", - "close": "Schließen" + "close": "Schließen", + "resize": "Größe ändern" }, "dialogManager": { "noMinimizedDialogs": "Keine minimierten Dialoge", diff --git a/frontend/src/store.js b/frontend/src/store.js index 2ac71405..a65cc371 100644 --- a/frontend/src/store.js +++ b/frontend/src/store.js @@ -109,15 +109,39 @@ const store = createStore({ // Dialog-Mutations openDialog(state, dialog) { const id = ++state.dialogCounter; + let position = { x: 48, y: 48 }; + if ( + dialog.position && + Number.isFinite(dialog.position.x) && + Number.isFinite(dialog.position.y) + ) { + position = { x: dialog.position.x, y: dialog.position.y }; + } else if (typeof window !== 'undefined') { + position = { + x: Math.max(16, Math.round(window.innerWidth * 0.05)), + y: Math.max(16, Math.round(window.innerHeight * 0.05)), + }; + } const newDialog = { id, ...dialog, isMinimized: false, zIndex: 1000 + state.dialogs.length, - position: { x: 0, y: 0 } // Position wird durch CSS festgelegt + position, + width: dialog.width != null ? dialog.width : null, + height: dialog.height != null ? dialog.height : null, }; state.dialogs.push(newDialog); }, + updateDialogBounds(state, { dialogId, position, width, height }) { + const d = state.dialogs.find((x) => x.id === dialogId); + if (!d) return; + if (position && Number.isFinite(position.x) && Number.isFinite(position.y)) { + d.position = { x: position.x, y: position.y }; + } + if (width !== undefined) d.width = width; + if (height !== undefined) d.height = height; + }, closeDialog(state, dialogId) { state.dialogs = state.dialogs.filter(dialog => dialog.id !== dialogId); }, @@ -219,7 +243,10 @@ const store = createStore({ }, bringDialogToFront({ commit }, dialogId) { commit('bringDialogToFront', dialogId); - } + }, + updateDialogBounds({ commit }, payload) { + commit('updateDialogBounds', payload); + }, }, getters: { isAuthenticated: state => !!state.token,