|
|
|
|
| |
| |
|
|
|
|
| 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;
|
|
|