PageSourceSearch

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

js jayreddinjr-jr-odysseus-hf.hf.space collected 2026-10-04 16:59:45 UTC 14,882 bytes, 333 lines download raw bytes

1// Shared window-drag helper. Replaces the duplicated mousedown / mousemove
2// / mouseup + snap-to-top fullscreen + left/right edge dock patterns that
3// were copy-pasted across calendar.js, tasks.js, gallery.js, emailLibrary.js,
4// documentLibrary.js, theme.js. Behavior stays identical to the old per-file
5// copies — each callsite provides its own enter/exit-fullscreen callbacks
6// since the CSS class + inline styles differ per modal.
7//
8// API:
9//   makeWindowDraggable(modal, { content, header, ...options })
10//     modal:           the wrapping .modal element (or a standalone pane)
11//     content:         the element being moved (usually .modal-content)
12//     header:          the drag handle (usually .modal-header)
13//     fsClass:         optional class name representing "fullscreen" state
14//     onEnterFullscreen: optional () => void — called when cursor releases
15//                        near the top edge (within SNAP_PX). Caller is
16//                        responsible for adding fsClass + applying inline
17//                        styles that produce the fullscreen layout.
18//     onExitFullscreen:  optional (cx, cy) => void — called mid-drag when
19//                        the cursor leaves the fullscreen "unsnap" band
20//                        (down > UNSNAP_PX OR near either horizontal edge
21//                        in dock-snap range). Caller restores windowed
22//                        inline styles centered around the cursor.
23//     skipSelector:    CSS selector for elements inside `header` whose
24//                        clicks should NOT start a drag (close button,
25//                        form fields, etc). Default: 'button, input, select'
26//     onDragEnd:       optional (state) => void — fires after mouseup
27//                        WHEN no snap was committed. state = { rect } so
28//                        callers can persist the final position.
29//     enableTouch:     bool — also wire touchstart/touchmove/touchend
30//                        with the same drag (no fs/dock on touch). Default
31//                        true on desktop, irrelevant on mobile (mobileSkip).
32//     mobileSkip:      drag is disabled below this viewport width.
33//                        Default 768. Set to 0 to never skip.
34//     enableDock:      bool — enable left + right edge docks.
35//                        Default true.
36//     enableFullscreen: bool — enable top-edge fullscreen snap.
37//                        Default true when onEnterFullscreen is supplied.
38
39import { makeEdgeDockController } from './modalSnap.js';
40import { makeWindowResizable } from './windowResize.js';
41
42const SNAP_PX = 6;        // cursor distance from top edge for fullscreen snap
43const UNSNAP_PX = 24;     // cursor distance from top before fullscreen exits
44const DOCK_EDGE_PX = 60;  // cursor distance from L/R edge to trigger dock
45                          // exit while still in fullscreen state
46
47// CSS-var lookup for the rail+sidebar width — used to decide where the
48// "left edge" effectively is during a fullscreen drag-out (the cursor
49// has to pass the rail to count as "near left").
50function _leftNavWidth() {
51  const rs = getComputedStyle(document.documentElement);
52  const rail = parseInt(rs.getPropertyValue('--icon-rail-w') || '48', 10) || 0;
53  const sb = parseInt(rs.getPropertyValue('--sidebar-w') || '0', 10) || 0;
54  return rail + sb;
55}
56
57export function makeWindowDraggable(modal, options = {}) {
58  const content = options.content;
59  const header = options.header;
60  if (!content || !header) return;
61  const fsClass = options.fsClass || null;
62  const onEnterFullscreen = options.onEnterFullscreen || null;
63  const onExitFullscreen = options.onExitFullscreen || null;
64  const enableFullscreen = options.enableFullscreen !== false && !!onEnterFullscreen;
65  const onDragEnd = options.onDragEnd || null;
66  const onDragStart = options.onDragStart || null;
67  const skipSelector = options.skipSelector || 'button, input, select';
68  const mobileSkip = (typeof options.mobileSkip === 'number') ? options.mobileSkip : 768;
69  const enableTouch = options.enableTouch !== false;
70  const enableDock = options.enableDock !== false && !!modal;
71
72  header.style.cursor = 'move';
73  header.style.userSelect = 'none';
74
75  // Edge/corner resize. Every draggable window also becomes resizable — the
76  // same gesture a native desktop window uses (grab an edge or corner, drag).
77  // Skipped on mobile (windows are full-screen sheets there) and while the
78  // window is fullscreen-snapped or docked. Wired here so all ~12 callsites
79  // get it without per-file changes.
80  if (options.enableResize !== false) {
81    const _dockClasses = ['modal-right-docked', 'modal-left-docked'];
82    makeWindowResizable(content, {
83      modal,
84      mobileSkip,
85      minWidth: options.minWidth,
86      minHeight: options.minHeight,
87      isLocked: () => (fsClass && modal && modal.classList.contains(fsClass))
88        || (modal && _dockClasses.some((c) => modal.classList.contains(c))),
89      storageKey: options.resizeStorageKey
90        || (modal && modal.id ? 'winsize-' + modal.id
91          : (content.id ? 'winsize-' + content.id : null)),
92    });
93  }
94
95  const rightDock = enableDock ? makeEdgeDockController(modal, 'right') : null;
96  // Left dock is enabled by default too. modalSnap collapses the wide sidebar
97  // and anchors the panel beside the icon rail, so it no longer collides with
98  // the navigation. Callers can still pass enableLeftDock:false for a special
99  // modal that should only dock right.
100  const leftDock = (enableDock && options.enableLeftDock !== false) ? makeEdgeDockController(modal, 'left') : null;
101
102  // Per-drag state, reset on mousedown.
103  let dragging = false;
104  let startX = 0, startY = 0;
105  let startLeft = 0, startTop = 0;
106  let snapHint = null;
107  // Whether the pointer actually moved beyond a small threshold this drag.
108  // Used to suppress the synthetic click the browser fires on mouseup —
109  // header click handlers (e.g. "collapse expanded card / back to list")
110  // would otherwise fire after a drag and collapse the modal contents.
111  let movedDuringDrag = false;
112  const MOVE_THRESHOLD = 4;
113
114  const _showSnapHint = (on) => {
115    // Top-edge fullscreen hint. Side hints come from the dock controllers.
116    if (!on) {
117      if (snapHint) { snapHint.remove(); snapHint = null; }
118      return;
119    }
120    if (snapHint) return;
121    snapHint = document.createElement('div');
122    snapHint.className = 'modal-snap-hint';
123    snapHint.style.cssText =
124      'position:fixed;left:0;top:0;right:0;bottom:0;' +
125      'background:color-mix(in srgb, var(--accent-primary, #60a5fa) 12%, transparent);' +
126      'border:2px dashed color-mix(in srgb, var(--accent-primary, #60a5fa) 60%, transparent);' +
127      'z-index:9998;pointer-events:none;';
128    document.body.appendChild(snapHint);
129  };
130
131  const _enterFs = () => {
132    if (!onEnterFullscreen) return;
133    if (fsClass && modal && modal.classList.contains(fsClass)) return;
134    onEnterFullscreen();
135  };
136  const _exitFs = (cx, cy) => {
137    if (!onExitFullscreen) return;
138    if (fsClass && modal && !modal.classList.contains(fsClass)) return;
139    onExitFullscreen(cx, cy);
140    // After exit, re-anchor the drag offsets to the new windowed rect so
141    // the drag continues smoothly from the cursor's position.
142    const r = content.getBoundingClientRect();
143    startX = cx; startY = cy;
144    startLeft = r.left; startTop = r.top;
145  };
146
147  const _isFullscreen = () => fsClass && modal && modal.classList.contains(fsClass);
148
149  const _startDrag = (cx, cy) => {
150    dragging = true;
151    if (modal) modal.classList.add('modal-dragging');
152    // Cancel any in-flight open animation so we don't pin a mid-animation
153    // rect and then jump once the animation settles.
154    try {
155      content.getAnimations()
156        .filter(a => a.playState !== 'finished')
157        .forEach(a => a.cancel());
158    } catch (_) {}
159    const rect = content.getBoundingClientRect();
160    if (onDragStart) {
161      try { onDragStart({ rect, cx, cy }); } catch (_) {}
162    }
163    startX = cx; startY = cy;
164    startLeft = rect.left; startTop = rect.top;
165    // Pin position so the drag follows the cursor instead of fighting a
166    // centering transform / margin. Inline styles win unless CSS uses
167    // !important (the fullscreen rules do, by design).
168    content.style.position = 'fixed';
169    content.style.left = startLeft + 'px';
170    content.style.top = startTop + 'px';
171    content.style.transform = 'none';
172    content.style.margin = '0';
173  };
174
175  const _onMove = (cx, cy) => {
176    if (!dragging) return;
177    // Fullscreen state: unsnap on drag-down or drag toward either horizontal
178    // edge. Update dock hover immediately after exit so a fast release
179    // commits the dock instead of dropping the modal mid-air.
180    if (_isFullscreen()) {
181      // Corner guard: ignore the side edges while the cursor is still in the
182      // top fullscreen band, so dragging across the top corners keeps
183      // fullscreen instead of flipping into a corner dock.
184      const inTopBand = cy <= SNAP_PX;
185      const nearRight = !inTopBand && (window.innerWidth - cx) <= DOCK_EDGE_PX;
186      const nearLeft = !inTopBand && (cx - _leftNavWidth()) <= DOCK_EDGE_PX;
187      // Dragging a fullscreen window to a SIDE edge → keep it fullscreen and
188      // just arm the side-dock hint; releasing there docks it (handled in
189      // _onEnd, which drops the fullscreen class). Previously this exited
190      // fullscreen first, which re-CENTERED the window — so it looked like
191      // it "centered instead of docking". Only a downward drag unsnaps to a
192      // windowed (centered) modal.
193      if (nearRight && rightDock) {
194        if (leftDock) leftDock.release();
195        rightDock.onMove(cx, cy);
196        return;
197      }
198      if (nearLeft && leftDock) {
199        if (rightDock) rightDock.release();
200        leftDock.onMove(cx, cy);
201        return;
202      }
203      if (cy > UNSNAP_PX) {
204        _exitFs(cx, cy);
205        if (rightDock) rightDock.onMove(cx, cy);
206        if (leftDock) leftDock.onMove(cx, cy);
207      } else {
208        if (rightDock) rightDock.release();
209        if (leftDock) leftDock.release();
210      }
211      return;
212    }
213    // Right-docked: pulling away from the right edge un-docks. Same for left.
214    if (rightDock && modal && modal.classList.contains('modal-right-docked')) {
215      if (rightDock.onMove(cx, cy)) {
216        const r = content.getBoundingClientRect();
217        startX = cx; startY = cy;
218        startLeft = r.left; startTop = r.top;
219      }
220      return;
221    }
222    if (leftDock && modal && modal.classList.contains('modal-left-docked')) {
223      if (leftDock.onMove(cx, cy)) {
224        const r = content.getBoundingClientRect();
225        startX = cx; startY = cy;
226        startLeft = r.left; startTop = r.top;
227      }
228      return;
229    }
230    // Windowed: just follow the cursor.
231    if (Math.abs(cx - startX) > MOVE_THRESHOLD || Math.abs(cy - startY) > MOVE_THRESHOLD) {
232      movedDuringDrag = true;
233    }
234    content.style.left = (startLeft + cx - startX) + 'px';
235    content.style.top = (startTop + cy - startY) + 'px';
236    // Corner guard: in the top fullscreen band the side docks stay OFF, so a
237    // top corner only ever snaps to fullscreen — never the corner hybrid.
238    const inTopBand = cy <= SNAP_PX;
239    _showSnapHint(enableFullscreen && inTopBand);
240    if (inTopBand) {
241      if (rightDock) rightDock.release();
242      if (leftDock) leftDock.release();
243    } else {
244      if (rightDock) rightDock.onMove(cx, cy);
245      if (leftDock) leftDock.onMove(cx, cy);
246    }
247  };
248
249  const _onEnd = (cx, cy) => {
250    if (!dragging) return;
251    dragging = false;
252    if (modal) modal.classList.remove('modal-dragging');
253    _showSnapHint(false);
254    // Top edge wins over side edges — fullscreen is the more common gesture.
255    if (enableFullscreen && typeof cy === 'number' && cy <= SNAP_PX) {
256      if (rightDock) rightDock.release();
257      if (leftDock) leftDock.release();
258      _enterFs();
259      return;
260    }
261    if (rightDock && rightDock.hovering()) {
262      if (leftDock) leftDock.release();
263      if (fsClass && modal) modal.classList.remove(fsClass);  // dock takes over from fullscreen
264      rightDock.commit();
265      return;
266    }
267    if (leftDock && leftDock.hovering()) {
268      if (rightDock) rightDock.release();
269      if (fsClass && modal) modal.classList.remove(fsClass);
270      leftDock.commit();
271      return;
272    }
273    if (rightDock) rightDock.release();
274    if (leftDock) leftDock.release();
275    if (onDragEnd) {
276      const r = content.getBoundingClientRect();
277      try { onDragEnd({ rect: r }); } catch (_) {}
278    }
279  };
280
281  header.addEventListener('mousedown', (e) => {
282    if (mobileSkip > 0 && window.innerWidth <= mobileSkip) return;
283    if (skipSelector && e.target.closest(skipSelector)) return;
284    e.preventDefault();
285    movedDuringDrag = false;
286    _startDrag(e.clientX, e.clientY);
287    const onMove = (ev) => _onMove(ev.clientX, ev.clientY);
288    const onUp = (ev) => {
289      _onEnd(ev.clientX, ev.clientY);
290      document.removeEventListener('mousemove', onMove);
291      document.removeEventListener('mouseup', onUp);
292      // If the pointer actually moved, swallow the synthetic click the
293      // browser fires next — otherwise a header click handler (collapse
294      // expanded card / "back to list") runs and undoes the drag intent.
295      if (movedDuringDrag) {
296        const swallow = (clickEv) => {
297          clickEv.stopPropagation();
298          clickEv.preventDefault();
299        };
300        header.addEventListener('click', swallow, { capture: true, once: true });
301        // Safety: if no click fires (some browsers), drop the listener.
302        setTimeout(() => header.removeEventListener('click', swallow, { capture: true }), 50);
303      }
304    };
305    document.addEventListener('mousemove', onMove);
306    document.addEventListener('mouseup', onUp);
307  });
308
309  if (enableTouch) {
310    header.addEventListener('touchstart', (e) => {
311      if (mobileSkip > 0 && window.innerWidth <= mobileSkip) return;
312      if (skipSelector && e.target.closest(skipSelector)) return;
313      const t = e.touches[0];
314      if (!t) return;
315      movedDuringDrag = false;
316      _startDrag(t.clientX, t.clientY);
317      const onMove = (ev) => {
318        const tt = ev.touches[0];
319        if (tt) _onMove(tt.clientX, tt.clientY);
320      };
321      const onEnd = (ev) => {
322        const tt = (ev.changedTouches && ev.changedTouches[0]) || null;
323        _onEnd(tt ? tt.clientX : null, tt ? tt.clientY : null);
324        document.removeEventListener('touchmove', onMove);
325        document.removeEventListener('touchend', onEnd);
326        document.removeEventListener('touchcancel', onEnd);
327      };
328      document.addEventListener('touchmove', onMove, { passive: true });
329      document.addEventListener('touchend', onEnd);
330      document.addEventListener('touchcancel', onEnd);
331    }, { passive: true });
332  }
333}

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.