mirror of
https://github.com/tenrok/vue-ganttastic.git
synced 2026-06-25 13:30:33 +03:00
First commit. Prepared for npm packaging.
Wrote README.
This commit is contained in:
+21
@@ -0,0 +1,21 @@
|
|||||||
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
/dist
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# Log files
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
# vue-ganttastic
|
||||||
|
|
||||||
|
A simple and easy-to-use Gantt chart component for Vue.js
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
If you use <kbd>npm</kbd> in your project, you can install vue-ganttastic simply with:
|
||||||
|
```
|
||||||
|
npm install vue-ganttastic
|
||||||
|
```
|
||||||
|
If you do not use <kbd>npm</kbd> in your project, you may alternatively copy and paste all files from the
|
||||||
|
<code>components</code> folder and
|
||||||
|
and import the components <code>GGanttChart</code> and <code>GGanttRow</code> wherever you need them
|
||||||
|
|
||||||
|
## Basic Usage
|
||||||
|
Import the components <code>GGanttChart</code> and <code>GGanttRow</code>.
|
||||||
|
Use <code>g-gantt-chart</code> in your template, pass the desired chart start and chart end time as props (<code>chart-start</code> and <code>chart-end</code>) and add <code>g-gantt-row</code>s
|
||||||
|
to the default template slot.
|
||||||
|
Pass an array containing your bar objects to every row using the <code>bars</code> prop, while specifying the name of the properties in your bar objects that stand for the bar start and bar end time using the props <code>bar-start</code> and <code>bar-end</code>
|
||||||
|
|
||||||
|
For more detailed information, such as how to style the bars or additional configuration options, please refer to the [docs]() on the project's homepage (coming soon).
|
||||||
|
|
||||||
|
The following code showcases a simple usage example in a .vue SFC (Single File Component)
|
||||||
|
```html
|
||||||
|
<template>
|
||||||
|
...
|
||||||
|
|
||||||
|
<g-gantt-chart
|
||||||
|
:chart-start="myChartStart"
|
||||||
|
:chart-end="myChartEnd"
|
||||||
|
>
|
||||||
|
<g-gantt-row
|
||||||
|
v-for="row in rows"
|
||||||
|
:key="row.label"
|
||||||
|
:label="row.label"
|
||||||
|
:bars="row.bars"
|
||||||
|
bar-start="myStart"
|
||||||
|
bar-end="myEnd"
|
||||||
|
/>
|
||||||
|
</g-gantt-chart>
|
||||||
|
|
||||||
|
...
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
import {GGanttChart, GGanttBar} from 'vue-ganttastic'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
components:{
|
||||||
|
GGanttChart,
|
||||||
|
GGanttBar
|
||||||
|
},
|
||||||
|
|
||||||
|
data(){
|
||||||
|
return {
|
||||||
|
myChartStart: "2020-03-01 00:00",
|
||||||
|
myChartEnd: "2020-03-03 00:00",
|
||||||
|
rows: [
|
||||||
|
{
|
||||||
|
label: "My row #1",
|
||||||
|
bars: [
|
||||||
|
{
|
||||||
|
myStart: "2020-03-01 12:10",
|
||||||
|
myEnd: "2020-03-01 16:35"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "My row #2",
|
||||||
|
bars: [
|
||||||
|
{
|
||||||
|
myStart: "2020-03-02 01:00",
|
||||||
|
myEnd: "2020-03-02 12:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
myStart: "2020-03-02 13:00",
|
||||||
|
myEnd: "2020-03-02 22:00"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
Pull requests are warmly welcomed, while every major change or proposal ought to be discussed in an issue first. As the project is still new, I will gladly accept suggestions, proposals, contributions etc.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
[Moment.js](https://momentjs.com/)
|
||||||
|
|
||||||
|
## License
|
||||||
|
[MIT](https://choosealicense.com/licenses/mit/)
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
module.exports = {
|
||||||
|
banner: true,
|
||||||
|
output: {
|
||||||
|
extractCSS: false,
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
vue: {
|
||||||
|
css: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
Generated
+5156
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"name": "vue-ganttastic",
|
||||||
|
"version": "0.9.0",
|
||||||
|
"description": "A simple and customizable Gantt chart component for Vue.js",
|
||||||
|
"main": "./dist/index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"build": "bili --name index --plugin vue --vue.css false"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist/*"
|
||||||
|
],
|
||||||
|
"keywords": [
|
||||||
|
"gantt",
|
||||||
|
"chart",
|
||||||
|
"bar",
|
||||||
|
"diagram",
|
||||||
|
"vue",
|
||||||
|
"vuejs",
|
||||||
|
"ganttastic"
|
||||||
|
],
|
||||||
|
"author": "Marko Zunic",
|
||||||
|
"license": "MIT",
|
||||||
|
"devDependencies": {
|
||||||
|
"bili": "^4.8.1",
|
||||||
|
"rollup-plugin-vue": "^5.1.6",
|
||||||
|
"vue-template-compiler": "^2.6.11"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"moment": "^2.24.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,449 @@
|
|||||||
|
<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)"
|
||||||
|
@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="showTooltip || isDragging"
|
||||||
|
class="g-gantt-tooltip"
|
||||||
|
:style="tooltipStyle"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="color-indicator"
|
||||||
|
:style="{background: this.barStyle.background || this.barStyle.backgroundColor}"
|
||||||
|
/>
|
||||||
|
{{bar[barStart] | TimeFilter}}
|
||||||
|
-
|
||||||
|
{{bar[barEnd] | TimeFilter}}
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import moment from 'moment'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "GGanttBar",
|
||||||
|
|
||||||
|
props:{
|
||||||
|
bar: {type: Object},
|
||||||
|
barStart: {type: String}, // property name of the bar objects that represents the start datetime
|
||||||
|
barEnd: {type: String}, // property name of the bar objects that represents the end datetime,
|
||||||
|
barContainer: [Object, DOMRect],
|
||||||
|
allBarsInRow: {type: Array}
|
||||||
|
},
|
||||||
|
|
||||||
|
inject: [
|
||||||
|
"getHourCount",
|
||||||
|
"ganttChartProps",
|
||||||
|
"initDragOfBarsFromBundle",
|
||||||
|
"moveBarsFromBundleOfPushedBar",
|
||||||
|
"setDragLimitsOfGanttBar",
|
||||||
|
"onBarEvent",
|
||||||
|
"onDragendBar"
|
||||||
|
],
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed:{
|
||||||
|
// use these computed moment objects to work with the bar's start/end dates:
|
||||||
|
// instead of directly mutating them:
|
||||||
|
barStartMoment:{
|
||||||
|
get(){
|
||||||
|
return moment(this.bar[this.barStart])
|
||||||
|
},
|
||||||
|
set(value){
|
||||||
|
this.bar[this.barStart] = moment(value).format("YYYY-MM-DD HH:mm:ss")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
barEndMoment: {
|
||||||
|
get(){
|
||||||
|
return moment(this.bar[this.barEnd])
|
||||||
|
},
|
||||||
|
set(value){
|
||||||
|
this.bar[this.barEnd] = moment(value).format("YYYY-MM-DD HH:mm:ss")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
barConfig(){
|
||||||
|
return this.bar.ganttBarConfig || {}
|
||||||
|
},
|
||||||
|
|
||||||
|
barStyle(){
|
||||||
|
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.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(e, this)
|
||||||
|
},
|
||||||
|
|
||||||
|
onMouseleave(e){
|
||||||
|
clearTimeout(this.tooltipTimeout)
|
||||||
|
this.showTooltip = false
|
||||||
|
this.onBarEvent(e, this)
|
||||||
|
},
|
||||||
|
|
||||||
|
onMousedown(e){
|
||||||
|
e.preventDefault()
|
||||||
|
if(e.button === 2 || this.barConfig.immobile){
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.setDragLimitsOfGanttBar(this)
|
||||||
|
// initialize the dragging on next mousemove event:
|
||||||
|
window.addEventListener("mousemove", this.onFirstMousemove, {once: true})
|
||||||
|
this.onBarEvent(e, 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
|
||||||
|
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 = "w-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({...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.isPosOutOfDragRange(null, newXEnd)){
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.barEndMoment = newEndMoment
|
||||||
|
this.manageOverlapping()
|
||||||
|
},
|
||||||
|
|
||||||
|
isPosOutOfDragRange(xStart, xEnd){
|
||||||
|
if(xStart && this.dragLimitLeft !== null && xStart < this.dragLimitLeft+2){
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if(xEnd && this.dragLimitRight !== null && xEnd > this.dragLimitRight-2){
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
|
||||||
|
endDrag(e){
|
||||||
|
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)
|
||||||
|
this.isMainBarOfDrag = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
manageOverlapping(){
|
||||||
|
if(!this.ganttChartProps.pushOnOverlap){
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let currentBar = this.bar
|
||||||
|
let {overlapBar, overlapType} = this.getOverlapBarAndType(currentBar)
|
||||||
|
while(overlapBar){
|
||||||
|
let minuteDiff
|
||||||
|
let currentStartMoment = moment(currentBar[this.barStart])
|
||||||
|
let currentEndMoment = moment(currentBar[this.barEnd])
|
||||||
|
let overlapStartMoment = moment(overlapBar[this.barStart])
|
||||||
|
let overlapEndMoment = moment(overlapBar[this.barEnd])
|
||||||
|
switch(overlapType){
|
||||||
|
case "left":
|
||||||
|
minuteDiff = overlapEndMoment.diff(currentStartMoment, "minutes", true)
|
||||||
|
overlapBar[this.barEnd] = currentBar[this.barStart]
|
||||||
|
overlapBar[this.barStart] = overlapStartMoment.subtract(minuteDiff, "minutes", true)
|
||||||
|
break
|
||||||
|
case "right":
|
||||||
|
minuteDiff = currentEndMoment.diff(overlapStartMoment, "minutes", true)
|
||||||
|
overlapBar[this.barStart] = currentBar[this.barEnd]
|
||||||
|
overlapBar[this.barEnd] = overlapEndMoment.add(minuteDiff, "minutes", true)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
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.barStart])
|
||||||
|
let barEndMoment = moment(bar[this.barEnd])
|
||||||
|
let overlapLeft, overlapRight, overlapInBetween
|
||||||
|
let overlapBar = this.allBarsInRow.find(otherBar => {
|
||||||
|
if(otherBar === bar){
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
let otherBarStart = moment(otherBar[this.barStart])
|
||||||
|
let otherBarEnd = moment(otherBar[this.barEnd])
|
||||||
|
overlapLeft = barStartMoment.isBetween(otherBarStart, otherBarEnd)
|
||||||
|
overlapRight = barEndMoment.isBetween(otherBarStart, otherBarEnd)
|
||||||
|
overlapInBetween = otherBarStart.isBetween(barStartMoment, barEndMoment)
|
||||||
|
|| otherBarEnd.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
|
||||||
|
moveBarByMinutesAndPush(minuteCount, direction){
|
||||||
|
switch(direction){
|
||||||
|
case "left":
|
||||||
|
this.barStartMoment = moment(this.barStartMoment).subtract(minuteCount, "minutes", true)
|
||||||
|
this.barEndMoment = moment(this.barEndMoment).subtract(minuteCount, "minutes", true)
|
||||||
|
break
|
||||||
|
case "right":
|
||||||
|
this.barStartMoment = moment(this.barStartMoment).add(minuteCount, "minutes", true)
|
||||||
|
this.barEndMoment = moment(this.barEndMoment).add(minuteCount, "minutes", true)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
console.warn("wrong direction in moveBarByMinutesAndPush")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.manageOverlapping()
|
||||||
|
},
|
||||||
|
|
||||||
|
/* --------------------------------------------------------- */
|
||||||
|
/* ------- MAPPING POSITION TO TIME (AND VICE VERSA) ------- */
|
||||||
|
/* --------------------------------------------------------- */
|
||||||
|
mapTimeToPosition(time){
|
||||||
|
let hourDiffFromStart = moment(time).diff(this.chartStartMoment, "hour", true)
|
||||||
|
return (hourDiffFromStart / this.getHourCount()) * this.barContainer.width
|
||||||
|
},
|
||||||
|
|
||||||
|
mapPositionToTime(xPos){
|
||||||
|
let hourDiffFromStart = (xPos/this.barContainer.width)*this.getHourCount()
|
||||||
|
return this.chartStartMoment.clone().add(hourDiffFromStart, "hours")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
filters:{
|
||||||
|
TimeFilter(value){
|
||||||
|
return moment(value).format("HH:mm")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
</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;
|
||||||
|
cursor: w-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-gantt-bar-handle-left {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-gantt-bar-handle-right {
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-gantt-bar-label img {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-gantt-tooltip{
|
||||||
|
position: absolute;
|
||||||
|
background: black;
|
||||||
|
color: white;
|
||||||
|
z-index: 3;
|
||||||
|
font-size: 0.70em;
|
||||||
|
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 .3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-leave-active {
|
||||||
|
animation: fade-in .3s reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fade-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
} to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,291 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
id="g-gantt-chart"
|
||||||
|
:style="{width: width, background: themeColors.background}"
|
||||||
|
>
|
||||||
|
<g-gantt-timeaxis
|
||||||
|
v-if="!hideTimeaxis"
|
||||||
|
:chart-start="chartStart"
|
||||||
|
:chart-end="chartEnd"
|
||||||
|
:row-label-width="rowLabelWidth"
|
||||||
|
:timemarker-offset="timemarkerOffset"
|
||||||
|
:theme-colors="themeColors"
|
||||||
|
:locale="locale"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<g-gantt-grid
|
||||||
|
v-if="grid"
|
||||||
|
:chart-start="chartStart"
|
||||||
|
:chart-end="chartEnd"
|
||||||
|
:row-label-width="rowLabelWidth"
|
||||||
|
:highlighted-hours="highlightedHours"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div id="g-gantt-rows-container">
|
||||||
|
<slot/> <!-- the g-gantt-row components go here -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import moment from 'moment'
|
||||||
|
import GanttasticThemeColors from './GanttasticThemeColors.js'
|
||||||
|
import GGanttTimeaxis from './GGanttTimeaxis.vue'
|
||||||
|
import GGanttGrid from './GGanttGrid.vue'
|
||||||
|
import GGanttRow from './GGanttRow.vue'
|
||||||
|
import GGanttBar from './GGanttBar.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
|
||||||
|
name: "GGanttChart",
|
||||||
|
|
||||||
|
components:{
|
||||||
|
GGanttTimeaxis,
|
||||||
|
GGanttGrid
|
||||||
|
},
|
||||||
|
|
||||||
|
props:{
|
||||||
|
chartStart: {type: String, default: moment().startOf("day").format("YYYY-MM-DD HH:mm:ss")},
|
||||||
|
chartEnd: {type: String, default: moment().startOf("day").add(12,"hours").format("YYYY-MM-DD HH:mm:ss")},
|
||||||
|
hideTimeaxis: Boolean,
|
||||||
|
rowLabelWidth: {type: String, default: "10%"},
|
||||||
|
rowHeight: {type: Number, default: 40},
|
||||||
|
locale: {type: String, default: "en"},
|
||||||
|
theme: String,
|
||||||
|
grid: Boolean,
|
||||||
|
highlightedHours: {type: Array, default: () => []},
|
||||||
|
width: {type: String, default: "100%"}, // the total width of the entire ganttastic component in %
|
||||||
|
pushOnOverlap: {type: Boolean}
|
||||||
|
},
|
||||||
|
|
||||||
|
data(){
|
||||||
|
return{
|
||||||
|
timemarkerOffset: 0,
|
||||||
|
movedBarsInDrag: new Set()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed:{
|
||||||
|
|
||||||
|
hourCount(){
|
||||||
|
let momentChartStart = moment(this.chartStart)
|
||||||
|
let momentChartEnd = moment(this.chartEnd)
|
||||||
|
return Math.floor(momentChartEnd.diff(momentChartStart, "hour", true))
|
||||||
|
},
|
||||||
|
|
||||||
|
themeColors(){
|
||||||
|
return GanttasticThemeColors[this.theme] || GanttasticThemeColors.default
|
||||||
|
},
|
||||||
|
|
||||||
|
ganttBarChildrenList(){
|
||||||
|
let ganttBarChildren = []
|
||||||
|
let ganttRowChildrenList = this.$children.filter(childComp => childComp.$options.name === GGanttRow.name)
|
||||||
|
ganttRowChildrenList.forEach(row => {
|
||||||
|
let ganttBarChildrenOfRow = row.$children.filter(childComp => childComp.$options.name === GGanttBar.name)
|
||||||
|
ganttBarChildren.push(...ganttBarChildrenOfRow)
|
||||||
|
})
|
||||||
|
return ganttBarChildren
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
|
||||||
|
getBarsFromBundle(bundleId){
|
||||||
|
if(bundleId === undefined || bundleId === null){
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return this.ganttBarChildrenList.filter(ganttBarChild => ganttBarChild.barConfig.bundle === bundleId)
|
||||||
|
},
|
||||||
|
|
||||||
|
initDragOfBarsFromBundle(gGanttBar, e){
|
||||||
|
gGanttBar.initDrag(e)
|
||||||
|
this.movedBarsInDrag.add(gGanttBar.bar)
|
||||||
|
if(gGanttBar.barConfig.bundle !== null && gGanttBar.barConfig.bundle !== undefined){
|
||||||
|
this.ganttBarChildrenList.forEach(ganttBarChild => {
|
||||||
|
if(ganttBarChild.barConfig.bundle === gGanttBar.barConfig.bundle && ganttBarChild !== gGanttBar){
|
||||||
|
ganttBarChild.initDrag(e)
|
||||||
|
this.movedBarsInDrag.add(ganttBarChild)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
moveBarsFromBundleOfPushedBar(pushedBar, minuteDiff, overlapType){
|
||||||
|
this.movedBarsInDrag.add(pushedBar)
|
||||||
|
let bundleId = pushedBar.ganttBarConfig.bundle
|
||||||
|
if(bundleId === undefined || bundleId === null){
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.ganttBarChildrenList.forEach(ganttBarChild => {
|
||||||
|
if(ganttBarChild.barConfig.bundle === bundleId && ganttBarChild.bar !== pushedBar){
|
||||||
|
ganttBarChild.moveBarByMinutesAndPush(minuteDiff, overlapType)
|
||||||
|
this.movedBarsInDrag.add(ganttBarChild.bar)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
onBarEvent(e, ganttBar){
|
||||||
|
this.$emit(`${e.type}-bar`, {event: e, bar: ganttBar.bar})
|
||||||
|
},
|
||||||
|
|
||||||
|
onDragendBar(e, ganttBar){
|
||||||
|
let movedBarsInDrag = this.movedBarsInDrag
|
||||||
|
this.movedBarsInDrag = new Set()
|
||||||
|
this.$emit("dragend-bar", {event: e, bar: ganttBar.bar, movedBars: movedBarsInDrag})
|
||||||
|
},
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------
|
||||||
|
// -------- METHODS FOR SETTING THE DRAG LIMIT OF A BAR ----------------
|
||||||
|
// ------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// how far you can drag a bar depends on the position of the closest immobile bar
|
||||||
|
// note that if a bar from the same row belongs to a bundle
|
||||||
|
// other rows might need to be taken into consideration, too
|
||||||
|
setDragLimitsOfGanttBar(bar){
|
||||||
|
if(!this.pushOnOverlap){
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for(let side of ["left", "right"]){
|
||||||
|
let [totalGapDistance, bundleBarsOnPath] = this.countGapDistanceToNextImmobileBar(bar, 0, side)
|
||||||
|
for(let i=0; i< bundleBarsOnPath.length; i++){
|
||||||
|
let barFromBundle = bundleBarsOnPath[i].bar
|
||||||
|
let gapDist = bundleBarsOnPath[i].gapDistance
|
||||||
|
let otherBarsFromBundle = this.getBarsFromBundle(barFromBundle.barConfig.bundle).filter(otherBar => otherBar !== barFromBundle)
|
||||||
|
otherBarsFromBundle.forEach(otherBar => {
|
||||||
|
let [newGapDistance, newBundleBars] = this.countGapDistanceToNextImmobileBar(otherBar, gapDist, side)
|
||||||
|
if(newGapDistance !== null && (newGapDistance < totalGapDistance || !totalGapDistance)){
|
||||||
|
totalGapDistance = newGapDistance
|
||||||
|
}
|
||||||
|
newBundleBars.forEach(newBundleBar => {
|
||||||
|
if(!bundleBarsOnPath.find(barAndGap => barAndGap.bar === newBundleBar.bar)){
|
||||||
|
bundleBarsOnPath.push(newBundleBar)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if(totalGapDistance && side === "left"){
|
||||||
|
bar.dragLimitLeft = bar.$refs['g-gantt-bar'].offsetLeft - totalGapDistance
|
||||||
|
} else if(totalGapDistance && side === "right"){
|
||||||
|
bar.dragLimitRight = bar.$refs['g-gantt-bar'].offsetLeft+ bar.$refs['g-gantt-bar'].offsetWidth + totalGapDistance
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// all bars from the bundle of the clicked bar need to have the same drag limit:
|
||||||
|
let barsFromBundleOfClickedBar = this.getBarsFromBundle(bar.barConfig.bundle)
|
||||||
|
barsFromBundleOfClickedBar.forEach(barFromBundle => {
|
||||||
|
barFromBundle.dragLimitLeft = bar.dragLimitLeft
|
||||||
|
barFromBundle.dragLimitRight = bar.dragLimitRight
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
// returns the gap distance to the next immobile bar
|
||||||
|
// in the row where the given bar (parameter) is (added to gapDistanceSoFar)
|
||||||
|
// and a list of all bars on that path that belong to a bundle
|
||||||
|
countGapDistanceToNextImmobileBar(bar, gapDistanceSoFar, side="left"){
|
||||||
|
let bundleBarsAndGapDist = bar.barConfig.bundle ? [{bar, gapDistance: gapDistanceSoFar}] : []
|
||||||
|
let currentBar = bar
|
||||||
|
let nextBar = this.getNextGanttBar(currentBar, side)
|
||||||
|
// left side:
|
||||||
|
if(side === "left"){
|
||||||
|
while(nextBar){
|
||||||
|
let nextBarOffsetRight = nextBar.$refs['g-gantt-bar'].offsetLeft + nextBar.$refs['g-gantt-bar'].offsetWidth
|
||||||
|
gapDistanceSoFar += currentBar.$refs['g-gantt-bar'].offsetLeft - nextBarOffsetRight
|
||||||
|
if(nextBar.barConfig.immobile){
|
||||||
|
return [gapDistanceSoFar, bundleBarsAndGapDist]
|
||||||
|
} else if(nextBar.barConfig.bundle){
|
||||||
|
bundleBarsAndGapDist.push({bar: nextBar, gapDistance: gapDistanceSoFar})
|
||||||
|
}
|
||||||
|
currentBar = nextBar
|
||||||
|
nextBar = this.getNextGanttBar(nextBar, "left")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(side === "right"){
|
||||||
|
while(nextBar){
|
||||||
|
let currentBarOffsetRight = currentBar.$refs['g-gantt-bar'].offsetLeft + currentBar.$refs['g-gantt-bar'].offsetWidth
|
||||||
|
gapDistanceSoFar += nextBar.$refs['g-gantt-bar'].offsetLeft - currentBarOffsetRight
|
||||||
|
if(nextBar.barConfig.immobile){
|
||||||
|
return [gapDistanceSoFar, bundleBarsAndGapDist]
|
||||||
|
} else if(nextBar.barConfig.bundle){
|
||||||
|
bundleBarsAndGapDist.push({bar: nextBar, gapDistance: gapDistanceSoFar})
|
||||||
|
}
|
||||||
|
currentBar = nextBar
|
||||||
|
nextBar = this.getNextGanttBar(nextBar, "right")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [null, bundleBarsAndGapDist]
|
||||||
|
},
|
||||||
|
|
||||||
|
getNextGanttBar(bar, side="left"){
|
||||||
|
let allBarsLeftOrRight = []
|
||||||
|
if(side === "left"){
|
||||||
|
allBarsLeftOrRight = bar.$parent.$children.filter(gBar => {
|
||||||
|
return gBar.$parent === bar.$parent && gBar.$refs['g-gantt-bar'].offsetLeft < bar.$refs['g-gantt-bar'].offsetLeft
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
allBarsLeftOrRight = bar.$parent.$children.filter(gBar => {
|
||||||
|
return gBar.$parent === bar.$parent && gBar.$refs['g-gantt-bar'].offsetLeft > bar.$refs['g-gantt-bar'].offsetLeft
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if(allBarsLeftOrRight.length > 0){
|
||||||
|
return allBarsLeftOrRight.reduce(
|
||||||
|
(bar1, bar2) => {
|
||||||
|
let bar1Dist = Math.abs(bar1.$refs['g-gantt-bar'].offsetLeft - bar.$refs['g-gantt-bar'].offsetLeft)
|
||||||
|
let bar2Dist = Math.abs(bar2.$refs['g-gantt-bar'].offsetLeft - bar.$refs['g-gantt-bar'].offsetLeft)
|
||||||
|
return bar1Dist < bar2Dist ? bar1 : bar2
|
||||||
|
},
|
||||||
|
allBarsLeftOrRight[0]
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------
|
||||||
|
// ------------------------------------------------------------------------
|
||||||
|
// ------------------------------------------------------------------------
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
// all child components of GGanttChart may have access to
|
||||||
|
// the following values by using Vue's "inject" option:
|
||||||
|
provide(){
|
||||||
|
return {
|
||||||
|
getChartStart: () => this.chartStart,
|
||||||
|
getChartEnd: () => this.chartEnd,
|
||||||
|
getHourCount: () => this.hourCount,
|
||||||
|
ganttChartProps: this.$props,
|
||||||
|
getThemeColors: () => this.themeColors,
|
||||||
|
initDragOfBarsFromBundle: (bundleId, e) => this.initDragOfBarsFromBundle(bundleId, e),
|
||||||
|
moveBarsFromBundleOfPushedBar: (bar, minuteDiff, overlapType) => this.moveBarsFromBundleOfPushedBar(bar, minuteDiff, overlapType),
|
||||||
|
setDragLimitsOfGanttBar : (ganttBar) => this.setDragLimitsOfGanttBar(ganttBar),
|
||||||
|
onBarEvent: (e, ganttBar) => this.onBarEvent(e, ganttBar),
|
||||||
|
onDragendBar: (e, ganttBar) => this.onDragendBar(e, ganttBar)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
#g-gantt-chart{
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow-x: hidden;
|
||||||
|
-webkit-touch-callout: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-khtml-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
padding-bottom: 23px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#g-gantt-chart >>> * {
|
||||||
|
font-family: Roboto, Verdana;
|
||||||
|
}
|
||||||
|
|
||||||
|
#g-gantt-rows-container{
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="g-grid-container"
|
||||||
|
:style="{
|
||||||
|
left: rowLabelWidth,
|
||||||
|
width: `${100-(this.rowLabelWidth).replace('%','')}%`
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="(hour,index) in allHours"
|
||||||
|
:key="index"
|
||||||
|
:class="{
|
||||||
|
'g-grid-line': true,
|
||||||
|
'g-grid-line-highlighted': highlightedHours.includes(hour)
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import moment from 'moment'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
|
||||||
|
name: "GGanttGrid",
|
||||||
|
|
||||||
|
props: {
|
||||||
|
chartStart: {type: String},
|
||||||
|
chartEnd: {type: String},
|
||||||
|
rowLabelWidth: String,
|
||||||
|
highlightedHours: {type: Array, default: () => []}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
allHours(){
|
||||||
|
let momentChartStart = moment(this.chartStart)
|
||||||
|
let momentChartEnd = moment(this.chartEnd)
|
||||||
|
let res = []
|
||||||
|
while(momentChartStart.isSameOrBefore(momentChartEnd)){
|
||||||
|
res.push(momentChartStart.hour())
|
||||||
|
momentChartStart.add(1,"hour")
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.g-grid-container{
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 30%; /* must correspond to width of row title */
|
||||||
|
width: 70%;
|
||||||
|
height: calc(100% - 23px);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-grid-line{
|
||||||
|
width: 1px;
|
||||||
|
height: 100%;
|
||||||
|
background: #eaeaea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-grid-line-highlighted{
|
||||||
|
background: #90CAF9;
|
||||||
|
box-shadow: 0px 0px 0px 1px #90CAF9;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="g-gantt-row"
|
||||||
|
ref="g-gantt-row"
|
||||||
|
:style="{height: `${$parent.rowHeight}px`}"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="g-gantt-row-label"
|
||||||
|
:style="rowLabelStyle"
|
||||||
|
>
|
||||||
|
<slot name="label">
|
||||||
|
{{label}}
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="g-gantt-row-bars-container"
|
||||||
|
ref="barContainer"
|
||||||
|
:style="barsContainerStyle"
|
||||||
|
@mouseover="onMouseover()"
|
||||||
|
@mouseleave="onMouseleave()"
|
||||||
|
>
|
||||||
|
<g-gantt-bar
|
||||||
|
v-for="(bar, index) in bars"
|
||||||
|
:key="`ganttastic_bar_${index}`"
|
||||||
|
:bar="bar"
|
||||||
|
ref="ganttBar"
|
||||||
|
:bar-start="barStart"
|
||||||
|
:bar-end="barEnd"
|
||||||
|
:bar-container="barContainer"
|
||||||
|
:all-bars-in-row="bars"
|
||||||
|
>
|
||||||
|
<template #bar-label="{bar}">
|
||||||
|
<slot
|
||||||
|
name="bar-label"
|
||||||
|
:bar="bar"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</g-gantt-bar>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import GGanttBar from './GGanttBar.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
|
||||||
|
name: "GGanttRow",
|
||||||
|
|
||||||
|
components:{
|
||||||
|
GGanttBar
|
||||||
|
},
|
||||||
|
|
||||||
|
props:{
|
||||||
|
label: {type: String, default: "Row"},
|
||||||
|
bars: {type: Array, default: () => []},
|
||||||
|
barStart: {type: String, required: true}, // property name of the bar objects that represents the start datetime
|
||||||
|
barEnd: {type: String, required: true}, // property name of the bar objects that represents the end datetime,
|
||||||
|
highlightOnHover: Boolean,
|
||||||
|
},
|
||||||
|
|
||||||
|
inject: ["ganttChartProps", "getThemeColors"],
|
||||||
|
|
||||||
|
data(){
|
||||||
|
return {
|
||||||
|
barContainer: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed:{
|
||||||
|
|
||||||
|
rowLabelStyle(){
|
||||||
|
return {
|
||||||
|
width: this.ganttChartProps.rowLabelWidth,
|
||||||
|
height: this.ganttChartProps.rowHeight,
|
||||||
|
background: this.$parent.themeColors.ternary,
|
||||||
|
color: this.$parent.themeColors.text
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
barsContainerStyle(){
|
||||||
|
return{
|
||||||
|
width: `${100 - this.ganttChartProps.rowLabelWidth.replace('%','')}%`,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted(){
|
||||||
|
this.barContainer = this.$refs.barContainer.getBoundingClientRect()
|
||||||
|
window.addEventListener("resize", this.onWindowResize)
|
||||||
|
},
|
||||||
|
|
||||||
|
methods:{
|
||||||
|
|
||||||
|
onMouseover(){
|
||||||
|
if(this.highlightOnHover){
|
||||||
|
this.$refs["g-gantt-row"].style.backgroundColor = this.getThemeColors().hoverHighlight
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onMouseleave(){
|
||||||
|
this.$refs["g-gantt-row"].style.backgroundColor = null
|
||||||
|
},
|
||||||
|
|
||||||
|
onWindowResize(){
|
||||||
|
// re-initialize the barContainer DOMRect variable, which will trigger re-rendering in the gantt bars
|
||||||
|
this.barContainer = this.$refs.barContainer.getBoundingClientRect()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
watch:{
|
||||||
|
'ganttChartProps.rowLabelWidth' : function(){
|
||||||
|
this.barContainer = this.$refs.barContainer.getBoundingClientRect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.g-gantt-row{
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
height: 40px;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-gantt-row > .g-gantt-row-label{
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 20%;
|
||||||
|
background: #E8E8E8;
|
||||||
|
color: #424242;
|
||||||
|
font-size: 0.9em;
|
||||||
|
z-index: 3;
|
||||||
|
overflow: hidden;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-gantt-row > .g-gantt-row-bars-container{
|
||||||
|
position: relative;
|
||||||
|
border-top: 1px solid #eaeaea;
|
||||||
|
width: 70%;
|
||||||
|
border-bottom: 1px solid #eaeaea;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
<template>
|
||||||
|
<div id="g-timeaxis">
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="g-timeaxis-empty-space"
|
||||||
|
:style="{width: rowLabelWidth, background: themeColors.secondary}"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="g-timeaxis-days"
|
||||||
|
:style="{width: `${100-rowLabelWidth.replace('%','')}%`}"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="(day, index) in axisDays"
|
||||||
|
:key="day.text"
|
||||||
|
class="g-timeaxis-day"
|
||||||
|
:style="{
|
||||||
|
width: day.widthPercentage+'%',
|
||||||
|
background: index%2===0 ? themeColors.primary : themeColors.secondary,
|
||||||
|
color: themeColors.text
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div> {{dayFormatted(day)}} </div>
|
||||||
|
<div :style="{background: themeColors.ternary, color: themeColors.text}">
|
||||||
|
<div
|
||||||
|
v-for="hour in day.ganttHours"
|
||||||
|
:key="hour.fullDatetime"
|
||||||
|
class="g-timeaxis-hour"
|
||||||
|
>
|
||||||
|
<span :style="{fontSize: hourFontSize}">{{hour.text}}</span>
|
||||||
|
<div
|
||||||
|
class="g-timeaxis-hour-pin"
|
||||||
|
:style="{background: themeColors.text}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div id="g-timeaxis-marker"/>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import moment from 'moment'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
|
||||||
|
name:"GGanttTimeaxis",
|
||||||
|
|
||||||
|
props: {
|
||||||
|
chartStart: String,
|
||||||
|
chartEnd: String,
|
||||||
|
rowLabelWidth: String,
|
||||||
|
timemarkerOffset: {type: Number, default: 0},
|
||||||
|
locale: String,
|
||||||
|
themeColors: Object
|
||||||
|
},
|
||||||
|
|
||||||
|
data(){
|
||||||
|
return {
|
||||||
|
axisDays: [],
|
||||||
|
hourCount: null,
|
||||||
|
timemarker: null,
|
||||||
|
hourFontSize: "11px",
|
||||||
|
dayFormat: "dddd, DD. MMMM"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted(){
|
||||||
|
this.timemarker = document.querySelector("#g-timeaxis-marker")
|
||||||
|
this.initAxisDaysAndHours()
|
||||||
|
this.onWindowResize()
|
||||||
|
window.addEventListener('resize', this.onWindowResize)
|
||||||
|
window.addEventListener("mousemove", (event) => this.moveTimemarker(event))
|
||||||
|
window.addEventListener("dragover", (event) => this.moveTimemarker(event))
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
|
||||||
|
initAxisDaysAndHours(){
|
||||||
|
this.axisDays = []
|
||||||
|
let start = moment(this.chartStart)
|
||||||
|
let end = moment(this.chartEnd)
|
||||||
|
this.hourCount = Math.floor(end.diff(start, "hour", true))
|
||||||
|
while(start.isBefore(end)){
|
||||||
|
let hourCountOfDay = start.format("DD.MM.YYYY")==end.format("DD.MM.YYYY") ? end.hour() : 24-start.hour()
|
||||||
|
let widthPercentage = hourCountOfDay/this.hourCount*100
|
||||||
|
let endHour = start.day()===end.day() ? end.hour()-1 : 23 // -1 because the last hour is not included e.g if chartEnd=04:00 the last interval we display is between 03 and 04
|
||||||
|
this.axisDays.push(this.getAxisDayObject(start, widthPercentage, endHour))
|
||||||
|
start.add(1,"day").hour(0)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getAxisDayObject(datetime, widthPercentage, endHour){
|
||||||
|
let datetimeMoment = moment(datetime)
|
||||||
|
let axisDayObject = {
|
||||||
|
widthPercentage : widthPercentage,
|
||||||
|
value : datetime.format("YYYY-MM-DD"),
|
||||||
|
ganttHours : []
|
||||||
|
}
|
||||||
|
let startHour = datetimeMoment.hour()
|
||||||
|
for(let i=0; i <=(endHour-startHour); i++) {
|
||||||
|
let hour ={
|
||||||
|
text: datetimeMoment.format("HH"),
|
||||||
|
fullDatetime: datetimeMoment.format("DD.MM.YYYY HH:mm")
|
||||||
|
}
|
||||||
|
axisDayObject.ganttHours.push(hour)
|
||||||
|
datetimeMoment.add(1,"hour")
|
||||||
|
}
|
||||||
|
return axisDayObject
|
||||||
|
},
|
||||||
|
|
||||||
|
moveTimemarker(event){
|
||||||
|
this.timemarker.style.left = (event.clientX - this.timemarkerOffset - this.horizontalAxisContainer.left)+"px"
|
||||||
|
},
|
||||||
|
|
||||||
|
onWindowResize(){
|
||||||
|
this.horizontalAxisContainer = document.querySelector("#g-timeaxis").getBoundingClientRect()
|
||||||
|
this.hourFontSize = Math.min(9.5, 0.75*(this.horizontalAxisContainer.width/this.hourCount))+"px"
|
||||||
|
},
|
||||||
|
|
||||||
|
dayFormatted(day){ // do not display day text if the day is smaller than 12%
|
||||||
|
return day.widthPercentage>=12 ? moment(day.value).locale(this.locale).format(this.dayFormat) : ""
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
chartStart(){
|
||||||
|
this.initAxisDaysAndHours()
|
||||||
|
},
|
||||||
|
chartEnd(){
|
||||||
|
this.initAxisDaysAndHours()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
#g-timeaxis, .g-timeaxis-days, .g-timeaxis-day, .g-timeaxis-day > div {
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#g-timeaxis {
|
||||||
|
position: sticky;
|
||||||
|
top:0;
|
||||||
|
width: 100%;
|
||||||
|
height: 8%;
|
||||||
|
min-height: 75px;
|
||||||
|
background: white;
|
||||||
|
z-index: 4;
|
||||||
|
box-shadow: 0px 1px 3px 2px rgba(50,50,50, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
#g-timeaxis > .g-timeaxis-empty-space {
|
||||||
|
width: 20%; /* this has to be as wide as .ganttRowTitle in VGanttastic.css */
|
||||||
|
height: 100%;
|
||||||
|
background: #F5F5F5;
|
||||||
|
}
|
||||||
|
|
||||||
|
#g-timeaxis > .g-timeaxis-days {
|
||||||
|
position: relative;
|
||||||
|
width: 80%;
|
||||||
|
height: 100%,
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-timeaxis-day {
|
||||||
|
height: 100%;
|
||||||
|
flex-direction: column;
|
||||||
|
background: #E0E0E0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-timeaxis-day:nth-child(odd) {
|
||||||
|
background: #E8E8E8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-timeaxis-day > div:nth-child(1) { /* day text */
|
||||||
|
height: 50%;
|
||||||
|
justify-content: space-around;
|
||||||
|
font-weight: bold;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-timeaxis-day > div:nth-child(2) { /* hours of a day */
|
||||||
|
align-items: flex-end;
|
||||||
|
height: 50%;
|
||||||
|
justify-content: space-between;
|
||||||
|
background:#F5F5F5;
|
||||||
|
padding-top:2px;
|
||||||
|
color: #212121;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-timeaxis-hour {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-direction: column;
|
||||||
|
opacity: 0.5;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-timeaxis-hour-pin {
|
||||||
|
width: 1px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#g-timeaxis-marker {
|
||||||
|
position: absolute;
|
||||||
|
top:0;
|
||||||
|
left:0;
|
||||||
|
height: 100%;
|
||||||
|
width: 3px;
|
||||||
|
background: black;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
export default {
|
||||||
|
|
||||||
|
"default": {
|
||||||
|
primary: "#eeeeee",
|
||||||
|
secondary: "#E0E0E0",
|
||||||
|
ternary: "#F5F5F5",
|
||||||
|
hoverHighlight: "rgba(204, 216, 219, 0.5)",
|
||||||
|
text: "#404040",
|
||||||
|
background: "white"
|
||||||
|
},
|
||||||
|
|
||||||
|
"creamy": {
|
||||||
|
primary:"#ffe8d9",
|
||||||
|
secondary: "#fcdcc5",
|
||||||
|
ternary:"#fff6f0",
|
||||||
|
hoverHighlight: "rgba(230, 221, 202, 0.5)",
|
||||||
|
text: "#542d05",
|
||||||
|
background: "white"
|
||||||
|
},
|
||||||
|
|
||||||
|
"crimson":{
|
||||||
|
primary:"#a82039",
|
||||||
|
secondary: "#c41238",
|
||||||
|
ternary:"#db4f56",
|
||||||
|
hoverHighlight: "rgba(196, 141, 141, 0.5)",
|
||||||
|
text: "white",
|
||||||
|
background: "white"
|
||||||
|
},
|
||||||
|
|
||||||
|
"dark": {
|
||||||
|
primary:"#404040",
|
||||||
|
secondary: "#303030",
|
||||||
|
ternary:"#353535",
|
||||||
|
hoverHighlight: "rgba(159, 160, 161, 0.5)",
|
||||||
|
text: "white",
|
||||||
|
background: "#525252",
|
||||||
|
toast: "#1f1f1f"
|
||||||
|
},
|
||||||
|
|
||||||
|
"flare": {
|
||||||
|
primary:"#e08a38",
|
||||||
|
secondary: "#e67912",
|
||||||
|
ternary:"#5e5145",
|
||||||
|
hoverHighlight: "rgba(196, 141, 141, 0.5)",
|
||||||
|
text: "white",
|
||||||
|
background: "white"
|
||||||
|
},
|
||||||
|
|
||||||
|
"fuchsia": {
|
||||||
|
primary:"#de1d5a",
|
||||||
|
secondary: "#b50b41",
|
||||||
|
ternary:"#ff7da6",
|
||||||
|
hoverHighlight: "rgba(196, 141, 141, 0.5)",
|
||||||
|
text: "white",
|
||||||
|
background: "white"
|
||||||
|
},
|
||||||
|
|
||||||
|
"grove": {
|
||||||
|
primary:"#3d9960",
|
||||||
|
secondary: "#288542",
|
||||||
|
ternary:"#72b585",
|
||||||
|
hoverHighlight: "rgba(160, 219, 171, 0.5)",
|
||||||
|
text: "white",
|
||||||
|
background: "white"
|
||||||
|
},
|
||||||
|
|
||||||
|
"material-blue": {
|
||||||
|
primary:"#0D47A1",
|
||||||
|
secondary: "#1565C0",
|
||||||
|
ternary:"#42a5f5",
|
||||||
|
hoverHighlight: "rgba(110, 165, 196, 0.5)",
|
||||||
|
text: "white",
|
||||||
|
background: "white"
|
||||||
|
},
|
||||||
|
|
||||||
|
"sky": {
|
||||||
|
primary:"#b5e3ff",
|
||||||
|
secondary: "#a1d6f7",
|
||||||
|
ternary:"#d6f7ff",
|
||||||
|
hoverHighlight: "rgba(193, 202, 214, 0.5)",
|
||||||
|
text: "#022c47",
|
||||||
|
background: "white"
|
||||||
|
},
|
||||||
|
|
||||||
|
"slumber":{
|
||||||
|
primary:"#2c2e36",
|
||||||
|
secondary: "#2f3447",
|
||||||
|
ternary:"#35394d",
|
||||||
|
hoverHighlight: "rgba(179, 162, 127, 0.5)",
|
||||||
|
text: "#ffe0b3",
|
||||||
|
background: "#38383b",
|
||||||
|
toast:"#1f1f1f"
|
||||||
|
},
|
||||||
|
|
||||||
|
"vue": {
|
||||||
|
primary:"#258a5d",
|
||||||
|
secondary: "#41B883",
|
||||||
|
ternary:"#35495E",
|
||||||
|
hoverHighlight: "rgba(160, 219, 171, 0.5)",
|
||||||
|
text: "white",
|
||||||
|
background: "white"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import GGanttChart from "./GGanttChart.vue"
|
||||||
|
import GGanttRow from "./GGanttRow.vue"
|
||||||
|
|
||||||
|
|
||||||
|
export {GGanttChart, GGanttRow}
|
||||||
Reference in New Issue
Block a user