import { useEffect, useRef, useCallback, useMemo } from 'react'; import * as d3 from 'd3'; /** * Hook optimisé pour la gestion des tooltips * Séparation claire entre logique de positionnement et affichage */ export const useTooltipOptimized = (darkMode) => { const tooltipRef = useRef(null); const selectedTooltipRef = useRef(null); const hoverTooltipRef = useRef(null); const currentTransformRef = useRef(d3.zoomIdentity); const imageLoadTimeoutsRef = useRef(new Map()); // Mémoriser les styles du tooltip selon le mode sombre const tooltipStyles = useMemo(() => ({ dark: { backgroundColor: '#000000', borderColor: '#404040', color: '#ffffff' }, light: { backgroundColor: '#ffffff', borderColor: '#e0e0e0', color: '#000000' } }), []); // Créer les éléments tooltip useEffect(() => { // Tooltip pour la police sélectionnée selectedTooltipRef.current = d3.select('body') .append('div') .attr('class', 'font-tooltip font-tooltip-selected') .style('opacity', 0) .style('position', 'absolute') .style('pointer-events', 'none') .style('z-index', 1001) .style('transition', 'opacity 0.2s ease'); // Tooltip pour la police survolée hoverTooltipRef.current = d3.select('body') .append('div') .attr('class', 'font-tooltip font-tooltip-hover') .style('opacity', 0) .style('position', 'absolute') .style('pointer-events', 'none') .style('z-index', 1000) .style('transition', 'opacity 0.2s ease'); return () => { d3.selectAll('.font-tooltip').remove(); // Nettoyer les timeouts imageLoadTimeoutsRef.current.forEach(timeout => clearTimeout(timeout)); imageLoadTimeoutsRef.current.clear(); }; }, []); // Mettre à jour les styles selon le mode sombre useEffect(() => { const updateTooltipStyles = (tooltip) => { if (!tooltip) return; tooltip.classed('dark-mode', darkMode); const styles = darkMode ? tooltipStyles.dark : tooltipStyles.light; tooltip .style('background-color', styles.backgroundColor) .style('border-color', styles.borderColor) .style('color', styles.color); }; updateTooltipStyles(selectedTooltipRef.current); updateTooltipStyles(hoverTooltipRef.current); }, [darkMode, tooltipStyles]); // Fonction optimisée pour créer le contenu du tooltip const createTooltipContent = useCallback((font) => { const imageName = font.imageName || font.name; const sentenceImagePath = `/data/sentences/${imageName.toLowerCase().replace(/\s+/g, '_')}_sentence.svg`; const textColor = darkMode ? '#ffffff' : '#000000'; const imageFilter = darkMode ? 'invert(1)' : 'none'; return `
${font.name}
${font.name} sentence preview
Loading...
`; }, [darkMode]); // Fonction optimisée pour positionner un tooltip const positionTooltip = useCallback((tooltip, svgElement) => { if (!tooltip || !svgElement) return; const tooltipNode = tooltip.node(); const tooltipRect = tooltipNode.getBoundingClientRect(); const elementRect = svgElement.getBoundingClientRect(); // Calculer la position du centre de l'élément const centerX = elementRect.left + (elementRect.width / 2); const centerY = elementRect.top + (elementRect.height / 2); // Positionner le tooltip let x = centerX - (tooltipRect.width / 2); let y = centerY - tooltipRect.height - 20; // Ajuster si le tooltip sort de l'écran const margin = 10; if (x < margin) x = margin; if (x + tooltipRect.width > window.innerWidth - margin) { x = window.innerWidth - tooltipRect.width - margin; } if (y < margin) { y = centerY + 30; } tooltip .style('left', `${x}px`) .style('top', `${y}px`); }, []); // Fonction optimisée pour afficher un tooltip const showTooltip = useCallback((tooltip, font, svgElement) => { if (!tooltip || !font) return; tooltip .html(createTooltipContent(font)) .style('opacity', 1); // Positionner après un court délai setTimeout(() => { positionTooltip(tooltip, svgElement); }, 10); }, [createTooltipContent, positionTooltip]); // Fonction pour masquer un tooltip const hideTooltip = useCallback((tooltip) => { if (!tooltip) return; tooltip.style('opacity', 0); }, []); // Fonction optimisée pour mettre à jour les positions des tooltips const updatePositions = useCallback(() => { const svg = d3.select('.fontmap-svg'); if (svg.empty()) return; const viewportGroup = svg.select('.viewport-group'); if (viewportGroup.empty()) return; // Mettre à jour le tooltip sélectionné if (selectedTooltipRef.current && selectedTooltipRef.current.style('opacity') !== '0') { const selectedGlyph = viewportGroup.selectAll('.font-glyph-group') .filter(function(d) { return d && d.name && window.currentSelectedFont && d.name === window.currentSelectedFont.name; }); if (!selectedGlyph.empty()) { const glyphElement = selectedGlyph.node(); positionTooltip(selectedTooltipRef.current, glyphElement); } } // Mettre à jour le tooltip hover if (hoverTooltipRef.current && hoverTooltipRef.current.style('opacity') !== '0') { const hoveredGlyph = viewportGroup.selectAll('.font-glyph-group') .filter(function(d) { return d && d.name && window.currentHoveredFont && d.name === window.currentHoveredFont.name; }); if (!hoveredGlyph.empty()) { const glyphElement = hoveredGlyph.node(); positionTooltip(hoverTooltipRef.current, glyphElement); } } }, [positionTooltip]); // Fonction pour mettre à jour la transformation const updateTransform = useCallback((transform) => { currentTransformRef.current = transform; setTimeout(() => { updatePositions(); }, 0); }, [updatePositions]); // Fonction pour gérer la sélection d'une police const handleFontSelect = useCallback((font, svgElement) => { if (!font) { hideTooltip(selectedTooltipRef.current); return; } hideTooltip(hoverTooltipRef.current); showTooltip(selectedTooltipRef.current, font, svgElement); }, [showTooltip, hideTooltip]); // Fonction pour gérer le hover d'une police const handleFontHover = useCallback((font, svgElement) => { if (!font) { hideTooltip(hoverTooltipRef.current); return; } showTooltip(hoverTooltipRef.current, font, svgElement); }, [showTooltip, hideTooltip]); // Fonction pour gérer la fin du hover const handleFontUnhover = useCallback(() => { hideTooltip(hoverTooltipRef.current); }, [hideTooltip]); return { handleFontSelect, handleFontHover, handleFontUnhover, updateTransform, updatePositions }; };