173 lines
4.1 KiB
Vue
173 lines
4.1 KiB
Vue
<template>
|
|
<q-dialog
|
|
ref="dialogRef"
|
|
:maximized="minMaxState"
|
|
:full-width="minMaxState"
|
|
:no-focus="!minMaxState"
|
|
:no-refocus="!minMaxState"
|
|
:seamless="!minMaxState"
|
|
>
|
|
<q-card class="layout" :style="cardStyle">
|
|
<!-- Draggable Header -->
|
|
<div
|
|
class="dialog-header row items-center justify-between bg-grey-1"
|
|
v-touch-pan.mouse.prevent.stop="handlePan"
|
|
>
|
|
<div v-if="headerTitle" class="text-left text-bold text-caption q-mx-sm">
|
|
{{ headerTitle }}
|
|
</div>
|
|
<div class="row justify-end q-mx-sm">
|
|
<q-btn dense flat :icon="minMaxIcon" size="md" @click="minMax" />
|
|
<q-btn dense flat icon="close" size="md" v-close-popup />
|
|
</div>
|
|
</div>
|
|
|
|
<q-separator color="black" />
|
|
|
|
<!-- Content Slot -->
|
|
<div class="scrollArea">
|
|
<slot />
|
|
</div>
|
|
|
|
<!-- Resize Handle -->
|
|
<div v-if="!minMaxState" class="resize-handle" @mousedown.prevent="startResizing" />
|
|
</q-card>
|
|
</q-dialog>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, computed, watch } from 'vue';
|
|
import type { TouchPanValue } from 'quasar';
|
|
|
|
const dialogRef = ref();
|
|
const open = () => dialogRef.value?.show();
|
|
const close = () => dialogRef.value?.hide();
|
|
defineExpose({ open, close });
|
|
|
|
const props = defineProps({
|
|
headerTitle: { type: String, default: '' },
|
|
width: { type: Number, default: 400 },
|
|
height: { type: Number, default: 250 },
|
|
});
|
|
|
|
// Fullscreen toggle
|
|
const minMaxIcon = ref('fullscreen');
|
|
const minMaxState = ref(false);
|
|
function minMax() {
|
|
minMaxState.value = !minMaxState.value;
|
|
minMaxIcon.value = minMaxState.value ? 'fullscreen_exit' : 'fullscreen';
|
|
}
|
|
|
|
// Position and Size
|
|
const position = ref({ x: 0, y: 0 });
|
|
const width = ref(props.width || 400);
|
|
const height = ref(props.height || 250);
|
|
|
|
// Watch prop changes and sync local ref
|
|
watch(
|
|
() => props.width,
|
|
(newWidth) => {
|
|
if (newWidth !== undefined && newWidth !== width.value) {
|
|
width.value = newWidth;
|
|
}
|
|
},
|
|
);
|
|
watch(
|
|
() => props.height,
|
|
(newHeight) => {
|
|
if (newHeight !== undefined && newHeight !== height.value) {
|
|
height.value = newHeight;
|
|
}
|
|
},
|
|
);
|
|
|
|
// Dragging (only from header)
|
|
const handlePan: TouchPanValue = (details) => {
|
|
if (!minMaxState.value && details.delta) {
|
|
position.value.x += details.delta.x || 0;
|
|
position.value.y += details.delta.y || 0;
|
|
}
|
|
};
|
|
|
|
// Resizing
|
|
const isResizing = ref(false);
|
|
function startResizing(e: MouseEvent) {
|
|
isResizing.value = true;
|
|
const startX = e.clientX;
|
|
const startY = e.clientY;
|
|
const startWidth = width.value;
|
|
const startHeight = height.value;
|
|
|
|
function onMouseMove(e: MouseEvent) {
|
|
width.value = Math.max(200, startWidth + e.clientX - startX);
|
|
height.value = Math.max(200, startHeight + e.clientY - startY);
|
|
}
|
|
|
|
function onMouseUp() {
|
|
isResizing.value = false;
|
|
window.removeEventListener('mousemove', onMouseMove);
|
|
window.removeEventListener('mouseup', onMouseUp);
|
|
}
|
|
|
|
window.addEventListener('mousemove', onMouseMove);
|
|
window.addEventListener('mouseup', onMouseUp);
|
|
}
|
|
|
|
// Styles
|
|
const cardStyle = computed(() => {
|
|
if (minMaxState.value) {
|
|
return {};
|
|
}
|
|
|
|
return {
|
|
width:
|
|
typeof width.value === 'number' || /^\d+$/.test(width.value)
|
|
? `${width.value}px`
|
|
: width.value,
|
|
height:
|
|
typeof height.value === 'number' || /^\d+$/.test(height.value)
|
|
? `${height.value}px`
|
|
: height.value,
|
|
transform: `translate(${position.value.x}px, ${position.value.y}px)`,
|
|
};
|
|
});
|
|
</script>
|
|
|
|
<style scoped>
|
|
.layout {
|
|
position: relative;
|
|
display: flex;
|
|
flex-direction: column;
|
|
border-radius: 10px;
|
|
background-color: white;
|
|
}
|
|
|
|
/* Draggable header */
|
|
.dialog-header {
|
|
padding: 8px 0;
|
|
background: #f5f5f5;
|
|
cursor: move;
|
|
user-select: none;
|
|
}
|
|
|
|
/* Scrollable content */
|
|
.scrollArea {
|
|
flex: 1 1 auto;
|
|
min-height: 0;
|
|
overflow-y: auto;
|
|
padding: 16px;
|
|
}
|
|
|
|
/* Resize handle in bottom right */
|
|
.resize-handle {
|
|
position: absolute;
|
|
width: 16px;
|
|
height: 16px;
|
|
right: 0;
|
|
bottom: 0;
|
|
background: rgba(0, 0, 0, 0.1);
|
|
cursor: nwse-resize;
|
|
z-index: 10;
|
|
}
|
|
</style>
|