gd-bs
Version:
Bootstrap JavaScript, TypeScript and Web Components library.
412 lines (363 loc) • 13.7 kB
text/typescript
import {
arrow, autoPlacement, computePosition, flip, hide, inline, offset, shift, size, ComputePositionConfig
} from "@floating-ui/dom";
import { setClassNames } from "../common";
import { IFloatingUIProps, IFloatingUI } from "./types";
export * as FloatingUILib from "@floating-ui/dom";
/**
* Floating UI Placements
*/
export enum FloatingUIPlacements {
Auto = 1,
AutoStart = 2,
AutoEnd = 3,
Bottom = 4,
BottomStart = 5,
BottomEnd = 6,
Left = 7,
LeftStart = 8,
LeftEnd = 9,
Right = 10,
RightStart = 11,
RightEnd = 12,
Top = 13,
TopStart = 14,
TopEnd = 15
}
/**
* Floating UI Types
*/
export enum FloatingUITypes {
Danger = 1,
Dark = 2,
Info = 3,
Light = 4,
LightBorder = 5,
Material = 6,
Primary = 7,
Secondary = 8,
Success = 9,
Translucent = 10,
Warning = 11
}
/**
* Floating UI Element
*/
class _FloatingUI {
private _elArrow: HTMLElement = null;
private _elContent: HTMLElement = null;
private _elIgnore: HTMLElement[] = null;
private _elTarget: HTMLElement = null;
private _options: ComputePositionConfig = null;
private _props: IFloatingUIProps = null;
// Constructor
constructor(props: IFloatingUIProps) {
this._elIgnore = [];
this._elTarget = props.elTarget;
this._props = props;
// Create the content element
this._elContent = document.createElement("div");
this._elContent.classList.add("bs");
this._elContent.classList.add("floating-ui");
this._elContent.appendChild(props.elContent);
this._elContent.setAttribute("data-theme", this.getTheme(this._props.theme));
setClassNames(this._elContent, this._props.className);
// Add the events
this.addEvents(props.options?.trigger);
// Create the floating ui element
this.create();
// Set the visibility
this._props.show ? this.show() : this.hide();
}
// Add the events to trigger, refresh and hide the element
private addEvents(trigger: string = "") {
// Events
if (trigger.indexOf("mouse") >= 0) {
this._elTarget.addEventListener("mouseenter", () => { this.show(); });
this._elTarget.addEventListener("mouseleave", () => { this.hide(); });
}
if (trigger.indexOf("focus") >= 0) {
this._elTarget.addEventListener("focus", () => { this.show(); });
this._elTarget.addEventListener("blur", () => { this.hide(); });
}
if (trigger.indexOf("click") >= 0) {
this._elTarget.addEventListener("click", (ev) => {
// Call the event
this.isVisible ? this.hide() : this.show();
});
}
// Create the event
document.addEventListener("click", (ev) => {
// Do nothing if we do not want to hide on click
if (this._props.options?.hideOnClick == false) { return; }
// Do nothing if we toggled this component
if (this._elTarget.contains(ev.target as HTMLElement)) { return; }
// Parse the elements to ignore
for (let i = 0; i < this._elIgnore.length; i++) {
// Do nothing if it triggered the click
if (this._elIgnore[i].contains(ev.target as HTMLElement)) { return; }
}
// Hide the element
this.hide();
});
// Create the scroll event
window.addEventListener("scroll", (ev) => {
// Wait for the other events to run
setTimeout(() => {
// Refresh the content
this.refresh();
}, 10);
});
}
// Creates the floating ui
private create() {
let placement = this.getPlacement(this._props.placement);
let middleware = this.getMiddleware(placement);
// See if we are adding an arrow
if (this._props.options?.arrow) {
// Create the element
this._elArrow = document.createElement("div");
this._elArrow.classList.add("arrow");
this._elContent.appendChild(this._elArrow);
// Add the plugin
middleware.push(arrow({ element: this._elArrow }));
middleware = [offset(6)].concat(middleware);
}
// Set the options
this._options = {
middleware,
placement: placement.placement as any
};
}
// Returns the plugins
private getMiddleware(placement: { autoPlacement: boolean; placement: string; }) {
let middleware = [];
// See if we are adding the offset option
if (this._props.options?.offset) {
middleware.push(typeof (this._props.options.offset) === "boolean" ? offset() : offset(this._props.options.offset));
}
// See if we are adding the auto placement option
if (this._props.options?.autoPlacement || placement.autoPlacement) {
middleware.push(typeof (this._props.options.autoPlacement) === "boolean" ? autoPlacement() : autoPlacement(this._props.options.autoPlacement));
}
// Else, see if we are adding the flip option
else if (this._props.options?.flip) {
middleware.push(flip());
}
// See if we are adding the hide option
if (this._props.options?.hide) {
middleware.push(typeof (this._props.options.hide) === "boolean" ? hide() : hide(this._props.options.hide));
}
// See if we are adding the inline option
if (this._props.options?.inline) {
middleware.push(typeof (this._props.options.inline) === "boolean" ? inline() : inline(this._props.options.inline));
}
// See if we are adding the shift option
if (this._props.options?.shift) {
middleware.push(typeof (this._props.options.shift) === "boolean" ? shift() : shift(this._props.options?.shift));
}
// See if we are adding the size option
if (this._props.options?.size) {
middleware.push(typeof (this._props.options.size) === "boolean" ? size() : size(this._props.options?.size));
}
// Return the middle ware
return middleware;
}
// Returns the placement information
private getPlacement(placementValue: number): { autoPlacement: boolean; placement: string; } {
let autoPlacement = false;
let placement = "top-end";
switch (placementValue) {
// Auto
case FloatingUIPlacements.Auto:
autoPlacement = true;
break;
case FloatingUIPlacements.AutoEnd:
placement = 'end';
autoPlacement = true;
break;
case FloatingUIPlacements.AutoStart:
placement = 'start';
autoPlacement = true;
break;
// Bottom
case FloatingUIPlacements.Bottom:
placement = "bottom";
break;
case FloatingUIPlacements.BottomEnd:
placement = "bottom-end";
break;
case FloatingUIPlacements.BottomStart:
placement = "bottom-start";
break;
// Left
case FloatingUIPlacements.Left:
placement = "left";
break;
case FloatingUIPlacements.LeftEnd:
placement = "left-end";
break;
case FloatingUIPlacements.LeftStart:
placement = "left-start";
break;
// Right
case FloatingUIPlacements.Right:
placement = "right";
break;
case FloatingUIPlacements.RightEnd:
placement = "right-end";
break;
case FloatingUIPlacements.RightStart:
placement = "right-start";
break;
// Top
case FloatingUIPlacements.Top:
placement = "top";
break;
case FloatingUIPlacements.TopEnd:
placement = "top-end";
break;
case FloatingUIPlacements.TopStart:
placement = "top-start";
break;
}
// Return the placement
return { autoPlacement, placement };
}
// Returns the theme
private getTheme(themeValue: number) {
let theme = null;
// Set the theme
switch (themeValue) {
// Dark
case FloatingUITypes.Dark:
theme = "dark";
break;
// Danger
case FloatingUITypes.Danger:
theme = "danger";
break;
// Info
case FloatingUITypes.Info:
theme = "info";
break;
// Light
case FloatingUITypes.Light:
theme = "light";
break;
case FloatingUITypes.LightBorder:
theme = "light-border";
break;
// Material
case FloatingUITypes.Material:
theme = "material";
break;
// Primary
case FloatingUITypes.Primary:
theme = "primary";
break;
// Secondary
case FloatingUITypes.Secondary:
theme = "secondary";
break;
// Success
case FloatingUITypes.Success:
theme = "success";
break;
// Translucent
case FloatingUITypes.Translucent:
theme = "translucent";
break;
// Warning
case FloatingUITypes.Warning:
theme = "warning";
break;
// Default - Light Border
default:
theme = "light-border";
break;
}
// Return the theme
return theme;
}
// Refresh the element position
private refresh() {
// Create the floating ui
computePosition(this._elTarget, this._elContent, this._options).then(({ x, y, middlewareData }) => {
// Update the location
Object.assign(this._elContent.style, {
left: `${x}px`,
top: `${y}px`
});
// See if the arrow exists
if (this._elArrow) {
let arrowX = middlewareData.arrow.x;
let arrowY = middlewareData.arrow.y;
let placement = (middlewareData.offset?.placement || this._options.placement).split('-')[0];
let side = {
top: 'bottom',
right: 'left',
bottom: 'top',
left: 'right'
}[placement]
// Set the placement
this._elContent.setAttribute("data-placement", placement);
// Update the location
Object.assign(this._elArrow.style, {
left: arrowX != null ? `${arrowX}px` : '',
top: arrowY != null ? `${arrowY}px` : '',
right: '',
bottom: '',
[side]: '-4px'
});
}
});
}
/**
* Public Methods
*/
addIgnoreElement(el: HTMLElement) { this._elIgnore.push(el); }
removeIgnoreElement(el: HTMLElement) {
// Parse the elements
for (let i = 0; i < this._elIgnore.length; i++) {
// See if this is the element to remove
if (this._elIgnore[i].isEqualNode(el)) {
// Remove it
this._elIgnore.splice(i, 1);
return;
}
}
}
setContent(el) { this._elContent = el; this.refresh(); }
// Hides the content
hide() {
// Remove it from the document
this._elContent.classList.add("d-none");
if (document.body.contains(this._elContent)) {
// Remove the element from the page
document.body.removeChild(this._elContent);
// Call the event
this._props.onHide ? this._props.onHide() : null;
}
}
// Determines if the content is visible
get isVisible(): boolean { return !this._elContent.classList.contains("d-none"); }
// Refreshes the position of the floating ui
refreshPosition() { this.refresh(); }
// Shows the content
show() {
// Append it to the document
this._elContent.classList.remove("d-none");
if (!document.body.contains(this._elContent)) {
// Add the element to the page
document.body.appendChild(this._elContent);
// Refresh the position
this.refresh();
// Call the event
this._props.onShow ? this._props.onShow() : null;
}
}
// Toggles the floating ui
toggle() { this.isVisible ? this.hide() : this.show(); }
}
export const FloatingUI = (props: IFloatingUIProps): IFloatingUI => { return new _FloatingUI(props); }