import { SCROLL_BEHAVIOR, SCROLL_SETTLE_DELAY_MS, SCROLL_SETTLE_TOLERANCE_PX, SCROLL_TARGET_RATIO, } from "#configuration/constants/tab.constants"; import { ACTIVE_CLASS } from "#configuration/constants/element.constants"; import { ANCHOR_PREFIX } from "#configuration/constants/document.constants"; import { ICON_BASE_CLASS } from "#configuration/icons/element.icons"; import { MAIN_ID } from "#core/ids/site.ids"; import { TAB_SELECTED } from "#core/ids/tab.ids"; import type { Tab } from "#types/document.types"; import { createElement } from "#core/factories/element.factory"; import { emitEvent } from "#core/buses/base.bus"; const TAB_ATTRIBUTE = "data-tab"; const LABEL_CLASS = "tab-label"; const DETAILS_TAG = "details"; const OWN_DETAILS_SELECTOR = ":scope > details"; const TOGGLE_EVENT = "toggle"; const SCROLL_END_EVENT = "scrollend"; export const scroller = function scroller(): HTMLElement | null { return document.getElementById(MAIN_ID); }; const expand = function expand(details: HTMLDetailsElement): void { if (details.open) { return; } details.open = true; details.dispatchEvent(new Event(TOGGLE_EVENT)); }; const reveal = function reveal(target: HTMLElement): void { for (const details of target.querySelectorAll(OWN_DETAILS_SELECTOR)) { expand(details); } let ancestor = target.closest(DETAILS_TAG); while (ancestor !== null) { expand(ancestor); ancestor = ancestor.parentElement?.closest(DETAILS_TAG) ?? null; } }; const centredTop = function centredTop(main: HTMLElement, target: HTMLElement): number { const mainRect = main.getBoundingClientRect(); const targetRect = target.getBoundingClientRect(); const offset = targetRect.top - mainRect.top + main.scrollTop; const free = Math.max(0, mainRect.height - targetRect.height); return offset - free * SCROLL_TARGET_RATIO; }; const travel = function travel(main: HTMLElement, target: HTMLElement): boolean { const top = centredTop(main, target); if (Math.abs(top - main.scrollTop) <= SCROLL_SETTLE_TOLERANCE_PX) { return false; } main.scrollTo({ behavior: SCROLL_BEHAVIOR, top }); return true; }; const settleAfter = function settleAfter(main: HTMLElement, target: HTMLElement): void { let settled = false; const settle = function settle(): void { if (settled) { return; } settled = true; main.removeEventListener(SCROLL_END_EVENT, settle); travel(main, target); }; main.addEventListener(SCROLL_END_EVENT, settle); window.setTimeout(settle, SCROLL_SETTLE_DELAY_MS); }; export const scrollToId = function scrollToId(targetId: string): void { const main = scroller(); const target = document.getElementById(targetId); if (main === null || target === null) { return; } reveal(target); if (travel(main, target)) { settleAfter(main, target); } }; export const createTabButton = function createTabButton( page: string, tab: Tab, active: boolean, href: string, ): HTMLAnchorElement { const link = createElement("a", { attributes: { [TAB_ATTRIBUTE]: tab.id, href }, children: [ createElement("i", { className: `${ICON_BASE_CLASS} ${tab.icon}` }), createElement("span", { className: LABEL_CLASS, text: tab.label }), ], className: active ? `tab-button ${ACTIVE_CLASS}` : "tab-button", }); link.addEventListener("click", (event) => { event.preventDefault(); emitEvent({ name: TAB_SELECTED, page, tab: tab.id }); }); return link; }; export const createJumpButton = function createJumpButton( symbol: string, label: string, targetId: string, ): HTMLAnchorElement { const link = createElement("a", { attributes: { href: ANCHOR_PREFIX + targetId }, children: [ createElement("span", { className: "tab-symbol", text: symbol }), createElement("span", { className: LABEL_CLASS, text: label }), ], className: "tab-button", }); link.addEventListener("click", (event) => { event.preventDefault(); window.history.replaceState({}, "", ANCHOR_PREFIX + targetId); scrollToId(targetId); }); return link; };