Implement collapse/expand toggles and refine site tree navigation

This commit is contained in:
Simon Repp
2025-08-06 15:20:01 +02:00
parent 8e7fc3c237
commit dfeb789cdc
7 changed files with 183 additions and 38 deletions
+107 -18
View File
@@ -1,3 +1,5 @@
const chevronRightIcon = document.querySelector('#chevron_right_icon').content;
const browseButton = document.querySelector('button.browse');
const navigation = document.querySelector('header .navigation');
@@ -70,6 +72,10 @@ function initializeSiteTree() {
if (video.image) {
image = document.createElement('img');
image.src = rootPrefix + video.sitePath + video.image;
if (video.lqip) {
image.style.background = lqipToRadialGradient(video.lqip);
}
} else {
image = document.createElement('span');
image.classList.add('placeholder');
@@ -92,10 +98,24 @@ function initializeSiteTree() {
function initializePlaylist(playlist, ancestors = undefined) {
const traversal = ancestors ? [...ancestors, playlist] : [];
playlist.expandToggle = document.createElement('span');
const spanTitle = document.createElement('span');
spanTitle.textContent = ancestors
? traversal.map(ancestor => ancestor.title).join(' > ')
: playlist.title;
if (ancestors) {
for (const [index, ancestor] of traversal.entries()) {
if (index > 0) {
const separator = document.createElement('span');
separator.appendChild(chevronRightIcon.cloneNode(true));
separator.setAttribute('aria-label', '>');
spanTitle.appendChild(separator);
}
spanTitle.append(ancestor.title);
}
} else {
spanTitle.textContent = playlist.title;
}
const spanInfo = document.createElement('span');
spanInfo.textContent = NAVIGATION_JS_T.xxxVideos(playlist.videos.length);
@@ -103,9 +123,20 @@ function initializeSiteTree() {
playlist.element = document.createElement('a');
playlist.element.classList.add('playlist');
playlist.element.href = rootPrefix + playlist.sitePath + indexSuffix;
playlist.element.appendChild(playlist.expandToggle);
playlist.element.appendChild(spanTitle);
playlist.element.appendChild(spanInfo);
if (playlist.videos.length) {
playlist.expandToggle.appendChild(chevronRightIcon.cloneNode(true));
playlist.expandToggle.setAttribute('aria-label', NAVIGATION_JS_T.expand);
playlist.expandToggle.setAttribute('title', NAVIGATION_JS_T.expand);
playlist.expandToggle.addEventListener('click', event => {
collapseExpand(playlist.element);
event.preventDefault();
});
}
siteTreeElements.appendChild(playlist.element);
for (const video of playlist.videos) {
@@ -118,20 +149,44 @@ function initializeSiteTree() {
function initializeCollection(collection, ancestors = undefined) {
const traversal = ancestors ? [...ancestors, collection] : [];
const spanText = document.createElement('span');
spanText.textContent = ancestors
? traversal.map(ancestor => ancestor.title).join(' > ')
: collection.title;
collection.expandToggle = document.createElement('span');
const spanTitle = document.createElement('span');
if (ancestors) {
for (const [index, ancestor] of traversal.entries()) {
if (index > 0) {
const separator = document.createElement('span');
separator.appendChild(chevronRightIcon.cloneNode(true));
separator.setAttribute('aria-label', '>');
spanTitle.appendChild(separator);
}
spanTitle.append(ancestor.title);
}
} else {
spanTitle.textContent = collection.title;
}
collection.element = document.createElement('a');
collection.element.classList.add('collection');
collection.element.href = rootPrefix + collection.sitePath + indexSuffix;
collection.element.appendChild(spanText);
collection.element.appendChild(collection.expandToggle);
collection.element.appendChild(spanTitle);
if (collection.videos.length) {
const spanInfo = document.createElement('span');
spanInfo.textContent = NAVIGATION_JS_T.xxxVideos(collection.videos.length);
collection.element.appendChild(spanInfo);
collection.expandToggle.appendChild(chevronRightIcon.cloneNode(true));
collection.expandToggle.setAttribute('aria-label', NAVIGATION_JS_T.expand);
collection.expandToggle.setAttribute('title', NAVIGATION_JS_T.expand);
collection.expandToggle.addEventListener('click', event => {
collapseExpand(collection.element);
event.preventDefault();
});
}
siteTreeElements.appendChild(collection.element);
@@ -158,15 +213,13 @@ function initializeSiteTree() {
siteTreeInitialized = true;
}
// TODO: Currently unused but kept around in case we want to pick it up again
// (the lqip property is available as video.lqip in the siteContent tree structure).
// function lqipToRadialGradient(lqip) {
// const [ne, nw, se, sw] = lqip;
// return `radial-gradient(ellipse at top right, rgb(${ne[0]},${ne[1]},${ne[2]}), transparent),` +
// `radial-gradient(ellipse at top left, rgb(${nw[0]},${nw[1]},${nw[2]}), transparent),` +
// `radial-gradient(ellipse at bottom right, rgb(${se[0]},${se[1]},${se[2]}), transparent),` +
// `radial-gradient(ellipse at bottom left, rgb(${sw[0]},${sw[1]},${sw[2]}), transparent)`;
// }
function lqipToRadialGradient(lqip) {
const [ne, nw, se, sw] = lqip;
return `radial-gradient(ellipse at top right, rgb(${ne[0]},${ne[1]},${ne[2]}), transparent),` +
`radial-gradient(ellipse at top left, rgb(${nw[0]},${nw[1]},${nw[2]}), transparent),` +
`radial-gradient(ellipse at bottom right, rgb(${se[0]},${se[1]},${se[2]}), transparent),` +
`radial-gradient(ellipse at bottom left, rgb(${sw[0]},${sw[1]},${sw[2]}), transparent)`;
}
function toggleBrowse() {
function updateVideo(video) {
@@ -179,6 +232,9 @@ function toggleBrowse() {
}
playlist.element.classList.add('visible');
playlist.element.classList.remove('expanded');
playlist.expandToggle.setAttribute('aria-label', NAVIGATION_JS_T.expand);
playlist.expandToggle.setAttribute('title', NAVIGATION_JS_T.expand);
}
function updateCollection(collection) {
@@ -195,6 +251,9 @@ function toggleBrowse() {
}
collection.element.classList.add('visible');
collection.element.classList.remove('expanded');
collection.expandToggle.setAttribute('aria-label', NAVIGATION_JS_T.expand);
collection.expandToggle.setAttribute('title', NAVIGATION_JS_T.expand);
}
siteTreeStatus.textContent = '';
@@ -240,6 +299,9 @@ function updateSearch() {
display = updateVideo(video) || display;
}
playlist.element.classList.remove('expanded');
playlist.expandToggle.setAttribute('aria-label', NAVIGATION_JS_T.collapse);
playlist.expandToggle.setAttribute('title', NAVIGATION_JS_T.collapse);
playlist.element.classList.toggle('visible', display);
}
@@ -259,6 +321,9 @@ function updateSearch() {
updateCollection(subcollection);
}
collection.element.classList.remove('expanded');
collection.expandToggle.setAttribute('aria-label', NAVIGATION_JS_T.collapse);
collection.expandToggle.setAttribute('title', NAVIGATION_JS_T.collapse);
collection.element.classList.toggle('visible', display);
}
@@ -351,7 +416,7 @@ navigation.addEventListener('keydown', event => {
} else if (event.key === ' ') {
if (event.target.classList.contains('collection') ||
event.target.classList.contains('playlist')) {
collapseExpand(event.target, undefined);
collapseExpand(event.target);
event.preventDefault();
} else if (event.target.classList.contains('video')) {
event.preventDefault();
@@ -380,15 +445,27 @@ navigation.addEventListener('keydown', event => {
function collapseExpand(siteTreeElement, expand = undefined) {
function visitPlaylist(playlist) {
if (playlist.element === siteTreeElement) {
if (expand === undefined) {
expand = !playlist.element.classList.contains('expanded');
}
for (const video of playlist.videos) {
video.element.classList.toggle('visible', expand);
}
playlist.element.classList.toggle('expanded', expand);
const expandOrCollapse = NAVIGATION_JS_T[expand ? 'collapse' : 'expand'];
playlist.expandToggle.setAttribute('aria-label', expandOrCollapse);
playlist.expandToggle.setAttribute('title', expandOrCollapse);
return true;
} else if (!expand && playlist.videos.some(video => video.element === siteTreeElement)) {
for (const video of playlist.videos) {
video.element.classList.remove('visible');
}
playlist.element.classList.remove('expanded');
playlist.expandToggle.setAttribute('aria-label', NAVIGATION_JS_T.expand);
playlist.expandToggle.setAttribute('title', NAVIGATION_JS_T.expand);
playlist.element.focus();
return true;
@@ -399,15 +476,27 @@ function collapseExpand(siteTreeElement, expand = undefined) {
function visitCollection(collection) {
if (collection.element === siteTreeElement) {
if (expand === undefined) {
expand = !collection.element.classList.contains('expanded');
}
for (const video of collection.videos) {
video.element.classList.toggle('visible', expand);
}
collection.element.classList.toggle('expanded', expand);
const expandOrCollapse = NAVIGATION_JS_T[expand ? 'collapse' : 'expand'];
collection.expandToggle.setAttribute('aria-label', expandOrCollapse);
collection.expandToggle.setAttribute('title', expandOrCollapse);
return true;
} else if (!expand && collection.videos.some(video => video.element === siteTreeElement)) {
for (const video of collection.videos) {
video.element.classList.remove('visible');
}
collection.element.classList.remove('expanded');
collection.expandToggle.setAttribute('aria-label', NAVIGATION_JS_T.expand);
collection.expandToggle.setAttribute('title', NAVIGATION_JS_T.expand);
collection.element.focus();
return true;
+48 -7
View File
@@ -506,6 +506,10 @@ img {
top: 3.5rem;
z-index: 2;
}
.site_tree [role="status"]:not(:empty) {
font-weight: 500;
padding: .5rem;
}
.site_tree .elements {
display: flex;
flex-direction: column;
@@ -514,9 +518,7 @@ img {
.site_tree .elements a {
align-items: center;
border-radius: .2rem;
column-gap: .7rem;
display: flex;
padding: .5rem;
}
.site_tree .elements a:focus-visible,
.site_tree .elements a:hover {
@@ -525,19 +527,58 @@ img {
outline: none;
}
.site_tree .elements a:not(.visible) { display: none; }
.site_tree .elements .collection,
.site_tree .elements .playlist {
column-gap: .3rem;
.site_tree .elements .collection > :nth-child(1),
.site_tree .elements .playlist > :nth-child(1) {
align-items: center;
display: flex;
height: 1rem;
justify-content: center;
padding: .5rem;
width: 1rem;
}
.site_tree .elements .collection > :nth-child(1) svg,
.site_tree .elements .playlist > :nth-child(1) svg {
border-radius: 50%;
color: var(--fg-1);
font-size: .6rem;
padding: .2rem;
}
.site_tree .elements .collection > :nth-child(1):hover svg,
.site_tree .elements .playlist > :nth-child(1):hover svg {
background: var(--fg-3);
color: var(--bg-1);
}
.site_tree .elements .collection.expanded > :nth-child(1) svg,
.site_tree .elements .playlist.expanded > :nth-child(1) svg {
transform: rotate(90deg);
}
.site_tree .elements .collection > :nth-child(2),
.site_tree .elements .playlist > :nth-child(2) {
color: var(--fg-3);
align-items: center;
column-gap: .3rem;
display: flex;
}
.site_tree .elements .collection > :nth-child(2) svg,
.site_tree .elements .playlist > :nth-child(2) svg {
color: var(--mg);
font-size: .5rem;
}
.site_tree .elements .collection > :nth-child(3),
.site_tree .elements .playlist > :nth-child(3) {
color: var(--fg-3);
margin-left: .3rem;
}
.site_tree .elements .video { padding: .5rem; }
.site_tree .elements .video > :nth-child(1) {
aspect-ratio: var(--poster-aspect);
border-radius: .2rem;
height: 3rem;
margin-left: 1.5rem;
}
.site_tree .elements .video > :nth-child(2) {
flex: 1;
margin-left: .7rem;
}
.site_tree .elements .video > :nth-child(2) { flex: 1; }
.site_tree .elements .placeholder { background: var(--bg-3); }
.site_tree:not(.open) { display: none; }
#speed .x { font-size: .8em; }
+11 -11
View File
@@ -102,6 +102,8 @@ impl Layout {
String::new()
};
let mut templates = String::new();
let browse;
let search;
let navigation_script;
@@ -131,6 +133,15 @@ impl Layout {
</div>
"#);
navigation_script = format!(r#"<script defer src="{root_prefix}navigation.js?{navigation_js_asset_hash}"></script>"#);
let chevron_right_icon = icons::CHEVRON_RIGHT;
let navigation_templates = formatdoc!(r#"
<template id="chevron_right_icon">
{chevron_right_icon}
</template>
"#);
templates.push_str(&navigation_templates);
} else {
browse = String::new();
search = String::new();
@@ -152,15 +163,6 @@ impl Layout {
</head>
"#);
let breadcrumbs = traversal.parents(site)
.iter()
.map(|breadcrumb| {
let breadcrumb_rendered = breadcrumb.step.breadcrumb(&breadcrumb.href, site);
format!(r#"{breadcrumb_rendered} <span style="font-size: 1.6rem; pointer-events: none; position: relative; top: -.2rem;"></span> "#)
})
.collect::<Vec<String>>()
.join("");
let dark_icon = icons::dark(translations.dark_color_scheme);
let light_icon = icons::light(translations.light_color_scheme);
let theme_toggle = match &site.theme.kind {
@@ -175,8 +177,6 @@ impl Layout {
let version_display = env!("HYPER8_VERSION_DISPLAY");
let mut templates = String::new();
if self.clipboard_script {
templates.push_str(&copy_button_icon_templates(translations));
}
-2
View File
@@ -731,8 +731,6 @@ impl TraversalStep<'_> {
// String::from(r#"<div class="placeholder"></div>"#)
// };
let label = playlist.title_or_slug_or_generic_label(translations);
if let Some(video) = video {
let index_suffix = site.index_suffix();
+4
View File
@@ -142,11 +142,15 @@ pub fn generate_navigation_js(
};
if let Some(site_content) = site_content {
let t_collapse = translations.collapse;
let t_expand = translations.expand;
let t_nothing_found_for_xxx = js_escape_inside_single_quoted_string(translations.nothing_found_for_xxx);
let t_showing_xxx_results_for_xxx = js_escape_inside_single_quoted_string(translations.showing_xxx_results_for_xxx);
let t_xxx_videos = js_escape_inside_single_quoted_string(translations.xxx_videos);
let mut js = formatdoc!(r#"
const NAVIGATION_JS_T = {{
collapse: '{t_collapse}',
expand: '{t_expand}',
nothingFoundForXxx: query => '{t_nothing_found_for_xxx}'.replace('{{query}}', query),
showingXxxResultsForXxx: (count, query) => '{t_showing_xxx_results_for_xxx}'.replace('{{count}}', count).replace('{{query}}', query),
xxxVideos: count => '{t_xxx_videos}'.replace('{{count}}', count)
+7
View File
@@ -20,6 +20,13 @@ pub const ATOM: &str = indoc!(r#"
</svg>
"#);
/// A rightwards-facing, perfectly perpendicular chevron (like a closing angled bracket with a 90 degrees angle)
pub const CHEVRON_RIGHT: &str = indoc!(r#"
<svg aria-hidden="true" width="1em" height="1em" version="1.1" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
<path d="m15.587 56.681 7.3191 7.3191 32-32-32-32-7.3191 7.3191 24.681 24.681z"/>
</svg>
"#);
/// The stylized string "</>"
pub const CODE: &str = indoc!(r#"
<svg aria-hidden="true" width="1em" height="1em" version="1.1" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
+6
View File
@@ -36,6 +36,7 @@ pub struct Translations {
pub clean_urls_label: &'static str,
pub close: &'static str,
pub codec: &'static str,
pub collapse: &'static str,
pub collection: &'static str,
pub collection_manifest: &'static str,
pub collection_settings: &'static str,
@@ -100,6 +101,7 @@ pub struct Translations {
pub encoding_approach_label: &'static str,
pub enabled: &'static str,
pub error: &'static str,
pub expand: &'static str,
pub external_link: &'static str,
pub failed: &'static str,
pub feed: &'static str,
@@ -323,6 +325,7 @@ impl Translations {
clean_urls_label: "Dekorative URLs (example.com/xyz/ statt example.com/xyz/index.html)",
close: "Schließen",
codec: "Codec",
collapse: "Zuklappen",
collection: "Collection",
collection_manifest: "Collection Manifest",
collection_settings: "Collection Einstellungen",
@@ -380,6 +383,7 @@ impl Translations {
enabled: "Aktiviert",
encoding_approach_label: "Encoding Ansatz (Wähle den langsamsten für den du Zeit hast)",
error: "Fehler",
expand: "Aufklappen",
external_link: "Externer Link",
failed: "Fehlgeschlagen",
feed: "Feed",
@@ -615,6 +619,7 @@ impl Translations {
clean_urls_label: "Clean URLs (example.com/xyz/ instead of example.com/xyz/index.html)",
close: "Close",
codec: "Codec",
collapse: "Collapse",
collection: "Collection",
collection_manifest: "Collection manifest",
collection_settings: "Collection settings",
@@ -672,6 +677,7 @@ impl Translations {
enabled: "Enabled",
encoding_approach_label: "Encoding Approach (Use the slowest one you have time for)",
error: "Error",
expand: "Expand",
external_link: "External Link",
failed: "Failed",
feed: "Feed",