interface Point { x: number; y: number; } interface Size { height: string; width: string; } class SexyTooltip { private static readonly carrierStyle: Partial = { opacity: "0", display: "block", pointerEvents: "none", backgroundColor: "black", border: "white solid 1px", color: "white", padding: "10px", position: "absolute", zIndex: "1", overflow: "hidden", height: "0", width: "0", transition: "opacity 200ms, height 200ms, width 200ms", }; private static readonly textCarrierStyle: Partial = { width: "350px", display: "block", overflow: "hidden", }; private static readonly defaultWidth = 350; private readonly carrier: HTMLDivElement; private readonly textCarrier: HTMLSpanElement; private readonly elementsWithTooltips: Element[] = []; private active: boolean = false; constructor(carrier: HTMLDivElement) { if (carrier.childNodes.length > 0) { throw new Error("Incorrect setup for tooltip! Remove the child nodes from the tooltip div."); } this.carrier = carrier; this.textCarrier = document.createElement("span"); this.carrier.appendChild(this.textCarrier); Object.assign(this.carrier.style, SexyTooltip.carrierStyle); Object.assign(this.textCarrier.style, SexyTooltip.textCarrierStyle); document.querySelectorAll(".tooltip").forEach((element) => { if (element.nodeName === "SPAN") { console.log(element.previousElementSibling); this.elementsWithTooltips.push(element.previousElementSibling); } }); document.addEventListener("mousemove", (event) => this.rerenderTooltip({ x: event.pageX, y: event.pageY })); } rerenderTooltip(mousePos: Point) { const newText = this.getTooltipTextForHoveredElement(mousePos); if (newText !== this.textCarrier.innerHTML) { this.transitionTooltipSize(newText); const tooltipHasText = newText !== ""; if (tooltipHasText !== this.active) { this.toggleActive(); } } if (this.isStillVisible()) { this.updatePosition(mousePos); } } isStillVisible() { return getComputedStyle(this.carrier).opacity !== "0"; } updatePosition(pos: Point) { if (pos.x + 15 + this.carrier.clientWidth <= document.body.scrollWidth) { this.carrier.style.left = (pos.x + 15) + "px"; } else { this.carrier.style.left = (document.body.scrollWidth - this.carrier.clientWidth - 5) + "px"; } if (pos.y + this.carrier.clientHeight <= document.body.scrollHeight) { this.carrier.style.top = pos.y + "px"; } else { this.carrier.style.top = (document.body.scrollHeight - this.carrier.clientHeight - 5) + "px"; } } toggleActive() { if (this.active) { this.active = false; this.carrier.style.opacity = "0"; this.carrier.style.padding = "0"; } else { this.active = true; this.carrier.style.opacity = "1"; this.carrier.style.padding = SexyTooltip.carrierStyle.padding; } } transitionTooltipSize(text: string) { const testDiv = document.createElement("div"); testDiv.style.height = "auto"; testDiv.style.width = SexyTooltip.defaultWidth + "px"; testDiv.innerHTML = text; document.body.appendChild(testDiv); const calculatedHeight = testDiv.scrollHeight; testDiv.style.display = "inline"; testDiv.style.width = "auto"; document.body.removeChild(testDiv); const size = { height: calculatedHeight + "px", width: "350px" }; this.carrier.style.height = size.height; this.carrier.style.width = text === "" ? "0" : size.width; this.textCarrier.innerHTML = text; } getTooltipTextForHoveredElement(mousePos: Point): string { for (const elem of this.elementsWithTooltips) { const boundingRect = elem.getBoundingClientRect(); const inYRange = mousePos.y >= boundingRect.top + window.pageYOffset - 1 && mousePos.y <= boundingRect.bottom + window.pageYOffset + 1; const inXRange = mousePos.x >= boundingRect.left + window.pageXOffset - 1 && mousePos.x <= boundingRect.right + window.pageXOffset + 1; if (inYRange && inXRange) { return (elem.nextElementSibling as HTMLSpanElement)?.innerText ?? ""; } } return ""; } } export default SexyTooltip;