Skip to content

Commit e25b86c

Browse files
committed
[UPD] Refactoring drag and time axis
1 parent ce969d3 commit e25b86c

File tree

4 files changed

+323
-240
lines changed

4 files changed

+323
-240
lines changed

src/components/GGanttTimeaxis.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
/>
4141
</div>
4242
</div>
43-
<div v-if="enableMinutes" class="g-timeunits-container">
43+
<div v-if="precision === 'hour' && enableMinutes" class="g-timeunits-container">
4444
<div
4545
v-for="({ label, width, date }, index) in timeaxisUnits.minutesUnits"
4646
:key="`${label}-${index}`"
Lines changed: 181 additions & 146 deletions
Original file line numberDiff line numberDiff line change
@@ -1,198 +1,231 @@
1-
import type { GanttBarObject } from "../types"
2-
3-
import createBarDrag from "./createBarDrag"
4-
import useDayjsHelper from "./useDayjsHelper"
51
import provideConfig from "../provider/provideConfig"
2+
import type { GanttBarObject } from "../types/bar"
63
import provideGetChartRows from "../provider/provideGetChartRows"
74
import provideEmitBarEvent from "../provider/provideEmitBarEvent"
5+
import useDayjsHelper from "./useDayjsHelper"
6+
import createBarDrag from "./createBarDrag"
7+
// Types for better typing and code organization
8+
type DragState = {
9+
movedBars: Map<GanttBarObject, { oldStart: string; oldEnd: string }>
10+
isDragging: boolean
11+
}
12+
13+
type BarMovement = {
14+
bar: GanttBarObject
15+
minutes: number
16+
direction: "left" | "right"
17+
}
818

9-
export default function useBarDragManagement() {
19+
type OverlapResult = {
20+
overlapBar?: GanttBarObject
21+
overlapType: "left" | "right" | "between" | null
22+
}
23+
24+
// Main hook for drag and drop management
25+
const useBarDragManagement = () => {
1026
const config = provideConfig()
1127
const getChartRows = provideGetChartRows()
1228
const emitBarEvent = provideEmitBarEvent()
1329
const { pushOnOverlap, barStart, barEnd, noOverlap, dateFormat } = config
1430

15-
const movedBarsInDrag = new Map<GanttBarObject, { oldStart: string; oldEnd: string }>()
31+
// Central state management for drag operations
32+
const dragState: DragState = {
33+
movedBars: new Map(),
34+
isDragging: false
35+
}
1636

1737
const { toDayjs, format } = useDayjsHelper()
1838

39+
// Initialize drag for a single bar
1940
const initDragOfBar = (bar: GanttBarObject, e: MouseEvent) => {
20-
const { initDrag } = createBarDrag(bar, onDrag, onEndDrag, config)
21-
emitBarEvent({ ...e, type: "dragstart" }, bar)
22-
initDrag(e)
41+
const dragHandler = createDragHandler(bar)
42+
dragHandler.initiateDrag(e)
2343
addBarToMovedBars(bar)
44+
emitBarEvent({ ...e, type: "dragstart" }, bar)
2445
}
2546

47+
// Initialize drag for a bundle of bars
2648
const initDragOfBundle = (mainBar: GanttBarObject, e: MouseEvent) => {
2749
const bundle = mainBar.ganttBarConfig.bundle
28-
if (bundle == null) {
29-
return
30-
}
31-
getChartRows().forEach((row) => {
32-
row.bars.forEach((bar) => {
33-
if (bar.ganttBarConfig.bundle === bundle) {
34-
const dragEndHandler = bar === mainBar ? onEndDrag : () => null
35-
const { initDrag } = createBarDrag(bar, onDrag, dragEndHandler, config)
36-
initDrag(e)
37-
addBarToMovedBars(bar)
38-
}
39-
})
50+
if (!bundle) return
51+
52+
const getBundleBars = () =>
53+
getChartRows()
54+
.flatMap((row) => row.bars)
55+
.filter((bar) => bar.ganttBarConfig.bundle === bundle)
56+
57+
getBundleBars().forEach((bar) => {
58+
const isMainBar = bar === mainBar
59+
const dragHandler = createDragHandler(bar, isMainBar)
60+
dragHandler.initiateDrag(e)
61+
addBarToMovedBars(bar)
4062
})
63+
4164
emitBarEvent({ ...e, type: "dragstart" }, mainBar)
4265
}
4366

44-
const onDrag = (e: MouseEvent, bar: GanttBarObject) => {
67+
// Create handler for drag events
68+
const createDragHandler = (bar: GanttBarObject, isMainBar = true) => ({
69+
initiateDrag: (e: MouseEvent) => {
70+
const { initDrag } = createBarDrag(
71+
bar,
72+
(e) => handleDrag(e, bar),
73+
isMainBar ? handleDragEnd : () => null,
74+
config
75+
)
76+
initDrag(e)
77+
}
78+
})
79+
80+
// Handle ongoing drag operations
81+
const handleDrag = (e: MouseEvent, bar: GanttBarObject) => {
4582
emitBarEvent({ ...e, type: "drag" }, bar)
46-
fixOverlaps(bar)
83+
if (pushOnOverlap?.value) {
84+
handleOverlaps(bar)
85+
}
4786
}
4887

49-
const fixOverlaps = (ganttBar: GanttBarObject) => {
50-
if (!pushOnOverlap?.value) {
51-
return
88+
// Manage overlapping bars during drag
89+
const handleOverlaps = (bar: GanttBarObject) => {
90+
let currentBar = bar
91+
let overlap = detectOverlap(currentBar)
92+
93+
while (overlap.overlapBar) {
94+
const adjustedBar = adjustOverlappingBar(currentBar, overlap)
95+
if (!adjustedBar) break
96+
97+
currentBar = adjustedBar
98+
overlap = detectOverlap(currentBar)
5299
}
53-
let currentBar = ganttBar
54-
let { overlapBar, overlapType } = getOverlapBarAndType(currentBar)
55-
while (overlapBar) {
56-
addBarToMovedBars(overlapBar)
57-
const currentBarStart = toDayjs(currentBar[barStart.value])
58-
const currentBarEnd = toDayjs(currentBar[barEnd.value])
59-
const overlapBarStart = toDayjs(overlapBar[barStart.value])
60-
const overlapBarEnd = toDayjs(overlapBar[barEnd.value])
61-
let minuteDiff: number
62-
switch (overlapType) {
63-
case "left":
64-
minuteDiff = overlapBarEnd.diff(currentBarStart, "minutes", true)
65-
overlapBar[barEnd.value] = format(currentBar[barStart.value], dateFormat.value)
66-
overlapBar[barStart.value] = format(
67-
overlapBarStart.subtract(minuteDiff, "minutes"),
68-
dateFormat.value
69-
)
70-
break
71-
case "right":
72-
minuteDiff = currentBarEnd.diff(overlapBarStart, "minutes", true)
73-
overlapBar[barStart.value] = format(currentBarEnd, dateFormat.value)
74-
overlapBar[barEnd.value] = format(
75-
overlapBarEnd.add(minuteDiff, "minutes"),
76-
dateFormat.value
77-
)
78-
break
79-
default:
80-
console.warn(
81-
"Vue-Ganttastic: One bar is inside of the other one! This should never occur while push-on-overlap is active!"
82-
)
83-
return
84-
}
85-
if (overlapBar && (overlapType === "left" || overlapType === "right")) {
86-
moveBundleOfPushedBarByMinutes(overlapBar, minuteDiff, overlapType)
100+
}
101+
102+
// Detect if and how bars overlap
103+
const detectOverlap = (bar: GanttBarObject): OverlapResult => {
104+
const barsInRow = getChartRows().find((row) => row.bars.includes(bar))?.bars || []
105+
106+
for (const otherBar of barsInRow) {
107+
if (otherBar === bar) continue
108+
109+
const overlapType = determineOverlapType(bar, otherBar)
110+
if (overlapType) {
111+
return { overlapBar: otherBar, overlapType }
87112
}
88-
currentBar = overlapBar
89-
;({ overlapBar, overlapType } = getOverlapBarAndType(overlapBar))
90113
}
114+
115+
return { overlapType: null }
116+
}
117+
118+
// Determine the type of overlap between two bars
119+
const determineOverlapType = (bar1: GanttBarObject, bar2: GanttBarObject) => {
120+
const start1 = toDayjs(bar1[barStart.value])
121+
const end1 = toDayjs(bar1[barEnd.value])
122+
const start2 = toDayjs(bar2[barStart.value])
123+
const end2 = toDayjs(bar2[barEnd.value])
124+
125+
if (start1.isBetween(start2, end2)) return "left"
126+
if (end1.isBetween(start2, end2)) return "right"
127+
if (start2.isBetween(start1, end1) || end2.isBetween(start1, end1)) return "between"
128+
129+
return null
130+
}
131+
132+
// Adjust bar positions when overlap is detected
133+
const adjustOverlappingBar = (currentBar: GanttBarObject, overlap: OverlapResult) => {
134+
if (!overlap.overlapBar || !overlap.overlapType) return null
135+
136+
const adjustment = calculateBarMovement(currentBar, overlap.overlapBar, overlap.overlapType)
137+
applyBarAdjustment(overlap.overlapBar, adjustment)
138+
139+
if (overlap.overlapBar.ganttBarConfig.bundle) {
140+
adjustBundleBars(overlap.overlapBar, adjustment)
141+
}
142+
143+
return overlap.overlapBar
91144
}
92145

93-
const getOverlapBarAndType = (ganttBar: GanttBarObject) => {
94-
let overlapLeft, overlapRight, overlapInBetween
95-
const allBarsInRow = getChartRows().find((row) => row.bars.includes(ganttBar))?.bars ?? []
96-
const ganttBarStart = toDayjs(ganttBar[barStart.value])
97-
const ganttBarEnd = toDayjs(ganttBar[barEnd.value])
98-
const overlapBar = allBarsInRow.find((otherBar) => {
99-
if (otherBar === ganttBar) {
100-
return false
101-
}
102-
const otherBarStart = toDayjs(otherBar[barStart.value])
103-
const otherBarEnd = toDayjs(otherBar[barEnd.value])
104-
overlapLeft = ganttBarStart.isBetween(otherBarStart, otherBarEnd)
105-
overlapRight = ganttBarEnd.isBetween(otherBarStart, otherBarEnd)
106-
overlapInBetween =
107-
otherBarStart.isBetween(ganttBarStart, ganttBarEnd) ||
108-
otherBarEnd.isBetween(ganttBarStart, ganttBarEnd)
109-
return overlapLeft || overlapRight || overlapInBetween
110-
})
111-
const overlapType = overlapLeft
112-
? "left"
113-
: overlapRight
114-
? "right"
115-
: overlapInBetween
116-
? "between"
117-
: null
118-
return { overlapBar, overlapType }
119-
}
120-
121-
const moveBundleOfPushedBarByMinutes = (
122-
pushedBar: GanttBarObject,
123-
minutes: number,
124-
direction: "left" | "right"
125-
) => {
126-
addBarToMovedBars(pushedBar)
127-
if (!pushedBar.ganttBarConfig.bundle) {
128-
return
146+
// Calculate the required movement to resolve overlap
147+
const calculateBarMovement = (
148+
currentBar: GanttBarObject,
149+
overlapBar: GanttBarObject,
150+
type: string
151+
): BarMovement => {
152+
const currentStart = toDayjs(currentBar[barStart.value])
153+
const currentEnd = toDayjs(currentBar[barEnd.value])
154+
const overlapStart = toDayjs(overlapBar[barStart.value])
155+
const overlapEnd = toDayjs(overlapBar[barEnd.value])
156+
157+
if (type === "left") {
158+
const minutes = overlapEnd.diff(currentStart, "minutes", true)
159+
return { bar: overlapBar, minutes, direction: "left" }
129160
}
161+
162+
const minutes = currentEnd.diff(overlapStart, "minutes", true)
163+
return { bar: overlapBar, minutes, direction: "right" }
164+
}
165+
166+
// Apply movement adjustment to a bar
167+
const applyBarAdjustment = (bar: GanttBarObject, adjustment: BarMovement) => {
168+
addBarToMovedBars(bar)
169+
170+
const timeAdjustment =
171+
adjustment.direction === "left"
172+
? (time: string) => toDayjs(time).subtract(adjustment.minutes, "minutes")
173+
: (time: string) => toDayjs(time).add(adjustment.minutes, "minutes")
174+
175+
bar[barStart.value] = format(timeAdjustment(bar[barStart.value]), dateFormat.value)
176+
bar[barEnd.value] = format(timeAdjustment(bar[barEnd.value]), dateFormat.value)
177+
}
178+
179+
// Adjust all bars in a bundle
180+
const adjustBundleBars = (sourceBar: GanttBarObject, adjustment: BarMovement) => {
181+
const bundle = sourceBar.ganttBarConfig.bundle
182+
if (!bundle) return
183+
130184
getChartRows().forEach((row) => {
131185
row.bars.forEach((bar) => {
132-
if (bar.ganttBarConfig.bundle === pushedBar.ganttBarConfig.bundle && bar !== pushedBar) {
186+
if (bar.ganttBarConfig.bundle === bundle && bar !== sourceBar) {
133187
addBarToMovedBars(bar)
134-
moveBarByMinutes(bar, minutes, direction)
188+
applyBarAdjustment(bar, adjustment)
135189
}
136190
})
137191
})
138192
}
139193

140-
const moveBarByMinutes = (bar: GanttBarObject, minutes: number, direction: "left" | "right") => {
141-
switch (direction) {
142-
case "left":
143-
bar[barStart.value] = format(
144-
toDayjs(bar, "start").subtract(minutes, "minutes"),
145-
dateFormat.value
146-
)
147-
bar[barEnd.value] = format(
148-
toDayjs(bar, "end").subtract(minutes, "minutes"),
149-
dateFormat.value
150-
)
151-
break
152-
case "right":
153-
bar[barStart.value] = format(
154-
toDayjs(bar, "start").add(minutes, "minutes"),
155-
dateFormat.value
156-
)
157-
bar[barEnd.value] = format(toDayjs(bar, "end").add(minutes, "minutes"), dateFormat.value)
194+
// Handle the end of a drag operation
195+
const handleDragEnd = (e: MouseEvent, bar: GanttBarObject) => {
196+
if (shouldSnapBack()) {
197+
snapBackMovedBars()
158198
}
159-
fixOverlaps(bar)
160-
}
161199

162-
const onEndDrag = (e: MouseEvent, bar: GanttBarObject) => {
163-
snapBackAllMovedBarsIfNeeded()
164-
const ev = {
165-
...e,
166-
type: "dragend"
167-
}
168-
emitBarEvent(ev, bar, undefined, new Map(movedBarsInDrag))
169-
movedBarsInDrag.clear()
200+
emitBarEvent({ ...e, type: "dragend" }, bar, undefined, new Map(dragState.movedBars))
201+
202+
dragState.movedBars.clear()
203+
dragState.isDragging = false
170204
}
171205

206+
// Add a bar to the moved bars tracking
172207
const addBarToMovedBars = (bar: GanttBarObject) => {
173-
if (!movedBarsInDrag.has(bar)) {
174-
const oldStart = bar[barStart.value]
175-
const oldEnd = bar[barEnd.value]
176-
movedBarsInDrag.set(bar, { oldStart, oldEnd })
208+
if (!dragState.movedBars.has(bar)) {
209+
dragState.movedBars.set(bar, {
210+
oldStart: bar[barStart.value],
211+
oldEnd: bar[barEnd.value]
212+
})
177213
}
178214
}
179215

180-
const snapBackAllMovedBarsIfNeeded = () => {
181-
if (pushOnOverlap.value || !noOverlap.value) {
182-
return
183-
}
216+
// Check if bars should snap back to original positions
217+
const shouldSnapBack = () => {
218+
if (pushOnOverlap.value || !noOverlap.value) return false
184219

185-
let isAnyOverlap = false
186-
movedBarsInDrag.forEach((_, bar) => {
187-
const { overlapBar } = getOverlapBarAndType(bar)
188-
if (overlapBar != null) {
189-
isAnyOverlap = true
190-
}
220+
return Array.from(dragState.movedBars.keys()).some((bar) => {
221+
const { overlapBar } = detectOverlap(bar)
222+
return overlapBar != null
191223
})
192-
if (!isAnyOverlap) {
193-
return
194-
}
195-
movedBarsInDrag.forEach(({ oldStart, oldEnd }, bar) => {
224+
}
225+
226+
// Reset all moved bars to their original positions
227+
const snapBackMovedBars = () => {
228+
dragState.movedBars.forEach(({ oldStart, oldEnd }, bar) => {
196229
bar[barStart.value] = oldStart
197230
bar[barEnd.value] = oldEnd
198231
})
@@ -203,3 +236,5 @@ export default function useBarDragManagement() {
203236
initDragOfBundle
204237
}
205238
}
239+
240+
export default useBarDragManagement

0 commit comments

Comments
 (0)