UNPKG

phx-react

Version:

PHX REACT

299 lines 14.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const tslib_1 = require("tslib"); const react_1 = tslib_1.__importStar(require("react")); const react_dom_1 = require("react-dom"); const PHXTooltip = ({ children, className = '', content, contentClassName = 'max-w-[18rem]', isDisableHover = false, placement, underLine = false, hideDelay = 200, }) => { const contentRef = (0, react_1.useRef)(null); const titleRef = (0, react_1.useRef)(null); const [contentProps, setContentProps] = (0, react_1.useState)({ top: 0, left: 0, width: 0, height: 0, pos: 'top', show: false, arrowOffset: 0, }); const hideTimeoutRef = (0, react_1.useRef)(); const isMobile = () => window.innerWidth <= 768; const getOverflowDirections = ({ height, left, top, width, }) => { const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; const margin = isMobile() ? 20 : 10; return { isOverFlowTop: top < margin, isOverFlowLeft: left < margin, isOverFlowBottom: top + height > viewportHeight - margin, isOverFlowRight: left + width > viewportWidth - margin, }; }; const getPositionProps = (position, titleBounding, contentBounding) => { const titleTop = titleBounding.top; const titleLeft = titleBounding.left; const titleWidth = titleBounding.width; const titleHeight = titleBounding.height; switch (position) { case 'top': return { width: contentBounding.width, height: contentBounding.height, top: titleTop - contentBounding.height - 10, left: titleLeft - contentBounding.width / 2 + titleWidth / 2, pos: 'top', }; case 'bottom': return { width: contentBounding.width, height: contentBounding.height, top: titleTop + titleHeight + 10, left: titleLeft - contentBounding.width / 2 + titleWidth / 2, pos: 'bottom', }; case 'left': return { width: contentBounding.width, height: contentBounding.height, top: titleTop - contentBounding.height / 2 + titleHeight / 2, left: titleLeft - contentBounding.width - 20, pos: 'left', }; case 'right': return { width: contentBounding.width, height: contentBounding.height, top: titleTop - contentBounding.height / 2 + titleHeight / 2, left: titleLeft + titleWidth + 20, pos: 'right', }; default: return { width: contentBounding.width, height: contentBounding.height, top: titleTop - contentBounding.height - 10, left: titleLeft - contentBounding.width / 2 + titleWidth / 2, pos: 'top', }; } }; const showTimeoutRef = (0, react_1.useRef)(); const calculateTooltipPosition = (0, react_1.useCallback)(() => { if (!contentRef.current || !titleRef.current) return; const titleBounding = titleRef.current.getBoundingClientRect(); const contentBounding = contentRef.current.getBoundingClientRect(); const preferredPlacement = isMobile() ? 'bottom' : placement || 'top'; let newProps = getPositionProps(preferredPlacement, titleBounding, contentBounding); const overflowDirections = getOverflowDirections(newProps); const titleCenter = titleBounding.left + titleBounding.width / 2; const titleVerticalCenter = titleBounding.top + titleBounding.height / 2; let arrowOffset = 0; if (isMobile()) { if (overflowDirections.isOverFlowBottom) { newProps = getPositionProps('top', titleBounding, contentBounding); const topOverflow = getOverflowDirections(newProps); if (topOverflow.isOverFlowTop) { newProps = { ...newProps, top: Math.max(20, Math.min(window.innerHeight - newProps.height - 20, titleBounding.top)), }; } } } else { switch (preferredPlacement) { case 'top': if (overflowDirections.isOverFlowTop) { newProps = getPositionProps('bottom', titleBounding, contentBounding); } break; case 'bottom': if (overflowDirections.isOverFlowBottom) { newProps = getPositionProps('top', titleBounding, contentBounding); } break; case 'left': if (overflowDirections.isOverFlowLeft) { newProps = getPositionProps('right', titleBounding, contentBounding); } break; case 'right': if (overflowDirections.isOverFlowRight) { newProps = getPositionProps('left', titleBounding, contentBounding); } break; default: break; } } const finalOverflow = getOverflowDirections(newProps); if (finalOverflow.isOverFlowLeft || finalOverflow.isOverFlowRight) { if (newProps.pos === 'top' || newProps.pos === 'bottom') { if (finalOverflow.isOverFlowRight) { const leftProps = getPositionProps('left', titleBounding, contentBounding); const leftOverflow = getOverflowDirections(leftProps); if (!leftOverflow.isOverFlowLeft) { newProps = leftProps; } else { const margin = isMobile() ? 20 : 10; newProps.left = Math.max(margin, Math.min(window.innerWidth - newProps.width - margin, newProps.left)); } } else if (finalOverflow.isOverFlowLeft) { const rightProps = getPositionProps('right', titleBounding, contentBounding); const rightOverflow = getOverflowDirections(rightProps); if (!rightOverflow.isOverFlowRight) { newProps = rightProps; } else { const margin = isMobile() ? 20 : 10; newProps.left = Math.max(margin, Math.min(window.innerWidth - newProps.width - margin, newProps.left)); } } } else { const margin = isMobile() ? 20 : 10; newProps.left = Math.max(margin, Math.min(window.innerWidth - newProps.width - margin, newProps.left)); } } if (newProps.pos === 'top' || newProps.pos === 'bottom') { arrowOffset = titleCenter - (newProps.left + newProps.width / 2); const maxOffset = newProps.width / 2 - 15; arrowOffset = Math.max(-maxOffset, Math.min(maxOffset, arrowOffset)); } if (newProps.pos === 'left' || newProps.pos === 'right') { arrowOffset = titleVerticalCenter - (newProps.top + newProps.height / 2); const maxOffset = newProps.height / 2 - 15; arrowOffset = Math.max(-maxOffset, Math.min(maxOffset, arrowOffset)); } setContentProps((prev) => ({ ...prev, ...newProps, arrowOffset })); }, [placement]); const handleMouseEnter = () => { if (hideTimeoutRef.current) { clearTimeout(hideTimeoutRef.current); hideTimeoutRef.current = undefined; } if (showTimeoutRef.current) { clearTimeout(showTimeoutRef.current); } showTimeoutRef.current = setTimeout(() => { setContentProps((prev) => ({ ...prev, show: true })); setTimeout(() => { calculateTooltipPosition(); }, 10); }, 250); // <-- Độ trễ 250ms trước khi hiển thị tooltip }; const handleMouseLeave = () => { if (showTimeoutRef.current) { clearTimeout(showTimeoutRef.current); showTimeoutRef.current = undefined; } if (hideTimeoutRef.current) { clearTimeout(hideTimeoutRef.current); } hideTimeoutRef.current = setTimeout(() => { setContentProps((prev) => ({ ...prev, show: false })); }, hideDelay); }; const handleMobileClick = () => { if (isMobile()) { setContentProps((prev) => { setTimeout(() => { calculateTooltipPosition(); }, 10); return { ...prev, show: true }; }); } }; (0, react_1.useEffect)(() => { return () => { if (hideTimeoutRef.current) { clearTimeout(hideTimeoutRef.current); } if (showTimeoutRef.current) { clearTimeout(showTimeoutRef.current); } }; }, []); (0, react_1.useEffect)(() => { let timeoutId; let isCalculating = false; let touchStartX = 0; let touchStartY = 0; const updatePosition = () => { if (contentProps.show && !isCalculating) { isCalculating = true; calculateTooltipPosition(); timeoutId = setTimeout(() => { isCalculating = false; }, 100); } }; const handleTouchStart = (e) => { if (contentProps.show && isMobile()) { touchStartX = e.touches[0].clientX; touchStartY = e.touches[0].clientY; } }; const handleTouchMove = (e) => { if (contentProps.show && isMobile()) { const touchEndX = e.touches[0].clientX; const touchEndY = e.touches[0].clientY; const deltaX = Math.abs(touchEndX - touchStartX); const deltaY = Math.abs(touchEndY - touchStartY); if (deltaX > 30 || deltaY > 30) { setContentProps((prev) => ({ ...prev, show: false })); } } }; if (contentProps.show) { updatePosition(); if (isMobile()) { document.addEventListener('touchstart', handleTouchStart, { passive: true, }); document.addEventListener('touchmove', handleTouchMove, { passive: true, }); } } return () => { if (timeoutId) { clearTimeout(timeoutId); } if (isMobile()) { document.removeEventListener('touchstart', handleTouchStart); document.removeEventListener('touchmove', handleTouchMove); } }; }, [contentProps.show, calculateTooltipPosition]); return (react_1.default.createElement("div", { className: `relative w-fit text-xs font-normal text-gray-600 ${className}`, onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave }, content && (0, react_dom_1.createPortal)(react_1.default.createElement("div", { className: `fixed z-[999] mb-2 flex transform flex-col items-center justify-center drop-shadow-md transition-opacity duration-200 ${contentProps.show ? 'visible opacity-100' : 'invisible opacity-0 pointer-events-none'}`, onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, style: { top: contentProps.top, left: contentProps.left, } }, react_1.default.createElement("p", { ref: contentRef, className: `whitespace-pre-wrap break-words rounded-lg border-[1px] border-gray-300 bg-white p-2 text-xs text-gray-800 ${contentClassName}` }, content), react_1.default.createElement("svg", { className: `absolute ${contentProps.pos === 'top' ? '-bottom-[10px] rotate-180' : ''} ${contentProps.pos === 'bottom' ? '-top-[10px]' : ''} ${contentProps.pos === 'left' ? '-right-[12px] rotate-90' : ''} ${contentProps.pos === 'right' ? '-left-[12px] -rotate-90' : ''}`, fill: 'none', height: '16', style: { ...(contentProps.pos === 'top' || contentProps.pos === 'bottom' ? { left: `calc(50% + ${contentProps.arrowOffset}px)`, transform: `translateX(-50%) ${contentProps.pos === 'top' ? 'rotate(180deg)' : ''}`, } : { top: `calc(50% + ${contentProps.arrowOffset}px)`, transform: `translateY(-50%) ${contentProps.pos === 'left' ? 'rotate(90deg)' : contentProps.pos === 'right' ? 'rotate(-90deg)' : ''}`, }), }, viewBox: '0 0 20 16', width: '20', xmlns: 'http://www.w3.org/2000/svg' }, react_1.default.createElement("path", { d: 'M10.6082 1.2699L17.3135 10.5611C17.6715 11.0571 17.3171 11.75 16.7053 11.75H3.29465C2.68295 11.75 2.32852 11.0571 2.68649 10.5611L9.39184 1.2699C9.69119 0.855102 10.3088 0.855102 10.6082 1.2699Z', fill: 'white', stroke: '#D1D5DB', strokeWidth: '1' }), react_1.default.createElement("rect", { fill: 'white', height: '3', rx: '1.5', width: '16', x: '2', y: '10' })), !isDisableHover && react_1.default.createElement("div", { className: 'absolute cursor-pointer top-12 h-7 w-1' })), document.body), react_1.default.createElement("div", { className: `hover:cursor-pointer ${underLine && 'border-b-2 border-dotted border-[#B5B5B5] pb-[1px]'}`, ref: titleRef, onClick: handleMobileClick }, children))); }; exports.default = PHXTooltip; //# sourceMappingURL=ToolTip.js.map