Skip to content

Commit 5b22965

Browse files
committed
feat: multiple selection drag
1 parent fc636cb commit 5b22965

File tree

4 files changed

+98
-60
lines changed

4 files changed

+98
-60
lines changed

README.md

+5-4
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,12 @@ type Options = {
3333
/**
3434
* Events
3535
*/
36-
onStart?: EventHandler<void>;
37-
onLeave?: EventHandler<void>;
38-
onOver?: EventHandler<void>;
36+
onStart?: EventHandler;
37+
onLeave?: EventHandler;
38+
onOver?: EventHandler;
3939
onBeforeDrop?: EventHandler<boolean>;
40-
onDrop?: EventHandler<void>;
40+
onDrop?: EventHandler;
41+
onShadow?: EventHandler;
4142

4243
/**
4344
* Specifies where a draggable can be dropped.

lib/constants.ts

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ const CLASSNAMES = {
33
dropzone: "draggy-dropzone",
44
dragging: "draggy-dragging",
55
origin: "draggy-origin",
6+
selection: "draggy-selection",
67
};
78

89
export { CLASSNAMES };

lib/main.ts

+76-45
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { CLASSNAMES } from "./constants";
12
import { Options, Context } from "./types";
23
import { isElement } from "./utils";
34

@@ -14,6 +15,7 @@ function draggy({ target, ...options }: Options) {
1415
removeMouseMove: null,
1516
delay: 100,
1617
lastMove: -1,
18+
multiple: [],
1719
options: {
1820
optimistic: true,
1921
direction: "vertical",
@@ -46,12 +48,10 @@ function draggy({ target, ...options }: Options) {
4648
const onMouseDown = (ev: MouseEvent) => {
4749
ev.preventDefault();
4850
handleMouseDown(context, ev, c);
49-
if (context.options.onStart && context.origin) {
50-
context.options.onStart(ev, {
51-
dragged: context.origin,
52-
dropzone: context.zone,
53-
});
54-
}
51+
context.options.onStart?.(ev, {
52+
origin: context.origin,
53+
zone: context.zone,
54+
});
5555
};
5656
c.addEventListener("mousedown", onMouseDown);
5757
context.events.set(c, onMouseDown);
@@ -63,26 +63,36 @@ function draggy({ target, ...options }: Options) {
6363

6464
if (context.options.onBeforeDrop && context.originZone && context.origin) {
6565
const bool = context.options.onBeforeDrop(ev, {
66-
dragged: context.origin,
67-
dropzone: context.zone,
66+
origin: context.origin,
67+
zone: context.zone,
6868
});
6969

7070
if (!bool) {
7171
context.originZone.insertBefore(context.origin, context.nextSibling);
7272
}
7373
}
7474

75-
if (context.options.onDrop && context.origin) {
76-
context.options.onDrop(ev, {
77-
dragged: context.origin,
78-
dropzone: context.zone,
79-
});
80-
}
75+
context.options.onDrop?.(ev, {
76+
origin: context.origin,
77+
zone: context.zone,
78+
});
8179

8280
context.shadow?.remove();
8381
context.shadow = null;
8482

85-
context.origin?.classList.remove("placeholder");
83+
if (context.multiple && context.zone && context.origin) {
84+
for (let i = 0; i < context.multiple.length; i++) {
85+
const m = context.multiple[i];
86+
if (!m || !m.origin) return;
87+
context.zone.insertBefore(m.origin, context.origin.nextElementSibling);
88+
m.origin.style.display = m.style.display;
89+
m.origin.classList.remove(CLASSNAMES.selection);
90+
}
91+
92+
context.multiple = [];
93+
}
94+
95+
context.origin?.classList.remove(CLASSNAMES.origin);
8696
context.origin = null;
8797

8898
context.removeMouseMove?.();
@@ -106,37 +116,64 @@ function draggy({ target, ...options }: Options) {
106116
}
107117

108118
const handleMouseDown = (context: Context, ev: MouseEvent, el: HTMLElement) => {
109-
context.shadow = createShadow(context, el, ev.clientX, ev.clientY);
119+
if (ev.shiftKey) {
120+
context.multiple.push({
121+
origin: el,
122+
style: {
123+
display: el.style.display,
124+
},
125+
originZone: el.parentElement,
126+
nextSibling: el.nextElementSibling as HTMLElement | null,
127+
});
128+
129+
el.classList.add(CLASSNAMES.selection);
130+
131+
return;
132+
}
110133

111-
el.classList.add("placeholder");
134+
context.shadow = createShadow(context, ev, el);
135+
136+
el.classList.add(CLASSNAMES.origin);
112137
context.origin = el;
113138

139+
if (context.multiple.length) {
140+
for (let i = 0; i < context.multiple.length; i++) {
141+
const m = context.multiple[i];
142+
if (!m?.origin) return;
143+
if (m.origin !== context.origin) {
144+
m.origin.style.display = "none";
145+
}
146+
}
147+
}
148+
114149
context.originZone = el.parentElement;
115150
context.nextSibling = el.nextElementSibling as HTMLElement | null;
116151

117152
handleChildren(context);
118153
};
119154

120-
const createShadow = (
121-
context: Context,
122-
el: HTMLElement,
123-
clientX: number,
124-
clientY: number,
125-
) => {
155+
const createShadow = (context: Context, ev: MouseEvent, el: HTMLElement) => {
126156
const shadow = el.cloneNode(true) as HTMLElement;
127157

128-
shadow.classList.add("dragging");
158+
shadow.classList.add(CLASSNAMES.dragging);
129159
shadow.style.position = "absolute";
130160
shadow.style.pointerEvents = "none";
131161
shadow.style.width = `${el.offsetWidth}px`;
132162
shadow.style.height = `${el.offsetHeight}px`;
133163
shadow.style.zIndex = "9999";
134164

135165
const rect = el.getBoundingClientRect();
136-
const offsets = { x: clientX - rect.left, y: clientY - rect.top };
166+
const offsets = { x: ev.clientX - rect.left, y: ev.clientY - rect.top };
167+
168+
shadow.style.left = `${ev.clientX - offsets.x + scrollX}px`;
169+
shadow.style.top = `${ev.clientY - offsets.y + scrollY}px`;
137170

138-
shadow.style.left = `${clientX - offsets.x + scrollX}px`;
139-
shadow.style.top = `${clientY - offsets.y + scrollY}px`;
171+
context.options.onShadow?.(ev, {
172+
shadow,
173+
origin: el,
174+
zone: context.zone,
175+
multiple: context.multiple,
176+
});
140177

141178
const onMouseMove = (ev: Event) => handleMouseMove(ev, offsets);
142179
document.addEventListener("mousemove", onMouseMove);
@@ -158,12 +195,10 @@ const createShadow = (
158195
}
159196

160197
if (context.zone && !context.zone.contains(point)) {
161-
if (context.options.onLeave && context.origin) {
162-
context.options.onLeave(ev, {
163-
dragged: context.origin,
164-
dropzone: context.zone,
165-
});
166-
}
198+
context.options.onLeave?.(ev, {
199+
origin: context.origin,
200+
zone: context.zone,
201+
});
167202
context.zone = null;
168203
}
169204

@@ -179,19 +214,15 @@ const createShadow = (
179214
if (z.contains(point)) {
180215
if (context.zone !== z) {
181216
context.zone = z;
182-
if (context.options.onEnter && context.origin) {
183-
context.options.onEnter(ev, {
184-
dragged: context.origin,
185-
dropzone: context.zone,
186-
});
187-
}
188-
}
189-
if (context.options.onOver && context.origin) {
190-
context.options.onOver(ev, {
191-
dragged: context.origin,
192-
dropzone: context.zone,
217+
context.options.onEnter?.(ev, {
218+
origin: context.origin,
219+
zone: context.zone,
193220
});
194221
}
222+
context.options.onOver?.(ev, {
223+
origin: context.origin,
224+
zone: context.zone,
225+
});
195226
handlePushing(context, x, y);
196227
break;
197228
}
@@ -254,7 +285,7 @@ const handlePushing = (context: Context, x: number, y: number) => {
254285
}
255286

256287
const children = Array.from(z.children).filter(
257-
(c) => c !== placeholder && !c.classList.contains("placeholder"),
288+
(c) => c !== placeholder && !c.classList.contains(CLASSNAMES.origin),
258289
);
259290
const zones = children.map((c) => c.getBoundingClientRect());
260291

lib/types.ts

+16-11
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,25 @@ type Context = {
1010
removeMouseMove: (() => void) | null;
1111
delay: number;
1212
lastMove: number;
13+
multiple: Draggable[];
1314
options: Omit<Options, "target">;
1415
};
1516

17+
type Draggable = Pick<Context, "origin" | "originZone" | "nextSibling"> & {
18+
style: {
19+
display: string;
20+
};
21+
};
22+
1623
type Options = {
1724
target: string | Element | Element[] | NodeListOf<Element> | null;
18-
onStart?: EventHandler<void>;
19-
onLeave?: EventHandler<void>;
20-
onEnter?: EventHandler<void>;
21-
onOver?: EventHandler<void>;
25+
onStart?: EventHandler;
26+
onLeave?: EventHandler;
27+
onEnter?: EventHandler;
28+
onOver?: EventHandler;
2229
onBeforeDrop?: EventHandler<boolean>;
23-
onDrop?: EventHandler<void>;
30+
onDrop?: EventHandler;
31+
onShadow?: EventHandler;
2432
/**
2533
* Specifies where a draggable can be dropped.
2634
* - "start": Only allow dropping at the start. With direction=vertical this is the top, and direction=horizontal is to the right.
@@ -49,13 +57,10 @@ type Options = {
4957
optimistic?: boolean;
5058
};
5159

52-
type EventHandler<T> = (
60+
type EventHandler<T = void> = (
5361
event: Event,
54-
context: {
55-
// @TODO: Rename this to "origin" or something else.
56-
dragged: HTMLElement;
57-
dropzone: HTMLElement | null;
58-
},
62+
context: Pick<Context, "origin" | "zone"> &
63+
Partial<Pick<Context, "shadow" | "multiple">>,
5964
) => T;
6065

6166
export { Context, Options };

0 commit comments

Comments
 (0)