@ribajs/bs4
Version:
Bootstrap 4 module for Riba.js
183 lines (166 loc) • 4.37 kB
text/typescript
import { Component, ScopeBase } from "@ribajs/core";
import { hasChildNodesTrim } from "@ribajs/utils/src/dom.js";
export interface Anchor {
element: HTMLHeadingElement;
href: string;
title: string;
childs: Anchor[];
}
export interface Scope extends ScopeBase {
/**
* If start is `2` and depth is `2` depth starts on `h2` and ends on `h2`.
*/
headersStart: number;
/**
* If start is `1` and depth is `1` only `h1` headers are detected, if depth is `2` also `h2` is detected.
*/
headersDepth: number;
/**
* Depth in how many parents elements should be searched for an id for each found header element (default `1`)
*/
findHeaderIdDepth: number;
/**
* Selector to search for headers insite of the element
*/
headerParentSelector?: string;
/**
* Pixels to offset from top when calculating position to scroll there.
*/
scrollOffset: number;
/**
* The element to scroll (default `window`)
*/
scrollElement?: string;
/**
* Array of found headers / anchors
*/
anchors: Anchor[];
}
export class Bs4ContentsComponent extends Component {
public static tagName = "bs4-contents";
protected autobind = true;
protected wrapperElement?: Element;
static get observedAttributes(): string[] {
return [
"headers-start",
"headers-depth",
"find-header-id-depth",
"header-parent-selector",
"scroll-offset",
"scroll-element",
];
}
public scope: Scope = {
headersDepth: 1,
headersStart: 2,
findHeaderIdDepth: 1,
headerParentSelector: undefined,
scrollOffset: 0,
anchors: [],
};
constructor() {
super();
}
protected connectedCallback() {
super.connectedCallback();
this.init(Bs4ContentsComponent.observedAttributes);
}
protected getIdFromElementOrParent(
element: HTMLElement,
depth = 1,
): string | null {
if (element.id) {
return element.id;
}
if (depth <= this.scope.findHeaderIdDepth) {
if (element.parentElement) {
return this.getIdFromElementOrParent(element.parentElement, ++depth);
}
}
return null;
}
protected pushHeaders(
wrapperElement: Element,
headersStart: number,
headersDepth: number,
pushTo: Anchor[],
) {
const headerElements = wrapperElement.querySelectorAll(
"h" + headersStart,
) as NodeListOf<HTMLHeadingElement>;
headerElements.forEach((headerElement) => {
const id = this.getIdFromElementOrParent(headerElement);
if (!id) {
return;
}
pushTo.push({
element: headerElement,
href: "#" + id,
title: headerElement.innerHTML,
childs: [],
});
if (headerElement.parentElement && headersDepth >= headersStart + 1) {
this.pushHeaders(
headerElement.parentElement,
headersStart + 1,
headersDepth,
pushTo[pushTo.length - 1].childs,
);
}
});
}
protected async afterBind() {
if (
this.scope.headerParentSelector &&
this.scope.headersStart &&
this.scope.headersDepth
) {
this.wrapperElement =
document.querySelector(this.scope.headerParentSelector) || undefined;
this.scope.anchors = [];
if (!this.wrapperElement) {
console.error("No wrapper element found!");
return;
}
this.pushHeaders(
this.wrapperElement,
this.scope.headersStart,
this.scope.headersDepth,
this.scope.anchors,
);
}
await super.afterBind();
}
protected requiredAttributes(): string[] {
return ["headersStart", "headersDepth", "headerParentSelector"];
}
protected async attributeChangedCallback(
attributeName: string,
oldValue: any,
newValue: any,
namespace: string | null,
) {
super.attributeChangedCallback(
attributeName,
oldValue,
newValue,
namespace,
);
}
// deconstruction
protected disconnectedCallback() {
super.disconnectedCallback();
this.scope.anchors = [];
}
protected async template() {
// Only set the component template if there no childs already
if (hasChildNodesTrim(this)) {
return null;
} else {
const { default: template } = await import(
"./bs4-contents.component.html?raw"
);
return template;
}
}
}