feat: enhance ChatWidget with improved drag-and-resize functionality
- Introduced drag threshold and movement detection to prevent unintended clicks during dragging. - Added reactive window dimensions to ensure proper positioning and resizing of the chat widget. - Refactored mouse event handlers for better separation of concerns and improved user experience. - Enhanced position adjustment logic to maintain visibility within the viewport during dragging and resizing.
This commit is contained in:
@@ -44,6 +44,8 @@
|
||||
let isDragging = $state(false);
|
||||
let dragStart = $state({ x: 0, y: 0 });
|
||||
let isAnimating = $state(false);
|
||||
let dragThreshold = $state(5); // Distância mínima em pixels para considerar arrastar
|
||||
let hasMoved = $state(false); // Flag para verificar se houve movimento durante o arrastar
|
||||
|
||||
// Tamanho da janela (redimensionável)
|
||||
const MIN_WIDTH = 300;
|
||||
@@ -76,6 +78,38 @@
|
||||
let previousSize = $state({ width: DEFAULT_WIDTH, height: DEFAULT_HEIGHT });
|
||||
let previousPosition = $state({ x: 0, y: 0 });
|
||||
|
||||
// Dimensões da janela (reativo)
|
||||
let windowDimensions = $state({ width: 0, height: 0 });
|
||||
|
||||
// Atualizar dimensões da janela
|
||||
function updateWindowDimensions() {
|
||||
if (typeof window !== 'undefined') {
|
||||
windowDimensions = {
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Inicializar e atualizar dimensões da janela
|
||||
$effect(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
updateWindowDimensions();
|
||||
|
||||
const handleResize = () => {
|
||||
updateWindowDimensions();
|
||||
// Ajustar posição quando a janela redimensionar
|
||||
ajustarPosicao();
|
||||
};
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
};
|
||||
});
|
||||
|
||||
// Salvar tamanho no localStorage
|
||||
function saveSize() {
|
||||
if (typeof window !== 'undefined') {
|
||||
@@ -119,15 +153,19 @@
|
||||
newWidth = Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, resizeStart.width + deltaX));
|
||||
}
|
||||
if (resizeDirection.includes('w')) {
|
||||
newWidth = Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, resizeStart.width - deltaX));
|
||||
newX = position.x + (resizeStart.width - newWidth);
|
||||
const calculatedWidth = Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, resizeStart.width - deltaX));
|
||||
const widthDelta = resizeStart.width - calculatedWidth;
|
||||
newWidth = calculatedWidth;
|
||||
newX = position.x + widthDelta;
|
||||
}
|
||||
if (resizeDirection.includes('s')) {
|
||||
newHeight = Math.max(MIN_HEIGHT, Math.min(MAX_HEIGHT, resizeStart.height + deltaY));
|
||||
}
|
||||
if (resizeDirection.includes('n')) {
|
||||
newHeight = Math.max(MIN_HEIGHT, Math.min(MAX_HEIGHT, resizeStart.height - deltaY));
|
||||
newY = position.y + (resizeStart.height - newHeight);
|
||||
const calculatedHeight = Math.max(MIN_HEIGHT, Math.min(MAX_HEIGHT, resizeStart.height - deltaY));
|
||||
const heightDelta = resizeStart.height - calculatedHeight;
|
||||
newHeight = calculatedHeight;
|
||||
newY = position.y + heightDelta;
|
||||
}
|
||||
|
||||
windowSize = { width: newWidth, height: newHeight };
|
||||
@@ -337,9 +375,12 @@
|
||||
previousPosition = { ...position };
|
||||
|
||||
// Maximizar completamente: usar toda a largura e altura da tela
|
||||
const winWidth = windowDimensions.width || (typeof window !== 'undefined' ? window.innerWidth : DEFAULT_WIDTH);
|
||||
const winHeight = windowDimensions.height || (typeof window !== 'undefined' ? window.innerHeight : DEFAULT_HEIGHT);
|
||||
|
||||
windowSize = {
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight
|
||||
width: winWidth,
|
||||
height: winHeight
|
||||
};
|
||||
position = {
|
||||
x: 0,
|
||||
@@ -355,6 +396,7 @@
|
||||
// Funcionalidade de arrastar
|
||||
function handleMouseDown(e: MouseEvent) {
|
||||
if (e.button !== 0) return; // Apenas botão esquerdo
|
||||
hasMoved = false;
|
||||
isDragging = true;
|
||||
dragStart = {
|
||||
x: e.clientX - position.x,
|
||||
@@ -364,6 +406,20 @@
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
// Handler específico para o botão flutuante (evita conflito com clique)
|
||||
function handleButtonMouseDown(e: MouseEvent) {
|
||||
if (e.button !== 0) return;
|
||||
// Resetar flag de movimento
|
||||
hasMoved = false;
|
||||
isDragging = true;
|
||||
dragStart = {
|
||||
x: e.clientX - position.x,
|
||||
y: e.clientY - position.y,
|
||||
};
|
||||
document.body.classList.add('dragging');
|
||||
// Não prevenir default para permitir clique funcionar se não houver movimento
|
||||
}
|
||||
|
||||
function handleMouseMove(e: MouseEvent) {
|
||||
if (isResizing) {
|
||||
handleResizeMove(e);
|
||||
@@ -375,15 +431,26 @@
|
||||
const newX = e.clientX - dragStart.x;
|
||||
const newY = e.clientY - dragStart.y;
|
||||
|
||||
// Verificar se houve movimento significativo
|
||||
const deltaX = Math.abs(newX - position.x);
|
||||
const deltaY = Math.abs(newY - position.y);
|
||||
if (deltaX > dragThreshold || deltaY > dragThreshold) {
|
||||
hasMoved = true;
|
||||
}
|
||||
|
||||
// Dimensões do widget
|
||||
const widgetWidth = isOpen && !isMinimized ? windowSize.width : 72;
|
||||
const widgetHeight = isOpen && !isMinimized ? windowSize.height : 72;
|
||||
|
||||
// Usar dimensões reativas da janela
|
||||
const winWidth = windowDimensions.width || (typeof window !== 'undefined' ? window.innerWidth : 0);
|
||||
const winHeight = windowDimensions.height || (typeof window !== 'undefined' ? window.innerHeight : 0);
|
||||
|
||||
// Limites da tela com margem de segurança
|
||||
const minX = -(widgetWidth - 100); // Permitir até 100px visíveis
|
||||
const maxX = window.innerWidth - 100; // Manter 100px dentro da tela
|
||||
const maxX = Math.max(0, winWidth - 100); // Manter 100px dentro da tela
|
||||
const minY = -(widgetHeight - 100);
|
||||
const maxY = window.innerHeight - 100;
|
||||
const maxY = Math.max(0, winHeight - 100);
|
||||
|
||||
position = {
|
||||
x: Math.max(minX, Math.min(newX, maxX)),
|
||||
@@ -391,14 +458,26 @@
|
||||
};
|
||||
}
|
||||
|
||||
function handleMouseUp() {
|
||||
function handleMouseUp(e?: MouseEvent) {
|
||||
const hadMoved = hasMoved;
|
||||
|
||||
if (isDragging) {
|
||||
isDragging = false;
|
||||
hasMoved = false;
|
||||
document.body.classList.remove('dragging');
|
||||
|
||||
// Se estava arrastando e houve movimento, prevenir clique
|
||||
if (hadMoved && e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
// Garantir que está dentro dos limites ao soltar
|
||||
ajustarPosicao();
|
||||
}
|
||||
handleResizeEnd();
|
||||
|
||||
return !hadMoved; // Retorna true se não houve movimento (permite clique)
|
||||
}
|
||||
|
||||
function ajustarPosicao() {
|
||||
@@ -408,22 +487,32 @@
|
||||
const widgetWidth = isOpen && !isMinimized ? windowSize.width : 72;
|
||||
const widgetHeight = isOpen && !isMinimized ? windowSize.height : 72;
|
||||
|
||||
// Usar dimensões reativas da janela
|
||||
const winWidth = windowDimensions.width || (typeof window !== 'undefined' ? window.innerWidth : 0);
|
||||
const winHeight = windowDimensions.height || (typeof window !== 'undefined' ? window.innerHeight : 0);
|
||||
|
||||
// Verificar se está fora dos limites
|
||||
let newX = position.x;
|
||||
let newY = position.y;
|
||||
|
||||
// Ajustar X
|
||||
if (newX < -(widgetWidth - 100)) {
|
||||
newX = -(widgetWidth - 100);
|
||||
} else if (newX > window.innerWidth - 100) {
|
||||
newX = window.innerWidth - 100;
|
||||
// Ajustar X - garantir que pelo menos 100px fiquem visíveis
|
||||
const minX = -(widgetWidth - 100);
|
||||
const maxX = Math.max(0, winWidth - 100);
|
||||
|
||||
if (newX < minX) {
|
||||
newX = minX;
|
||||
} else if (newX > maxX) {
|
||||
newX = maxX;
|
||||
}
|
||||
|
||||
// Ajustar Y
|
||||
if (newY < -(widgetHeight - 100)) {
|
||||
newY = -(widgetHeight - 100);
|
||||
} else if (newY > window.innerHeight - 100) {
|
||||
newY = window.innerHeight - 100;
|
||||
// Ajustar Y - garantir que pelo menos 100px fiquem visíveis
|
||||
const minY = -(widgetHeight - 100);
|
||||
const maxY = Math.max(0, winHeight - 100);
|
||||
|
||||
if (newY < minY) {
|
||||
newY = minY;
|
||||
} else if (newY > maxY) {
|
||||
newY = maxY;
|
||||
}
|
||||
|
||||
position = { x: newX, y: newY };
|
||||
@@ -433,15 +522,26 @@
|
||||
}, 300);
|
||||
}
|
||||
|
||||
// Event listeners globais
|
||||
if (typeof window !== 'undefined') {
|
||||
// Event listeners globais com cleanup adequado
|
||||
$effect(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
window.addEventListener('mousemove', handleMouseMove);
|
||||
window.addEventListener('mouseup', handleMouseUp);
|
||||
}
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('mousemove', handleMouseMove);
|
||||
window.removeEventListener('mouseup', handleMouseUp);
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Botão flutuante MODERNO E ARRASTÁVEL -->
|
||||
{#if !isOpen || isMinimized}
|
||||
{@const winWidth = windowDimensions.width || (typeof window !== 'undefined' ? window.innerWidth : 0)}
|
||||
{@const winHeight = windowDimensions.height || (typeof window !== 'undefined' ? window.innerHeight : 0)}
|
||||
{@const bottomPos = position.y === 0 ? '1.5rem' : `${Math.max(0, winHeight - position.y - 72)}px`}
|
||||
{@const rightPos = position.x === 0 ? '1.5rem' : `${Math.max(0, winWidth - position.x - 72)}px`}
|
||||
<button
|
||||
type="button"
|
||||
class="fixed group relative border-0 backdrop-blur-xl"
|
||||
@@ -449,8 +549,8 @@
|
||||
z-index: 99999 !important;
|
||||
width: 4.5rem;
|
||||
height: 4.5rem;
|
||||
bottom: {position.y === 0 ? '1.5rem' : `${window.innerHeight - position.y - 72}px`};
|
||||
right: {position.x === 0 ? '1.5rem' : `${window.innerWidth - position.x - 72}px`};
|
||||
bottom: {bottomPos};
|
||||
right: {rightPos};
|
||||
position: fixed !important;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
|
||||
box-shadow:
|
||||
@@ -462,8 +562,26 @@
|
||||
transform: {isDragging ? 'scale(1.05)' : 'scale(1)'};
|
||||
transition: {isAnimating ? 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)' : 'transform 0.2s, box-shadow 0.3s'};
|
||||
"
|
||||
onclick={handleToggle}
|
||||
onmousedown={handleMouseDown}
|
||||
onmousedown={handleButtonMouseDown}
|
||||
onmouseup={(e) => {
|
||||
const hadMovedBefore = hasMoved;
|
||||
handleMouseUp(e);
|
||||
// Se houve movimento, prevenir o clique
|
||||
if (hadMovedBefore) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}}
|
||||
onclick={(e) => {
|
||||
// Só executar toggle se não houve movimento
|
||||
if (!hasMoved) {
|
||||
handleToggle();
|
||||
} else {
|
||||
// Prevenir clique se houve movimento
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}}
|
||||
aria-label="Abrir chat"
|
||||
>
|
||||
<!-- Anel de brilho rotativo -->
|
||||
@@ -521,12 +639,16 @@
|
||||
|
||||
<!-- Janela do Chat ULTRA MODERNA E ARRASTÁVEL -->
|
||||
{#if isOpen && !isMinimized}
|
||||
{@const winWidth = windowDimensions.width || (typeof window !== 'undefined' ? window.innerWidth : 0)}
|
||||
{@const winHeight = windowDimensions.height || (typeof window !== 'undefined' ? window.innerHeight : 0)}
|
||||
{@const bottomPos = position.y === 0 ? '1.5rem' : `${Math.max(0, winHeight - position.y - windowSize.height)}px`}
|
||||
{@const rightPos = position.x === 0 ? '1.5rem' : `${Math.max(0, winWidth - position.x - windowSize.width)}px`}
|
||||
<div
|
||||
class="fixed flex flex-col overflow-hidden backdrop-blur-2xl"
|
||||
style="
|
||||
z-index: 99999 !important;
|
||||
bottom: {position.y === 0 ? '1.5rem' : `${window.innerHeight - position.y - windowSize.height}px`};
|
||||
right: {position.x === 0 ? '1.5rem' : `${window.innerWidth - position.x - windowSize.width}px`};
|
||||
bottom: {bottomPos};
|
||||
right: {rightPos};
|
||||
width: {windowSize.width}px;
|
||||
height: {windowSize.height}px;
|
||||
max-width: calc(100vw - 3rem);
|
||||
|
||||
Reference in New Issue
Block a user