UNPKG

@ribajs/bs4

Version:

Bootstrap 4 module for Riba.js

183 lines (166 loc) 4.37 kB
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; } } }