mirror of
https://github.com/glanceapp/glance.git
synced 2025-06-21 18:31:24 +02: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) {
|
function throttledDebounce(callback, maxDebounceTimes, debounceDelay) {
|
||||||
let debounceTimeout;
|
let debounceTimeout;
|
||||||
let timesDebounced = 0;
|
let timesDebounced = 0;
|
||||||
@ -593,6 +595,7 @@ async function setupPage() {
|
|||||||
pageContentElement.innerHTML = pageContent;
|
pageContentElement.innerHTML = pageContent;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
setupPopovers();
|
||||||
setupClocks()
|
setupClocks()
|
||||||
setupCarousels();
|
setupCarousels();
|
||||||
setupSearchBoxes();
|
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-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-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-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));
|
--ths: var(--bgh), calc(var(--bgs) * var(--tsm));
|
||||||
--color-text-base: hsl(var(--ths), calc(var(--scheme) var(--cm) * 58%));
|
--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);
|
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 {
|
.content-bounds {
|
||||||
max-width: 1600px;
|
max-width: 1600px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
<link rel="manifest" href="{{ .App.AssetPath "manifest.json" }}">
|
<link rel="manifest" href="{{ .App.AssetPath "manifest.json" }}">
|
||||||
<link rel="icon" type="image/png" href="{{ .App.AssetPath "favicon.png" }}" />
|
<link rel="icon" type="image/png" href="{{ .App.AssetPath "favicon.png" }}" />
|
||||||
<link rel="stylesheet" href="{{ .App.AssetPath "main.css" }}">
|
<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 }}
|
{{ block "document-head-after" . }}{{ end }}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user