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.
This commit is contained in:
@@ -1018,6 +1018,9 @@ function setupMobileToggle() {
|
|||||||
if (pos.right) $mobileToggle.css('right', pos.right);
|
if (pos.right) $mobileToggle.css('right', pos.right);
|
||||||
if (pos.bottom) $mobileToggle.css('bottom', pos.bottom);
|
if (pos.bottom) $mobileToggle.css('bottom', pos.bottom);
|
||||||
if (pos.left) $mobileToggle.css('left', pos.left);
|
if (pos.left) $mobileToggle.css('left', pos.left);
|
||||||
|
|
||||||
|
// Constrain to viewport after position is applied
|
||||||
|
requestAnimationFrame(() => constrainFabToViewport());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Touch/drag state
|
// Touch/drag state
|
||||||
@@ -1032,8 +1035,6 @@ function setupMobileToggle() {
|
|||||||
|
|
||||||
// Touch start - begin tracking
|
// Touch start - begin tracking
|
||||||
$mobileToggle.on('touchstart', function(e) {
|
$mobileToggle.on('touchstart', function(e) {
|
||||||
console.log('[RPG Mobile] >>> TOUCHSTART EVENT FIRED <<<');
|
|
||||||
|
|
||||||
const touch = e.originalEvent.touches[0];
|
const touch = e.originalEvent.touches[0];
|
||||||
|
|
||||||
touchStartTime = Date.now();
|
touchStartTime = Date.now();
|
||||||
@@ -1045,14 +1046,6 @@ function setupMobileToggle() {
|
|||||||
buttonStartY = offset.top;
|
buttonStartY = offset.top;
|
||||||
|
|
||||||
isDragging = false;
|
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
|
// Touch move - check if should start dragging
|
||||||
@@ -1063,17 +1056,10 @@ function setupMobileToggle() {
|
|||||||
const timeSinceStart = Date.now() - touchStartTime;
|
const timeSinceStart = Date.now() - touchStartTime;
|
||||||
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
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
|
// Start dragging if held long enough OR moved far enough
|
||||||
if (!isDragging && (timeSinceStart > LONG_PRESS_DURATION || distance > MOVE_THRESHOLD)) {
|
if (!isDragging && (timeSinceStart > LONG_PRESS_DURATION || distance > MOVE_THRESHOLD)) {
|
||||||
isDragging = true;
|
isDragging = true;
|
||||||
$mobileToggle.css('transition', 'none'); // Disable transitions while dragging
|
$mobileToggle.css('transition', 'none'); // Disable transitions while dragging
|
||||||
console.log('[RPG Mobile] Started dragging:', { timeSinceStart, distance });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isDragging) {
|
if (isDragging) {
|
||||||
@@ -1110,8 +1096,6 @@ function setupMobileToggle() {
|
|||||||
let mouseDown = false;
|
let mouseDown = false;
|
||||||
|
|
||||||
$mobileToggle.on('mousedown', function(e) {
|
$mobileToggle.on('mousedown', function(e) {
|
||||||
console.log('[RPG Mobile] >>> MOUSEDOWN EVENT FIRED <<<');
|
|
||||||
|
|
||||||
// Prevent default to avoid text selection
|
// Prevent default to avoid text selection
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
@@ -1125,14 +1109,6 @@ function setupMobileToggle() {
|
|||||||
|
|
||||||
isDragging = false;
|
isDragging = false;
|
||||||
mouseDown = true;
|
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
|
// Mouse move - only track if mouse is down
|
||||||
@@ -1144,18 +1120,10 @@ function setupMobileToggle() {
|
|||||||
const timeSinceStart = Date.now() - touchStartTime;
|
const timeSinceStart = Date.now() - touchStartTime;
|
||||||
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
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
|
// Start dragging if held long enough OR moved far enough
|
||||||
if (!isDragging && (timeSinceStart > LONG_PRESS_DURATION || distance > MOVE_THRESHOLD)) {
|
if (!isDragging && (timeSinceStart > LONG_PRESS_DURATION || distance > MOVE_THRESHOLD)) {
|
||||||
isDragging = true;
|
isDragging = true;
|
||||||
$mobileToggle.css('transition', 'none');
|
$mobileToggle.css('transition', 'none');
|
||||||
console.log('[RPG Mobile] Started mouse dragging:', { timeSinceStart, distance });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isDragging) {
|
if (isDragging) {
|
||||||
@@ -1192,8 +1160,6 @@ function setupMobileToggle() {
|
|||||||
$(document).on('mouseup', function(e) {
|
$(document).on('mouseup', function(e) {
|
||||||
if (!mouseDown) return;
|
if (!mouseDown) return;
|
||||||
|
|
||||||
console.log('[RPG Mobile] >>> MOUSEUP EVENT FIRED <<<', { isDragging });
|
|
||||||
|
|
||||||
mouseDown = false;
|
mouseDown = false;
|
||||||
|
|
||||||
if (isDragging) {
|
if (isDragging) {
|
||||||
@@ -1209,6 +1175,9 @@ function setupMobileToggle() {
|
|||||||
|
|
||||||
console.log('[RPG Mobile] Saved new FAB position (mouse):', newPosition);
|
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
|
// Re-enable transitions
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
$mobileToggle.css('transition', '');
|
$mobileToggle.css('transition', '');
|
||||||
@@ -1231,13 +1200,9 @@ function setupMobileToggle() {
|
|||||||
|
|
||||||
// Touch end - save position or toggle panel
|
// Touch end - save position or toggle panel
|
||||||
$mobileToggle.on('touchend', function(e) {
|
$mobileToggle.on('touchend', function(e) {
|
||||||
console.log('[RPG Mobile] >>> TOUCHEND EVENT FIRED <<<', { isDragging });
|
|
||||||
|
|
||||||
// TEMPORARILY COMMENTED FOR DIAGNOSIS - might be blocking click fallback
|
// TEMPORARILY COMMENTED FOR DIAGNOSIS - might be blocking click fallback
|
||||||
// e.preventDefault();
|
// e.preventDefault();
|
||||||
|
|
||||||
console.log('[RPG Mobile] Touch end details:', { isDragging });
|
|
||||||
|
|
||||||
if (isDragging) {
|
if (isDragging) {
|
||||||
// Was dragging - save new position
|
// Was dragging - save new position
|
||||||
const offset = $mobileToggle.offset();
|
const offset = $mobileToggle.offset();
|
||||||
@@ -1251,6 +1216,9 @@ function setupMobileToggle() {
|
|||||||
|
|
||||||
console.log('[RPG Mobile] Saved new FAB position:', newPosition);
|
console.log('[RPG Mobile] Saved new FAB position:', newPosition);
|
||||||
|
|
||||||
|
// Constrain to viewport bounds (now that position is saved)
|
||||||
|
setTimeout(() => constrainFabToViewport(), 10);
|
||||||
|
|
||||||
// Re-enable transitions
|
// Re-enable transitions
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
$mobileToggle.css('transition', '');
|
$mobileToggle.css('transition', '');
|
||||||
@@ -1397,6 +1365,9 @@ function setupMobileToggle() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
wasMobile = isMobile;
|
wasMobile = isMobile;
|
||||||
|
|
||||||
|
// Constrain FAB to viewport after resize (only if user has positioned it)
|
||||||
|
constrainFabToViewport();
|
||||||
}, 150); // Debounce only for mobile→desktop
|
}, 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.
|
* Sets up mobile tab navigation for organizing content.
|
||||||
* Only runs on mobile viewports (<=1000px).
|
* Only runs on mobile viewports (<=1000px).
|
||||||
|
|||||||
Reference in New Issue
Block a user