257 lines
6.3 KiB
Vue
257 lines
6.3 KiB
Vue
<template>
|
|
<v-dialog
|
|
v-model="showDialog"
|
|
:max-width="mobile ? '100%' : '500'"
|
|
:fullscreen="mobile"
|
|
:transition="mobile ? 'dialog-bottom-transition' : 'dialog-transition'"
|
|
persistent
|
|
>
|
|
<v-card>
|
|
<v-card-title class="d-flex align-center">
|
|
<v-icon class="mr-2" :color="iconColor">{{ icon }}</v-icon>
|
|
{{ title }}
|
|
<v-spacer />
|
|
<v-btn
|
|
icon="mdi-close"
|
|
variant="text"
|
|
@click="cancel"
|
|
:disabled="isSliding"
|
|
></v-btn>
|
|
</v-card-title>
|
|
|
|
<v-divider />
|
|
|
|
<v-card-text :class="mobile ? 'pa-4' : 'pa-6'">
|
|
<div :class="mobile ? 'text-body-2 mb-4' : 'text-body-1 mb-6'">
|
|
{{ message }}
|
|
</div>
|
|
|
|
<div v-if="confirmationList && confirmationList.length > 0" class="mb-4">
|
|
<ul class="text-caption">
|
|
<li v-for="item in confirmationList" :key="item">{{ item }}</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="text-error mb-4" :class="mobile ? 'text-caption' : 'text-body-2'">
|
|
{{ warningText }}
|
|
</div>
|
|
|
|
<!-- Slider Track -->
|
|
<div class="slider-container mb-4">
|
|
<div class="slider-track" ref="sliderTrack">
|
|
<div
|
|
class="slider-fill"
|
|
:style="{ width: `${sliderProgress}%` }"
|
|
></div>
|
|
<div
|
|
class="slider-handle"
|
|
:style="{ left: `${sliderProgress}%` }"
|
|
@mousedown="startSliding"
|
|
@touchstart="startSliding"
|
|
>
|
|
<v-icon size="20" color="white">
|
|
{{ sliderProgress >= 100 ? 'mdi-check' : 'mdi-chevron-right' }}
|
|
</v-icon>
|
|
</div>
|
|
</div>
|
|
<div class="slider-text" :class="mobile ? 'text-caption' : 'text-body-2'">
|
|
{{ sliderProgress >= 100 ? 'Ready to delete!' : sliderText }}
|
|
</div>
|
|
</div>
|
|
</v-card-text>
|
|
|
|
<v-divider />
|
|
|
|
<v-card-actions class="pa-4">
|
|
<v-spacer />
|
|
<v-btn
|
|
@click="cancel"
|
|
variant="text"
|
|
:size="mobile ? 'default' : 'large'"
|
|
:disabled="isSliding"
|
|
>
|
|
Cancel
|
|
</v-btn>
|
|
<v-btn
|
|
color="error"
|
|
variant="flat"
|
|
@click="confirm"
|
|
:loading="isConfirming"
|
|
:disabled="sliderProgress < 100"
|
|
:size="mobile ? 'default' : 'large'"
|
|
>
|
|
{{ confirmButtonText }}
|
|
</v-btn>
|
|
</v-card-actions>
|
|
</v-card>
|
|
</v-dialog>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
interface Props {
|
|
modelValue: boolean;
|
|
title?: string;
|
|
message?: string;
|
|
icon?: string;
|
|
iconColor?: string;
|
|
confirmButtonText?: string;
|
|
sliderText?: string;
|
|
warningText?: string;
|
|
confirmationList?: string[];
|
|
loading?: boolean;
|
|
}
|
|
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
title: 'Confirm Action',
|
|
message: 'Are you sure you want to proceed?',
|
|
icon: 'mdi-alert',
|
|
iconColor: 'warning',
|
|
confirmButtonText: 'Confirm',
|
|
sliderText: 'Slide to confirm',
|
|
warningText: 'This action cannot be undone.',
|
|
confirmationList: () => [],
|
|
loading: false
|
|
});
|
|
|
|
const emit = defineEmits<{
|
|
'update:modelValue': [value: boolean];
|
|
'confirm': [];
|
|
'cancel': [];
|
|
}>();
|
|
|
|
const { mobile } = useDisplay();
|
|
|
|
const showDialog = computed({
|
|
get: () => props.modelValue,
|
|
set: (value) => emit('update:modelValue', value)
|
|
});
|
|
|
|
const isConfirming = computed(() => props.loading);
|
|
|
|
// Slider state
|
|
const sliderProgress = ref(0);
|
|
const isSliding = ref(false);
|
|
const sliderTrack = ref<HTMLElement>();
|
|
|
|
const startSliding = (event: MouseEvent | TouchEvent) => {
|
|
event.preventDefault();
|
|
isSliding.value = true;
|
|
|
|
const handleMove = (moveEvent: MouseEvent | TouchEvent) => {
|
|
if (!sliderTrack.value || !isSliding.value) return;
|
|
|
|
const rect = sliderTrack.value.getBoundingClientRect();
|
|
const clientX = 'touches' in moveEvent ? moveEvent.touches[0].clientX : moveEvent.clientX;
|
|
const handleWidth = 50; // Handle width in pixels
|
|
const availableWidth = rect.width - handleWidth;
|
|
const relativeX = clientX - rect.left;
|
|
const progress = Math.max(0, Math.min(100, (relativeX / availableWidth) * 100));
|
|
|
|
sliderProgress.value = progress;
|
|
};
|
|
|
|
const handleEnd = () => {
|
|
isSliding.value = false;
|
|
if (sliderProgress.value < 100) {
|
|
// Snap back if not fully slid
|
|
sliderProgress.value = 0;
|
|
}
|
|
document.removeEventListener('mousemove', handleMove);
|
|
document.removeEventListener('mouseup', handleEnd);
|
|
document.removeEventListener('touchmove', handleMove);
|
|
document.removeEventListener('touchend', handleEnd);
|
|
};
|
|
|
|
document.addEventListener('mousemove', handleMove);
|
|
document.addEventListener('mouseup', handleEnd);
|
|
document.addEventListener('touchmove', handleMove);
|
|
document.addEventListener('touchend', handleEnd);
|
|
};
|
|
|
|
const confirm = () => {
|
|
if (sliderProgress.value >= 100) {
|
|
emit('confirm');
|
|
}
|
|
};
|
|
|
|
const cancel = () => {
|
|
if (!isSliding.value) {
|
|
sliderProgress.value = 0;
|
|
emit('cancel');
|
|
showDialog.value = false;
|
|
}
|
|
};
|
|
|
|
// Reset slider when dialog is closed
|
|
watch(showDialog, (newValue) => {
|
|
if (!newValue) {
|
|
sliderProgress.value = 0;
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<style scoped>
|
|
.slider-container {
|
|
width: 100%;
|
|
}
|
|
|
|
.slider-track {
|
|
position: relative;
|
|
width: 100%;
|
|
height: 50px;
|
|
background-color: #e0e0e0;
|
|
border-radius: 25px;
|
|
overflow: hidden;
|
|
cursor: pointer;
|
|
user-select: none;
|
|
}
|
|
|
|
.slider-fill {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
height: 100%;
|
|
background: linear-gradient(90deg, #ff5722, #d32f2f);
|
|
border-radius: 25px;
|
|
transition: none;
|
|
will-change: width;
|
|
}
|
|
|
|
.slider-handle {
|
|
position: absolute;
|
|
top: 0;
|
|
width: 50px;
|
|
height: 50px;
|
|
background-color: #9e9e9e;
|
|
border-radius: 50%;
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
|
cursor: grab;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
transform: translateX(0);
|
|
transition: none;
|
|
will-change: transform, left;
|
|
z-index: 1;
|
|
}
|
|
|
|
.slider-handle:active {
|
|
cursor: grabbing;
|
|
transform: translateX(-50%) scale(1.1);
|
|
}
|
|
|
|
.slider-text {
|
|
text-align: center;
|
|
margin-top: 8px;
|
|
color: #666;
|
|
}
|
|
|
|
/* Prevent text selection during sliding */
|
|
.slider-container * {
|
|
-webkit-user-select: none;
|
|
-moz-user-select: none;
|
|
-ms-user-select: none;
|
|
user-select: none;
|
|
}
|
|
</style>
|