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 ]
*
* A Basic DynamicLayout .
* I uses a Toolbar , Sidebar
*
* Toast are implemented by https : //fkhadra.github.io/react-toastify/introduction
* Dynamic Account https : //github.com/STRML/react-grid-layout
*
* /
2021-02-12 07:39:03 +00:00
import { faEye , faEyeSlash , faWindowClose } from "@fortawesome/free-solid-svg-icons" ;
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" ;
import { rgetattr , rsetattr } from "../../../lib/helpers/objectMethods" ;
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 {
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']
2021-02-12 07:39:03 +00:00
resizeHandles? : Array < "s" | "w" | "e" | "n" | "sw" | "nw" | "se" | "ne" >
2020-10-30 18:30:59 +00:00
// If true and draggable, item will be moved only within grid.
isBounded? : boolean
} ,
2021-02-12 07:39:03 +00:00
preventRenderingCard? : boolean
2020-11-04 21:36:52 +00:00
showLabel? : boolean ;
2020-10-30 18:30:59 +00:00
hideable? : boolean ;
2021-02-12 07:39:03 +00:00
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'.
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.
2021-02-12 07:39:03 +00:00
compactType ? : ( "vertical" | "horizontal" ) ;
2020-10-30 18:30:59 +00:00
// 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)
2021-02-12 07:39:03 +00:00
resizeHandles? : Array < "s" | "w" | "e" | "n" | "sw" | "nw" | "se" | "ne" >
2020-10-30 18:30:59 +00:00
// 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" >
} ,
onResize ? : ( ) = > void ;
onMount ? : ( layout : DynamicLayout < CallbackData > ) = > void ;
onUnmount ? : ( ) = > void ;
onWidgetToggle ? : ( widget : IDynamicWidget , visble : boolean ) = > Promise < boolean > ;
menuName? : string
}
export interface IDynamicLayoutState < CallbackData > extends IDynamicLayoutProps < CallbackData > {
modalSettings? : IModalSettings
modalVisible? : boolean
}
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 .
* /
componentDidMount() {
const _this = this ;
// Listening to resizing Events
function handleResize() {
2021-02-12 07:39:03 +00:00
if ( typeof _this . state . onResize === "function" ) {
2020-10-30 18:30:59 +00:00
_this . state . onResize ( ) ;
}
}
function handleKeyDown ( event ) {
2020-11-04 21:36:52 +00:00
2020-10-30 18:30:59 +00:00
if ( ! _this . hotkeysEnabled || _this . state . hotkeys ? . length === 0 ) {
return ;
}
if ( _this . pressedKey !== event . code ) {
_this . pressedKey = event . code ;
try {
for ( const hotkey of _this . state . hotkeys || [ ] ) {
if ( hotkey . key === event . code ) {
2021-02-12 07:39:03 +00:00
console . log ( "perform" , hotkey . key ) ;
hotkey . onPress ( _this . state . generateData ( ) , event ) ;
2020-10-30 18:30:59 +00:00
break ;
}
}
} catch ( e ) {
}
}
}
function handleKeyUp ( event ) {
if ( ! _this . hotkeysEnabled || _this . state . hotkeys ? . length === 0 ) {
return ;
}
2021-02-12 07:39:03 +00:00
_this . pressedKey = "" ;
2020-10-30 18:30:59 +00:00
try {
for ( const hotkey of _this . state . hotkeys || [ ] ) {
if ( hotkey . key === event . code && hotkey . onRelease ) {
2021-02-12 07:39:03 +00:00
hotkey . onRelease ( _this . state . generateData ( ) , event ) ;
2020-10-30 18:30:59 +00:00
break ;
}
}
} catch ( e ) {
}
}
function handleMouse ( event ) {
_this . currentMousePosition = event ;
}
2021-02-12 07:39:03 +00:00
if ( typeof _this . state . onMount === "function" ) {
2020-10-30 18:30:59 +00:00
_this . state . onMount ( this ) ;
}
this . _handleResize = handleResize ;
this . _handleKeyDown = handleKeyDown ;
this . _handleKeyUp = handleKeyUp ;
this . _handleMouse = handleMouse ;
2021-02-12 07:39:03 +00:00
window . addEventListener ( "resize" , this . _handleResize ) ;
window . addEventListener ( "keydown" , this . _handleKeyDown ) ;
window . addEventListener ( "keyup" , this . _handleKeyUp ) ;
window . addEventListener ( "mousemove" , this . _handleMouse ) ;
2020-10-30 18:30:59 +00:00
2021-02-12 07:39:03 +00:00
this . widgets = this . props . components ;
2020-10-30 18:30:59 +00:00
2021-02-12 07:39:03 +00:00
this . _mounted = false ;
2020-10-30 18:30:59 +00:00
}
/ * *
* Function , that will be called before the network fails .
* /
componentWillUnmount() {
2021-02-12 07:39:03 +00:00
window . removeEventListener ( "resize" , this . _handleResize ) ;
window . removeEventListener ( "keydown" , this . _handleKeyDown ) ;
window . removeEventListener ( "keyup" , this . _handleKeyUp ) ;
window . removeEventListener ( "mousemove" , this . _handleMouse ) ;
2020-10-30 18:30:59 +00:00
// Call the unmount
2021-02-12 07:39:03 +00:00
if ( typeof this . state . onUnmount === "function" ) {
2020-10-30 18:30:59 +00:00
this . state . onUnmount ( ) ;
}
2021-02-12 07:39:03 +00:00
this . _mounted = false ;
2020-10-30 18:30:59 +00:00
}
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 ) = > {
2021-02-12 07:39:03 +00:00
if ( typeof _originalFunction === "function" ) {
2020-10-30 18:30:59 +00:00
_originalFunction ( _close ) ;
} else {
_close ( ) ;
}
2021-02-12 07:39:03 +00:00
} ;
2020-10-30 18:30:59 +00:00
_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:
2021-02-12 07:39:03 +00:00
const _onSubmit = rgetattr < ( data ) = > Promise < void > > ( settings , "content.props.onSubmit" , async ( data ) = > { } ) ;
const _onCancel = rgetattr < ( err ) = > Promise < void > > ( settings , "content.props.onCancel" , async ( err ) = > { } ) ;
2020-10-30 18:30:59 +00:00
const onSubmit = async ( data ) = > {
close ( ) ;
_onSubmit ( data ) ;
resolve ( data ) ;
2021-02-12 07:39:03 +00:00
} ;
2020-10-30 18:30:59 +00:00
const onCancel = async ( data ) = > {
close ( ) ;
_onCancel ( data ) ;
reject ( data ) ;
2021-02-12 07:39:03 +00:00
} ;
2020-10-30 18:30:59 +00:00
2021-02-12 07:39:03 +00:00
rsetattr ( settings , "content.props.onSubmit" , onSubmit ) ;
rsetattr ( settings , "content.props.onCancel" , onCancel ) ;
2020-10-30 18:30:59 +00:00
close = _this . openPopup ( settings ) ;
} ) ;
}
private _dynamicMenuEntry : IMenu < CallbackData > = null ;
public get dynamicMenuEntry() {
if ( this . _dynamicMenuEntry === null ) {
this . _dynamicMenuEntry = {
2021-02-12 07:39:03 +00:00
type : "menu" ,
2020-10-30 18:30:59 +00:00
id : this._menuEntryId ,
2021-02-12 07:39:03 +00:00
label : this.props.menuName || "Widgets" ,
2020-10-30 18:30:59 +00:00
items : [ ]
} ;
2021-02-12 07:39:03 +00:00
this . toolbar . push ( this . _dynamicMenuEntry ) ;
2020-10-30 18:30:59 +00:00
}
return this . _dynamicMenuEntry ;
}
public get toolbar() {
return this . state . toolbar . items ;
}
2021-02-12 07:39:03 +00:00
protected _mounted = false ;
componentDidUpdate ( prevProps :IDynamicLayoutProps < CallbackData > ) {
if ( prevProps . components != this . props . components || this . _mounted ) {
this . widgets = this . props . components ;
}
}
2020-10-30 18:30:59 +00:00
public get widgets ( ) : IDynamicWidget [ ] {
return this . state . components ;
}
public set widgets ( value : IDynamicWidget [ ] ) {
const menu = this . dynamicMenuEntry ;
const _this = this ;
menu . items = value . filter ( item = > item . hideable ) . map ( item = > {
return {
2021-02-12 07:39:03 +00:00
type : "action" ,
2020-10-30 18:30:59 +00:00
onClick() {
_this . toggleWidget ( item , ! item . visible ) ;
} ,
2021-02-12 07:39:03 +00:00
label : ( item . visible ? "Hide" : "Show" ) + " Widget \"" + item . label + "\"" ,
2020-10-30 18:30:59 +00:00
icon : ( item . visible ? faEyeSlash : faEye )
2021-02-12 07:39:03 +00:00
} ;
} ) ;
2020-10-30 18:30:59 +00:00
this . setState ( {
components : value ,
toolbar : {
items : this.toolbar
}
2021-02-12 07:39:03 +00:00
} ) ;
2020-10-30 18:30:59 +00:00
}
public async toggleWidget ( widget : IDynamicWidget | string , visibility : boolean ) {
let widgetToAdapt : IDynamicWidget ;
const widgets = this . widgets ;
if ( typeof widget === "string" ) {
for ( const _widget of widgets ) {
if ( _widget . id = widget ) {
widgetToAdapt = _widget ;
break ;
}
}
} else {
widgetToAdapt = widget ;
}
let adapted = false ;
2021-02-12 07:39:03 +00:00
if ( typeof this . state . onWidgetToggle === "function" ) {
2020-10-30 18:30:59 +00:00
// 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 ;
}
if ( adapted ) {
// Update the State.
this . widgets = widgets ;
}
}
/ * *
* 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
* /
2021-02-12 07:39:03 +00:00
public showToast ( message : string , type ? : "info" | "warning" | "success" | "error" | "default" | "dark" | "light" | "dark" , autoClose = 5000 ) {
2020-10-30 18:30:59 +00:00
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 ,
} ) ;
}
}
public render() {
const _this = this ;
const toolbar = ( < Toolbar toolbar = { this . state . toolbar } generateData = { this . state . generateData } brand = { {
2021-02-12 07:39:03 +00:00
label : "nopeBackend" ,
ref : "/" ,
type : "link" ,
2020-10-30 18:30:59 +00:00
} } > < / Toolbar > ) ;
2020-11-04 21:36:52 +00:00
let layout = this . state . components . filter ( item = > item . visible ) . map ( item = > [ item . gridSettings . x , item . gridSettings . y , item . gridSettings . w , item . gridSettings . h ] ) ;
2020-10-30 18:30:59 +00:00
layout = layout . length > 0 ? layout : undefined ;
const children = this . state . components . filter ( item = > item . visible ) . map ( ( item , idx ) = > {
2021-02-12 07:39:03 +00:00
if ( item . preventRenderingCard ) {
return ( < div
key = { idx }
data - grid = { item . gridSettings } style = { { overflow : "auto" } } >
{
React . createElement ( DynamicRenderer , { . . . item } )
}
< / div > ) ;
}
2020-10-30 18:30:59 +00:00
if ( item . hideable ) {
return (
2021-02-12 07:39:03 +00:00
< Card key = { idx } data - grid = { item . gridSettings } bg = { item . bg } border = { item . border } text = { item . text } >
2020-10-30 18:30:59 +00:00
< FontAwesomeIcon icon = { faWindowClose } style = { {
2021-02-12 07:39:03 +00:00
position : "absolute" ,
top : "-5px" ,
left : "-5px"
2020-11-04 21:36:52 +00:00
} } onClick = { _ = > _this . toggleWidget ( item , ! item . visible ) } / >
2020-10-30 18:30:59 +00:00
2021-02-12 07:39:03 +00:00
{ item . showLabel ? < Card.Header > { item . label } < / Card.Header > : "" }
2020-10-30 18:30:59 +00:00
2021-02-12 07:39:03 +00:00
< Card.Body style = { { overflow : "auto" } } >
2020-10-30 18:30:59 +00:00
{
React . createElement ( DynamicRenderer , { . . . item } )
}
< / Card.Body >
< / Card >
2021-02-12 07:39:03 +00:00
) ;
2020-10-30 18:30:59 +00:00
}
return (
< Card key = { idx } data - grid = { item . gridSettings } bg = { item . bg } border = { item . border } text = { item . text } >
2021-02-12 07:39:03 +00:00
{ item . showLabel ? < Card.Header > { item . label } < / Card.Header > : "" }
< Card.Body style = { { overflow : "auto" } } >
2020-10-30 18:30:59 +00:00
{
React . createElement ( DynamicRenderer , { . . . item } )
}
< / Card.Body >
2021-02-12 07:39:03 +00:00
< / Card > ) ;
2020-10-30 18:30:59 +00:00
} ) ;
const ResponsiveGridLayout = WidthProvider ( Responsive ) ;
2020-11-04 21:36:52 +00:00
const grid = React . createElement ( GridLayout ,
process . browser ? {
. . . this . state . layoutSettings ,
layout
} as any : {
. . . this . state . layoutSettings
} as any ,
. . . children ) ;
2020-10-30 18:30:59 +00:00
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 }
2021-02-12 07:39:03 +00:00
backdrop = { typeof this . state . modalSettings . backdrop !== undefined ? this . state . modalSettings . backdrop : "static" }
2020-10-30 18:30:59 +00:00
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 > :
2021-02-12 07:39:03 +00:00
""
2020-10-30 18:30:59 +00:00
}
< Modal.Body >
{ /* Render the dynamic Component */ }
< DynamicRenderer component = { this . state . modalSettings . content . component } props = { this . state . modalSettings . content . props } / >
< / Modal.Body >
< / Modal > ) ) ;
}
return React . createElement ( "div" , { } , . . . renderedElements ) ;
}
}
export default DynamicLayout ;