| |
|
|
| |
| |
| |
|
|
| import Storage from './storage.js'; |
|
|
| const instances = new Map(); |
|
|
| |
| |
| |
| export function enable(containerId, itemSelector, options = {}) { |
| const container = document.getElementById(containerId); |
| if (!container) { |
| console.warn('[DragSort] container not found:', containerId); |
| return; |
| } |
|
|
| |
| const key = options.instanceKey || containerId; |
|
|
| |
| if (instances.has(key)) { |
| instances.get(key).cleanup(); |
| instances.delete(key); |
| } |
|
|
| const config = { |
| onReorder: options.onReorder || null, |
| handleSelector: options.handleSelector || null, |
| excludeSelector: options.excludeSelector || null, |
| storageKey: options.storageKey || null, |
| }; |
|
|
| let draggedEl = null; |
| let placeholder = null; |
| let offsetY = 0; |
| let items = []; |
|
|
| function getItems() { |
| let all = Array.from(container.querySelectorAll(itemSelector)); |
| if (config.excludeSelector) { |
| all = all.filter(el => !el.matches(config.excludeSelector)); |
| } |
| return all; |
| } |
|
|
| function createPlaceholder(height) { |
| const ph = document.createElement('div'); |
| ph.className = 'drag-placeholder'; |
| ph.style.height = height + 'px'; |
| ph.style.margin = '4px 0'; |
| return ph; |
| } |
|
|
| |
|
|
| function startDrag(clientY, item) { |
| const rect = item.getBoundingClientRect(); |
| const containerRect = container.getBoundingClientRect(); |
| const relativeTop = rect.top - containerRect.top + container.scrollTop; |
| const relativeLeft = rect.left - containerRect.left; |
|
|
| offsetY = clientY - rect.top; |
| items = getItems(); |
| draggedEl = item; |
|
|
| if (getComputedStyle(container).position === 'static') { |
| container.style.position = 'relative'; |
| } |
|
|
| placeholder = createPlaceholder(rect.height); |
| item.parentNode.insertBefore(placeholder, item); |
|
|
| item.classList.add('dragging'); |
| Object.assign(item.style, { |
| position: 'absolute', |
| width: rect.width + 'px', |
| left: relativeLeft + 'px', |
| top: relativeTop + 'px', |
| zIndex: '9999', |
| pointerEvents: 'none', |
| margin: '0', |
| boxSizing: 'border-box', |
| transition: 'none' |
| }); |
| } |
|
|
| function moveDrag(clientY) { |
| if (!draggedEl) return; |
| const containerRect = container.getBoundingClientRect(); |
| const newTop = clientY - offsetY - containerRect.top + container.scrollTop; |
| draggedEl.style.top = newTop + 'px'; |
|
|
| const otherItems = items.filter(i => i !== draggedEl); |
| const dragRect = draggedEl.getBoundingClientRect(); |
| const dragCenter = dragRect.top + dragRect.height / 2; |
| let insertBefore = null; |
|
|
| for (const item of otherItems) { |
| const rect = item.getBoundingClientRect(); |
| const itemCenter = rect.top + rect.height / 2; |
| if (dragCenter < itemCenter) { |
| insertBefore = item; |
| break; |
| } |
| } |
|
|
| if (insertBefore) { |
| if (placeholder.nextElementSibling !== insertBefore) { |
| container.insertBefore(placeholder, insertBefore); |
| } |
| } else if (otherItems.length > 0) { |
| const lastItem = otherItems[otherItems.length - 1]; |
| if (placeholder !== lastItem.nextElementSibling) { |
| container.insertBefore(placeholder, lastItem.nextElementSibling); |
| } |
| } |
| } |
|
|
| function endDrag() { |
| if (!draggedEl) return; |
|
|
| const phRect = placeholder.getBoundingClientRect(); |
| const containerRect = container.getBoundingClientRect(); |
| const snapTop = phRect.top - containerRect.top + container.scrollTop; |
| draggedEl.style.transition = 'top 0.08s ease-out'; |
| draggedEl.style.top = snapTop + 'px'; |
|
|
| setTimeout(() => { |
| placeholder.parentNode.insertBefore(draggedEl, placeholder); |
| placeholder.remove(); |
| draggedEl.classList.remove('dragging'); |
| draggedEl.style.cssText = ''; |
|
|
| if (config.storageKey) { |
| const ids = getItems().map(el => |
| el.dataset.sessionId || el.dataset.modelId || el.dataset.filePath |
| ).filter(Boolean); |
| if (ids.length) { |
| Storage.setJSON(config.storageKey, ids); |
| } |
| } |
| if (config.onReorder) { |
| config.onReorder(getItems()); |
| } |
|
|
| draggedEl = null; |
| placeholder = null; |
| items = []; |
| }, 80); |
| } |
|
|
| |
|
|
| function onMouseDown(e) { |
| if (e.button !== 0) return; |
| if (config.handleSelector && !e.target.closest(config.handleSelector)) return; |
| const item = e.target.closest(itemSelector); |
| if (!item || !container.contains(item)) return; |
| if (config.excludeSelector && item.matches(config.excludeSelector)) return; |
|
|
| e.preventDefault(); |
| e.stopPropagation(); |
| startDrag(e.clientY, item); |
| document.addEventListener('mousemove', onMouseMove); |
| document.addEventListener('mouseup', onMouseUp); |
| } |
|
|
| function onMouseMove(e) { moveDrag(e.clientY); } |
|
|
| function onMouseUp() { |
| document.removeEventListener('mousemove', onMouseMove); |
| document.removeEventListener('mouseup', onMouseUp); |
| endDrag(); |
| } |
|
|
| |
|
|
| let _touchTimer = null; |
| let _touchStartY = 0; |
| let _touchItem = null; |
|
|
| function onTouchStart(e) { |
| |
| if (e.target.closest('button, input, select, a')) return; |
| |
| |
| if (config.handleSelector && !e.target.closest(config.handleSelector)) return; |
| const item = e.target.closest(itemSelector); |
| if (!item || !container.contains(item)) return; |
| if (config.excludeSelector && item.matches(config.excludeSelector)) return; |
|
|
| _touchItem = item; |
| const startY = e.touches[0].clientY; |
| _touchStartY = startY; |
|
|
| |
| _touchTimer = setTimeout(() => { |
| _touchTimer = null; |
| if (!_touchItem) return; |
| |
| if (navigator.vibrate) navigator.vibrate(30); |
| startDrag(startY, _touchItem); |
| _touchItem.classList.add('touch-dragging'); |
| }, 400); |
| } |
|
|
| function onTouchMove(e) { |
| |
| if (_touchTimer) { |
| const dy = Math.abs(e.touches[0].clientY - _touchStartY); |
| if (dy > 10) { |
| clearTimeout(_touchTimer); |
| _touchTimer = null; |
| _touchItem = null; |
| } |
| return; |
| } |
|
|
| |
| if (draggedEl) { |
| e.preventDefault(); |
| moveDrag(e.touches[0].clientY); |
| } |
| } |
|
|
| function onTouchEnd() { |
| if (_touchTimer) { |
| clearTimeout(_touchTimer); |
| _touchTimer = null; |
| _touchItem = null; |
| return; |
| } |
| if (draggedEl) { |
| draggedEl.classList.remove('touch-dragging'); |
| endDrag(); |
| } |
| } |
|
|
| container.addEventListener('mousedown', onMouseDown); |
| container.addEventListener('touchstart', onTouchStart, { passive: true }); |
| container.addEventListener('touchmove', onTouchMove, { passive: false }); |
| container.addEventListener('touchend', onTouchEnd); |
| container.addEventListener('touchcancel', onTouchEnd); |
|
|
| const instance = { |
| cleanup: () => { |
| container.removeEventListener('mousedown', onMouseDown); |
| container.removeEventListener('touchstart', onTouchStart); |
| container.removeEventListener('touchmove', onTouchMove); |
| container.removeEventListener('touchend', onTouchEnd); |
| container.removeEventListener('touchcancel', onTouchEnd); |
| }, |
| refresh: () => { items = getItems(); } |
| }; |
| instances.set(key, instance); |
| return instance; |
| } |
|
|
| const dragSortModule = { enable }; |
| export default dragSortModule; |
| window.dragSortModule = dragSortModule; |
|
|