2
0
mirror of https://github.com/tenrok/vue-ganttastic.git synced 2026-06-11 13:02:24 +03:00
Files
vue-ganttastic/lib/components/GGanttBar.vue
T

671 lines
20 KiB
Vue

<template>
<div>
<div
ref="g-bar"
:class="[
'g-gantt-bar',
{ 'g-gantt-bar-immobile': barConfig.immobile },
{ 'g-gantt-bar-resizable': barConfig.handles }
]"
: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="localBar">
{{ 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-bar__tooltip"
:style="tooltipStyle"
>
<div
class="color-indicator"
:style="{
background:
this.barStyle.background || this.barStyle.backgroundColor
}"
/>
<div>
<div>{{ localBar.tooltip }}</div>
<div>{{ tooltipContent }}</div>
</div>
</div>
</transition>
</div>
</template>
<script>
import moment from 'moment'
String.prototype.formatUnicorn =
String.prototype.formatUnicorn ||
function () {
'use strict'
let str = this /*.toString()*/
if (arguments.length) {
const notSeenInNature = '#$%#$%' // or whatever
const t = typeof arguments[0]
let args =
'string' === t || 'number' === t
? Array.prototype.slice.call(arguments)
: arguments[0]
for (let key in args) {
let rv = String(args[key]).replace('{', notSeenInNature)
str = str.replace(new RegExp('\\{' + key + '\\}', 'gi'), rv)
}
str = str.replace(notSeenInNature, '{')
}
return str
}
export default {
name: 'GGanttBar',
props: {
bar: { type: Object },
barContainer: [Object, DOMRect],
allBarsInRow: { type: Array }
},
inject: [
'getTimeCount',
'getChartProps',
'initDragOfBarsFromBundle',
'moveBarsFromBundleOfPushedBar',
'setDragLimitsOfGanttBar',
'onBarEvent',
'onDragendBar',
'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,
localBar: this.bar
}
},
computed: {
chartProps() {
return this.getChartProps()
},
minGapBetweenBars() {
return this.chartProps.minGapBetweenBars
},
timeChildKey() {
return this.chartProps.precision === 'month' ? 'hours' : 'minutes'
},
timeChildFormat() {
return this.chartProps.precision === 'month'
? 'DD.MM.YYYY'
: 'DD.MM.YYYY HH:mm'
},
barConfigKey() {
return this.chartProps.barConfigKey
},
barStartKey() {
return this.chartProps.barStartKey
},
barEndKey() {
return this.chartProps.barEndKey
},
timeCount() {
return this.getTimeCount()
},
timeUnit() {
return this.getTimeUnit()
},
timeFormat() {
return this.getTimeFormat()
},
barStartMoment: {
get() {
return moment(this.localBar[this.barStartKey], this.timeFormat)
},
set(value) {
this.localBar[this.barStartKey] = value.format(this.timeFormat)
}
},
barEndMoment: {
get() {
return moment(this.localBar[this.barEndKey])
},
set(value) {
this.localBar[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)
}
},
barDurationText() {
const duration = moment.duration(
this.barEndMoment.diff(this.barStartMoment)
)
return `${Math.floor(duration.as('d'))} ${moment
.utc(duration.as('ms'))
.format('HH:mm')}`
},
barConfig() {
if (this.localBar[this.barConfigKey]) {
return {
...this.localBar[this.barConfigKey],
background: this.localBar[this.barConfigKey].isShadow
? 'grey'
: this.localBar[this.barConfigKey].background ||
this.localBar[this.barConfigKey].backgroundColor,
opacity: this.localBar[this.barConfigKey].isShadow
? '0.3'
: this.localBar[this.barConfigKey].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.chartProps.rowHeight - 6}px`,
zIndex: this.barConfig.zIndex || (this.isDragging ? 2 : 1)
}
},
tooltipContent() {
return this.chartProps.tooltipFormat.formatUnicorn({
start: this.barStartText,
end: this.barEndText,
duration: this.barDurationText
})
},
tooltipStyle() {
return {
left: this.barStyle.left,
top: `${this.chartProps.rowHeight}px`
}
},
chartStartMoment() {
return moment(this.chartProps.chartStart)
},
chartEndMoment() {
return moment(this.chartProps.chartEnd)
}
},
watch: {
bar(value) {
this.localBar = value
}
},
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.localBar[this.barStartKey]
this.barEndBeforeDrag = this.localBar[this.barEndKey]
let barX = this.$refs['g-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)
},
getBarWidth(bar) {
let xStart = this.mapTimeToPosition(moment(bar[this.barStartKey]))
let xEnd = this.mapTimeToPosition(moment(bar[this.barEndKey]))
return xEnd - xStart
},
drag(e) {
const chart = e.target.closest('.g-gantt-chart')
if (!chart) return
let barWidth = this.$refs['g-bar'].getBoundingClientRect().width
let newXStart =
chart.scrollLeft +
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) {
const chart = e.target.closest('.g-gantt-chart')
if (!chart) return
let newXStart = chart.scrollLeft + e.clientX - this.barContainer.left
let newStartMoment = this.mapPositionToTime(newXStart)
if (
this.barEndMoment.diff(newStartMoment, this.timeUnit) < 1 ||
this.isPosOutOfDragRange(newXStart, null)
) {
return
}
this.barStartMoment = newStartMoment
this.manageOverlapping()
},
dragByHandleRight(e) {
const chart = e.target.closest('.g-gantt-chart')
if (!chart) return
let newXEnd = chart.scrollLeft + 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(newXStart, newXEnd) {
if (newXStart && newXStart < 0) {
return true
}
if (newXEnd > this.barContainer.width) {
return true
}
if (
newXStart &&
this.dragLimitLeft !== null &&
newXStart < this.dragLimitLeft + this.minGapBetweenBars
) {
return true
}
if (
newXEnd &&
this.dragLimitRight !== null &&
newXEnd > this.dragLimitRight - this.minGapBetweenBars
) {
return true
}
// if (
// moment(this.localBar[this.barStartKey]).isAfter(this.barStartBeforeDrag) &&
// moment(this.localBar[this.barStartKey])
// .add(1, this.timeUnit)
// .isAfter(this.localBar[this.barEndBeforeDrag])
// ) {
// return true
// }
if (
!this.chartProps.pushOnOverlap ||
this.barConfig.pushOnOverlap === false
) {
return false
}
const isSqueezeToLeft =
newXStart &&
moment(this.localBar[this.barStartKey]).isBefore(
this.barStartBeforeDrag
)
const isSqueezeToRight =
newXEnd &&
moment(this.localBar[this.barEndKey]).isAfter(this.barEndBeforeDrag)
const currentIndex = this.allBarsInRow.findIndex(
bar => bar == this.localBar
)
let otherBars = []
if (isSqueezeToRight) {
otherBars = this.allBarsInRow.slice(currentIndex + 1)
if (otherBars.length) {
let otherBarTotalWidth = otherBars
.map(bar => this.getBarWidth(bar))
.reduce((accumulator, currentValue) => accumulator + currentValue)
if (newXEnd > this.barContainer.width - otherBarTotalWidth) {
return true
}
}
} else if (isSqueezeToLeft) {
otherBars = this.allBarsInRow.slice(0, currentIndex)
if (otherBars.length) {
let otherBarTotalWidth = otherBars
.map(bar => this.getBarWidth(bar))
.reduce((accumulator, currentValue) => accumulator + currentValue)
if (newXStart < otherBarTotalWidth) {
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.chartProps.pushOnOverlap ||
this.barConfig.pushOnOverlap === false
) {
return
}
let currentBar = this.localBar
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.minGapBetweenBars
overlapBar[this.barEndKey] = currentStartMoment
.subtract(this.minGapBetweenBars, 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.minGapBetweenBars
overlapBar[this.barStartKey] = currentEndMoment
.add(this.minGapBetweenBars, 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[this.barConfigKey] &&
otherBar[this.barConfigKey].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.timeCount) * this.barContainer.width
return pos
},
mapPositionToTime(xPos) {
let timeDiffFromStart = (xPos / this.barContainer.width) * this.timeCount
if (this.timeUnit === 'days') {
let duration = moment.duration(timeDiffFromStart, 'days')
timeDiffFromStart = duration.asHours()
}
return moment(this.chartStartMoment).add(timeDiffFromStart, 'hours')
}
}
}
</script>