@ribajs/bs5
Version:
Bootstrap 5 module for Riba.js
186 lines (165 loc) • 5.57 kB
text/typescript
import { BasicComponent, Binder, View } from "@ribajs/core";
import { Bs5Service } from "../services/bs5.service.js";
import { parseJsonString, jsonStringify, camelCase } from "@ribajs/utils";
export abstract class Bs5AbstractBreakpointBinder<
E extends HTMLElement = HTMLElement,
> extends Binder<string, E> {
protected abstract defaultAttributeBinder: Binder<
any,
HTMLElement | BasicComponent
>;
protected bs5: Bs5Service;
protected breakpoint: string;
protected attributeName: string;
protected val?: any;
constructor(
view: View,
el: E,
type: string | null,
name: string,
keypath: string | undefined,
formatters: string[] | null,
identifier: string | null,
) {
super(view, el, type, name, keypath, formatters, identifier);
this.bs5 = Bs5Service.getSingleton();
if (this.args.length !== 2) {
throw new Error(
"The Bs5AttributeBreakpointBinder was not initialized correctly!",
);
}
const breakpoint = this.args[0].toString();
const attributeName = this.args[1].toString();
if (!this.bs5.breakpointNames.includes(breakpoint)) {
throw new Error(
`Unknown breakpoint "${breakpoint}"! You can define breakpoints at the initialization of the Bs5Module.`,
);
}
this.breakpoint = breakpoint;
this.attributeName = attributeName;
}
protected onBreakpointChanges(/* activeBreakpoint: Breakpoint */) {
this.setAttributeOnMatch();
}
/**
* Sets the attribute if the current breakpoint matches
* @returns
*/
protected setAttributeOnMatch() {
if (!this.bs5.activeBreakpoint) return;
if (this.isBreakpointMatch(this.breakpoint)) {
return this.defaultAttributeBinder.routine(this.el, this.val);
}
if (this.breakpointUnhandled(this.bs5.activeBreakpoint.name)) {
// Remove attribute
this.defaultAttributeBinder.routine(this.el, undefined);
}
}
/**
* Returns `true` if the current active breakpoint matches the breakpoints handled by this binder
* @param breakpoint
* @returns
*/
protected isBreakpointMatch(breakpoint = this.breakpoint) {
if (!this.bs5.activeBreakpoint) {
return false;
}
if (this.bs5.activeBreakpoint.name === breakpoint) {
return true;
}
const myBreakpoints = this.myBreakpoints(breakpoint);
if (myBreakpoints.includes(this.bs5.activeBreakpoint.name)) {
return true;
}
return false;
}
/**
* Returns all breakpoints handled by this binder.
* This binder handles multiple breakpoints if other larger breakpoints exists but are not handled by another binder.
* @returns
*/
protected myBreakpoints(breakpoint = this.breakpoint) {
const myBreakpoints: string[] = [breakpoint];
let nextBreakpoint: string | null = breakpoint;
while (nextBreakpoint) {
nextBreakpoint = this.bs5.getNextBreakpointByName(nextBreakpoint);
if (!nextBreakpoint || this.breakpointHandledByAnother(nextBreakpoint)) {
break;
}
myBreakpoints.push(nextBreakpoint);
}
return myBreakpoints;
}
/**
* Check if another breakpoint (mostly the next breakpoint) is handled by another binder
*/
protected breakpointHandledByAnother(name: string) {
const handledBreakpoints = this.getHandledBreakpoints();
if (handledBreakpoints.includes(name)) {
return true;
}
return false;
}
/**
* Check if another breakpoint (mostly the previous breakpoint) is handled by no other breakpoint binders.
* These are all breakpoints that are smaller than the minimum breakpoint.
* I.e. for these breakpoints there is no binder, neither this one nor another one.
* @returns
*/
protected breakpointUnhandled(breakpoint: string) {
let prevBreakpoint: string | null = breakpoint;
// Check if all smaller breakpoints are unhandled
let unhandled = true;
while (prevBreakpoint && unhandled) {
if (this.breakpointHandledByAnother(prevBreakpoint)) {
unhandled = false;
break;
}
prevBreakpoint = this.bs5.getPrevBreakpointByName(prevBreakpoint);
if (!prevBreakpoint) {
break;
}
}
return unhandled;
}
/**
* Used to check if there are more binders for other breakpoints
**/
protected addToHandledBreakpoints() {
const handledBreakpoints = this.getHandledBreakpoints();
handledBreakpoints.push(this.breakpoint);
this.el.dataset[camelCase(this.attributeName)] =
jsonStringify(handledBreakpoints);
}
/**
* Get all handled breakpoints.
* These are the own but also those of other binders.
* @returns
*/
protected getHandledBreakpoints() {
const handledBreakpoints: string[] = parseJsonString(
this.el.dataset[camelCase(this.attributeName)] || "[]",
);
if (!Array.isArray(handledBreakpoints)) {
throw new Error("breakpoints dataset has unsupported values!");
}
return handledBreakpoints;
}
bind(el: HTMLElement) {
this.bs5.on("breakpoint:changed", this.onBreakpointChanges, this);
if (typeof this.defaultAttributeBinder.bind === "function") {
this.defaultAttributeBinder.bind(el);
}
}
routine(el: HTMLElement, newValue: any) {
this.addToHandledBreakpoints();
this.val = newValue;
this.setAttributeOnMatch();
}
unbind(el: HTMLElement) {
this.bs5?.off("breakpoint:changed", this.onBreakpointChanges, this);
if (typeof this.defaultAttributeBinder.unbind === "function") {
this.defaultAttributeBinder.unbind(el);
}
}
}