PageSourceSearch

https://odysseus-lxcq.onrender.com/static/js/section-management.js

js odysseus-lxcq.onrender.com collected 2026-10-03 11:44:30 UTC 9,488 bytes, 260 lines download raw bytes

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.