presentation/components/tab.component.ts
presentation/components/tab.component.ts is a file in Bane's Lab Site. 121 lines of code and 25 definitions.
import { ACTIVE_CLASS, DETAILS_TAG, TOGGLE_EVENT } from "#configuration/constants/element.constants";
import {
SCROLL_BEHAVIOR,
SCROLL_SETTLE_DELAY_MS,
SCROLL_SETTLE_TOLERANCE_PX,
SCROLL_TARGET_RATIO,
} from "#configuration/constants/tab.constants";
import { contextOf, namedFor } from "#presentation/renderers/element.renderer";
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";
import { getPage } from "#domain/registries/page.registry";
const TAB_ATTRIBUTE = "data-tab";
const LABEL_CLASS = "tab-label";
const OWN_DETAILS_SELECTOR = ":scope > details";
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<HTMLDetailsElement>(OWN_DETAILS_SELECTOR)) {
expand(details);
}
let ancestor = target.closest<HTMLDetailsElement>(DETAILS_TAG);
while (ancestor !== null) {
expand(ancestor);
ancestor = ancestor.parentElement?.closest<HTMLDetailsElement>(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 title = getPage(page)?.title ?? page;
const link = createElement("a", {
attributes: { [TAB_ATTRIBUTE]: tab.id, "aria-label": namedFor(tab.label, title), href },
children: [
createElement("i", { className: `${ICON_BASE_CLASS} ${tab.icon}` }),
createElement("span", { className: LABEL_CLASS, text: tab.label }),
contextOf(title),
],
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;
};