1// ============================================ 2// Section Management â collapse/expand + drag reorder 3// ============================================ 4 5/** 6 * Initialize section collapse/expand with chevron buttons. 7 * @param {Object} Storage - Storage module 8 */ 9export function initSectionCollapse(Storage) { 10 const _chevronHtml = '<button type="button" class="section-collapse-btn" title="Collapse section"><svg class="section-collapse-chevron" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg></button>'; 11 const savedState = Storage.getJSON('section-collapsed') || {}; 12 13 document.querySelectorAll('.section .section-header-flex').forEach(header => { 14 const section = header.closest('.section'); 15 if (!section || !section.id) return; 16 17 // Skip email section â it doesn't collapse (title opens popup instead) 18 if (section.id === 'email-section') return; 19 20 // Add chevron (always visible â rotates when collapsed) 21 header.insertAdjacentHTML('beforeend', _chevronHtml); 22 23 // Restore saved state 24 if (savedState[section.id]) { 25 section.classList.add('collapsed'); 26 } 27 28 function toggleCollapse() { 29 const wasCollapsed = section.classList.contains('collapsed'); 30 const willCollapse = !wasCollapsed; 31 const state = Storage.getJSON('section-collapsed') || {}; 32 state[section.id] = willCollapse; 33 Storage.setJSON('section-collapsed', state); 34 35 // Always clear any in-flight animation classes from a previous toggle 36 // so back-to-back clicks restart cleanly. Bump a generation token so 37 // any callback still pending from a superseded toggle becomes a no-op. 38 section.classList.remove('section-just-expanded', 'section-just-collapsing'); 39 const gen = (section._collapseGen = (section._collapseGen || 0) + 1); 40 41 if (willCollapse) { 42 // Domino-out: play the fade/slide-down on the row children BEFORE 43 // actually adding .collapsed (which hides them via display:none), 44 // then lock in collapse once the cascade finishes. 45 // 46 // We wait on the REAL animations (getAnimations) rather than a fixed 47 // timeout. Different sections animate different rows â .list-item in 48 // most, .models-row in #models-section â so any hard-coded duration 49 // either stalls with a dead pause (when the selector matches nothing, 50 // as it did for #models-section) or guesses the wrong length. Force a 51 // reflow first so the keyframes restart from the top. 52 // eslint-disable-next-line no-unused-expressions 53 section.offsetHeight; 54 section.classList.add('section-just-collapsing'); 55 56 const lockCollapsed = () => { 57 if (section._collapseGen !== gen) return; // superseded by a newer toggle 58 section.classList.remove('section-just-collapsing'); 59 section.classList.add('collapsed'); 60 }; 61 // Only the domino-out keyframes gate the collapse â ignore unrelated 62 // (and possibly infinite, e.g. spinners) animations in the subtree. 63 const dominoOut = section.getAnimations({ subtree: true }) 64 .filter(a => a.animationName === 'section-domino-out'); 65 if (dominoOut.length === 0) { 66 lockCollapsed(); // nothing to animate â collapse now, no dead pause 67 } else { 68 Promise.allSettled(dominoOut.map(a => a.finished)).then(lockCollapsed); 69 // Safety net: if an animation never settles (e.g. element removed), 70 // still lock in the collapse so the section can't get stuck open. 71 setTimeout(lockCollapsed, 600); 72 } 73 } else { 74 // Expand path â remove .collapsed and replay the inbound domino. 75 section.classList.remove('collapsed'); 76 // eslint-disable-next-line no-unused-expressions 77 section.offsetHeight; 78 section.classList.add('section-just-expanded'); 79 setTimeout(() => { 80 if (section._collapseGen !== gen) return; // superseded by a newer toggle 81 section.classList.remove('section-just-expanded'); 82 }, 700); 83 } 84 } 85 86 // Click title to collapse/expand 87 const title = header.querySelector('h4') || header.querySelector('.section-title'); 88 if (title) { 89 title.style.cursor = 'pointer'; 90 title.addEventListener('click', (e) => { 91 e.stopPropagation(); 92 toggleCollapse(); 93 }); 94 } 95 96 // Click chevron button 97 const chevronBtn = header.querySelector('.section-collapse-btn'); 98 if (chevronBtn) { 99 chevronBtn.addEventListener('click', (e) => { 100 e.stopPropagation(); 101 toggleCollapse(); 102 }); 103 } 104
105 // Click anywhere on collapsed section to expand 106 section.addEventListener('click', (e) => { 107 if (!section.classList.contains('collapsed')) return; 108 if (e.target.closest('button, select, .dropdown')) return; 109 e.stopPropagation(); 110 toggleCollapse(); 111 }); 112 }); 113} 114 115/** 116 * Initialize section drag reorder (mouse-based, desktop only). 117 * @param {Object} Storage - Storage module 118 * @param {Function} loadUIVis - Function that returns UI visibility state 119 */ 120export function initSectionDrag(Storage, loadUIVis) { 121 const sidebar = document.getElementById('sidebar'); 122 const sidebarInner = sidebar ? sidebar.querySelector('.sidebar-inner') : null; 123 if (!sidebarInner) return; 124 125 // Disable draggable on mobile to prevent scroll interference 126 if ('ontouchstart' in window) { 127 document.querySelectorAll('.section[draggable]').forEach(s => { 128 s.setAttribute('draggable', 'false'); 129 }); 130 } 131 132 let draggedSection = null; 133 let placeholder = null; 134 let offsetY = 0; 135 136 function getSections() { 137 return Array.from(sidebar.querySelectorAll('.section[draggable="true"]')); 138 } 139 140 function onMouseDown(e) { 141 if (!e.target.classList.contains('drag-handle')) return; 142 143 // Check if drag reorder is enabled 144 const uiState = loadUIVis(); 145 if (uiState['section-drag-reorder'] === false) return; 146 147 const section = e.target.closest('.section[draggable="true"]'); 148 if (!section) return; 149 150 e.preventDefault(); 151 152 const rect = section.getBoundingClientRect(); 153 offsetY = e.clientY - rect.top; 154 draggedSection = section; 155 156 // Create placeholder 157 placeholder = document.createElement('div'); 158 placeholder.className = 'section-placeholder'; 159 placeholder.style.cssText = ` 160 height: ${rect.height}px; 161 margin: 4px 0; 162 border: 2px dashed rgba(0, 170, 255, 0.5); 163 border-radius: 8px; 164 background: rgba(0, 170, 255, 0.1); 165 `; 166 section.parentNode.insertBefore(placeholder, section); 167 168 // Float the section 169 Object.assign(section.style, { 170 position: 'fixed', 171 width: rect.width + 'px', 172 left: rect.left + 'px', 173 top: rect.top + 'px', 174 zIndex: '9999', 175 opacity: '0.95', 176 boxShadow: '0 4px 20px rgba(0,0,0,0.4)', 177 pointerEvents: 'none', 178 transition: 'none' 179 }); 180 181 document.addEventListener('mousemove', onMouseMove); 182 document.addEventListener('mouseup', onMouseUp); 183 } 184 185 function onMouseMove(e) { 186 if (!draggedSection) return; 187 188 // Only move vertically - horizontal stays locked 189 draggedSection.style.top = (e.clientY - offsetY) + 'px'; 190 191 const sections = getSections().filter(s => s !== draggedSection); 192 const dragRect = draggedSection.getBoundingClientRect(); 193 const dragTop = dragRect.top; 194 195 let insertBefore = null; 196 197 // Find which section we should go before 198 for (let i = 0; i < sections.length; i++) { 199 const section = sections[i]; 200 const rect = section.getBoundingClientRect(); 201 202 // If our top edge is above this section's bottom, we go before it 203 if (dragTop < rect.bottom - 10) { 204 insertBefore = section; 205 break; 206 } 207 } 208 209 // Move placeholder 210 if (insertBefore) { 211 if (placeholder.nextElementSibling !== insertBefore) { 212 sidebarInner.insertBefore(placeholder, insertBefore); 213 } 214 } else if (sections.length > 0) { 215 const lastSection = sections[sections.length - 1]; 216 if (placeholder !== lastSection.nextElementSibling) { 217 sidebarInner.insertBefore(placeholder, lastSection.nextElementSibling); 218 } 219 } 220 } 221 222 function onMouseUp() { 223 if (!draggedSection) return; 224 225 document.removeEventListener('mousemove', onMouseMove); 226 document.removeEventListener('mouseup', onMouseUp); 227 228 // Snap to placeholder - fast! 229 const phRect = placeholder.getBoundingClientRect(); 230 draggedSection.style.transition = 'top 0.08s ease-out'; 231 draggedSection.style.top = phRect.top + 'px'; 232 233 setTimeout(() => { 234 placeholder.parentNode.insertBefore(draggedSection, placeholder); 235 placeholder.remove(); 236 draggedSection.style.cssText = ''; 237 238 // Save order 239 const ids = getSections().map(s =>
239 s.id).filter(Boolean); 240 Storage.setJSON(Storage.KEYS.SECTION_ORDER, ids); 241 242 draggedSection = null; 243 placeholder = null; 244 }, 80); 245 } 246 247 sidebar.addEventListener('mousedown', onMouseDown); 248 249 // Restore saved order on load 250 try { 251 const saved = Storage.get(Storage.KEYS.SECTION_ORDER); 252 if (saved) { 253 const order = JSON.parse(saved); 254 order.forEach(id => { 255 const section = document.getElementById(id); 256 if (section) sidebarInner.appendChild(section); 257 }); 258 } 259 } catch (e) {} 260}
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.