From 97fc60360bc964a7f8bdd8b22883f197291e3c44 Mon Sep 17 00:00:00 2001 From: Xi Xu Date: Mon, 14 Jul 2025 14:14:39 +0800 Subject: [PATCH] Add drag-and-drop reordering for selected services Implements drag-and-drop functionality to reorder selected services in ServicesManager.js, including state management and event handlers. Updates main.css with styles for dragging, placeholders, and drag handles to enhance the user experience. --- src/scripts/components/ServicesManager.js | 175 +++++++++++++++++++++- src/styles/main.css | 57 ++++++- 2 files changed, 228 insertions(+), 4 deletions(-) diff --git a/src/scripts/components/ServicesManager.js b/src/scripts/components/ServicesManager.js index 010e0c3..2f02951 100644 --- a/src/scripts/components/ServicesManager.js +++ b/src/scripts/components/ServicesManager.js @@ -21,8 +21,17 @@ export class ServicesManager { this.allServices = []; this.selectedServices = new Set(); + this.selectedServicesOrder = []; // Track order for drag functionality this.searchTimeout = null; + // Drag state + this.dragState = { + isDragging: false, + draggedElement: null, + draggedIndex: -1, + placeholder: null, + }; + this.elements = { searchInput: document.getElementById("search-input"), searchResults: document.getElementById("search-results"), @@ -115,6 +124,26 @@ export class ServicesManager { this.elements.selectedServices.addEventListener("click", (e) => { this.handleSelectedServiceClick(e); }); + + // Handle drag events + this.setupDragListeners(); + } + + /** + * Set up drag and drop event listeners + */ + setupDragListeners() { + this.elements.selectedServices.addEventListener("mousedown", (e) => { + this.handleDragStart(e); + }); + + document.addEventListener("mousemove", (e) => { + this.handleDragMove(e); + }); + + document.addEventListener("mouseup", (e) => { + this.handleDragEnd(e); + }); } /** @@ -207,6 +236,7 @@ export class ServicesManager { addService(serviceName) { if (!this.selectedServices.has(serviceName)) { this.selectedServices.add(serviceName); + this.selectedServicesOrder.push(serviceName); this.updateUI(); this.clearSearch(); this.triggerSelectionChange(); @@ -220,6 +250,10 @@ export class ServicesManager { removeService(serviceName) { if (this.selectedServices.has(serviceName)) { this.selectedServices.delete(serviceName); + const index = this.selectedServicesOrder.indexOf(serviceName); + if (index > -1) { + this.selectedServicesOrder.splice(index, 1); + } this.updateUI(); this.triggerSelectionChange(); } @@ -265,8 +299,8 @@ export class ServicesManager { return; } - const selectedArray = Array.from(this.selectedServices).sort(); - this.elements.selectedServices.innerHTML = selectedArray + // Use ordered array instead of sorted Set + this.elements.selectedServices.innerHTML = this.selectedServicesOrder .map( (serviceName, index) => `
@@ -291,7 +325,7 @@ export class ServicesManager { * @returns {Array} Array of selected service names */ getSelectedServices() { - return Array.from(this.selectedServices); + return [...this.selectedServicesOrder]; } /** @@ -311,4 +345,139 @@ export class ServicesManager { isLoaded() { return this.allServices.length > 0; } + + /** + * Handle drag start + * @param {MouseEvent} event - Mouse event + */ + handleDragStart(event) { + const dragHandle = event.target.closest(".service-drag-handle"); + if (!dragHandle) return; + + event.preventDefault(); + + const serviceItem = dragHandle.closest(".selected-service-item"); + if (!serviceItem) return; + + this.dragState.isDragging = true; + this.dragState.draggedElement = serviceItem; + this.dragState.draggedIndex = parseInt(serviceItem.dataset.index); + + // Create placeholder + this.dragState.placeholder = document.createElement("div"); + this.dragState.placeholder.className = + "selected-service-item drag-placeholder"; + this.dragState.placeholder.innerHTML = ` +
+ + Drop here +
+ `; + + // Add dragging class + serviceItem.classList.add("dragging"); + document.body.classList.add("dragging"); + + // Insert placeholder + serviceItem.parentNode.insertBefore( + this.dragState.placeholder, + serviceItem.nextSibling + ); + } + + /** + * Handle drag move + * @param {MouseEvent} event - Mouse event + */ + handleDragMove(event) { + if (!this.dragState.isDragging) return; + + const serviceItems = Array.from( + this.elements.selectedServices.querySelectorAll( + ".selected-service-item:not(.dragging):not(.drag-placeholder)" + ) + ); + + let targetIndex = serviceItems.length; // Default to end + + for (let i = 0; i < serviceItems.length; i++) { + const item = serviceItems[i]; + const rect = item.getBoundingClientRect(); + const itemCenter = rect.top + rect.height / 2; + + if (event.clientY < itemCenter) { + targetIndex = i; + break; + } + } + + // Move placeholder to new position + const targetItem = serviceItems[targetIndex]; + if (targetItem) { + targetItem.parentNode.insertBefore( + this.dragState.placeholder, + targetItem + ); + } else { + // Insert at end + this.elements.selectedServices.appendChild(this.dragState.placeholder); + } + } + + /** + * Handle drag end + * @param {MouseEvent} event - Mouse event + */ + handleDragEnd(event) { + if (!this.dragState.isDragging) return; + + // Calculate new index based on placeholder position + const allItems = Array.from( + this.elements.selectedServices.querySelectorAll( + ".selected-service-item, .drag-placeholder" + ) + ); + const placeholderIndex = allItems.indexOf(this.dragState.placeholder); + const newIndex = + placeholderIndex > this.dragState.draggedIndex + ? placeholderIndex - 1 + : placeholderIndex; + + // Update services order + this.reorderServices(this.dragState.draggedIndex, newIndex); + + // Clean up + this.dragState.draggedElement.classList.remove("dragging"); + document.body.classList.remove("dragging"); + if (this.dragState.placeholder && this.dragState.placeholder.parentNode) { + this.dragState.placeholder.parentNode.removeChild( + this.dragState.placeholder + ); + } + + // Reset drag state + this.dragState = { + isDragging: false, + draggedElement: null, + draggedIndex: -1, + placeholder: null, + }; + + // Update UI and trigger change event + this.renderSelectedServices(); + this.triggerSelectionChange(); + } + + /** + * Reorder services array + * @param {number} fromIndex - Original index + * @param {number} toIndex - Target index + */ + reorderServices(fromIndex, toIndex) { + if (fromIndex === toIndex) return; + + const item = this.selectedServicesOrder[fromIndex]; + this.selectedServicesOrder.splice(fromIndex, 1); + this.selectedServicesOrder.splice(toIndex, 0, item); + } } diff --git a/src/styles/main.css b/src/styles/main.css index 2cf3247..e763271 100644 --- a/src/styles/main.css +++ b/src/styles/main.css @@ -1574,5 +1574,60 @@ body::before { } /* ========================================================================== - Global Styles + Drag and drop styles ========================================================================== */ +.dragging .selected-service-item { + opacity: 0.5; + transform: rotate(5deg); + z-index: 1000; + box-shadow: 0 10px 20px rgba(0, 0, 0, 0.3); +} + +.drag-placeholder { + border: 2px dashed var(--primary-color) !important; + background: rgba(37, 99, 235, 0.1) !important; + opacity: 0.7; +} + +.drag-placeholder .service-name { + color: var(--primary-color) !important; + font-style: italic; +} + +body.dragging { + cursor: grabbing !important; + user-select: none; +} + +body.dragging * { + cursor: grabbing !important; +} + +.service-drag-handle { + cursor: grab; + opacity: 0.6; + transition: opacity 0.2s ease; +} + +.service-drag-handle:hover { + opacity: 1; + color: var(--primary-color); +} + +.service-drag-handle:active { + cursor: grabbing; +} + +.selected-service-item.dragging { + opacity: 0.5; + transform: rotate(5deg); + transition: none; + z-index: 1000; + position: relative; +} + +/* Dark theme drag styles */ +[data-theme="dark"] .drag-placeholder { + border-color: var(--primary-color) !important; + background: rgba(37, 99, 235, 0.15) !important; +}