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 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>({
|
||||
@@ -80,8 +88,18 @@ export function SwipeTabs<T extends string | number>({
|
||||
});
|
||||
|
||||
const shouldIgnoreSwipe = useCallback((target: EventTarget | null) => {
|
||||
if (!target || typeof (target as Element).closest !== 'function') return false;
|
||||
return Boolean((target as Element).closest('[data-swipe-ignore="true"]'));
|
||||
const element = resolveSwipeTarget(target);
|
||||
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) => {
|
||||
|
||||
@@ -9,6 +9,17 @@ import { SwipeTabs } from '../../components/SwipeTabs';
|
||||
import '../../styles/AdminPanel.css';
|
||||
|
||||
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() {
|
||||
const { user: currentUser } = useAuth();
|
||||
@@ -26,6 +37,27 @@ export default function Features() {
|
||||
const isUserEditing = useRef(false); // Track if user is actively editing
|
||||
const initialOrderRef = useRef<string[]>([]);
|
||||
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 fullOrder = [...order];
|
||||
@@ -86,6 +118,14 @@ export default function Features() {
|
||||
}
|
||||
}, [buildPositionsFromStates]);
|
||||
|
||||
useEffect(() => {
|
||||
localOrderRef.current = localOrder;
|
||||
}, [localOrder]);
|
||||
|
||||
useEffect(() => {
|
||||
localPositionsRef.current = localPositions;
|
||||
}, [localPositions]);
|
||||
|
||||
// Scroll active tab to center of container
|
||||
const scrollActiveTabIntoView = useCallback((tabId: string) => {
|
||||
if (tabsContainerRef.current) {
|
||||
@@ -186,79 +226,248 @@ export default function Features() {
|
||||
);
|
||||
};
|
||||
|
||||
// Drag and drop handlers for ordering
|
||||
const handleDragStart = (e: React.DragEvent, moduleId: string) => {
|
||||
const getSectionForY = useCallback((y: number): 'top' | 'bottom' => {
|
||||
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();
|
||||
setDraggedItem(moduleId);
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
e.dataTransfer.setData('text/plain', moduleId);
|
||||
};
|
||||
dragPointerRef.current.pointerId = event.pointerId;
|
||||
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);
|
||||
};
|
||||
if (!activeId || !wasDragging) return;
|
||||
applyReorder(activeId, dropY);
|
||||
}, [applyReorder, removeTouchListeners, resetPointerDrag]);
|
||||
|
||||
const handleDragOver = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = 'move';
|
||||
};
|
||||
|
||||
const handleDrop = (e: React.DragEvent, targetModuleId: string, targetSection: 'top' | 'bottom') => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (!draggedItem) return;
|
||||
|
||||
const draggedPosition = localPositions[draggedItem] || 'top';
|
||||
|
||||
const isSameItem = draggedItem === targetModuleId;
|
||||
const positionChanged = draggedPosition !== targetSection;
|
||||
|
||||
if (isSameItem && !positionChanged) return;
|
||||
const handleGlobalTouchCancel = 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;
|
||||
removeTouchListeners();
|
||||
resetPointerDrag();
|
||||
setDraggedItem(null);
|
||||
}, [removeTouchListeners, resetPointerDrag]);
|
||||
|
||||
const handleOrderTouchStart = useCallback((event: React.TouchEvent<HTMLDivElement>, moduleId: string) => {
|
||||
if (event.changedTouches.length === 0) return;
|
||||
beginOrderEdit();
|
||||
if (positionChanged) {
|
||||
// Update local positions immediately for UI responsiveness
|
||||
setLocalPositions(prev => ({ ...prev, [draggedItem]: targetSection }));
|
||||
}
|
||||
if (isSameItem) {
|
||||
setHasOrderChanges(true);
|
||||
return;
|
||||
}
|
||||
setDraggedItem(moduleId);
|
||||
const touch = event.changedTouches[0];
|
||||
dragPointerRef.current.pointerId = null;
|
||||
dragPointerRef.current.touchId = touch.identifier;
|
||||
dragPointerRef.current.activeId = moduleId;
|
||||
dragPointerRef.current.startX = touch.clientX;
|
||||
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
|
||||
const newOrder = [...localOrder];
|
||||
const draggedIndex = newOrder.indexOf(draggedItem);
|
||||
const targetIndex = newOrder.indexOf(targetModuleId);
|
||||
useEffect(() => {
|
||||
touchHandlersRef.current = {
|
||||
move: handleGlobalTouchMove,
|
||||
end: handleGlobalTouchEnd,
|
||||
cancel: handleGlobalTouchCancel
|
||||
};
|
||||
}, [handleGlobalTouchCancel, handleGlobalTouchEnd, handleGlobalTouchMove]);
|
||||
|
||||
if (draggedIndex === -1 || targetIndex === -1) return;
|
||||
|
||||
newOrder.splice(draggedIndex, 1);
|
||||
newOrder.splice(targetIndex, 0, draggedItem);
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
removeTouchListeners();
|
||||
};
|
||||
}, [removeTouchListeners]);
|
||||
|
||||
const handleApplyOrder = async () => {
|
||||
const orderToApply = localOrderRef.current.length ? localOrderRef.current : localOrder;
|
||||
const positionsToApply = Object.keys(localPositionsRef.current).length
|
||||
? localPositionsRef.current
|
||||
: localPositions;
|
||||
try {
|
||||
// Update positions in context first
|
||||
const positionUpdates: Array<{ id: ModuleId; position: 'top' | 'bottom' }> = [];
|
||||
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;
|
||||
if (desiredPosition !== currentPosition) {
|
||||
positionUpdates.push({ id: module.id, position: desiredPosition });
|
||||
@@ -266,35 +475,52 @@ export default function Features() {
|
||||
});
|
||||
|
||||
if (positionUpdates.length > 0) {
|
||||
hasUserMadeChanges.current = true;
|
||||
positionUpdates.forEach(({ id, position }) => {
|
||||
setModulePosition(id, position);
|
||||
});
|
||||
}
|
||||
|
||||
setModuleOrder(localOrder);
|
||||
await saveModuleOrder(localOrder);
|
||||
// Update order in context
|
||||
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) {
|
||||
console.error('Failed to save order:', error);
|
||||
} finally {
|
||||
setDraggedItem(null); // Reset drag state
|
||||
initialOrderRef.current = buildFullOrder(localOrder);
|
||||
initialPositionsRef.current = normalizePositions(localPositions);
|
||||
setDraggedItem(null);
|
||||
localOrderRef.current = orderToApply;
|
||||
localPositionsRef.current = positionsToApply;
|
||||
initialOrderRef.current = buildFullOrder(orderToApply);
|
||||
initialPositionsRef.current = normalizePositions(positionsToApply);
|
||||
isUserEditing.current = false;
|
||||
setHasOrderChanges(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancelOrder = () => {
|
||||
setDraggedItem(null); // Reset drag state
|
||||
isUserEditing.current = false; // Done editing
|
||||
// Restore to currently applied state from context
|
||||
const restoredOrder = buildFullOrder(moduleOrder);
|
||||
const restoredPositions = buildPositionsFromStates();
|
||||
setLocalOrder(restoredOrder);
|
||||
setLocalPositions(restoredPositions);
|
||||
initialOrderRef.current = restoredOrder;
|
||||
initialPositionsRef.current = restoredPositions;
|
||||
// Reset drag state
|
||||
setDraggedItem(null);
|
||||
isUserEditing.current = false;
|
||||
|
||||
const restoredOrder = initialOrderRef.current.length
|
||||
? [...initialOrderRef.current]
|
||||
: buildFullOrder(moduleOrder);
|
||||
const restoredPositions = Object.keys(initialPositionsRef.current).length
|
||||
? { ...normalizePositions(initialPositionsRef.current) }
|
||||
: buildPositionsFromStates();
|
||||
|
||||
localOrderRef.current = restoredOrder;
|
||||
localPositionsRef.current = restoredPositions;
|
||||
setLocalOrder([...restoredOrder]);
|
||||
setLocalPositions({ ...restoredPositions });
|
||||
initialOrderRef.current = [...restoredOrder];
|
||||
initialPositionsRef.current = { ...restoredPositions };
|
||||
setHasOrderChanges(false);
|
||||
};
|
||||
|
||||
@@ -339,8 +565,7 @@ export default function Features() {
|
||||
<div
|
||||
className="order-cards"
|
||||
data-swipe-ignore="true"
|
||||
onDragOver={handleDragOver}
|
||||
onDrop={(e) => handleSectionDrop(e, 'top')}
|
||||
ref={topOrderRef}
|
||||
>
|
||||
{topOrderModules.map((moduleId) => {
|
||||
const moduleInfo = getModuleInfo(moduleId);
|
||||
@@ -349,11 +574,12 @@ export default function Features() {
|
||||
<div
|
||||
key={moduleId}
|
||||
className={`order-card ${draggedItem === moduleId ? 'dragging' : ''}`}
|
||||
draggable
|
||||
onDragStart={(e) => handleDragStart(e, moduleId)}
|
||||
onDragEnd={handleDragEnd}
|
||||
onDragOver={handleDragOver}
|
||||
onDrop={(e) => handleDrop(e, moduleId, 'top')}
|
||||
data-module-id={moduleId}
|
||||
onPointerDown={supportsPointerEvents ? (e) => handleOrderPointerDown(e, moduleId) : undefined}
|
||||
onPointerMove={supportsPointerEvents ? handleOrderPointerMove : undefined}
|
||||
onPointerUp={supportsPointerEvents ? handleOrderPointerUp : undefined}
|
||||
onPointerCancel={supportsPointerEvents ? handleOrderPointerCancel : undefined}
|
||||
onTouchStart={!supportsPointerEvents ? (e) => handleOrderTouchStart(e, moduleId) : undefined}
|
||||
>
|
||||
<div className="order-card-preview">
|
||||
<span className="material-symbols-outlined">{moduleInfo.icon}</span>
|
||||
@@ -380,8 +606,7 @@ export default function Features() {
|
||||
<div
|
||||
className="order-cards"
|
||||
data-swipe-ignore="true"
|
||||
onDragOver={handleDragOver}
|
||||
onDrop={(e) => handleSectionDrop(e, 'bottom')}
|
||||
ref={bottomOrderRef}
|
||||
>
|
||||
{bottomOrderModules.map((moduleId) => {
|
||||
const moduleInfo = getModuleInfo(moduleId);
|
||||
@@ -390,11 +615,12 @@ export default function Features() {
|
||||
<div
|
||||
key={moduleId}
|
||||
className={`order-card ${draggedItem === moduleId ? 'dragging' : ''}`}
|
||||
draggable
|
||||
onDragStart={(e) => handleDragStart(e, moduleId)}
|
||||
onDragEnd={handleDragEnd}
|
||||
onDragOver={handleDragOver}
|
||||
onDrop={(e) => handleDrop(e, moduleId, 'bottom')}
|
||||
data-module-id={moduleId}
|
||||
onPointerDown={supportsPointerEvents ? (e) => handleOrderPointerDown(e, moduleId) : undefined}
|
||||
onPointerMove={supportsPointerEvents ? handleOrderPointerMove : undefined}
|
||||
onPointerUp={supportsPointerEvents ? handleOrderPointerUp : undefined}
|
||||
onPointerCancel={supportsPointerEvents ? handleOrderPointerCancel : undefined}
|
||||
onTouchStart={!supportsPointerEvents ? (e) => handleOrderTouchStart(e, moduleId) : undefined}
|
||||
>
|
||||
<div className="order-card-preview">
|
||||
<span className="material-symbols-outlined">{moduleInfo.icon}</span>
|
||||
@@ -415,7 +641,7 @@ export default function Features() {
|
||||
</div>
|
||||
|
||||
{hasOrderChanges && (
|
||||
<div className="order-actions">
|
||||
<div className="order-actions" data-swipe-ignore="true">
|
||||
<button className="btn-secondary" onClick={handleCancelOrder}>
|
||||
<span className="material-symbols-outlined">close</span>
|
||||
{t.featuresPage?.cancelOrder || 'Annulla'}
|
||||
|
||||
@@ -2502,6 +2502,7 @@
|
||||
cursor: grab;
|
||||
transition: all 0.25s ease;
|
||||
user-select: none;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.order-card:hover {
|
||||
|
||||
Reference in New Issue
Block a user