# presentation/components/tab.component.ts

> 123 lines of code and 27 definitions.

Tree: Site tree
Language: typescript
Layer: product
Canonical: https://banes-lab.com/anatomy/tree#file-presentation-components-tab-component-ts
Source text: https://banes-lab.com/assets/sources/source.c3cbe258a0318ccd9e6c3a800028975d90bbf773a59e82388f7a74f95db3358e.generated.txt

## Definitions

- `createJumpButton` (lexical_declaration, line 114, exported)
- `scrollToId` (lexical_declaration, line 79, exported)
- `expand` (lexical_declaration, line 29)
- `travel` (lexical_declaration, line 56)
- `reveal` (lexical_declaration, line 37)
- `createTabButton` (lexical_declaration, line 91, exported)
- `scroller` (lexical_declaration, line 25, exported)
- `settleAfter` (lexical_declaration, line 65)
- `settle` (lexical_declaration, line 67)
- `TAB_ATTRIBUTE` (lexical_declaration, line 18)
- `LABEL_CLASS` (lexical_declaration, line 19)
- `DETAILS_TAG` (lexical_declaration, line 20)
- `OWN_DETAILS_SELECTOR` (lexical_declaration, line 21)
- `TOGGLE_EVENT` (lexical_declaration, line 22)
- `SCROLL_END_EVENT` (lexical_declaration, line 23)
- `ancestor` (lexical_declaration, line 41)
- `centredTop` (lexical_declaration, line 48)
- `mainRect` (lexical_declaration, line 49)
- `targetRect` (lexical_declaration, line 50)
- `offset` (lexical_declaration, line 51)
- `free` (lexical_declaration, line 52)
- `top` (lexical_declaration, line 57)
- `settled` (lexical_declaration, line 66)
- `main` (lexical_declaration, line 80, exported)
- `target` (lexical_declaration, line 81, exported)
- `title` (lexical_declaration, line 97, exported)
- `link` (lexical_declaration, line 119, exported)

## Uses

- [core/buses/base.bus.ts](https://banes-lab.com/source/tree/core/buses/base.bus.ts.md)

## Used by

- [presentation/components/link.component.ts](https://banes-lab.com/source/tree/presentation/components/link.component.ts.md)
- [presentation/renderers/glossary.renderer.ts](https://banes-lab.com/source/tree/presentation/renderers/glossary.renderer.ts.md)
- [presentation/renderers/tab.renderer.ts](https://banes-lab.com/source/tree/presentation/renderers/tab.renderer.ts.md)
- [presentation/widgets/tab.widget.ts](https://banes-lab.com/source/tree/presentation/widgets/tab.widget.ts.md)

## Source

```typescript
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 { 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";
import { getPage } from "#domain/registries/page.registry";

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<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;
};
```
