accordion view (fix #117)

This commit is contained in:
DrMaxNix 2024-07-28 17:38:40 +02:00
parent d55ff7c47c
commit 5c0a348369
3 changed files with 303 additions and 0 deletions

91
page/accordion.css Normal file
View File

@ -0,0 +1,91 @@
.accordion {
display: flex;
flex-direction: column;
flex-wrap: nowrap;
gap: 2rem;
}
@media only screen and (max-width: 1000px) {
.accordion {
gap: 1rem;
}
}
.accordion > .item.box,
.accordion > .wrapper > .item.box {
justify-content: flex-start;
align-items: stretch;
gap: 0;
padding: 0;
overflow: hidden;
text-align: left;
}
.accordion > .item.box > .head,
.accordion > .wrapper > .item.box > .head {
display: flex;
flex-direction: row;
align-items: center;
flex-wrap: nowrap;
gap: 1.5rem;
padding: 1rem 2rem;
/* HACK: This is equal to an `outline-bottom` */
border-bottom: 0.125rem solid var(--color-gray-dark-dark);
margin-bottom: -0.125rem;
}
.accordion > .item.box > .head:hover,
.accordion > .wrapper > .item.box > .head:hover {
cursor: pointer;
}
.accordion > .item.box > .head > .icon,
.accordion > .wrapper > .item.box > .head > .icon {
color: var(--theme);
}
.accordion > .item.box > .head > .title,
.accordion > .wrapper > .item.box > .head > .title {
flex-grow: 1;
justify-content: flex-start;
}
@media only screen and (max-width: 1000px) {
.accordion > .item.box > .head > .icon,
.accordion > .wrapper > .item.box > .head > .icon,
.accordion > .item.box > .head > .title,
.accordion > .wrapper > .item.box > .head > .title {
font-size: 1rem;
}
.accordion .head > .title .copylink {
/* HACK: Fix copylink positioning */
top: -0.35rem;
}
}
.accordion > .item.box > .body-container,
.accordion > .wrapper > .item.box > .body-container {
transition: max-height 0.5s ease-out;
}
.accordion > .item.box:not(.open) > .body-container,
.accordion > .wrapper > .item.box:not(.open) > .body-container {
max-height: 0;
}
.accordion > .item.box > .body-container > .body,
.accordion > .wrapper > .item.box > .body-container > .body {
display: flex;
flex-direction: column;
flex-wrap: nowrap;
gap: 1rem;
padding: 2rem;
}

208
page/accordion.js Normal file
View File

@ -0,0 +1,208 @@
"use strict";
let accordion_view_list = [];
const accordion_resize_observer = new ResizeObserver((changed_item_list) => {
for(let one_changed_item of changed_item_list){
accordion_body_resize(one_changed_item.target);
}
});
window.addEventListener("load", function(event){
// COLLECT ACCORDION VIEWS //
let accordion_root_list = document.getElementsByClassName("accordion");
for(let one_accordion_root of accordion_root_list){
// collect this view
let accordion_view = accordion_collect_view(one_accordion_root);
accordion_view_list.push(accordion_view);
// initialized this view
accordion_init(accordion_view);
}
});
/**
* HELPER: Collect data for one accordion view.
*
* @param root Root element.
*
* @return Accordion view object.
*/
function accordion_collect_view(root){
// COLLECT ITEMS //
let item_list = [];
for(let one_child of root.children){
// maybe unwrap
if(one_child.classList.contains("wrapper")) one_child = one_child.children[0] ?? null;
// validate child as item
if(!one_child.classList.contains("item")) continue;
// find head element
let head;
for(let one_item_child of one_child.children){
if(one_item_child.classList.contains("head")){
head = one_item_child;
break;
}
}
if(head === undefined) throw "Unable to find accordion item head";
// find icon
let icon;
for(let one_head_child of head.children){
if(one_head_child.classList.contains("icon")){
icon = one_head_child;
break;
}
}
if(icon === undefined) throw "Unable to find accordion item icon";
// find title
let title;
for(let one_head_child of head.children){
if(one_head_child.classList.contains("title")){
title = one_head_child;
break;
}
}
if(title === undefined) throw "Unable to find accordion item title";
// find body container
let bodyContainer;
for(let one_item_child of one_child.children){
if(one_item_child.classList.contains("body-container")){
bodyContainer = one_item_child;
break;
}
}
if(bodyContainer === undefined) throw "Unable to find accordion body container";
// add to item list
item_list.push({
element: one_child,
head: head,
icon: icon,
title: title,
bodyContainer: bodyContainer,
});
}
// BUILD OBJECT //
return {
root: root,
item_list: item_list,
};
}
/**
* HELPER: Initialize accordion view.
*
* @param accordion Accordion view object.
*/
function accordion_init(accordion){
// REGISTER ONCLICK HANDLERS //
for(let one_item of accordion.item_list){
// head
one_item.head.onclick = function(event){
event.stopPropagation();
accordion_click(one_item, accordion);
};
// title
one_item.title.tabIndex = 0;
one_item.title.onclick = function(event){
event.stopPropagation();
accordion_click(one_item, accordion);
};
one_item.title.onkeypress = function(event){
if(event.key === "Enter"){
event.preventDefault();
event.stopPropagation();
accordion_click(one_item, accordion);
}
};
}
// RESET OPENED STATE //
for(let one_item of accordion.item_list){
accordion_state_set(one_item, accordion_state_get(one_item));
}
}
/**
* CALLBACK: Item head was clicked.
*
* @param item Accordion item.
* @param accordion This item's accordion view object.
*/
function accordion_click(item, accordion){
// get our old state
let old_state = accordion_state_get(item);
// close all other items //
for(let one_item of accordion.item_list){
accordion_state_set(one_item, false);
}
// set our new state
accordion_state_set(item, !old_state);
}
/**
* GETTER: Get state of one accordion item.
*
* @param item Accordion item.
*/
function accordion_state_get(item){
return item.element.classList.contains("open");
}
/**
* SETTER: Set state of one accordion item.
*
* @param item Accordion item.
* @param state New state.
*/
function accordion_state_set(item, state){
// set observe state
if(state) accordion_resize_observer.observe(item.bodyContainer);
else accordion_resize_observer.unobserve(item.bodyContainer);
// update class
item.element.classList.remove("open");
if(state) item.element.classList.add("open");
// update icon direction
item.icon.classList.remove("ti-chevron-up", "ti-chevron-right", "ti-chevron-down", "ti-chevron-left");
if(state) item.icon.classList.add("ti-chevron-down");
else item.icon.classList.add("ti-chevron-right");
// set body height
if(state) accordion_body_resize(item.bodyContainer);
else item.bodyContainer.style.maxHeight = 0;
}
/**
* HELPER: Resize body height.
*
* @param bodyContainer Accordion body container.
*/
function accordion_body_resize(bodyContainer){
bodyContainer.style.maxHeight = bodyContainer.scrollHeight + "px";
}

View File

@ -27,11 +27,15 @@
Page::css("./page/style.css");
Page::css("./page/style.css.php", eval: true);
require("./page/copylink_strings.php");
Page::js("./page/copylink_dict.js.php", eval: true);
Page::js("./page/copylink.js");
Page::css("./page/copylink.css");
Page::js("./page/accordion.js");
Page::css("./page/accordion.css");
Page::font("ubuntu");
Page::font("tabler");
?>