UNPKG

@ribajs/bs5

Version:

Bootstrap 5 module for Riba.js

562 lines (502 loc) 15 kB
import { Component, TemplateFunction } from "@ribajs/core"; import { TouchEventsService, TouchSwipeData } from "@ribajs/extras"; import { EventDispatcher } from "@ribajs/events"; import { TOGGLE_BUTTON } from "../../constants/index.js"; import { Bs5Service } from "../../services/index.js"; import { JsxBs5SidebarProps, Bs5SidebarComponentScope, SidebarState, // Breakpoint, } from "../../types/index.js"; import { hasChildNodesTrim } from "@ribajs/utils/src/dom.js"; import { debounce } from "@ribajs/utils/src/control"; export class Bs5SidebarComponent extends Component { public static tagName = "bs5-sidebar"; public static states = [ "hidden", "side-left", "side-right", "overlap-left", "overlap-right", "move-left", "move-right", ]; protected computedStyle?: CSSStyleDeclaration; protected autobind = true; public _debug = false; protected bs5: Bs5Service; protected touch: TouchEventsService = new TouchEventsService(this); static get observedAttributes(): (keyof JsxBs5SidebarProps)[] { return [ "id", "container-selector", "position", "mode", "width", "auto-show", "auto-hide", "force-hide-on-location-pathnames", "force-show-on-location-pathnames", "watch-new-page-ready-event", "close-on-swipe", "prevent-scrolling-on-overlap", ]; } public events?: EventDispatcher; protected routerEvents = new EventDispatcher("main"); public defaults: Bs5SidebarComponentScope = { // Template properties containerSelector: undefined, state: "hidden", oldState: "hidden", id: undefined, width: "250px", // Options position: "left", mode: "overlap", autoShow: false, autoHide: false, watchNewPageReadyEvent: true, forceHideOnLocationPathnames: [], forceShowOnLocationPathnames: [], closeOnSwipe: true, preventScrollingOnOverlap: true, // Template methods hide: this.hide, show: this.show, toggle: this.toggle, }; public scope: Bs5SidebarComponentScope = { ...this.defaults, }; constructor() { super(); this.bs5 = Bs5Service.getSingleton(); // assign this to bound version, so we can remove window EventListener later without problem this.onEnvironmentChanges = this.onEnvironmentChanges.bind(this); } public setState(state: SidebarState) { this.scope.oldState = this.scope.state; this.scope.state = state; this.onStateChange(); } public getState() { return this.scope.state; } public getShowMode() { const mode: SidebarState = `${this.scope.mode}-${this.scope.position}` as SidebarState; return mode; } public hide() { this.setState("hidden"); } public show() { const state = this.getShowMode(); this.setState(state); } public toggle() { this.debug("toggle state: " + this.scope.state); if (this.scope.state === "hidden") { this.show(); } else { this.hide(); } this.debug("toggled state: " + this.scope.state); } protected preventScrolling(scrollEl = document.body) { scrollEl.style.overflow = "hidden"; } protected allowScrolling(scrollEl = document.body) { if (scrollEl.style.overflow === "hidden") { scrollEl.style.overflow = ""; } } protected connectedCallback() { super.connectedCallback(); this.init(Bs5SidebarComponent.observedAttributes); this.computedStyle = window.getComputedStyle(this); this.addEventListeners(); // initial this.onEnvironmentChanges(); } protected onBreakpoint(/*breakpoint: Breakpoint | null*/) { this.onEnvironmentChanges(); } protected addEventListeners() { // window.addEventListener("resize", this.onEnvironmentChanges, { // passive: true, // }); this.addEventListener("swipe" as any, this.onSwipe); this.bs5.events.on("breakpoint:changed", this.onBreakpoint, this); } protected removeEventListeners() { this.events?.off(TOGGLE_BUTTON.eventNames.init, this.triggerState, this); this.events?.off(TOGGLE_BUTTON.eventNames.toggle, this.toggle, this); this.routerEvents.off("newPageReady", this.onEnvironmentChanges, this); // window.removeEventListener("resize", this.onEnvironmentChanges); this.bs5.events.off("breakpoint:changed", this.onBreakpoint, this); } protected initToggleButtonEventDispatcher() { if (this.events) { this.events.off(TOGGLE_BUTTON.eventNames.toggle, this.toggle, this); this.events.off(TOGGLE_BUTTON.eventNames.init, this.triggerState, this); } const namespace = TOGGLE_BUTTON.nsPrefix + this.scope.id; this.debug(`Init event dispatcher for namespace ${namespace}`); this.events = new EventDispatcher(namespace); this.events.on(TOGGLE_BUTTON.eventNames.toggle, this.toggle, this); this.events.on(TOGGLE_BUTTON.eventNames.init, this.triggerState, this); } protected initRouterEventDispatcher() { if (this.scope.watchNewPageReadyEvent) { this.routerEvents.on("newPageReady", this.onEnvironmentChanges, this); } } protected _onSwipe(event: CustomEvent<TouchSwipeData>) { if (!this.scope.closeOnSwipe) { return; } if (this.scope.state === "side-left" || this.scope.state === "side-right") { return; } if (this.scope.position === "left" && event.detail.direction === "left") { this.hide(); } if (this.scope.position === "right" && event.detail.direction === "right") { this.hide(); } } protected onSwipe = this._onSwipe.bind(this); protected onHidden() { const translateX = this.scope.position === "left" ? "-100%" : "100%"; this.style.transform = `translateX(${translateX})`; this.width = this.scope.width; this.setContainersStyle(this.scope.state); if (this.scope.preventScrollingOnOverlap) { this.allowScrolling(); } } protected onMove(state: SidebarState) { this.style.transform = `translateX(0)`; this.style.width = this.scope.width; this.width = this.scope.width; this.setContainersStyle(state); if (this.scope.preventScrollingOnOverlap) { this.allowScrolling(); } } protected onSide(state: SidebarState) { this.style.transform = `translateX(0)`; this.style.width = this.scope.width; this.width = this.scope.width; this.setContainersStyle(state); if (this.scope.preventScrollingOnOverlap) { this.allowScrolling(); } } protected onOverlap(state: SidebarState) { this.style.transform = `translateX(0)`; this.width = this.scope.width; this.setContainersStyle(state); if (this.scope.preventScrollingOnOverlap) { this.preventScrolling(); } } protected triggerState() { // Global event this.events?.trigger("state", this.scope.state); } protected onStateChange() { switch (this.scope.state) { case "side-left": case "side-right": this.onSide(this.scope.state); break; case "overlap-left": case "overlap-right": this.onOverlap(this.scope.state); break; case "move-left": case "move-right": this.onMove(this.scope.state); break; default: this.onHidden(); break; } this.classList.remove(...Bs5SidebarComponent.states); this.classList.add(this.scope.state); if (this.events) { this.events.trigger(TOGGLE_BUTTON.eventNames.toggled, this.scope.state); } this.dispatchEvent( new CustomEvent(TOGGLE_BUTTON.eventNames.toggled, { detail: this.scope.state, }), ); } protected get width() { if (this.scope.width === this.defaults.width) { return this.offsetWidth + "px"; } return this.scope.width; } protected set width(width: string) { this.scope.width = width; this.style.width = width; } protected setStateByEnvironment() { if ( this.scope.forceHideOnLocationPathnames.includes(window.location.pathname) ) { return this.hide(); } if ( this.scope.forceShowOnLocationPathnames.includes(window.location.pathname) ) { return this.show(); } if (this.scope.autoHide) { return this.hide(); } if (this.scope.autoShow) { return this.show(); } } /** * Internal (not debounced) version of `onEnvironmentChanges`. */ protected _onEnvironmentChanges() { this.setStateByEnvironment(); } /** * If viewport size changes, location url changes or something else. */ protected onEnvironmentChanges = debounce( this._onEnvironmentChanges.bind(this), ); protected getContainers() { return this.scope.containerSelector ? document.querySelectorAll<HTMLUnknownElement>( this.scope.containerSelector, ) : undefined; } protected initContainers(state: SidebarState) { this.setContainersStyle(state); } protected setContainersStyle(state: SidebarState) { const containers: | NodeListOf<HTMLUnknownElement> | Array<HTMLUnknownElement> = this.getContainers() || []; if (containers) { for (let i = 0; i < containers.length; i++) { const container = containers[i]; this.setContainerStyle(container, state); } } } /** * Sets the container style, takes overs always the transition style of this sidebar * @param container * @param style * @param state */ protected setContainerStyle( container: HTMLUnknownElement, state: SidebarState, ) { const currStyle = container.style; if (state) { const width = this.width; const conStyle = window.getComputedStyle(container); if (this.scope.mode === "move" && state.startsWith("overlap-")) { switch (conStyle.position) { case "fixed": case "absolute": currStyle.left = "0"; currStyle.right = "0"; break; default: currStyle.marginLeft = "0"; currStyle.marginRight = "0"; break; } } switch (state) { case "side-left": switch (conStyle.position) { case "fixed": case "absolute": currStyle.left = width; break; default: currStyle.marginLeft = width; break; } break; case "side-right": switch (conStyle.position) { case "fixed": case "absolute": currStyle.right = width; break; default: currStyle.marginRight = width; break; } break; case "move-left": switch (conStyle.position) { case "fixed": case "absolute": currStyle.left = width; currStyle.right = `-${width}`; break; default: currStyle.marginLeft = width; currStyle.marginRight = `-${width}`; break; } break; case "move-right": switch (conStyle.position) { case "fixed": case "absolute": currStyle.right = width; currStyle.left = `-${width}`; break; default: currStyle.marginRight = width; currStyle.marginLeft = `-${width}`; break; } break; case "hidden": switch (this.scope.oldState) { case "side-left": switch (conStyle.position) { case "fixed": case "absolute": currStyle.left = "0"; break; default: currStyle.marginLeft = "0"; break; } break; case "side-right": switch (conStyle.position) { case "fixed": case "absolute": currStyle.right = "0"; break; default: currStyle.marginRight = "0"; break; } break; case "move-left": switch (conStyle.position) { case "fixed": case "absolute": currStyle.left = "0"; currStyle.right = "0"; break; default: currStyle.marginRight = "0"; currStyle.marginLeft = "0"; break; } break; case "move-right": switch (conStyle.position) { case "fixed": case "absolute": currStyle.right = "0"; currStyle.left = "0"; break; default: currStyle.marginRight = "0"; currStyle.marginLeft = "0"; break; } break; default: break; } default: break; } } container.style.transition = this.computedStyle ? this.computedStyle.transition : ""; } protected async beforeBind() { await super.beforeBind(); this.scope.oldState = this.getShowMode(); this.initRouterEventDispatcher(); return this.onEnvironmentChanges(); } protected async afterBind() { this.onEnvironmentChanges(); await super.afterBind(); } protected requiredAttributes(): string[] { return ["id"]; } protected parsedAttributeChangedCallback( attributeName: string, oldValue: any, newValue: any, namespace: string | null, ) { super.parsedAttributeChangedCallback( attributeName, oldValue, newValue, namespace, ); if (attributeName === "containerSelector") { } if (attributeName === "id") { this.initToggleButtonEventDispatcher(); } switch (attributeName) { case "containerSelector": this.initContainers(this.scope.state); break; case "id": this.initToggleButtonEventDispatcher(); break; case "width": this.width = newValue; case "mode": this.onStateChange(); this.initContainers(this.scope.state); break; case "autoHide": case "autoShow": this.setStateByEnvironment(); break; default: break; } } // deconstruction protected disconnectedCallback() { super.disconnectedCallback(); this.removeEventListeners(); } protected template(): ReturnType<TemplateFunction> { if (!hasChildNodesTrim(this)) { console.warn( "No child elements found, this component as no template so you need to define your own as child of this component.", ); } return null; } }