131 lines
4.7 KiB
TypeScript
131 lines
4.7 KiB
TypeScript
interface Point {
|
|
x: number;
|
|
y: number;
|
|
}
|
|
|
|
interface Size {
|
|
height: string;
|
|
width: string;
|
|
}
|
|
|
|
class SexyTooltip {
|
|
private static readonly carrierStyle: Partial<CSSStyleDeclaration> = {
|
|
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<CSSStyleDeclaration> = {
|
|
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;
|