diff --git a/internal/assets/static/main.css b/internal/assets/static/main.css index 983fd1a..05108e0 100644 --- a/internal/assets/static/main.css +++ b/internal/assets/static/main.css @@ -57,6 +57,14 @@ font-size: var(--font-size-h4); } +.page-content, .page.content-ready .page-loading-container { + display: none; +} + +.page.content-ready > .page-content { + display: block; +} + .page-column-full .size-title-dynamic { font-size: var(--font-size-h3); } @@ -117,6 +125,14 @@ padding-top: var(--list-half-gap); } +.list-collapsible:not(.list-collapsible-expanded) > .list-collapsible-item { + display: none; +} + +.list-collapsible-item { + animation: listItemReveal 0.3s backwards; +} + @keyframes listItemReveal { from { opacity: 0; @@ -124,56 +140,42 @@ } } -.list-collapsible-item { - display: none; - animation: listItemReveal 0.3s backwards; - animation-delay: var(--animation-delay); -} - .list-collapsible-label { - display: flex; - align-items: center; - gap: 1rem; + font: inherit; + border: 0; + cursor: pointer; + display: block; + width: 100%; + text-align: left; + color: var(--color-text-base); + text-transform: uppercase; + font-size: var(--font-size-h4); padding: var(--widget-content-vertical-padding) 0; background: var(--color-widget-background); } -.list-collapsible-label:has(.list-collapsible-input:checked) { +.list-collapsible-label-expanded { position: sticky; bottom: 0; } -.list-collapsible:has(+ .list-collapsible-label > .list-collapsible-input:checked) .list-collapsible-item { - display: block; +.list-collapsible-label-icon { + display: inline-block; + margin-left: 1rem; + position: relative; + top: -.2rem; } -.list-collapsible-input { - display: none; -} - -.list-collapsible-label::before, .list-collapsible-label::after { - cursor: pointer; - display: block; -} - -.list-collapsible-label::before { - content: 'SHOW MORE'; - font-size: var(--font-size-h4); -} - -.list-collapsible-label:has(.list-collapsible-input:checked)::before { - content: 'SHOW LESS'; -} - -.list-collapsible-label::after { +.list-collapsible-label-icon::before { content: ''; font-size: 0.8rem; transform: rotate(90deg); line-height: 1; + display: inline-block; transition: transform 0.3s; } -.list-collapsible-label:has(.list-collapsible-input:checked)::after { +.list-collapsible-label-expanded .list-collapsible-label-icon::before { transform: rotate(-90deg); } @@ -1107,7 +1109,7 @@ body { box-shadow: 0 calc(var(--spacing) * -1) 0 0 currentColor, 0 var(--spacing) 0 0 currentColor; } - .list-collapsible-label:has(.list-collapsible-input:checked) { + .list-collapsible-label-expanded { bottom: var(--mobile-navigation-height); } } diff --git a/internal/assets/static/main.js b/internal/assets/static/main.js index 7045c51..5f6827b 100644 --- a/internal/assets/static/main.js +++ b/internal/assets/static/main.js @@ -21,7 +21,7 @@ function throttledDebounce(callback, maxDebounceTimes, debounceDelay) { }; -async function fetchPageContents (pageSlug) { +async function fetchPageContent(pageSlug) { // TODO: handle non 200 status codes/time outs // TODO: add retries const response = await fetch(`/api/pages/${pageSlug}/content/`); @@ -33,6 +33,10 @@ async function fetchPageContents (pageSlug) { function setupCarousels() { const carouselElements = document.getElementsByClassName("carousel-container"); + if (carouselElements.length == 0) { + return; + } + for (let i = 0; i < carouselElements.length; i++) { const carousel = carouselElements[i]; carousel.classList.add("show-right-cutoff"); @@ -57,7 +61,7 @@ function setupCarousels() { itemsContainer.addEventListener("scroll", determineSideCutoffsRateLimited); document.addEventListener("resize", determineSideCutoffsRateLimited); - determineSideCutoffs(); + setTimeout(determineSideCutoffs, 1); } } @@ -108,6 +112,8 @@ function setupDynamicRelativeTime() { const updateInterval = 60 * 1000; let lastUpdateTime = Date.now(); + updateRelativeTimeForElements(elements); + const updateElementsAndTimestamp = () => { updateRelativeTimeForElements(elements); lastUpdateTime = Date.now(); @@ -154,35 +160,101 @@ function setupLazyImages() { image.classList.add("finished-transition"); } - for (let i = 0; i < images.length; i++) { - const image = images[i]; + setTimeout(() => { + for (let i = 0; i < images.length; i++) { + const image = images[i]; - if (image.complete) { - image.classList.add("cached"); - setTimeout(() => imageFinishedTransition(image), 5); - } else { - // TODO: also handle error event - image.addEventListener("load", () => { - image.classList.add("loaded"); - setTimeout(() => imageFinishedTransition(image), 500); - }); + if (image.complete) { + image.classList.add("cached"); + setTimeout(() => imageFinishedTransition(image), 5); + } else { + // TODO: also handle error event + image.addEventListener("load", () => { + image.classList.add("loaded"); + setTimeout(() => imageFinishedTransition(image), 500); + }); + } + } + }, 5); +} + +function setupCollapsibleLists() { + const collapsibleListElements = document.getElementsByClassName("list-collapsible"); + + if (collapsibleListElements.length == 0) { + return; + } + + const showMoreText = "Show more"; + const showLessText = "Show less"; + + const attachExpandToggleButton = (listElement) => { + let expanded = false; + const button = document.createElement("button"); + const arrowElement = document.createElement("span"); + arrowElement.classList.add("list-collapsible-label-icon"); + const textNode = document.createTextNode(showMoreText); + button.classList.add("list-collapsible-label"); + button.append(textNode, arrowElement); + button.addEventListener("click", () => { + if (expanded) { + listElement.classList.remove("list-collapsible-expanded"); + button.classList.remove("list-collapsible-label-expanded"); + textNode.nodeValue = showMoreText; + } else { + listElement.classList.add("list-collapsible-expanded"); + button.classList.add("list-collapsible-label-expanded"); + textNode.nodeValue = showLessText; + } + + expanded = !expanded; + }); + + listElement.after(button); + }; + + for (let i = 0; i < collapsibleListElements.length; i++) { + const listElement = collapsibleListElements[i]; + + if (listElement.dataset.collapseAfter === undefined) { + continue; + } + + const collapseAfter = parseInt(listElement.dataset.collapseAfter); + + if (listElement.children.length <= collapseAfter) { + continue; + } + + attachExpandToggleButton(listElement); + + for (let c = collapseAfter; c < listElement.children.length; c++) { + const child = listElement.children[c]; + child.classList.add("list-collapsible-item"); + child.style.animationDelay = ((c - collapseAfter) * 20).toString() + "ms"; } } } async function setupPage() { const pageElement = document.getElementById("page"); - const pageContents = await fetchPageContents(pageData.slug); + const pageContentElement = document.getElementById("page-content"); + const pageContent = await fetchPageContent(pageData.slug); - pageElement.innerHTML = pageContents; + pageContentElement.innerHTML = pageContent; setTimeout(() => { document.body.classList.add("animate-element-transition"); }, 200); - setTimeout(setupLazyImages, 5); - setupCarousels(); - setupDynamicRelativeTime(); + try { + setupLazyImages(); + setupCarousels(); + setupCollapsibleLists(); + setupDynamicRelativeTime(); + } finally { + pageElement.classList.add("content-ready"); + } } if (document.readyState === "loading") { diff --git a/internal/assets/templates/forum-posts.html b/internal/assets/templates/forum-posts.html index 32f7076..fb1fed5 100644 --- a/internal/assets/templates/forum-posts.html +++ b/internal/assets/templates/forum-posts.html @@ -1,14 +1,14 @@ {{ template "widget-base.html" . }} {{ define "widget-content" }} -