nope/resources/ui/layout/toolbar.tsx
2020-12-04 19:10:33 +01:00

362 lines
8.6 KiB
TypeScript

import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import "bootstrap/dist/css/bootstrap.min.css";
import * as React from "react";
import { Nav, Navbar, NavDropdown } from "react-bootstrap";
export interface ToolbarProps<D> {
toolbar: IToolbar<D>;
generateData(): D;
position?: "top" | "bottom";
sticky?: boolean;
bg?: "light" | "dark";
brand?: ILink;
}
/**
* Definition of a Toolbar.
*
* @export
* @interface IToolbar
* @template D
*/
export interface IToolbar<D> {
/**
* Items of the toolbar.
*
* @type {(Array<IAction<D> | IMenu<D> | ILink>)}
* @memberof IToolbar
*/
items: Array<IAction<D> | IMenu<D> | ILink>;
}
export interface IAction<D> {
type: "action";
/**
* Action which should be performed if the element
* has been clicked.
*
* @param {D} data
* @memberof IAction
*/
onClick(data: D): void;
/**
* Label of the Menu.
*
* @type {string}
* @memberof IAction
*/
label: string;
/**
* Additional Icon.
*/
icon?: IconDefinition;
}
/**
* A Menu of the Toolbar
*
* @export
* @interface IMenu
* @template D
*/
export interface IMenu<D> {
type: "menu";
/**
* The Menu ID.
*
* @type {string}
* @memberof IMenu
*/
id: string;
/**
* Label of the Menu
*
* @type {string}
* @memberof IMenu
*/
label: string;
/**
* Items of the Menu.
*
* @type {(Array<IAction<D> | ILink | IDivider>)}
* @memberof IMenu
*/
items: Array<IAction<D> | IMenu<D> | ILink | IDivider>;
}
/**
* A Classical Link.
*
* @export
* @interface ILink
*/
export interface ILink {
/**
* Type of the Element.
*
* @type {'link'}
* @memberof ILink
*/
type: "link";
/**
* Label
*
* @type {string}
* @memberof ILink
*/
label: string;
/**
* Referene, where the client should be navigated on click
*
* @type {string}
* @memberof ILink
*/
ref: string;
/**
* Additional Icon.
*/
icon?: IconDefinition | string;
}
/**
* A Divider in a Menu.
*
* @export
* @interface IDivider
*/
export interface IDivider {
/**
* Type Element
*
* @type {'divider'}
* @memberof IDivider
*/
type: "divider";
}
export type ToolbarState<D> = ToolbarProps<D>;
class Toolbar<D> extends React.Component<ToolbarProps<D>, ToolbarState<D>> {
private _currentId = 0;
constructor(props) {
super(props);
this.state = Object.assign(
{
bg: "dark",
position: "top",
sticky: true
},
this.props
);
}
_generateId() {
const id = this._currentId;
this._currentId += 1;
return id;
}
_renderMenuEntry(entry: IAction<D> | IMenu<D> | ILink) {
const _this = this;
switch (entry.type) {
case "action":
if (entry.icon) {
return (
<Nav.Link
key={this._generateId()}
onClick={(e) => {
entry.onClick(this.state.generateData());
}}
>
<FontAwesomeIcon icon={entry.icon} /> {entry.label}
</Nav.Link>
);
}
return (
<Nav.Link
key={this._generateId()}
onClick={(e) => {
entry.onClick(this.state.generateData());
}}
>
{entry.label}
</Nav.Link>
);
case "menu":
return this._renderDropdown(entry);
case "link":
if (entry.icon && typeof entry.icon === "object") {
return (
<Nav.Link key={this._generateId()} href={entry.ref}>
<FontAwesomeIcon icon={entry.icon} /> {entry.label}
</Nav.Link>
);
} else if (entry.icon && typeof entry.icon === "string") {
return (
<Nav.Link key={this._generateId()} href={entry.ref}>
<img
src={entry.icon}
width="30"
height="30"
className="d-inline-block align-top"
/>
</Nav.Link>
);
}
return (
<Nav.Link key={this._generateId()} href={entry.ref}>
{entry.label}
</Nav.Link>
);
}
}
_renderDropdown(item: IMenu<D>) {
// Issue: Drop-Down wont display the Dropdowns after opening a modal:
// Issue: https://github.com/react-bootstrap/react-bootstrap/issues/5409
// Dirty Fix: onClick={e => e.stopPropagation() in the "NavDropdown"
return (
<NavDropdown
key={this._generateId()}
id={item.id}
title={item.label}
onClick={(e) => e.stopPropagation()}
>
{/* Iterate over the deteced Modules. */}
{item.items.map((entry) => {
switch (entry.type) {
case "action":
if (entry.icon) {
return (
<NavDropdown.Item
key={this._generateId()}
onClick={(e) => {
entry.onClick(this.state.generateData());
}}
>
<FontAwesomeIcon icon={entry.icon} /> {entry.label}
</NavDropdown.Item>
);
}
return (
<Nav.Link
key={this._generateId()}
onClick={(e) => {
entry.onClick(this.state.generateData());
}}
>
{entry.label}
</Nav.Link>
);
case "divider":
return <NavDropdown.Divider key={this._generateId()} />;
case "link":
if (entry.icon && typeof entry.icon === "object") {
return (
<NavDropdown.Item key={this._generateId()} href={entry.ref}>
<FontAwesomeIcon icon={entry.icon} /> {entry.label}
</NavDropdown.Item>
);
}
return (
<NavDropdown.Item key={this._generateId()} href={entry.ref}>
{entry.label}
</NavDropdown.Item>
);
case "menu":
return this._renderDropdown(entry);
}
})}
</NavDropdown>
);
}
_renderBrand() {
switch (typeof this.state.brand.icon) {
case "string":
return (
<img
src={this.state.brand.icon}
height="30"
className="d-inline-block align-top"
/>
);
case "object":
return <FontAwesomeIcon icon={this.state.brand.icon as any} />;
}
}
/**
* Main Function to Render the Layout
*/
render() {
this._currentId = 0;
return (
<>
{this.state.sticky ? (
// Render a collapsable Navbar containing the defined Menu structure.
<Navbar
collapseOnSelect
sticky={this.state.position}
expand="lg"
bg={this.state.bg}
variant={this.state.bg}
>
{this.state.brand ? (
<>
<Navbar.Brand href={this.state.brand.ref}>
{this._renderBrand()}
{this.state.brand.label}
</Navbar.Brand>
<Navbar.Toggle aria-controls="responsive-navbar-nav" />
</>
) : (
<Navbar.Toggle aria-controls="responsive-navbar-nav" />
)}
<Navbar.Collapse id="responsive-navbar-nav">
<Nav>
{this.state.toolbar.items.map((item) =>
this._renderMenuEntry(item)
)}
</Nav>
</Navbar.Collapse>
</Navbar>
) : (
<Navbar
collapseOnSelect
fixed={this.state.position}
expand="lg"
bg={this.state.bg}
variant={this.state.bg}
>
{this.state.brand ? (
<>
<Navbar.Brand href={this.state.brand.ref}>
{this._renderBrand()}
{this.state.brand.label}
</Navbar.Brand>
<Navbar.Toggle aria-controls="responsive-navbar-nav" />
</>
) : (
<Navbar.Toggle aria-controls="responsive-navbar-nav" />
)}
<Navbar.Collapse id="responsive-navbar-nav">
<Nav>
{this.state.toolbar.items.map((item) =>
this._renderMenuEntry(item)
)}
</Nav>
</Navbar.Collapse>
</Navbar>
)}
</>
);
}
}
export default Toolbar;