glance/internal/assets/static/js/popover.js

173 lines
6.5 KiB
JavaScript
Raw Normal View History

2024-08-19 03:15:22 +02:00
const defaultShowDelayMs = 200;
const defaultHideDelayMs = 500;
const defaultMaxWidth = "300px";
const defaultDistanceFromTarget = "0px"
const htmlContentSelector = "[data-popover-html]";
let activeTarget = null;
let pendingTarget = null;
let cleanupOnHidePopover = null;
let togglePopoverTimeout = null;
const containerElement = document.createElement("div");
const containerComputedStyle = getComputedStyle(containerElement);
containerElement.addEventListener("mouseenter", clearTogglePopoverTimeout);
containerElement.addEventListener("mouseleave", handleMouseLeave);
containerElement.classList.add("popover-container");
const frameElement = document.createElement("div");
frameElement.classList.add("popover-frame");
const contentElement = document.createElement("div");
contentElement.classList.add("popover-content");
frameElement.append(contentElement);
containerElement.append(frameElement);
document.body.append(containerElement);
const observer = new ResizeObserver(repositionContainer);
function handleMouseEnter(event) {
clearTogglePopoverTimeout();
const target = event.target;
pendingTarget = target;
const showDelay = target.dataset.popoverShowDelay || defaultShowDelayMs;
if (activeTarget !== null) {
if (activeTarget !== target) {
hidePopover();
2024-08-20 14:29:44 +02:00
requestAnimationFrame(showPopover);
2024-08-19 03:15:22 +02:00
}
return;
}
togglePopoverTimeout = setTimeout(showPopover, showDelay);
}
function handleMouseLeave(event) {
clearTogglePopoverTimeout();
const target = activeTarget || event.target;
togglePopoverTimeout = setTimeout(hidePopover, target.dataset.popoverHideDelay || defaultHideDelayMs);
}
function clearTogglePopoverTimeout() {
clearTimeout(togglePopoverTimeout);
}
function showPopover() {
activeTarget = pendingTarget;
pendingTarget = null;
const popoverType = activeTarget.dataset.popoverType;
const contentMaxWidth = activeTarget.dataset.popoverMaxWidth || defaultMaxWidth;
if (popoverType === "text") {
const text = activeTarget.dataset.popoverText;
if (text === undefined || text === "") return;
contentElement.textContent = text;
} else if (popoverType === "html") {
const htmlContent = activeTarget.querySelector(htmlContentSelector);
if (htmlContent === null) return;
/**
* The reason for all of the below shenanigans is that I want to preserve
* all attached event listeners of the original HTML content. This is so I don't have to
* re-setup events for things like lazy images, they'd just work as expected.
*/
const placeholder = document.createComment("");
htmlContent.replaceWith(placeholder);
contentElement.replaceChildren(htmlContent);
htmlContent.removeAttribute("data-popover-html");
cleanupOnHidePopover = () => {
htmlContent.setAttribute("data-popover-html", "");
placeholder.replaceWith(htmlContent);
placeholder.remove();
};
} else {
return;
}
contentElement.style.maxWidth = contentMaxWidth;
containerElement.style.display = "block";
activeTarget.classList.add("popover-active");
document.addEventListener("keydown", handleHidePopoverOnEscape);
window.addEventListener("resize", repositionContainer);
observer.observe(containerElement);
}
function repositionContainer() {
2024-08-20 14:29:44 +02:00
const targetBounds = activeTarget.getBoundingClientRect();
2024-08-19 03:15:22 +02:00
const containerBounds = containerElement.getBoundingClientRect();
const containerInlinePadding = parseInt(containerComputedStyle.getPropertyValue("padding-inline"));
2024-08-20 14:29:44 +02:00
const activeTargetBoundsWidthOffset = targetBounds.width * (activeTarget.dataset.popoverOffset || 0.5);
const position = activeTarget.dataset.popoverPosition || "below";
const left = targetBounds.left + activeTargetBoundsWidthOffset - (containerBounds.width / 2) + 1;
2024-08-19 03:15:22 +02:00
if (left < 0) {
containerElement.style.left = 0;
containerElement.style.removeProperty("right");
2024-08-20 14:29:44 +02:00
containerElement.style.setProperty("--triangle-offset", targetBounds.left - containerInlinePadding + activeTargetBoundsWidthOffset + "px");
2024-08-19 03:15:22 +02:00
} else if (left + containerBounds.width > window.innerWidth) {
containerElement.style.removeProperty("left");
containerElement.style.right = 0;
2024-08-20 14:29:44 +02:00
containerElement.style.setProperty("--triangle-offset", containerBounds.width - containerInlinePadding - (window.innerWidth - targetBounds.left - activeTargetBoundsWidthOffset) + "px");
2024-08-19 03:15:22 +02:00
} else {
containerElement.style.removeProperty("right");
containerElement.style.left = left + "px";
containerElement.style.removeProperty("--triangle-offset");
}
2024-08-20 14:29:44 +02:00
const distanceFromTarget = activeTarget.dataset.popoverMargin || defaultDistanceFromTarget;
const topWhenAbove = targetBounds.top + window.scrollY - containerBounds.height;
const topWhenBelow = targetBounds.top + window.scrollY + targetBounds.height;
if (
position === "above" && topWhenAbove > window.scrollY ||
(position === "below" && topWhenBelow + containerBounds.height > window.scrollY + window.innerHeight)
) {
containerElement.classList.add("position-above");
frameElement.style.removeProperty("margin-top");
frameElement.style.marginBottom = distanceFromTarget;
containerElement.style.top = topWhenAbove + "px";
} else {
containerElement.classList.remove("position-above");
frameElement.style.removeProperty("margin-bottom");
frameElement.style.marginTop = distanceFromTarget;
containerElement.style.top = topWhenBelow + "px";
}
2024-08-19 03:15:22 +02:00
}
function hidePopover() {
if (activeTarget === null) return;
activeTarget.classList.remove("popover-active");
containerElement.style.display = "none";
document.removeEventListener("keydown", handleHidePopoverOnEscape);
window.removeEventListener("resize", repositionContainer);
observer.unobserve(containerElement);
if (cleanupOnHidePopover !== null) {
cleanupOnHidePopover();
cleanupOnHidePopover = null;
}
activeTarget = null;
}
function handleHidePopoverOnEscape(event) {
if (event.key === "Escape") {
hidePopover();
}
}
export function setupPopovers() {
const targets = document.querySelectorAll("[data-popover-type]");
for (let i = 0; i < targets.length; i++) {
const target = targets[i];
target.addEventListener("mouseenter", handleMouseEnter);
target.addEventListener("mouseleave", handleMouseLeave);
}
}