mirror of
https://codeberg.org/simonrepp/hyper8.git
synced 2026-08-14 13:45:27 +02:00
Implement collapse/expand toggles and refine site tree navigation
This commit is contained in:
+107
-18
@@ -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;
|
||||
|
||||
@@ -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
@@ -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(©_button_icon_templates(translations));
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user