/** * Utilitários para criar janela flutuante redimensionável e arrastável */ export interface PosicaoJanela { x: number; y: number; width: number; height: number; } export interface LimitesJanela { minWidth: number; minHeight: number; maxWidth?: number; maxHeight?: number; } function getDefaultLimits(): LimitesJanela { if (typeof window === 'undefined') { return { minWidth: 400, minHeight: 300, maxWidth: 1920, maxHeight: 1080 }; } return { minWidth: 400, minHeight: 300, maxWidth: window.innerWidth, maxHeight: window.innerHeight }; } const DEFAULT_LIMITS: LimitesJanela = getDefaultLimits(); /** * Salvar posição da janela no localStorage */ export function salvarPosicaoJanela( id: string, posicao: PosicaoJanela ): void { if (typeof window === 'undefined' || typeof localStorage === 'undefined') { return; } try { const key = `floating-window-${id}`; localStorage.setItem(key, JSON.stringify(posicao)); } catch (error) { console.warn('Erro ao salvar posição da janela:', error); } } /** * Restaurar posição da janela do localStorage */ export function restaurarPosicaoJanela(id: string): PosicaoJanela | null { if (typeof window === 'undefined' || typeof localStorage === 'undefined') { return null; } try { const key = `floating-window-${id}`; const saved = localStorage.getItem(key); if (!saved) return null; const posicao = JSON.parse(saved) as PosicaoJanela; // Validar se a posição ainda é válida (dentro da tela) if ( posicao.x >= 0 && posicao.y >= 0 && posicao.x + posicao.width <= window.innerWidth + 100 && posicao.y + posicao.height <= window.innerHeight + 100 && posicao.width >= DEFAULT_LIMITS.minWidth && posicao.height >= DEFAULT_LIMITS.minHeight ) { return posicao; } return null; } catch (error) { console.warn('Erro ao restaurar posição da janela:', error); return null; } } /** * Obter posição inicial da janela (centralizada) */ export function obterPosicaoInicial( width: number = 800, height: number = 600 ): PosicaoJanela { if (typeof window === 'undefined') { return { x: 100, y: 100, width, height }; } return { x: (window.innerWidth - width) / 2, y: (window.innerHeight - height) / 2, width, height }; } /** * Criar handler de arrastar para janela */ export function criarDragHandler( element: HTMLElement, handle: HTMLElement, onPositionChange?: (x: number, y: number) => void ): () => void { let isDragging = false; let startX = 0; let startY = 0; let initialX = 0; let initialY = 0; function handleMouseDown(e: MouseEvent): void { if (e.button !== 0) return; // Apenas botão esquerdo isDragging = true; startX = e.clientX; startY = e.clientY; const rect = element.getBoundingClientRect(); initialX = rect.left; initialY = rect.top; document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mouseup', handleMouseUp); e.preventDefault(); } function handleMouseMove(e: MouseEvent): void { if (!isDragging) return; const deltaX = e.clientX - startX; const deltaY = e.clientY - startY; let newX = initialX + deltaX; let newY = initialY + deltaY; // Limitar movimento dentro da tela if (typeof window === 'undefined') return; const maxX = window.innerWidth - element.offsetWidth; const maxY = window.innerHeight - element.offsetHeight; newX = Math.max(0, Math.min(newX, maxX)); newY = Math.max(0, Math.min(newY, maxY)); element.style.left = `${newX}px`; element.style.top = `${newY}px`; if (onPositionChange) { onPositionChange(newX, newY); } } function handleMouseUp(): void { isDragging = false; document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); } // Suporte para touch (mobile) function handleTouchStart(e: TouchEvent): void { if (e.touches.length !== 1) return; isDragging = true; const touch = e.touches[0]; startX = touch.clientX; startY = touch.clientY; const rect = element.getBoundingClientRect(); initialX = rect.left; initialY = rect.top; document.addEventListener('touchmove', handleTouchMove, { passive: false }); document.addEventListener('touchend', handleTouchEnd); e.preventDefault(); } function handleTouchMove(e: TouchEvent): void { if (!isDragging || e.touches.length !== 1) return; const touch = e.touches[0]; const deltaX = touch.clientX - startX; const deltaY = touch.clientY - startY; let newX = initialX + deltaX; let newY = initialY + deltaY; if (typeof window === 'undefined') return; const maxX = window.innerWidth - element.offsetWidth; const maxY = window.innerHeight - element.offsetHeight; newX = Math.max(0, Math.min(newX, maxX)); newY = Math.max(0, Math.min(newY, maxY)); element.style.left = `${newX}px`; element.style.top = `${newY}px`; if (onPositionChange) { onPositionChange(newX, newY); } e.preventDefault(); } function handleTouchEnd(): void { isDragging = false; document.removeEventListener('touchmove', handleTouchMove); document.removeEventListener('touchend', handleTouchEnd); } handle.addEventListener('mousedown', handleMouseDown); handle.addEventListener('touchstart', handleTouchStart, { passive: false }); // Retornar função de cleanup return () => { handle.removeEventListener('mousedown', handleMouseDown); handle.removeEventListener('touchstart', handleTouchStart); document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); document.removeEventListener('touchmove', handleTouchMove); document.removeEventListener('touchend', handleTouchEnd); }; } /** * Criar handler de redimensionar para janela */ export function criarResizeHandler( element: HTMLElement, handles: HTMLElement[], limites: LimitesJanela = DEFAULT_LIMITS, onSizeChange?: (width: number, height: number) => void ): () => void { let isResizing = false; let currentHandle: HTMLElement | null = null; let startX = 0; let startY = 0; let startWidth = 0; let startHeight = 0; let startLeft = 0; let startTop = 0; function handleMouseDown(e: MouseEvent, handle: HTMLElement): void { if (e.button !== 0) return; isResizing = true; currentHandle = handle; startX = e.clientX; startY = e.clientY; const rect = element.getBoundingClientRect(); startWidth = rect.width; startHeight = rect.height; startLeft = rect.left; startTop = rect.top; document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mouseup', handleMouseUp); e.preventDefault(); e.stopPropagation(); } function handleMouseMove(e: MouseEvent): void { if (!isResizing || !currentHandle) return; const deltaX = e.clientX - startX; const deltaY = e.clientY - startY; let newWidth = startWidth; let newHeight = startHeight; let newLeft = startLeft; let newTop = startTop; // Determinar direção do resize baseado na classe do handle const classes = currentHandle.className; // Right if (classes.includes('resize-right') || classes.includes('resize-e')) { newWidth = startWidth + deltaX; } // Bottom if (classes.includes('resize-bottom') || classes.includes('resize-s')) { newHeight = startHeight + deltaY; } // Left if (classes.includes('resize-left') || classes.includes('resize-w')) { newWidth = startWidth - deltaX; newLeft = startLeft + deltaX; } // Top if (classes.includes('resize-top') || classes.includes('resize-n')) { newHeight = startHeight - deltaY; newTop = startTop + deltaY; } // Corner handles if (classes.includes('resize-se')) { newWidth = startWidth + deltaX; newHeight = startHeight + deltaY; } if (classes.includes('resize-sw')) { newWidth = startWidth - deltaX; newHeight = startHeight + deltaY; newLeft = startLeft + deltaX; } if (classes.includes('resize-ne')) { newWidth = startWidth + deltaX; newHeight = startHeight - deltaY; newTop = startTop + deltaY; } if (classes.includes('resize-nw')) { newWidth = startWidth - deltaX; newHeight = startHeight - deltaY; newLeft = startLeft + deltaX; newTop = startTop + deltaY; } if (typeof window === 'undefined') return; // Aplicar limites const maxWidth = limites.maxWidth || window.innerWidth - newLeft; const maxHeight = limites.maxHeight || window.innerHeight - newTop; newWidth = Math.max(limites.minWidth, Math.min(newWidth, maxWidth)); newHeight = Math.max(limites.minHeight, Math.min(newHeight, maxHeight)); // Ajustar posição se necessário if (newLeft + newWidth > window.innerWidth) { newLeft = window.innerWidth - newWidth; } if (newTop + newHeight > window.innerHeight) { newTop = window.innerHeight - newHeight; } if (newLeft < 0) { newLeft = 0; newWidth = Math.min(newWidth, window.innerWidth); } if (newTop < 0) { newTop = 0; newHeight = Math.min(newHeight, window.innerHeight); } element.style.width = `${newWidth}px`; element.style.height = `${newHeight}px`; element.style.left = `${newLeft}px`; element.style.top = `${newTop}px`; if (onSizeChange) { onSizeChange(newWidth, newHeight); } } function handleMouseUp(): void { isResizing = false; currentHandle = null; document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); } const cleanupFunctions: (() => void)[] = []; // Adicionar listeners para cada handle for (const handle of handles) { const handler = (e: MouseEvent) => handleMouseDown(e, handle); handle.addEventListener('mousedown', handler); cleanupFunctions.push(() => handle.removeEventListener('mousedown', handler)); } // Retornar função de cleanup return () => { cleanupFunctions.forEach((cleanup) => cleanup()); document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); }; }