Skip to content

Commit 8cefb78

Browse files
authored
Merge pull request #74 from journeyapps-labs/polish_drag_and_drop
Polish drag and drop and add demo
2 parents 59327bd + aab22fb commit 8cefb78

File tree

7 files changed

+412
-26
lines changed

7 files changed

+412
-26
lines changed

.changeset/polish-dnd-demo.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@journeyapps-labs/reactor-mod': minor
3+
---
4+
5+
Improve between-zone drag and drop UX and add a dedicated playground demo for it.
6+
7+
- Added drag target highlighting for `useDroppableBetweenZone` using DnD theme hover colors.
8+
- Added shared gap configuration fields for between-zone hooks/widgets:
9+
- `gap_standard`
10+
- `gap_hint`
11+
- `gap_expand`
12+
- Added automatic hiding of the two drop zones around the currently dragged child.
13+
- Added support for rendering an end-of-list between-zone drop target.
14+
- Added `useDraggingElement` to centralize drag source element tracking.
15+
- Added a new `Drag + Drop` playground panel with vertical and horizontal demos and reorder reset.

demo/module-playground/src/ReactorPlaygroundModule.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { PlaygroundCardsPanelWidget } from './panels/PlaygroundCardsPanelWidget'
99
import { PlaygroundButtonsPanelWidget } from './panels/PlaygroundButtonsPanelWidget';
1010
import { PlaygroundEditorsPanelWidget } from './panels/PlaygroundEditorsPanelWidget';
1111
import { PlaygroundTreeSearchPanelWidget } from './panels/tree/PlaygroundTreeSearchPanelWidget';
12+
import { PlaygroundDragDropPanelWidget } from './panels/PlaygroundDragDropPanelWidget';
1213

1314
export class ReactorPlaygroundModule extends AbstractReactorModule {
1415
constructor() {
@@ -68,6 +69,14 @@ export class ReactorPlaygroundModule extends AbstractReactorModule {
6869
widget: PlaygroundEditorsPanelWidget
6970
})
7071
);
72+
workspaceStore.registerFactory(
73+
new PlaygroundPanelFactory({
74+
type: 'playground.drag-drop',
75+
name: 'Drag + Drop',
76+
icon: 'arrows-alt',
77+
widget: PlaygroundDragDropPanelWidget
78+
})
79+
);
7180

7281
setupWorkspaces();
7382
}
Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
import * as React from 'react';
2+
import { observer } from 'mobx-react';
3+
import {
4+
CardWidget,
5+
PanelButtonMode,
6+
PanelButtonWidget,
7+
ReactorPanelModel,
8+
StatusCardState,
9+
StatusCardWidget,
10+
styled,
11+
useDraggableRaw,
12+
useDroppableBetweenZone
13+
} from '@journeyapps-labs/reactor-mod';
14+
15+
const VERTICAL_DEMO_MIME = 'playground/demo-item-vertical';
16+
const HORIZONTAL_DEMO_MIME = 'playground/demo-item-horizontal';
17+
18+
interface DemoItem {
19+
id: string;
20+
label: string;
21+
status: StatusCardState;
22+
}
23+
24+
interface DropZoneListener {
25+
highlight?: (highlight: boolean) => any;
26+
}
27+
28+
class DemoBetweenDropZone {
29+
private listeners = new Set<DropZoneListener>();
30+
index = 0;
31+
32+
constructor(
33+
private mime: string,
34+
private dropped: (itemId: string, index: number) => any
35+
) {}
36+
37+
registerListener(listener: DropZoneListener) {
38+
this.listeners.add(listener);
39+
return () => {
40+
this.listeners.delete(listener);
41+
};
42+
}
43+
44+
highlight(highlight: boolean) {
45+
this.listeners.forEach((listener) => listener.highlight?.(highlight));
46+
}
47+
48+
acceptsMimeType(type: string) {
49+
return type === this.mime;
50+
}
51+
52+
handleDrop(event: { data: { [key: string]: string } }) {
53+
const itemId = event.data[this.mime];
54+
if (!itemId) {
55+
return;
56+
}
57+
this.dropped(itemId, this.index);
58+
}
59+
}
60+
61+
interface DemoDraggableItemProps {
62+
item: DemoItem;
63+
mime: string;
64+
}
65+
66+
const DemoDraggableItem: React.FC<DemoDraggableItemProps> = (props) => {
67+
const ref = React.useRef<HTMLDivElement>(null);
68+
useDraggableRaw({
69+
forwardRef: ref,
70+
encode: () => {
71+
return {
72+
data: {
73+
[props.mime]: props.item.id
74+
},
75+
icon: ref.current || document.body
76+
};
77+
}
78+
});
79+
80+
return (
81+
<S.Item ref={ref}>
82+
<StatusCardWidget
83+
label={{
84+
label: props.item.label,
85+
icon: 'arrows-alt'
86+
}}
87+
status={props.item.status}
88+
/>
89+
</S.Item>
90+
);
91+
};
92+
93+
export interface PlaygroundDragDropPanelWidgetProps {
94+
model: ReactorPanelModel;
95+
}
96+
97+
const initialItems: DemoItem[] = [
98+
{ id: 'a', label: 'Alpha', status: StatusCardState.COMPLETE },
99+
{ id: 'b', label: 'Beta', status: StatusCardState.LOADING },
100+
{ id: 'c', label: 'Gamma', status: StatusCardState.WAITING },
101+
{ id: 'd', label: 'Delta', status: StatusCardState.FAILED }
102+
];
103+
104+
const reorderItems = (prev: DemoItem[], itemId: string, index: number) => {
105+
const currentIndex = prev.findIndex((item) => item.id === itemId);
106+
if (currentIndex === -1) {
107+
return prev;
108+
}
109+
const next = prev.slice();
110+
const [moved] = next.splice(currentIndex, 1);
111+
const adjustedIndex = currentIndex < index ? index - 1 : index;
112+
const targetIndex = Math.max(0, Math.min(adjustedIndex, next.length));
113+
next.splice(targetIndex, 0, moved);
114+
return next;
115+
};
116+
117+
interface DemoLaneProps {
118+
vertical: boolean;
119+
mime: string;
120+
items: DemoItem[];
121+
setItems: React.Dispatch<React.SetStateAction<DemoItem[]>>;
122+
}
123+
124+
const DemoLane: React.FC<DemoLaneProps> = (props) => {
125+
const dropZone = React.useMemo(() => {
126+
return new DemoBetweenDropZone(props.mime, (itemId, index) => {
127+
props.setItems((prev) => {
128+
return reorderItems(prev, itemId, index);
129+
});
130+
});
131+
}, [props.mime, props.setItems]);
132+
133+
const laneRef = React.useRef<HTMLDivElement>(null);
134+
const { children } = useDroppableBetweenZone({
135+
forwardRef: laneRef,
136+
vertical: props.vertical,
137+
dropzone: dropZone as any,
138+
gap_standard: 4,
139+
gap_hint: 8,
140+
setIndex: (zone: any, index) => {
141+
zone.index = index;
142+
},
143+
children: props.items.map((item) => {
144+
return <DemoDraggableItem item={item} mime={props.mime} key={item.id} />;
145+
})
146+
});
147+
148+
return (
149+
<S.DemoArea ref={laneRef} vertical={props.vertical}>
150+
{children}
151+
</S.DemoArea>
152+
);
153+
};
154+
155+
export const PlaygroundDragDropPanelWidget: React.FC<PlaygroundDragDropPanelWidgetProps> = observer(() => {
156+
const [verticalItems, setVerticalItems] = React.useState<DemoItem[]>(initialItems);
157+
const [horizontalItems, setHorizontalItems] = React.useState<DemoItem[]>(initialItems);
158+
159+
return (
160+
<S.Container>
161+
<CardWidget
162+
title="Drag + Drop"
163+
subHeading="Between-zone drop indicators now use Reactor DnD hint and hover colors"
164+
sections={[
165+
{
166+
key: 'dnd-zone-vertical',
167+
content: () => {
168+
return (
169+
<DemoLane vertical={true} mime={VERTICAL_DEMO_MIME} items={verticalItems} setItems={setVerticalItems} />
170+
);
171+
}
172+
},
173+
{
174+
key: 'dnd-zone-horizontal',
175+
content: () => {
176+
return (
177+
<DemoLane
178+
vertical={false}
179+
mime={HORIZONTAL_DEMO_MIME}
180+
items={horizontalItems}
181+
setItems={setHorizontalItems}
182+
/>
183+
);
184+
}
185+
},
186+
{
187+
key: 'dnd-actions',
188+
content: () => {
189+
return (
190+
<S.Actions>
191+
<PanelButtonWidget
192+
label="Reset order"
193+
icon="redo"
194+
mode={PanelButtonMode.PRIMARY}
195+
action={() => {
196+
setVerticalItems(initialItems);
197+
setHorizontalItems(initialItems);
198+
}}
199+
/>
200+
</S.Actions>
201+
);
202+
}
203+
}
204+
]}
205+
/>
206+
</S.Container>
207+
);
208+
});
209+
210+
namespace S {
211+
export const Container = styled.div`
212+
padding: 12px;
213+
display: flex;
214+
flex-direction: column;
215+
row-gap: 12px;
216+
min-height: 100%;
217+
box-sizing: border-box;
218+
`;
219+
220+
export const DemoArea = styled.div<{ vertical: boolean }>`
221+
border-radius: 8px;
222+
padding: 8px;
223+
display: flex;
224+
flex-direction: ${(p) => (p.vertical ? 'column' : 'row')};
225+
align-items: ${(p) => (p.vertical ? 'flex-start' : 'stretch')};
226+
width: ${(p) => (p.vertical ? '280px' : '100%')};
227+
max-width: 100%;
228+
overflow: visible;
229+
`;
230+
231+
export const Item = styled.div`
232+
cursor: grab;
233+
user-select: none;
234+
width: 260px;
235+
display: flex;
236+
`;
237+
238+
export const Actions = styled.div`
239+
display: flex;
240+
gap: 8px;
241+
`;
242+
}

demo/module-playground/src/setupWorkspaces.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export const setupWorkspaces = () => {
1717
.addModel(new PlaygroundPanelModel('playground.cards'))
1818
.addModel(new PlaygroundPanelModel('playground.buttons'))
1919
.addModel(new PlaygroundPanelModel('playground.editors'))
20+
.addModel(new PlaygroundPanelModel('playground.drag-drop'))
2021
);
2122

2223
return model;

modules/module-reactor/src/stores/themes/ThemeFragment.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { CreateStyled } from './emotion';
22
import s from '@emotion/styled';
33
import * as _ from 'lodash';
4-
import { EntityDefinition } from '../../entities/EntityDefinition';
54

65
const s2 = (comp, options) => {
76
return s(comp, {
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { useEffect, useState } from 'react';
2+
3+
export const useDraggingElement = () => {
4+
const [draggingElement, setDraggingElement] = useState<HTMLElement | null>(null);
5+
6+
useEffect(() => {
7+
const dragStart = (event: DragEvent) => {
8+
if (event.target instanceof Element) {
9+
setDraggingElement(event.target as HTMLElement);
10+
} else {
11+
setDraggingElement(null);
12+
}
13+
};
14+
const dragEnd = () => {
15+
setDraggingElement(null);
16+
};
17+
document.addEventListener('dragstart', dragStart, true);
18+
document.addEventListener('dragend', dragEnd, true);
19+
return () => {
20+
document.removeEventListener('dragstart', dragStart, true);
21+
document.removeEventListener('dragend', dragEnd, true);
22+
};
23+
}, []);
24+
25+
return draggingElement;
26+
};

0 commit comments

Comments
 (0)