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:
+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