Refactor, again

Make collapsible components reusable
Add ability to collapse video rows in videos-grid style
Lose sanity while dealing with all the intricacies
This commit is contained in:
Svilen Markov 2024-05-17 12:15:44 +01:00
parent 8977114806
commit b0018c3f06
9 changed files with 190 additions and 88 deletions

View File

@ -125,22 +125,22 @@
padding-top: var(--list-half-gap); padding-top: var(--list-half-gap);
} }
.list-collapsible:not(.list-collapsible-expanded) > .list-collapsible-item { .collapsible-container:not(.container-expanded) > .collapsible-item {
display: none; display: none;
} }
.list-collapsible-item { .collapsible-item {
animation: listItemReveal 0.3s backwards; animation: collapsibleItemReveal .25s backwards;
} }
@keyframes listItemReveal { @keyframes collapsibleItemReveal {
from { from {
opacity: 0; opacity: 0;
transform: translateY(10px); transform: translateY(10px);
} }
} }
.list-collapsible-label { .expand-toggle-button {
font: inherit; font: inherit;
border: 0; border: 0;
cursor: pointer; cursor: pointer;
@ -154,19 +154,20 @@
background: var(--color-widget-background); background: var(--color-widget-background);
} }
.list-collapsible-label-expanded { .expand-toggle-button.container-expanded {
position: sticky; position: sticky;
bottom: 0; /* -1px to hide 1px gap on chrome */
bottom: -1px;
} }
.list-collapsible-label-icon { .expand-toggle-button-icon {
display: inline-block; display: inline-block;
margin-left: 1rem; margin-left: 1rem;
position: relative; position: relative;
top: -.2rem; top: -.2rem;
} }
.list-collapsible-label-icon::before { .expand-toggle-button-icon::before {
content: ''; content: '';
font-size: 0.8rem; font-size: 0.8rem;
transform: rotate(90deg); transform: rotate(90deg);
@ -175,14 +176,25 @@
transition: transform 0.3s; transition: transform 0.3s;
} }
.list-collapsible-label-expanded .list-collapsible-label-icon::before { .expand-toggle-button.container-expanded .expand-toggle-button-icon::before {
transform: rotate(-90deg); transform: rotate(-90deg);
} }
.widget-content:has(.list-collapsible-label:last-child) { .widget-content:has(.expand-toggle-button:last-child) {
padding-bottom: 0; padding-bottom: 0;
} }
.cards-grid.collapsible-container + .expand-toggle-button {
text-align: center;
margin-top: 0.5rem;
background-color: var(--color-background);
}
/* required to prevent collapsed lazy images from being loaded while the container is being setup */
.collapsible-container:not(.ready) img[loading=lazy] {
display: none;
}
::selection { ::selection {
background-color: hsl(var(--bghs), calc(var(--scheme) (var(--scheme) var(--bgl) + 20%))); background-color: hsl(var(--bghs), calc(var(--scheme) (var(--scheme) var(--bgl) + 20%)));
color: var(--color-text-highlight); color: var(--color-text-highlight);
@ -270,6 +282,7 @@ body {
gap: var(--widget-gap); gap: var(--widget-gap);
margin: var(--widget-gap) 0; margin: var(--widget-gap) 0;
animation: pageColumnsEntrance .3s cubic-bezier(0.25, 1, 0.5, 1) backwards; animation: pageColumnsEntrance .3s cubic-bezier(0.25, 1, 0.5, 1) backwards;
animation-delay: 3ms;
} }
@keyframes pageColumnsEntrance { @keyframes pageColumnsEntrance {
@ -1109,8 +1122,10 @@ body {
box-shadow: 0 calc(var(--spacing) * -1) 0 0 currentColor, 0 var(--spacing) 0 0 currentColor; box-shadow: 0 calc(var(--spacing) * -1) 0 0 currentColor, 0 var(--spacing) 0 0 currentColor;
} }
.list-collapsible-label-expanded { .expand-toggle-button.container-expanded {
bottom: var(--mobile-navigation-height); bottom: var(--mobile-navigation-height);
/* hides content that peeks through the rounded borders of the mobile navigation */
box-shadow: 0 var(--border-radius) 0 0 var(--color-background);
} }
} }

View File

@ -178,74 +178,158 @@ function setupLazyImages() {
}, 5); }, 5);
} }
function setupCollapsibleLists() { function attachExpandToggleButton(collapsibleContainer) {
const collapsibleListElements = document.getElementsByClassName("list-collapsible");
if (collapsibleListElements.length == 0) {
return;
}
const showMoreText = "Show more"; const showMoreText = "Show more";
const showLessText = "Show less"; const showLessText = "Show less";
const attachExpandToggleButton = (listElement) => { let expanded = false;
let expanded = false; const button = document.createElement("button");
const button = document.createElement("button"); const icon = document.createElement("span");
const arrowElement = document.createElement("span"); icon.classList.add("expand-toggle-button-icon");
arrowElement.classList.add("list-collapsible-label-icon"); const textNode = document.createTextNode(showMoreText);
const textNode = document.createTextNode(showMoreText); button.classList.add("expand-toggle-button");
button.classList.add("list-collapsible-label"); button.append(textNode, icon);
button.append(textNode, arrowElement); button.addEventListener("click", () => {
button.addEventListener("click", () => { expanded = !expanded;
expanded = !expanded;
if (expanded) { if (expanded) {
listElement.classList.add("list-collapsible-expanded"); collapsibleContainer.classList.add("container-expanded");
button.classList.add("list-collapsible-label-expanded"); button.classList.add("container-expanded");
textNode.nodeValue = showLessText; textNode.nodeValue = showLessText;
return;
}
const topBefore = button.getClientRects()[0].top;
collapsibleContainer.classList.remove("container-expanded");
button.classList.remove("container-expanded");
textNode.nodeValue = showMoreText;
const topAfter = button.getClientRects()[0].top;
if (topAfter > 0)
return;
window.scrollBy({
top: topAfter - topBefore,
behavior: "instant"
});
});
collapsibleContainer.after(button);
return button;
};
function setupCollapsibleLists() {
const collapsibleLists = document.querySelectorAll(".list.collapsible-container");
if (collapsibleLists.length == 0) {
return;
}
for (let i = 0; i < collapsibleLists.length; i++) {
const list = collapsibleLists[i];
if (list.dataset.collapseAfter === undefined) {
continue;
}
const collapseAfter = parseInt(list.dataset.collapseAfter);
if (collapseAfter == -1) {
continue;
}
if (list.children.length <= collapseAfter) {
continue;
}
attachExpandToggleButton(list);
for (let c = collapseAfter; c < list.children.length; c++) {
const child = list.children[c];
child.classList.add("collapsible-item");
child.style.animationDelay = ((c - collapseAfter) * 20).toString() + "ms";
}
list.classList.add("ready");
}
}
function setupCollapsibleGrids() {
const collapsibleGridElements = document.querySelectorAll(".cards-grid.collapsible-container");
if (collapsibleGridElements.length == 0) {
return;
}
for (let i = 0; i < collapsibleGridElements.length; i++) {
const gridElement = collapsibleGridElements[i];
if (gridElement.dataset.collapseAfterRows === undefined) {
continue;
}
const collapseAfterRows = parseInt(gridElement.dataset.collapseAfterRows);
if (collapseAfterRows == -1) {
continue;
}
const getCardsPerRow = () => {
return parseInt(getComputedStyle(gridElement).getPropertyValue('--cards-per-row'));
};
const button = attachExpandToggleButton(gridElement);
let cardsPerRow = 2;
const resolveCollapsibleItems = () => {
const hideItemsAfterIndex = cardsPerRow * collapseAfterRows;
if (hideItemsAfterIndex >= gridElement.children.length) {
button.style.display = "none";
} else {
button.style.removeProperty("display");
}
let row = 0;
for (let i = 0; i < gridElement.children.length; i++) {
const child = gridElement.children[i];
if (i >= hideItemsAfterIndex) {
child.classList.add("collapsible-item");
child.style.animationDelay = (row * 40).toString() + "ms";
if (i % cardsPerRow + 1 == cardsPerRow) {
row++;
}
} else {
child.classList.remove("collapsible-item");
child.style.removeProperty("animation-delay");
}
}
};
setTimeout(() => {
cardsPerRow = getCardsPerRow();
resolveCollapsibleItems();
gridElement.classList.add("ready");
}, 1);
window.addEventListener("resize", () => {
const newCardsPerRow = getCardsPerRow();
if (cardsPerRow == newCardsPerRow) {
return; return;
} }
const topBefore = button.getClientRects()[0].top; cardsPerRow = newCardsPerRow;
resolveCollapsibleItems();
listElement.classList.remove("list-collapsible-expanded");
button.classList.remove("list-collapsible-label-expanded");
textNode.nodeValue = showMoreText;
const topAfter = button.getClientRects()[0].top;
if (topAfter > 0)
return;
window.scrollBy({
top: topAfter - topBefore,
behavior: "instant"
});
}); });
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";
}
} }
} }
@ -264,6 +348,7 @@ async function setupPage() {
setupLazyImages(); setupLazyImages();
setupCarousels(); setupCarousels();
setupCollapsibleLists(); setupCollapsibleLists();
setupCollapsibleGrids();
setupDynamicRelativeTime(); setupDynamicRelativeTime();
} finally { } finally {
pageElement.classList.add("content-ready"); pageElement.classList.add("content-ready");

View File

@ -1,7 +1,7 @@
{{ template "widget-base.html" . }} {{ template "widget-base.html" . }}
{{ define "widget-content" }} {{ define "widget-content" }}
<ul class="list list-gap-14 list-collapsible" data-collapse-after="{{ .CollapseAfter }}"> <ul class="list list-gap-14 collapsible-container" data-collapse-after="{{ .CollapseAfter }}">
{{ range .Posts }} {{ range .Posts }}
<li> <li>
<div class="forum-post-list-item thumbnail-container"> <div class="forum-post-list-item thumbnail-container">

View File

@ -1,9 +1,9 @@
{{ template "widget-base.html" . }} {{ template "widget-base.html" . }}
{{ define "widget-content" }} {{ define "widget-content" }}
<ul class="list list-gap-10 list-collapsible"> <ul class="list list-gap-10 collapsible-container" data-collapse-after="{{ .CollapseAfter }}">
{{ range $i, $release := .Releases }} {{ range $i, $release := .Releases }}
<li {{ if shouldCollapse $i $.CollapseAfter }}class="list-collapsible-item" style="--animation-delay: {{ itemAnimationDelay $i $.CollapseAfter }};"{{ end }}> <li>
<a class="size-h4 block text-truncate color-primary-if-not-visited" href="{{ $release.NotesUrl }}" target="_blank" rel="noreferrer">{{ .Name }}</a> <a class="size-h4 block text-truncate color-primary-if-not-visited" href="{{ $release.NotesUrl }}" target="_blank" rel="noreferrer">{{ .Name }}</a>
<ul class="list-horizontal-text"> <ul class="list-horizontal-text">
<li {{ dynamicRelativeTimeAttrs $release.TimeReleased }}></li> <li {{ dynamicRelativeTimeAttrs $release.TimeReleased }}></li>
@ -15,7 +15,4 @@
</li> </li>
{{ end }} {{ end }}
</ul> </ul>
{{ if gt (len .Releases) $.CollapseAfter }}
<label class="list-collapsible-label"><input type="checkbox" autocomplete="off" class="list-collapsible-input"></label>
{{ end }}
{{ end }} {{ end }}

View File

@ -1,7 +1,7 @@
{{ template "widget-base.html" . }} {{ template "widget-base.html" . }}
{{ define "widget-content" }} {{ define "widget-content" }}
<ul class="list list-gap-14 list-collapsible" data-collapse-after="{{ .CollapseAfter }}"> <ul class="list list-gap-14 collapsible-container" data-collapse-after="{{ .CollapseAfter }}">
{{ range .Items }} {{ range .Items }}
<li> <li>
<a class="size-title-dynamic color-primary-if-not-visited" href="{{ .Link }}" target="_blank" rel="noreferrer">{{ .Title }}</a> <a class="size-title-dynamic color-primary-if-not-visited" href="{{ .Link }}" target="_blank" rel="noreferrer">{{ .Title }}</a>

View File

@ -1,7 +1,7 @@
{{ template "widget-base.html" . }} {{ template "widget-base.html" . }}
{{ define "widget-content" }} {{ define "widget-content" }}
<ul class="list list-gap-14 list-collapsible" data-collapse-after="{{ .CollapseAfter }}"> <ul class="list list-gap-14 collapsible-container" data-collapse-after="{{ .CollapseAfter }}">
{{ range .Channels }} {{ range .Channels }}
<li> <li>
<div class="{{ if .IsLive }}twitch-channel-live {{ end }}flex gap-10 items-start thumbnail-container"> <div class="{{ if .IsLive }}twitch-channel-live {{ end }}flex gap-10 items-start thumbnail-container">

View File

@ -1,7 +1,7 @@
{{ template "widget-base.html" . }} {{ template "widget-base.html" . }}
{{ define "widget-content" }} {{ define "widget-content" }}
<ul class="list list-gap-14 list-collapsible" data-collapse-after="{{ .CollapseAfter }}"> <ul class="list list-gap-14 collapsible-container" data-collapse-after="{{ .CollapseAfter }}">
{{ range .Categories }} {{ range .Categories }}
<li class="twitch-category thumbnail-container"> <li class="twitch-category thumbnail-container">
<div class="flex gap-10 items-center"> <div class="flex gap-10 items-center">

View File

@ -3,7 +3,7 @@
{{ define "widget-content-classes" }}widget-content-frameless{{ end }} {{ define "widget-content-classes" }}widget-content-frameless{{ end }}
{{ define "widget-content" }} {{ define "widget-content" }}
<div class="cards-grid"> <div class="cards-grid collapsible-container" data-collapse-after-rows="{{ .CollapseAfterRows }}">
{{ range .Videos }} {{ range .Videos }}
<div class="card widget-content-frame thumbnail-container"> <div class="card widget-content-frame thumbnail-container">
{{ template "video-card-contents" . }} {{ template "video-card-contents" . }}

View File

@ -10,12 +10,13 @@ import (
) )
type Videos struct { type Videos struct {
widgetBase `yaml:",inline"` widgetBase `yaml:",inline"`
Videos feed.Videos `yaml:"-"` Videos feed.Videos `yaml:"-"`
VideoUrlTemplate string `yaml:"video-url-template"` VideoUrlTemplate string `yaml:"video-url-template"`
Style string `yaml:"style"` Style string `yaml:"style"`
Channels []string `yaml:"channels"` CollapseAfterRows int `yaml:"collapse-after-rows"`
Limit int `yaml:"limit"` Channels []string `yaml:"channels"`
Limit int `yaml:"limit"`
} }
func (widget *Videos) Initialize() error { func (widget *Videos) Initialize() error {
@ -25,6 +26,10 @@ func (widget *Videos) Initialize() error {
widget.Limit = 25 widget.Limit = 25
} }
if widget.CollapseAfterRows == 0 || widget.CollapseAfterRows < -1 {
widget.CollapseAfterRows = 4
}
return nil return nil
} }