phx-react
Version:
PHX REACT
299 lines • 14.6 kB
JavaScript
;
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