1/** 2 * Avatar Draggable Module - Lazy-loaded only when avatarDraggable setting is enabled 3 * Allows mobile users to drag the avatar button to reposition it 4 */ 5 6const POSITION_STORAGE_KEY = 'bubblePosition'; 7const DRAG_THRESHOLD = 5; // pixels before considered a drag 8const POSITION_EXPIRATION_MS = 60 * 60 * 1000; // 1 hour 9 10/** 11 * Initialize draggable functionality on the avatar 12 * @param {Object} component - The UserAgentComponent instance 13 */ 14export function initDraggable(component) { 15 const $handle = component.container.find('.user-agent-avatar-img-wrapper'); 16 if (!$handle.length) return; 17 18 // Only enable dragging on mobile devices (small screen + touch capability) 19 const isMobile = window.innerWidth <= 768 && ('ontouchstart' in window || navigator.maxTouchPoints > 0); 20 if (!isMobile) return; 21 22 // Store handle reference on component for other methods 23 component.$handle = $handle; 24 25 // Wait for container to be visible before setting up position 26 waitForVisible(component, () => { 27 convertToLeftTopPositioning(component); 28 loadPosition(component); 29 }); 30 31 // Set up event handlers 32 setupDragEvents(component); 33} 34 35/** 36 * Wait for container to be visible, then execute callback 37 */ 38function waitForVisible(component, callback) { 39 if (component.container.is(':visible')) { 40 callback(); 41 } else { 42 setTimeout(() => waitForVisible(component, callback), 100); 43 } 44} 45 46/** 47 * Convert from CSS bottom/right to left/top positioning for dragging 48 */ 49function convertToLeftTopPositioning(component) { 50 const rect = component.container[0].getBoundingClientRect(); 51 component.container.css({ 52 left: rect.left + 'px', 53 top: rect.top + 'px', 54 right: 'auto', 55 bottom: 'auto' 56 }); 57} 58 59/** 60 * Set up mouse and touch drag events 61 */ 62function setupDragEvents(component) { 63 let startX, startY, startLeft, startTop, hasMoved; 64 let isTouchingAvatar = false; 65 66 const beginDragTracking = (clientX, clientY) => { 67 startX = clientX; 68 startY = clientY; 69 startLeft = parseInt(component.container.css('left')) || 0; 70 startTop = parseInt(component.container.css('top')) || 0; 71 hasMoved = false; 72 }; 73 74 const onMove = (clientX, clientY, e) => { 75 const deltaX = clientX - startX; 76 const deltaY = clientY - startY; 77 78 // Only start dragging after moving past threshold 79 if (!hasMoved && (Math.abs(deltaX) > DRAG_THRESHOLD || Math.abs(deltaY) > DRAG_THRESHOLD)) { 80 hasMoved = true; 81 startDragging(component); 82 startLeft = parseInt(component.container.css('left')) || 0; 83 } 84 85 if (hasMoved) { 86 e.preventDefault(); 87 88 let newLeft = startLeft + deltaX; 89 let newTop = startTop + deltaY; 90 91 const avatarRect = component.$handle[0].getBoundingClientRect(); 92 const currentCssLeft = parseInt(component.container.css('left')) || 0; 93 const currentCssTop = parseInt(component.container.css('top')) || 0; 94 95 const minLeft = currentCssLeft - avatarRect.left; 96 const maxLeft = currentCssLeft + (window.innerWidth - avatarRect.right); 97 const minTop = currentCssTop - avatarRect.top; 98 const maxTop = currentCssTop + (window.innerHeight - avatarRect.bottom); 99 100 newLeft = Math.max(minLeft, Math.min(maxLeft, newLeft)); 101 newTop = Math.max(minTop, Math.min(maxTop, newTop)); 102 103 component.container.css({ 104 left: newLeft + 'px', 105 top: newTop + 'px' 106 }); 107 } 108 }; 109 110 const onEnd = () => { 111 if (hasMoved) { 112 finishDragging(component); 113 } 114 }; 115 116 // Mouse events 117 const onMouseDown = (e) => { 118 beginDragTracking(e.clientX, e.clientY); 119 120 const onMouseMove = (e) => onMove(e.clientX, e.clientY, e); 121 const onMouseUp = () => { 122 $(document).off('mousemove.drag mouseup.drag'); 123 onEnd(); 124 }; 125 126 $(document).on('mousemove.drag', onMouseMove); 127 $(document).on('mouseup.drag', onMouseUp); 128 }; 129 130 // Touch events 131 const onTouchStart = (e) => { 132 isTouchingAvatar = true; 133 const touch = e.touches[0]; 134 beginDragTracking(touch.clientX, touch.clientY); 135 }; 136 137 const onTouchMove = (e) => { 138 if (!isTouchingAvatar) return; 139 const touch = e.touches[0]; 140 onMove(touch.clientX, touch.clientY, e); 141 }; 142 143 const onTouchEnd = () => { 144 if (!isTouchingAvatar) return; 145 isTouchingAvatar = false;
146 onEnd(); 147 }; 148 149 // Attach to handle and img 150 const $img = component.$handle.find('img'); 151 [component.$handle[0], $img[0]].forEach(el => { 152 if (el) { 153 el.addEventListener('mousedown', onMouseDown, true); 154 el.addEventListener('touchstart', onTouchStart, { passive: true, capture: true }); 155 } 156 }); 157 document.addEventListener('touchmove', onTouchMove, { passive: false }); 158 document.addEventListener('touchend', onTouchEnd); 159} 160 161/** 162 * Called when drag movement starts (past threshold) 163 */ 164function startDragging(component) { 165 component.isDragging = true; 166 component.container.addClass('dragging'); 167 168 const avatarBefore = component.$handle[0].getBoundingClientRect(); 169 component.setMessage(""); 170 171 const avatarAfter = component.$handle[0].getBoundingClientRect(); 172 const shiftX = avatarAfter.left - avatarBefore.left; 173 const shiftY = avatarAfter.top - avatarBefore.top; 174 if (shiftX !== 0) { 175 const currentLeft = parseInt(component.container.css('left')) || 0; 176 component.container.css('left', (currentLeft - shiftX) + 'px'); 177 } 178 if (shiftY !== 0) { 179 const currentTop = parseInt(component.container.css('top')) || 0; 180 component.container.css('top', (currentTop - shiftY) + 'px'); 181 } 182} 183 184/** 185 * Called when drag ends 186 */ 187function finishDragging(component) { 188 component.isDragging = false; 189 component.container.removeClass('dragging'); 190 191 savePosition(component); 192 193 component.justFinishedDragging = true; 194 setTimeout(() => { 195 component.justFinishedDragging = false; 196 }, 100); 197} 198 199/** 200 * Save current position to localStorage with timestamp 201 */ 202function savePosition(component) { 203 const left = parseInt(component.container.css('left')) || 0; 204 const top = parseInt(component.container.css('top')) || 0; 205 const timestamp = Date.now(); 206 localStorage.setItem(POSITION_STORAGE_KEY, JSON.stringify({ left, top, timestamp })); 207} 208 209/** 210 * Load saved position from localStorage if not expired 211 */ 212function loadPosition(component) { 213 const saved = localStorage.getItem(POSITION_STORAGE_KEY); 214 if (!saved) return; 215 216 try { 217 const pos = JSON.parse(saved); 218 219 if (pos.timestamp && (Date.now() - pos.timestamp) > POSITION_EXPIRATION_MS) { 220 localStorage.removeItem(POSITION_STORAGE_KEY); 221 return; 222 } 223 224 component.setMessage(""); 225 226 component.container.css({ 227 left: pos.left + 'px', 228 top: pos.top + 'px' 229 }); 230 231 checkViewportBounds(component); 232 } catch (e) { 233 // Invalid saved data, ignore 234 } 235} 236 237/** 238 * Check if avatar is within viewport, reset to default if not 239 */ 240function checkViewportBounds(component) { 241 const $avatar = component.container.find('.user-agent-avatar-img-wrapper'); 242 if (!$avatar.length) return; 243 244 const avatarRect = $avatar[0].getBoundingClientRect(); 245 const isOffScreen = avatarRect.left < 0 || 246 avatarRect.right > window.innerWidth || 247 avatarRect.top < 0 || 248 avatarRect.bottom > window.innerHeight; 249 250 if (isOffScreen) { 251 resetToDefault(component); 252 } 253} 254 255/** 256 * Reset position to default (bottom-right corner) 257 */ 258function resetToDefault(component) { 259 component.container.addClass('message-left'); 260 component.container.css({ left: 'auto', top: 'auto', right: '0', bottom: '0' }); 261 262 convertToLeftTopPositioning(component); 263 savePosition(component); 264}
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.