export function makeDraggable($element, onClick, storageKey) { let isDragging = false; let hasDragged = false; let startPos = { x: 0, y: 0 }; let elementStartPos = { x: 0, y: 0 }; const getEventCoords = (e) => { if (e.touches && e.touches.length > 0) { return { x: e.touches[0].clientX, y: e.touches[0].clientY }; } else if (e.changedTouches && e.changedTouches.length > 0) { return { x: e.changedTouches[0].clientX, y: e.changedTouches[0].clientY }; } return { x: e.clientX, y: e.clientY }; }; const keepInBounds = ($elem) => { const windowWidth = $(window).width(); const windowHeight = $(window).height(); const elemWidth = $elem.outerWidth(); const elemHeight = $elem.outerHeight(); let currentPos = $elem.offset(); let newLeft = Math.max(0, Math.min(currentPos.left, windowWidth - elemWidth)); let newTop = Math.max(0, Math.min(currentPos.top, windowHeight - elemHeight)); $elem.css({ left: newLeft + 'px', top: newTop + 'px', transform: 'none' }); if (storageKey) { localStorage.setItem(storageKey, JSON.stringify({ left: newLeft + 'px', top: newTop + 'px' })); } }; const dragStart = (e) => { e.preventDefault(); isDragging = true; hasDragged = false; const coords = getEventCoords(e.originalEvent || e); startPos = { x: coords.x, y: coords.y }; const offset = $element.offset(); elementStartPos = { x: offset.left, y: offset.top }; $element.css({ 'cursor': 'grabbing', 'transition': 'none' }); $('body').css({ 'user-select': 'none', '-webkit-user-select': 'none', 'overflow': 'hidden' }); }; const DRAG_THRESHOLD = 5; const dragMove = (e) => { if (!isDragging) return; e.preventDefault(); if (!hasDragged) { const coords = getEventCoords(e.originalEvent || e); const dist = Math.abs(coords.x - startPos.x) + Math.abs(coords.y - startPos.y); if (dist < DRAG_THRESHOLD) return; } hasDragged = true; const coords = getEventCoords(e.originalEvent || e); const deltaX = coords.x - startPos.x; const deltaY = coords.y - startPos.y; let newLeft = elementStartPos.x + deltaX; let newTop = elementStartPos.y + deltaY; const windowWidth = $(window).width(); const windowHeight = $(window).height(); const elemWidth = $element.outerWidth(); const elemHeight = $element.outerHeight(); newLeft = Math.max(0, Math.min(newLeft, windowWidth - elemWidth)); newTop = Math.max(0, Math.min(newTop, windowHeight - elemHeight)); $element.css({ left: newLeft + 'px', top: newTop + 'px', transform: 'none' }); }; const dragEnd = (e) => { if (!isDragging) return; isDragging = false; $element.css({ 'cursor': 'grab', 'transition': 'transform 0.2s ease, box-shadow 0.2s ease' }); $('body').css({ 'user-select': 'auto', '-webkit-user-select': 'auto', 'overflow': 'auto' }); keepInBounds($element); if (!hasDragged && onClick) { if (e.type === 'touchend') { e.preventDefault(); setTimeout(onClick, 10); } else { onClick(); } } }; $element.on('mousedown', dragStart); $element.on('touchstart', dragStart); const namespace = '.draggable' + Date.now(); $(document).on(`mousemove${namespace}`, dragMove); $(document).on(`touchmove${namespace}`, dragMove); $(document).on(`mouseup${namespace}`, dragEnd); $(document).on(`touchend${namespace}`, dragEnd); $element.on('click', (e) => { if (hasDragged) { e.preventDefault(); e.stopPropagation(); } }); $(window).on(`resize${namespace}`, () => { if ($element.length) { keepInBounds($element); } }); $element.css({ 'cursor': 'grab', 'user-select': 'none', '-webkit-user-select': 'none' }); if (storageKey) { const savedPos = localStorage.getItem(storageKey); if (savedPos) { $element.css(JSON.parse(savedPos)); setTimeout(() => keepInBounds($element), 0); } } return () => { $element.off('mousedown touchstart click'); $(document).off(namespace); $(window).off(namespace); }; }