PageSourceSearch

https://jayreddinjr-jr-odysseus-hf.hf.space/static/js/dragSort.js

js jayreddinjr-jr-odysseus-hf.hf.space collected 2026-10-04 16:59:02 UTC 7,893 bytes, 265 lines download raw bytes

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.