2
0
mirror of https://github.com/tenrok/vue-ganttastic.git synced 2026-06-17 18:00:33 +03:00

First commit. Prepared for npm packaging.

Wrote README.
This commit is contained in:
Marko Zunic
2020-03-16 20:20:59 +01:00
commit 4639d75fbe
12 changed files with 6608 additions and 0 deletions
+21
View File
@@ -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?
+102
View File
@@ -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/)
+11
View File
@@ -0,0 +1,11 @@
module.exports = {
banner: true,
output: {
extractCSS: false,
},
plugins: {
vue: {
css: true
}
}
};
+5156
View File
File diff suppressed because it is too large Load Diff
+32
View File
@@ -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"
}
}
+449
View File
@@ -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>
+291
View File
@@ -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>
+72
View File
@@ -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>
+148
View File
@@ -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>
+218
View File
@@ -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>
+103
View File
@@ -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"
}
}
+5
View File
@@ -0,0 +1,5 @@
import GGanttChart from "./GGanttChart.vue"
import GGanttRow from "./GGanttRow.vue"
export {GGanttChart, GGanttRow}