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 isDragging = $state(false);
|
||||||
let dragStart = $state({ x: 0, y: 0 });
|
let dragStart = $state({ x: 0, y: 0 });
|
||||||
let isAnimating = $state(false);
|
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)
|
// Tamanho da janela (redimensionável)
|
||||||
const MIN_WIDTH = 300;
|
const MIN_WIDTH = 300;
|
||||||
@@ -76,6 +78,38 @@
|
|||||||
let previousSize = $state({ width: DEFAULT_WIDTH, height: DEFAULT_HEIGHT });
|
let previousSize = $state({ width: DEFAULT_WIDTH, height: DEFAULT_HEIGHT });
|
||||||
let previousPosition = $state({ x: 0, y: 0 });
|
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
|
// Salvar tamanho no localStorage
|
||||||
function saveSize() {
|
function saveSize() {
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
@@ -119,15 +153,19 @@
|
|||||||
newWidth = Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, resizeStart.width + deltaX));
|
newWidth = Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, resizeStart.width + deltaX));
|
||||||
}
|
}
|
||||||
if (resizeDirection.includes('w')) {
|
if (resizeDirection.includes('w')) {
|
||||||
newWidth = Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, resizeStart.width - deltaX));
|
const calculatedWidth = Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, resizeStart.width - deltaX));
|
||||||
newX = position.x + (resizeStart.width - newWidth);
|
const widthDelta = resizeStart.width - calculatedWidth;
|
||||||
|
newWidth = calculatedWidth;
|
||||||
|
newX = position.x + widthDelta;
|
||||||
}
|
}
|
||||||
if (resizeDirection.includes('s')) {
|
if (resizeDirection.includes('s')) {
|
||||||
newHeight = Math.max(MIN_HEIGHT, Math.min(MAX_HEIGHT, resizeStart.height + deltaY));
|
newHeight = Math.max(MIN_HEIGHT, Math.min(MAX_HEIGHT, resizeStart.height + deltaY));
|
||||||
}
|
}
|
||||||
if (resizeDirection.includes('n')) {
|
if (resizeDirection.includes('n')) {
|
||||||
newHeight = Math.max(MIN_HEIGHT, Math.min(MAX_HEIGHT, resizeStart.height - deltaY));
|
const calculatedHeight = Math.max(MIN_HEIGHT, Math.min(MAX_HEIGHT, resizeStart.height - deltaY));
|
||||||
newY = position.y + (resizeStart.height - newHeight);
|
const heightDelta = resizeStart.height - calculatedHeight;
|
||||||
|
newHeight = calculatedHeight;
|
||||||
|
newY = position.y + heightDelta;
|
||||||
}
|
}
|
||||||
|
|
||||||
windowSize = { width: newWidth, height: newHeight };
|
windowSize = { width: newWidth, height: newHeight };
|
||||||
@@ -337,9 +375,12 @@
|
|||||||
previousPosition = { ...position };
|
previousPosition = { ...position };
|
||||||
|
|
||||||
// Maximizar completamente: usar toda a largura e altura da tela
|
// 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 = {
|
windowSize = {
|
||||||
width: window.innerWidth,
|
width: winWidth,
|
||||||
height: window.innerHeight
|
height: winHeight
|
||||||
};
|
};
|
||||||
position = {
|
position = {
|
||||||
x: 0,
|
x: 0,
|
||||||
@@ -355,6 +396,7 @@
|
|||||||
// Funcionalidade de arrastar
|
// Funcionalidade de arrastar
|
||||||
function handleMouseDown(e: MouseEvent) {
|
function handleMouseDown(e: MouseEvent) {
|
||||||
if (e.button !== 0) return; // Apenas botão esquerdo
|
if (e.button !== 0) return; // Apenas botão esquerdo
|
||||||
|
hasMoved = false;
|
||||||
isDragging = true;
|
isDragging = true;
|
||||||
dragStart = {
|
dragStart = {
|
||||||
x: e.clientX - position.x,
|
x: e.clientX - position.x,
|
||||||
@@ -364,6 +406,20 @@
|
|||||||
e.preventDefault();
|
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) {
|
function handleMouseMove(e: MouseEvent) {
|
||||||
if (isResizing) {
|
if (isResizing) {
|
||||||
handleResizeMove(e);
|
handleResizeMove(e);
|
||||||
@@ -375,15 +431,26 @@
|
|||||||
const newX = e.clientX - dragStart.x;
|
const newX = e.clientX - dragStart.x;
|
||||||
const newY = e.clientY - dragStart.y;
|
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
|
// Dimensões do widget
|
||||||
const widgetWidth = isOpen && !isMinimized ? windowSize.width : 72;
|
const widgetWidth = isOpen && !isMinimized ? windowSize.width : 72;
|
||||||
const widgetHeight = isOpen && !isMinimized ? windowSize.height : 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
|
// Limites da tela com margem de segurança
|
||||||
const minX = -(widgetWidth - 100); // Permitir até 100px visíveis
|
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 minY = -(widgetHeight - 100);
|
||||||
const maxY = window.innerHeight - 100;
|
const maxY = Math.max(0, winHeight - 100);
|
||||||
|
|
||||||
position = {
|
position = {
|
||||||
x: Math.max(minX, Math.min(newX, maxX)),
|
x: Math.max(minX, Math.min(newX, maxX)),
|
||||||
@@ -391,14 +458,26 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleMouseUp() {
|
function handleMouseUp(e?: MouseEvent) {
|
||||||
|
const hadMoved = hasMoved;
|
||||||
|
|
||||||
if (isDragging) {
|
if (isDragging) {
|
||||||
isDragging = false;
|
isDragging = false;
|
||||||
|
hasMoved = false;
|
||||||
document.body.classList.remove('dragging');
|
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
|
// Garantir que está dentro dos limites ao soltar
|
||||||
ajustarPosicao();
|
ajustarPosicao();
|
||||||
}
|
}
|
||||||
handleResizeEnd();
|
handleResizeEnd();
|
||||||
|
|
||||||
|
return !hadMoved; // Retorna true se não houve movimento (permite clique)
|
||||||
}
|
}
|
||||||
|
|
||||||
function ajustarPosicao() {
|
function ajustarPosicao() {
|
||||||
@@ -408,22 +487,32 @@
|
|||||||
const widgetWidth = isOpen && !isMinimized ? windowSize.width : 72;
|
const widgetWidth = isOpen && !isMinimized ? windowSize.width : 72;
|
||||||
const widgetHeight = isOpen && !isMinimized ? windowSize.height : 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
|
// Verificar se está fora dos limites
|
||||||
let newX = position.x;
|
let newX = position.x;
|
||||||
let newY = position.y;
|
let newY = position.y;
|
||||||
|
|
||||||
// Ajustar X
|
// Ajustar X - garantir que pelo menos 100px fiquem visíveis
|
||||||
if (newX < -(widgetWidth - 100)) {
|
const minX = -(widgetWidth - 100);
|
||||||
newX = -(widgetWidth - 100);
|
const maxX = Math.max(0, winWidth - 100);
|
||||||
} else if (newX > window.innerWidth - 100) {
|
|
||||||
newX = window.innerWidth - 100;
|
if (newX < minX) {
|
||||||
|
newX = minX;
|
||||||
|
} else if (newX > maxX) {
|
||||||
|
newX = maxX;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ajustar Y
|
// Ajustar Y - garantir que pelo menos 100px fiquem visíveis
|
||||||
if (newY < -(widgetHeight - 100)) {
|
const minY = -(widgetHeight - 100);
|
||||||
newY = -(widgetHeight - 100);
|
const maxY = Math.max(0, winHeight - 100);
|
||||||
} else if (newY > window.innerHeight - 100) {
|
|
||||||
newY = window.innerHeight - 100;
|
if (newY < minY) {
|
||||||
|
newY = minY;
|
||||||
|
} else if (newY > maxY) {
|
||||||
|
newY = maxY;
|
||||||
}
|
}
|
||||||
|
|
||||||
position = { x: newX, y: newY };
|
position = { x: newX, y: newY };
|
||||||
@@ -433,15 +522,26 @@
|
|||||||
}, 300);
|
}, 300);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Event listeners globais
|
// Event listeners globais com cleanup adequado
|
||||||
if (typeof window !== 'undefined') {
|
$effect(() => {
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
|
||||||
window.addEventListener('mousemove', handleMouseMove);
|
window.addEventListener('mousemove', handleMouseMove);
|
||||||
window.addEventListener('mouseup', handleMouseUp);
|
window.addEventListener('mouseup', handleMouseUp);
|
||||||
}
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('mousemove', handleMouseMove);
|
||||||
|
window.removeEventListener('mouseup', handleMouseUp);
|
||||||
|
};
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Botão flutuante MODERNO E ARRASTÁVEL -->
|
<!-- Botão flutuante MODERNO E ARRASTÁVEL -->
|
||||||
{#if !isOpen || isMinimized}
|
{#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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="fixed group relative border-0 backdrop-blur-xl"
|
class="fixed group relative border-0 backdrop-blur-xl"
|
||||||
@@ -449,8 +549,8 @@
|
|||||||
z-index: 99999 !important;
|
z-index: 99999 !important;
|
||||||
width: 4.5rem;
|
width: 4.5rem;
|
||||||
height: 4.5rem;
|
height: 4.5rem;
|
||||||
bottom: {position.y === 0 ? '1.5rem' : `${window.innerHeight - position.y - 72}px`};
|
bottom: {bottomPos};
|
||||||
right: {position.x === 0 ? '1.5rem' : `${window.innerWidth - position.x - 72}px`};
|
right: {rightPos};
|
||||||
position: fixed !important;
|
position: fixed !important;
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
|
||||||
box-shadow:
|
box-shadow:
|
||||||
@@ -462,8 +562,26 @@
|
|||||||
transform: {isDragging ? 'scale(1.05)' : 'scale(1)'};
|
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'};
|
transition: {isAnimating ? 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)' : 'transform 0.2s, box-shadow 0.3s'};
|
||||||
"
|
"
|
||||||
onclick={handleToggle}
|
onmousedown={handleButtonMouseDown}
|
||||||
onmousedown={handleMouseDown}
|
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"
|
aria-label="Abrir chat"
|
||||||
>
|
>
|
||||||
<!-- Anel de brilho rotativo -->
|
<!-- Anel de brilho rotativo -->
|
||||||
@@ -521,12 +639,16 @@
|
|||||||
|
|
||||||
<!-- Janela do Chat ULTRA MODERNA E ARRASTÁVEL -->
|
<!-- Janela do Chat ULTRA MODERNA E ARRASTÁVEL -->
|
||||||
{#if isOpen && !isMinimized}
|
{#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
|
<div
|
||||||
class="fixed flex flex-col overflow-hidden backdrop-blur-2xl"
|
class="fixed flex flex-col overflow-hidden backdrop-blur-2xl"
|
||||||
style="
|
style="
|
||||||
z-index: 99999 !important;
|
z-index: 99999 !important;
|
||||||
bottom: {position.y === 0 ? '1.5rem' : `${window.innerHeight - position.y - windowSize.height}px`};
|
bottom: {bottomPos};
|
||||||
right: {position.x === 0 ? '1.5rem' : `${window.innerWidth - position.x - windowSize.width}px`};
|
right: {rightPos};
|
||||||
width: {windowSize.width}px;
|
width: {windowSize.width}px;
|
||||||
height: {windowSize.height}px;
|
height: {windowSize.height}px;
|
||||||
max-width: calc(100vw - 3rem);
|
max-width: calc(100vw - 3rem);
|
||||||
|
|||||||
Reference in New Issue
Block a user