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
*
* /
import React from 'react' ;
import { Card , Modal } from 'react-bootstrap' ;
import { toast , ToastContainer } from 'react-toastify' ;
import 'react-toastify/dist/ReactToastify.css' ;
import { rgetattr , rsetattr } from '../../../lib/helpers/objectMethods' ;
import DynamicRenderer from '../dynamic/dynamicRenderer' ;
import { IHotKeyAction } from './interfaces/IHotkeyAction' ;
import { IModalSettings } from './interfaces/IModalSettings' ;
import Toolbar , { IMenu , ToolbarProps } from './toolbar' ;
import { IDynamicRenderSettings } from '../dynamic/interfaces/IDynamicRenderSettings'
import { generateId } from '../../../lib/helpers/idMethods' ;
import { faEye , faEyeSlash , faMinusSquare , faWindowClose } from '@fortawesome/free-solid-svg-icons' ;
import GridLayout , { Responsive , WidthProvider } from 'react-grid-layout' ;
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' ;
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']
resizeHandles? : Array < 's' | 'w' | 'e' | 'n' | 'sw' | 'nw' | 'se' | 'ne' >
// If true and draggable, item will be moved only within grid.
isBounded? : boolean
} ,
2020-11-04 21:36:52 +00:00
showLabel? : boolean ;
2020-10-30 18:30:59 +00:00
hideable? : boolean ;
bg ? : 'primary' | 'secondary' | 'success' | 'danger' | 'warning' | 'info' | 'dark' | 'light' ;
border ? : 'primary' | 'secondary' | 'success' | 'danger' | 'warning' | 'info' | 'dark' | 'light' ;
2020-11-04 21:36:52 +00:00
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.
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" >
} ,
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() {
if ( typeof _this . state . onResize === 'function' ) {
_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 ) {
console . log ( 'perform' , hotkey . key )
hotkey . onPress ( _this . state . generateData ( ) , event )
break ;
}
}
} catch ( e ) {
}
}
}
function handleKeyUp ( event ) {
if ( ! _this . hotkeysEnabled || _this . state . hotkeys ? . length === 0 ) {
return ;
}
2020-11-04 21:36:52 +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 ) {
hotkey . onRelease ( _this . state . generateData ( ) , event )
break ;
}
}
} catch ( e ) {
}
}
function handleMouse ( event ) {
_this . currentMousePosition = event ;
}
if ( typeof _this . state . onMount === 'function' ) {
_this . state . onMount ( this ) ;
}
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 . widgets ;
}
/ * *
* Function , that will be called before the network fails .
* /
componentWillUnmount() {
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 ( ) ;
}
}
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 get dynamicMenuEntry() {
if ( this . _dynamicMenuEntry === null ) {
this . _dynamicMenuEntry = {
type : 'menu' ,
id : this._menuEntryId ,
label : this.props.menuName || 'Widgets' ,
items : [ ]
} ;
this . toolbar . push ( this . _dynamicMenuEntry )
}
return this . _dynamicMenuEntry ;
}
public get toolbar() {
return this . state . toolbar . items ;
}
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 {
type : 'action' ,
onClick() {
_this . toggleWidget ( item , ! item . visible ) ;
} ,
label : ( item . visible ? 'Hide' : 'Show' ) + ' Widget "' + item . label + '"' ,
icon : ( item . visible ? faEyeSlash : faEye )
}
} )
this . setState ( {
components : value ,
toolbar : {
items : this.toolbar
}
} )
}
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 ;
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 ;
}
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
* /
public showToast ( message : string , type ? : 'info' | 'warning' | 'success' | 'error' | 'default' | 'dark' | 'light' | 'dark' , autoClose = 5000 ) {
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 = { {
label : 'nopeBackend' ,
ref : '/' ,
type : 'link' ,
} } > < / 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 ) = > {
if ( item . hideable ) {
return (
< Card key = { idx } data - grid = { item . gridSettings } bg = { item . bg } border = { item . border } text = { item . text } >
< FontAwesomeIcon icon = { faWindowClose } style = { {
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
{ item . showLabel ? < Card.Header > { item . label } < / Card.Header > : '' }
2020-11-04 21:36:52 +00:00
< Card.Body style = { { overflow : 'scroll' } } >
2020-10-30 18:30:59 +00:00
{
React . createElement ( DynamicRenderer , { . . . item } )
}
< / Card.Body >
< / Card >
)
}
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 : 'scroll' } } >
{
React . createElement ( DynamicRenderer , { . . . item } )
}
< / Card.Body >
< / Card > )
} ) ;
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 }
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 > ) ) ;
}
return React . createElement ( "div" , { } , . . . renderedElements ) ;
}
}
export default DynamicLayout ;