1// static/js/dragSort.js 2 3/** 4 * Vertical drag-and-drop sorting with magnetic snap behavior 5 */ 6 7import Storage from './storage.js'; 8 9const instances = new Map(); 10 11/** 12 * Make a container's children sortable via vertical drag 13 */ 14export function enable(containerId, itemSelector, options = {}) { 15 const container = document.getElementById(containerId); 16 if (!container) { 17 console.warn('[DragSort] container not found:', containerId); 18 return; 19 } 20 21 // Allow multiple instances per container via instanceKey 22 const key = options.instanceKey || containerId; 23 24 // Clean up previous instance 25 if (instances.has(key)) { 26 instances.get(key).cleanup(); 27 instances.delete(key); 28 } 29 30 const config = { 31 onReorder: options.onReorder || null, 32 handleSelector: options.handleSelector || null, 33 excludeSelector: options.excludeSelector || null, 34 storageKey: options.storageKey || null, 35 }; 36 37 let draggedEl = null; 38 let placeholder = null; 39 let offsetY = 0; 40 let items = []; 41 42 function getItems() { 43 let all = Array.from(container.querySelectorAll(itemSelector)); 44 if (config.excludeSelector) { 45 all = all.filter(el => !el.matches(config.excludeSelector)); 46 } 47 return all; 48 } 49 50 function createPlaceholder(height) { 51 const ph = document.createElement('div'); 52 ph.className = 'drag-placeholder'; 53 ph.style.height = height + 'px'; 54 ph.style.margin = '4px 0'; 55 return ph; 56 } 57 58 // --- Shared drag logic --- 59 60 function startDrag(clientY, item) { 61 const rect = item.getBoundingClientRect(); 62 const containerRect = container.getBoundingClientRect(); 63 const relativeTop = rect.top - containerRect.top + container.scrollTop; 64 const relativeLeft = rect.left - containerRect.left; 65 66 offsetY = clientY - rect.top; 67 items = getItems(); 68 draggedEl = item; 69 70 if (getComputedStyle(container).position === 'static') { 71 container.style.position = 'relative'; 72 } 73 74 placeholder = createPlaceholder(rect.height); 75 item.parentNode.insertBefore(placeholder, item); 76 77 item.classList.add('dragging'); 78 Object.assign(item.style, { 79 position: 'absolute', 80 width: rect.width + 'px', 81 left: relativeLeft + 'px', 82 top: relativeTop + 'px', 83 zIndex: '9999', 84 pointerEvents: 'none', 85 margin: '0', 86 boxSizing: 'border-box', 87 transition: 'none' 88 }); 89 } 90 91 function moveDrag(clientY) { 92 if (!draggedEl) return; 93 const containerRect = container.getBoundingClientRect(); 94 const newTop = clientY - offsetY - containerRect.top + container.scrollTop; 95 draggedEl.style.top = newTop + 'px'; 96 97 const otherItems = items.filter(i => i !== draggedEl); 98 const dragRect = draggedEl.getBoundingClientRect(); 99 const dragCenter = dragRect.top + dragRect.height / 2; 100 let insertBefore = null; 101 102 for (const item of otherItems) { 103 const rect = item.getBoundingClientRect(); 104 const itemCenter = rect.top + rect.height / 2; 105 if (dragCenter < itemCenter) { 106 insertBefore = item; 107 break; 108 } 109 } 110 111 if (insertBefore) { 112 if (placeholder.nextElementSibling !== insertBefore) { 113 container.insertBefore(placeholder, insertBefore); 114 } 115 } else if (otherItems.length > 0) { 116 const lastItem = otherItems[otherItems.length - 1]; 117 if (placeholder !== lastItem.nextElementSibling) { 118 container.insertBefore(placeholder, lastItem.nextElementSibling); 119 } 120 } 121 } 122 123 function endDrag() { 124 if (!draggedEl) return; 125 126 const phRect = placeholder.getBoundingClientRect(); 127 const containerRect = container.getBoundingClientRect(); 128 const snapTop = phRect.top - containerRect.top + container.scrollTop; 129 draggedEl.style.transition = 'top 0.08s ease-out'; 130 draggedEl.style.top = snapTop + 'px'; 131 132 setTimeout(() => { 133 placeholder.parentNode.insertBefore(draggedEl, placeholder); 134 placeholder.remove(); 135 draggedEl.classList.remove('dragging'); 136 draggedEl.style.cssText = ''; 137 138 if (config.storageKey) { 139 const ids = getItems().map(el => 140 el.dataset.sessionId || el.dataset.modelId || el.dataset.f
140ilePath 141 ).filter(Boolean); 142 if (ids.length) { 143 Storage.setJSON(config.storageKey, ids); 144 } 145 } 146 if (config.onReorder) { 147 config.onReorder(getItems()); 148 } 149 150 draggedEl = null; 151 placeholder = null; 152 items = []; 153 }, 80); 154 } 155 156 // --- Mouse events --- 157 158 function onMouseDown(e) { 159 if (e.button !== 0) return; 160 if (config.handleSelector && !e.target.closest(config.handleSelector)) return; 161 const item = e.target.closest(itemSelector); 162 if (!item || !container.contains(item)) return; 163 if (config.excludeSelector && item.matches(config.excludeSelector)) return; 164 165 e.preventDefault(); 166 e.stopPropagation(); 167 startDrag(e.clientY, item); 168 document.addEventListener('mousemove', onMouseMove); 169 document.addEventListener('mouseup', onMouseUp); 170 } 171 172 function onMouseMove(e) { moveDrag(e.clientY); } 173 174 function onMouseUp() { 175 document.removeEventListener('mousemove', onMouseMove); 176 document.removeEventListener('mouseup', onMouseUp); 177 endDrag(); 178 } 179 180 // --- Touch events (long-press to start) --- 181 182 let _touchTimer = null; 183 let _touchStartY = 0; 184 let _touchItem = null; 185 186 function onTouchStart(e) { 187 // Don't start on buttons/inputs. 188 if (e.target.closest('button, input, select, a')) return; 189 // Respect handleSelector on touch too â long-press anywhere was 190 // unintentionally letting users start a reorder from the whole row. 191 if (config.handleSelector && !e.target.closest(config.handleSelector)) return; 192 const item = e.target.closest(itemSelector); 193 if (!item || !container.contains(item)) return; 194 if (config.excludeSelector && item.matches(config.excludeSelector)) return; 195 196 _touchItem = item; 197 const startY = e.touches[0].clientY; 198 _touchStartY = startY; 199 200 // Long-press: 400ms hold to initiate drag 201 _touchTimer = setTimeout(() => { 202 _touchTimer = null; 203 if (!_touchItem) return; 204 // Haptic feedback if available 205 if (navigator.vibrate) navigator.vibrate(30); 206 startDrag(startY, _touchItem); 207 _touchItem.classList.add('touch-dragging'); 208 }, 400); 209 } 210 211 function onTouchMove(e) { 212 // If long-press hasn't fired yet, cancel if finger moved too much 213 if (_touchTimer) { 214 const dy = Math.abs(e.touches[0].clientY - _touchStartY); 215 if (dy > 10) { 216 clearTimeout(_touchTimer); 217 _touchTimer = null; 218 _touchItem = null; 219 } 220 return; 221 } 222 223 // If dragging, prevent scroll and move 224 if (draggedEl) { 225 e.preventDefault(); 226 moveDrag(e.touches[0].clientY); 227 } 228 } 229 230 function onTouchEnd() { 231 if (_touchTimer) { 232 clearTimeout(_touchTimer); 233 _touchTimer = null; 234 _touchItem = null; 235 return; 236 } 237 if (draggedEl) { 238 draggedEl.classList.remove('touch-dragging'); 239 endDrag(); 240 } 241 } 242 243 container.addEventListener('mousedown', onMouseDown); 244 container.addEventListener('touchstart', onTouchStart, { passive: true }); 245 container.addEventListener('touchmove', onTouchMove, { passive: false }); 246 container.addEventListener('touchend', onTouchEnd); 247 container.addEventListener('touchcancel', onTouchEnd); 248 249 const instance = { 250 cleanup: () => { 251 container.removeEventListener('mousedown', onMouseDown); 252 container.removeEventListener('touchstart', onTouchStart); 253 container.removeEventListener('touchmove', onTouchMove); 254 container.removeEventListener('touchend', onTouchEnd); 255 container.removeEventListener('touchcancel', onTouchEnd); 256 }, 257 refresh: () => { items = getItems(); } 258 }; 259 instances.set(key, instance); 260 return instance; 261} 262 263const dragSortModule = { enable }; 264export default dragSortModule; 265window.dragSortModule = dragSortModule;
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.