Skip to content

Commit

Permalink
feat: dnd between different groups (#166)
Browse files Browse the repository at this point in the history
* feat: dnd between different groups

* chore: add comments

* feat: simplified drag states, removed additional onDropShared callback

* fix: calculate current drugging element only for shared dnd

* fix: drag exit handler

* fix: cleanup saved layout when needed

* fix: drag between shared groups

* fix: drop to empty space after differet group visit

* fix: undefined parent while working with groups

* fix: inline group styles
  • Loading branch information
flops authored Jul 30, 2024
1 parent 1b69f84 commit c0e21e4
Show file tree
Hide file tree
Showing 6 changed files with 378 additions and 91 deletions.
29 changes: 7 additions & 22 deletions src/components/DashKit/__stories__/DashKitGroupsShowcase.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react';

import {cn} from '@bem-react/classname';
import {ChartColumn, Copy, Heading, Sliders, TextAlignLeft} from '@gravity-ui/icons';
import {Button, Icon} from '@gravity-ui/uikit';

Expand All @@ -21,6 +22,8 @@ import {DeleteIcon} from '../../../icons/DeleteIcon';
import {Demo, DemoRow} from './Demo';
import {fixedGroup, getConfig} from './utils';

const b = cn('dashkit-demo');

export const DashKitGroupsShowcase: React.FC = () => {
const [editMode, setEditMode] = React.useState(true);

Expand Down Expand Up @@ -158,29 +161,11 @@ export const DashKitGroupsShowcase: React.FC = () => {
{
id: fixedGroup,
render: (id: string, children: React.ReactNode, props: DashkitGroupRenderProps) => {
const defaultStyles: React.CSSProperties = {
backgroundColor: '#ccc',
display: 'flex',
flexDirection: 'column',
};

const style: React.CSSProperties = props.editMode
? {
position: 'static',
overflow: 'visible',
minHeight: 48,
}
: {
position: 'sticky',
overflow: 'auto',
top: 0,
zIndex: 3,
maxHeight: 300,
minHeight: 'unset',
};

return (
<div key={id} style={{...defaultStyles, ...style}}>
<div
key={id}
className={b('inline-group', {['edit-mode']: props.editMode})}
>
{children}
</div>
);
Expand Down
21 changes: 21 additions & 0 deletions src/components/DashKit/__stories__/Demo.scss
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,25 @@
color: var(--g-color-text-complementary);
}
}

&-inline-group {
background-color: var(--g-color-base-generic-ultralight);
position: sticky;
overflow: auto;
top: 0;
z-index: 3;
min-height: 44px;
display: flex;
flex-direction: column;
margin-bottom: 8px;

&_edit-mode {
position: static;
overflow: visible;
}

> .react-grid-layout {
flex: 1;
}
}
}
2 changes: 2 additions & 0 deletions src/components/GridItem/GridItem.scss
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@
transition: none;
z-index: 3;
will-change: transform;
// needs for drag n drop between multiple groups
pointer-events: none;
}

.react-grid-item.dashkit-grid-item_is-focused {
Expand Down
166 changes: 141 additions & 25 deletions src/components/GridLayout/GridLayout.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export default class GridLayout extends React.PureComponent {
this.state = {
isDragging: false,
isPageHidden: false,
currentDraggingElement: null,
draggedOverGroup: null,
};
}

Expand Down Expand Up @@ -114,18 +116,19 @@ export default class GridLayout extends React.PureComponent {

getMemoGroupCallbacks = (group) => {
if (!this._memoCallbacksForGroups[group]) {
const onStart = this._onStart;
const onDragStart = this._onDragStart.bind(this, group);
const onStop = this._onStop.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: onStart,
onResizeStart: onStart,
onDragStart,
onDragStop: onStop,
onResizeStop: onStop,
onDrop: onDrop,
onDrop,
onDropDragOver,
onDragTargetRestore,
};
}

Expand Down Expand Up @@ -171,13 +174,11 @@ export default class GridLayout extends React.PureComponent {

const newItemsLayoutById = newLayout.reduce((memo, item) => {
const parent = itemsByGroup[item.i].parent;

memo[item.i] = {...item};

if (parent) {
memo[item.i].parent = parent;
}

return memo;
}, {});

Expand Down Expand Up @@ -216,16 +217,65 @@ export default class GridLayout extends React.PureComponent {
}
}

_onStart = () => {
_onDragStart = (group, _newLayout, layoutItem) => {
if (this.temporaryLayout) return;

this.setState({isDragging: true});
if (this.context.dragOverPlugin) {
this.setState({isDragging: true});
} else {
let currentDraggingElement = this.state.currentDraggingElement;
if (!currentDraggingElement) {
const _id = layoutItem.i;
const item = this.context.config.items.find(({id}) => id === _id);
currentDraggingElement = [group, layoutItem, item];
}

this.setState({
isDragging: true,
currentDraggingElement,
draggedOverGroup: group,
});
}
};

_onResizeStart = () => {
this.setState({
isDragging: true,
});
};

_onTargetRestore = () => {
const {currentDraggingElement} = this.state;

if (currentDraggingElement) {
this.setState({
draggedOverGroup: currentDraggingElement[0],
});
}
};

_onStop = (group, newLayout) => {
const {layoutChange, onDrop, temporaryLayout} = this.context;
const {draggedOverGroup, currentDraggingElement} = this.state;

if (
currentDraggingElement &&
draggedOverGroup !== null &&
draggedOverGroup !== currentDraggingElement[0]
) {
// Skipping layout update when change event called for source grid
// and waiting _onDrop
return;
}

const groupedLayout = this.mergeGroupsLayout(group, newLayout);

this.setState({
isDragging: false,
currentDraggingElement: null,
draggedOverGroup: null,
});

if (temporaryLayout) {
onDrop?.(
groupedLayout,
Expand All @@ -234,38 +284,91 @@ export default class GridLayout extends React.PureComponent {
} else {
layoutChange(groupedLayout);
}
this.setState({isDragging: false});
};

_onDropDragOver = (group, e) => {
const {editMode, dragOverPlugin} = this.context;
_onSharedDrop = (targetGroup, newLayout, tempItem) => {
const {currentDraggingElement} = this.state;
const {layoutChange} = this.context;

if (!editMode || !dragOverPlugin) {
return false;
if (!currentDraggingElement) {
return;
}

const {properties, layout} = this.getLayoutAndPropsByGroup(group);
const [, sourceItem] = currentDraggingElement;

const groupedLayout = this.mergeGroupsLayout(targetGroup, newLayout, tempItem).map(
(item) => {
if (item.i === sourceItem.i) {
const copy = {...tempItem};

return this.context.onDropDragOver(e, properties, layout);
delete copy.parent;
if (targetGroup !== DEFAULT_GROUP) {
copy.parent = targetGroup;
}
copy.i = sourceItem.i;

return copy;
}

return item;
},
);

this.setState({
isDragging: false,
currentDraggingElement: null,
draggedOverGroup: null,
});

layoutChange(groupedLayout);
};

_onExternalDrop = (group, newLayout, item, e) => {
const {onDrop} = this.context;

if (group !== DEFAULT_GROUP) {
item.parent = group;
}

const groupedLayout = this.mergeGroupsLayout(group, newLayout, item);
this.setState({isDragging: false});

onDrop?.(groupedLayout, item, e);
};

_onDrop = (group, newLayout, item, e) => {
if (!item) {
if (!item || !this.context.editMode) {
return false;
}

const {editMode, onDrop} = this.context;
if (!editMode) {
const {draggedOverGroup, currentDraggingElement} = this.state;
if (currentDraggingElement && draggedOverGroup === group) {
this._onSharedDrop(group, newLayout, item);
} else {
this._onExternalDrop(group, newLayout, item, e);
}
};

_onDropDragOver = (group, e) => {
const {editMode, dragOverPlugin, onDropDragOver} = this.context;
const {currentDraggingElement} = this.state;

if (!editMode || (!dragOverPlugin && !currentDraggingElement)) {
return false;
}

if (group !== DEFAULT_GROUP) {
item.parent = group;
const {properties, layout} = this.getLayoutAndPropsByGroup(group);

if (currentDraggingElement) {
const [, {h, w, i}, {type}] = currentDraggingElement;
return onDropDragOver(e, properties, layout, {h, w, i, type});
}

const groupedLayout = this.mergeGroupsLayout(group, newLayout, item);
if (dragOverPlugin) {
return onDropDragOver(e, properties, layout);
}

onDrop?.(groupedLayout, item, e);
return false;
};

renderTemporaryPlaceholder() {
Expand Down Expand Up @@ -303,6 +406,8 @@ export default class GridLayout extends React.PureComponent {
outerDnDEnable,
} = this.context;

const {currentDraggingElement, draggedOverGroup} = this.state;

const properties = groupGridProperties
? groupGridProperties({
...registerManager.gridLayout,
Expand All @@ -315,6 +420,14 @@ export default class GridLayout extends React.PureComponent {
}

const {callbacks, layout} = this.getMemoGroupProps(group, renderLayout, properties);
const hasSharedDragItem = Boolean(
currentDraggingElement && currentDraggingElement[0] !== group,
);
const isDragCaptured =
currentDraggingElement &&
group === currentDraggingElement[0] &&
draggedOverGroup !== null &&
draggedOverGroup !== group;

return (
<Layout
Expand All @@ -324,18 +437,21 @@ export default class GridLayout extends React.PureComponent {
key={`group_${group}`}
isDraggable={editMode}
isResizable={editMode}
onResizeStart={this._onResizeStart}
onDragStart={callbacks.onDragStart}
onDragStop={callbacks.onDragStop}
onResizeStart={callbacks.onResizeStart}
onResizeStop={callbacks.onResizeStop}
onDragTargetRestore={callbacks.onDragTargetRestore}
onDropDragOver={callbacks.onDropDragOver}
onDrop={callbacks.onDrop}
hasSharedDragItem={hasSharedDragItem}
isDragCaptured={isDragCaptured}
{...(draggableHandleClassName
? {draggableHandle: `.${draggableHandleClassName}`}
: null)}
{...(outerDnDEnable
? {
isDroppable: true,
onDropDragOver: callbacks.onDropDragOver,
onDrop: callbacks.onDrop,
}
: null)}
draggableCancel={`.${OVERLAY_CONTROLS_CLASS_NAME}`}
Expand Down
Loading

0 comments on commit c0e21e4

Please sign in to comment.