From f4f0ab1484a70fe767605f313e05109293496f0b Mon Sep 17 00:00:00 2001 From: Lucas 'Paperboy' Rose-Winters Date: Thu, 16 Oct 2025 10:52:09 +1100 Subject: [PATCH] feat: add smart viewport constraint for mobile FAB button - Add constrainFabToViewport() function with top-bar awareness - Only constrains when mobileFabPosition exists (user has dragged button) - Respects SillyTavern's top bar height via CSS variable - Prevents button from being hidden behind UI elements - Applies constraint after drag operations and window resize - Remove verbose debug logging from drag/touch event handlers This implements a state-driven approach where the constraint only activates for user-positioned buttons, allowing CSS defaults to work naturally while protecting custom positions from viewport changes. --- index.js | 124 +++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 83 insertions(+), 41 deletions(-) diff --git a/index.js b/index.js index 5ed265c..b03fe29 100644 --- a/index.js +++ b/index.js @@ -1018,6 +1018,9 @@ function setupMobileToggle() { if (pos.right) $mobileToggle.css('right', pos.right); if (pos.bottom) $mobileToggle.css('bottom', pos.bottom); if (pos.left) $mobileToggle.css('left', pos.left); + + // Constrain to viewport after position is applied + requestAnimationFrame(() => constrainFabToViewport()); } // Touch/drag state @@ -1032,8 +1035,6 @@ function setupMobileToggle() { // Touch start - begin tracking $mobileToggle.on('touchstart', function(e) { - console.log('[RPG Mobile] >>> TOUCHSTART EVENT FIRED <<<'); - const touch = e.originalEvent.touches[0]; touchStartTime = Date.now(); @@ -1045,14 +1046,6 @@ function setupMobileToggle() { buttonStartY = offset.top; isDragging = false; - - console.log('[RPG Mobile] Touch start:', { - time: touchStartTime, - touchX: touchStartX, - touchY: touchStartY, - buttonX: buttonStartX, - buttonY: buttonStartY - }); }); // Touch move - check if should start dragging @@ -1063,17 +1056,10 @@ function setupMobileToggle() { const timeSinceStart = Date.now() - touchStartTime; const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); - console.log('[RPG Mobile] >>> TOUCHMOVE EVENT FIRED <<<', { - distance: distance.toFixed(2), - timeSinceStart, - isDragging - }); - // Start dragging if held long enough OR moved far enough if (!isDragging && (timeSinceStart > LONG_PRESS_DURATION || distance > MOVE_THRESHOLD)) { isDragging = true; $mobileToggle.css('transition', 'none'); // Disable transitions while dragging - console.log('[RPG Mobile] Started dragging:', { timeSinceStart, distance }); } if (isDragging) { @@ -1110,8 +1096,6 @@ function setupMobileToggle() { let mouseDown = false; $mobileToggle.on('mousedown', function(e) { - console.log('[RPG Mobile] >>> MOUSEDOWN EVENT FIRED <<<'); - // Prevent default to avoid text selection e.preventDefault(); @@ -1125,14 +1109,6 @@ function setupMobileToggle() { isDragging = false; mouseDown = true; - - console.log('[RPG Mobile] Mouse down:', { - time: touchStartTime, - mouseX: touchStartX, - mouseY: touchStartY, - buttonX: buttonStartX, - buttonY: buttonStartY - }); }); // Mouse move - only track if mouse is down @@ -1144,18 +1120,10 @@ function setupMobileToggle() { const timeSinceStart = Date.now() - touchStartTime; const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); - console.log('[RPG Mobile] >>> MOUSEMOVE EVENT FIRED <<<', { - distance: distance.toFixed(2), - timeSinceStart, - isDragging, - mouseDown - }); - // Start dragging if held long enough OR moved far enough if (!isDragging && (timeSinceStart > LONG_PRESS_DURATION || distance > MOVE_THRESHOLD)) { isDragging = true; $mobileToggle.css('transition', 'none'); - console.log('[RPG Mobile] Started mouse dragging:', { timeSinceStart, distance }); } if (isDragging) { @@ -1192,8 +1160,6 @@ function setupMobileToggle() { $(document).on('mouseup', function(e) { if (!mouseDown) return; - console.log('[RPG Mobile] >>> MOUSEUP EVENT FIRED <<<', { isDragging }); - mouseDown = false; if (isDragging) { @@ -1209,6 +1175,9 @@ function setupMobileToggle() { console.log('[RPG Mobile] Saved new FAB position (mouse):', newPosition); + // Constrain to viewport bounds (now that position is saved) + setTimeout(() => constrainFabToViewport(), 10); + // Re-enable transitions setTimeout(() => { $mobileToggle.css('transition', ''); @@ -1231,13 +1200,9 @@ function setupMobileToggle() { // Touch end - save position or toggle panel $mobileToggle.on('touchend', function(e) { - console.log('[RPG Mobile] >>> TOUCHEND EVENT FIRED <<<', { isDragging }); - // TEMPORARILY COMMENTED FOR DIAGNOSIS - might be blocking click fallback // e.preventDefault(); - console.log('[RPG Mobile] Touch end details:', { isDragging }); - if (isDragging) { // Was dragging - save new position const offset = $mobileToggle.offset(); @@ -1251,6 +1216,9 @@ function setupMobileToggle() { console.log('[RPG Mobile] Saved new FAB position:', newPosition); + // Constrain to viewport bounds (now that position is saved) + setTimeout(() => constrainFabToViewport(), 10); + // Re-enable transitions setTimeout(() => { $mobileToggle.css('transition', ''); @@ -1397,6 +1365,9 @@ function setupMobileToggle() { } wasMobile = isMobile; + + // Constrain FAB to viewport after resize (only if user has positioned it) + constrainFabToViewport(); }, 150); // Debounce only for mobile→desktop }); @@ -1423,6 +1394,77 @@ function setupMobileToggle() { } } +/** + * Constrains the mobile FAB button to viewport bounds with top-bar awareness. + * Only runs when button is in user-controlled state (mobileFabPosition exists). + * Ensures button never goes behind the top bar or outside viewport edges. + */ +function constrainFabToViewport() { + // Only constrain if user has set a custom position + if (!extensionSettings.mobileFabPosition) { + console.log('[RPG Mobile] Skipping viewport constraint - using CSS defaults'); + return; + } + + const $mobileToggle = $('#rpg-mobile-toggle'); + if ($mobileToggle.length === 0) return; + + // Skip if button is not visible + if (!$mobileToggle.is(':visible')) { + console.log('[RPG Mobile] Skipping viewport constraint - button not visible'); + return; + } + + // Get current position + const offset = $mobileToggle.offset(); + if (!offset) return; + + let currentX = offset.left; + let currentY = offset.top; + + const buttonWidth = $mobileToggle.outerWidth(); + const buttonHeight = $mobileToggle.outerHeight(); + + // Get top bar height from CSS variable (fallback to 50px if not set) + const topBarHeight = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--topBarBlockSize')) || 50; + + // Calculate viewport bounds with padding + // Use top bar height + extra padding for top bound + const minX = 10; + const maxX = window.innerWidth - buttonWidth - 10; + const minY = topBarHeight + 60; // Top bar + extra space for visibility + const maxY = window.innerHeight - buttonHeight - 10; + + // Constrain to bounds + let newX = Math.max(minX, Math.min(maxX, currentX)); + let newY = Math.max(minY, Math.min(maxY, currentY)); + + // Only update if position changed + if (newX !== currentX || newY !== currentY) { + console.log('[RPG Mobile] Constraining FAB to viewport:', { + old: { x: currentX, y: currentY }, + new: { x: newX, y: newY }, + viewport: { width: window.innerWidth, height: window.innerHeight }, + topBarHeight + }); + + // Apply new position + $mobileToggle.css({ + left: newX + 'px', + top: newY + 'px', + right: 'auto', + bottom: 'auto' + }); + + // Save corrected position + extensionSettings.mobileFabPosition = { + left: newX + 'px', + top: newY + 'px' + }; + saveSettings(); + } +} + /** * Sets up mobile tab navigation for organizing content. * Only runs on mobile viewports (<=1000px).