Repository: InfectoOne/vue-ganttastic
Branch: master
Commit: b06e161fba42
Files: 49
Total size: 98.2 KB
Directory structure:
gitextract_6_i3sk9e/
├── .browserslistrc
├── .editorconfig
├── .eslintrc.cjs
├── .github/
│ ├── FUNDING.yml
│ └── workflows/
│ └── deploy.yml
├── .gitignore
├── .prettierrc
├── README.md
├── deploy.sh
├── docs/
│ ├── .vitepress/
│ │ ├── config.ts
│ │ └── theme/
│ │ ├── custom.css
│ │ └── index.js
│ ├── GGanttChart.md
│ ├── GGanttRow.md
│ ├── common-use-cases.md
│ ├── examples.md
│ ├── getting-started.md
│ ├── index.md
│ └── introduction.md
├── docs-deploy.yml
├── env.d.ts
├── index.html
├── package.json
├── src/
│ ├── GanttPlayground.vue
│ ├── color-schemes.ts
│ ├── components/
│ │ ├── GGanttBar.vue
│ │ ├── GGanttBarTooltip.vue
│ │ ├── GGanttChart.vue
│ │ ├── GGanttCurrentTime.vue
│ │ ├── GGanttGrid.vue
│ │ ├── GGanttLabelColumn.vue
│ │ ├── GGanttRow.vue
│ │ └── GGanttTimeaxis.vue
│ ├── composables/
│ │ ├── createBarDrag.ts
│ │ ├── useBarDragLimit.ts
│ │ ├── useBarDragManagement.ts
│ │ ├── useDayjsHelper.ts
│ │ ├── useTimePositionMapping.ts
│ │ └── useTimeaxisUnits.ts
│ ├── playground.ts
│ ├── provider/
│ │ ├── provideConfig.ts
│ │ ├── provideEmitBarEvent.ts
│ │ ├── provideGetChartRows.ts
│ │ └── symbols.ts
│ ├── types.ts
│ └── vue-ganttastic.ts
├── tsconfig.config.json
├── tsconfig.json
└── vite.config.mts
================================================
FILE CONTENTS
================================================
================================================
FILE: .browserslistrc
================================================
> 1%
last 2 versions
not dead
================================================
FILE: .editorconfig
================================================
[*.{js,jsx,ts,tsx,vue}]
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
insert_final_newline = true
max_line_length = 100
================================================
FILE: .eslintrc.cjs
================================================
/* eslint-env node */
require("@rushstack/eslint-patch/modern-module-resolution")
module.exports = {
root: true,
extends: [
"plugin:vue/vue3-recommended",
"eslint:recommended",
"@vue/eslint-config-typescript/recommended",
"@vue/eslint-config-prettier"
],
rules: {
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
quotes: ["error", "double"],
// "object-curly-spacing": ["error", "never"],
"prettier/prettier": ["error", {}, { usePrettierrc: true }]
}
}
================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
================================================
FILE: .github/workflows/deploy.yml
================================================
# Sample workflow for building and deploying a VitePress site to GitHub Pages
#
name: Deploy VitePress site to Pages
on:
# Runs on pushes targeting the `main` branch. Change this to `master` if you're
# using the `master` branch as the default branch.
push:
branches: [master]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
group: pages
cancel-in-progress: false
jobs:
# Build job
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0 # Not needed if lastUpdated is not enabled
# - uses: pnpm/action-setup@v3 # Uncomment this if you're using pnpm
# - uses: oven-sh/setup-bun@v1 # Uncomment this if you're using Bun
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm # or pnpm / yarn
- name: Setup Pages
uses: actions/configure-pages@v4
- name: Install dependencies
run: npm ci # or pnpm install / yarn install / bun install
- name: Build with VitePress
run: npm run docs:build # or pnpm docs:build / yarn docs:build / bun run docs:build
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: docs/.vitepress/dist
# Deployment job
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
needs: build
runs-on: ubuntu-latest
name: Deploy
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
================================================
FILE: .gitignore
================================================
.DS_Store
node_modules
/lib
/lib_types
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Docs dist directory
docs/.vitepress/cache
docs/.vitepress/temp
docs/.vitepress/dist
*.tgz
================================================
FILE: .prettierrc
================================================
{
"semi": false,
"trailingComma": "none",
"endOfLine": "auto"
}
================================================
FILE: README.md
================================================
# Vue Ganttastic
<div style="display: flex; flex-direction: column; align-items:center;">
<img
src="https://user-images.githubusercontent.com/28678851/148047714-301f07df-4101-48b8-9e47-1f272b290e80.png"
style="margin: 10px;" height="150"
alt="Vue Ganttastic logo"
/>
<b>Vue Ganttastic</b> is a simple, interactive and highly customizable Gantt chart component for Vue 3.

</div>
## Features
- **[Vue 3](https://v3.vuejs.org/) support**
- **[TypeScript](https://www.typescriptlang.org/) support** _(ships with out of the box type declarations)_
- **Interactivity** _(dynamic, movable and pushable bars)_
- **Reactivity / Responsiveness** (_when changes occur, bars are repositioned accordingly_)
- **Customization options** (_chart/bar styling, slots, event handlers etc._)
Using Vue 2? Check out [Vue-Ganttastic v1](https://github.com/zunnzunn/vue-ganttastic/tree/vue-ganttastic-v1).
## Guide and Docs
For further guides and references, check out the [official docs](https://zunnzunn.github.io/vue-ganttastic/getting-started.html).
## Quickstart
Install using
```
npm install @infectoone/vue-ganttastic
```
Then, initalize the plugin in the starting point of your app (most likely src/main.js):
```js
import { createApp } from "vue"
import App from "./App.vue"
...
import ganttastic from '@infectoone/vue-ganttastic'
...
createApp(App)
.use(ganttastic)
.mount('#app')
```
This will globally register the components g-gantt-chart and g-gantt-row and you will be able to use those two components right away.
```html
<template>
<g-gantt-chart
chart-start="2021-07-12 12:00"
chart-end="2021-07-14 12:00"
precision="hour"
bar-start="myBeginDate"
bar-end="myEndDate"
>
<g-gantt-row label="My row 1" :bars="row1BarList" />
<g-gantt-row label="My row 2" :bars="row2BarList" />
</g-gantt-chart>
</template>
<script setup>
import { ref } from "vue"
const row1BarList = ref([
{
myBeginDate: "2021-07-13 13:00",
myEndDate: "2021-07-13 19:00",
ganttBarConfig: {
// each bar must have a nested ganttBarConfig object ...
id: "unique-id-1", // ... and a unique "id" property
label: "Lorem ipsum dolor"
}
}
])
const row2BarList = ref([
{
myBeginDate: "2021-07-13 00:00",
myEndDate: "2021-07-14 02:00",
ganttBarConfig: {
id: "another-unique-id-2",
hasHandles: true,
label: "Hey, look at me",
style: {
// arbitrary CSS styling for your bar
background: "#e09b69",
borderRadius: "20px",
color: "black"
},
class: "foo" // you can also add CSS classes to your bars!
}
}
])
</script>
```
## Contributing
Clone the project, make some changes, test your changes out, create a pull request with a short summary of what changes you made. Contributing is warmly welcomed!
To test your changes out before creating a pull request, create a build:
```
npm run build
```
To test out the build, you should create a tarball using:
```
npm pack
```
Then, place the tarball in some other test project and install the package from the tarball by using:
```
npm install <name_of_the_package>.tgz
```
## About
**License** [MIT](https://choosealicense.com/licenses/mit/)
**Author**: Marko Žunić, BSc
[GitHub Repository](https://github.com/zunnzunn/vue-ganttastic)
## Support the project!
In case you found the library useful, a little tip would be much appreciated!
<form action="https://www.paypal.com/donate" method="post" target="_top">
<input type="hidden" name="hosted_button_id" value="M63C8DAMV5YDJ" />
<input type="image" src="https://pics.paypal.com/00/s/MTdhMWZmNTUtOWQ1Yi00YmRjLWJjMjgtY2Y0NTNhODM0OTJl/file.PNG" border="0" name="submit" title="PayPal - The safer, easier way to pay online!" alt="Donate with PayPal button" style="max-width:200px"/>
<img alt="" border="0" src="https://www.paypal.com/en_AT/i/scr/pixel.gif" width="1" height="1" />
</form>
BTC address:

## Screenshots



================================================
FILE: deploy.sh
================================================
set -e
npm run docs:build
cd docs/.vuepress/dist
git init
git add -A
git commit -m 'deploy'
git push -f git@github.com:InfectoOne/vue-ganttastic.git master:gh-pages
cd -
================================================
FILE: docs/.vitepress/config.ts
================================================
import { defineConfig } from 'vitepress'
// https://vitepress.dev/reference/site-config
export default defineConfig({
lang: 'en-US',
title: 'Vue-Ganttastic',
description: 'Simple and customizable Gantt chart component for Vue 3.',
base: '/vue-ganttastic/',
head: [['link', { rel: 'icon', href: 'https://user-images.githubusercontent.com/28678851/148047714-301f07df-4101-48b8-9e47-1f272b290e80.png' }]],
themeConfig: {
logo: 'https://user-images.githubusercontent.com/28678851/148047714-301f07df-4101-48b8-9e47-1f272b290e80.png',
nav: [
{ text: 'Home', link: '/' },
],
sidebar: [
{ text: 'Introduction', link: '/introduction'},
{ text: 'Getting Started',link: '/getting-started' },
{ text: 'Common use cases', link: '/common-use-cases' },
{ text: 'Examples', link: '/examples' },
{
text: 'API Reference',
items: [
{ text: 'GGanttChart', link: '/GGanttChart' },
{ text: 'GGanttRow', link: '/GGanttRow' }
]
}
],
socialLinks: [
{ icon: 'github', link: 'https://github.com/zunnzunn/vue-ganttastic' }
]
}
})
================================================
FILE: docs/.vitepress/theme/custom.css
================================================
:root {
--vp-home-hero-name-color: #2fb585;
--vp-button-brand-bg: #2fb585;
--vp-button-brand-hover-bg: #354b5d;
--vp-c-brand: #2fb585;
}
================================================
FILE: docs/.vitepress/theme/index.js
================================================
import DefaultTheme from 'vitepress/theme'
import './custom.css'
import {ganttastic} from "../../../src/vue-ganttastic"
export default {
extends: DefaultTheme,
enhanceApp(ctx) {
ctx.app.use(ganttastic)
}
}
================================================
FILE: docs/GGanttChart.md
================================================
# API: GGanttChart
The main component of Vue Ganttastic. Represents an entire chart and is meant to have at least one `g-gantt-row` child component.
## Props
| Prop | Type | Default | Description |
|-------------|---------|---------|------------------------------|
| `chart-start` | string | | Start date-time of the chart.
| `chart-end` | string | | End date-time of the chart.
| `precision` | string? | `"hour"` | Display precision of the time-axis. Possible values: `hour`, `day`, `date`, `week` and `month`. |
| `bar-start` | string | | Name of the property in bar objects that represents the start date.
| `bar-end` | string | | Name of the property in bar objects that represents the end date .
| `date-format` | string \| false | `"YYYY-MM-DD HH:mm"` | Datetime string format of `chart-start`, `chart-end` and the values of the `bar-start`, `bar-end` properties in bar objects. See [Day.js format tokens](https://day.js.org/docs/en/parse/string-format). If the aforementioned properties are native JavaScript [Date](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date) objects in your use case, pass `false`.
| `width` | string? | `"100%"` | Width of the chart (e.g. `80%` or `800px`)
| `hide-timeaxis` | boolean? | `false` | Toggle visibility of the time axis.
| `color-scheme` | string \| ColorScheme | `"default"` | Color scheme (theme) of the chart. Either use the name of one of the predefined schemes or pass a color-scheme-object of your own. See [color schemes](#color-schemes).
| `grid` | string? | `false` | Toggle visibility of background grid.
| `current-time` | boolean? | `false` | Toggle visibility of current time marker.
| `current-time-label` | string? | `''` | Text to be displayed next to the current time marker.
| `push-on-overlap` | boolean? | `false` | Specifies whether bars "push one another" when dragging and overlaping.
| `no-overlap` | boolean? | `false` | If `push-on-overlap` is `false`, toggle this to prevent overlaps after drag by snapping the dragged bar back to its original position.
| `row-height` | number? | `40` | Height of each row in pixels.
| `highlighted-units` | number[]? | `[]` | The time units specified here will be visually highlighted in the chart with a mild opaque color.
| `font` | string | `"Helvetica"`| Font family of the chart.
| `label-column-title` | string? | `''` | If specified, a dedicated column for the row labels will be rendered on the left side of the graph. The specified title is displayed in the upper left corner, as the column's header.
| `label-column-width` | string? | `150px` | Width of the column containing the row labels (if `label-column-title` specified)
## Custom Events
| Event name | Event data |
|----------------------------|------------------------------------------------------------|
| `click-bar` | `{bar: GanttBarObject, e: MouseEvent, datetime?: string}` |
| `mousedown-bar` | `{bar: GanttBarObject, e: MouseEvent, datetime?: string}` |
| `mouseup-bar` | `{bar: GanttBarObject, e: MouseEvent, datetime?: string}` |
| `dblclick-bar` | `{bar: GanttBarObject, e: MouseEvent, datetime?: string}` |
| `mouseenter-bar` | `{bar: GanttBarObject, e: MouseEvent}` |
| `mouseleave-bar` | `{bar: GanttBarObject, e: MouseEvent}` |
| `dragstart-bar` | `{bar: GanttBarObject, e: MouseEvent}` |
| `drag-bar` | `{bar: GanttBarObject, e: MouseEvent}` |
| `dragend-bar` | `{bar: GanttBarObject, e: MouseEvent, movedBars?: Map<GanttBarObject, {oldStart: string, oldEnd: string}>}` |
| `contextmenu-bar` | `{bar: GanttBarObject, e: MouseEvent, datetime?: string}` |
## Slots
| Slot name | Slot data | Description |
|----------------------------|-----------------------| ----------------------------------------|
| `upper-timeunit` | `{label: string, value: string}` | Content of an upper time-unit section in the time axis. |
| `timeunit` | `{label: string, value: string}` | Content of a time-unit section in the time axis. |
| `bar-tooltip` | `{bar: GanttBarObject}` | Slot for the tooltip which appears when hovering over a bar. |
| `current-time-label` | | Slot for the text shown next to the current time marker when the prop `current-time` is set to `true`. |
| `label-column-title` | | Slot for the title of the extra column to the left where the row labels are shown if the prop `label-column-title` is set. |
| `label-column-row` | `{ label: string } ` | Slot for the label of a row if `label-column-title` is set. |
## Color Schemes
List of pre-defined color schemes:
- `default`
- `creamy`
- `crimson`
- `dark`
- `flare`
- `fuchsia`
- `grove`
- `material-blue`
- `sky`
- `slumber`
- `vue`
You can also provide your own color scheme. Your custom color scheme should be an object of the following shape:
```typescript
{
primary: string,
secondary: string,
ternary: string,
quartenary: string,
hoverHighlight: string,
markerCurrentTime: string,
text: string,
background: string,
toast?: string
}
```
================================================
FILE: docs/GGanttRow.md
================================================
# API: GGanttRow
Represents a single row of the chart. It is meant to be a child component of `g-gantt-chart`.
## Props
| Prop | Type | Default | Description |
|-------------|---------|---------|------------------------------|
| `label` |`string?`| `""` | Text that is floating in the upper left corner of the row.
| `bars` |`GanttBarObject[]`| | Array of objects, each representing a bar in this row. Any JavaScript/TypeScript object with a nested `ganttBarConfig` object with a unique `id` string property is compatible. The objects must also contain two properties which represent the start and end datetime of the bar. The names of those properties must be passed to the `bar-start` and `bar-end` props of the `g-gantt-chart` parent.
| `highlight-on-hover` | `boolean?` | `false` | Used for toggling a background color transition effect on mouse hover.
## Custom Events
| Event name | Event data |
|----------------------------|------------------------------------------------------------|
| `drop` | `{ e: MouseEvent, datetime: string}` |
## Slots
| Slot name | Slot data | Description |
|----------------------------|-----------------------| ----------------------------------------|
| `label` | | Used for modifying the text that is floating in the upper left corner of the row. |
| `bar-label` | `{bar: GanttBarObject}` | Used for modifying the text in a bar. |
================================================
FILE: docs/common-use-cases.md
================================================
# Common use cases
The following section provides a non-exhausting list of common use cases and special features of Vue Ganttastic with corresponding code snippets.
## Adding new bars
For each row of the chart, you will render a `g-gantt-row` component, which accepts a `bars` prop, which is an array of bar objects. Since the prop is reactive, all you need to do to add a new bar is to push a new bar-object into that array. Just make sure that the new bar-object has a nested `ganttBarConfig` object with a unique `id`, and don't forget to specify the property values for the start and end date of the bar (the properties' names must be the ones you passed to the `bar-start` and `bar-end` props of `g-gantt-chart`):
```vue
<template>
<g-gantt-chart
chart-start="2021-07-11 12:00"
chart-end="2021-07-15 12:00"
precision="hour"
width="100%"
bar-start="myBeginDate"
bar-end="myEndDate"
>
<g-gantt-row
label="My row 1"
:bars="myBarList"
/>
</g-gantt-chart>
</template>
<script setup>
import { ref } from "vue"
const myBarList = ref([])
const addNewBar = () => {
const bar = {
myBeginDate: "2021-07-11 17:00",
myEndDate: "2021-07-12 03:00",
ganttBarConfig : {
id: "some-id-blabla" // make sure this is unique!
}
}
myBarList.push(bar)
}
</script>
```
## Configuring and styling bars
Your bar objects can be of any type and contain any properties you want. The only requirements are:
- a datetime-string property for the bar start date
- a datetime-string property for the bar end date
- a nested object `ganttBarConfig` with a unique string property `id`
For further configuration, you can add some optional properties to the nested `ganttBarConfig` object:
| Property name | Type | Description |
|---------------|---------|-----------------------|
| `id` | `string` | A unique string identifier for the bar. (**mandatory**)
| `label` | `string?` | Text displayed on the bar.
| `html` | `string?` | Optional HTML Code that will be rendered after the label (e.g. for tags). Please sanitize user input to avoid cross site scripting, if applicable.
| `hasHandles` | `boolean?` | Used to toggle handles on the left and right side of the bar that can be dragged to change the width of the bar. |
| `immobile` | `boolean?` | Used to toggle whether bar can be moved (dragged).
| `bundle` | `string?` | A string identifier for a bundle. A bundle is a collection of bars that are dragged simultaneously.
| `style` | `CSSStyleSheet?` | CSS-based styling for your bar (e.g `background`, `fontSize`, `borderRadius` etc.).
## Extending the width of a bar
Simply add `hasHandles: true` to the `ganttBarConfig` to make the bar extendable by dragging the handles on its left or right side.
## Push bars when dragging
By default, bars can overlap with other bars while being dragged. If you would like to prevent this and have the bars "push" one another while dragging, use the `push-on-overlap` prop:
```vue
<g-gantt-chart
...
push-on-overlap
...
>
...
</g-gantt-chart>
```
## Bundling bars together
If you want to bind a group of bars one to another so that when you drag one bar, all the others move together with it, specify a `bundle` string in the `ganttBarConfig` of each affected bar.
## Custom behavior on clicking/dragging a bar
It is completely up to you to specify which kind of behavior you would like e.g. when a bar is clicked on or when a bar-drag is ended. For this, you may use special events emitted by `g-gantt-chart`:
```vue
<g-gantt-chart
...
@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-chart>
<script setup lang="ts">
...
const onMousedownBar = (bar: GanttBarObject, e: MouseEvent, datetime?: string) => {
// do something
}
...
</script>
```
## Drag and drop
The `g-gantt-row` component comes with a special `drop` event, that you can use to implement custom drag-and-drop behavior. The event data also includes the `datetime` position on which the drop occured.
```vue
<g-gantt-chart
...
>
<g-gantt-row
label="This is my row"
:bars="bars1"
@drop="onDrop($event.e, $event.datetime)"
/>
</g-gantt-chart>
<script setup lang="ts">
...
const onDrop = (e: MouseEvent, datetime?: string) => {
// do something
}
...
</script>
```
## Time axis precision
If the time-range (`chart-start` to `chart-end`) of your chart is very large, the displayed time units in the time axis might be too dense if the chart is not wide enough. You might want to specify the precision of the time axis accordingly. Use the `precision` prop of `g-gantt-chart` for this. Possible values are `hour`, `day`, `week` and `month`.
## Chart themes
Vue Ganttastic ships with several pre-made color schemes that you may specify using the `color-scheme` prop of `g-gantt-chart`. [List of available color-schemes](https://infectoone.github.io/vue-ganttastic/GGanttChart.html#color-schemes)
## Locale
Since Vue Ganttastic uses Day.js for all datetime manipulations, you can change the locale of Vue Ganttastic by [changing the global locale of Day.js](https://day.js.org/docs/en/i18n/changing-locale). You will usually do this in your `src/main.js` before you initialize the Ganttastic plugin.
================================================
FILE: docs/examples.md
================================================
# Live Demos
## Simple hour chart
- `precision`: `hour`
<g-gantt-chart chart-start="01.01.2022 12:00" chart-end="02.01.2022 12:00" precision="hour" grid width="100%" bar-start="beginDate" bar-end="endDate" date-format="DD.MM.YYYY HH:mm">
<g-gantt-row label="My row 1" :bars="hourBarList1" highlight-on-hover/>
<g-gantt-row label="Another row" :bars="hourBarList2" highlight-on-hover/>
</g-gantt-chart>
<button @click="addHourBar()" :disabled="hourBarList2.length > 0"> Add bar </button>
<button @click="deleteHourBar()" style="margin-left: 10px" :disabled="hourBarList2.length === 0"> Delete bar </button>
## Day chart with dark theme
- `precision`: `day`
- `row-height` : `70`
- `no-overlap`
- `color-scheme` : `dark`
Used slots:
`g-gantt-row` > `label`, `bar-label`
<g-gantt-chart chart-start="30.10.2022 12:00" chart-end="02.11.2022 13:00" precision="day" grid width="100%" :row-height="70" bar-start="beginDate" bar-end="endDate" date-format="DD.MM.YYYY HH:mm" color-scheme="dark" no-overlap>
<g-gantt-row label="Row label" :bars="dayBarList1" highlight-on-hover>
<template #bar-label="{bar}">
<img v-if="bar.imgSrc" :src="bar.imgSrc" height="30" width="30"/>
{{bar.ganttBarConfig.label}}
</template>
</g-gantt-row>
<g-gantt-row label="My row 2" :bars="[]" highlight-on-hover>
<template #label>
<img src='https://user-images.githubusercontent.com/28678851/148047714-301f07df-4101-48b8-9e47-1f272b290e80.png' height="30" width="30" style="padding-right:10px"/>
Label with image
</template>
</g-gantt-row>
</g-gantt-chart>
## Month chart pushing and bundles
- `precision`: `month`
- `push-on-overlap`
- `color-scheme` : `vue`
- `font` : `Courier`
<g-gantt-chart chart-start="01.01.2022 12:00" chart-end="15.03.2022 03:00" precision="month" grid width="100%" bar-start="beginDate" bar-end="endDate" date-format="DD.MM.YYYY HH:mm" color-scheme="vue" font="Courier" push-on-overlap>
<g-gantt-row label="My row 1" :bars="monthBarList1" highlight-on-hover/>
<g-gantt-row label="My row 2" :bars="monthBarList2" highlight-on-hover/>
<g-gantt-row label="Look at me!" :bars="monthBarList3" highlight-on-hover/>
<g-gantt-row label="Fourth row" :bars="[]" highlight-on-hover/>
</g-gantt-chart>
<script setup>
import { ref } from "vue"
const hourBarList1 = ref([
{
beginDate: "01.01.2022 15:00",
endDate: "01.01.2022 19:45",
ganttBarConfig: {
id: "8621987329",
label: "Drag me",
style: {
color: "white"
}
}
},
{
beginDate: "01.01.2022 23:00",
endDate: "02.01.2022 08:05",
ganttBarConfig: {
id: "8621987322",
label: "Drag my handles",
hasHandles: true,
style: {
background: "#d66f2a",
color: "white"
}
}
}
])
const hourBarList2 = ref([])
const dayBarList1 = ref([
{
beginDate: "31.10.2022 15:00",
endDate: "01.11.2022 05:45",
ganttBarConfig: {
id: "a621987323",
label: "Drag me",
style: {
background: "#cc2a2d",
color: "white"
}
}
},
{
beginDate: "01.11.2022 09:00",
endDate: "02.11.2022 08:00",
imgSrc: "https://user-images.githubusercontent.com/28678851/148047714-301f07df-4101-48b8-9e47-1f272b290e80.png",
ganttBarConfig: {
id: "x21987322",
label: "I have an image",
hasHandles: true,
style: {
background: "#e2e595",
color: "black",
borderRadius: "40px"
}
}
}
])
const monthBarList1 = ref([
{
beginDate: "01.01.2022 23:00",
endDate: "02.02.2022 08:05",
ganttBarConfig: {
id: "5621987352",
label: "I'm in a bundle",
hasHandles: true,
bundle: "myBundle",
style: {
background: "#1c8745",
color: "white",
borderRadius: "20px"
}
}
}
])
const monthBarList2 = ref([
{
beginDate: "01.01.2022 23:00",
endDate: "02.02.2022 08:05",
ganttBarConfig: {
id: "8621987321",
label: "I'm in a bundle",
hasHandles: true,
bundle: "myBundle",
style: {
background: "#a02353",
color: "white",
borderRadius: "20px"
}
}
},
{
beginDate: "15.02.2022 00:00",
endDate: "01.03.2022 00:05",
ganttBarConfig: {
id: "7721987321",
label: "Lorem ispum dolor",
bundle: "bundle2",
style: {
backgroundImage: "repeating-linear-gradient(45deg, #ccc, #ccc 30px, #8221b2 30px, #8221b2 60px)",
borderRadius: "20px",
color: "black"
}
}
}
])
const monthBarList3 = ref([{
beginDate: "15.02.2022 00:00",
endDate: "01.03.2022 00:05",
ganttBarConfig: {
id: "7721987325",
label: "Lorem ispum dolor",
bundle: "bundle2",
style: {
backgroundImage: "repeating-linear-gradient(45deg, #ccc, #ccc 30px, #8221b2 30px, #8221b2 60px)",
borderRadius: "20px",
color: "black"
}
}
}])
const addHourBar = () => {
if (hourBarList2.value.some(bar => bar.ganttBarConfig.id === "test1")) {
return
}
const bar = {
beginDate: "01.01.2022 18:00",
endDate: "02.01.2022 02:00",
ganttBarConfig: {
id: "test1",
hasHandles: true,
label: "Hello!",
style: {
background: "#5484b7",
borderRadius: "20px",
color: "white"
}
}
}
hourBarList2.value.push(bar)
}
const deleteHourBar = () => {
const idx = hourBarList2.value.findIndex(b => b.ganttBarConfig.id === "test1")
if (idx !== -1) {
hourBarList2.value.splice(idx, 1)
}
}
</script>
<style scoped>
button {
padding: 10px;
background: #258A5D;
color: white;
border: none;
border-radius: 5px;
}
button:disabled {
opacity: 0.5;
}
</style>
================================================
FILE: docs/getting-started.md
================================================
# Getting started
## Install
You can add Vue Ganttastic to your project using <kbd>npm</kbd>:
```
npm install @infectoone/vue-ganttastic
```
Then, initalize the plugin in the starting point of your app (most likely `src/main.js`):
```javascript
import { createApp } from "vue"
import App from "./App.vue"
...
import ganttastic from '@infectoone/vue-ganttastic'
...
createApp(App)
.use(ganttastic)
.mount('#app')
```
This will globally register the components `g-gantt-chart` and `g-gantt-row` and you will be able to use those two components right away.
## Basic usage
```vue
<template>
<g-gantt-chart
chart-start="2021-07-12 12:00"
chart-end="2021-07-14 12:00"
precision="hour"
bar-start="myBeginDate"
bar-end="myEndDate"
>
<g-gantt-row label="My row 1" :bars="row1BarList" />
<g-gantt-row label="My row 2" :bars="row2BarList" />
</g-gantt-chart>
</template>
<script setup>
import { ref } from "vue"
const row1BarList = ref([
{
myBeginDate: "2021-07-13 13:00",
myEndDate: "2021-07-13 19:00",
ganttBarConfig: {
// each bar must have a nested ganttBarConfig object ...
id: "unique-id-1", // ... and a unique "id" property
label: "Lorem ipsum dolor"
}
}
])
const row2BarList = ref([
{
myBeginDate: "2021-07-13 00:00",
myEndDate: "2021-07-14 02:00",
ganttBarConfig: {
id: "another-unique-id-2",
hasHandles: true,
label: "Hey, look at me",
style: {
// arbitrary CSS styling for your bar
background: "#e09b69",
borderRadius: "20px",
color: "black"
}
}
}
])
</script>
```
The result shoud look like this:
<g-gantt-chart chart-start="2021-07-12 12:00" chart-end="2021-07-14 12:00" precision="hour" width="100%" bar-start="myBeginDate" bar-end="myEndDate"> <g-gantt-row label="My row 1" :bars="row1BarList"/>
<g-gantt-row label="My row 2" :bars="row2BarList"/>
</g-gantt-chart>
<script setup>
import { ref } from "vue"
const row1BarList = ref([
{
myBeginDate: "2021-07-13 13:00",
myEndDate: "2021-07-13 19:00",
ganttBarConfig: { // each bar must have a nested ganttBarConfig object ...
id: "unique-id-1", // ... and a unique "id" property
label: "Lorem ipsum dolor"
}
}
])
const row2BarList = ref([
{
myBeginDate: "2021-07-13 00:00",
myEndDate: "2021-07-14 02:00",
ganttBarConfig: {
id: "another-unique-id-2",
hasHandles: true,
label: "Hey, look at me",
style: {
// arbitrary CSS styling for your bar
background: "#e09b69",
borderRadius: "20px",
color: "#000000"
},
class: "foo" // you can also add CSS classes to your bars!
}
}
])
</script>
================================================
FILE: docs/index.md
================================================
---
# https://vitepress.dev/reference/default-theme-home-page
layout: home
hero:
name: Vue-Ganttastic
text: Gantt chart component for Vue
tagline: A simple, interactive and highly customizable Gantt chart component for Vue.js
image:
src: https://user-images.githubusercontent.com/28678851/148047714-301f07df-4101-48b8-9e47-1f272b290e80.png
alt: Vue-Ganttastic logo
actions:
- theme: brand
text: Get started
link: /introduction
- theme: alt
text: API Reference
link: /GGanttChart
features:
- title: Vue 3 and TypeScript support
details: Written in Vue 3 and TypeScript. Ships with out-of-the-box type declarations.
- title: Interactive
details: Dynamic Gantt chart with movable bars and numerous event handlers.
- title: Customizable
details: Style the chart and each individual bar to your own liking!
---
================================================
FILE: docs/introduction.md
================================================
# Introduction
Vue Ganttastic is a simple, interactive and highly customizable Gantt chart component for Vue 3.
## Features
- **[Vue 3](https://v3.vuejs.org/) support**
- **[TypeScript](https://www.typescriptlang.org/) support** *(ships with out of the box type declarations)*
- **Interactivity** *(dynamic, movable and pushable bars)*
- **Reactivity / Responsiveness** (*when changes occur, bars are repositioned accordingly*)
- **Customization options** (*chart/bar styling, slots, event handlers etc.*)
## About
**License** [MIT](https://choosealicense.com/licenses/mit/)
**Author**: Marko Žunić, BSc
[GitHub Repository](https://github.com/InfectoOne/vue-ganttastic)
## Support the project!
In case you found the library useful, a little tip would be much appreciated!
<form action="https://www.paypal.com/donate" method="post" target="_top">
<input type="hidden" name="hosted_button_id" value="M63C8DAMV5YDJ" />
<input type="image" src="https://pics.paypal.com/00/s/MTdhMWZmNTUtOWQ1Yi00YmRjLWJjMjgtY2Y0NTNhODM0OTJl/file.PNG" border="0" name="submit" title="PayPal - The safer, easier way to pay online!" alt="Donate with PayPal button" style="max-width:200px"/>
<img alt="" border="0" src="https://www.paypal.com/en_AT/i/scr/pixel.gif" width="1" height="1" />
</form>
BTC address:

================================================
FILE: docs-deploy.yml
================================================
# Sample workflow for building and deploying a VitePress site to GitHub Pages
#
name: Deploy VitePress site to Pages
on:
# Runs on pushes targeting the `main` branch. Change this to `master` if you're
# using the `master` branch as the default branch.
push:
branches: [master]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
group: pages
cancel-in-progress: false
jobs:
# Build job
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0 # Not needed if lastUpdated is not enabled
# - uses: pnpm/action-setup@v2 # Uncomment this if you're using pnpm
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: 18
cache: npm # or pnpm / yarn
- name: Setup Pages
uses: actions/configure-pages@v3
- name: Install dependencies
run: npm ci # or pnpm install / yarn install
- name: Build with VitePress
run: npm run docs:build # or pnpm docs:build / yarn docs:build
- name: Upload artifact
uses: actions/upload-pages-artifact@v2
with:
path: docs/.vitepress/dist
# Deployment job
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
needs: build
runs-on: ubuntu-latest
name: Deploy
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v2
================================================
FILE: env.d.ts
================================================
/// <reference types="vite/client" />
================================================
FILE: index.html
================================================
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>@infectoone/vue-ganttastic</title>
<link rel="icon" href="/favicon.ico">
<style>
body {
font-family: Helvetica, sans-serif;
}
</style>
</head>
<body>
<noscript>
<strong>We're sorry but the app doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<script type="module" src="/src/playground.ts"></script>
</body>
</html>
================================================
FILE: package.json
================================================
{
"name": "@infectoone/vue-ganttastic",
"version": "2.3.2",
"description": "A simple and customizable Gantt chart component for Vue.js",
"author": "Marko Zunic (@zunnzunn)",
"scripts": {
"serve": "vite",
"build": "npm run build:types && npm run build:lib",
"build:lib": "vite build",
"build:types": "vue-tsc --declaration --emitDeclarationOnly --outDir lib_types",
"typecheck": "vue-tsc --noEmit",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --ignore-path .gitignore",
"lint:fix": "npm run lint --fix",
"docs:build": "vitepress build docs",
"docs:dev": "vitepress dev docs",
"docs:preview": "vitepress preview docs"
},
"type": "module",
"exports": {
".": {
"types": "./lib_types/vue-ganttastic.d.ts",
"import": "./lib/vue-ganttastic.js",
"require": "./lib/vue-ganttastic.umd.cjs"
}
},
"main": "./lib/vue-ganttastic.umd.cjs",
"types": "./lib_types/vue-ganttastic.d.ts",
"files": [
"lib_types",
"lib/vue-ganttastic.js",
"lib/vue-ganttastic.umd.cjs"
],
"devDependencies": {
"@rushstack/eslint-patch": "^1.2.0",
"@senojs/rollup-plugin-style-inject": "^0.1.1",
"@types/node": "^16.11.58",
"@types/postcss-preset-env": "^7.7.0",
"@vitejs/plugin-vue": "^3.1.2",
"@vue/eslint-config-prettier": "^7.0.0",
"@vue/eslint-config-typescript": "^11.0.2",
"@vue/tsconfig": "^0.1.3",
"@vuepress/plugin-search": "2.0.0-beta.51",
"eslint": "^8.25.0",
"eslint-plugin-vue": "^9.6.0",
"npm-run-all": "^4.1.5",
"postcss": "^8.4.17",
"postcss-preset-env": "^7.8.2",
"prettier": "^2.7.1",
"typescript": "~4.8.4",
"vite": "^3.1.6",
"vitepress": "^1.0.0-rc.4",
"vue-tsc": "^1.8.27"
},
"peerDependencies": {
"dayjs": "^1.11.5",
"vue": "^3.2.40"
},
"homepage": "https://zunnzunn.github.io/vue-ganttastic/",
"keywords": [
"gantt",
"chart",
"bar",
"diagram",
"vue",
"vuejs",
"ganttastic"
],
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/zunnzunn/vue-ganttastic"
},
"dependencies": {
"@vueuse/core": "^9.1.1"
}
}
================================================
FILE: src/GanttPlayground.vue
================================================
<template>
<g-gantt-chart
:chart-start="chartStart"
:chart-end="chartEnd"
precision="week"
:row-height="40"
grid
current-time
width="100%"
bar-start="beginDate"
bar-end="endDate"
:date-format="format"
@click-bar="onClickBar($event.bar, $event.e, $event.datetime)"
@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 to test" :bars="bars1" highlight-on-hover />
<g-gantt-row label="My another new row to test" highlight-on-hover :bars="bars2" />
<g-gantt-row label="just another row to test gantt" highlight-on-hover :bars="bars3" />
<g-gantt-row
label="errors teach us, and debugging makes us stronger!"
highlight-on-hover
:bars="bars4"
/>
</g-gantt-chart>
<button type="button" @click="addBar()">Add bar</button>
<button type="button" @click="deleteBar()">Delete bar</button>
</template>
<script setup lang="ts">
import { ref } from "vue"
import type { GanttBarObject } from "./types"
import dayjs from "dayjs"
const format = ref("DD.MM.YYYY HH:mm")
const chartStart = ref(dayjs().startOf("day").format(format.value))
const chartEnd = ref(
dayjs(chartStart.value, format.value).add(3, "days").hour(12).format(format.value)
)
const bars1 = ref<GanttBarObject[]>([
{
beginDate: dayjs().hour(13).startOf("hour").format(format.value),
endDate: dayjs().hour(19).startOf("hour").format(format.value),
ganttBarConfig: {
id: "8621987329",
label: "I'm in a bundle",
bundle: "bundle2"
}
}
])
const bars2 = ref([
{
beginDate: dayjs().hour(13).startOf("hour").format(format.value),
endDate: dayjs().hour(19).startOf("hour").format(format.value),
ganttBarConfig: {
id: "1592311887",
label: "I'm in a bundle",
bundle: "bundle2",
style: {
background: "magenta"
}
}
},
{
beginDate: dayjs().add(2, "day").hour(0).startOf("hour").format(format.value),
endDate: dayjs().add(2, "day").hour(19).startOf("hour").format(format.value),
ganttBarConfig: {
id: "7716981641",
label: "Lorem ipsum dolor",
hasHandles: true,
style: {
background: "#b74b52"
}
}
},
{
beginDate: dayjs().add(1, "day").hour(4).startOf("hour").format(format.value),
endDate: dayjs().add(1, "day").hour(16).startOf("hour").format(format.value),
ganttBarConfig: {
id: "9716981641",
label: "Oh hey",
style: {
background: "#69e064",
borderRadius: "15px",
color: "blue",
fontSize: "10px"
}
}
}
])
const bars3 = [
{
beginDate: "15.01.2024 08:30",
endDate: "20.02.2024 16:45",
ganttBarConfig: {
id: "9876543210",
label: "Updated Bundle",
bundle: "bundle3",
style: {
background: "cyan"
}
}
},
{
beginDate: "20.02.2024 12:00",
endDate: "10.03.2024 18:30",
ganttBarConfig: {
id: "1234567890",
label: "New Task",
hasHandles: true,
style: {
background: "#f79466"
}
}
},
{
beginDate: "25.04.2024 09:15",
endDate: "30.04.2024 21:00",
ganttBarConfig: {
id: "2468135790",
label: "Greetings",
style: {
background: "#aabbcc",
borderRadius: "8px",
color: "white",
fontSize: "12px"
}
}
}
]
const bars4 = [
{
beginDate: "10.01.2024 08:00",
endDate: "15.03.2024 16:30",
ganttBarConfig: {
id: "9876543210",
label: "Novo Pacote",
bundle: "pacote3",
style: {
background: "pink"
}
}
},
{
beginDate: "05.03.2024 10:00",
endDate: "15.04.2024 22:15",
ganttBarConfig: {
id: "2468135790",
label: "hello folks",
style: {
background: "#ffd700",
borderRadius: "10px",
color: "black",
fontSize: "14px"
}
}
}
]
const addBar = () => {
if (bars1.value.some((bar) => bar.ganttBarConfig.id === "test1")) {
return
}
const bar = {
beginDate: dayjs().add(1, "day").hour(4).startOf("hour").format(format.value),
endDate: dayjs().add(2, "day").hour(4).startOf("hour").format(format.value),
ganttBarConfig: {
id: "test1",
hasHandles: true,
label: "Hello!",
style: {
background: "#5484b7",
borderRadius: "20px"
}
}
}
bars1.value.push(bar)
}
const deleteBar = () => {
const idx = bars1.value.findIndex((b) => b.ganttBarConfig.id === "test1")
if (idx !== -1) {
bars1.value.splice(idx, 1)
}
}
const onClickBar = (bar: GanttBarObject, e: MouseEvent, datetime?: string) => {
console.log("click-bar", bar, e, datetime)
}
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)
}
const onMouseenterBar = (bar: GanttBarObject, e: MouseEvent) => {
console.log("mouseenter-bar", bar, e)
}
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)
}
const onDragendBar = (
bar: GanttBarObject,
e: MouseEvent,
movedBars?: Map<GanttBarObject, { oldStart: string; oldEnd: string }>
) => {
console.log("dragend-bar", bar, e, movedBars)
}
const onContextmenuBar = (bar: GanttBarObject, e: MouseEvent, datetime?: string) => {
console.log("contextmenu-bar", bar, e, datetime)
}
</script>
================================================
FILE: src/color-schemes.ts
================================================
import type * as CSS from "csstype"
type Color = CSS.DataType.Color
export type ColorScheme = {
primary: Color
secondary: Color
ternary: Color
quartenary: Color
hoverHighlight: Color
markerCurrentTime: Color
text: Color
background: Color
toast?: Color
}
export const colorSchemes: Record<string, ColorScheme> = {
default: {
primary: "#eeeeee",
secondary: "#E0E0E0",
ternary: "#F5F5F5",
quartenary: "#ededed",
hoverHighlight: "rgba(204, 216, 219, 0.5)",
markerCurrentTime: "#000",
text: "#404040",
background: "white"
},
creamy: {
primary: "#ffe8d9",
secondary: "#fcdcc5",
ternary: "#fff6f0",
quartenary: "#f7ece6",
hoverHighlight: "rgba(230, 221, 202, 0.5)",
markerCurrentTime: "#000",
text: "#542d05",
background: "white"
},
crimson: {
primary: "#a82039",
secondary: "#c41238",
ternary: "#db4f56",
quartenary: "#ce5f64",
hoverHighlight: "rgba(196, 141, 141, 0.5)",
markerCurrentTime: "#000",
text: "white",
background: "white"
},
dark: {
primary: "#404040",
secondary: "#303030",
ternary: "#353535",
quartenary: "#383838",
hoverHighlight: "rgba(159, 160, 161, 0.5)",
markerCurrentTime: "#fff",
text: "white",
background: "#525252",
toast: "#1f1f1f"
},
flare: {
primary: "#e08a38",
secondary: "#e67912",
ternary: "#5e5145",
quartenary: "#665648",
hoverHighlight: "rgba(196, 141, 141, 0.5)",
markerCurrentTime: "#000",
text: "white",
background: "white"
},
fuchsia: {
primary: "#de1d5a",
secondary: "#b50b41",
ternary: "#ff7da6",
quartenary: "#f2799f",
hoverHighlight: "rgba(196, 141, 141, 0.5)",
markerCurrentTime: "#000",
text: "white",
background: "white"
},
grove: {
primary: "#3d9960",
secondary: "#288542",
ternary: "#72b585",
quartenary: "#65a577",
hoverHighlight: "rgba(160, 219, 171, 0.5)",
markerCurrentTime: "#000",
text: "white",
background: "white"
},
"material-blue": {
primary: "#0D47A1",
secondary: "#1565C0",
ternary: "#42a5f5",
quartenary: "#409fed",
hoverHighlight: "rgba(110, 165, 196, 0.5)",
markerCurrentTime: "#000",
text: "white",
background: "white"
},
sky: {
primary: "#b5e3ff",
secondary: "#a1d6f7",
ternary: "#d6f7ff",
quartenary: "#d0edf4",
hoverHighlight: "rgba(193, 202, 214, 0.5)",
markerCurrentTime: "#000",
text: "#022c47",
background: "white"
},
slumber: {
primary: "#2a2f42",
secondary: "#2f3447",
ternary: "#35394d",
quartenary: "#2c3044",
hoverHighlight: "rgba(179, 162, 127, 0.5)",
markerCurrentTime: "#fff",
text: "#ffe0b3",
background: "#38383b",
toast: "#1f1f1f"
},
vue: {
primary: "#258a5d",
secondary: "#41B883",
ternary: "#35495E",
quartenary: "#2a3d51",
hoverHighlight: "rgba(160, 219, 171, 0.5)",
markerCurrentTime: "#000",
text: "white",
background: "white"
}
}
export type ColorSchemeKey = keyof typeof colorSchemes
export default colorSchemes
================================================
FILE: src/components/GGanttBar.vue
================================================
<template>
<div
:id="barConfig.id"
:class="['g-gantt-bar', barConfig.class || '']"
:style="{
...barConfig.style,
position: 'absolute',
top: `${rowHeight * 0.1}px`,
left: `${xStart}px`,
width: `${xEnd - xStart}px`,
height: `${rowHeight * 0.8}px`,
zIndex: isDragging ? 3 : 2
}"
@mousedown="onMouseEvent"
@click="onMouseEvent"
@dblclick="onMouseEvent"
@mouseenter="onMouseEvent"
@mouseleave="onMouseEvent"
@contextmenu="onMouseEvent"
>
<div class="g-gantt-bar-label">
<slot :bar="bar">
<div>
{{ barConfig.label || "" }}
</div>
<div v-if="barConfig.html" v-html="barConfig.html"/>
</slot>
</div>
<template v-if="barConfig.hasHandles">
<div class="g-gantt-bar-handle-left" />
<div class="g-gantt-bar-handle-right" />
</template>
</div>
</template>
<script setup lang="ts">
import { computed, ref, toRefs, watch, onMounted, inject } from "vue"
import useBarDragManagement from "../composables/useBarDragManagement.js"
import useTimePositionMapping from "../composables/useTimePositionMapping.js"
import useBarDragLimit from "../composables/useBarDragLimit.js"
import type { GanttBarObject } from "../types"
import provideEmitBarEvent from "../provider/provideEmitBarEvent.js"
import provideConfig from "../provider/provideConfig.js"
import { BAR_CONTAINER_KEY } from "../provider/symbols"
const props = defineProps<{
bar: GanttBarObject
}>()
const emitBarEvent = provideEmitBarEvent()
const config = provideConfig()
const { rowHeight } = config
const { bar } = toRefs(props)
const { mapTimeToPosition, mapPositionToTime } = useTimePositionMapping()
const { initDragOfBar, initDragOfBundle } = useBarDragManagement()
const { setDragLimitsOfGanttBar } = useBarDragLimit()
const isDragging = ref(false)
const barConfig = computed(() => bar.value.ganttBarConfig)
function firstMousemoveCallback(e: MouseEvent) {
barConfig.value.bundle != null ? initDragOfBundle(bar.value, e) : initDragOfBar(bar.value, e)
isDragging.value = true
}
const prepareForDrag = () => {
setDragLimitsOfGanttBar(bar.value)
if (barConfig.value.immobile) {
return
}
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 barContainerEl = inject(BAR_CONTAINER_KEY)
const onMouseEvent = (e: MouseEvent) => {
e.preventDefault()
if (e.type === "mousedown") {
prepareForDrag()
}
const barContainer = barContainerEl?.value?.getBoundingClientRect()
if (!barContainer) {
return
}
const datetime = mapPositionToTime(e.clientX - barContainer.left)
emitBarEvent(e, bar.value, datetime)
}
const { barStart, barEnd, width, chartStart, chartEnd, chartSize } = config
const xStart = ref(0)
const xEnd = ref(0)
onMounted(() => {
watch(
[bar, width, chartStart, chartEnd, chartSize.width],
() => {
xStart.value = mapTimeToPosition(bar.value[barStart.value])
xEnd.value = mapTimeToPosition(bar.value[barEnd.value])
},
{ deep: true, immediate: true }
)
})
</script>
<style>
.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: ew-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>
================================================
FILE: src/components/GGanttBarTooltip.vue
================================================
<template>
<teleport to="body">
<transition name="g-fade" mode="out-in">
<div
v-if="modelValue"
class="g-gantt-tooltip"
:style="{
top: tooltipTop,
left: tooltipLeft,
fontFamily: font
}"
>
<div class="g-gantt-tooltip-color-dot" :style="{ background: dotColor }" />
<slot :bar="bar" :bar-start="barStartRaw" :bar-end="barEndRaw">
{{ tooltipContent }}
</slot>
</div>
</transition>
</teleport>
</template>
<script setup lang="ts">
import { computed, toRefs, ref, watch, nextTick } from "vue"
import type { GanttBarObject } from "../types"
import useDayjsHelper from "../composables/useDayjsHelper.js"
import provideConfig from "../provider/provideConfig.js"
const TOOLTIP_FORMATS = {
hour: "HH:mm",
day: "DD. MMM HH:mm",
date: "DD. MMMM YYYY",
month: "DD. MMMM YYYY",
week: "DD. MMMM YYYY (WW)"
} as const
const DEFAULT_DOT_COLOR = "cadetblue"
const props = defineProps<{
bar: GanttBarObject | undefined
modelValue: boolean
}>()
const { bar } = toRefs(props)
const { precision, font, barStart, barEnd, rowHeight } = provideConfig()
const tooltipTop = ref("0px")
const tooltipLeft = ref("0px")
watch(
() => props.bar,
async () => {
await nextTick()
const barId = bar?.value?.ganttBarConfig.id || ""
if (!barId) {
return
}
const barElement = document.getElementById(barId)
const { top, left } = barElement?.getBoundingClientRect() || {
top: 0,
left: 0
}
const leftValue = Math.max(left, 10)
tooltipTop.value = `${top + rowHeight.value - 10}px`
tooltipLeft.value = `${leftValue}px`
},
{ deep: true, immediate: true }
)
const dotColor = computed(() => bar?.value?.ganttBarConfig.style?.background || DEFAULT_DOT_COLOR)
const { toDayjs } = useDayjsHelper()
const barStartRaw = computed(() => bar.value?.[barStart.value])
const barEndRaw = computed(() => bar.value?.[barEnd.value])
const tooltipContent = computed(() => {
if (!bar?.value) {
return ""
}
const format = TOOLTIP_FORMATS[precision.value]
const barStartFormatted = toDayjs(barStartRaw.value).format(format)
const barEndFormatted = toDayjs(barEndRaw.value).format(format)
return `${barStartFormatted} \u2013 ${barEndFormatted}`
})
</script>
<style>
.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;
font-variant-numeric: tabular-nums;
}
.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-dot {
width: 8px;
height: 8px;
border-radius: 100%;
margin-right: 4px;
}
.g-fade-enter-active,
.g-fade-leave-active {
transition: opacity 0.3s ease;
}
.g-fade-enter-from,
.g-fade-leave-to {
opacity: 0;
}
</style>
================================================
FILE: src/components/GGanttChart.vue
================================================
<template>
<div>
<div :class="[{ 'labels-in-column': !!labelColumnTitle }]">
<g-gantt-label-column
v-if="labelColumnTitle"
:style="{
width: labelColumnWidth
}"
>
<template #label-column-title>
<slot name="label-column-title" />
</template>
<template #label-column-row="{ label }">
<slot name="label-column-row" :label="label" />
</template>
</g-gantt-label-column>
<div
ref="ganttChart"
:class="['g-gantt-chart', { 'with-column': labelColumnTitle }]"
:style="{ width, background: colors.background, fontFamily: font }"
>
<g-gantt-timeaxis v-if="!hideTimeaxis">
<template #upper-timeunit="{ label, value, date }">
<!-- expose upper-timeunit slot of g-gantt-timeaxis-->
<slot name="upper-timeunit" :label="label" :value="value" :date="date" />
</template>
<template #timeunit="{ label, value, date }">
<!-- expose timeunit slot of g-gantt-timeaxis-->
<slot name="timeunit" :label="label" :value="value" :date="date" />
</template>
</g-gantt-timeaxis>
<g-gantt-grid v-if="grid" :highlighted-units="highlightedUnits" />
<g-gantt-current-time v-if="currentTime">
<template #current-time-label>
<slot name="current-time-label" />
</template>
</g-gantt-current-time>
<div class="g-gantt-rows-container">
<slot />
<!-- the g-gantt-row components go here -->
</div>
</div>
</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 {
computed,
provide,
ref,
toRefs,
useSlots,
type ComputedRef,
type Ref,
type ToRefs
} from "vue"
import GGanttGrid from "./GGanttGrid.vue"
import GGanttLabelColumn from "./GGanttLabelColumn.vue"
import GGanttTimeaxis from "./GGanttTimeaxis.vue"
import GGanttBarTooltip from "./GGanttBarTooltip.vue"
import GGanttCurrentTime from "./GGanttCurrentTime.vue"
import type { GanttBarObject } from "../types"
import type { ColorSchemeKey } from "../color-schemes.js"
import { useElementSize } from "@vueuse/core"
import { DEFAULT_DATE_FORMAT } from "../composables/useDayjsHelper"
import { colorSchemes, type ColorScheme } from "../color-schemes.js"
import {
CHART_ROWS_KEY,
CONFIG_KEY,
EMIT_BAR_EVENT_KEY,
type ChartRow
} from "../provider/symbols.js"
export interface GGanttChartProps {
chartStart: string | Date
chartEnd: string | Date
precision?: "hour" | "day" | "date" | "week" | "month"
barStart: string
barEnd: string
currentTime?: boolean
currentTimeLabel?: string
dateFormat?: string | false
width?: string
hideTimeaxis?: boolean
colorScheme?: ColorSchemeKey | ColorScheme
grid?: boolean
pushOnOverlap?: boolean
noOverlap?: boolean
rowHeight?: number
highlightedUnits?: number[]
font?: string
labelColumnTitle?: string
labelColumnWidth?: string
}
export type GGanttChartConfig = ToRefs<Required<GGanttChartProps>> & {
colors: ComputedRef<ColorScheme>
chartSize: {
width: Ref<number>
height: Ref<number>
}
}
const props = withDefaults(defineProps<GGanttChartProps>(), {
currentTimeLabel: "",
dateFormat: DEFAULT_DATE_FORMAT,
precision: "day",
width: "100%",
hideTimeaxis: false,
colorScheme: "default",
grid: false,
pushOnOverlap: false,
noOverlap: false,
rowHeight: 40,
highlightedUnits: () => [],
font: "inherit",
labelColumnTitle: "",
labelColumnWidth: "150px"
})
const emit = defineEmits<{
(e: "click-bar", value: { bar: GanttBarObject; e: MouseEvent; datetime?: string | Date }): void
(
e: "mousedown-bar",
value: { bar: GanttBarObject; e: MouseEvent; datetime?: string | Date }
): void
(e: "mouseup-bar", value: { bar: GanttBarObject; e: MouseEvent; datetime?: string | Date }): void
(e: "dblclick-bar", value: { bar: GanttBarObject; e: MouseEvent; datetime?: string | Date }): 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 | Date }
): void
}>()
const { width, font, colorScheme } = toRefs(props)
const slots = useSlots()
const colors = computed(() =>
typeof colorScheme.value !== "string"
? colorScheme.value
: colorSchemes[colorScheme.value as ColorSchemeKey] || colorSchemes.default
)
const getChartRows = () => {
const defaultSlot = slots.default?.()
const allBars: ChartRow[] = []
if (!defaultSlot) {
return allBars
}
defaultSlot.forEach((child) => {
if (child.props?.bars) {
const { label, bars } = child.props
allBars.push({ label, 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?: ChartRow
}
if (granchildNode?.props?.bars) {
const { label, bars } = granchildNode.props
allBars.push({ label, bars })
}
})
}
})
return allBars
}
const showTooltip = ref(false)
const isDragging = ref(false)
const tooltipBar = ref<GanttBarObject | undefined>(undefined)
let tooltipTimeoutId: ReturnType<typeof setTimeout>
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 | Date,
movedBars?: Map<GanttBarObject, { oldStart: string; oldEnd: string }>
) => {
switch (e.type) {
case "click":
emit("click-bar", { bar, e, datetime })
break
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 ganttChart = ref<HTMLElement | null>(null)
const chartSize = useElementSize(ganttChart)
provide(CHART_ROWS_KEY, getChartRows)
provide(CONFIG_KEY, {
...toRefs(props),
colors,
chartSize
})
provide(EMIT_BAR_EVENT_KEY, emitBarEvent)
</script>
<style>
.g-gantt-chart {
position: relative;
display: flex;
flex-direction: column;
overflow-x: hidden;
-webkit-touch-callout: none;
user-select: none;
font-variant-numeric: tabular-nums;
border-radius: 5px;
}
.with-column {
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
border-top-right-radius: 5px;
border-bottom-right-radius: 5px;
}
.g-gantt-rows-container {
position: relative;
}
.labels-in-column {
display: flex;
flex-direction: row;
}
</style>
================================================
FILE: src/components/GGanttCurrentTime.vue
================================================
<template>
<div
class="g-grid-current-time"
:style="{
left: `${xDist}px`
}"
>
<div
class="g-grid-current-time-marker"
:style="{
border: `1px dashed ${colors.markerCurrentTime}`
}"
/>
<span class="g-grid-current-time-text" :style="{ color: colors.markerCurrentTime }">
<slot name="current-time-label">
{{ currentTimeLabel }}
</slot>
</span>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from "vue"
import useTimePositionMapping from "../composables/useTimePositionMapping.js"
import dayjs from "dayjs"
import provideConfig from "../provider/provideConfig.js"
const { mapTimeToPosition } = useTimePositionMapping()
const currentMoment = ref(dayjs())
const { colors, dateFormat, currentTimeLabel } = provideConfig()
const xDist = computed(() => {
const format = dateFormat.value || "YYYY-MM-DD HH:mm"
return mapTimeToPosition(dayjs(currentMoment.value, format).format(format))
})
</script>
<style>
.g-grid-current-time {
position: absolute;
height: 100%;
display: flex;
z-index: 5;
pointer-events: none;
}
.g-grid-current-time-marker {
width: 0px;
height: calc(100% - 2px);
display: flex;
}
.g-grid-current-time-text {
font-size: x-small;
}
</style>
================================================
FILE: src/components/GGanttGrid.vue
================================================
<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 : undefined
}"
/>
</div>
</template>
<script setup lang="ts">
import provideConfig from "../provider/provideConfig.js"
import useTimeaxisUnits from "../composables/useTimeaxisUnits.js"
defineProps<{
highlightedUnits?: number[]
}>()
const { colors } = provideConfig()
const { timeaxisUnits } = useTimeaxisUnits()
</script>
<style>
.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>
================================================
FILE: src/components/GGanttLabelColumn.vue
================================================
<template>
<div class="g-label-column" :style="{ fontFamily: font, color: colors.text }">
<slot name="label-column-title">
<div class="g-label-column-header" :style="{ background: colors.primary }">
{{ labelColumnTitle }}
</div>
</slot>
<div class="g-label-column-rows">
<div
v-for="({ label }, index) in getChartRows()"
:key="`${label}_${index}`"
class="g-label-column-row"
:style="{
background: index % 2 === 0 ? colors.ternary : colors.quartenary,
height: `${rowHeight}px`
}"
>
<slot name="label-column-row" :label="label">
<span>{{ label }}</span>
</slot>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import provideGetChartRows from "../provider/provideGetChartRows"
import provideConfig from "../provider/provideConfig.js"
const { font, colors, labelColumnTitle, rowHeight } = provideConfig()
const getChartRows = provideGetChartRows()
</script>
<style>
.g-label-column {
display: flex;
align-items: center;
flex-direction: column;
color: rgb(64, 64, 64);
font-variant-numeric: tabular-nums;
font-size: 0.9em;
}
.g-label-column-header {
width: 100%;
height: 80px;
min-height: 80px;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
border-top-left-radius: 5px;
}
.g-label-column-rows {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
border-bottom-left-radius: 5px;
}
.g-label-column-row {
width: 100%;
height: 100%;
display: flex;
padding: 0.1rem 0.3rem;
overflow: hidden;
white-space: normal;
box-sizing: border-box;
text-align: center;
align-items: center;
justify-content: center;
}
.g-label-column-row:last-child {
border-bottom-left-radius: 5px;
}
</style>
================================================
FILE: src/components/GGanttRow.vue
================================================
<template>
<div
class="g-gantt-row"
:style="rowStyle"
@dragover.prevent="isHovering = true"
@dragleave="isHovering = false"
@drop="onDrop($event)"
@mouseover="isHovering = true"
@mouseleave="isHovering = false"
>
<div
v-if="!isBlank(label) && !labelColumnTitle"
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 { ref, type Ref, toRefs, computed, type StyleValue, provide } from "vue"
import useTimePositionMapping from "../composables/useTimePositionMapping.js"
import provideConfig from "../provider/provideConfig.js"
import type { GanttBarObject } from "../types"
import GGanttBar from "./GGanttBar.vue"
import { BAR_CONTAINER_KEY } from "../provider/symbols"
const props = defineProps<{
label: string
bars: GanttBarObject[]
highlightOnHover?: boolean
}>()
const emit = defineEmits<{
(e: "drop", value: { e: MouseEvent; datetime: string | Date }): void
}>()
const { rowHeight, colors, labelColumnTitle } = provideConfig()
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
} as StyleValue
})
const { mapPositionToTime } = useTimePositionMapping()
const barContainer: Ref<HTMLElement | null> = ref(null)
provide(BAR_CONTAINER_KEY, barContainer)
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 })
}
const isBlank = (str: string) => {
return (!str || /^\s*$/.test(str))
}
</script>
<style>
.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: all 0.2s;
}
.bar-transition-enter-from,
.bar-transition-leave-to {
transform: scale(0.8);
opacity: 0;
}
</style>
================================================
FILE: src/components/GGanttTimeaxis.vue
================================================
<template>
<div class="g-timeaxis">
<div class="g-timeunits-container">
<div
v-for="({ label, value, date, 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" :date="date">
{{ label }}
</slot>
</div>
</div>
<div class="g-timeunits-container">
<div
v-for="({ label, value, date, 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' ? 'column' : 'row',
alignItems: precision === 'hour' ? '' : 'center',
width
}"
>
<slot name="timeunit" :label="label" :value="value" :date="date">
{{ label }}
</slot>
<div
v-if="precision === 'hour'"
class="g-timeaxis-hour-pin"
:style="{ background: colors.text }"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import provideConfig from "../provider/provideConfig.js"
import useTimeaxisUnits from "../composables/useTimeaxisUnits.js"
const { precision, colors } = provideConfig()
const { timeaxisUnits } = useTimeaxisUnits()
</script>
<style>
.g-timeaxis {
position: sticky;
top: 0;
width: 100%;
height: 80px;
background: white;
z-index: 4;
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;
}
</style>
================================================
FILE: src/composables/createBarDrag.ts
================================================
import { ref } from "vue"
import type { GGanttChartConfig } from "../components/GGanttChart.vue"
import provideConfig from "../provider/provideConfig.js"
import type { GanttBarObject } from "../types"
import useDayjsHelper from "./useDayjsHelper.js"
import useTimePositionMapping from "./useTimePositionMapping.js"
export default function createBarDrag(
bar: GanttBarObject,
onDrag: (e: MouseEvent, bar: GanttBarObject) => void = () => null,
onEndDrag: (e: MouseEvent, bar: GanttBarObject) => void = () => null,
config: GGanttChartConfig = provideConfig()
) {
const { barStart, barEnd, pushOnOverlap } = config
const isDragging = ref(false)
let cursorOffsetX = 0
let dragCallBack: (e: MouseEvent) => void
const { mapPositionToTime } = useTimePositionMapping(config)
const { toDayjs } = useDayjsHelper(config)
const initDrag = (e: MouseEvent) => {
const barElement = document.getElementById(bar.ganttBarConfig.id)
if (!barElement) {
return
}
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 = "ew-resize"
dragCallBack = dragByLeftHandle
break
case "g-gantt-bar-handle-right":
document.body.style.cursor = "ew-resize"
dragCallBack = dragByRightHandle
break
default:
dragCallBack = drag
}
isDragging.value = true
window.addEventListener("mousemove", dragCallBack)
window.addEventListener("mouseup", endDrag)
}
const getBarElements = () => {
const barElement = document.getElementById(bar.ganttBarConfig.id)
const barContainer = barElement?.closest(".g-gantt-row-bars-container")?.getBoundingClientRect()
return { barElement, barContainer }
}
const drag = (e: MouseEvent) => {
const { barElement, barContainer } = getBarElements()
if (!barElement || !barContainer) {
return
}
const barWidth = barElement.getBoundingClientRect().width
const xStart = e.clientX - barContainer.left - cursorOffsetX
const xEnd = xStart + barWidth
if (isOutOfRange(xStart, xEnd)) {
return
}
bar[barStart.value] = mapPositionToTime(xStart)
bar[barEnd.value] = mapPositionToTime(xEnd)
onDrag(e, bar)
}
const dragByLeftHandle = (e: MouseEvent) => {
const { barElement, barContainer } = getBarElements()
if (!barElement || !barContainer) {
return
}
const xStart = e.clientX - barContainer.left
const newBarStart = mapPositionToTime(xStart)
if (toDayjs(newBarStart).isSameOrAfter(toDayjs(bar, "end"))) {
return
}
bar[barStart.value] = newBarStart
onDrag(e, bar)
}
const dragByRightHandle = (e: MouseEvent) => {
const { barElement, barContainer } = getBarElements()
if (!barElement || !barContainer) {
return
}
const xEnd = e.clientX - barContainer.left
const newBarEnd = mapPositionToTime(xEnd)
if (toDayjs(newBarEnd).isSameOrBefore(toDayjs(bar, "start"))) {
return
}
bar[barEnd.value] = newBarEnd
onDrag(e, bar)
}
const isOutOfRange = (xStart?: number, xEnd?: number) => {
if (!pushOnOverlap.value) {
return false
}
const dragLimitLeft = bar.ganttBarConfig.dragLimitLeft
const dragLimitRight = bar.ganttBarConfig.dragLimitRight
return (
(xStart && dragLimitLeft != null && xStart < dragLimitLeft) ||
(xEnd && dragLimitRight != null && xEnd > dragLimitRight)
)
}
const endDrag = (e: MouseEvent) => {
isDragging.value = false
document.body.style.cursor = ""
window.removeEventListener("mousemove", dragCallBack)
window.removeEventListener("mouseup", endDrag)
onEndDrag(e, bar)
}
return {
isDragging,
initDrag
}
}
================================================
FILE: src/composables/useBarDragLimit.ts
================================================
import type { GanttBarObject } from "../types"
import provideConfig from "../provider/provideConfig.js"
import provideGetChartRows from "../provider/provideGetChartRows.js"
export default function useBarDragLimit() {
const { pushOnOverlap } = provideConfig()
const getChartRows = provideGetChartRows()
const getBarsFromBundle = (bundle?: string) => {
const res: GanttBarObject[] = []
if (bundle != null) {
getChartRows().forEach((row) => {
row.bars.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) {
continue
}
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 = getChartRows().find((row) => row.bars.includes(bar))?.bars ?? []
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
}
}
================================================
FILE: src/composables/useBarDragManagement.ts
================================================
import type { GanttBarObject } from "../types"
import createBarDrag from "./createBarDrag.js"
import useDayjsHelper from "./useDayjsHelper.js"
import provideConfig from "../provider/provideConfig.js"
import provideGetChartRows from "../provider/provideGetChartRows.js"
import provideEmitBarEvent from "../provider/provideEmitBarEvent.js"
export default function useBarDragManagement() {
const config = provideConfig()
const getChartRows = provideGetChartRows()
const emitBarEvent = provideEmitBarEvent()
const { pushOnOverlap, barStart, barEnd, noOverlap, dateFormat } = config
const movedBarsInDrag = new Map<GanttBarObject, { oldStart: string; oldEnd: string }>()
const { toDayjs, format } = useDayjsHelper()
const initDragOfBar = (bar: GanttBarObject, e: MouseEvent) => {
const { initDrag } = createBarDrag(bar, onDrag, onEndDrag, config)
emitBarEvent({ ...e, type: "dragstart" }, bar)
initDrag(e)
addBarToMovedBars(bar)
}
const initDragOfBundle = (mainBar: GanttBarObject, e: MouseEvent) => {
const bundle = mainBar.ganttBarConfig.bundle
if (bundle == null) {
return
}
getChartRows().forEach((row) => {
row.bars.forEach((bar) => {
if (bar.ganttBarConfig.bundle === bundle) {
const dragEndHandler = bar === mainBar ? onEndDrag : () => null
const { initDrag } = createBarDrag(bar, onDrag, dragEndHandler, config)
initDrag(e)
addBarToMovedBars(bar)
}
})
})
emitBarEvent({ ...e, type: "dragstart" }, mainBar)
}
const onDrag = (e: MouseEvent, bar: GanttBarObject) => {
emitBarEvent({ ...e, type: "drag" }, 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] = format(currentBar[barStart.value], dateFormat.value)
overlapBar[barStart.value] = format(
overlapBarStart.subtract(minuteDiff, "minutes"),
dateFormat.value
)
break
case "right":
minuteDiff = currentBarEnd.diff(overlapBarStart, "minutes", true)
overlapBar[barStart.value] = format(currentBarEnd, dateFormat.value)
overlapBar[barEnd.value] = format(
overlapBarEnd.add(minuteDiff, "minutes"),
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 = getChartRows().find((row) => row.bars.includes(ganttBar))?.bars ?? []
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
})
const 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) {
return
}
getChartRows().forEach((row) => {
row.bars.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] = format(
toDayjs(bar, "start").subtract(minutes, "minutes"),
dateFormat.value
)
bar[barEnd.value] = format(
toDayjs(bar, "end").subtract(minutes, "minutes"),
dateFormat.value
)
break
case "right":
bar[barStart.value] = format(
toDayjs(bar, "start").add(minutes, "minutes"),
dateFormat.value
)
bar[barEnd.value] = format(toDayjs(bar, "end").add(minutes, "minutes"), 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) {
return
}
let isAnyOverlap = false
movedBarsInDrag.forEach((_, bar) => {
const { overlapBar } = getOverlapBarAndType(bar)
if (overlapBar != null) {
isAnyOverlap = true
}
})
if (!isAnyOverlap) {
return
}
movedBarsInDrag.forEach(({ oldStart, oldEnd }, bar) => {
bar[barStart.value] = oldStart
bar[barEnd.value] = oldEnd
})
}
return {
initDragOfBar,
initDragOfBundle
}
}
================================================
FILE: src/composables/useDayjsHelper.ts
================================================
import dayjs, { type Dayjs } from "dayjs"
import { computed } from "vue"
import type { GGanttChartConfig } from "../components/GGanttChart.vue"
import type { GanttBarObject } from "../types"
import provideConfig from "../provider/provideConfig.js"
export const DEFAULT_DATE_FORMAT = "YYYY-MM-DD HH:mm"
export default function useDayjsHelper(config: GGanttChartConfig = provideConfig()) {
const { chartStart, chartEnd, barStart, barEnd, dateFormat } = config
const chartStartDayjs = computed(() => toDayjs(chartStart.value))
const chartEndDayjs = computed(() => toDayjs(chartEnd.value))
const toDayjs = (input: string | Date | GanttBarObject, startOrEnd?: "start" | "end") => {
let value
if (startOrEnd !== undefined && typeof input !== "string" && !(input instanceof Date)) {
value = startOrEnd === "start" ? input[barStart.value] : input[barEnd.value]
}
if (typeof input === "string") {
value = input
} else if (input instanceof Date) {
return dayjs(input)
}
const format = dateFormat.value || DEFAULT_DATE_FORMAT
return dayjs(value, format, true)
}
const format = (input: string | Date | Dayjs, pattern?: string | false) => {
if (pattern === false) {
return input instanceof Date ? input : dayjs(input).toDate()
}
const inputDayjs = typeof input === "string" || input instanceof Date ? toDayjs(input) : input
return inputDayjs.format(pattern)
}
return {
chartStartDayjs,
chartEndDayjs,
toDayjs,
format
}
}
================================================
FILE: src/composables/useTimePositionMapping.ts
================================================
import type { GGanttChartConfig } from "../components/GGanttChart.vue"
import { computed } from "vue"
import useDayjsHelper from "./useDayjsHelper.js"
import provideConfig from "../provider/provideConfig.js"
export default function useTimePositionMapping(config: GGanttChartConfig = provideConfig()) {
const { dateFormat, chartSize } = config
const { chartStartDayjs, chartEndDayjs, toDayjs, format } = useDayjsHelper(config)
const totalNumOfMinutes = computed(() => {
return chartEndDayjs.value.diff(chartStartDayjs.value, "minutes")
})
const mapTimeToPosition = (time: string) => {
const width = chartSize.width.value || 0
const diffFromStart = toDayjs(time).diff(chartStartDayjs.value, "minutes", true)
return Math.ceil((diffFromStart / totalNumOfMinutes.value) * width)
}
const mapPositionToTime = (xPos: number) => {
const width = chartSize.width.value || 0
const diffFromStart = (xPos / width) * totalNumOfMinutes.value
return format(chartStartDayjs.value.add(diffFromStart, "minutes"), dateFormat.value)
}
return {
mapTimeToPosition,
mapPositionToTime
}
}
================================================
FILE: src/composables/useTimeaxisUnits.ts
================================================
import { computed } from "vue"
import useDayjsHelper from "./useDayjsHelper.js"
import provideConfig from "../provider/provideConfig.js"
export default function useTimeaxisUnits() {
const { precision } = provideConfig()
const { chartStartDayjs, chartEndDayjs } = useDayjsHelper()
const upperPrecision = computed(() => {
switch (precision?.value) {
case "hour":
return "day"
case "day":
return "month"
case "date":
case "week":
return "month"
case "month":
return "year"
default:
throw new Error(
"Precision prop incorrect. Must be one of the following: 'hour', 'day', 'date', 'week', 'month'"
)
}
})
const lowerPrecision = computed(() => {
switch (precision.value) {
case "date":
return "day"
case "week":
return "isoWeek"
default:
return precision.value
}
})
const displayFormats = {
hour: "HH",
date: "DD.MMM",
day: "DD.MMM",
week: "WW",
month: "MMMM YYYY",
year: "YYYY"
}
const timeaxisUnits = computed(() => {
const upperUnits: { label: string; value?: string; date: Date; width?: string }[] = []
const lowerUnits: { label: string; value?: string; date: Date; width?: string }[] = []
const totalMinutes = chartEndDayjs.value.diff(chartStartDayjs.value, "minutes", true)
const upperUnit = upperPrecision.value
const lowerUnit = lowerPrecision.value
let currentUpperUnit = chartStartDayjs.value
let currentLowerUnit = chartStartDayjs.value
while (currentLowerUnit.isSameOrBefore(chartEndDayjs.value)) {
const endCurrentLowerUnit = currentLowerUnit.endOf(lowerUnit)
const isLastItem = endCurrentLowerUnit.isAfter(chartEndDayjs.value)
const lowerWidth = isLastItem
? (chartEndDayjs.value.diff(currentLowerUnit, "minutes", true) / totalMinutes) * 100
: (endCurrentLowerUnit.diff(currentLowerUnit, "minutes", true) / totalMinutes) * 100
lowerUnits.push({
label: currentLowerUnit.format(displayFormats[precision?.value]),
value: String(currentLowerUnit),
date: currentLowerUnit.toDate(),
width: String(lowerWidth) + "%"
})
currentLowerUnit = endCurrentLowerUnit
.add(1, lowerUnit === "isoWeek" ? "week" : lowerUnit)
.startOf(lowerUnit)
}
while (currentUpperUnit.isSameOrBefore(chartEndDayjs.value)) {
const endCurrentUpperUnit = currentUpperUnit.endOf(upperUnit)
const isLastItem = endCurrentUpperUnit.isAfter(chartEndDayjs.value)
const upperWidth = isLastItem
? (chartEndDayjs.value.diff(currentUpperUnit, "minutes", true) / totalMinutes) * 100
: (endCurrentUpperUnit.diff(currentUpperUnit, "minutes", true) / totalMinutes) * 100
upperUnits.push({
label: currentUpperUnit.format(displayFormats[upperUnit]),
value: String(currentUpperUnit),
date: currentUpperUnit.toDate(),
width: String(upperWidth) + "%"
})
currentUpperUnit = endCurrentUpperUnit.add(1, upperUnit).startOf(upperUnit)
}
return { upperUnits, lowerUnits }
})
return {
timeaxisUnits
}
}
================================================
FILE: src/playground.ts
================================================
import { createApp } from "vue"
import Playground from "./GanttPlayground.vue"
import ganttastic from "./vue-ganttastic.js"
createApp(Playground).use(ganttastic).mount("#app")
================================================
FILE: src/provider/provideConfig.ts
================================================
import { inject } from "vue"
import { CONFIG_KEY } from "./symbols.js"
export default function provideConfig() {
const config = inject(CONFIG_KEY)
if (!config) {
throw Error("Failed to inject config!")
}
return config
}
================================================
FILE: src/provider/provideEmitBarEvent.ts
================================================
import { inject } from "vue"
import { EMIT_BAR_EVENT_KEY } from "./symbols.js"
export default function provideEmitBarEvent() {
const emitBarEvent = inject(EMIT_BAR_EVENT_KEY)
if (!emitBarEvent) {
throw Error("Failed to inject emitBarEvent!")
}
return emitBarEvent
}
================================================
FILE: src/provider/provideGetChartRows.ts
================================================
import { inject } from "vue"
import { CHART_ROWS_KEY } from "./symbols.js"
export default function provideGetChartRows() {
const getChartRows = inject(CHART_ROWS_KEY)
if (!getChartRows) {
throw Error("Failed to inject getChartRows!")
}
return getChartRows
}
================================================
FILE: src/provider/symbols.ts
================================================
import type { InjectionKey, Ref } from "vue"
import type { GGanttChartConfig } from "../components/GGanttChart.vue"
import type { GanttBarObject } from "../types"
export type ChartRow = { label: string; bars: GanttBarObject[] }
export type GetChartRows = () => ChartRow[]
export type EmitBarEvent = (
e: MouseEvent,
bar: GanttBarObject,
datetime?: string | Date,
movedBars?: Map<GanttBarObject, { oldStart: string; oldEnd: string }>
) => void
export const CHART_ROWS_KEY = Symbol("CHART_ROWS_KEY") as InjectionKey<GetChartRows>
export const CONFIG_KEY = Symbol("CONFIG_KEY") as InjectionKey<GGanttChartConfig>
export const EMIT_BAR_EVENT_KEY = Symbol("EMIT_BAR_EVENT_KEY") as InjectionKey<EmitBarEvent>
export const BAR_CONTAINER_KEY = Symbol("BAR_CONTAINER_KEY") as InjectionKey<
Ref<HTMLElement | null>
>
================================================
FILE: src/types.ts
================================================
import type { CSSProperties } from "vue"
export type GanttBarObject = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any
ganttBarConfig: {
id: string
label?: string
html?: string
hasHandles?: boolean
immobile?: boolean
bundle?: string
pushOnOverlap?: boolean
dragLimitLeft?: number
dragLimitRight?: number
style?: CSSProperties
class?: string
}
}
================================================
FILE: src/vue-ganttastic.ts
================================================
import type { Plugin } from "vue"
import dayjs from "dayjs"
import isoWeek from "dayjs/plugin/isoWeek"
import isSameOrBefore from "dayjs/plugin/isSameOrBefore.js"
import isSameOrAfter from "dayjs/plugin/isSameOrAfter.js"
import isBetween from "dayjs/plugin/isBetween.js"
import weekOfYear from "dayjs/plugin/weekOfYear"
import advancedFormat from "dayjs/plugin/advancedFormat"
import customParseFormat from "dayjs/plugin/customParseFormat.js"
import type { GanttBarObject } from "./types.js"
import type { ColorScheme } from "./color-schemes"
import GGanttChart from "./components/GGanttChart.vue"
import GGanttRow from "./components/GGanttRow.vue"
export function extendDayjs() {
dayjs.extend(isSameOrBefore)
dayjs.extend(isSameOrAfter)
dayjs.extend(isBetween)
dayjs.extend(customParseFormat)
dayjs.extend(weekOfYear)
dayjs.extend(isoWeek)
dayjs.extend(advancedFormat)
}
export type { ColorScheme, GanttBarObject }
export { GGanttChart, GGanttRow }
export const ganttastic: Plugin = {
install(app, options?) {
extendDayjs()
app.component("GGanttChart", GGanttChart)
app.component("GGanttRow", GGanttRow)
}
}
export default ganttastic
================================================
FILE: tsconfig.config.json
================================================
{
"extends": "@vue/tsconfig/tsconfig.node.json",
"include": ["vite.config.*", "vitest.config.*", "cypress.config.*"],
"compilerOptions": {
"composite": true,
"types": ["node"]
}
}
================================================
FILE: tsconfig.json
================================================
{
"extends": "@vue/tsconfig/tsconfig.web.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"compilerOptions": {
"baseUrl": "."
},
"references": [
{
"path": "./tsconfig.config.json"
}
]
}
================================================
FILE: vite.config.mts
================================================
import { fileURLToPath, URL } from "node:url"
import { defineConfig } from "vite"
import vue from "@vitejs/plugin-vue"
import postcssPresetEnv from "postcss-preset-env"
import styleInject from "@senojs/rollup-plugin-style-inject"
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
styleInject({
insertAt: "top"
})
],
css: {
postcss: {
plugins: [postcssPresetEnv()]
}
},
build: {
lib:
process.env.NODE_ENV === "production"
? {
entry: fileURLToPath(
new URL("src/vue-ganttastic.ts", import.meta.url)
),
name: "VueGanttastic",
fileName: "vue-ganttastic"
}
: undefined,
outDir: process.env.NODE_ENV === "production" ? "lib" : "dist",
rollupOptions: {
// make sure to externalize deps that shouldn't be bundled
// into the library
external: ["vue", "dayjs"],
output: {
// Provide global variables to use in the UMD build
// for externalized deps
globals: {
vue: "Vue",
dayjs: "dayjs"
},
exports: "named"
}
}
}
})
gitextract_6_i3sk9e/ ├── .browserslistrc ├── .editorconfig ├── .eslintrc.cjs ├── .github/ │ ├── FUNDING.yml │ └── workflows/ │ └── deploy.yml ├── .gitignore ├── .prettierrc ├── README.md ├── deploy.sh ├── docs/ │ ├── .vitepress/ │ │ ├── config.ts │ │ └── theme/ │ │ ├── custom.css │ │ └── index.js │ ├── GGanttChart.md │ ├── GGanttRow.md │ ├── common-use-cases.md │ ├── examples.md │ ├── getting-started.md │ ├── index.md │ └── introduction.md ├── docs-deploy.yml ├── env.d.ts ├── index.html ├── package.json ├── src/ │ ├── GanttPlayground.vue │ ├── color-schemes.ts │ ├── components/ │ │ ├── GGanttBar.vue │ │ ├── GGanttBarTooltip.vue │ │ ├── GGanttChart.vue │ │ ├── GGanttCurrentTime.vue │ │ ├── GGanttGrid.vue │ │ ├── GGanttLabelColumn.vue │ │ ├── GGanttRow.vue │ │ └── GGanttTimeaxis.vue │ ├── composables/ │ │ ├── createBarDrag.ts │ │ ├── useBarDragLimit.ts │ │ ├── useBarDragManagement.ts │ │ ├── useDayjsHelper.ts │ │ ├── useTimePositionMapping.ts │ │ └── useTimeaxisUnits.ts │ ├── playground.ts │ ├── provider/ │ │ ├── provideConfig.ts │ │ ├── provideEmitBarEvent.ts │ │ ├── provideGetChartRows.ts │ │ └── symbols.ts │ ├── types.ts │ └── vue-ganttastic.ts ├── tsconfig.config.json ├── tsconfig.json └── vite.config.mts
SYMBOL INDEX (24 symbols across 14 files)
FILE: docs/.vitepress/theme/index.js
method enhanceApp (line 7) | enhanceApp(ctx) {
FILE: src/color-schemes.ts
type Color (line 3) | type Color = CSS.DataType.Color
type ColorScheme (line 5) | type ColorScheme = {
type ColorSchemeKey (line 142) | type ColorSchemeKey = keyof typeof colorSchemes
FILE: src/composables/createBarDrag.ts
function createBarDrag (line 9) | function createBarDrag(
FILE: src/composables/useBarDragLimit.ts
function useBarDragLimit (line 5) | function useBarDragLimit() {
FILE: src/composables/useBarDragManagement.ts
function useBarDragManagement (line 9) | function useBarDragManagement() {
FILE: src/composables/useDayjsHelper.ts
constant DEFAULT_DATE_FORMAT (line 8) | const DEFAULT_DATE_FORMAT = "YYYY-MM-DD HH:mm"
function useDayjsHelper (line 10) | function useDayjsHelper(config: GGanttChartConfig = provideConfig()) {
FILE: src/composables/useTimePositionMapping.ts
function useTimePositionMapping (line 7) | function useTimePositionMapping(config: GGanttChartConfig = provideConfi...
FILE: src/composables/useTimeaxisUnits.ts
function useTimeaxisUnits (line 5) | function useTimeaxisUnits() {
FILE: src/provider/provideConfig.ts
function provideConfig (line 4) | function provideConfig() {
FILE: src/provider/provideEmitBarEvent.ts
function provideEmitBarEvent (line 4) | function provideEmitBarEvent() {
FILE: src/provider/provideGetChartRows.ts
function provideGetChartRows (line 4) | function provideGetChartRows() {
FILE: src/provider/symbols.ts
type ChartRow (line 6) | type ChartRow = { label: string; bars: GanttBarObject[] }
type GetChartRows (line 7) | type GetChartRows = () => ChartRow[]
type EmitBarEvent (line 8) | type EmitBarEvent = (
constant CHART_ROWS_KEY (line 15) | const CHART_ROWS_KEY = Symbol("CHART_ROWS_KEY") as InjectionKey<GetChart...
constant CONFIG_KEY (line 16) | const CONFIG_KEY = Symbol("CONFIG_KEY") as InjectionKey<GGanttChartConfig>
constant EMIT_BAR_EVENT_KEY (line 17) | const EMIT_BAR_EVENT_KEY = Symbol("EMIT_BAR_EVENT_KEY") as InjectionKey<...
constant BAR_CONTAINER_KEY (line 18) | const BAR_CONTAINER_KEY = Symbol("BAR_CONTAINER_KEY") as InjectionKey<
FILE: src/types.ts
type GanttBarObject (line 3) | type GanttBarObject = {
FILE: src/vue-ganttastic.ts
function extendDayjs (line 17) | function extendDayjs() {
method install (line 31) | install(app, options?) {
Condensed preview — 49 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (109K chars).
[
{
"path": ".browserslistrc",
"chars": 30,
"preview": "> 1%\nlast 2 versions\nnot dead\n"
},
{
"path": ".editorconfig",
"chars": 142,
"preview": "[*.{js,jsx,ts,tsx,vue}]\nindent_style = space\nindent_size = 2\ntrim_trailing_whitespace = true\ninsert_final_newline = true"
},
{
"path": ".eslintrc.cjs",
"chars": 706,
"preview": "/* eslint-env node */\nrequire(\"@rushstack/eslint-patch/modern-module-resolution\")\n\nmodule.exports = {\n root: true,\n\n e"
},
{
"path": ".github/FUNDING.yml",
"chars": 723,
"preview": "# These are supported funding model platforms\n\ngithub: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [u"
},
{
"path": ".github/workflows/deploy.yml",
"chars": 1997,
"preview": "# Sample workflow for building and deploying a VitePress site to GitHub Pages\n#\nname: Deploy VitePress site to Pages\n\non"
},
{
"path": ".gitignore",
"chars": 335,
"preview": ".DS_Store\nnode_modules\n/lib\n/lib_types\n\n\n# local env files\n.env.local\n.env.*.local\n\n# Log files\nnpm-debug.log*\nyarn-debu"
},
{
"path": ".prettierrc",
"chars": 69,
"preview": "{\n \"semi\": false,\n \"trailingComma\": \"none\",\n \"endOfLine\": \"auto\"\n}"
},
{
"path": "README.md",
"chars": 4592,
"preview": "# Vue Ganttastic\n\n<div style=\"display: flex; flex-direction: column; align-items:center;\">\n<img\n src=\"https://user-im"
},
{
"path": "deploy.sh",
"chars": 173,
"preview": "set -e\n\nnpm run docs:build\ncd docs/.vuepress/dist\n\ngit init\ngit add -A\ngit commit -m 'deploy'\n\ngit push -f git@github.co"
},
{
"path": "docs/.vitepress/config.ts",
"chars": 1137,
"preview": "import { defineConfig } from 'vitepress'\n\n// https://vitepress.dev/reference/site-config\nexport default defineConfig({\n "
},
{
"path": "docs/.vitepress/theme/custom.css",
"chars": 152,
"preview": ":root {\n --vp-home-hero-name-color: #2fb585;\n --vp-button-brand-bg: #2fb585;\n --vp-button-brand-hover-bg: #354b"
},
{
"path": "docs/.vitepress/theme/index.js",
"chars": 227,
"preview": "import DefaultTheme from 'vitepress/theme'\nimport './custom.css'\nimport {ganttastic} from \"../../../src/vue-ganttastic\"\n"
},
{
"path": "docs/GGanttChart.md",
"chars": 5279,
"preview": "# API: GGanttChart\nThe main component of Vue Ganttastic. Represents an entire chart and is meant to have at least one `g"
},
{
"path": "docs/GGanttRow.md",
"chars": 1601,
"preview": "# API: GGanttRow\nRepresents a single row of the chart. It is meant to be a child component of `g-gantt-chart`. \n\n## Pro"
},
{
"path": "docs/common-use-cases.md",
"chars": 5747,
"preview": "# Common use cases\n The following section provides a non-exhausting list of common use cases and special features of Vue"
},
{
"path": "docs/examples.md",
"chars": 5708,
"preview": "\n# Live Demos\n\n## Simple hour chart \n- `precision`: `hour`\n<g-gantt-chart chart-start=\"01.01.2022 12:00\" chart-end=\"02."
},
{
"path": "docs/getting-started.md",
"chars": 2736,
"preview": "# Getting started\n\n## Install\n\nYou can add Vue Ganttastic to your project using <kbd>npm</kbd>:\n\n```\nnpm install @infect"
},
{
"path": "docs/index.md",
"chars": 879,
"preview": "---\n# https://vitepress.dev/reference/default-theme-home-page\nlayout: home\n\nhero:\n name: Vue-Ganttastic\n text: Gantt c"
},
{
"path": "docs/introduction.md",
"chars": 1416,
"preview": "# Introduction\nVue Ganttastic is a simple, interactive and highly customizable Gantt chart component for Vue 3. \n\n## F"
},
{
"path": "docs-deploy.yml",
"chars": 1888,
"preview": "# Sample workflow for building and deploying a VitePress site to GitHub Pages\n#\nname: Deploy VitePress site to Pages\n\non"
},
{
"path": "env.d.ts",
"chars": 38,
"preview": "/// <reference types=\"vite/client\" />\n"
},
{
"path": "index.html",
"chars": 598,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n\t<meta charset=\"utf-8\">\n\t<meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n\t"
},
{
"path": "package.json",
"chars": 2186,
"preview": "{\n \"name\": \"@infectoone/vue-ganttastic\",\n \"version\": \"2.3.2\",\n \"description\": \"A simple and customizable Gantt chart "
},
{
"path": "src/GanttPlayground.vue",
"chars": 6198,
"preview": "<template>\n <g-gantt-chart\n :chart-start=\"chartStart\"\n :chart-end=\"chartEnd\"\n precision=\"week\"\n :row-height"
},
{
"path": "src/color-schemes.ts",
"chars": 3129,
"preview": "import type * as CSS from \"csstype\"\n\ntype Color = CSS.DataType.Color\n\nexport type ColorScheme = {\n primary: Color\n sec"
},
{
"path": "src/components/GGanttBar.vue",
"chars": 4167,
"preview": "<template>\n <div\n :id=\"barConfig.id\"\n :class=\"['g-gantt-bar', barConfig.class || '']\"\n :style=\"{\n ...barC"
},
{
"path": "src/components/GGanttBarTooltip.vue",
"chars": 3079,
"preview": "<template>\n <teleport to=\"body\">\n <transition name=\"g-fade\" mode=\"out-in\">\n <div\n v-if=\"modelValue\"\n "
},
{
"path": "src/components/GGanttChart.vue",
"chars": 8131,
"preview": "<template>\n <div>\n <div :class=\"[{ 'labels-in-column': !!labelColumnTitle }]\">\n <g-gantt-label-column\n v"
},
{
"path": "src/components/GGanttCurrentTime.vue",
"chars": 1283,
"preview": "<template>\n <div\n class=\"g-grid-current-time\"\n :style=\"{\n left: `${xDist}px`\n }\"\n >\n <div\n class"
},
{
"path": "src/components/GGanttGrid.vue",
"chars": 868,
"preview": "<template>\n <div class=\"g-grid-container\">\n <div\n v-for=\"{ label, value, width } in timeaxisUnits.lowerUnits\"\n "
},
{
"path": "src/components/GGanttLabelColumn.vue",
"chars": 1839,
"preview": "<template>\n <div class=\"g-label-column\" :style=\"{ fontFamily: font, color: colors.text }\">\n <slot name=\"label-column"
},
{
"path": "src/components/GGanttRow.vue",
"chars": 3104,
"preview": "<template>\n <div\n class=\"g-gantt-row\"\n :style=\"rowStyle\"\n @dragover.prevent=\"isHovering = true\"\n @dragleave"
},
{
"path": "src/components/GGanttTimeaxis.vue",
"chars": 2080,
"preview": "<template>\n <div class=\"g-timeaxis\">\n <div class=\"g-timeunits-container\">\n <div\n v-for=\"({ label, value,"
},
{
"path": "src/composables/createBarDrag.ts",
"chars": 3868,
"preview": "import { ref } from \"vue\"\nimport type { GGanttChartConfig } from \"../components/GGanttChart.vue\"\nimport provideConfig fr"
},
{
"path": "src/composables/useBarDragLimit.ts",
"chars": 6827,
"preview": "import type { GanttBarObject } from \"../types\"\nimport provideConfig from \"../provider/provideConfig.js\"\nimport provideGe"
},
{
"path": "src/composables/useBarDragManagement.ts",
"chars": 6818,
"preview": "import type { GanttBarObject } from \"../types\"\n\nimport createBarDrag from \"./createBarDrag.js\"\nimport useDayjsHelper fro"
},
{
"path": "src/composables/useDayjsHelper.ts",
"chars": 1522,
"preview": "import dayjs, { type Dayjs } from \"dayjs\"\nimport { computed } from \"vue\"\n\nimport type { GGanttChartConfig } from \"../com"
},
{
"path": "src/composables/useTimePositionMapping.ts",
"chars": 1125,
"preview": "import type { GGanttChartConfig } from \"../components/GGanttChart.vue\"\nimport { computed } from \"vue\"\n\nimport useDayjsHe"
},
{
"path": "src/composables/useTimeaxisUnits.ts",
"chars": 3196,
"preview": "import { computed } from \"vue\"\nimport useDayjsHelper from \"./useDayjsHelper.js\"\nimport provideConfig from \"../provider/p"
},
{
"path": "src/playground.ts",
"chars": 177,
"preview": "import { createApp } from \"vue\"\nimport Playground from \"./GanttPlayground.vue\"\nimport ganttastic from \"./vue-ganttastic."
},
{
"path": "src/provider/provideConfig.ts",
"chars": 233,
"preview": "import { inject } from \"vue\"\nimport { CONFIG_KEY } from \"./symbols.js\"\n\nexport default function provideConfig() {\n cons"
},
{
"path": "src/provider/provideEmitBarEvent.ts",
"chars": 279,
"preview": "import { inject } from \"vue\"\nimport { EMIT_BAR_EVENT_KEY } from \"./symbols.js\"\n\nexport default function provideEmitBarEv"
},
{
"path": "src/provider/provideGetChartRows.ts",
"chars": 271,
"preview": "import { inject } from \"vue\"\nimport { CHART_ROWS_KEY } from \"./symbols.js\"\n\nexport default function provideGetChartRows("
},
{
"path": "src/provider/symbols.ts",
"chars": 820,
"preview": "import type { InjectionKey, Ref } from \"vue\"\n\nimport type { GGanttChartConfig } from \"../components/GGanttChart.vue\"\nimp"
},
{
"path": "src/types.ts",
"chars": 433,
"preview": "import type { CSSProperties } from \"vue\"\n\nexport type GanttBarObject = {\n // eslint-disable-next-line @typescript-eslin"
},
{
"path": "src/vue-ganttastic.ts",
"chars": 1171,
"preview": "import type { Plugin } from \"vue\"\nimport dayjs from \"dayjs\"\nimport isoWeek from \"dayjs/plugin/isoWeek\"\nimport isSameOrBe"
},
{
"path": "tsconfig.config.json",
"chars": 196,
"preview": "{\n \"extends\": \"@vue/tsconfig/tsconfig.node.json\",\n \"include\": [\"vite.config.*\", \"vitest.config.*\", \"cypress.config.*\"]"
},
{
"path": "tsconfig.json",
"chars": 228,
"preview": "{\n \"extends\": \"@vue/tsconfig/tsconfig.web.json\",\n \"include\": [\"env.d.ts\", \"src/**/*\", \"src/**/*.vue\"],\n \"compilerOpti"
},
{
"path": "vite.config.mts",
"chars": 1181,
"preview": "import { fileURLToPath, URL } from \"node:url\"\n\nimport { defineConfig } from \"vite\"\nimport vue from \"@vitejs/plugin-vue\"\n"
}
]
About this extraction
This page contains the full source code of the InfectoOne/vue-ganttastic GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 49 files (98.2 KB), approximately 29.3k tokens, and a symbol index with 24 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.