mirror of
https://github.com/glanceapp/glance.git
synced 2025-02-16 10:29:41 +01:00
Add popover functionality
This commit is contained in:
parent
b4a4df480e
commit
83c7c4a14a
@ -1,3 +1,5 @@
|
||||
import { setupPopovers } from './popover.js';
|
||||
|
||||
function throttledDebounce(callback, maxDebounceTimes, debounceDelay) {
|
||||
let debounceTimeout;
|
||||
let timesDebounced = 0;
|
||||
@ -593,6 +595,7 @@ async function setupPage() {
|
||||
pageContentElement.innerHTML = pageContent;
|
||||
|
||||
try {
|
||||
setupPopovers();
|
||||
setupClocks()
|
||||
setupCarousels();
|
||||
setupSearchBoxes();
|
155
internal/assets/static/js/popover.js
Normal file
155
internal/assets/static/js/popover.js
Normal file
@ -0,0 +1,155 @@
|
||||
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();
|
||||
setTimeout(showPopover, 5);
|
||||
}
|
||||
|
||||
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() {
|
||||
const activeTargetBounds = activeTarget.getBoundingClientRect();
|
||||
const containerBounds = containerElement.getBoundingClientRect();
|
||||
const containerInlinePadding = parseInt(containerComputedStyle.getPropertyValue("padding-inline"));
|
||||
const activeTargetBoundsWidthOffset = activeTargetBounds.width * (activeTarget.dataset.popoverOffset || 0.5);
|
||||
const left = activeTargetBounds.left + activeTargetBoundsWidthOffset - (containerBounds.width / 2);
|
||||
|
||||
if (left < 0) {
|
||||
containerElement.style.left = 0;
|
||||
containerElement.style.removeProperty("right");
|
||||
containerElement.style.setProperty("--triangle-offset", activeTargetBounds.left - containerInlinePadding + activeTargetBoundsWidthOffset + "px");
|
||||
} else if (left + containerBounds.width > window.innerWidth) {
|
||||
containerElement.style.removeProperty("left");
|
||||
containerElement.style.right = 0;
|
||||
containerElement.style.setProperty("--triangle-offset", containerBounds.width - containerInlinePadding - (window.innerWidth - activeTargetBounds.left - activeTargetBoundsWidthOffset) + "px");
|
||||
} else {
|
||||
containerElement.style.removeProperty("right");
|
||||
containerElement.style.left = left + "px";
|
||||
containerElement.style.removeProperty("--triangle-offset");
|
||||
}
|
||||
|
||||
frameElement.style.marginTop = activeTarget.dataset.popoverMargin || defaultDistanceFromTarget;
|
||||
containerElement.style.top = activeTargetBounds.top + window.scrollY + activeTargetBounds.height + "px";
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
@ -34,6 +34,8 @@
|
||||
--color-separator: hsl(var(--bghs), calc(var(--scheme) ((var(--scheme) var(--bgl)) + 4% * var(--cm))));
|
||||
--color-widget-content-border: hsl(var(--bghs), calc(var(--scheme) (var(--scheme) var(--bgl) + 4%)));
|
||||
--color-widget-background-highlight: hsl(var(--bghs), calc(var(--scheme) (var(--scheme) var(--bgl) + 4%)));
|
||||
--color-popover-background: hsl(var(--bgh), calc(var(--bgs) + 3%), calc(var(--bgl) + 2%));
|
||||
--color-popover-border: hsl(var(--bghs), calc(var(--scheme) (var(--scheme) var(--bgl) + 7%)));
|
||||
|
||||
--ths: var(--bgh), calc(var(--bgs) * var(--tsm));
|
||||
--color-text-base: hsl(var(--ths), calc(var(--scheme) var(--cm) * 58%));
|
||||
@ -436,6 +438,50 @@ kbd:active {
|
||||
box-shadow: 0 0 0 0 var(--color-widget-background-highlight);
|
||||
}
|
||||
|
||||
.popover-container, [data-popover-html] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.popover-container {
|
||||
--triangle-size: 10px;
|
||||
--triangle-offset: 50%;
|
||||
z-index: 20;
|
||||
position: absolute;
|
||||
padding-top: calc(var(--triangle-size) + 3px);
|
||||
padding-inline: var(--content-bounds-padding);
|
||||
}
|
||||
|
||||
.popover-frame {
|
||||
position: relative;
|
||||
padding: 10px;
|
||||
background: var(--color-popover-background);
|
||||
border: 1px solid var(--color-popover-border);
|
||||
border-radius: 5px;
|
||||
animation: popoverFrameEntrance 0.3s backwards cubic-bezier(0.16, 1, 0.3, 1);
|
||||
box-shadow: 0 15px 30px -5px hsla(var(--bghs), calc(var(--bgl) * 0.2), 0.5);
|
||||
}
|
||||
|
||||
.popover-frame::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: var(--triangle-size);
|
||||
height: var(--triangle-size);
|
||||
transform: rotate(45deg);
|
||||
background-color: var(--color-popover-background);
|
||||
border-top-left-radius: 4px;
|
||||
border-left: 1px solid var(--color-popover-border);
|
||||
border-top: 1px solid var(--color-popover-border);
|
||||
left: calc(var(--triangle-offset) - (var(--triangle-size) / 2));
|
||||
top: calc(var(--triangle-size) / 2 * -1 - 1px);
|
||||
}
|
||||
|
||||
@keyframes popoverFrameEntrance {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
}
|
||||
|
||||
.content-bounds {
|
||||
max-width: 1600px;
|
||||
width: 100%;
|
||||
|
@ -16,7 +16,7 @@
|
||||
<link rel="manifest" href="{{ .App.AssetPath "manifest.json" }}">
|
||||
<link rel="icon" type="image/png" href="{{ .App.AssetPath "favicon.png" }}" />
|
||||
<link rel="stylesheet" href="{{ .App.AssetPath "main.css" }}">
|
||||
<script type="module" src="{{ .App.AssetPath "main.js" }}"></script>
|
||||
<script type="module" src="{{ .App.AssetPath "js/main.js" }}"></script>
|
||||
{{ block "document-head-after" . }}{{ end }}
|
||||
</head>
|
||||
<body>
|
||||
|
Loading…
Reference in New Issue
Block a user