diff --git a/page/accordion.css b/page/accordion.css new file mode 100644 index 0000000..c8e0203 --- /dev/null +++ b/page/accordion.css @@ -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; +} diff --git a/page/accordion.js b/page/accordion.js new file mode 100644 index 0000000..eb8c5a3 --- /dev/null +++ b/page/accordion.js @@ -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"; +} diff --git a/page/page_base.php b/page/page_base.php index 6e39e68..c637d7d 100644 --- a/page/page_base.php +++ b/page/page_base.php @@ -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"); ?>