Enhance drag-and-drop UI for service reordering

Improves the drag-and-drop experience in ServicesManager by adding smooth animations, placeholder transitions, and mouse-following effects. Updates CSS for more visually appealing drag states, including animated placeholders, improved shadows, and transitions for service items during reordering.
This commit is contained in:
xixu-me committed 2025-07-14 15:06:06 +08:00
1 parent 3d415ad92b
commit 56298322ba
2 files changed
+148 -23

No files matched your search

+98 -14
View File
@@ -30,6 +30,9 @@ export class ServicesManager {
draggedElement: null,
draggedIndex: -1,
placeholder: null,
initialMouseY: 0,
initialElementY: 0,
mouseOffset: 0,
};
this.elements = {
@@ -363,26 +366,42 @@ export class ServicesManager {
this.dragState.draggedElement = serviceItem;
this.dragState.draggedIndex = parseInt(serviceItem.dataset.index);
// Create placeholder
// Store initial positions for mouse following
const rect = serviceItem.getBoundingClientRect();
this.dragState.initialMouseY = event.clientY;
this.dragState.initialElementY = rect.top;
this.dragState.mouseOffset = event.clientY - rect.top;
// Create placeholder with smooth appearance
this.dragState.placeholder = document.createElement("div");
this.dragState.placeholder.className =
"selected-service-item drag-placeholder";
this.dragState.placeholder.innerHTML = `
<div class="service-info">
<i class="fas fa-grip-lines service-drag-handle"></i>
<span class="service-name">Drop here</span>
<span class="service-name"></span>
</div>
`;
// Add dragging class
// Add dragging classes with smooth animation
serviceItem.classList.add("dragging");
document.body.classList.add("dragging");
// Insert placeholder
// Insert placeholder with animation
this.dragState.placeholder.style.height = "0px";
this.dragState.placeholder.style.opacity = "0";
this.dragState.placeholder.style.marginBottom = "0px";
serviceItem.parentNode.insertBefore(
this.dragState.placeholder,
serviceItem.nextSibling
);
// Animate placeholder appearance
requestAnimationFrame(() => {
this.dragState.placeholder.style.height = serviceItem.offsetHeight + "px";
this.dragState.placeholder.style.opacity = "1";
this.dragState.placeholder.style.marginBottom = "0.75rem";
});
}
/**
@@ -392,6 +411,15 @@ export class ServicesManager {
handleDragMove(event) {
if (!this.dragState.isDragging) return;
// Make dragged element follow mouse vertically
const draggedElement = this.dragState.draggedElement;
if (draggedElement) {
const deltaY = event.clientY - this.dragState.initialMouseY;
draggedElement.style.transform = `translateY(${deltaY}px) scale(1.05)`;
draggedElement.style.zIndex = "1000";
draggedElement.style.position = "relative";
}
const serviceItems = Array.from(
this.elements.selectedServices.querySelectorAll(
".selected-service-item:not(.dragging):not(.drag-placeholder)"
@@ -399,11 +427,20 @@ export class ServicesManager {
);
let targetIndex = serviceItems.length; // Default to end
let closestItem = null;
let closestDistance = Infinity;
// Find the closest item for smoother animation
for (let i = 0; i < serviceItems.length; i++) {
const item = serviceItems[i];
const rect = item.getBoundingClientRect();
const itemCenter = rect.top + rect.height / 2;
const distance = Math.abs(event.clientY - itemCenter);
if (distance < closestDistance) {
closestDistance = distance;
closestItem = item;
}
if (event.clientY < itemCenter) {
targetIndex = i;
@@ -411,7 +448,21 @@ export class ServicesManager {
}
}
// Move placeholder to new position
// Add smooth animation classes to other items
serviceItems.forEach((item, index) => {
const rect = item.getBoundingClientRect();
const itemCenter = rect.top + rect.height / 2;
if (event.clientY < itemCenter && index >= targetIndex) {
item.style.transform = "translateY(8px)";
} else if (event.clientY > itemCenter && index < targetIndex) {
item.style.transform = "translateY(-8px)";
} else {
item.style.transform = "translateY(0)";
}
});
// Move placeholder to new position with smooth transition
const targetItem = serviceItems[targetIndex];
if (targetItem) {
targetItem.parentNode.insertBefore(
@@ -431,6 +482,18 @@ export class ServicesManager {
handleDragEnd(event) {
if (!this.dragState.isDragging) return;
// Reset transforms on all items with smooth animation
const allServiceItems = Array.from(
this.elements.selectedServices.querySelectorAll(
".selected-service-item:not(.dragging):not(.drag-placeholder)"
)
);
allServiceItems.forEach((item) => {
item.style.transform = "";
item.style.transition = "transform 0.3s cubic-bezier(0.4, 0, 0.2, 1)";
});
// Calculate new index based on placeholder position
const allItems = Array.from(
this.elements.selectedServices.querySelectorAll(
@@ -446,26 +509,47 @@ export class ServicesManager {
// Update services order
this.reorderServices(this.dragState.draggedIndex, newIndex);
// Clean up
this.dragState.draggedElement.classList.remove("dragging");
document.body.classList.remove("dragging");
// Animate placeholder disappearance
if (this.dragState.placeholder && this.dragState.placeholder.parentNode) {
this.dragState.placeholder.parentNode.removeChild(
this.dragState.placeholder
);
this.dragState.placeholder.style.height = "0px";
this.dragState.placeholder.style.opacity = "0";
this.dragState.placeholder.style.marginBottom = "0px";
setTimeout(() => {
if (
this.dragState.placeholder &&
this.dragState.placeholder.parentNode
) {
this.dragState.placeholder.parentNode.removeChild(
this.dragState.placeholder
);
}
}, 300);
}
// Clean up with smooth animation
this.dragState.draggedElement.classList.remove("dragging");
this.dragState.draggedElement.style.transform = "";
this.dragState.draggedElement.style.zIndex = "";
this.dragState.draggedElement.style.position = "";
document.body.classList.remove("dragging");
// Reset drag state
this.dragState = {
isDragging: false,
draggedElement: null,
draggedIndex: -1,
placeholder: null,
initialMouseY: 0,
initialElementY: 0,
mouseOffset: 0,
};
// Update UI and trigger change event
this.renderSelectedServices();
this.triggerSelectionChange();
// Update UI and trigger change event with smooth animation
setTimeout(() => {
this.renderSelectedServices();
this.triggerSelectionChange();
}, 50);
}
/**
+50 -9
View File
@@ -996,7 +996,7 @@ body::before {
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 12px;
transition: all 0.3s ease;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
}
@@ -1578,18 +1578,37 @@ body::before {
========================================================================== */
.dragging .selected-service-item {
z-index: 1000;
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.3);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
transform: scale(1.02);
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
.drag-placeholder {
border: 2px dashed var(--primary-color) !important;
background: rgba(37, 99, 235, 0.1) !important;
opacity: 0.7;
border: 2px solid var(--primary-color) !important;
background: linear-gradient(
135deg,
rgba(37, 99, 235, 0.08),
rgba(37, 99, 235, 0.15)
) !important;
opacity: 1;
animation: pulse-glow 1.5s ease-in-out infinite alternate;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
@keyframes pulse-glow {
0% {
box-shadow: 0 0 5px rgba(37, 99, 235, 0.3);
transform: scale(1);
}
100% {
box-shadow: 0 0 20px rgba(37, 99, 235, 0.6);
transform: scale(1.02);
}
}
.drag-placeholder .service-name {
color: var(--primary-color) !important;
font-style: italic;
opacity: 0.8;
}
body.dragging {
@@ -1604,26 +1623,48 @@ body.dragging * {
.service-drag-handle {
cursor: grab;
opacity: 0.6;
transition: opacity 0.2s ease;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
.service-drag-handle:hover {
opacity: 1;
color: var(--primary-color);
transform: scale(1.1);
}
.service-drag-handle:active {
cursor: grabbing;
transform: scale(0.95);
}
.selected-service-item.dragging {
transition: none;
transition: none !important;
z-index: 1000;
position: relative;
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.4);
pointer-events: none;
}
/* Smooth animations for reordering */
.selected-service-item:not(.dragging):not(.drag-placeholder) {
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
/* Enhanced animations for drag interactions */
.selected-service-item.drag-over-top {
transform: translateY(-8px);
}
.selected-service-item.drag-over-bottom {
transform: translateY(8px);
}
/* Dark theme drag styles */
[data-theme="dark"] .drag-placeholder {
border-color: var(--primary-color) !important;
background: rgba(37, 99, 235, 0.15) !important;
background: linear-gradient(
135deg,
rgba(37, 99, 235, 0.12),
rgba(37, 99, 235, 0.2)
) !important;
}