/** * @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 }, 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'; } // 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 extends ToolbarProps { hotkeys?: IHotKeyAction[]; components: IDynamicWidget[] layoutSettings: { // // Basic props // // This allows setting the initial width on the server side. // This is required unless using the HOC 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 | ((resizeHandleAxis) => React.ReactElement) // // 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) => void; onUnmount?: () => void; onWidgetToggle?: (widget: IDynamicWidget, visble: boolean) => Promise; menuName?: string } export interface IDynamicLayoutState extends IDynamicLayoutProps { modalSettings?: IModalSettings modalVisible?: boolean } class DynamicLayout extends React.Component, IDynamicLayoutState> { 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) { 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; } _this.pressedKey=''; 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(settings: IModalSettings) { const _this = this; let close: () => void; return new Promise((resolve, reject) => { // Adapt the Settings: const _onSubmit = rgetattr<(data) => Promise>(settings, 'content.props.onSubmit', async (data) => { }); const _onCancel = rgetattr<(err) => Promise>(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 = 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 = (); 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) => { if (item.hideable) { return ( _this.toggleWidget(item, !item.visible)}/> { item.showLabel ? {item.label} : ''} { React.createElement(DynamicRenderer, { ...item }) } ) } return ( { item.showLabel ? {item.label} : ''} { React.createElement(DynamicRenderer, { ...item }) } ) }); const ResponsiveGridLayout = WidthProvider(Responsive); const grid = React.createElement(GridLayout, { ...this.state.layoutSettings, layout // breakpoints: { // lg: 1200, // md: 996, // sm: 768, // xs: 480, // xxs: 0 // }, // cols: { // lg: 12, // md: 10, // sm: 6, // xs: 4, // xxs: 1 // } } as any, ...children); const toast = (); const renderedElements = [toolbar, grid, toast]; if (this.state.modalVisible && this.state.modalSettings != undefined) { renderedElements.push(( { this.state.modalSettings.header ? {this.state.modalSettings.header} : '' } {/* Render the dynamic Component */} )); } return React.createElement("div", {}, ...renderedElements); } } export default DynamicLayout;