feat(Dialog): enhance dialog functionality with resizing and improved positioning
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 36s
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 36s
- 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.
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
@click.self="handleOverlayClick"
|
||||
>
|
||||
<div
|
||||
ref="dialogContainer"
|
||||
:class="['dialog-container', sizeClass]"
|
||||
:style="dialogStyle"
|
||||
@mousedown="handleMouseDown"
|
||||
@@ -17,9 +18,10 @@
|
||||
>
|
||||
<h3 class="dialog-title">{{ title }}</h3>
|
||||
<slot name="header-actions"></slot>
|
||||
<div class="dialog-controls">
|
||||
<div class="dialog-controls" @mousedown.stop>
|
||||
<button
|
||||
v-if="minimizable"
|
||||
type="button"
|
||||
@click="$emit('minimize')"
|
||||
class="control-btn minimize-btn"
|
||||
:title="$t('baseDialog.minimize')"
|
||||
@@ -28,6 +30,7 @@
|
||||
</button>
|
||||
<button
|
||||
v-if="closable"
|
||||
type="button"
|
||||
@click="handleClose"
|
||||
class="control-btn close-btn"
|
||||
:title="$t('baseDialog.close')"
|
||||
@@ -46,6 +49,13 @@
|
||||
<div v-if="$slots.footer" class="dialog-footer">
|
||||
<slot name="footer"></slot>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="!isModal && resizable"
|
||||
class="dialog-resize-handle"
|
||||
:title="$t('baseDialog.resize')"
|
||||
@mousedown.stop="startResize"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)"
|
||||
>
|
||||
<div class="dialog-header" @mousedown="startDrag(dialog.id, $event)">
|
||||
@@ -39,14 +35,19 @@
|
||||
@action="handleHeaderAction"
|
||||
/>
|
||||
</div>
|
||||
<div class="dialog-controls">
|
||||
<button @click="minimizeDialog(dialog.id)" class="control-btn minimize-btn" :title="$t('dialogManager.minimize')">−</button>
|
||||
<button @click="closeDialog(dialog.id)" class="control-btn close-btn" :title="$t('dialogManager.close')">×</button>
|
||||
<div class="dialog-controls" @mousedown.stop>
|
||||
<button type="button" @click="minimizeDialog(dialog.id)" class="control-btn minimize-btn" :title="$t('dialogManager.minimize')">−</button>
|
||||
<button type="button" @click="closeDialog(dialog.id)" class="control-btn close-btn" :title="$t('dialogManager.close')">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dialog-content">
|
||||
<component :is="getDialogComponent(dialog.component)" v-bind="dialog.props" @close="() => closeDialog(dialog.id)" />
|
||||
</div>
|
||||
<div
|
||||
class="dialog-resize-handle"
|
||||
:title="$t('baseDialog.resize')"
|
||||
@mousedown.stop="startResize(dialog.id, $event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -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 {
|
||||
|
||||
@@ -2078,7 +2078,8 @@
|
||||
},
|
||||
"baseDialog": {
|
||||
"minimize": "Minimieren",
|
||||
"close": "Schließen"
|
||||
"close": "Schließen",
|
||||
"resize": "Größe ändern"
|
||||
},
|
||||
"dialogManager": {
|
||||
"noMinimizedDialogs": "Keine minimierten Dialoge",
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user