2
0
mirror of https://github.com/tenrok/vue-ganttastic.git synced 2026-06-22 10:10:33 +03:00
Files
vue-ganttastic/src/GGanttBar.vue
T

629 lines
17 KiB
Vue

<template>
<div>
<div
class="g-gantt-bar"
ref="g-gantt-bar"
:style="barStyle"
@mouseenter.stop="onMouseenter($event)"
@mouseleave.stop="onMouseleave($event)"
@mousedown.stop="onMousedown($event)"
@click.stop="onClick($event)"
@dblclick="onDblclick($event)"
@contextmenu="onContextmenu($event)"
>
<div class="g-gantt-bar-label">
<slot name="bar-label" :bar="bar">
{{ barConfig.label || '' }}
</slot>
</div>
<template v-if="barConfig.handles">
<div class="g-gantt-bar-handle-left" />
<div class="g-gantt-bar-handle-right" />
</template>
</div>
<transition name="fade" mode="out-in">
<div
v-if="!barConfig.noTooltip && (showTooltip || isDragging)"
class="g-gantt-tooltip"
:style="tooltipStyle"
>
<div
class="color-indicator"
:style="{
background:
this.barStyle.background || this.barStyle.backgroundColor,
}"
/>
{{ barStartText }}
-
{{ barEndText }}
</div>
</transition>
</div>
</template>
<script>
import moment from 'moment'
export default {
name: 'GGanttBar',
props: {
bar: { type: Object },
barContainer: [Object, DOMRect],
allBarsInRow: { type: Array },
},
inject: [
'getTimeCount',
'ganttChartProps',
'initDragOfBarsFromBundle',
'moveBarsFromBundleOfPushedBar',
'setDragLimitsOfGanttBar',
'onBarEvent',
'onDragendBar',
'getMinGapBetweenBars',
'getTimeUnit',
'getTimeFormat',
],
data() {
return {
showTooltip: false,
tooltipTimeout: null,
dragLimitLeft: null,
dragLimitRight: null,
isDragging: false,
isMainBarOfDrag: false, // is this the bar that was clicked on when starting to drag
// or is it dragged along some other bar from the same bundle
cursorOffsetX: 0,
mousemoveCallback: null, // gets initialized when starting to drag
// possible values: drag, dragByHandleLeft, dragByHandleRight,
barStartBeforeDrag: null,
barEndBeforeDrag: null,
timeUnit: this.getTimeUnit(),
timeChildKey:
this.ganttChartProps.timeaxisMode === 'month_days'
? 'hours'
: 'minutes',
timeChildFormat:
this.ganttChartProps.timeaxisMode === 'month_days' ? 'MM-DD' : 'HH:mm',
timeFormat: this.getTimeFormat(),
barStartKey: this.ganttChartProps.barStartKey,
barEndKey: this.ganttChartProps.barEndKey,
}
},
computed: {
barStartMoment: {
get: function () {
return moment(this.bar[this.barStartKey], this.timeFormat)
},
set: function (value) {
this.bar[this.barStartKey] = value.format(this.timeFormat)
},
},
barEndMoment: {
get: function () {
return moment(this.bar[this.barEndKey])
},
set: function (value) {
this.bar[this.barEndKey] = value.format(this.timeFormat)
},
},
barStartText: {
get() {
return moment(this.barStartMoment).format(this.timeChildFormat)
},
},
barEndText: {
get() {
let endMoment = moment(this.barEndMoment)
return endMoment.format(this.timeChildFormat)
},
},
barConfig() {
if (this.bar.ganttBarConfig) {
return {
...this.bar.ganttBarConfig,
background: this.bar.ganttBarConfig.isShadow
? 'grey'
: this.bar.ganttBarConfig.background ||
this.bar.ganttBarConfig.backgroundColor,
opacity: this.bar.ganttBarConfig.isShadow
? '0.3'
: this.bar.ganttBarConfig.opacity,
}
}
return {}
},
barStyle() {
if (!this.barContainer.width) return
let xStart = this.mapTimeToPosition(this.barStartMoment)
let xEnd = this.mapTimeToPosition(this.barEndMoment)
return {
...(this.barConfig || {}),
left: `${xStart}px`,
width: `${xEnd - xStart}px`,
height: `${this.ganttChartProps.rowHeight - 6}px`,
zIndex: this.barConfig.zIndex || (this.isDragging ? 2 : 1),
}
},
tooltipStyle() {
return {
left: this.barStyle.left,
top: `${this.ganttChartProps.rowHeight}px`,
}
},
chartStartMoment() {
return moment(this.ganttChartProps.chartStart)
},
chartEndMoment() {
return moment(this.ganttChartProps.chartEnd)
},
},
methods: {
onMouseenter(e) {
if (this.tooltipTimeout) {
clearTimeout(this.tooltipTimeout)
}
this.tooltipTimeout = setTimeout(() => (this.showTooltip = true), 800)
this.onBarEvent({ event: e, type: e.type }, this)
},
onMouseleave(e) {
clearTimeout(this.tooltipTimeout)
this.showTooltip = false
this.onBarEvent({ event: e, type: e.type }, this)
},
onContextmenu(e) {
const time = this.mapPositionToTime(
e.clientX - this.barContainer.left
).format(this.timeFormat)
this.onBarEvent({ event: e, type: e.type, time }, this)
},
onClick(e) {
const time = this.mapPositionToTime(
e.clientX - this.barContainer.left
).format(this.timeFormat)
this.onBarEvent({ event: e, type: e.type, time }, this)
},
onDblclick(e) {
const time = this.mapPositionToTime(
e.clientX - this.barContainer.left
).format(this.timeFormat)
this.onBarEvent({ event: e, type: e.type, time }, this)
},
onMousedown(e) {
e.preventDefault()
if (e.button === 2) {
return
}
if (!this.barConfig.immobile && !this.barConfig.isShadow) {
this.setDragLimitsOfGanttBar(this)
// initialize the dragging on next mousemove event:
window.addEventListener('mousemove', this.onFirstMousemove, {
once: true,
})
// if next mousemove happens after mouse up (if user just presses mouse button down, then up, without moving):
window.addEventListener(
'mouseup',
() => window.removeEventListener('mousemove', this.onFirstMousemove),
{ once: true }
)
}
const time = this.mapPositionToTime(
e.clientX - this.barContainer.left
).format(this.timeFormat)
this.onBarEvent({ event: e, type: e.type, time }, this)
},
onFirstMousemove(e) {
this.isMainBarOfDrag = true
// this method is injected here by GGanttChart.vue, and calls initDrag()
// for all GGanttBars that belong to the same bundle as this bar:
this.initDragOfBarsFromBundle(this, e)
},
/* --------------------------------------------------------- */
/* ------------- METHODS FOR DRAGGING THE BAR -------------- */
/* --------------------------------------------------------- */
initDrag(e) {
// "e" must be the mousedown event
this.isDragging = true
this.barStartBeforeDrag = this.bar[this.barStartKey]
this.barEndBeforeDrag = this.bar[this.barEndKey]
let barX = this.$refs['g-gantt-bar'].getBoundingClientRect().left
this.cursorOffsetX = e.clientX - barX
let mousedownType = e.target.className
switch (mousedownType) {
case 'g-gantt-bar-handle-left':
document.body.style.cursor = 'w-resize'
this.mousemoveCallback = this.dragByHandleLeft
break
case 'g-gantt-bar-handle-right':
document.body.style.cursor = 'e-resize'
this.mousemoveCallback = this.dragByHandleRight
break
default:
this.mousemoveCallback = this.drag
}
window.addEventListener('mousemove', this.mousemoveCallback)
window.addEventListener('mouseup', this.endDrag)
},
drag(e) {
let barWidth = this.$refs['g-gantt-bar'].getBoundingClientRect().width
let newXStart = e.clientX - this.barContainer.left - this.cursorOffsetX
let newXEnd = newXStart + barWidth
if (this.isPosOutOfDragRange(newXStart, newXEnd)) {
return
}
this.barStartMoment = this.mapPositionToTime(newXStart)
this.barEndMoment = this.mapPositionToTime(newXEnd)
this.manageOverlapping()
this.onBarEvent({ event: e, type: 'drag' }, this)
},
dragByHandleLeft(e) {
let newXStart = e.clientX - this.barContainer.left
let newStartMoment = this.mapPositionToTime(newXStart)
if (
newStartMoment.isSameOrAfter(this.barEndMoment) ||
this.isPosOutOfDragRange(newXStart, null)
) {
return
}
this.barStartMoment = newStartMoment
this.manageOverlapping()
},
dragByHandleRight(e) {
let newXEnd = e.clientX - this.barContainer.left
let newEndMoment = this.mapPositionToTime(newXEnd)
if (
newEndMoment.isSameOrBefore(this.barStartMoment, this.timeUnit) ||
this.isPosOutOfDragRange(null, newXEnd)
) {
return
}
this.barEndMoment = newEndMoment
this.manageOverlapping()
},
isPosOutOfDragRange(xStart, xEnd) {
if (xStart && xStart < 0) {
return true
}
// 设置允许推动旁边的bar时,拖拽到到位置就算进入了重叠,也不算超出范围
if (this.ganttChartProps.pushOnOverlap) {
return false
}
if (
xStart &&
this.dragLimitLeft !== null &&
xStart < this.dragLimitLeft + this.getMinGapBetweenBars()
) {
return true
}
if (
xEnd &&
this.dragLimitRight !== null &&
xEnd > this.dragLimitRight - this.getMinGapBetweenBars()
) {
return true
}
return false
},
endDrag(e) {
let left = false,
right = false,
move = false
switch (document.body.style.cursor) {
case 'e-resize':
right = true
break
case 'w-resize':
left = true
break
default:
move = true
break
}
// console.log('endDrag', { left, right, move })
this.isDragging = false
this.dragLimitLeft = null
this.dragLimitRight = null
document.body.style.cursor = 'auto'
window.removeEventListener('mousemove', this.mousemoveCallback)
window.removeEventListener('mouseup', this.endDrag)
if (this.isMainBarOfDrag) {
this.onDragendBar(e, this, { left, right, move })
this.isMainBarOfDrag = false
}
},
snapBack() {
this.barStartMoment = moment(this.barStartBeforeDrag)
this.barEndMoment = moment(this.barEndBeforeDrag)
},
manageOverlapping() {
if (
!this.ganttChartProps.pushOnOverlap ||
this.barConfig.pushOnOverlap === false
) {
return
}
let currentBar = this.bar
let { overlapBar, overlapType } = this.getOverlapBarAndType(currentBar)
while (overlapBar) {
let minuteDiff
let currentStartMoment = moment(currentBar[this.barStartKey])
let currentEndMoment = moment(currentBar[this.barEndKey])
let overlapStartMoment = moment(overlapBar[this.barStartKey])
let overlapEndMoment = moment(overlapBar[this.barEndKey])
switch (overlapType) {
case 'left':
minuteDiff =
overlapEndMoment.diff(
currentStartMoment,
this.timeChildKey,
true
) + this.getMinGapBetweenBars()
overlapBar[this.barEndKey] = currentStartMoment
.subtract(this.getMinGapBetweenBars(), this.timeChildKey, true)
.format(this.timeFormat)
overlapBar[this.barStartKey] = overlapStartMoment
.subtract(minuteDiff, this.timeChildKey, true)
.format(this.timeFormat)
break
case 'right':
minuteDiff =
currentEndMoment.diff(
overlapStartMoment,
this.timeChildKey,
true
) + this.getMinGapBetweenBars()
overlapBar[this.barStartKey] = currentEndMoment
.add(this.getMinGapBetweenBars(), this.timeChildKey, true)
.format(this.timeFormat)
overlapBar[this.barEndKey] = overlapEndMoment
.add(minuteDiff, this.timeChildKey, true)
.format(this.timeFormat)
break
default:
// eslint-disable-next-line
console.warn(
'One bar is inside of the other one! This should never occur while push-on-overlap is active!'
)
return
}
this.moveBarsFromBundleOfPushedBar(overlapBar, minuteDiff, overlapType)
currentBar = overlapBar
;({ overlapBar, overlapType } = this.getOverlapBarAndType(overlapBar))
}
},
getOverlapBarAndType(bar) {
let barStartMoment = moment(bar[this.barStartKey])
let barEndMoment = moment(bar[this.barEndKey])
let overlapLeft, overlapRight, overlapInBetween
let overlapBar = this.allBarsInRow.find((otherBar) => {
if (
otherBar === bar ||
otherBar.ganttBarConfig.pushOnOverlap === false
) {
return false
}
let otherBarStartMoment = moment(otherBar[this.barStartKey])
let otherBarEndMoment = moment(otherBar[this.barEndKey])
overlapLeft = barStartMoment.isBetween(
otherBarStartMoment,
otherBarEndMoment
)
overlapRight = barEndMoment.isBetween(
otherBarStartMoment,
otherBarEndMoment
)
overlapInBetween =
otherBarStartMoment.isBetween(barStartMoment, barEndMoment) ||
otherBarEndMoment.isBetween(barStartMoment, barEndMoment)
return overlapLeft || overlapRight || overlapInBetween
})
let overlapType = overlapLeft
? 'left'
: overlapRight
? 'right'
: overlapInBetween
? 'between'
: null
return { overlapBar, overlapType }
},
// this is used in GGanttChart, when a bar from a bundle is pushed
// so that bars from its bundle also get pushed
moveBarByChildPointsAndPush(childPointCount, direction) {
switch (direction) {
case 'left':
this.barStartMoment = moment(this.barStartMoment).subtract(
childPointCount,
this.timeChildKey,
true
)
this.barEndMoment = moment(this.barEndMoment).subtract(
childPointCount,
this.timeChildKey,
true
)
break
case 'right':
this.barStartMoment = moment(this.barStartMoment).add(
childPointCount,
this.timeChildKey,
true
)
this.barEndMoment = moment(this.barEndMoment).add(
childPointCount,
this.timeChildKey,
true
)
break
default:
// eslint-disable-next-line
console.warn('wrong direction in moveBarByChildPointsAndPush')
return
}
this.manageOverlapping()
},
/* --------------------------------------------------------- */
/* ------- MAPPING POSITION TO TIME (AND VICE VERSA) ------- */
/* --------------------------------------------------------- */
mapTimeToPosition(time) {
let timeDiffFromStart = moment(time).diff(
this.chartStartMoment,
this.timeUnit,
true
)
let pos =
(timeDiffFromStart / this.getTimeCount()) * this.barContainer.width
return pos
},
mapPositionToTime(xPos) {
let timeDiffFromStart =
(xPos / this.barContainer.width) * this.getTimeCount()
if (this.timeUnit === 'days') {
let duration = moment.duration(timeDiffFromStart, 'days')
timeDiffFromStart = duration.asHours()
}
return moment(this.chartStartMoment).add(timeDiffFromStart, 'hours')
},
},
}
</script>
<style scoped>
.g-gantt-bar {
position: absolute;
top: 2px;
left: 30px;
display: flex;
justify-content: space-between;
align-items: center;
color: white;
width: 300px;
height: 34px;
border-radius: 15px;
background: #79869c;
overflow: hidden;
}
.g-gantt-bar-label {
width: 100%;
height: 100%;
box-sizing: border-box;
padding: 0 14px 0 14px; /* 14px is the width of the handle */
display: flex;
justify-content: center;
align-items: center;
}
.g-gantt-bar-label > * {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.g-gantt-bar > .g-gantt-bar-handle-left,
.g-gantt-bar > .g-gantt-bar-handle-right {
position: absolute;
width: 10px;
height: 100%;
background: white;
opacity: 0.7;
border-radius: 40px;
}
.g-gantt-bar-handle-left {
left: 0;
cursor: w-resize;
}
.g-gantt-bar-handle-right {
right: 0;
cursor: e-resize;
}
.g-gantt-bar-label img {
pointer-events: none;
}
.g-gantt-tooltip {
position: absolute;
background: black;
color: white;
z-index: 3;
font-size: 0.7em;
padding: 3px;
border-radius: 3px;
transition: opacity 0.2s;
display: flex;
align-items: center;
}
.g-gantt-tooltip:before {
content: '';
position: absolute;
top: 0;
left: 10%;
width: 0;
height: 0;
border: 10px solid transparent;
border-bottom-color: black;
border-top: 0;
margin-left: -5px;
margin-top: -5px;
}
.g-gantt-tooltip > .color-indicator {
width: 8px;
height: 8px;
border-radius: 100%;
margin-right: 4px;
}
.fade-enter-active {
animation: fade-in 0.3s;
}
.fade-leave-active {
animation: fade-in 0.3s reverse;
}
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
</style>