diff --git a/README.md b/README.md index f7d2439..a099f6d 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,15 @@ Plugins are required to create custom widgets. ### Props ```ts +type ItemManipulationCallback = (eventData: { + layout: Layouts; + oldItem: Layout; + newItem: Layout; + placeholder: Layout; + e: MouseEvent; + element: HTMLElement; +}) => void; + interface DashKitProps { config: Config; editMode: boolean; @@ -32,6 +41,14 @@ interface DashKitProps { onDrop: (dropProps: ItemDropProps) => void; onItemMountChange: (item: ConfigItem, state: {isAsync: boolead; isMounted: boolean}) => void; onItemRender: (item: ConfigItem) => void; + + onDragStart?: ItemManipulationCallback; + onDrag?: ItemManipulationCallback; + onDragStop?: ItemManipulationCallback; + onResizeStart?: ItemManipulationCallback; + onResize?: ItemManipulationCallback; + onResizeStop?: ItemManipulationCallback; + defaultGlobalParams: GlobalParams; globalParams: GlobalParams; itemsStateAndParams: ItemsStateAndParams; @@ -61,6 +78,12 @@ interface DashKitProps { - **noOverlay**: If `true`, overlay and controls are not displayed while editing. - **focusable**: If `true`, grid items will be focusable. - **draggableHandleClassName** : СSS class name of the element that makes the widget draggable. +- **onDragStart**: ReactGridLayout called when item drag started +- **onDrag**: ReactGridLayout called while item drag +- **onDragStop**: ReactGridLayout called when item drag stopped +- **onResizeStart**: ReactGridLayout called when item resize started +- **onResize**: ReactGridLayout called while item resizing +- **onResizeStop**: ReactGridLayout called when item resize stoped ## Usage diff --git a/src/components/DashKit/DashKit.tsx b/src/components/DashKit/DashKit.tsx index edefd71..0892770 100644 --- a/src/components/DashKit/DashKit.tsx +++ b/src/components/DashKit/DashKit.tsx @@ -18,6 +18,7 @@ import { ContextProps, DashKitGroup, GridReflowOptions, + ItemManipulationCallback, MenuItem, Plugin, ReactGridLayoutProps, @@ -45,9 +46,18 @@ interface DashKitDefaultProps { itemsStateAndParams: ItemsStateAndParams; groups?: DashKitGroup[]; }) => void; - onDrop: (dropProps: ItemDropProps) => void; + onDrop?: (dropProps: ItemDropProps) => void; + onItemMountChange?: (item: ConfigItem, state: {isAsync: boolean; isMounted: boolean}) => void; onItemRender?: (item: ConfigItem) => void; + + onDragStart?: ItemManipulationCallback; + onDrag?: ItemManipulationCallback; + onDragStop?: ItemManipulationCallback; + onResizeStart?: ItemManipulationCallback; + onResize?: ItemManipulationCallback; + onResizeStop?: ItemManipulationCallback; + defaultGlobalParams: GlobalParams; globalParams: GlobalParams; itemsStateAndParams: ItemsStateAndParams; diff --git a/src/components/DashKit/__stories__/DashKitGroupsShowcase.tsx b/src/components/DashKit/__stories__/DashKitGroupsShowcase.tsx index be809de..7630608 100644 --- a/src/components/DashKit/__stories__/DashKitGroupsShowcase.tsx +++ b/src/components/DashKit/__stories__/DashKitGroupsShowcase.tsx @@ -13,6 +13,7 @@ import { DashKitGroup, DashKitProps, DashkitGroupRenderProps, + ItemManipulationCallback, ReactGridLayoutProps, } from '../../..'; import {DEFAULT_GROUP, MenuItems} from '../../../helpers'; @@ -247,6 +248,25 @@ export const DashKitGroupsShowcase: React.FC = () => { [config, groups], ); + const updateConfigOrder = React.useCallback( + (eventProps) => { + const index = config.items.findIndex((item) => item.id === eventProps.newItem.i); + + const copyItems = [...config.items]; + copyItems.push(copyItems.splice(index, 1)[0]); + + const copyLyaout = [...config.layout]; + copyLyaout.push(copyLyaout.splice(index, 1)[0]); + + setConfig({ + ...config, + items: copyItems, + layout: copyLyaout, + }); + }, + [config], + ); + return ( { @@ -270,6 +290,8 @@ export const DashKitGroupsShowcase: React.FC = () => { onChange={onChange} onDrop={onDrop} overlayMenuItems={overlayMenuItems} + onDragStart={updateConfigOrder} + onResizeStart={updateConfigOrder} /> diff --git a/src/components/GridLayout/GridLayout.js b/src/components/GridLayout/GridLayout.js index 2a9934f..5f8fbfb 100644 --- a/src/components/GridLayout/GridLayout.js +++ b/src/components/GridLayout/GridLayout.js @@ -7,6 +7,7 @@ import { TEMPORARY_ITEM_ID, } from '../../constants'; import {DashKitContext} from '../../context/DashKitContext'; +import {resolveLayoutGroup} from '../../utils'; import GridItem from '../GridItem/GridItem'; import {Layout} from './ReactGridLayout'; @@ -93,7 +94,7 @@ export default class GridLayout extends React.PureComponent { return temporaryLayout?.data || layout; } - getMemoGroupLayout = (group, layout) => { + getMemoGroupLayout(group, layout) { // fastest possible way to match json const key = JSON.stringify(layout); @@ -112,20 +113,29 @@ export default class GridLayout extends React.PureComponent { } return this._memoGroupsLayouts[group]; - }; + } - getMemoGroupCallbacks = (group) => { + getMemoGroupCallbacks(group) { if (!this._memoCallbacksForGroups[group]) { const onDragStart = this._onDragStart.bind(this, group); - const onStop = this._onStop.bind(this, group); + const onDrag = this._onDrag.bind(this, group); + const onDragStop = this._onDragStop.bind(this, group); + + const onResizeStart = this._onResizeStart.bind(this, group); + const onResize = this._onResize.bind(this, group); + const onResizeStop = this._onResizeStop.bind(this, group); + const onDrop = this._onDrop.bind(this, group); const onDropDragOver = this._onDropDragOver.bind(this, group); const onDragTargetRestore = this._onTargetRestore.bind(this, group); this._memoCallbacksForGroups[group] = { onDragStart, - onDragStop: onStop, - onResizeStop: onStop, + onDrag, + onDragStop, + onResizeStart, + onResize, + onResizeStop, onDrop, onDropDragOver, onDragTargetRestore, @@ -133,7 +143,7 @@ export default class GridLayout extends React.PureComponent { } return this._memoCallbacksForGroups[group]; - }; + } getMemoGroupProps = (group, renderLayout, properties) => { // Needed for _onDropDragOver @@ -183,7 +193,7 @@ export default class GridLayout extends React.PureComponent { }, {}); return renderLayout.map((currentItem) => { - const itemParent = itemsByGroup[currentItem.i].parent || DEFAULT_GROUP; + const itemParent = resolveLayoutGroup(itemsByGroup[currentItem.i]); if (itemParent === group) { return newItemsLayoutById[currentItem.i]; @@ -217,42 +227,109 @@ export default class GridLayout extends React.PureComponent { } } - _onDragStart = (group, _newLayout, layoutItem, _newItem, _placeholder, e) => { - if (this.temporaryLayout) return; + prepareDefaultArguments(group, layout, oldItem, newItem, placeholder, e, element) { + return { + group, + layout, + oldItem, + newItem, + placeholder, + e, + element, + }; + } + + updateeDraggingElementState(group, layoutItem, e) { + let currentDraggingElement = this.state.currentDraggingElement; + + if (!currentDraggingElement) { + const {temporaryLayout} = this.context; + const layoutId = layoutItem.i; + + const item = temporaryLayout + ? temporaryLayout.dragProps + : this.context.config.items.find(({id}) => id === layoutId); + const {offsetX, offsetY} = e.nativeEvent; + + currentDraggingElement = { + group, + layoutItem, + item, + cursorPosition: {offsetX, offsetY}, + }; + } + + this.setState({currentDraggingElement, draggedOverGroup: group}); + } + + _onDragStart(group, _newLayout, layoutItem, _newItem, _placeholder, e, element) { + this.context.onDragStart?.call( + this, + this.prepareDefaultArguments( + group, + _newLayout, + layoutItem, + _newItem, + _placeholder, + e, + element, + ), + ); if (this.context.dragOverPlugin) { this.setState({isDragging: true}); } else { - let currentDraggingElement = this.state.currentDraggingElement; - - if (!currentDraggingElement) { - const layoutId = layoutItem.i; - const item = this.context.config.items.find(({id}) => id === layoutId); - const {offsetX, offsetY} = e.nativeEvent; - - currentDraggingElement = { - group, - layoutItem, - item, - cursorPosition: {offsetX, offsetY}, - }; - } - - this.setState({ - isDragging: true, - currentDraggingElement, - draggedOverGroup: group, - }); + this.updateeDraggingElementState(group, layoutItem, e); + this.setState({isDragging: true}); } - }; + } + + _onDrag(group, layout, oldItem, newItem, placeholder, e, element) { + this.context.onDrag?.call( + this, + this.prepareDefaultArguments(group, layout, oldItem, newItem, placeholder, e, element), + ); + } + + _onDragStop(group, layout, oldItem, newItem, placeholder, e, element) { + this._onStop(group, layout); - _onResizeStart = () => { + this.context.onDragStop?.call( + this, + this.prepareDefaultArguments(group, layout, oldItem, newItem, placeholder, e, element), + ); + } + + _onResizeStart(group, layout, oldItem, newItem, placeholder, e, element) { this.setState({ isDragging: true, }); - }; - _onTargetRestore = () => { + this.context.onResizeStart?.call( + this, + this.prepareDefaultArguments(group, layout, oldItem, newItem, placeholder, e, element), + ); + } + + _onResize(group, layout, oldItem, newItem, placeholder, e, element) { + this.context.onResize?.call( + this, + this.prepareDefaultArguments(group, layout, oldItem, newItem, placeholder, e, element), + ); + } + + _onResizeStop(group, layout, oldItem, newItem, placeholder, e, element) { + this.context.onResizeStop?.call( + this, + this.prepareDefaultArguments(group, layout, oldItem, newItem, placeholder, e, element), + ); + } + + _onTargetRestore() { + if (this.context.temporaryLayout) { + return; + } + const {currentDraggingElement} = this.state; if (currentDraggingElement) { @@ -260,7 +337,7 @@ export default class GridLayout extends React.PureComponent { draggedOverGroup: currentDraggingElement.group, }); } - }; + } _onStop = (group, newLayout) => { const {layoutChange, onDrop, temporaryLayout} = this.context; @@ -328,6 +405,7 @@ export default class GridLayout extends React.PureComponent { draggedOverGroup: null, }); + // TODO temporaryLayout layoutChange(groupedLayout); }; @@ -358,9 +436,14 @@ export default class GridLayout extends React.PureComponent { }; _onDropDragOver = (group, e) => { - const {editMode, dragOverPlugin, onDropDragOver} = this.context; + const {editMode, dragOverPlugin, onDropDragOver, temporaryLayout} = this.context; const {currentDraggingElement} = this.state; + // TODO If temporary item is trying to change group + if (temporaryLayout && currentDraggingElement) { + return false; + } + if (!editMode || (!dragOverPlugin && !currentDraggingElement)) { return false; } @@ -443,31 +526,37 @@ export default class GridLayout extends React.PureComponent { return ( {renderItems.map((item, i) => { return ( diff --git a/src/hocs/withContext.js b/src/hocs/withContext.js index bcd2237..c15be22 100644 --- a/src/hocs/withContext.js +++ b/src/hocs/withContext.js @@ -17,7 +17,7 @@ import { } from '../context/DashKitContext'; import {useDeepEqualMemo} from '../hooks/useDeepEqualMemo'; import {getItemsParams, getItemsState} from '../shared'; -import {UpdateManager} from '../utils'; +import {UpdateManager, resolveLayoutGroup} from '../utils'; const ITEM_PROPS = ['i', 'h', 'w', 'x', 'y', 'parent']; @@ -219,7 +219,7 @@ function useMemoStateContext(props) { if (hasNowrapGroups) { layout.forEach((item) => { const widgetId = item.i; - const parentId = item.parent || DEFAULT_GROUP; + const parentId = resolveLayoutGroup(item); if (nowrapGroups[parentId]) { // Collecting nowrap elements @@ -390,7 +390,7 @@ function useMemoStateContext(props) { } setTemporaryLayout({ - data: newLayout, + data: [...newLayout, item], dragProps, }); @@ -440,6 +440,14 @@ function useMemoStateContext(props) { draggableHandleClassName: props.draggableHandleClassName, outerDnDEnable, dragOverPlugin, + + /* default bypassing handlers */ + onDragStart: props.onDragStart, + onDrag: props.onDrag, + onDragStop: props.onDragStop, + onResizeStart: props.onResizeStart, + onResize: props.onResize, + onResizeStop: props.onResizeStop, }), [ resultLayout, @@ -469,6 +477,13 @@ function useMemoStateContext(props) { props.draggableHandleClassName, outerDnDEnable, dragOverPlugin, + + props.onDragStart, + props.onDrag, + props.onDragStop, + props.onResizeStart, + props.onResize, + props.onResizeStop, ], ); diff --git a/src/typings/config.ts b/src/typings/config.ts index 3f37547..52b74d1 100644 --- a/src/typings/config.ts +++ b/src/typings/config.ts @@ -1,3 +1,5 @@ +import type {Layout, Layouts} from 'react-grid-layout'; + import type {Config, ConfigItem, ConfigLayout} from '../shared'; export interface AddConfigItem extends Omit { @@ -67,3 +69,12 @@ export interface DashKitGroup { ) => React.ReactNode; gridProperties?: (props: ReactGridLayoutProps) => ReactGridLayoutProps; } + +export type ItemManipulationCallback = (eventData: { + layout: Layouts; + oldItem: Layout; + newItem: Layout; + placeholder: Layout; + e: MouseEvent; + element: HTMLElement; +}) => void; diff --git a/src/utils/group-helpers.ts b/src/utils/group-helpers.ts new file mode 100644 index 0000000..6ce42e8 --- /dev/null +++ b/src/utils/group-helpers.ts @@ -0,0 +1,14 @@ +import {DEFAULT_GROUP} from '../constants'; +import type {ConfigLayout} from '../shared/types'; + +export const resolveLayoutGroup = (item: ConfigLayout) => { + if (!item.parent) { + return DEFAULT_GROUP; + } + + return item.parent; +}; + +export const isDefaultLayoutGroup = (item: ConfigLayout) => { + return item.parent === DEFAULT_GROUP || item.parent === undefined; +}; diff --git a/src/utils/index.ts b/src/utils/index.ts index e628e97..89f8492 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,2 +1,3 @@ export * from './register-manager'; export * from './update-manager'; +export * from './group-helpers'; diff --git a/src/utils/update-manager.ts b/src/utils/update-manager.ts index 1b8679f..7d9c2f7 100644 --- a/src/utils/update-manager.ts +++ b/src/utils/update-manager.ts @@ -41,6 +41,7 @@ import type { import {getNewId} from './get-new-id'; import {bottom, compact} from './grid-layout'; +import {resolveLayoutGroup} from './group-helpers'; import {RegisterManagerPluginLayout} from './register-manager'; extend('$auto', (value, object) => (object ? update(object, value) : update({}, value))); @@ -409,7 +410,7 @@ export function reflowLayout({ isNewItem?: boolean, ) => { memo[item.i] = i; - const parent = item.parent || DEFAULT_GROUP; + const parent = resolveLayoutGroup(item); if (byGroup[parent]) { if (isNewItem) {