Rewrite Features config drag-drop with pointer/touch events
Replace HTML5 Drag and Drop API with pointer/touch event handling for better mobile support: - Add pointer event handlers for desktop/stylus drag - Add global touch event listeners for mobile drag - Track drag state with refs for consistent behavior - Calculate drop position based on Y coordinate - Improve Cancel to restore from initial snapshot Also improve SwipeTabs to ignore interactive elements (buttons, links, inputs) during swipe detection, and add touch-action: none to order cards for proper touch handling. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -35,7 +35,15 @@ const findTouchById = (touches: React.TouchList, id: number): React.Touch | null
|
|||||||
return touch;
|
return touch;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolveSwipeTarget = (target: EventTarget | null): Element | null => {
|
||||||
|
if (!target) return null;
|
||||||
|
const element = target as Element;
|
||||||
|
if (typeof element.closest === 'function') return element;
|
||||||
|
const node = target as Node & { parentElement?: Element | null };
|
||||||
|
return node.parentElement ?? null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function SwipeTabs<T extends string | number>({
|
export function SwipeTabs<T extends string | number>({
|
||||||
@@ -80,8 +88,18 @@ export function SwipeTabs<T extends string | number>({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const shouldIgnoreSwipe = useCallback((target: EventTarget | null) => {
|
const shouldIgnoreSwipe = useCallback((target: EventTarget | null) => {
|
||||||
if (!target || typeof (target as Element).closest !== 'function') return false;
|
const element = resolveSwipeTarget(target);
|
||||||
return Boolean((target as Element).closest('[data-swipe-ignore="true"]'));
|
if (!element) return false;
|
||||||
|
if (element.closest(
|
||||||
|
'[data-swipe-ignore="true"], button, a, input, select, textarea, [role="button"], [role="link"], [role="switch"], [contenteditable="true"]'
|
||||||
|
)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const label = element.closest('label');
|
||||||
|
if (label && label.querySelector('input, select, textarea, button')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const applyTransform = useCallback((offset: number, animate: boolean) => {
|
const applyTransform = useCallback((offset: number, animate: boolean) => {
|
||||||
|
|||||||
@@ -9,6 +9,17 @@ import { SwipeTabs } from '../../components/SwipeTabs';
|
|||||||
import '../../styles/AdminPanel.css';
|
import '../../styles/AdminPanel.css';
|
||||||
|
|
||||||
type TabId = 'config' | 'dashboard' | 'feature1' | 'feature2' | 'feature3' | 'search' | 'notifications';
|
type TabId = 'config' | 'dashboard' | 'feature1' | 'feature2' | 'feature3' | 'search' | 'notifications';
|
||||||
|
const ORDER_DRAG_THRESHOLD = 4;
|
||||||
|
|
||||||
|
const findTouchById = (touches: TouchList, id: number) => {
|
||||||
|
for (let i = 0; i < touches.length; i += 1) {
|
||||||
|
const touch = touches.item(i);
|
||||||
|
if (touch && touch.identifier === id) {
|
||||||
|
return touch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
export default function Features() {
|
export default function Features() {
|
||||||
const { user: currentUser } = useAuth();
|
const { user: currentUser } = useAuth();
|
||||||
@@ -26,6 +37,27 @@ export default function Features() {
|
|||||||
const isUserEditing = useRef(false); // Track if user is actively editing
|
const isUserEditing = useRef(false); // Track if user is actively editing
|
||||||
const initialOrderRef = useRef<string[]>([]);
|
const initialOrderRef = useRef<string[]>([]);
|
||||||
const initialPositionsRef = useRef<Record<string, 'top' | 'bottom'>>({});
|
const initialPositionsRef = useRef<Record<string, 'top' | 'bottom'>>({});
|
||||||
|
const topOrderRef = useRef<HTMLDivElement>(null);
|
||||||
|
const bottomOrderRef = useRef<HTMLDivElement>(null);
|
||||||
|
const localOrderRef = useRef<string[]>([]);
|
||||||
|
const localPositionsRef = useRef<Record<string, 'top' | 'bottom'>>({});
|
||||||
|
const dragPointerRef = useRef({
|
||||||
|
pointerId: null as number | null,
|
||||||
|
touchId: null as number | null,
|
||||||
|
activeId: null as string | null,
|
||||||
|
startX: 0,
|
||||||
|
startY: 0,
|
||||||
|
currentY: 0,
|
||||||
|
isDragging: false,
|
||||||
|
source: null as 'pointer' | 'touch' | null
|
||||||
|
});
|
||||||
|
const globalListenersRef = useRef({ touch: false });
|
||||||
|
const touchHandlersRef = useRef({
|
||||||
|
move: (_event: TouchEvent) => {},
|
||||||
|
end: (_event: TouchEvent) => {},
|
||||||
|
cancel: (_event: TouchEvent) => {}
|
||||||
|
});
|
||||||
|
const supportsPointerEvents = typeof window !== 'undefined' && 'PointerEvent' in window;
|
||||||
|
|
||||||
const buildFullOrder = useCallback((order: string[]) => {
|
const buildFullOrder = useCallback((order: string[]) => {
|
||||||
const fullOrder = [...order];
|
const fullOrder = [...order];
|
||||||
@@ -86,6 +118,14 @@ export default function Features() {
|
|||||||
}
|
}
|
||||||
}, [buildPositionsFromStates]);
|
}, [buildPositionsFromStates]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
localOrderRef.current = localOrder;
|
||||||
|
}, [localOrder]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
localPositionsRef.current = localPositions;
|
||||||
|
}, [localPositions]);
|
||||||
|
|
||||||
// Scroll active tab to center of container
|
// Scroll active tab to center of container
|
||||||
const scrollActiveTabIntoView = useCallback((tabId: string) => {
|
const scrollActiveTabIntoView = useCallback((tabId: string) => {
|
||||||
if (tabsContainerRef.current) {
|
if (tabsContainerRef.current) {
|
||||||
@@ -186,79 +226,248 @@ export default function Features() {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Drag and drop handlers for ordering
|
const getSectionForY = useCallback((y: number): 'top' | 'bottom' => {
|
||||||
const handleDragStart = (e: React.DragEvent, moduleId: string) => {
|
const topRect = topOrderRef.current?.getBoundingClientRect();
|
||||||
|
const bottomRect = bottomOrderRef.current?.getBoundingClientRect();
|
||||||
|
const inTop = topRect && y >= topRect.top && y <= topRect.bottom;
|
||||||
|
const inBottom = bottomRect && y >= bottomRect.top && y <= bottomRect.bottom;
|
||||||
|
if (inTop) return 'top';
|
||||||
|
if (inBottom) return 'bottom';
|
||||||
|
if (topRect && bottomRect) {
|
||||||
|
const topDistance = Math.abs(y - (topRect.top + topRect.bottom) / 2);
|
||||||
|
const bottomDistance = Math.abs(y - (bottomRect.top + bottomRect.bottom) / 2);
|
||||||
|
return topDistance <= bottomDistance ? 'top' : 'bottom';
|
||||||
|
}
|
||||||
|
return topRect ? 'top' : 'bottom';
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getInsertIndex = useCallback((section: 'top' | 'bottom', y: number, draggedId: string) => {
|
||||||
|
const container = section === 'top' ? topOrderRef.current : bottomOrderRef.current;
|
||||||
|
if (!container) return 0;
|
||||||
|
const cards = Array.from(container.querySelectorAll<HTMLElement>('[data-module-id]'))
|
||||||
|
.filter(card => card.dataset.moduleId !== draggedId);
|
||||||
|
for (let i = 0; i < cards.length; i += 1) {
|
||||||
|
const rect = cards[i].getBoundingClientRect();
|
||||||
|
const midpoint = rect.top + rect.height / 2;
|
||||||
|
if (y < midpoint) return i;
|
||||||
|
}
|
||||||
|
return cards.length;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const resetPointerDrag = useCallback(() => {
|
||||||
|
dragPointerRef.current.pointerId = null;
|
||||||
|
dragPointerRef.current.touchId = null;
|
||||||
|
dragPointerRef.current.activeId = null;
|
||||||
|
dragPointerRef.current.startX = 0;
|
||||||
|
dragPointerRef.current.startY = 0;
|
||||||
|
dragPointerRef.current.currentY = 0;
|
||||||
|
dragPointerRef.current.isDragging = false;
|
||||||
|
dragPointerRef.current.source = null;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const applyReorder = useCallback((activeId: string, dropY: number) => {
|
||||||
|
const targetSection = getSectionForY(dropY);
|
||||||
|
const insertIndex = getInsertIndex(targetSection, dropY, activeId);
|
||||||
|
const baseOrder = localOrderRef.current.length
|
||||||
|
? localOrderRef.current
|
||||||
|
: buildFullOrder(moduleOrder);
|
||||||
|
const basePositions = Object.keys(localPositionsRef.current).length
|
||||||
|
? localPositionsRef.current
|
||||||
|
: buildPositionsFromStates();
|
||||||
|
const nextPositions = { ...basePositions, [activeId]: targetSection };
|
||||||
|
const remaining = baseOrder.filter(id => id !== activeId);
|
||||||
|
const topList = remaining.filter(id => nextPositions[id] !== 'bottom');
|
||||||
|
const bottomList = remaining.filter(id => nextPositions[id] === 'bottom');
|
||||||
|
const targetList = targetSection === 'top' ? topList : bottomList;
|
||||||
|
const clampedIndex = Math.max(0, Math.min(insertIndex, targetList.length));
|
||||||
|
targetList.splice(clampedIndex, 0, activeId);
|
||||||
|
const nextOrder = targetSection === 'top'
|
||||||
|
? [...targetList, ...bottomList]
|
||||||
|
: [...topList, ...targetList];
|
||||||
|
const orderChanged = nextOrder.length !== baseOrder.length
|
||||||
|
|| nextOrder.some((id, index) => id !== baseOrder[index]);
|
||||||
|
const previousPosition = basePositions[activeId] || 'top';
|
||||||
|
const positionChanged = previousPosition !== targetSection;
|
||||||
|
if (orderChanged || positionChanged) {
|
||||||
|
localOrderRef.current = nextOrder;
|
||||||
|
localPositionsRef.current = nextPositions;
|
||||||
|
setLocalPositions(nextPositions);
|
||||||
|
setLocalOrder(nextOrder);
|
||||||
|
setHasOrderChanges(true);
|
||||||
|
}
|
||||||
|
}, [buildFullOrder, buildPositionsFromStates, getInsertIndex, getSectionForY, moduleOrder]);
|
||||||
|
|
||||||
|
const handleOrderPointerMove = useCallback((event: React.PointerEvent<HTMLDivElement>) => {
|
||||||
|
const dragState = dragPointerRef.current;
|
||||||
|
if (dragState.pointerId !== event.pointerId || !dragState.activeId || dragState.source !== 'pointer') return;
|
||||||
|
dragState.currentY = event.clientY;
|
||||||
|
const dx = event.clientX - dragState.startX;
|
||||||
|
const dy = event.clientY - dragState.startY;
|
||||||
|
if (!dragState.isDragging) {
|
||||||
|
if (Math.abs(dx) < ORDER_DRAG_THRESHOLD && Math.abs(dy) < ORDER_DRAG_THRESHOLD) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dragState.isDragging = true;
|
||||||
|
setDraggedItem(dragState.activeId);
|
||||||
|
}
|
||||||
|
if (dragState.isDragging) {
|
||||||
|
applyReorder(dragState.activeId, dragState.currentY);
|
||||||
|
}
|
||||||
|
if (dragState.isDragging && event.pointerType !== 'mouse' && event.cancelable) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
}, [applyReorder]);
|
||||||
|
|
||||||
|
const handleOrderPointerUp = useCallback((event: React.PointerEvent<HTMLDivElement>) => {
|
||||||
|
const dragState = dragPointerRef.current;
|
||||||
|
if (dragState.pointerId !== event.pointerId || dragState.source !== 'pointer') return;
|
||||||
|
if (event.currentTarget.hasPointerCapture?.(event.pointerId)) {
|
||||||
|
event.currentTarget.releasePointerCapture(event.pointerId);
|
||||||
|
}
|
||||||
|
const activeId = dragState.activeId;
|
||||||
|
const wasDragging = dragState.isDragging;
|
||||||
|
const dropY = dragState.currentY;
|
||||||
|
resetPointerDrag();
|
||||||
|
setDraggedItem(null);
|
||||||
|
if (!activeId || !wasDragging) return;
|
||||||
|
applyReorder(activeId, dropY);
|
||||||
|
}, [applyReorder, resetPointerDrag]);
|
||||||
|
|
||||||
|
const handleOrderPointerCancel = useCallback((event: React.PointerEvent<HTMLDivElement>) => {
|
||||||
|
const dragState = dragPointerRef.current;
|
||||||
|
if (dragState.pointerId !== event.pointerId || dragState.source !== 'pointer') return;
|
||||||
|
if (event.currentTarget.hasPointerCapture?.(event.pointerId)) {
|
||||||
|
event.currentTarget.releasePointerCapture(event.pointerId);
|
||||||
|
}
|
||||||
|
resetPointerDrag();
|
||||||
|
setDraggedItem(null);
|
||||||
|
}, [resetPointerDrag]);
|
||||||
|
|
||||||
|
const addTouchListeners = useCallback(() => {
|
||||||
|
if (typeof window === 'undefined' || globalListenersRef.current.touch) return;
|
||||||
|
window.addEventListener('touchmove', touchHandlersRef.current.move, { passive: false });
|
||||||
|
window.addEventListener('touchend', touchHandlersRef.current.end, { passive: false });
|
||||||
|
window.addEventListener('touchcancel', touchHandlersRef.current.cancel, { passive: false });
|
||||||
|
globalListenersRef.current.touch = true;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const removeTouchListeners = useCallback(() => {
|
||||||
|
if (typeof window === 'undefined' || !globalListenersRef.current.touch) return;
|
||||||
|
window.removeEventListener('touchmove', touchHandlersRef.current.move);
|
||||||
|
window.removeEventListener('touchend', touchHandlersRef.current.end);
|
||||||
|
window.removeEventListener('touchcancel', touchHandlersRef.current.cancel);
|
||||||
|
globalListenersRef.current.touch = false;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleOrderPointerDown = useCallback((event: React.PointerEvent<HTMLDivElement>, moduleId: string) => {
|
||||||
|
if (event.pointerType === 'mouse' && event.button !== 0) return;
|
||||||
beginOrderEdit();
|
beginOrderEdit();
|
||||||
setDraggedItem(moduleId);
|
setDraggedItem(moduleId);
|
||||||
e.dataTransfer.effectAllowed = 'move';
|
dragPointerRef.current.pointerId = event.pointerId;
|
||||||
e.dataTransfer.setData('text/plain', moduleId);
|
dragPointerRef.current.touchId = null;
|
||||||
};
|
dragPointerRef.current.activeId = moduleId;
|
||||||
|
dragPointerRef.current.startX = event.clientX;
|
||||||
|
dragPointerRef.current.startY = event.clientY;
|
||||||
|
dragPointerRef.current.currentY = event.clientY;
|
||||||
|
dragPointerRef.current.isDragging = false;
|
||||||
|
dragPointerRef.current.source = 'pointer';
|
||||||
|
if (event.currentTarget.setPointerCapture) {
|
||||||
|
try {
|
||||||
|
event.currentTarget.setPointerCapture(event.pointerId);
|
||||||
|
} catch {
|
||||||
|
// Ignore pointer capture failures.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [beginOrderEdit]);
|
||||||
|
|
||||||
const handleDragEnd = () => {
|
const handleGlobalTouchMove = useCallback((event: TouchEvent) => {
|
||||||
|
const dragState = dragPointerRef.current;
|
||||||
|
if (dragState.source !== 'touch' || dragState.touchId === null || !dragState.activeId) return;
|
||||||
|
const touch = findTouchById(event.changedTouches, dragState.touchId);
|
||||||
|
if (!touch) return;
|
||||||
|
dragState.currentY = touch.clientY;
|
||||||
|
const dx = touch.clientX - dragState.startX;
|
||||||
|
const dy = touch.clientY - dragState.startY;
|
||||||
|
if (!dragState.isDragging) {
|
||||||
|
if (Math.abs(dx) < ORDER_DRAG_THRESHOLD && Math.abs(dy) < ORDER_DRAG_THRESHOLD) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dragState.isDragging = true;
|
||||||
|
setDraggedItem(dragState.activeId);
|
||||||
|
}
|
||||||
|
if (dragState.isDragging) {
|
||||||
|
applyReorder(dragState.activeId, dragState.currentY);
|
||||||
|
}
|
||||||
|
if (dragState.isDragging && event.cancelable) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
}, [applyReorder]);
|
||||||
|
|
||||||
|
const handleGlobalTouchEnd = useCallback((event: TouchEvent) => {
|
||||||
|
const dragState = dragPointerRef.current;
|
||||||
|
if (dragState.source !== 'touch' || dragState.touchId === null) return;
|
||||||
|
const touch = findTouchById(event.changedTouches, dragState.touchId);
|
||||||
|
if (!touch) return;
|
||||||
|
const activeId = dragState.activeId;
|
||||||
|
const wasDragging = dragState.isDragging;
|
||||||
|
const dropY = dragState.currentY;
|
||||||
|
removeTouchListeners();
|
||||||
|
resetPointerDrag();
|
||||||
setDraggedItem(null);
|
setDraggedItem(null);
|
||||||
};
|
if (!activeId || !wasDragging) return;
|
||||||
|
applyReorder(activeId, dropY);
|
||||||
|
}, [applyReorder, removeTouchListeners, resetPointerDrag]);
|
||||||
|
|
||||||
const handleDragOver = (e: React.DragEvent) => {
|
const handleGlobalTouchCancel = useCallback((event: TouchEvent) => {
|
||||||
e.preventDefault();
|
const dragState = dragPointerRef.current;
|
||||||
e.dataTransfer.dropEffect = 'move';
|
if (dragState.source !== 'touch' || dragState.touchId === null) return;
|
||||||
};
|
const touch = findTouchById(event.changedTouches, dragState.touchId);
|
||||||
|
if (!touch) return;
|
||||||
const handleDrop = (e: React.DragEvent, targetModuleId: string, targetSection: 'top' | 'bottom') => {
|
removeTouchListeners();
|
||||||
e.preventDefault();
|
resetPointerDrag();
|
||||||
e.stopPropagation();
|
setDraggedItem(null);
|
||||||
if (!draggedItem) return;
|
}, [removeTouchListeners, resetPointerDrag]);
|
||||||
|
|
||||||
const draggedPosition = localPositions[draggedItem] || 'top';
|
|
||||||
|
|
||||||
const isSameItem = draggedItem === targetModuleId;
|
|
||||||
const positionChanged = draggedPosition !== targetSection;
|
|
||||||
|
|
||||||
if (isSameItem && !positionChanged) return;
|
|
||||||
|
|
||||||
|
const handleOrderTouchStart = useCallback((event: React.TouchEvent<HTMLDivElement>, moduleId: string) => {
|
||||||
|
if (event.changedTouches.length === 0) return;
|
||||||
beginOrderEdit();
|
beginOrderEdit();
|
||||||
if (positionChanged) {
|
setDraggedItem(moduleId);
|
||||||
// Update local positions immediately for UI responsiveness
|
const touch = event.changedTouches[0];
|
||||||
setLocalPositions(prev => ({ ...prev, [draggedItem]: targetSection }));
|
dragPointerRef.current.pointerId = null;
|
||||||
}
|
dragPointerRef.current.touchId = touch.identifier;
|
||||||
if (isSameItem) {
|
dragPointerRef.current.activeId = moduleId;
|
||||||
setHasOrderChanges(true);
|
dragPointerRef.current.startX = touch.clientX;
|
||||||
return;
|
dragPointerRef.current.startY = touch.clientY;
|
||||||
}
|
dragPointerRef.current.currentY = touch.clientY;
|
||||||
|
dragPointerRef.current.isDragging = false;
|
||||||
|
dragPointerRef.current.source = 'touch';
|
||||||
|
addTouchListeners();
|
||||||
|
}, [addTouchListeners, beginOrderEdit]);
|
||||||
|
|
||||||
// Reorder within the list
|
useEffect(() => {
|
||||||
const newOrder = [...localOrder];
|
touchHandlersRef.current = {
|
||||||
const draggedIndex = newOrder.indexOf(draggedItem);
|
move: handleGlobalTouchMove,
|
||||||
const targetIndex = newOrder.indexOf(targetModuleId);
|
end: handleGlobalTouchEnd,
|
||||||
|
cancel: handleGlobalTouchCancel
|
||||||
|
};
|
||||||
|
}, [handleGlobalTouchCancel, handleGlobalTouchEnd, handleGlobalTouchMove]);
|
||||||
|
|
||||||
if (draggedIndex === -1 || targetIndex === -1) return;
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
newOrder.splice(draggedIndex, 1);
|
removeTouchListeners();
|
||||||
newOrder.splice(targetIndex, 0, draggedItem);
|
};
|
||||||
|
}, [removeTouchListeners]);
|
||||||
setLocalOrder(newOrder);
|
|
||||||
setHasOrderChanges(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSectionDrop = (e: React.DragEvent, section: 'top' | 'bottom') => {
|
|
||||||
e.preventDefault();
|
|
||||||
if (!draggedItem) return;
|
|
||||||
|
|
||||||
const draggedPosition = localPositions[draggedItem] || 'top';
|
|
||||||
|
|
||||||
// Change position if moving to different section
|
|
||||||
if (draggedPosition !== section) {
|
|
||||||
beginOrderEdit();
|
|
||||||
// Update local positions immediately for UI responsiveness
|
|
||||||
setLocalPositions(prev => ({ ...prev, [draggedItem]: section }));
|
|
||||||
setHasOrderChanges(true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleApplyOrder = async () => {
|
const handleApplyOrder = async () => {
|
||||||
|
const orderToApply = localOrderRef.current.length ? localOrderRef.current : localOrder;
|
||||||
|
const positionsToApply = Object.keys(localPositionsRef.current).length
|
||||||
|
? localPositionsRef.current
|
||||||
|
: localPositions;
|
||||||
try {
|
try {
|
||||||
|
// Update positions in context first
|
||||||
const positionUpdates: Array<{ id: ModuleId; position: 'top' | 'bottom' }> = [];
|
const positionUpdates: Array<{ id: ModuleId; position: 'top' | 'bottom' }> = [];
|
||||||
TOGGLEABLE_MODULES.forEach(module => {
|
TOGGLEABLE_MODULES.forEach(module => {
|
||||||
const desiredPosition = localPositions[module.id] || module.defaultPosition;
|
const desiredPosition = positionsToApply[module.id] || module.defaultPosition;
|
||||||
const currentPosition = moduleStates[module.id]?.position || module.defaultPosition;
|
const currentPosition = moduleStates[module.id]?.position || module.defaultPosition;
|
||||||
if (desiredPosition !== currentPosition) {
|
if (desiredPosition !== currentPosition) {
|
||||||
positionUpdates.push({ id: module.id, position: desiredPosition });
|
positionUpdates.push({ id: module.id, position: desiredPosition });
|
||||||
@@ -266,35 +475,52 @@ export default function Features() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (positionUpdates.length > 0) {
|
if (positionUpdates.length > 0) {
|
||||||
hasUserMadeChanges.current = true;
|
|
||||||
positionUpdates.forEach(({ id, position }) => {
|
positionUpdates.forEach(({ id, position }) => {
|
||||||
setModulePosition(id, position);
|
setModulePosition(id, position);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setModuleOrder(localOrder);
|
// Update order in context
|
||||||
await saveModuleOrder(localOrder);
|
setModuleOrder(orderToApply);
|
||||||
|
|
||||||
|
// Save both order and positions to backend immediately
|
||||||
|
await saveModuleOrder(orderToApply);
|
||||||
|
|
||||||
|
// Save positions explicitly (don't rely on debounced effect)
|
||||||
|
if (positionUpdates.length > 0) {
|
||||||
|
await saveModulesToBackend();
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to save order:', error);
|
console.error('Failed to save order:', error);
|
||||||
} finally {
|
} finally {
|
||||||
setDraggedItem(null); // Reset drag state
|
setDraggedItem(null);
|
||||||
initialOrderRef.current = buildFullOrder(localOrder);
|
localOrderRef.current = orderToApply;
|
||||||
initialPositionsRef.current = normalizePositions(localPositions);
|
localPositionsRef.current = positionsToApply;
|
||||||
|
initialOrderRef.current = buildFullOrder(orderToApply);
|
||||||
|
initialPositionsRef.current = normalizePositions(positionsToApply);
|
||||||
isUserEditing.current = false;
|
isUserEditing.current = false;
|
||||||
setHasOrderChanges(false);
|
setHasOrderChanges(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCancelOrder = () => {
|
const handleCancelOrder = () => {
|
||||||
setDraggedItem(null); // Reset drag state
|
// Reset drag state
|
||||||
isUserEditing.current = false; // Done editing
|
setDraggedItem(null);
|
||||||
// Restore to currently applied state from context
|
isUserEditing.current = false;
|
||||||
const restoredOrder = buildFullOrder(moduleOrder);
|
|
||||||
const restoredPositions = buildPositionsFromStates();
|
const restoredOrder = initialOrderRef.current.length
|
||||||
setLocalOrder(restoredOrder);
|
? [...initialOrderRef.current]
|
||||||
setLocalPositions(restoredPositions);
|
: buildFullOrder(moduleOrder);
|
||||||
initialOrderRef.current = restoredOrder;
|
const restoredPositions = Object.keys(initialPositionsRef.current).length
|
||||||
initialPositionsRef.current = restoredPositions;
|
? { ...normalizePositions(initialPositionsRef.current) }
|
||||||
|
: buildPositionsFromStates();
|
||||||
|
|
||||||
|
localOrderRef.current = restoredOrder;
|
||||||
|
localPositionsRef.current = restoredPositions;
|
||||||
|
setLocalOrder([...restoredOrder]);
|
||||||
|
setLocalPositions({ ...restoredPositions });
|
||||||
|
initialOrderRef.current = [...restoredOrder];
|
||||||
|
initialPositionsRef.current = { ...restoredPositions };
|
||||||
setHasOrderChanges(false);
|
setHasOrderChanges(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -339,8 +565,7 @@ export default function Features() {
|
|||||||
<div
|
<div
|
||||||
className="order-cards"
|
className="order-cards"
|
||||||
data-swipe-ignore="true"
|
data-swipe-ignore="true"
|
||||||
onDragOver={handleDragOver}
|
ref={topOrderRef}
|
||||||
onDrop={(e) => handleSectionDrop(e, 'top')}
|
|
||||||
>
|
>
|
||||||
{topOrderModules.map((moduleId) => {
|
{topOrderModules.map((moduleId) => {
|
||||||
const moduleInfo = getModuleInfo(moduleId);
|
const moduleInfo = getModuleInfo(moduleId);
|
||||||
@@ -349,11 +574,12 @@ export default function Features() {
|
|||||||
<div
|
<div
|
||||||
key={moduleId}
|
key={moduleId}
|
||||||
className={`order-card ${draggedItem === moduleId ? 'dragging' : ''}`}
|
className={`order-card ${draggedItem === moduleId ? 'dragging' : ''}`}
|
||||||
draggable
|
data-module-id={moduleId}
|
||||||
onDragStart={(e) => handleDragStart(e, moduleId)}
|
onPointerDown={supportsPointerEvents ? (e) => handleOrderPointerDown(e, moduleId) : undefined}
|
||||||
onDragEnd={handleDragEnd}
|
onPointerMove={supportsPointerEvents ? handleOrderPointerMove : undefined}
|
||||||
onDragOver={handleDragOver}
|
onPointerUp={supportsPointerEvents ? handleOrderPointerUp : undefined}
|
||||||
onDrop={(e) => handleDrop(e, moduleId, 'top')}
|
onPointerCancel={supportsPointerEvents ? handleOrderPointerCancel : undefined}
|
||||||
|
onTouchStart={!supportsPointerEvents ? (e) => handleOrderTouchStart(e, moduleId) : undefined}
|
||||||
>
|
>
|
||||||
<div className="order-card-preview">
|
<div className="order-card-preview">
|
||||||
<span className="material-symbols-outlined">{moduleInfo.icon}</span>
|
<span className="material-symbols-outlined">{moduleInfo.icon}</span>
|
||||||
@@ -380,8 +606,7 @@ export default function Features() {
|
|||||||
<div
|
<div
|
||||||
className="order-cards"
|
className="order-cards"
|
||||||
data-swipe-ignore="true"
|
data-swipe-ignore="true"
|
||||||
onDragOver={handleDragOver}
|
ref={bottomOrderRef}
|
||||||
onDrop={(e) => handleSectionDrop(e, 'bottom')}
|
|
||||||
>
|
>
|
||||||
{bottomOrderModules.map((moduleId) => {
|
{bottomOrderModules.map((moduleId) => {
|
||||||
const moduleInfo = getModuleInfo(moduleId);
|
const moduleInfo = getModuleInfo(moduleId);
|
||||||
@@ -390,11 +615,12 @@ export default function Features() {
|
|||||||
<div
|
<div
|
||||||
key={moduleId}
|
key={moduleId}
|
||||||
className={`order-card ${draggedItem === moduleId ? 'dragging' : ''}`}
|
className={`order-card ${draggedItem === moduleId ? 'dragging' : ''}`}
|
||||||
draggable
|
data-module-id={moduleId}
|
||||||
onDragStart={(e) => handleDragStart(e, moduleId)}
|
onPointerDown={supportsPointerEvents ? (e) => handleOrderPointerDown(e, moduleId) : undefined}
|
||||||
onDragEnd={handleDragEnd}
|
onPointerMove={supportsPointerEvents ? handleOrderPointerMove : undefined}
|
||||||
onDragOver={handleDragOver}
|
onPointerUp={supportsPointerEvents ? handleOrderPointerUp : undefined}
|
||||||
onDrop={(e) => handleDrop(e, moduleId, 'bottom')}
|
onPointerCancel={supportsPointerEvents ? handleOrderPointerCancel : undefined}
|
||||||
|
onTouchStart={!supportsPointerEvents ? (e) => handleOrderTouchStart(e, moduleId) : undefined}
|
||||||
>
|
>
|
||||||
<div className="order-card-preview">
|
<div className="order-card-preview">
|
||||||
<span className="material-symbols-outlined">{moduleInfo.icon}</span>
|
<span className="material-symbols-outlined">{moduleInfo.icon}</span>
|
||||||
@@ -415,7 +641,7 @@ export default function Features() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{hasOrderChanges && (
|
{hasOrderChanges && (
|
||||||
<div className="order-actions">
|
<div className="order-actions" data-swipe-ignore="true">
|
||||||
<button className="btn-secondary" onClick={handleCancelOrder}>
|
<button className="btn-secondary" onClick={handleCancelOrder}>
|
||||||
<span className="material-symbols-outlined">close</span>
|
<span className="material-symbols-outlined">close</span>
|
||||||
{t.featuresPage?.cancelOrder || 'Annulla'}
|
{t.featuresPage?.cancelOrder || 'Annulla'}
|
||||||
|
|||||||
@@ -2502,6 +2502,7 @@
|
|||||||
cursor: grab;
|
cursor: grab;
|
||||||
transition: all 0.25s ease;
|
transition: all 0.25s ease;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
touch-action: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.order-card:hover {
|
.order-card:hover {
|
||||||
|
|||||||
Reference in New Issue
Block a user