nope/resources/ui/layout/dynamicLayout.tsx

764 lines
21 KiB
TypeScript
Raw Normal View History

2020-10-30 18:30:59 +00:00
/**
* @author Martin Karkowski
* @email m.karkowski@zema.de
* @create date 2019-02-20 09:19:06
* @modify date 2020-10-29 09:55:30
* @desc [description]
2021-08-26 17:41:08 +00:00
*
2020-10-30 18:30:59 +00:00
* A Basic DynamicLayout.
* I uses a Toolbar, Sidebar
2021-08-26 17:41:08 +00:00
*
2020-10-30 18:30:59 +00:00
* Toast are implemented by https://fkhadra.github.io/react-toastify/introduction
* Dynamic Account https://github.com/STRML/react-grid-layout
2021-08-26 17:41:08 +00:00
*
2020-10-30 18:30:59 +00:00
*/
2021-08-26 17:41:08 +00:00
import {
faEye,
faEyeSlash,
faWindowClose,
} from "@fortawesome/free-solid-svg-icons";
2021-02-12 07:39:03 +00:00
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import React from "react";
import { Card, Modal } from "react-bootstrap";
import GridLayout, { Responsive, WidthProvider } from "react-grid-layout";
import { toast, ToastContainer } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
import { generateId } from "../../../lib/helpers/idMethods";
2021-08-26 17:41:08 +00:00
import {
deepClone,
rgetattr,
rsetattr,
} from "../../../lib/helpers/objectMethods";
2021-02-12 07:39:03 +00:00
import DynamicRenderer from "../dynamic/dynamicRenderer";
import { IDynamicRenderSettings } from "../dynamic/interfaces/IDynamicRenderSettings";
import { IHotKeyAction } from "./interfaces/IHotkeyAction";
import { IModalSettings } from "./interfaces/IModalSettings";
import Toolbar, { IMenu, ToolbarProps } from "./toolbar";
2020-10-30 18:30:59 +00:00
export interface IDynamicWidget extends IDynamicRenderSettings {
2021-08-26 17:41:08 +00:00
id: string;
label: string;
visible: boolean;
gridSettings: {
// A string corresponding to the component key
i?: string;
// These are all in grid units, not pixels
x: number;
y: number;
w: number;
h: number;
minW?: number;
maxW?: number;
minH?: number;
maxH?: number;
// If true, equal to `isDraggable: false, isResizable: false`.
static?: boolean;
// If false, will not be draggable. Overrides `static`.
isDraggable?: boolean;
// If false, will not be resizable. Overrides `static`.
isResizable?: boolean;
// By default, a handle is only shown on the bottom-right (southeast) corner.
// Note that resizing from the top or left is generally not intuitive.
// Defaults to ['se']
resizeHandles?: Array<"s" | "w" | "e" | "n" | "sw" | "nw" | "se" | "ne">;
// If true and draggable, item will be moved only within grid.
isBounded?: boolean;
};
preventRenderingCard?: boolean;
showLabel?: boolean;
hideable?: boolean;
bg?:
| "primary"
| "secondary"
| "success"
| "danger"
| "warning"
| "info"
| "dark"
| "light";
border?:
| "primary"
| "secondary"
| "success"
| "danger"
| "warning"
| "info"
| "dark"
| "light";
text?:
| "primary"
| "secondary"
| "success"
| "danger"
| "warning"
| "info"
| "dark"
| "light";
2020-10-30 18:30:59 +00:00
}
// All callbacks below have signature (layout, oldItem, newItem, placeholder, e, element).
// 'start' and 'stop' callbacks pass `undefined` for 'placeholder'.
2021-08-26 17:41:08 +00:00
export type ItemCallback = (
layout,
oldItem,
newItem,
placeholder,
e: MouseEvent,
element: HTMLElement
) => void;
export interface IDynamicLayoutProps<CallbackData>
extends ToolbarProps<CallbackData> {
hotkeys?: IHotKeyAction<CallbackData>[];
components: IDynamicWidget[];
layoutSettings: {
//
// Basic props
//
// This allows setting the initial width on the server side.
// This is required unless using the HOC <WidthProvider> or similar
width?: number;
// If true, the container height swells and contracts to fit contents
autoSize?: boolean;
// Number of columns in this layout.
cols?: number;
// A CSS selector for tags that will not be draggable.
// For example: draggableCancel:'.MyNonDraggableAreaClassName'
// If you forget the leading . it will not work.
draggableCancel?: string;
// A CSS selector for tags that will act as the draggable handle.
// For example: draggableHandle:'.MyDragHandleClassName'
// If you forget the leading . it will not work.
draggableHandle?: string;
// If true, the layout will compact vertically
verticalCompact?: boolean;
// Compaction type.
compactType?: "vertical" | "horizontal";
// Layout is an array of object with the format:
// {x: number, y: number, w: number, h: number}
// The index into the layout must match the key used on each item component.
// If you choose to use custom keys, you can specify that key in the layout
// array objects like so:
// {i: string, x: number, y: number, w: number, h: number}
layout?:
| Array<[number, number, number, number]>
| Array<{ i: string; x: number; y: number; w: number; h: number }>; // If not provided, use data-grid props on children
// Margin between items [x, y] in px.
margin?: [number, number];
// Padding inside the container [x, y] in px
containerPadding?: [number, number];
// Rows have a static height, but you can change this based on breakpoints
// if you like.
rowHeight?: number;
//
// Flags
//
isDraggable?: boolean;
isResizable?: boolean;
isBounded?: boolean;
// Uses CSS3 translate() instead of position top/left.
// This makes about 6x faster paint performance
useCSSTransforms?: boolean;
// If parent DOM node of ResponsiveReactGridLayout or ReactGridLayout has "transform: scale(n)" css property,
// we should set scale coefficient to avoid render artefacts while dragging.
transformScale?: number;
// If true, grid items won't change position when being
// dragged over.
preventCollision?: boolean;
// If true, droppable elements (with `draggable={true}` attribute)
// can be dropped on the grid. It triggers "onDrop" callback
// with position and event object as parameters.
// It can be useful for dropping an element in a specific position
//
// NOTE: In case of using Firefox you should add
// `onDragStart={e => e.dataTransfer.setData('text/plain', '')}` attribute
// along with `draggable={true}` otherwise this feature will work incorrect.
// onDragStart attribute is required for Firefox for a dragging initialization
// @see https://bugzilla.mozilla.org/show_bug.cgi?id=568313
isDroppable?: boolean;
// Defines which resize handles should be rendered
// Allows for any combination of:
// 's' - South handle (bottom-center)
// 'w' - West handle (left-center)
// 'e' - East handle (right-center)
// 'n' - North handle (top-center)
// 'sw' - Southwest handle (bottom-left)
// 'nw' - Northwest handle (top-left)
// 'se' - Southeast handle (bottom-right)
// 'ne' - Northeast handle (top-right)
resizeHandles?: Array<"s" | "w" | "e" | "n" | "sw" | "nw" | "se" | "ne">;
// Custom component for resize handles
resizeHandle?:
| React.ReactElement<any>
| ((resizeHandleAxis) => React.ReactElement<any>);
//
// Callbacks
//
// Callback so you can save the layout.
// Calls back with (currentLayout) after every drag or resize stop.
onLayoutChange?: (layout: GridLayout) => void;
// Calls when drag starts.
onDragStart?: ItemCallback;
// Calls on each drag movement.
onDrag?: ItemCallback;
// Calls when drag is complete.
onDragStop?: ItemCallback;
// Calls when resize starts.
onResizeStart?: ItemCallback;
// Calls when resize movement happens.
onResize?: ItemCallback;
// Calls when resize is complete.
onResizeStop?: ItemCallback;
// Calls when an element has been dropped into the grid from outside.
onDrop?: (layout, item, e: Event) => void;
// Ref for getting a reference for the grid's wrapping div.
// You can use this instead of a regular ref and the deprecated `ReactDOM.findDOMNode()`` function.
innerRef?: React.Ref<"div">;
};
onChanged?: (settings: IDynamicLayoutProps<CallbackData>) => void;
onResize?: () => void;
onMount?: (layout: DynamicLayout<CallbackData>) => void;
onUnmount?: () => void;
onWidgetToggle?: (
widget: IDynamicWidget,
visble: boolean
) => Promise<boolean>;
menuName?: string;
2020-10-30 18:30:59 +00:00
}
2021-08-26 17:41:08 +00:00
export interface IDynamicLayoutState<CallbackData>
extends IDynamicLayoutProps<CallbackData> {
modalSettings?: IModalSettings;
modalVisible?: boolean;
2020-10-30 18:30:59 +00:00
}
2021-08-26 17:41:08 +00:00
export class DynamicLayout<CallbackData> extends React.Component<
IDynamicLayoutProps<CallbackData>,
IDynamicLayoutState<CallbackData>
> {
protected _handleResize: () => void;
protected _handleKeyDown: (event) => void;
protected _handleKeyUp: (event) => void;
protected _handleMouse: (event: MouseEvent) => void;
protected _menuEntryId: string;
public hotkeysEnabled = true;
public pressedKey: string = null;
public currentMousePosition: MouseEvent;
/**
* Function will be called if the Item has been rendered sucessfully.
*/
public componentDidMount(): void {
const _this = this;
// Listening to resizing Events
function handleResize() {
if (typeof _this.state.onResize === "function") {
_this.state.onResize();
}
}
2020-10-30 18:30:59 +00:00
2021-08-26 17:41:08 +00:00
function handleKeyDown(event) {
if (!_this.hotkeysEnabled || _this.state.hotkeys?.length === 0) {
return;
}
2020-10-30 18:30:59 +00:00
2021-08-26 17:41:08 +00:00
if (_this.pressedKey !== event.code) {
_this.pressedKey = event.code;
2020-10-30 18:30:59 +00:00
2021-08-26 17:41:08 +00:00
try {
for (const hotkey of _this.state.hotkeys || []) {
if (hotkey.key === event.code) {
console.log("perform", hotkey.key);
hotkey.onPress(_this.state.generateData(), event);
break;
2020-10-30 18:30:59 +00:00
}
2021-08-26 17:41:08 +00:00
}
} catch (e) {}
}
2020-10-30 18:30:59 +00:00
}
2021-08-26 17:41:08 +00:00
function handleKeyUp(event) {
if (!_this.hotkeysEnabled || _this.state.hotkeys?.length === 0) {
return;
}
_this.pressedKey = "";
try {
for (const hotkey of _this.state.hotkeys || []) {
if (hotkey.key === event.code && hotkey.onRelease) {
hotkey.onRelease(_this.state.generateData(), event);
break;
}
2020-10-30 18:30:59 +00:00
}
2021-08-26 17:41:08 +00:00
} catch (e) {}
2020-10-30 18:30:59 +00:00
}
2021-08-26 17:41:08 +00:00
function handleMouse(event) {
_this.currentMousePosition = event;
2020-10-30 18:30:59 +00:00
}
2021-08-26 17:41:08 +00:00
if (typeof _this.state.onMount === "function") {
_this.state.onMount(this);
2020-10-30 18:30:59 +00:00
}
2021-08-26 17:41:08 +00:00
this._handleResize = handleResize;
this._handleKeyDown = handleKeyDown;
this._handleKeyUp = handleKeyUp;
this._handleMouse = handleMouse;
window.addEventListener("resize", this._handleResize);
window.addEventListener("keydown", this._handleKeyDown);
window.addEventListener("keyup", this._handleKeyUp);
window.addEventListener("mousemove", this._handleMouse);
this.widgets = this.props.components;
this._mounted = false;
}
/**
* Function, that will be called before the network fails.
*/
public componentWillUnmount(): void {
window.removeEventListener("resize", this._handleResize);
window.removeEventListener("keydown", this._handleKeyDown);
window.removeEventListener("keyup", this._handleKeyUp);
window.removeEventListener("mousemove", this._handleMouse);
// Call the unmount
if (typeof this.state.onUnmount === "function") {
this.state.onUnmount();
2020-10-30 18:30:59 +00:00
}
2021-08-26 17:41:08 +00:00
this._mounted = false;
}
public get currentConfig(): IDynamicLayoutProps<CallbackData> {
const ret = deepClone(this.state);
return ret;
}
constructor(props) {
super(props);
this.state = Object.assign(
{
modalSettings: null,
modalVisible: false,
},
this.props
);
// Create a Menu Entry.
this._menuEntryId = generateId();
}
public openPopup(settings: IModalSettings) {
const modalSettings = settings;
const _this = this;
const _close = () => {
_this.setState({
modalVisible: false,
});
};
const _originalFunction = modalSettings.onHide;
modalSettings.onHide = (...args) => {
if (typeof _originalFunction === "function") {
_originalFunction(_close);
} else {
_close();
}
};
_this.setState({
modalVisible: true,
modalSettings: modalSettings,
});
// Return the Close Method.
return _close;
}
public getDialogData<T = any>(settings: IModalSettings) {
const _this = this;
let close: () => void;
return new Promise<T>((resolve, reject) => {
// Adapt the Settings:
const _onSubmit = rgetattr<(data) => Promise<void>>(
settings,
"content.props.onSubmit",
async (data) => {}
);
const _onCancel = rgetattr<(err) => Promise<void>>(
settings,
"content.props.onCancel",
async (err) => {}
);
const onSubmit = async (data) => {
close();
_onSubmit(data);
resolve(data);
};
const onCancel = async (data) => {
close();
_onCancel(data);
reject(data);
};
rsetattr(settings, "content.props.onSubmit", onSubmit);
rsetattr(settings, "content.props.onCancel", onCancel);
close = _this.openPopup(settings);
});
}
private _dynamicMenuEntry: IMenu<CallbackData> = null;
public refresh(widget: string) {
if (this._references[widget]) {
this._references[widget].current.forceUpdate();
}
}
public get dynamicMenuEntry() {
if (this._dynamicMenuEntry === null) {
this._dynamicMenuEntry = {
type: "menu",
id: this._menuEntryId,
label: this.props.menuName || "Widgets",
items: [],
};
this.toolbar.push(this._dynamicMenuEntry);
}
2020-10-30 18:30:59 +00:00
2021-08-26 17:41:08 +00:00
return this._dynamicMenuEntry;
}
2020-10-30 18:30:59 +00:00
2021-08-26 17:41:08 +00:00
public get toolbar() {
return this.state.toolbar.items;
}
2020-10-30 18:30:59 +00:00
2021-08-26 17:41:08 +00:00
protected _mounted = false;
2020-10-30 18:30:59 +00:00
2021-08-26 17:41:08 +00:00
componentDidUpdate(prevProps: IDynamicLayoutProps<CallbackData>) {
if (prevProps.components != this.props.components || this._mounted) {
this.widgets = this.props.components;
}
}
protected _references: { [index: string]: React.RefObject<DynamicRenderer> } =
{};
public get widgets(): IDynamicWidget[] {
return this.state.components;
}
public set widgets(value: IDynamicWidget[]) {
const menu = this.dynamicMenuEntry;
const _this = this;
// Create References
value.map((item) => {
_this._references[item.id] = React.createRef();
});
menu.items = value
.filter((item) => item.hideable)
.map((item) => {
return {
type: "action",
onClick() {
_this.toggleWidget(item, !item.visible);
},
label:
(item.visible ? "Hide" : "Show") + ' Widget "' + item.label + '"',
icon: item.visible ? faEyeSlash : faEye,
};
});
2020-10-30 18:30:59 +00:00
2021-08-26 17:41:08 +00:00
this.setState({
components: value,
toolbar: {
items: this.toolbar,
},
});
2020-10-30 18:30:59 +00:00
2021-08-26 17:41:08 +00:00
if (typeof this.props.onChanged == "function") {
this.props.onChanged(this.currentConfig);
2020-10-30 18:30:59 +00:00
}
2021-08-26 17:41:08 +00:00
}
2020-10-30 18:30:59 +00:00
2021-08-26 17:41:08 +00:00
public async toggleWidget(
widget: IDynamicWidget | string,
visibility: boolean
): Promise<void> {
let widgetToAdapt: IDynamicWidget;
2020-10-30 18:30:59 +00:00
2021-08-26 17:41:08 +00:00
const widgets = this.widgets;
2021-02-12 07:39:03 +00:00
2021-08-26 17:41:08 +00:00
if (typeof widget === "string") {
for (const _widget of widgets) {
if (_widget.id == widget) {
widgetToAdapt = _widget;
break;
2021-02-12 07:39:03 +00:00
}
2021-08-26 17:41:08 +00:00
}
} else {
widgetToAdapt = widget;
2021-02-12 07:39:03 +00:00
}
2021-08-26 17:41:08 +00:00
let adapted = false;
if (typeof this.state.onWidgetToggle === "function") {
// Call the Async Function to select a Tab.
if (await this.state.onWidgetToggle(widgetToAdapt, visibility)) {
widgetToAdapt.visible = visibility;
adapted = true;
}
} else {
// Change the visiblity.
widgetToAdapt.visible = visibility;
adapted = true;
2020-10-30 18:30:59 +00:00
}
2021-08-26 17:41:08 +00:00
if (adapted) {
// Update the State.
this.widgets = widgets;
2020-10-30 18:30:59 +00:00
}
2021-08-26 17:41:08 +00:00
}
/**
* Helper Function to render a Toast.
* @param message The Message of the Toast
* @param type The Type.
* @param timeout A Timeout in [ms], after which the Toast should disapear. 0 = infinity
*/
public showToast(
message: string,
type?:
| "info"
| "warning"
| "success"
| "error"
| "default"
| "dark"
| "light"
| "dark",
autoClose = 5000
): void {
if (type !== undefined && type !== null) {
toast[type](message, {
position: "top-center",
autoClose,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
});
} else {
toast(message, {
position: "top-center",
autoClose,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
});
2020-10-30 18:30:59 +00:00
}
2021-08-26 17:41:08 +00:00
}
public render() {
const _this = this;
const toolbar = (
<Toolbar
toolbar={this.state.toolbar}
generateData={this.state.generateData}
brand={{
label: "nopeBackend",
ref: "/",
type: "link",
}}
></Toolbar>
);
let layout = this.state.components
.filter((item) => item.visible)
.map((item) => [
item.gridSettings.x,
item.gridSettings.y,
item.gridSettings.w,
item.gridSettings.h,
]);
layout = layout.length > 0 ? layout : undefined;
const children = this.state.components
.filter((item) => item.visible)
.map((item, idx) => {
item.id;
if (item.preventRenderingCard) {
return (
<div
key={idx}
data-grid={item.gridSettings}
style={{ overflow: "auto" }}
>
{React.createElement(DynamicRenderer, {
...item,
ref: _this._references[item.id],
})}
</div>
);
2020-10-30 18:30:59 +00:00
}
2021-08-26 17:41:08 +00:00
if (item.hideable) {
return (
<Card
key={idx}
data-grid={item.gridSettings}
bg={item.bg}
border={item.border}
text={item.text}
2020-10-30 18:30:59 +00:00
>
2021-08-26 17:41:08 +00:00
<FontAwesomeIcon
icon={faWindowClose}
style={{
position: "absolute",
top: "-5px",
left: "-5px",
}}
onClick={(_) => _this.toggleWidget(item, !item.visible)}
/>
{item.showLabel ? <Card.Header>{item.label}</Card.Header> : ""}
<Card.Body style={{ overflow: "auto" }}>
{React.createElement(DynamicRenderer, {
...item,
ref: _this._references[item.id],
})}
</Card.Body>
</Card>
);
2020-10-30 18:30:59 +00:00
}
2021-08-26 17:41:08 +00:00
return (
<Card
key={idx}
data-grid={item.gridSettings}
bg={item.bg}
border={item.border}
text={item.text}
>
{item.showLabel ? <Card.Header>{item.label}</Card.Header> : ""}
<Card.Body style={{ overflow: "auto" }}>
{React.createElement(DynamicRenderer, {
...item,
ref: _this._references[item.id],
})}
</Card.Body>
</Card>
);
});
const ResponsiveGridLayout = WidthProvider(Responsive);
const grid = React.createElement(
GridLayout,
process.browser
? ({
...this.state.layoutSettings,
layout,
} as any)
: ({
...this.state.layoutSettings,
} as any),
...children
);
const toast = (
<ToastContainer
position="top-center"
autoClose={5000}
hideProgressBar={false}
newestOnTop={false}
closeOnClick
rtl={false}
pauseOnFocusLoss
draggable
pauseOnHover
/>
);
const renderedElements = [toolbar, grid, toast];
if (this.state.modalVisible && this.state.modalSettings != undefined) {
renderedElements.push(
<Modal
show={this.state.modalVisible}
backdrop={
typeof this.state.modalSettings.backdrop !== undefined
? this.state.modalSettings.backdrop
: "static"
}
keyboard={false}
size={this.state.modalSettings.size}
centered={this.state.modalSettings.centered || false}
onHide={this.state.modalSettings.onHide}
scrollable
>
{this.state.modalSettings.header ? (
<Modal.Header>
<Modal.Title>{this.state.modalSettings.header}</Modal.Title>
</Modal.Header>
) : (
""
)}
<Modal.Body>
{/* Render the dynamic Component */}
<DynamicRenderer
component={this.state.modalSettings.content.component}
props={this.state.modalSettings.content.props}
/>
</Modal.Body>
</Modal>
);
2020-10-30 18:30:59 +00:00
}
2021-08-26 17:41:08 +00:00
return React.createElement("div", {}, ...renderedElements);
}
2020-10-30 18:30:59 +00:00
}
2021-08-26 17:41:08 +00:00
export default DynamicLayout;