1/** 2 * WindowDraggable - Modular, DRY helper for draggable windows in SOL.VIN 3 * 4 * Allows windows with title bars (.window-header) to be dragged around freely 5 * (including outside viewport boundaries). Scrolling during drag is dynamically 6 * accounted for so the element stays locked to the pointer. Upon mouse/touch release, 7 * the window smoothly snaps back to its original layout origin. 8 */ 9(function(window) { 10 'use strict'; 11 12 var WindowDraggable = { 13 activeWindow: null, 14 startPageX: 0, 15 startPageY: 0, 16 lastClientX: 0, 17 lastClientY: 0, 18 currentTx: 0, 19 currentTy: 0, 20 initialTx: 0, 21 initialTy: 0, 22 dragged: false, 23 24 /** 25 * Initializes draggable behavior for all windows with header elements in scope. 26 */ 27 init: function(scope) { 28 scope = scope || document; 29 var selector = '.terminal-window, .gui-box, [data-window-drag]'; 30 var windows = scope.querySelectorAll(selector); 31 32 for (var i = 0; i < windows.length; i++) { 33 var win = windows[i]; 34 var header = win.querySelector(':scope > .window-header, :scope > .menu-window-header') || win.querySelector('.window-header'); 35 if (header) { 36 this.attach(win, header); 37 } 38 } 39 }, 40 41 /** 42 * Attaches dragging event handlers to a specific window and header. 43 */ 44 attach: function(win, header) { 45 if (!win || !header) return; 46 if (win.dataset.draggableInitialized === 'true') return; 47 win.dataset.draggableInitialized = 'true'; 48 49 header.style.cursor = 'grab'; 50 header.style.userSelect = 'none'; 51 header.style.webkitUserSelect = 'none'; 52 53 var self = this; 54 55 function updatePosition() { 56 if (!self.activeWindow || self.activeWindow !== win) return; 57 58 var scrollX = window.scrollX || window.pageXOffset || 0; 59 var scrollY = window.scrollY || window.pageYOffset || 0; 60 61 var currentPageX = self.lastClientX + scrollX; 62 var currentPageY = self.lastClientY + scrollY; 63 64 var dx = currentPageX - self.startPageX; 65 var dy = currentPageY - self.startPageY; 66 67 var proposedTx = self.initialTx + dx; 68 var proposedTy = self.initialTy + dy; 69 70 self.currentTx = proposedTx; 71 self.currentTy = proposedTy; 72 self.dragged = (Math.abs(dx) > 1 || Math.abs(dy) > 1); 73 74 win.style.transform = 'translate(' + proposedTx.toFixed(2) + 'px, ' + proposedTy.toFixed(2) + 'px)'; 75 } 76 77 function onPointerDown(e) { 78 // Ignore clicks on control buttons or interactive links/inputs 79 var target = e.target; 80 if (target.closest('.window-btn, .window-controls, button, a, input, textarea, select, .carousel-overlay-control')) { 81 return; 82 } 83 84 var pointer = e.touches ? e.touches[0] : e; 85 if (e.button !== undefined && e.button !== 0) return; // Only left click 86 87 self.activeWindow = win; 88 self.dragged = false; 89 90 var scrollX = window.scrollX || window.pageXOffset || 0; 91 var scrollY = window.scrollY || window.pageYOffset || 0; 92 93 self.lastClientX = pointer.clientX; 94 self.lastClientY = pointer.clientY; 95 self.startPageX = (pointer.pageX !== undefined) ? pointer.pageX : (pointer.clientX + scrollX); 96 self.startPageY = (pointer.pageY !== undefined) ? pointer.pageY : (pointer.clientY + scrollY); 97 98 // Parse current transform translation if already translated 99 var matrix = window.getComputedStyle(win).transform; 100 self.initialTx = 0; 101 self.initialTy = 0; 102 if (matrix && matrix !== 'none') { 103 var values = matrix.split('(')[1].split(')')[0].split(','); 104 self.initialTx = parseFloat(values[4]) || 0; 105 self.initialTy = parseFloat(values[5]) || 0; 106 } 107 108 self.currentTx = self.initialTx; 109 self.currentTy = self.initialTy; 110 111 win.style.transition = 'none'; 112 win.style.zIndex = '1000'; 113 win.classList.add('is-window-dragging'); 114 header.style.cursor = 'grabbing'; 115 116 document.addEventListener('mousemove', onPointerMove, { passive: false }); 117 document.addEventListener('mouseup', onPointerUp); 118 document.addEventListener('touchmove', onPointerMove, { passive: false }); 119 document.addEventListener('touchend', onPointerUp); 120 document.addEventListener('touchcancel', onPointerUp); 121 window.addEventListener('scroll', onScroll, { passive: true }); 122 123 e.preventDefault(); 124 } 125 126 function onPointerMove(e) { 127 if (!self.activeWindow || self.activeWindow !== win) return; 128 var pointer = e.touches ? e.touches[0] : e; 129 130 self.lastClientX = pointer.clientX; 131 self.lastClientY = pointer.clientY; 132 133 updatePosition(); 134 135 if (e.cancelable) e.preventDefault(); 136 } 137 138 function onScroll() { 139 if (!self.activeWindow || self.activeWindow !== win) return; 140 updatePosition(); 141 } 142 143 function onPointerUp() { 144 if (!self.activeWindow || self.activeWindow !== win) return; 145 146 document.removeEventListener('mousemove', onPointerMove); 147 document.removeEventListener('mouseup', onPointerUp); 148 document.removeEventListener('touchmove', onPointerMove); 149 document.removeEventListener('touchend', onPointerUp); 150 document.removeEventListener('touchcancel', onPointerUp); 151 window.removeEventListener('scroll', onScroll); 152 153 header.style.cursor = 'grab'; 154 win.classList.remove('is-window-dragging'); 155 156 if (self.dragged) { 157 // Snap back smoothly to original layout position (translate 0,0) 158 win.style.transition = 'transform 0.35s cubic-bezier(0.25, 0.8, 0.25, 1)'; 159 win.style.transform = 'translate(0px, 0px)'; 160 161 var onSnapEnd = function() { 162 win.removeEventListener('transitionend', onSnapEnd); 163 win.style.transform = ''; 164 win.style.transition = ''; 165 win.style.zIndex = ''; 166 }; 167 win.addEventListener('transitionend', onSnapEnd); 168
169 // Fallback timeout in case transitionend does not fire 170 setTimeout(onSnapEnd, 400); 171 } else { 172 win.style.transform = ''; 173 win.style.transition = ''; 174 win.style.zIndex = ''; 175 } 176 177 self.activeWindow = null; 178 self.dragged = false; 179 } 180 181 header.addEventListener('mousedown', onPointerDown); 182 header.addEventListener('touchstart', onPointerDown, { passive: false }); 183 } 184 }; 185 186 window.WindowDraggable = WindowDraggable; 187})(window);
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.