Commit 9f14c312 by 杨子

Initial commit

parent 579292e5
src/*
\ No newline at end of file
......@@ -9,6 +9,7 @@
"version": "0.1.0",
"dependencies": {
"core-js": "^3.8.3",
"dayjs": "^1.11.1",
"register-service-worker": "^1.7.2",
"vue": "^3.2.13",
"vue-class-component": "^8.0.0-0",
......@@ -3398,6 +3399,26 @@
}
}
},
"node_modules/@vue/cli-service/node_modules/@vue/vue-loader-v15": {
"name": "vue-loader",
"version": "15.9.8",
"resolved": "https://registry.npmmirror.com/vue-loader/-/vue-loader-15.9.8.tgz",
"integrity": "sha512-GwSkxPrihfLR69/dSV3+5CdMQ0D+jXg8Ma1S4nQXKJAznYFX14vHdc/NetQc34Dw+rBbIJyP7JOuVb9Fhprvog==",
"dev": true,
"dependencies": {
"@vue/component-compiler-utils": "^3.1.0",
"hash-sum": "^1.0.2",
"loader-utils": "^1.1.0",
"vue-hot-reload-api": "^2.3.0",
"vue-style-loader": "^4.1.0"
}
},
"node_modules/@vue/cli-service/node_modules/@vue/vue-loader-v15/node_modules/hash-sum": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/hash-sum/-/hash-sum-1.0.2.tgz",
"integrity": "sha512-fUs4B4L+mlt8/XAtSOGMUO1TXmAelItBPtJG7CyHJfYTdDjwisntGO2JQz7oUsatOY9o68+57eziUVNw/mRHmA==",
"dev": true
},
"node_modules/@vue/cli-shared-utils": {
"version": "5.0.4",
"resolved": "https://registry.npmmirror.com/@vue/cli-shared-utils/-/cli-shared-utils-5.0.4.tgz",
......@@ -3746,38 +3767,6 @@
"vue": "^3.0.1"
}
},
"node_modules/@vue/vue-loader-v15": {
"name": "vue-loader",
"version": "15.9.8",
"resolved": "https://registry.npmmirror.com/vue-loader/-/vue-loader-15.9.8.tgz",
"integrity": "sha512-GwSkxPrihfLR69/dSV3+5CdMQ0D+jXg8Ma1S4nQXKJAznYFX14vHdc/NetQc34Dw+rBbIJyP7JOuVb9Fhprvog==",
"dev": true,
"dependencies": {
"@vue/component-compiler-utils": "^3.1.0",
"hash-sum": "^1.0.2",
"loader-utils": "^1.1.0",
"vue-hot-reload-api": "^2.3.0",
"vue-style-loader": "^4.1.0"
},
"peerDependencies": {
"css-loader": "*",
"webpack": "^3.0.0 || ^4.1.0 || ^5.0.0-0"
},
"peerDependenciesMeta": {
"cache-loader": {
"optional": true
},
"vue-template-compiler": {
"optional": true
}
}
},
"node_modules/@vue/vue-loader-v15/node_modules/hash-sum": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/hash-sum/-/hash-sum-1.0.2.tgz",
"integrity": "sha512-fUs4B4L+mlt8/XAtSOGMUO1TXmAelItBPtJG7CyHJfYTdDjwisntGO2JQz7oUsatOY9o68+57eziUVNw/mRHmA==",
"dev": true
},
"node_modules/@vue/web-component-wrapper": {
"version": "1.3.0",
"resolved": "https://registry.npmmirror.com/@vue/web-component-wrapper/-/web-component-wrapper-1.3.0.tgz",
......@@ -6887,6 +6876,11 @@
"node": ">=12"
}
},
"node_modules/dayjs": {
"version": "1.11.1",
"resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.1.tgz",
"integrity": "sha512-ER7EjqVAMkRRsxNCC5YqJ9d9VQYuWdGt7aiH2qA5R5wt8ZmWaP2dLUSIK6y/kVzLMlmh1Tvu5xUf4M/wdGJ5KA=="
},
"node_modules/debug": {
"version": "4.3.4",
"resolved": "https://registry.npmmirror.com/debug/-/debug-4.3.4.tgz",
......@@ -21190,6 +21184,29 @@
"webpack-merge": "^5.7.3",
"webpack-virtual-modules": "^0.4.2",
"whatwg-fetch": "^3.6.2"
},
"dependencies": {
"@vue/vue-loader-v15": {
"version": "npm:vue-loader@15.9.8",
"resolved": "https://registry.npmmirror.com/vue-loader/-/vue-loader-15.9.8.tgz",
"integrity": "sha512-GwSkxPrihfLR69/dSV3+5CdMQ0D+jXg8Ma1S4nQXKJAznYFX14vHdc/NetQc34Dw+rBbIJyP7JOuVb9Fhprvog==",
"dev": true,
"requires": {
"@vue/component-compiler-utils": "^3.1.0",
"hash-sum": "^1.0.2",
"loader-utils": "^1.1.0",
"vue-hot-reload-api": "^2.3.0",
"vue-style-loader": "^4.1.0"
},
"dependencies": {
"hash-sum": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/hash-sum/-/hash-sum-1.0.2.tgz",
"integrity": "sha512-fUs4B4L+mlt8/XAtSOGMUO1TXmAelItBPtJG7CyHJfYTdDjwisntGO2JQz7oUsatOY9o68+57eziUVNw/mRHmA==",
"dev": true
}
}
}
}
},
"@vue/cli-shared-utils": {
......@@ -21490,27 +21507,6 @@
"integrity": "sha512-wIJR4e/jISBKVKfiod3DV32BlDsoD744WVCuCaGtaSKvhvTL9gI5vl2AYSy00V51YaM8dCOFi3zcpCON8G1WqA==",
"dev": true
},
"@vue/vue-loader-v15": {
"version": "npm:vue-loader@15.9.8",
"resolved": "https://registry.npmmirror.com/vue-loader/-/vue-loader-15.9.8.tgz",
"integrity": "sha512-GwSkxPrihfLR69/dSV3+5CdMQ0D+jXg8Ma1S4nQXKJAznYFX14vHdc/NetQc34Dw+rBbIJyP7JOuVb9Fhprvog==",
"dev": true,
"requires": {
"@vue/component-compiler-utils": "^3.1.0",
"hash-sum": "^1.0.2",
"loader-utils": "^1.1.0",
"vue-hot-reload-api": "^2.3.0",
"vue-style-loader": "^4.1.0"
},
"dependencies": {
"hash-sum": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/hash-sum/-/hash-sum-1.0.2.tgz",
"integrity": "sha512-fUs4B4L+mlt8/XAtSOGMUO1TXmAelItBPtJG7CyHJfYTdDjwisntGO2JQz7oUsatOY9o68+57eziUVNw/mRHmA==",
"dev": true
}
}
},
"@vue/web-component-wrapper": {
"version": "1.3.0",
"resolved": "https://registry.npmmirror.com/@vue/web-component-wrapper/-/web-component-wrapper-1.3.0.tgz",
......@@ -24041,6 +24037,11 @@
}
}
},
"dayjs": {
"version": "1.11.1",
"resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.1.tgz",
"integrity": "sha512-ER7EjqVAMkRRsxNCC5YqJ9d9VQYuWdGt7aiH2qA5R5wt8ZmWaP2dLUSIK6y/kVzLMlmh1Tvu5xUf4M/wdGJ5KA=="
},
"debug": {
"version": "4.3.4",
"resolved": "https://registry.npmmirror.com/debug/-/debug-4.3.4.tgz",
......@@ -11,6 +11,7 @@
},
"dependencies": {
"core-js": "^3.8.3",
"dayjs": "^1.11.1",
"register-service-worker": "^1.7.2",
"vue": "^3.2.13",
"vue-class-component": "^8.0.0-0",
......
export type ColorScheme = {
primary: string,
secondary: string,
ternary: string,
quartenary: string,
hoverHighlight: string,
text: string,
background: string,
toast?: string
}
export const colorSchemes: Record<string, ColorScheme> = {
default: {
primary: "#eeeeee",
secondary: "#E0E0E0",
ternary: "#F5F5F5",
quartenary: "#ededed",
hoverHighlight: "rgba(204, 216, 219, 0.5)",
text: "#404040",
background: "white"
},
creamy: {
primary: "#ffe8d9",
secondary: "#fcdcc5",
ternary: "#fff6f0",
quartenary: "#f7ece6",
hoverHighlight: "rgba(230, 221, 202, 0.5)",
text: "#542d05",
background: "white"
},
crimson: {
primary: "#a82039",
secondary: "#c41238",
ternary: "#db4f56",
quartenary: "#ce5f64",
hoverHighlight: "rgba(196, 141, 141, 0.5)",
text: "white",
background: "white"
},
dark: {
primary: "#404040",
secondary: "#303030",
ternary: "#353535",
quartenary: "#383838",
hoverHighlight: "rgba(159, 160, 161, 0.5)",
text: "white",
background: "#525252",
toast: "#1f1f1f"
},
flare: {
primary: "#e08a38",
secondary: "#e67912",
ternary: "#5e5145",
quartenary: "#665648",
hoverHighlight: "rgba(196, 141, 141, 0.5)",
text: "white",
background: "white"
},
fuchsia: {
primary: "#de1d5a",
secondary: "#b50b41",
ternary: "#ff7da6",
quartenary: "#f2799f",
hoverHighlight: "rgba(196, 141, 141, 0.5)",
text: "white",
background: "white"
},
grove: {
primary: "#3d9960",
secondary: "#288542",
ternary: "#72b585",
quartenary: "#65a577",
hoverHighlight: "rgba(160, 219, 171, 0.5)",
text: "white",
background: "white"
},
"material-blue": {
primary: "#0D47A1",
secondary: "#1565C0",
ternary: "#42a5f5",
quartenary: "#409fed",
hoverHighlight: "rgba(110, 165, 196, 0.5)",
text: "white",
background: "white"
},
sky: {
primary: "#b5e3ff",
secondary: "#a1d6f7",
ternary: "#d6f7ff",
quartenary: "#d0edf4",
hoverHighlight: "rgba(193, 202, 214, 0.5)",
text: "#022c47",
background: "white"
},
slumber: {
primary: "#2a2f42",
secondary: "#2f3447",
ternary: "#35394d",
quartenary: "#2c3044",
hoverHighlight: "rgba(179, 162, 127, 0.5)",
text: "#ffe0b3",
background: "#38383b",
toast: "#1f1f1f"
},
vue: {
primary: "#258a5d",
secondary: "#41B883",
ternary: "#35495E",
quartenary: "#2a3d51",
hoverHighlight: "rgba(160, 219, 171, 0.5)",
text: "white",
background: "white"
}
}
export default colorSchemes
<template>
<div
:id="bar.ganttBarConfig.id"
class="g-gantt-bar"
:style="barStyle"
@mousedown="onMouseEvent"
@mouseup="onMouseEvent"
@dblclick="onMouseEvent"
@mouseenter="onMouseEvent"
@mouseleave="onMouseEvent"
@contextmenu="onMouseEvent"
>
<div class="g-gantt-bar-label">
<slot :bar="bar">
<div>
{{ bar.ganttBarConfig.label || "" }}
</div>
</slot>
</div>
<template v-if="bar.ganttBarConfig.hasHandles">
<div class="g-gantt-bar-handle-left" />
<div class="g-gantt-bar-handle-right" />
</template>
</div>
</template>
<script setup lang="ts">
import useBarDragManagement from "../composables/useBarDragManagement"
import useTimePositionMapping from "../composables/useTimePositionMapping"
import useBarDragLimit from "../composables/useBarDragLimit"
import { GanttBarObject } from "../models/models"
import { computed, ref, toRefs, inject, watch, nextTick } from "vue"
import INJECTION_KEYS from "../models/symbols"
const props = defineProps<{
bar: GanttBarObject
}>()
const getRowsInChart = inject(INJECTION_KEYS.getChartRowsKey)
const gGanttChartPropsRefs = inject(INJECTION_KEYS.gGanttChartPropsKey)
const emitBarEvent = inject(INJECTION_KEYS.emitBarEventKey)
if (!getRowsInChart || !gGanttChartPropsRefs || !emitBarEvent) {
throw Error("GGanttBar: Failed to inject values from GGanttChart!")
}
const { bar } = toRefs(props)
const { rowHeight } = gGanttChartPropsRefs
const { mapTimeToPosition, mapPositionToTime } = useTimePositionMapping(gGanttChartPropsRefs)
const { initDragOfBar, initDragOfBundle } = useBarDragManagement(getRowsInChart, gGanttChartPropsRefs, emitBarEvent)
const { setDragLimitsOfGanttBar } = useBarDragLimit(getRowsInChart, gGanttChartPropsRefs)
const isDragging = ref(false)
const prepareForDrag = () => {
setDragLimitsOfGanttBar(bar.value)
if (!bar.value.ganttBarConfig.immobile) {
const firstMousemoveCallback = (e: MouseEvent) => {
bar.value.ganttBarConfig.bundle != null ? initDragOfBundle(bar.value, e) : initDragOfBar(bar.value, e)
isDragging.value = true
}
window.addEventListener("mousemove", firstMousemoveCallback, { once: true }) // on first mousemove event
window.addEventListener("mouseup",
() => { // in case user does not move the mouse after mousedown at all
window.removeEventListener("mousemove", firstMousemoveCallback)
isDragging.value = false
},
{ once: true }
)
}
}
const onMouseEvent = (e: MouseEvent) => {
e.preventDefault()
if (e.type === "mousedown") {
prepareForDrag()
}
const barElement = document.getElementById(bar.value.ganttBarConfig.id)
const barContainer = barElement?.closest(".g-gantt-row-bars-container")?.getBoundingClientRect()
let datetime
if (barContainer) {
datetime = mapPositionToTime(e.clientX - barContainer.left)
}
emitBarEvent(e, bar.value, datetime)
}
const { barStart, barEnd, width, chartStart, chartEnd } = gGanttChartPropsRefs
const xStart = ref(0)
const xEnd = ref(0)
watch([bar, width, chartStart, chartEnd], () => {
nextTick(() => {
xStart.value = mapTimeToPosition(bar.value[barStart.value])
xEnd.value = mapTimeToPosition(bar.value[barEnd.value])
})
}, { deep: true, immediate: true })
window.addEventListener("resize", () => {
xStart.value = mapTimeToPosition(bar.value[barStart.value])
xEnd.value = mapTimeToPosition(bar.value[barEnd.value])
})
const barStyle = computed(() => {
return {
...bar.value.ganttBarConfig.style,
position: "absolute",
top: `${rowHeight.value * 0.1}px`,
left: `${xStart.value}px`,
width: `${xEnd.value - xStart.value}px`,
height: `${rowHeight.value * 0.8}px`,
zIndex: isDragging.value ? 3 : 2
}
})
</script>
<style scoped>
.g-gantt-bar {
display: flex;
justify-content: center;
align-items: center;
background: cadetblue;
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-handle-left, .g-gantt-bar-handle-right {
position: absolute;
width: 10px;
height: 100%;
background: white;
opacity: 0.7;
border-radius: 0px;
cursor: w-resize;
top: 0;
}
.g-gantt-bar-handle-left {
left: 0;
}
.g-gantt-bar-handle-right {
right: 0;
}
.g-gantt-bar-label img {
pointer-events: none;
}
</style>
<template>
<teleport to="body">
<transition
name="fade"
mode="out-in"
>
<div
v-if="modelValue"
class="g-gantt-tooltip"
:style="{
top: tooltipTop,
left: tooltipLeft,
fontFamily: font
}"
>
<div
class="gantt-bar-tooltip-color-dot"
:style="{background: dotColor}"
/>
<slot>
{{ tooltipContent }}
</slot>
</div>
</transition>
</teleport>
</template>
<script setup lang="ts">
import { GanttBarObject } from "../models/models"
import INJECTION_KEYS from "../models/symbols"
import { computed, toRefs, ref, watch, nextTick, inject } from "vue"
import useDayjsHelper from "../composables/useDayjsHelper"
const props = defineProps<{
bar?: GanttBarObject
modelValue: boolean
}>()
const { bar } = toRefs(props)
const gGanttChartPropsRefs = inject(INJECTION_KEYS.gGanttChartPropsKey)
if (!gGanttChartPropsRefs) {
throw Error("GGanttBarTooltip: Failed to inject values from GGanttChart!")
}
const tooltipTop = ref("0px")
const tooltipLeft = ref("0px")
watch(bar, () => {
nextTick(() => {
const barId = bar?.value?.ganttBarConfig.id || ""
if (barId) {
const barElement = document.getElementById(barId)
let { top, left } = barElement?.getBoundingClientRect() || { top: 0, left: 0 }
const { rowHeight } = gGanttChartPropsRefs
left = Math.max(left, 10)
tooltipTop.value = `${top + rowHeight.value - 10}px`
tooltipLeft.value = `${left}px`
}
})
}, { deep: true, immediate: true })
const dotColor = computed(() => bar?.value?.ganttBarConfig.style?.background || "cadetblue")
const tooltipFormats = {
minute: "HH:mm",
hour: "HH:mm",
day: "DD. MMM HH:mm",
month: "DD. MMMM YYYY"
}
const { toDayjs } = useDayjsHelper(gGanttChartPropsRefs)
const { precision, font } = gGanttChartPropsRefs
const tooltipContent = computed(() => {
const format = tooltipFormats[precision.value]
if (bar && bar.value) {
const barStartFormatted = toDayjs(bar.value, "start").format(format)
const barEndFormatted = toDayjs(bar.value, "end").format(format)
return `${barStartFormatted} - ${barEndFormatted}`
}
return ""
})
</script>
<style scoped>
.g-gantt-tooltip {
position: fixed;
background: black;
color: white;
z-index: 4;
font-size: 0.85em;
padding: 5px;
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 > .gantt-bar-tooltip-color-dot {
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>
<template>
<div
id="g-gantt-chart"
ref="gGanttChart"
:style="{width, background: colors.background, fontFamily: font}"
>
<g-gantt-timeaxis
v-if="!hideTimeaxis"
:chart-start="chartStart"
:chart-end="chartEnd"
:precision="precision"
:minuteInterval="minuteInterval"
:colors="colors"
>
<template #upper-timeunit="{label, value}">
<!-- expose upper-timeunit slot of g-gantt-timeaxis-->
<slot
name="upper-timeunit"
:label="label"
:value="value"
/>
</template>
<template #timeunit="{label, value}">
<!-- expose timeunit slot of g-gantt-timeaxis-->
<slot
name="timeunit"
:label="label"
:value="value"
/>
</template>
</g-gantt-timeaxis>
<g-gantt-grid
v-if="grid"
:highlighted-units="highlightedUnits"
/>
<div id="g-gantt-rows-container">
<slot /> <!-- the g-gantt-row components go here -->
</div>
<g-gantt-bar-tooltip
:model-value="showTooltip || isDragging"
:bar="tooltipBar"
>
<template #default>
<slot
name="bar-tooltip"
:bar="tooltipBar"
/>
</template>
</g-gantt-bar-tooltip>
</div>
</template>
<script setup lang="ts">
import colorSchemes from "../color-schemes"
import GGanttTimeaxis from "./GGanttTimeaxis.vue"
import GGanttGrid from "./GGanttGrid.vue"
import GGanttBarTooltip from "./GGanttBarTooltip.vue"
import INJECTION_KEYS from "../models/symbols"
import { computed, provide, ref, toRefs, useSlots } from "vue"
import { GanttBarObject } from "../models/models"
interface GGanttChartProps {
chartStart: string
chartEnd: string
precision?: "hour" | "day" | "month"
barStart: string
barEnd: string
dateFormat?: string
width?: string
hideTimeaxis?: boolean
colorScheme?: string
grid?: boolean
pushOnOverlap?: boolean
noOverlap?: boolean
rowHeight?: number
highlightedUnits?: number[]
font?: string
minuteInterval?: number
}
const props = withDefaults(defineProps<GGanttChartProps>(), {
dateFormat: "YYYY-MM-DD HH:mm",
precision: "day",
width: "100%",
hideTimeaxis: false,
colorScheme: "default",
grid: false,
pushOnOverlap: false,
noOverlap: false,
rowHeight: 40,
highlightedUnits: () => [],
font: "Helvetica",
minuteInterval: 30
})
// eslint-disable-next-line func-call-spacing
const emit = defineEmits<{
(e: "mousedown-bar", value: {bar: GanttBarObject, e: MouseEvent, datetime?: string}) : void
(e: "mouseup-bar", value: {bar: GanttBarObject, e: MouseEvent, datetime?: string}) : void
(e: "dblclick-bar", value: {bar: GanttBarObject, e: MouseEvent, datetime?: string}) : void
(e: "mouseenter-bar", value: {bar: GanttBarObject, e: MouseEvent}) : void
(e: "mouseleave-bar", value: {bar: GanttBarObject, e: MouseEvent}) : void
(e: "dragstart-bar", value: {bar: GanttBarObject, e: MouseEvent}) : void
(e: "drag-bar", value: {bar: GanttBarObject, e: MouseEvent}) : void
(e: "dragend-bar", value: {bar: GanttBarObject, e: MouseEvent, movedBars?: Map<GanttBarObject, {oldStart: string, oldEnd: string}>}) : void
(e: "contextmenu-bar", value: {bar: GanttBarObject, e: MouseEvent, datetime?: string }) : void
}>()
const { chartStart, chartEnd, precision, width, font } = toRefs(props)
const slots = useSlots()
const colors = computed(() => {
return colorSchemes[props.colorScheme] || colorSchemes.default
})
const getChartRows = () => {
const defaultSlot = slots.default?.()
const allBars: GanttBarObject[][] = []
if (defaultSlot) {
defaultSlot.forEach(child => {
if (child.props?.bars) {
const bars = child.props.bars as GanttBarObject[]
allBars.push(bars)
// if using v-for to generate rows, rows will be children of a single "fragment" v-node:
} else if (Array.isArray(child.children)) {
child.children.forEach(grandchild => {
const granchildNode = grandchild as {props?: {bars?: GanttBarObject[]}}
if (granchildNode?.props?.bars) {
const bars = granchildNode.props.bars as GanttBarObject[]
allBars.push(bars)
}
})
}
})
}
return allBars
}
const showTooltip = ref(false)
const isDragging = ref(false)
const tooltipBar = ref<GanttBarObject | undefined>(undefined)
let tooltipTimeoutId : number
const initTooltip = (bar: GanttBarObject) => {
if (tooltipTimeoutId) {
clearTimeout(tooltipTimeoutId)
}
tooltipTimeoutId = setTimeout(() => { showTooltip.value = true }, 800)
tooltipBar.value = bar
}
const clearTooltip = () => {
clearTimeout(tooltipTimeoutId)
showTooltip.value = false
}
const emitBarEvent = (
e: MouseEvent,
bar: GanttBarObject,
datetime?: string,
movedBars?: Map<GanttBarObject, {oldStart: string, oldEnd: string}>
) => {
switch (e.type) {
case "mousedown": emit("mousedown-bar", { bar, e, datetime }); break
case "mouseup": emit("mouseup-bar", { bar, e, datetime }); break
case "dblclick": emit("dblclick-bar", { bar, e, datetime }); break
case "mouseenter":
initTooltip(bar)
emit("mouseenter-bar", { bar, e })
break
case "mouseleave":
clearTooltip()
emit("mouseleave-bar", { bar, e })
break
case "dragstart":
isDragging.value = true
emit("dragstart-bar", { bar, e })
break
case "drag": emit("drag-bar", { bar, e }); break
case "dragend":
isDragging.value = false
emit("dragend-bar", { bar, e, movedBars })
break
case "contextmenu": emit("contextmenu-bar", { bar, e, datetime }); break
}
}
const gGanttChart = ref<HTMLElement | null>(null)
provide(INJECTION_KEYS.getChartRowsKey, getChartRows)
provide(INJECTION_KEYS.gGanttChartPropsKey, { ...toRefs(props), gGanttChart })
provide(INJECTION_KEYS.emitBarEventKey, emitBarEvent)
</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;
border-radius: 5px;
}
#g-gantt-rows-container{
position: relative;
}
</style>
<template>
<div class="g-grid-container">
<div
v-for="{label, value, width} in timeaxisUnits.lowerUnits"
:key="label"
class="g-grid-line"
:style="{
width,
background: highlightedUnits.includes(Number(value)) ? colors.hoverHighlight : null
}"
/>
</div>
</template>
<script setup lang="ts">
import useColorScheme from "../composables/useColorScheme"
import useTimeaxisUnits from "../composables/useTimeaxisUnits"
import { inject, toRefs } from "vue"
import INJECTION_KEYS from "../models/symbols"
const props = defineProps<{
highlightedUnits?: number[]
}>()
const { highlightedUnits } = toRefs(props)
const gGanttChartPropsRefs = inject(INJECTION_KEYS.gGanttChartPropsKey)
if (!gGanttChartPropsRefs) {
throw new Error("GGanttBar: Provide/Inject of values from GGanttChart failed!")
}
const { colors } = useColorScheme(gGanttChartPropsRefs)
const { timeaxisUnits } = useTimeaxisUnits(gGanttChartPropsRefs)
</script>
<style scoped>
.g-grid-container {
position: absolute;
top: 0;
left: 0%;
width: 100%;
height: 100%;
display: flex;
justify-content: space-between;
}
.g-grid-line {
width: 1px;
height: 100%;
border-left: 1px solid #eaeaea;
}
</style>
<template>
<div
class="g-gantt-row"
:style="rowStyle"
@dragover="$event.preventDefault(); isHovering = true"
@dragleave="isHovering = false"
@drop="onDrop($event)"
@mouseover="isHovering = true"
@mouseleave="isHovering = false"
>
<div
class="g-gantt-row-label"
:style="{background: colors.primary, color: colors.text}"
>
<slot name="label">
{{ label }}
</slot>
</div>
<div
ref="barContainer"
class="g-gantt-row-bars-container"
v-bind="$attrs"
>
<transition-group
name="bar-transition"
tag="div"
>
<g-gantt-bar
v-for="bar in bars"
:key="bar.ganttBarConfig.id"
:bar="bar"
>
<slot
name="bar-label"
:bar="bar"
/>
</g-gantt-bar>
</transition-group>
</div>
</div>
</template>
<script setup lang="ts">
import useColorScheme from "../composables/useColorScheme"
import useTimePositionMapping from "../composables/useTimePositionMapping"
import INJECTION_KEYS from "../models/symbols"
import { inject, ref, Ref, toRefs, computed } from "vue"
import { GanttBarObject } from "../models/models"
import GGanttBar from "./GGanttBar.vue"
const props = defineProps<{
label: string
bars: GanttBarObject[]
highlightOnHover?: boolean
}>()
// eslint-disable-next-line func-call-spacing
const emit = defineEmits<{
(e: "drop", value: { e: MouseEvent, datetime: string}) : void
}>()
const gGanttChartPropsRefs = inject(INJECTION_KEYS.gGanttChartPropsKey)
if (!gGanttChartPropsRefs) {
throw Error("GGanttRow: Failed to inject GGanttChart props!")
}
const { colors } = useColorScheme(gGanttChartPropsRefs)
const { rowHeight } = gGanttChartPropsRefs
const { highlightOnHover } = toRefs(props)
const isHovering = ref(false)
const rowStyle = computed(() => {
return {
height: `${rowHeight.value}px`,
background: highlightOnHover?.value && isHovering.value ? colors.value.hoverHighlight : null
}
})
const { mapPositionToTime } = useTimePositionMapping(gGanttChartPropsRefs)
const barContainer: Ref<HTMLElement | null> = ref(null)
const onDrop = (e: MouseEvent) => {
const container = barContainer.value?.getBoundingClientRect()
if (!container) {
console.error("Vue-Ganttastic: failed to find bar container element for row.")
return
}
const xPos = e.clientX - container.left
const datetime = mapPositionToTime(xPos)
emit("drop", { e, datetime })
}
</script>
<style scoped>
.g-gantt-row {
width: 100%;
transition: background 0.4s;
position: relative;
}
.g-gantt-row > .g-gantt-row-bars-container{
position: relative;
border-top: 1px solid #eaeaea;
width: 100%;
border-bottom: 1px solid #eaeaea;
}
.g-gantt-row-label {
position: absolute;
top:0;
left: 0px;
padding: 0px 8px;
display: flex;
align-items: center;
height: 60%;
min-height: 20px;
font-size: 0.8em;
font-weight: bold;
border-bottom-right-radius: 6px;
background: #f2f2f2;
z-index: 3;
box-shadow: 0px 1px 4px 0px rgba(0,0,0,0.6);
}
.bar-transition-leave-active,
.bar-transition-enter-active {
transition: .2s;
}
.bar-transition-enter-from {
transform: scale(0);
}
.bar-transition-leave-to {
transform: scale(0);
}
</style>
<template>
<div class="g-timeaxis">
<div class="g-timeunits-container">
<div
v-for="({ label, value, width }, index) in timeaxisUnits.upperUnits"
:key="label"
class="g-upper-timeunit"
:style="{
background: index % 2 === 0 ? colors.primary : colors.secondary,
color: colors.text,
width
}"
>
<slot
name="upper-timeunit"
:label="label"
:value="value"
>
{{ label }}
</slot>
</div>
</div>
<div class="g-timeunits-container">
<div
v-for="({ label, value, width }, index) in timeaxisUnits.lowerUnits"
:key="label"
class="g-timeunit"
:style="{
background: index % 2 === 0 ? colors.ternary : colors.quartenary,
color: colors.text,
flexDirection: (precision === 'hour' || precision === 'minute' ) ? 'column' : 'row',
alignItems: (precision === 'hour' || precision === 'minute' ) ? '' : 'center',
width
}"
>
<slot
name="timeunit"
:label="label"
:value="value"
>
{{ label }}
</slot>
<div
v-if="precision === 'hour' || precision === 'minute' "
class="g-timeaxis-hour-pin"
:style="{background: colors.text}"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ColorScheme } from "../color-schemes"
import useTimeaxisUnits from "../composables/useTimeaxisUnits"
import { inject } from "vue"
import INJECTION_KEYS from "../models/symbols"
defineProps<{
chartStart: string
chartEnd: string
precision: "hour" | "day" | "month" | "minute"
colors: ColorScheme
minuteInterval: number
}>()
const gGanttChartPropsRefs = inject(INJECTION_KEYS.gGanttChartPropsKey)
if (!gGanttChartPropsRefs) {
throw new Error("GGanttBar: Provide/Inject of values from GGanttChart failed!")
}
const { precision } = gGanttChartPropsRefs
const { timeaxisUnits } = useTimeaxisUnits(gGanttChartPropsRefs)
</script>
<style scoped>
.g-timeaxis {
position: sticky;
top:0;
width: 100%;
height: 8vh;
min-height: 75px;
background: white;
z-index: 4;
box-shadow: 0px 1px 3px 2px rgba(50,50,50, 0.5);
display: flex;
flex-direction: column;
}
.g-timeunits-container {
display:flex;
width: 100%;
height: 50%;
}
.g-timeunit {
height: 100%;
font-size: 65%;
display: flex;
flex-direction: column;
justify-content: center;
}
.g-upper-timeunit {
display: flex;
height: 100%;
justify-content: center;
align-items: center;
}
.g-timeaxis-hour-pin {
width: 1px;
height: 10px;
}
#g-timeaxis-marker {
position: absolute;
top:0;
left:0;
height: 100%;
width: 3px;
background: black;
}
</style>
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<p>
For a guide and recipes on how to configure / customize this project,<br>
check out the
<a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
</p>
<h3>Installed CLI Plugins</h3>
<ul>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babel</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-pwa" target="_blank" rel="noopener">pwa</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-router" target="_blank" rel="noopener">router</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-vuex" target="_blank" rel="noopener">vuex</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint" target="_blank" rel="noopener">eslint</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-unit-mocha" target="_blank" rel="noopener">unit-mocha</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-e2e-webdriverio" target="_blank" rel="noopener">e2e-webdriverio</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-typescript" target="_blank" rel="noopener">typescript</a></li>
</ul>
<h3>Essential Links</h3>
<ul>
<li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
<li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
<li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
<li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
<li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
</ul>
<h3>Ecosystem</h3>
<ul>
<li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
<li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
<li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
<li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
<li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
</ul>
</div>
<g-gantt-chart
:chart-start="chartStart"
:chart-end="chartEnd"
precision="minute"
:minuteInterval = "minuteInterval"
:row-height="40"
grid
width="100%"
bar-start="beginDate"
bar-end="endDate"
:date-format="format"
@mousedown-bar="onMousedownBar($event.bar, $event.e, $event.datetime)"
@dblclick-bar="onMouseupBar($event.bar, $event.e, $event.datetime)"
@mouseenter-bar="onMouseenterBar($event.bar, $event.e)"
@mouseleave-bar="onMouseleaveBar($event.bar, $event.e)"
@dragstart-bar="onDragstartBar($event.bar, $event.e)"
@drag-bar="onDragBar($event.bar, $event.e)"
@dragend-bar="onDragendBar($event.bar, $event.e, $event.movedBars)"
@contextmenu-bar="onContextmenuBar($event.bar, $event.e, $event.datetime)"
>
<g-gantt-row
label="My row 1"
:bars="bars1"
highlight-on-hover
/>
<g-gantt-row
label="My row 2"
highlight-on-hover
:bars="bars2"
/>
</g-gantt-chart>
<button @click="addBar()">
Add bar
</button>
<button @click="deleteBar()">
Delete bar
</button>
选择开始日期: <input type="text" v-model="dateValueStart">
选择结束日期: <input type="text" v-model="dateValueEnd">
分钟间隔: <input type="text" v-model="minuteValue"> <button @click="confirm()">确定</button>
</template>
<script lang="ts">
import { Options, Vue } from 'vue-class-component'
<script setup lang="ts">
import { ref } from "vue"
import GGanttRow from "./GGanttRow.vue"
import GGanttChart from "./GGanttChart.vue"
import { GanttBarObject } from "../models/models"
let chartStart = ref("2022-02-01 12:00")
let chartEnd = ref("2022-02-01 18:00")
const format = ref("YYYY-MM-DD HH:mm")
const bars1 = ref([
{
beginDate: "2022-02-01 12:00",
endDate: "2022-02-01 12:30",
ganttBarConfig: {
id: "8621987329",
label: "I'm in a bundle",
bundle: "bundle2"
}
}
])
const dateValueStart = ref("");
const dateValueEnd = ref("");
const minuteInterval = ref(30);
const minuteValue = ref();
@Options({
props: {
msg: String
const bars2 = ref([
{
beginDate: "2022-02-01 12:30",
endDate: "2022-02-01 12:36",
ganttBarConfig: {
id: "9716981641",
label: "Oh hey",
style: {
background: "#69e064",
borderRadius: "15px",
color: "blue",
fontSize: "10px"
}
}
}
])
const addBar = () => {
if (bars1.value.some(bar => bar.ganttBarConfig.id === "test1")) {
return
}
const bar = {
beginDate: "2022-02-01 12:00",
endDate: "2022-02-01 13:00",
ganttBarConfig: {
id: "test1",
hasHandles: true,
label: "Hello!",
style: {
background: "#5484b7",
borderRadius: "20px"
}
}
}
})
export default class HelloWorld extends Vue {
msg!: string
bars1.value.push(bar)
}
const confirm = (type:number) =>{
if(dateValueStart.value){
chartStart.value = dateValueStart.value;
}
if(dateValueEnd.value){
chartEnd.value = dateValueEnd.value;
}
if(minuteValue.value){
minuteInterval.value = minuteValue.value;
}
}
const deleteBar = () => {
const idx = bars1.value.findIndex(b => b.ganttBarConfig.id === "test1")
if (idx !== -1) {
bars1.value.splice(idx, 1)
}
}
const onMousedownBar = (bar: GanttBarObject, e:MouseEvent, datetime?: string) => {
console.log("mousedown-bar", bar, e, datetime)
}
const onMouseupBar = (bar: GanttBarObject, e:MouseEvent, datetime?: string) => {
console.log("mouseup-bar", bar, e, datetime)
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="scss">
h3 {
margin: 40px 0 0;
const onMouseenterBar = (bar: GanttBarObject, e:MouseEvent) => {
console.log("mouseenter-bar", bar, e)
}
ul {
list-style-type: none;
padding: 0;
const onMouseleaveBar = (bar: GanttBarObject, e:MouseEvent) => {
console.log("mouseleave-bar", bar, e)
}
const onDragstartBar = (bar: GanttBarObject, e:MouseEvent) => {
console.log("dragstart-bar", bar, e)
}
const onDragBar = (bar: GanttBarObject, e:MouseEvent) => {
console.log("drag-bar", bar, e)
}
li {
display: inline-block;
margin: 0 10px;
const onDragendBar = (bar: GanttBarObject, e:MouseEvent, movedBars?: Map<GanttBarObject, {oldStart: string, oldEnd: string}>) => {
console.log("dragend-bar", bar, e, movedBars)
}
a {
color: #42b983;
const onContextmenuBar = (bar: GanttBarObject, e:MouseEvent, datetime?: string) => {
console.log("contextmenu-bar", bar, e, datetime)
}
</style>
</script>
import { GanttBarObject, GGanttChartPropsRefs } from "./../models/models"
import useDayjsHelper from "./useDayjsHelper"
import useTimePositionMapping from "./useTimePositionMapping"
import { Ref, ref } from "vue"
export default function useBarDrag (
bar: Ref<GanttBarObject>,
gGanttChartPropsRefs: GGanttChartPropsRefs,
onDrag: (e: MouseEvent, bar: GanttBarObject) => void = () => null,
onEndDrag: (e: MouseEvent, bar: GanttBarObject) => void = () => null
) {
const { barStart, barEnd, pushOnOverlap } = gGanttChartPropsRefs
const isDragging = ref(false)
let cursorOffsetX = 0
let dragCallBack : (e: MouseEvent) => void
const { mapPositionToTime } = useTimePositionMapping(gGanttChartPropsRefs)
const { toDayjs } = useDayjsHelper(gGanttChartPropsRefs)
const initDrag = (e: MouseEvent) => {
const barElement = document.getElementById(bar.value.ganttBarConfig.id)
if (barElement) {
cursorOffsetX = e.clientX - (barElement.getBoundingClientRect().left || 0)
const mousedownType = (e.target as Element).className
switch (mousedownType) {
case "g-gantt-bar-handle-left":
document.body.style.cursor = "w-resize"
dragCallBack = dragByLeftHandle
break
case "g-gantt-bar-handle-right":
document.body.style.cursor = "w-resize"
dragCallBack = dragByRightHandle
break
default: dragCallBack = drag
}
isDragging.value = true
window.addEventListener("mousemove", dragCallBack)
window.addEventListener("mouseup", endDrag)
}
}
const drag = (e: MouseEvent) => {
const barElement = document.getElementById(bar.value.ganttBarConfig.id)
const barContainer = barElement?.closest(".g-gantt-row-bars-container")?.getBoundingClientRect()
if (barElement && barContainer) {
const barWidth = barElement.getBoundingClientRect().width
const xStart = (e.clientX - barContainer.left - cursorOffsetX)
const xEnd = xStart + barWidth
if (isOutOfRange(xStart, xEnd)) {
return
}
bar.value[barStart.value] = mapPositionToTime(xStart)
bar.value[barEnd.value] = mapPositionToTime(xEnd)
onDrag(e, bar.value)
}
}
const dragByLeftHandle = (e: MouseEvent) => {
const barElement = document.getElementById(bar.value.ganttBarConfig.id)
const barContainer = barElement?.closest(".g-gantt-row-bars-container")?.getBoundingClientRect()
if (barElement && barContainer) {
const xStart = e.clientX - barContainer.left
const newBarStart = mapPositionToTime(xStart)
if (toDayjs(newBarStart).isSame(toDayjs(bar.value, "end"))) {
return
}
bar.value[barStart.value] = newBarStart
onDrag(e, bar.value)
}
}
const dragByRightHandle = (e: MouseEvent) => {
const barElement = document.getElementById(bar.value.ganttBarConfig.id)
const barContainer = barElement?.closest(".g-gantt-row-bars-container")?.getBoundingClientRect()
if (barElement && barContainer) {
const xEnd = e.clientX - barContainer.left
const newBarEnd = mapPositionToTime(xEnd)
if (toDayjs(newBarEnd).isSame(toDayjs(bar.value, "start"))) {
return
}
bar.value[barEnd.value] = newBarEnd
onDrag(e, bar.value)
}
}
const isOutOfRange = (xStart?: number, xEnd?: number) => {
if (!pushOnOverlap) {
return false
}
const dragLimitLeft = bar.value.ganttBarConfig.dragLimitLeft
const dragLimitRight = bar.value.ganttBarConfig.dragLimitRight
if (xStart && dragLimitLeft != null && xStart < dragLimitLeft) {
return true
}
if (xEnd && dragLimitRight != null && xEnd > dragLimitRight) {
return true
}
return false
}
const endDrag = (e: MouseEvent) => {
isDragging.value = false
document.body.style.cursor = "auto"
window.removeEventListener("mousemove", dragCallBack)
window.removeEventListener("mouseup", endDrag)
onEndDrag(e, bar.value)
}
return {
isDragging,
initDrag
}
}
import { GanttBarObject, GGanttChartPropsRefs } from "../models/models"
export default function useBarDragLimit (
getRowsInChart : () => GanttBarObject[][],
gGanttChartPropsRefs: GGanttChartPropsRefs
) {
const { pushOnOverlap } = gGanttChartPropsRefs
const getBarsFromBundle = (bundle?: string) => {
const res: GanttBarObject[] = []
if (bundle != null) {
getRowsInChart().forEach(row => {
row.forEach(bar => {
if (bar.ganttBarConfig.bundle === bundle) {
res.push(bar)
}
})
})
}
return res
}
const setDragLimitsOfGanttBar = (bar: GanttBarObject) => {
if (!pushOnOverlap.value || bar.ganttBarConfig.pushOnOverlap === false) {
return
}
for (const sideValue of ["left", "right"]) {
const side = sideValue as "left" | "right"
const { gapDistanceSoFar, bundleBarsAndGapDist } = countGapDistanceToNextImmobileBar(bar, 0, side)
let totalGapDistance = gapDistanceSoFar
const bundleBarsOnPath = bundleBarsAndGapDist
if (bundleBarsOnPath) {
for (let i = 0; i < bundleBarsOnPath.length; i++) {
const barFromBundle = bundleBarsOnPath[i].bar
const gapDist = bundleBarsOnPath[i].gapDistance
const otherBarsFromBundle = getBarsFromBundle(barFromBundle.ganttBarConfig.bundle).filter(otherBar => otherBar !== barFromBundle)
otherBarsFromBundle.forEach(otherBar => {
const nextGapDistanceAndBars = countGapDistanceToNextImmobileBar(otherBar, gapDist, side)
const newGapDistance = nextGapDistanceAndBars.gapDistanceSoFar
const newBundleBars = nextGapDistanceAndBars.bundleBarsAndGapDist
if (newGapDistance != null && (!totalGapDistance || newGapDistance < totalGapDistance)) {
totalGapDistance = newGapDistance
}
newBundleBars.forEach(newBundleBar => {
if (!bundleBarsOnPath.find(barAndGap => barAndGap.bar === newBundleBar.bar)) {
bundleBarsOnPath.push(newBundleBar)
}
})
})
}
const barElem = document.getElementById(bar.ganttBarConfig.id) as HTMLElement
if (totalGapDistance != null && side === "left") {
bar.ganttBarConfig.dragLimitLeft = barElem.offsetLeft - totalGapDistance
} else if (totalGapDistance != null && side === "right") {
bar.ganttBarConfig.dragLimitRight = barElem.offsetLeft + barElem.offsetWidth + totalGapDistance
}
}
}
// all bars from the bundle of the clicked bar need to have the same drag limit:
const barsFromBundleOfClickedBar = getBarsFromBundle(bar.ganttBarConfig.bundle)
barsFromBundleOfClickedBar.forEach(barFromBundle => {
barFromBundle.ganttBarConfig.dragLimitLeft = bar.ganttBarConfig.dragLimitLeft
barFromBundle.ganttBarConfig.dragLimitRight = bar.ganttBarConfig.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
const countGapDistanceToNextImmobileBar = (
bar: GanttBarObject,
gapDistanceSoFar = 0,
side: "left" | "right"
) => {
const bundleBarsAndGapDist = bar.ganttBarConfig.bundle ? [{ bar, gapDistance: gapDistanceSoFar }] : []
let currentBar = bar
let nextBar = getNextGanttBar(currentBar, side)
// left side:
if (side === "left") {
while (nextBar) {
const currentBarElem = document.getElementById(currentBar.ganttBarConfig.id) as HTMLElement
const nextBarElem = document.getElementById(nextBar.ganttBarConfig.id) as HTMLElement
const nextBarOffsetRight = nextBarElem.offsetLeft + nextBarElem.offsetWidth
gapDistanceSoFar += currentBarElem.offsetLeft - nextBarOffsetRight
if (nextBar.ganttBarConfig.immobile) {
return { gapDistanceSoFar, bundleBarsAndGapDist }
} else if (nextBar.ganttBarConfig.bundle) {
bundleBarsAndGapDist.push({ bar: nextBar, gapDistance: gapDistanceSoFar })
}
currentBar = nextBar
nextBar = getNextGanttBar(nextBar, "left")
}
}
if (side === "right") {
while (nextBar) {
const currentBarElem = document.getElementById(currentBar.ganttBarConfig.id) as HTMLElement
const nextBarElem = document.getElementById(nextBar.ganttBarConfig.id) as HTMLElement
const currentBarOffsetRight = currentBarElem.offsetLeft + currentBarElem.offsetWidth
gapDistanceSoFar += nextBarElem.offsetLeft - currentBarOffsetRight
if (nextBar.ganttBarConfig.immobile) {
return { gapDistanceSoFar, bundleBarsAndGapDist }
} else if (nextBar.ganttBarConfig.bundle) {
bundleBarsAndGapDist.push({ bar: nextBar, gapDistance: gapDistanceSoFar })
}
currentBar = nextBar
nextBar = getNextGanttBar(nextBar, "right")
}
}
return { gapDistanceSoFar: null, bundleBarsAndGapDist }
}
const getNextGanttBar = (bar: GanttBarObject, side: "left" | "right") => {
const barElem = document.getElementById(bar.ganttBarConfig.id) as HTMLElement
const allBarsInRow = getRowsInChart().find(row => row.includes(bar)) || []
let allBarsLeftOrRight = []
if (side === "left") {
allBarsLeftOrRight = allBarsInRow.filter(otherBar => {
const otherBarElem = document.getElementById(otherBar.ganttBarConfig.id) as HTMLElement
return otherBarElem && otherBarElem.offsetLeft < barElem.offsetLeft && otherBar.ganttBarConfig.pushOnOverlap !== false
})
} else {
allBarsLeftOrRight = allBarsInRow.filter(otherBar => {
const otherBarElem = document.getElementById(otherBar.ganttBarConfig.id) as HTMLElement
return otherBarElem && otherBarElem.offsetLeft > barElem.offsetLeft && otherBar.ganttBarConfig.pushOnOverlap !== false
})
}
if (allBarsLeftOrRight.length > 0) {
return allBarsLeftOrRight.reduce(
(bar1, bar2) => {
const bar1Elem = document.getElementById(bar1.ganttBarConfig.id) as HTMLElement
const bar2Elem = document.getElementById(bar2.ganttBarConfig.id) as HTMLElement
const bar1Dist = Math.abs(bar1Elem.offsetLeft - barElem.offsetLeft)
const bar2Dist = Math.abs(bar2Elem.offsetLeft - barElem.offsetLeft)
return bar1Dist < bar2Dist ? bar1 : bar2
},
allBarsLeftOrRight[0]
)
} else {
return null
}
}
return {
setDragLimitsOfGanttBar
}
}
import { GanttBarObject, GGanttChartPropsRefs } from "../models/models"
import { ref } from "vue"
import useBarDrag from "./useBarDrag"
import useDayjsHelper from "./useDayjsHelper"
export default function useBarDragManagement (
getRowsInChart : () => GanttBarObject[][],
gGanttChartPropsRefs: GGanttChartPropsRefs,
emitBarEvent: (
e: MouseEvent,
bar: GanttBarObject,
datetime?: string,
movedBars?: Map<GanttBarObject, {oldStart: string, oldEnd: string}>
) => void
) {
const movedBarsInDrag = new Map<GanttBarObject, {oldStart: string, oldEnd: string}>()
const { pushOnOverlap, barStart, barEnd, noOverlap, dateFormat } = gGanttChartPropsRefs
const { toDayjs } = useDayjsHelper(gGanttChartPropsRefs)
const initDragOfBar = (bar: GanttBarObject, e: MouseEvent) => {
const { initDrag } = useBarDrag(ref(bar), gGanttChartPropsRefs, onDrag, onEndDrag)
const ev = {
...e,
type: "dragstart"
}
emitBarEvent(ev, bar)
initDrag(e)
addBarToMovedBars(bar)
}
const initDragOfBundle = (mainBar: GanttBarObject, e: MouseEvent) => {
const bundle = mainBar.ganttBarConfig.bundle
if (bundle != null) {
getRowsInChart().forEach(row => {
row.forEach(bar => {
if (bar.ganttBarConfig.bundle === bundle) {
const dragEndHandler = bar === mainBar ? onEndDrag : () => null
const { initDrag } = useBarDrag(ref(bar), gGanttChartPropsRefs, onDrag, dragEndHandler)
initDrag(e)
addBarToMovedBars(bar)
}
})
})
const ev = {
...e,
type: "dragstart"
}
emitBarEvent(ev, mainBar)
}
}
const onDrag = (e: MouseEvent, bar: GanttBarObject) => {
const ev = {
...e,
type: "drag"
}
emitBarEvent(ev, bar)
fixOverlaps(bar)
}
const fixOverlaps = (ganttBar: GanttBarObject) => {
if (!pushOnOverlap.value) {
return
}
let currentBar = ganttBar
let { overlapBar, overlapType } = getOverlapBarAndType(currentBar)
while (overlapBar) {
addBarToMovedBars(overlapBar)
const currentBarStart = toDayjs(currentBar[barStart.value])
const currentBarEnd = toDayjs(currentBar[barEnd.value])
const overlapBarStart = toDayjs(overlapBar[barStart.value])
const overlapBarEnd = toDayjs(overlapBar[barEnd.value])
let minuteDiff : number
switch (overlapType) {
case "left":
minuteDiff = overlapBarEnd.diff(currentBarStart, "minutes", true)
overlapBar[barEnd.value] = currentBarStart.format(dateFormat.value)
overlapBar[barStart.value] = overlapBarStart.subtract(minuteDiff, "minutes").format(dateFormat.value)
break
case "right":
minuteDiff = currentBarEnd.diff(overlapBarStart, "minutes", true)
overlapBar[barStart.value] = currentBarEnd.format(dateFormat.value)
overlapBar[barEnd.value] = overlapBarEnd.add(minuteDiff, "minutes").format(dateFormat.value)
break
default:
console.warn("Vue-Ganttastic: One bar is inside of the other one! This should never occur while push-on-overlap is active!")
return
}
if (overlapBar && (overlapType === "left" || overlapType === "right")) {
moveBundleOfPushedBarByMinutes(overlapBar, minuteDiff, overlapType)
}
currentBar = overlapBar;
({ overlapBar, overlapType } = getOverlapBarAndType(overlapBar))
}
}
const getOverlapBarAndType = (ganttBar: GanttBarObject) => {
let overlapLeft, overlapRight, overlapInBetween
const allBarsInRow = getRowsInChart().find(row => row.includes(ganttBar)) || []
const ganttBarStart = toDayjs(ganttBar[barStart.value])
const ganttBarEnd = toDayjs(ganttBar[barEnd.value])
const overlapBar = allBarsInRow.find(otherBar => {
if (otherBar === ganttBar) {
return false
}
const otherBarStart = toDayjs(otherBar[barStart.value])
const otherBarEnd = toDayjs(otherBar[barEnd.value])
overlapLeft = ganttBarStart.isBetween(otherBarStart, otherBarEnd)
overlapRight = ganttBarEnd.isBetween(otherBarStart, otherBarEnd)
overlapInBetween = otherBarStart.isBetween(ganttBarStart, ganttBarEnd) ||
otherBarEnd.isBetween(ganttBarStart, ganttBarEnd)
return overlapLeft || overlapRight || overlapInBetween
})
let overlapType : "left" | "right" | "between" | null = null
overlapType = overlapLeft ? "left" : (overlapRight ? "right" : (overlapInBetween ? "between" : null))
return { overlapBar, overlapType }
}
const moveBundleOfPushedBarByMinutes = (pushedBar: GanttBarObject, minutes: number, direction: "left" | "right") => {
addBarToMovedBars(pushedBar)
if (pushedBar.ganttBarConfig.bundle) {
getRowsInChart().forEach(row => {
row.forEach(bar => {
if (bar.ganttBarConfig.bundle === pushedBar.ganttBarConfig.bundle && bar !== pushedBar) {
addBarToMovedBars(bar)
moveBarByMinutes(bar, minutes, direction)
}
})
})
}
}
const moveBarByMinutes = (bar: GanttBarObject, minutes: number, direction: "left" | "right") => {
switch (direction) {
case "left":
bar[barStart.value] = toDayjs(bar, "start").subtract(minutes, "minutes").format(dateFormat.value)
bar[barEnd.value] = toDayjs(bar, "end").subtract(minutes, "minutes").format(dateFormat.value)
break
case "right":
bar[barStart.value] = toDayjs(bar, "start").add(minutes, "minutes").format(dateFormat.value)
bar[barEnd.value] = toDayjs(bar, "end").add(minutes, "minutes").format(dateFormat.value)
}
fixOverlaps(bar)
}
const onEndDrag = (e: MouseEvent, bar: GanttBarObject) => {
snapBackAllMovedBarsIfNeeded()
const ev = {
...e,
type: "dragend"
}
emitBarEvent(ev, bar, undefined, new Map(movedBarsInDrag))
movedBarsInDrag.clear()
}
const addBarToMovedBars = (bar: GanttBarObject) => {
if (!movedBarsInDrag.has(bar)) {
const oldStart = bar[barStart.value]
const oldEnd = bar[barEnd.value]
movedBarsInDrag.set(bar, { oldStart, oldEnd })
}
}
const snapBackAllMovedBarsIfNeeded = () => {
if (!pushOnOverlap.value && noOverlap.value) {
let isAnyOverlap = false
movedBarsInDrag.forEach((_, bar) => {
const { overlapBar } = getOverlapBarAndType(bar)
if (overlapBar != null) {
isAnyOverlap = true
}
})
if (isAnyOverlap) {
movedBarsInDrag.forEach(({ oldStart, oldEnd }, bar) => {
bar[barStart.value] = oldStart
bar[barEnd.value] = oldEnd
})
}
}
}
return {
initDragOfBar,
initDragOfBundle
}
}
import { colorSchemes } from "../color-schemes"
import { computed } from "vue"
import { GGanttChartPropsRefs } from "../models/models"
export default function useColorScheme (
gGanttChartPropsRefs: GGanttChartPropsRefs
) {
const { colorScheme } = gGanttChartPropsRefs
const colors = computed(() => {
return colorSchemes[colorScheme.value] || colorSchemes.default
})
return {
colors
}
}
import { GanttBarObject, GGanttChartPropsRefs } from "./../models/models"
import dayjs from 'dayjs'
import isBetween from 'dayjs/plugin/isBetween';
dayjs.extend(isBetween)
import isSameOrAfter from 'dayjs/plugin/isSameOrAfter';
dayjs.extend(isSameOrAfter)
import { computed } from "vue"
export default function useBarDrag (
gGanttChartPropsRefs: GGanttChartPropsRefs
) {
const { chartStart, chartEnd, barStart, barEnd, dateFormat } = gGanttChartPropsRefs
const chartStartDayjs = computed(() => toDayjs(chartStart.value))
const chartEndDayjs = computed(() => toDayjs(chartEnd.value))
const toDayjs = (value: string | GanttBarObject, startOrEnd?: "start" | "end"): dayjs.Dayjs => {
if (typeof value === "string") {
return dayjs(value, dateFormat.value, true)
}
const property = startOrEnd === "start" ? value[barStart.value] : value[barEnd.value]
return dayjs(property, dateFormat.value, true)
}
return {
chartStartDayjs,
chartEndDayjs,
toDayjs
}
}
import { GGanttChartPropsRefs } from "../models/models"
import useDayjsHelper from "./useDayjsHelper"
import { computed } from "vue"
export default function useTimePositionMapping (
gGanttChartPropsRefs: GGanttChartPropsRefs
) {
const { chartStart, width, dateFormat, gGanttChart } = gGanttChartPropsRefs
const { chartStartDayjs, chartEndDayjs, toDayjs } = useDayjsHelper(gGanttChartPropsRefs)
const totalNumOfMinutes = computed(() => {
return chartEndDayjs.value.diff(chartStartDayjs.value, "minutes")
})
if (!chartStart || !width) {
throw new Error("useTimePositionMapping: Provide/Inject of values from GGanttChart failed!")
}
const mapTimeToPosition = (time: string) => {
const width = gGanttChart.value?.getBoundingClientRect().width || 0
const diffFromStart = toDayjs(time).diff(chartStartDayjs.value, "minutes", true)
return Math.ceil((diffFromStart / totalNumOfMinutes.value) * width)
}
const mapPositionToTime = (xPos: number) => {
const width = gGanttChart.value?.getBoundingClientRect().width || 0
const diffFromStart = (xPos / width * totalNumOfMinutes.value)
return chartStartDayjs.value.add(diffFromStart, "minutes").format(dateFormat.value)
}
return {
mapTimeToPosition,
mapPositionToTime
}
}
import { GGanttChartPropsRefs } from "../models/models"
import useDayjsHelper from "./useDayjsHelper"
import { computed } from "vue"
export default function useTimeaxisUnits(
gGanttChartPropsRefs: GGanttChartPropsRefs
) {
const { precision, minuteInterval } = gGanttChartPropsRefs
const { chartStartDayjs, chartEndDayjs } = useDayjsHelper(gGanttChartPropsRefs)
const upperPrecision = computed(() => {
switch (precision?.value) {
case "hour":
return "day"
case "day":
return "month"
case "month":
return "year"
case "minute":
return "day"
default:
throw new Error("Precision prop incorrect. Must be one of the following: 'hour', 'day', 'month'")
}
})
const displayFormats = {
hour: "HH",
date: "DD.MMM ",
day: "DD.MMM ",
month: "MMMM YYYY",
year: "YYYY",
minute: "HH:mm"
}
// const minuteFormat = {
// five: 5,
// fifteen: 15,
// half: 30
// }
const timeaxisUnits = computed(() => {
const upperUnits: { label: string, value?: string, width?: string }[] = []
const lowerUnits: { label: string, value?: string, width?: string }[] = []
const upperUnit = upperPrecision.value === "day" ? "date" : upperPrecision.value
const lowerUnit = precision.value
let currentUnit = chartStartDayjs.value.startOf(lowerUnit)
const totalMinutes = chartEndDayjs.value.diff(chartStartDayjs.value, "minutes", true)
let upperUnitMinutesCount = 0
let currentUpperUnitVal = currentUnit[upperUnit]()
while (currentUnit.isBefore(chartEndDayjs.value) || currentUnit.isSame(chartEndDayjs.value)) {
if (currentUnit[upperUnit]() !== currentUpperUnitVal) { // when upper unit changes:
let width = "0%"
if (upperUnits.length === 0) {
width = `${currentUnit.startOf(upperUnit).diff(chartStartDayjs.value, "minutes", true) / totalMinutes * 100}%`
} else if (currentUnit.isSameOrAfter(chartEndDayjs.value)) {
width = `${chartEndDayjs.value.diff(currentUnit.startOf(upperUnit), "minutes", true) / totalMinutes * 100}%`
} else {
const end = currentUnit.startOf(upperUnit)
const start = currentUnit.subtract(1, upperUnit).startOf(upperUnit)
width = `${end.diff(start, "minutes", true) / totalMinutes * 100}%`
}
upperUnits.push({
label: currentUnit.subtract(1, upperUnit).format(displayFormats[upperUnit]),
value: String(currentUpperUnitVal),
width
})
upperUnitMinutesCount = 0
currentUpperUnitVal = currentUnit[upperUnit]()
}
let width = "0%"
//create and push lower unit:
const addCount = precision.value == 'minute' ? minuteInterval?.value: 1;
const addCountUnit = currentUnit.add(addCount, lowerUnit);
if(precision.value == 'minute') {
width = 100/( (totalMinutes/addCount)) + "%"
if(currentUnit.isSame(chartEndDayjs.value)){
width = "0%"
}
}else{
if (lowerUnits.length === 0) {
width = `${currentUnit.endOf(lowerUnit).diff(chartStartDayjs.value, "minutes", true) / totalMinutes * 100}%`
} else if (addCountUnit.isSameOrAfter(chartEndDayjs.value)) {
width = `${chartEndDayjs.value.diff(currentUnit.startOf(lowerUnit), "minutes", true) / totalMinutes * 100}%`
} else {
width = `${currentUnit.endOf(lowerUnit).diff(currentUnit.startOf(lowerUnit), "minutes", true) / totalMinutes * 100}%`
}
}
lowerUnits.push({
label: currentUnit.format(displayFormats[lowerUnit]),
value: String(currentUnit[lowerUnit]()),
width
})
const prevUpperUnitUnit = currentUnit
currentUnit = addCountUnit
if (currentUnit.isBefore(chartEndDayjs.value) || currentUnit.isSame(chartEndDayjs.value)) {
upperUnitMinutesCount += currentUnit.diff(prevUpperUnitUnit, "minutes", true)
}
}
// for the very last upper unit :
if (!upperUnits.some(un => un.value === String(currentUpperUnitVal))) {
upperUnitMinutesCount += chartEndDayjs.value.diff(currentUnit.subtract(1, lowerUnit === 'minute' ? 'hour': lowerUnit ), "minutes", true)
upperUnits.push({
label: currentUnit.format(displayFormats[upperUnit]),
value: String(currentUpperUnitVal),
width: `${(upperUnitMinutesCount / totalMinutes) * 100}%`
})
}
return { upperUnits, lowerUnits }
})
return {
timeaxisUnits
}
}
import { Ref } from "vue"
export type GanttBarObject = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any,
ganttBarConfig: {
id: string,
label?: string
hasHandles?: boolean
immobile?: boolean
bundle?: string
pushOnOverlap?: boolean
dragLimitLeft?: number
dragLimitRight?: number
style?: CSSStyleSheet
}
}
export type GGanttChartPropsRefs = {
chartStart: Ref<string>
chartEnd: Ref<string>
precision: Ref<"hour" | "day" | "month" | "minute">
barStart: Ref<string>
barEnd: Ref<string>
rowHeight: Ref<number>
dateFormat: Ref<string>
width: Ref<string>
hideTimeaxis: Ref<boolean>
colorScheme: Ref<string>
grid: Ref<boolean>
pushOnOverlap: Ref<boolean>
noOverlap: Ref<boolean>
gGanttChart: Ref<HTMLElement | null>
font: Ref<string>
minuteInterval: Ref<number>
}
import { InjectionKey } from "vue"
import { GanttBarObject, GGanttChartPropsRefs } from "./models"
const INJECTION_KEYS = {
getChartRowsKey: Symbol("getChartRowsKey") as InjectionKey<() => GanttBarObject[][]>,
gGanttChartPropsKey: Symbol("gGanttChartPropsKey") as InjectionKey<GGanttChartPropsRefs>,
emitBarEventKey: Symbol("emitBarEventKey") as InjectionKey<
(e: MouseEvent, bar: GanttBarObject, datetime?: string) => void
>
}
export default INJECTION_KEYS
/* eslint-disable */
declare module '*.vue' {
import type { DefineComponent } from 'vue'
declare module "*.vue" {
import type { DefineComponent } from "vue"
const component: DefineComponent<{}, {}, any>
export default component
}
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true
transpileDependencies: true,
publicPath: process.env.NODE_ENV === 'production'
? '/v-gantt/'
: '/'
})
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment