├── .browserslistrc
├── .editorconfig
├── .eslintrc.cjs
├── .github
├── FUNDING.yml
└── workflows
│ └── deploy.yml
├── .gitignore
├── .prettierrc
├── README.md
├── deploy.sh
├── docs-deploy.yml
├── docs
├── .vitepress
│ ├── config.ts
│ ├── public
│ │ └── favicon.ico
│ └── theme
│ │ ├── custom.css
│ │ └── index.js
├── GGanttChart.md
├── GGanttRow.md
├── common-use-cases.md
├── examples.md
├── getting-started.md
├── index.md
└── introduction.md
├── env.d.ts
├── index.html
├── package-lock.json
├── package.json
├── public
└── favicon.ico
├── 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
/.browserslistrc:
--------------------------------------------------------------------------------
1 | > 1%
2 | last 2 versions
3 | not dead
4 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*.{js,jsx,ts,tsx,vue}]
2 | indent_style = space
3 | indent_size = 2
4 | trim_trailing_whitespace = true
5 | insert_final_newline = true
6 | max_line_length = 100
--------------------------------------------------------------------------------
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 | require("@rushstack/eslint-patch/modern-module-resolution")
3 |
4 | module.exports = {
5 | root: true,
6 |
7 | extends: [
8 | "plugin:vue/vue3-recommended",
9 | "eslint:recommended",
10 | "@vue/eslint-config-typescript/recommended",
11 | "@vue/eslint-config-prettier"
12 | ],
13 |
14 | rules: {
15 | "no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
16 | "no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
17 | "@typescript-eslint/no-unused-vars": "off",
18 | "@typescript-eslint/explicit-module-boundary-types": "off",
19 | quotes: ["error", "double"],
20 | // "object-curly-spacing": ["error", "never"],
21 | "prettier/prettier": ["error", {}, { usePrettierrc: true }]
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4 | patreon: # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | otechie: # Replace with a single Otechie username
12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
13 |
--------------------------------------------------------------------------------
/.github/workflows/deploy.yml:
--------------------------------------------------------------------------------
1 | # Sample workflow for building and deploying a VitePress site to GitHub Pages
2 | #
3 | name: Deploy VitePress site to Pages
4 |
5 | on:
6 | # Runs on pushes targeting the `main` branch. Change this to `master` if you're
7 | # using the `master` branch as the default branch.
8 | push:
9 | branches: [master]
10 |
11 | # Allows you to run this workflow manually from the Actions tab
12 | workflow_dispatch:
13 |
14 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
15 | permissions:
16 | contents: read
17 | pages: write
18 | id-token: write
19 |
20 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
21 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
22 | concurrency:
23 | group: pages
24 | cancel-in-progress: false
25 |
26 | jobs:
27 | # Build job
28 | build:
29 | runs-on: ubuntu-latest
30 | steps:
31 | - name: Checkout
32 | uses: actions/checkout@v4
33 | with:
34 | fetch-depth: 0 # Not needed if lastUpdated is not enabled
35 | # - uses: pnpm/action-setup@v3 # Uncomment this if you're using pnpm
36 | # - uses: oven-sh/setup-bun@v1 # Uncomment this if you're using Bun
37 | - name: Setup Node
38 | uses: actions/setup-node@v4
39 | with:
40 | node-version: 20
41 | cache: npm # or pnpm / yarn
42 | - name: Setup Pages
43 | uses: actions/configure-pages@v4
44 | - name: Install dependencies
45 | run: npm ci # or pnpm install / yarn install / bun install
46 | - name: Build with VitePress
47 | run: npm run docs:build # or pnpm docs:build / yarn docs:build / bun run docs:build
48 | - name: Upload artifact
49 | uses: actions/upload-pages-artifact@v3
50 | with:
51 | path: docs/.vitepress/dist
52 |
53 | # Deployment job
54 | deploy:
55 | environment:
56 | name: github-pages
57 | url: ${{ steps.deployment.outputs.page_url }}
58 | needs: build
59 | runs-on: ubuntu-latest
60 | name: Deploy
61 | steps:
62 | - name: Deploy to GitHub Pages
63 | id: deployment
64 | uses: actions/deploy-pages@v4
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /lib
4 | /lib_types
5 |
6 |
7 | # local env files
8 | .env.local
9 | .env.*.local
10 |
11 | # Log files
12 | npm-debug.log*
13 | yarn-debug.log*
14 | yarn-error.log*
15 | pnpm-debug.log*
16 |
17 | # Editor directories and files
18 | .idea
19 | .vscode
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
26 | # Docs dist directory
27 | docs/.vitepress/cache
28 | docs/.vitepress/temp
29 | docs/.vitepress/dist
30 |
31 | *.tgz
32 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": false,
3 | "trailingComma": "none",
4 | "endOfLine": "auto"
5 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Vue Ganttastic
2 |
3 |
4 |

9 |
10 |
Vue Ganttastic is a simple, interactive and highly customizable Gantt chart component for Vue 3.
11 |
12 | 
13 |
14 |
15 |
16 | ## Features
17 |
18 | - **[Vue 3](https://v3.vuejs.org/) support**
19 | - **[TypeScript](https://www.typescriptlang.org/) support** _(ships with out of the box type declarations)_
20 | - **Interactivity** _(dynamic, movable and pushable bars)_
21 | - **Reactivity / Responsiveness** (_when changes occur, bars are repositioned accordingly_)
22 | - **Customization options** (_chart/bar styling, slots, event handlers etc._)
23 |
24 | Using Vue 2? Check out [Vue-Ganttastic v1](https://github.com/zunnzunn/vue-ganttastic/tree/vue-ganttastic-v1).
25 |
26 | ## Guide and Docs
27 |
28 | For further guides and references, check out the [official docs](https://zunnzunn.github.io/vue-ganttastic/getting-started.html).
29 |
30 | ## Quickstart
31 |
32 | Install using
33 |
34 | ```
35 | npm install @infectoone/vue-ganttastic
36 | ```
37 |
38 | Then, initalize the plugin in the starting point of your app (most likely src/main.js):
39 |
40 | ```js
41 | import { createApp } from "vue"
42 | import App from "./App.vue"
43 | ...
44 | import ganttastic from '@infectoone/vue-ganttastic'
45 | ...
46 | createApp(App)
47 | .use(ganttastic)
48 | .mount('#app')
49 | ```
50 |
51 | 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.
52 |
53 | ```html
54 |
55 |
62 |
63 |
64 |
65 |
66 |
67 |
100 | ```
101 |
102 | ## Contributing
103 |
104 | 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!
105 |
106 | To test your changes out before creating a pull request, create a build:
107 |
108 | ```
109 | npm run build
110 | ```
111 |
112 | To test out the build, you should create a tarball using:
113 |
114 | ```
115 | npm pack
116 | ```
117 |
118 | Then, place the tarball in some other test project and install the package from the tarball by using:
119 |
120 | ```
121 | npm install .tgz
122 | ```
123 |
124 |
125 | ## About
126 |
127 | **License** [MIT](https://choosealicense.com/licenses/mit/)
128 | **Author**: Marko Žunić, BSc
129 | [GitHub Repository](https://github.com/zunnzunn/vue-ganttastic)
130 |
131 | ## Support the project!
132 |
133 | In case you found the library useful, a little tip would be much appreciated!
134 |
135 |
140 |
141 | BTC address:
142 | 
143 |
144 | ## Screenshots
145 |
146 | 
147 |
148 | 
149 |
150 | 
151 |
--------------------------------------------------------------------------------
/deploy.sh:
--------------------------------------------------------------------------------
1 | set -e
2 |
3 | npm run docs:build
4 | cd docs/.vuepress/dist
5 |
6 | git init
7 | git add -A
8 | git commit -m 'deploy'
9 |
10 | git push -f git@github.com:InfectoOne/vue-ganttastic.git master:gh-pages
11 |
12 | cd -
--------------------------------------------------------------------------------
/docs-deploy.yml:
--------------------------------------------------------------------------------
1 | # Sample workflow for building and deploying a VitePress site to GitHub Pages
2 | #
3 | name: Deploy VitePress site to Pages
4 |
5 | on:
6 | # Runs on pushes targeting the `main` branch. Change this to `master` if you're
7 | # using the `master` branch as the default branch.
8 | push:
9 | branches: [master]
10 |
11 | # Allows you to run this workflow manually from the Actions tab
12 | workflow_dispatch:
13 |
14 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
15 | permissions:
16 | contents: read
17 | pages: write
18 | id-token: write
19 |
20 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
21 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
22 | concurrency:
23 | group: pages
24 | cancel-in-progress: false
25 |
26 | jobs:
27 | # Build job
28 | build:
29 | runs-on: ubuntu-latest
30 | steps:
31 | - name: Checkout
32 | uses: actions/checkout@v3
33 | with:
34 | fetch-depth: 0 # Not needed if lastUpdated is not enabled
35 | # - uses: pnpm/action-setup@v2 # Uncomment this if you're using pnpm
36 | - name: Setup Node
37 | uses: actions/setup-node@v3
38 | with:
39 | node-version: 18
40 | cache: npm # or pnpm / yarn
41 | - name: Setup Pages
42 | uses: actions/configure-pages@v3
43 | - name: Install dependencies
44 | run: npm ci # or pnpm install / yarn install
45 | - name: Build with VitePress
46 | run: npm run docs:build # or pnpm docs:build / yarn docs:build
47 | - name: Upload artifact
48 | uses: actions/upload-pages-artifact@v2
49 | with:
50 | path: docs/.vitepress/dist
51 |
52 | # Deployment job
53 | deploy:
54 | environment:
55 | name: github-pages
56 | url: ${{ steps.deployment.outputs.page_url }}
57 | needs: build
58 | runs-on: ubuntu-latest
59 | name: Deploy
60 | steps:
61 | - name: Deploy to GitHub Pages
62 | id: deployment
63 | uses: actions/deploy-pages@v2
--------------------------------------------------------------------------------
/docs/.vitepress/config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vitepress'
2 |
3 | // https://vitepress.dev/reference/site-config
4 | export default defineConfig({
5 | lang: 'en-US',
6 | title: 'Vue-Ganttastic',
7 | description: 'Simple and customizable Gantt chart component for Vue 3.',
8 | base: '/vue-ganttastic/',
9 | head: [['link', { rel: 'icon', href: 'https://user-images.githubusercontent.com/28678851/148047714-301f07df-4101-48b8-9e47-1f272b290e80.png' }]],
10 | themeConfig: {
11 | logo: 'https://user-images.githubusercontent.com/28678851/148047714-301f07df-4101-48b8-9e47-1f272b290e80.png',
12 | nav: [
13 | { text: 'Home', link: '/' },
14 | ],
15 | sidebar: [
16 | { text: 'Introduction', link: '/introduction'},
17 | { text: 'Getting Started',link: '/getting-started' },
18 | { text: 'Common use cases', link: '/common-use-cases' },
19 | { text: 'Examples', link: '/examples' },
20 | {
21 | text: 'API Reference',
22 | items: [
23 | { text: 'GGanttChart', link: '/GGanttChart' },
24 | { text: 'GGanttRow', link: '/GGanttRow' }
25 | ]
26 | }
27 | ],
28 | socialLinks: [
29 | { icon: 'github', link: 'https://github.com/zunnzunn/vue-ganttastic' }
30 | ]
31 | }
32 | })
33 |
--------------------------------------------------------------------------------
/docs/.vitepress/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zunnzunn/vue-ganttastic/b06e161fba42c30886060448acde78ed540ee3e4/docs/.vitepress/public/favicon.ico
--------------------------------------------------------------------------------
/docs/.vitepress/theme/custom.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --vp-home-hero-name-color: #2fb585;
3 | --vp-button-brand-bg: #2fb585;
4 | --vp-button-brand-hover-bg: #354b5d;
5 | --vp-c-brand: #2fb585;
6 | }
--------------------------------------------------------------------------------
/docs/.vitepress/theme/index.js:
--------------------------------------------------------------------------------
1 | import DefaultTheme from 'vitepress/theme'
2 | import './custom.css'
3 | import {ganttastic} from "../../../src/vue-ganttastic"
4 |
5 | export default {
6 | extends: DefaultTheme,
7 | enhanceApp(ctx) {
8 | ctx.app.use(ganttastic)
9 |
10 | }
11 | }
--------------------------------------------------------------------------------
/docs/GGanttChart.md:
--------------------------------------------------------------------------------
1 | # API: GGanttChart
2 | The main component of Vue Ganttastic. Represents an entire chart and is meant to have at least one `g-gantt-row` child component.
3 | ## Props
4 | | Prop | Type | Default | Description |
5 | |-------------|---------|---------|------------------------------|
6 | | `chart-start` | string | | Start date-time of the chart.
7 | | `chart-end` | string | | End date-time of the chart.
8 | | `precision` | string? | `"hour"` | Display precision of the time-axis. Possible values: `hour`, `day`, `date`, `week` and `month`. |
9 | | `bar-start` | string | | Name of the property in bar objects that represents the start date.
10 | | `bar-end` | string | | Name of the property in bar objects that represents the end date .
11 | | `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`.
12 | | `width` | string? | `"100%"` | Width of the chart (e.g. `80%` or `800px`)
13 | | `hide-timeaxis` | boolean? | `false` | Toggle visibility of the time axis.
14 | | `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).
15 | | `grid` | string? | `false` | Toggle visibility of background grid.
16 | | `current-time` | boolean? | `false` | Toggle visibility of current time marker.
17 | | `current-time-label` | string? | `''` | Text to be displayed next to the current time marker.
18 | | `push-on-overlap` | boolean? | `false` | Specifies whether bars "push one another" when dragging and overlaping.
19 | | `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.
20 | | `row-height` | number? | `40` | Height of each row in pixels.
21 | | `highlighted-units` | number[]? | `[]` | The time units specified here will be visually highlighted in the chart with a mild opaque color.
22 | | `font` | string | `"Helvetica"`| Font family of the chart.
23 | | `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.
24 | | `label-column-width` | string? | `150px` | Width of the column containing the row labels (if `label-column-title` specified)
25 |
26 |
27 | ## Custom Events
28 | | Event name | Event data |
29 | |----------------------------|------------------------------------------------------------|
30 | | `click-bar` | `{bar: GanttBarObject, e: MouseEvent, datetime?: string}` |
31 | | `mousedown-bar` | `{bar: GanttBarObject, e: MouseEvent, datetime?: string}` |
32 | | `mouseup-bar` | `{bar: GanttBarObject, e: MouseEvent, datetime?: string}` |
33 | | `dblclick-bar` | `{bar: GanttBarObject, e: MouseEvent, datetime?: string}` |
34 | | `mouseenter-bar` | `{bar: GanttBarObject, e: MouseEvent}` |
35 | | `mouseleave-bar` | `{bar: GanttBarObject, e: MouseEvent}` |
36 | | `dragstart-bar` | `{bar: GanttBarObject, e: MouseEvent}` |
37 | | `drag-bar` | `{bar: GanttBarObject, e: MouseEvent}` |
38 | | `dragend-bar` | `{bar: GanttBarObject, e: MouseEvent, movedBars?: Map}` |
39 | | `contextmenu-bar` | `{bar: GanttBarObject, e: MouseEvent, datetime?: string}` |
40 |
41 |
42 | ## Slots
43 | | Slot name | Slot data | Description |
44 | |----------------------------|-----------------------| ----------------------------------------|
45 | | `upper-timeunit` | `{label: string, value: string}` | Content of an upper time-unit section in the time axis. |
46 | | `timeunit` | `{label: string, value: string}` | Content of a time-unit section in the time axis. |
47 | | `bar-tooltip` | `{bar: GanttBarObject}` | Slot for the tooltip which appears when hovering over a bar. |
48 | | `current-time-label` | | Slot for the text shown next to the current time marker when the prop `current-time` is set to `true`. |
49 | | `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. |
50 | | `label-column-row` | `{ label: string } ` | Slot for the label of a row if `label-column-title` is set. |
51 |
52 | ## Color Schemes
53 |
54 | List of pre-defined color schemes:
55 | - `default`
56 | - `creamy`
57 | - `crimson`
58 | - `dark`
59 | - `flare`
60 | - `fuchsia`
61 | - `grove`
62 | - `material-blue`
63 | - `sky`
64 | - `slumber`
65 | - `vue`
66 |
67 | You can also provide your own color scheme. Your custom color scheme should be an object of the following shape:
68 | ```typescript
69 | {
70 | primary: string,
71 | secondary: string,
72 | ternary: string,
73 | quartenary: string,
74 | hoverHighlight: string,
75 | markerCurrentTime: string,
76 | text: string,
77 | background: string,
78 | toast?: string
79 | }
80 | ```
--------------------------------------------------------------------------------
/docs/GGanttRow.md:
--------------------------------------------------------------------------------
1 | # API: GGanttRow
2 | Represents a single row of the chart. It is meant to be a child component of `g-gantt-chart`.
3 |
4 | ## Props
5 | | Prop | Type | Default | Description |
6 | |-------------|---------|---------|------------------------------|
7 | | `label` |`string?`| `""` | Text that is floating in the upper left corner of the row.
8 | | `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.
9 | | `highlight-on-hover` | `boolean?` | `false` | Used for toggling a background color transition effect on mouse hover.
10 |
11 | ## Custom Events
12 | | Event name | Event data |
13 | |----------------------------|------------------------------------------------------------|
14 | | `drop` | `{ e: MouseEvent, datetime: string}` |
15 |
16 |
17 | ## Slots
18 | | Slot name | Slot data | Description |
19 | |----------------------------|-----------------------| ----------------------------------------|
20 | | `label` | | Used for modifying the text that is floating in the upper left corner of the row. |
21 | | `bar-label` | `{bar: GanttBarObject}` | Used for modifying the text in a bar. |
--------------------------------------------------------------------------------
/docs/common-use-cases.md:
--------------------------------------------------------------------------------
1 | # Common use cases
2 | The following section provides a non-exhausting list of common use cases and special features of Vue Ganttastic with corresponding code snippets.
3 |
4 | ## Adding new bars
5 | 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`):
6 | ```vue
7 |
8 |
16 |
20 |
21 |
22 |
23 |
39 |
40 | ```
41 | ## Configuring and styling bars
42 | Your bar objects can be of any type and contain any properties you want. The only requirements are:
43 | - a datetime-string property for the bar start date
44 | - a datetime-string property for the bar end date
45 | - a nested object `ganttBarConfig` with a unique string property `id`
46 |
47 | For further configuration, you can add some optional properties to the nested `ganttBarConfig` object:
48 |
49 | | Property name | Type | Description |
50 | |---------------|---------|-----------------------|
51 | | `id` | `string` | A unique string identifier for the bar. (**mandatory**)
52 | | `label` | `string?` | Text displayed on the bar.
53 | | `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.
54 | | `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. |
55 | | `immobile` | `boolean?` | Used to toggle whether bar can be moved (dragged).
56 | | `bundle` | `string?` | A string identifier for a bundle. A bundle is a collection of bars that are dragged simultaneously.
57 | | `style` | `CSSStyleSheet?` | CSS-based styling for your bar (e.g `background`, `fontSize`, `borderRadius` etc.).
58 |
59 | ## Extending the width of a bar
60 | Simply add `hasHandles: true` to the `ganttBarConfig` to make the bar extendable by dragging the handles on its left or right side.
61 |
62 | ## Push bars when dragging
63 | 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:
64 | ```vue
65 |
70 | ...
71 |
72 | ```
73 |
74 | ## Bundling bars together
75 | 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.
76 |
77 | ## Custom behavior on clicking/dragging a bar
78 | 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`:
79 | ```vue
80 |
91 | ...
92 |
93 |
94 |
101 | ```
102 | ## Drag and drop
103 | 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.
104 | ```vue
105 |
108 |
113 |
114 |
115 |
122 | ```
123 |
124 | ## Time axis precision
125 | 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`.
126 |
127 | ## Chart themes
128 | 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)
129 |
130 | ## Locale
131 | 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.
132 |
133 |
--------------------------------------------------------------------------------
/docs/examples.md:
--------------------------------------------------------------------------------
1 |
2 | # Live Demos
3 |
4 | ## Simple hour chart
5 | - `precision`: `hour`
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | ## Day chart with dark theme
17 | - `precision`: `day`
18 | - `row-height` : `70`
19 | - `no-overlap`
20 | - `color-scheme` : `dark`
21 |
22 | Used slots:
23 | `g-gantt-row` > `label`, `bar-label`
24 |
25 |
26 |
27 |
28 | {{bar.ganttBarConfig.label}}
29 |
30 |
31 |
32 |
33 |
34 | Label with image
35 |
36 |
37 |
38 |
39 |
40 |
41 | ## Month chart pushing and bundles
42 | - `precision`: `month`
43 | - `push-on-overlap`
44 | - `color-scheme` : `vue`
45 | - `font` : `Courier`
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
209 |
210 |
222 |
--------------------------------------------------------------------------------
/docs/getting-started.md:
--------------------------------------------------------------------------------
1 | # Getting started
2 |
3 | ## Install
4 |
5 | You can add Vue Ganttastic to your project using npm:
6 |
7 | ```
8 | npm install @infectoone/vue-ganttastic
9 | ```
10 |
11 | Then, initalize the plugin in the starting point of your app (most likely `src/main.js`):
12 |
13 | ```javascript
14 | import { createApp } from "vue"
15 | import App from "./App.vue"
16 | ...
17 | import ganttastic from '@infectoone/vue-ganttastic'
18 | ...
19 | createApp(App)
20 | .use(ganttastic)
21 | .mount('#app')
22 | ```
23 |
24 | 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.
25 |
26 | ## Basic usage
27 |
28 | ```vue
29 |
30 |
37 |
38 |
39 |
40 |
41 |
42 |
74 | ```
75 |
76 | The result shoud look like this:
77 |
78 |
79 |
80 |
81 |
114 |
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | # https://vitepress.dev/reference/default-theme-home-page
3 | layout: home
4 |
5 | hero:
6 | name: Vue-Ganttastic
7 | text: Gantt chart component for Vue
8 | tagline: A simple, interactive and highly customizable Gantt chart component for Vue.js
9 | image:
10 | src: https://user-images.githubusercontent.com/28678851/148047714-301f07df-4101-48b8-9e47-1f272b290e80.png
11 | alt: Vue-Ganttastic logo
12 | actions:
13 | - theme: brand
14 | text: Get started
15 | link: /introduction
16 | - theme: alt
17 | text: API Reference
18 | link: /GGanttChart
19 |
20 | features:
21 | - title: Vue 3 and TypeScript support
22 | details: Written in Vue 3 and TypeScript. Ships with out-of-the-box type declarations.
23 | - title: Interactive
24 | details: Dynamic Gantt chart with movable bars and numerous event handlers.
25 | - title: Customizable
26 | details: Style the chart and each individual bar to your own liking!
27 | ---
28 |
29 |
--------------------------------------------------------------------------------
/docs/introduction.md:
--------------------------------------------------------------------------------
1 | # Introduction
2 | Vue Ganttastic is a simple, interactive and highly customizable Gantt chart component for Vue 3.
3 |
4 | ## Features
5 | - **[Vue 3](https://v3.vuejs.org/) support**
6 | - **[TypeScript](https://www.typescriptlang.org/) support** *(ships with out of the box type declarations)*
7 | - **Interactivity** *(dynamic, movable and pushable bars)*
8 | - **Reactivity / Responsiveness** (*when changes occur, bars are repositioned accordingly*)
9 | - **Customization options** (*chart/bar styling, slots, event handlers etc.*)
10 |
11 | ## About
12 | **License** [MIT](https://choosealicense.com/licenses/mit/)
13 | **Author**: Marko Žunić, BSc
14 | [GitHub Repository](https://github.com/InfectoOne/vue-ganttastic)
15 |
16 | ## Support the project!
17 | In case you found the library useful, a little tip would be much appreciated!
18 |
19 |
24 |
25 | BTC address:
26 | 
27 |
28 |
29 |
--------------------------------------------------------------------------------
/env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | @infectoone/vue-ganttastic
8 |
9 |
10 |
15 |
16 |
17 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@infectoone/vue-ganttastic",
3 | "version": "2.3.2",
4 | "description": "A simple and customizable Gantt chart component for Vue.js",
5 | "author": "Marko Zunic (@zunnzunn)",
6 | "scripts": {
7 | "serve": "vite",
8 | "build": "npm run build:types && npm run build:lib",
9 | "build:lib": "vite build",
10 | "build:types": "vue-tsc --declaration --emitDeclarationOnly --outDir lib_types",
11 | "typecheck": "vue-tsc --noEmit",
12 | "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --ignore-path .gitignore",
13 | "lint:fix": "npm run lint --fix",
14 | "docs:build": "vitepress build docs",
15 | "docs:dev": "vitepress dev docs",
16 | "docs:preview": "vitepress preview docs"
17 | },
18 | "type": "module",
19 | "exports": {
20 | ".": {
21 | "types": "./lib_types/vue-ganttastic.d.ts",
22 | "import": "./lib/vue-ganttastic.js",
23 | "require": "./lib/vue-ganttastic.umd.cjs"
24 | }
25 | },
26 | "main": "./lib/vue-ganttastic.umd.cjs",
27 | "types": "./lib_types/vue-ganttastic.d.ts",
28 | "files": [
29 | "lib_types",
30 | "lib/vue-ganttastic.js",
31 | "lib/vue-ganttastic.umd.cjs"
32 | ],
33 | "devDependencies": {
34 | "@rushstack/eslint-patch": "^1.2.0",
35 | "@senojs/rollup-plugin-style-inject": "^0.1.1",
36 | "@types/node": "^16.11.58",
37 | "@types/postcss-preset-env": "^7.7.0",
38 | "@vitejs/plugin-vue": "^3.1.2",
39 | "@vue/eslint-config-prettier": "^7.0.0",
40 | "@vue/eslint-config-typescript": "^11.0.2",
41 | "@vue/tsconfig": "^0.1.3",
42 | "@vuepress/plugin-search": "2.0.0-beta.51",
43 | "eslint": "^8.25.0",
44 | "eslint-plugin-vue": "^9.6.0",
45 | "npm-run-all": "^4.1.5",
46 | "postcss": "^8.4.17",
47 | "postcss-preset-env": "^7.8.2",
48 | "prettier": "^2.7.1",
49 | "typescript": "~4.8.4",
50 | "vite": "^3.1.6",
51 | "vitepress": "^1.0.0-rc.4",
52 | "vue-tsc": "^1.8.27"
53 | },
54 | "peerDependencies": {
55 | "dayjs": "^1.11.5",
56 | "vue": "^3.2.40"
57 | },
58 | "homepage": "https://zunnzunn.github.io/vue-ganttastic/",
59 | "keywords": [
60 | "gantt",
61 | "chart",
62 | "bar",
63 | "diagram",
64 | "vue",
65 | "vuejs",
66 | "ganttastic"
67 | ],
68 | "license": "MIT",
69 | "repository": {
70 | "type": "git",
71 | "url": "https://github.com/zunnzunn/vue-ganttastic"
72 | },
73 | "dependencies": {
74 | "@vueuse/core": "^9.1.1"
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zunnzunn/vue-ganttastic/b06e161fba42c30886060448acde78ed540ee3e4/public/favicon.ico
--------------------------------------------------------------------------------
/src/GanttPlayground.vue:
--------------------------------------------------------------------------------
1 |
2 |
23 |
24 |
25 |
26 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
238 |
--------------------------------------------------------------------------------
/src/color-schemes.ts:
--------------------------------------------------------------------------------
1 | import type * as CSS from "csstype"
2 |
3 | type Color = CSS.DataType.Color
4 |
5 | export type ColorScheme = {
6 | primary: Color
7 | secondary: Color
8 | ternary: Color
9 | quartenary: Color
10 | hoverHighlight: Color
11 | markerCurrentTime: Color
12 | text: Color
13 | background: Color
14 | toast?: Color
15 | }
16 |
17 | export const colorSchemes: Record = {
18 | default: {
19 | primary: "#eeeeee",
20 | secondary: "#E0E0E0",
21 | ternary: "#F5F5F5",
22 | quartenary: "#ededed",
23 | hoverHighlight: "rgba(204, 216, 219, 0.5)",
24 | markerCurrentTime: "#000",
25 | text: "#404040",
26 | background: "white"
27 | },
28 |
29 | creamy: {
30 | primary: "#ffe8d9",
31 | secondary: "#fcdcc5",
32 | ternary: "#fff6f0",
33 | quartenary: "#f7ece6",
34 | hoverHighlight: "rgba(230, 221, 202, 0.5)",
35 | markerCurrentTime: "#000",
36 | text: "#542d05",
37 | background: "white"
38 | },
39 |
40 | crimson: {
41 | primary: "#a82039",
42 | secondary: "#c41238",
43 | ternary: "#db4f56",
44 | quartenary: "#ce5f64",
45 | hoverHighlight: "rgba(196, 141, 141, 0.5)",
46 | markerCurrentTime: "#000",
47 | text: "white",
48 | background: "white"
49 | },
50 |
51 | dark: {
52 | primary: "#404040",
53 | secondary: "#303030",
54 | ternary: "#353535",
55 | quartenary: "#383838",
56 | hoverHighlight: "rgba(159, 160, 161, 0.5)",
57 | markerCurrentTime: "#fff",
58 | text: "white",
59 | background: "#525252",
60 | toast: "#1f1f1f"
61 | },
62 |
63 | flare: {
64 | primary: "#e08a38",
65 | secondary: "#e67912",
66 | ternary: "#5e5145",
67 | quartenary: "#665648",
68 | hoverHighlight: "rgba(196, 141, 141, 0.5)",
69 | markerCurrentTime: "#000",
70 | text: "white",
71 | background: "white"
72 | },
73 |
74 | fuchsia: {
75 | primary: "#de1d5a",
76 | secondary: "#b50b41",
77 | ternary: "#ff7da6",
78 | quartenary: "#f2799f",
79 | hoverHighlight: "rgba(196, 141, 141, 0.5)",
80 | markerCurrentTime: "#000",
81 | text: "white",
82 | background: "white"
83 | },
84 |
85 | grove: {
86 | primary: "#3d9960",
87 | secondary: "#288542",
88 | ternary: "#72b585",
89 | quartenary: "#65a577",
90 | hoverHighlight: "rgba(160, 219, 171, 0.5)",
91 | markerCurrentTime: "#000",
92 | text: "white",
93 | background: "white"
94 | },
95 |
96 | "material-blue": {
97 | primary: "#0D47A1",
98 | secondary: "#1565C0",
99 | ternary: "#42a5f5",
100 | quartenary: "#409fed",
101 | hoverHighlight: "rgba(110, 165, 196, 0.5)",
102 | markerCurrentTime: "#000",
103 | text: "white",
104 | background: "white"
105 | },
106 |
107 | sky: {
108 | primary: "#b5e3ff",
109 | secondary: "#a1d6f7",
110 | ternary: "#d6f7ff",
111 | quartenary: "#d0edf4",
112 | hoverHighlight: "rgba(193, 202, 214, 0.5)",
113 | markerCurrentTime: "#000",
114 | text: "#022c47",
115 | background: "white"
116 | },
117 |
118 | slumber: {
119 | primary: "#2a2f42",
120 | secondary: "#2f3447",
121 | ternary: "#35394d",
122 | quartenary: "#2c3044",
123 | hoverHighlight: "rgba(179, 162, 127, 0.5)",
124 | markerCurrentTime: "#fff",
125 | text: "#ffe0b3",
126 | background: "#38383b",
127 | toast: "#1f1f1f"
128 | },
129 |
130 | vue: {
131 | primary: "#258a5d",
132 | secondary: "#41B883",
133 | ternary: "#35495E",
134 | quartenary: "#2a3d51",
135 | hoverHighlight: "rgba(160, 219, 171, 0.5)",
136 | markerCurrentTime: "#000",
137 | text: "white",
138 | background: "white"
139 | }
140 | }
141 |
142 | export type ColorSchemeKey = keyof typeof colorSchemes
143 |
144 | export default colorSchemes
145 |
--------------------------------------------------------------------------------
/src/components/GGanttBar.vue:
--------------------------------------------------------------------------------
1 |
2 |
21 |
22 |
23 |
24 | {{ barConfig.label || "" }}
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
119 |
120 |
165 |
--------------------------------------------------------------------------------
/src/components/GGanttBarTooltip.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
18 |
19 |
20 |
21 |
22 |
88 |
89 |
135 |
--------------------------------------------------------------------------------
/src/components/GGanttChart.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
265 |
266 |
294 |
--------------------------------------------------------------------------------
/src/components/GGanttCurrentTime.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
14 |
15 |
16 | {{ currentTimeLabel }}
17 |
18 |
19 |
20 |
21 |
22 |
36 |
37 |
56 |
--------------------------------------------------------------------------------
/src/components/GGanttGrid.vue:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
15 |
26 |
27 |
44 |
--------------------------------------------------------------------------------
/src/components/GGanttLabelColumn.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ labelColumnTitle }}
6 |
7 |
8 |
9 |
18 |
19 | {{ label }}
20 |
21 |
22 |
23 |
24 |
25 |
26 |
33 |
34 |
80 |
--------------------------------------------------------------------------------
/src/components/GGanttRow.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
16 |
17 | {{ label }}
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
81 |
82 |
124 |
--------------------------------------------------------------------------------
/src/components/GGanttTimeaxis.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
14 |
15 | {{ label }}
16 |
17 |
18 |
19 |
20 |
21 |
33 |
34 | {{ label }}
35 |
36 |
41 |
42 |
43 |
44 |
45 |
46 |
53 |
54 |
92 |
--------------------------------------------------------------------------------
/src/composables/createBarDrag.ts:
--------------------------------------------------------------------------------
1 | import { ref } from "vue"
2 | import type { GGanttChartConfig } from "../components/GGanttChart.vue"
3 | import provideConfig from "../provider/provideConfig.js"
4 |
5 | import type { GanttBarObject } from "../types"
6 | import useDayjsHelper from "./useDayjsHelper.js"
7 | import useTimePositionMapping from "./useTimePositionMapping.js"
8 |
9 | export default function createBarDrag(
10 | bar: GanttBarObject,
11 | onDrag: (e: MouseEvent, bar: GanttBarObject) => void = () => null,
12 | onEndDrag: (e: MouseEvent, bar: GanttBarObject) => void = () => null,
13 | config: GGanttChartConfig = provideConfig()
14 | ) {
15 | const { barStart, barEnd, pushOnOverlap } = config
16 |
17 | const isDragging = ref(false)
18 | let cursorOffsetX = 0
19 | let dragCallBack: (e: MouseEvent) => void
20 |
21 | const { mapPositionToTime } = useTimePositionMapping(config)
22 | const { toDayjs } = useDayjsHelper(config)
23 |
24 | const initDrag = (e: MouseEvent) => {
25 | const barElement = document.getElementById(bar.ganttBarConfig.id)
26 | if (!barElement) {
27 | return
28 | }
29 |
30 | cursorOffsetX = e.clientX - (barElement.getBoundingClientRect().left || 0)
31 | const mousedownType = (e.target as Element).className
32 | switch (mousedownType) {
33 | case "g-gantt-bar-handle-left":
34 | document.body.style.cursor = "ew-resize"
35 | dragCallBack = dragByLeftHandle
36 | break
37 | case "g-gantt-bar-handle-right":
38 | document.body.style.cursor = "ew-resize"
39 | dragCallBack = dragByRightHandle
40 | break
41 | default:
42 | dragCallBack = drag
43 | }
44 | isDragging.value = true
45 | window.addEventListener("mousemove", dragCallBack)
46 | window.addEventListener("mouseup", endDrag)
47 | }
48 |
49 | const getBarElements = () => {
50 | const barElement = document.getElementById(bar.ganttBarConfig.id)
51 | const barContainer = barElement?.closest(".g-gantt-row-bars-container")?.getBoundingClientRect()
52 | return { barElement, barContainer }
53 | }
54 |
55 | const drag = (e: MouseEvent) => {
56 | const { barElement, barContainer } = getBarElements()
57 | if (!barElement || !barContainer) {
58 | return
59 | }
60 |
61 | const barWidth = barElement.getBoundingClientRect().width
62 | const xStart = e.clientX - barContainer.left - cursorOffsetX
63 | const xEnd = xStart + barWidth
64 | if (isOutOfRange(xStart, xEnd)) {
65 | return
66 | }
67 | bar[barStart.value] = mapPositionToTime(xStart)
68 | bar[barEnd.value] = mapPositionToTime(xEnd)
69 | onDrag(e, bar)
70 | }
71 |
72 | const dragByLeftHandle = (e: MouseEvent) => {
73 | const { barElement, barContainer } = getBarElements()
74 | if (!barElement || !barContainer) {
75 | return
76 | }
77 |
78 | const xStart = e.clientX - barContainer.left
79 | const newBarStart = mapPositionToTime(xStart)
80 | if (toDayjs(newBarStart).isSameOrAfter(toDayjs(bar, "end"))) {
81 | return
82 | }
83 | bar[barStart.value] = newBarStart
84 | onDrag(e, bar)
85 | }
86 |
87 | const dragByRightHandle = (e: MouseEvent) => {
88 | const { barElement, barContainer } = getBarElements()
89 | if (!barElement || !barContainer) {
90 | return
91 | }
92 |
93 | const xEnd = e.clientX - barContainer.left
94 | const newBarEnd = mapPositionToTime(xEnd)
95 | if (toDayjs(newBarEnd).isSameOrBefore(toDayjs(bar, "start"))) {
96 | return
97 | }
98 | bar[barEnd.value] = newBarEnd
99 | onDrag(e, bar)
100 | }
101 |
102 | const isOutOfRange = (xStart?: number, xEnd?: number) => {
103 | if (!pushOnOverlap.value) {
104 | return false
105 | }
106 | const dragLimitLeft = bar.ganttBarConfig.dragLimitLeft
107 | const dragLimitRight = bar.ganttBarConfig.dragLimitRight
108 |
109 | return (
110 | (xStart && dragLimitLeft != null && xStart < dragLimitLeft) ||
111 | (xEnd && dragLimitRight != null && xEnd > dragLimitRight)
112 | )
113 | }
114 |
115 | const endDrag = (e: MouseEvent) => {
116 | isDragging.value = false
117 | document.body.style.cursor = ""
118 | window.removeEventListener("mousemove", dragCallBack)
119 | window.removeEventListener("mouseup", endDrag)
120 | onEndDrag(e, bar)
121 | }
122 |
123 | return {
124 | isDragging,
125 | initDrag
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/src/composables/useBarDragLimit.ts:
--------------------------------------------------------------------------------
1 | import type { GanttBarObject } from "../types"
2 | import provideConfig from "../provider/provideConfig.js"
3 | import provideGetChartRows from "../provider/provideGetChartRows.js"
4 |
5 | export default function useBarDragLimit() {
6 | const { pushOnOverlap } = provideConfig()
7 | const getChartRows = provideGetChartRows()
8 |
9 | const getBarsFromBundle = (bundle?: string) => {
10 | const res: GanttBarObject[] = []
11 | if (bundle != null) {
12 | getChartRows().forEach((row) => {
13 | row.bars.forEach((bar) => {
14 | if (bar.ganttBarConfig.bundle === bundle) {
15 | res.push(bar)
16 | }
17 | })
18 | })
19 | }
20 | return res
21 | }
22 |
23 | const setDragLimitsOfGanttBar = (bar: GanttBarObject) => {
24 | if (!pushOnOverlap.value || bar.ganttBarConfig.pushOnOverlap === false) {
25 | return
26 | }
27 | for (const sideValue of ["left", "right"]) {
28 | const side = sideValue as "left" | "right"
29 | const { gapDistanceSoFar, bundleBarsAndGapDist } = countGapDistanceToNextImmobileBar(
30 | bar,
31 | 0,
32 | side
33 | )
34 | let totalGapDistance = gapDistanceSoFar
35 | const bundleBarsOnPath = bundleBarsAndGapDist
36 | if (!bundleBarsOnPath) {
37 | continue
38 | }
39 |
40 | for (let i = 0; i < bundleBarsOnPath.length; i++) {
41 | const barFromBundle = bundleBarsOnPath[i].bar
42 | const gapDist = bundleBarsOnPath[i].gapDistance
43 | const otherBarsFromBundle = getBarsFromBundle(barFromBundle.ganttBarConfig.bundle).filter(
44 | (otherBar) => otherBar !== barFromBundle
45 | )
46 | otherBarsFromBundle.forEach((otherBar) => {
47 | const nextGapDistanceAndBars = countGapDistanceToNextImmobileBar(otherBar, gapDist, side)
48 | const newGapDistance = nextGapDistanceAndBars.gapDistanceSoFar
49 | const newBundleBars = nextGapDistanceAndBars.bundleBarsAndGapDist
50 | if (newGapDistance != null && (!totalGapDistance || newGapDistance < totalGapDistance)) {
51 | totalGapDistance = newGapDistance
52 | }
53 | newBundleBars.forEach((newBundleBar) => {
54 | if (!bundleBarsOnPath.find((barAndGap) => barAndGap.bar === newBundleBar.bar)) {
55 | bundleBarsOnPath.push(newBundleBar)
56 | }
57 | })
58 | })
59 | }
60 | const barElem = document.getElementById(bar.ganttBarConfig.id) as HTMLElement
61 | if (totalGapDistance != null && side === "left") {
62 | bar.ganttBarConfig.dragLimitLeft = barElem.offsetLeft - totalGapDistance
63 | } else if (totalGapDistance != null && side === "right") {
64 | bar.ganttBarConfig.dragLimitRight =
65 | barElem.offsetLeft + barElem.offsetWidth + totalGapDistance
66 | }
67 | }
68 | // all bars from the bundle of the clicked bar need to have the same drag limit:
69 | const barsFromBundleOfClickedBar = getBarsFromBundle(bar.ganttBarConfig.bundle)
70 | barsFromBundleOfClickedBar.forEach((barFromBundle) => {
71 | barFromBundle.ganttBarConfig.dragLimitLeft = bar.ganttBarConfig.dragLimitLeft
72 | barFromBundle.ganttBarConfig.dragLimitRight = bar.ganttBarConfig.dragLimitRight
73 | })
74 | }
75 |
76 | // returns the gap distance to the next immobile bar
77 | // in the row where the given bar (parameter) is (added to gapDistanceSoFar)
78 | // and a list of all bars on that path that belong to a bundle
79 | const countGapDistanceToNextImmobileBar = (
80 | bar: GanttBarObject,
81 | gapDistanceSoFar = 0,
82 | side: "left" | "right"
83 | ) => {
84 | const bundleBarsAndGapDist = bar.ganttBarConfig.bundle
85 | ? [{ bar, gapDistance: gapDistanceSoFar }]
86 | : []
87 | let currentBar = bar
88 | let nextBar = getNextGanttBar(currentBar, side)
89 | // left side:
90 | if (side === "left") {
91 | while (nextBar) {
92 | const currentBarElem = document.getElementById(currentBar.ganttBarConfig.id) as HTMLElement
93 | const nextBarElem = document.getElementById(nextBar.ganttBarConfig.id) as HTMLElement
94 | const nextBarOffsetRight = nextBarElem.offsetLeft + nextBarElem.offsetWidth
95 | gapDistanceSoFar += currentBarElem.offsetLeft - nextBarOffsetRight
96 | if (nextBar.ganttBarConfig.immobile) {
97 | return { gapDistanceSoFar, bundleBarsAndGapDist }
98 | } else if (nextBar.ganttBarConfig.bundle) {
99 | bundleBarsAndGapDist.push({
100 | bar: nextBar,
101 | gapDistance: gapDistanceSoFar
102 | })
103 | }
104 | currentBar = nextBar
105 | nextBar = getNextGanttBar(nextBar, "left")
106 | }
107 | }
108 | if (side === "right") {
109 | while (nextBar) {
110 | const currentBarElem = document.getElementById(currentBar.ganttBarConfig.id) as HTMLElement
111 | const nextBarElem = document.getElementById(nextBar.ganttBarConfig.id) as HTMLElement
112 | const currentBarOffsetRight = currentBarElem.offsetLeft + currentBarElem.offsetWidth
113 | gapDistanceSoFar += nextBarElem.offsetLeft - currentBarOffsetRight
114 | if (nextBar.ganttBarConfig.immobile) {
115 | return { gapDistanceSoFar, bundleBarsAndGapDist }
116 | } else if (nextBar.ganttBarConfig.bundle) {
117 | bundleBarsAndGapDist.push({
118 | bar: nextBar,
119 | gapDistance: gapDistanceSoFar
120 | })
121 | }
122 | currentBar = nextBar
123 | nextBar = getNextGanttBar(nextBar, "right")
124 | }
125 | }
126 | return { gapDistanceSoFar: null, bundleBarsAndGapDist }
127 | }
128 |
129 | const getNextGanttBar = (bar: GanttBarObject, side: "left" | "right") => {
130 | const barElem = document.getElementById(bar.ganttBarConfig.id) as HTMLElement
131 | const allBarsInRow = getChartRows().find((row) => row.bars.includes(bar))?.bars ?? []
132 | let allBarsLeftOrRight = []
133 | if (side === "left") {
134 | allBarsLeftOrRight = allBarsInRow.filter((otherBar) => {
135 | const otherBarElem = document.getElementById(otherBar.ganttBarConfig.id) as HTMLElement
136 | return (
137 | otherBarElem &&
138 | otherBarElem.offsetLeft < barElem.offsetLeft &&
139 | otherBar.ganttBarConfig.pushOnOverlap !== false
140 | )
141 | })
142 | } else {
143 | allBarsLeftOrRight = allBarsInRow.filter((otherBar) => {
144 | const otherBarElem = document.getElementById(otherBar.ganttBarConfig.id) as HTMLElement
145 | return (
146 | otherBarElem &&
147 | otherBarElem.offsetLeft > barElem.offsetLeft &&
148 | otherBar.ganttBarConfig.pushOnOverlap !== false
149 | )
150 | })
151 | }
152 | if (allBarsLeftOrRight.length > 0) {
153 | return allBarsLeftOrRight.reduce((bar1, bar2) => {
154 | const bar1Elem = document.getElementById(bar1.ganttBarConfig.id) as HTMLElement
155 | const bar2Elem = document.getElementById(bar2.ganttBarConfig.id) as HTMLElement
156 | const bar1Dist = Math.abs(bar1Elem.offsetLeft - barElem.offsetLeft)
157 | const bar2Dist = Math.abs(bar2Elem.offsetLeft - barElem.offsetLeft)
158 | return bar1Dist < bar2Dist ? bar1 : bar2
159 | }, allBarsLeftOrRight[0])
160 | } else {
161 | return null
162 | }
163 | }
164 |
165 | return {
166 | setDragLimitsOfGanttBar
167 | }
168 | }
169 |
--------------------------------------------------------------------------------
/src/composables/useBarDragManagement.ts:
--------------------------------------------------------------------------------
1 | import type { GanttBarObject } from "../types"
2 |
3 | import createBarDrag from "./createBarDrag.js"
4 | import useDayjsHelper from "./useDayjsHelper.js"
5 | import provideConfig from "../provider/provideConfig.js"
6 | import provideGetChartRows from "../provider/provideGetChartRows.js"
7 | import provideEmitBarEvent from "../provider/provideEmitBarEvent.js"
8 |
9 | export default function useBarDragManagement() {
10 | const config = provideConfig()
11 | const getChartRows = provideGetChartRows()
12 | const emitBarEvent = provideEmitBarEvent()
13 | const { pushOnOverlap, barStart, barEnd, noOverlap, dateFormat } = config
14 |
15 | const movedBarsInDrag = new Map()
16 |
17 | const { toDayjs, format } = useDayjsHelper()
18 |
19 | const initDragOfBar = (bar: GanttBarObject, e: MouseEvent) => {
20 | const { initDrag } = createBarDrag(bar, onDrag, onEndDrag, config)
21 | emitBarEvent({ ...e, type: "dragstart" }, bar)
22 | initDrag(e)
23 | addBarToMovedBars(bar)
24 | }
25 |
26 | const initDragOfBundle = (mainBar: GanttBarObject, e: MouseEvent) => {
27 | const bundle = mainBar.ganttBarConfig.bundle
28 | if (bundle == null) {
29 | return
30 | }
31 | getChartRows().forEach((row) => {
32 | row.bars.forEach((bar) => {
33 | if (bar.ganttBarConfig.bundle === bundle) {
34 | const dragEndHandler = bar === mainBar ? onEndDrag : () => null
35 | const { initDrag } = createBarDrag(bar, onDrag, dragEndHandler, config)
36 | initDrag(e)
37 | addBarToMovedBars(bar)
38 | }
39 | })
40 | })
41 | emitBarEvent({ ...e, type: "dragstart" }, mainBar)
42 | }
43 |
44 | const onDrag = (e: MouseEvent, bar: GanttBarObject) => {
45 | emitBarEvent({ ...e, type: "drag" }, bar)
46 | fixOverlaps(bar)
47 | }
48 |
49 | const fixOverlaps = (ganttBar: GanttBarObject) => {
50 | if (!pushOnOverlap?.value) {
51 | return
52 | }
53 | let currentBar = ganttBar
54 | let { overlapBar, overlapType } = getOverlapBarAndType(currentBar)
55 | while (overlapBar) {
56 | addBarToMovedBars(overlapBar)
57 | const currentBarStart = toDayjs(currentBar[barStart.value])
58 | const currentBarEnd = toDayjs(currentBar[barEnd.value])
59 | const overlapBarStart = toDayjs(overlapBar[barStart.value])
60 | const overlapBarEnd = toDayjs(overlapBar[barEnd.value])
61 | let minuteDiff: number
62 | switch (overlapType) {
63 | case "left":
64 | minuteDiff = overlapBarEnd.diff(currentBarStart, "minutes", true)
65 | overlapBar[barEnd.value] = format(currentBar[barStart.value], dateFormat.value)
66 | overlapBar[barStart.value] = format(
67 | overlapBarStart.subtract(minuteDiff, "minutes"),
68 | dateFormat.value
69 | )
70 | break
71 | case "right":
72 | minuteDiff = currentBarEnd.diff(overlapBarStart, "minutes", true)
73 | overlapBar[barStart.value] = format(currentBarEnd, dateFormat.value)
74 | overlapBar[barEnd.value] = format(
75 | overlapBarEnd.add(minuteDiff, "minutes"),
76 | dateFormat.value
77 | )
78 | break
79 | default:
80 | console.warn(
81 | "Vue-Ganttastic: One bar is inside of the other one! This should never occur while push-on-overlap is active!"
82 | )
83 | return
84 | }
85 | if (overlapBar && (overlapType === "left" || overlapType === "right")) {
86 | moveBundleOfPushedBarByMinutes(overlapBar, minuteDiff, overlapType)
87 | }
88 | currentBar = overlapBar
89 | ;({ overlapBar, overlapType } = getOverlapBarAndType(overlapBar))
90 | }
91 | }
92 |
93 | const getOverlapBarAndType = (ganttBar: GanttBarObject) => {
94 | let overlapLeft, overlapRight, overlapInBetween
95 | const allBarsInRow = getChartRows().find((row) => row.bars.includes(ganttBar))?.bars ?? []
96 | const ganttBarStart = toDayjs(ganttBar[barStart.value])
97 | const ganttBarEnd = toDayjs(ganttBar[barEnd.value])
98 | const overlapBar = allBarsInRow.find((otherBar) => {
99 | if (otherBar === ganttBar) {
100 | return false
101 | }
102 | const otherBarStart = toDayjs(otherBar[barStart.value])
103 | const otherBarEnd = toDayjs(otherBar[barEnd.value])
104 | overlapLeft = ganttBarStart.isBetween(otherBarStart, otherBarEnd)
105 | overlapRight = ganttBarEnd.isBetween(otherBarStart, otherBarEnd)
106 | overlapInBetween =
107 | otherBarStart.isBetween(ganttBarStart, ganttBarEnd) ||
108 | otherBarEnd.isBetween(ganttBarStart, ganttBarEnd)
109 | return overlapLeft || overlapRight || overlapInBetween
110 | })
111 | const overlapType = overlapLeft
112 | ? "left"
113 | : overlapRight
114 | ? "right"
115 | : overlapInBetween
116 | ? "between"
117 | : null
118 | return { overlapBar, overlapType }
119 | }
120 |
121 | const moveBundleOfPushedBarByMinutes = (
122 | pushedBar: GanttBarObject,
123 | minutes: number,
124 | direction: "left" | "right"
125 | ) => {
126 | addBarToMovedBars(pushedBar)
127 | if (!pushedBar.ganttBarConfig.bundle) {
128 | return
129 | }
130 | getChartRows().forEach((row) => {
131 | row.bars.forEach((bar) => {
132 | if (bar.ganttBarConfig.bundle === pushedBar.ganttBarConfig.bundle && bar !== pushedBar) {
133 | addBarToMovedBars(bar)
134 | moveBarByMinutes(bar, minutes, direction)
135 | }
136 | })
137 | })
138 | }
139 |
140 | const moveBarByMinutes = (bar: GanttBarObject, minutes: number, direction: "left" | "right") => {
141 | switch (direction) {
142 | case "left":
143 | bar[barStart.value] = format(
144 | toDayjs(bar, "start").subtract(minutes, "minutes"),
145 | dateFormat.value
146 | )
147 | bar[barEnd.value] = format(
148 | toDayjs(bar, "end").subtract(minutes, "minutes"),
149 | dateFormat.value
150 | )
151 | break
152 | case "right":
153 | bar[barStart.value] = format(
154 | toDayjs(bar, "start").add(minutes, "minutes"),
155 | dateFormat.value
156 | )
157 | bar[barEnd.value] = format(toDayjs(bar, "end").add(minutes, "minutes"), dateFormat.value)
158 | }
159 | fixOverlaps(bar)
160 | }
161 |
162 | const onEndDrag = (e: MouseEvent, bar: GanttBarObject) => {
163 | snapBackAllMovedBarsIfNeeded()
164 | const ev = {
165 | ...e,
166 | type: "dragend"
167 | }
168 | emitBarEvent(ev, bar, undefined, new Map(movedBarsInDrag))
169 | movedBarsInDrag.clear()
170 | }
171 |
172 | const addBarToMovedBars = (bar: GanttBarObject) => {
173 | if (!movedBarsInDrag.has(bar)) {
174 | const oldStart = bar[barStart.value]
175 | const oldEnd = bar[barEnd.value]
176 | movedBarsInDrag.set(bar, { oldStart, oldEnd })
177 | }
178 | }
179 |
180 | const snapBackAllMovedBarsIfNeeded = () => {
181 | if (pushOnOverlap.value || !noOverlap.value) {
182 | return
183 | }
184 |
185 | let isAnyOverlap = false
186 | movedBarsInDrag.forEach((_, bar) => {
187 | const { overlapBar } = getOverlapBarAndType(bar)
188 | if (overlapBar != null) {
189 | isAnyOverlap = true
190 | }
191 | })
192 | if (!isAnyOverlap) {
193 | return
194 | }
195 | movedBarsInDrag.forEach(({ oldStart, oldEnd }, bar) => {
196 | bar[barStart.value] = oldStart
197 | bar[barEnd.value] = oldEnd
198 | })
199 | }
200 |
201 | return {
202 | initDragOfBar,
203 | initDragOfBundle
204 | }
205 | }
206 |
--------------------------------------------------------------------------------
/src/composables/useDayjsHelper.ts:
--------------------------------------------------------------------------------
1 | import dayjs, { type Dayjs } from "dayjs"
2 | import { computed } from "vue"
3 |
4 | import type { GGanttChartConfig } from "../components/GGanttChart.vue"
5 | import type { GanttBarObject } from "../types"
6 | import provideConfig from "../provider/provideConfig.js"
7 |
8 | export const DEFAULT_DATE_FORMAT = "YYYY-MM-DD HH:mm"
9 |
10 | export default function useDayjsHelper(config: GGanttChartConfig = provideConfig()) {
11 | const { chartStart, chartEnd, barStart, barEnd, dateFormat } = config
12 |
13 | const chartStartDayjs = computed(() => toDayjs(chartStart.value))
14 | const chartEndDayjs = computed(() => toDayjs(chartEnd.value))
15 |
16 | const toDayjs = (input: string | Date | GanttBarObject, startOrEnd?: "start" | "end") => {
17 | let value
18 | if (startOrEnd !== undefined && typeof input !== "string" && !(input instanceof Date)) {
19 | value = startOrEnd === "start" ? input[barStart.value] : input[barEnd.value]
20 | }
21 | if (typeof input === "string") {
22 | value = input
23 | } else if (input instanceof Date) {
24 | return dayjs(input)
25 | }
26 | const format = dateFormat.value || DEFAULT_DATE_FORMAT
27 | return dayjs(value, format, true)
28 | }
29 |
30 | const format = (input: string | Date | Dayjs, pattern?: string | false) => {
31 | if (pattern === false) {
32 | return input instanceof Date ? input : dayjs(input).toDate()
33 | }
34 | const inputDayjs = typeof input === "string" || input instanceof Date ? toDayjs(input) : input
35 |
36 | return inputDayjs.format(pattern)
37 | }
38 |
39 | return {
40 | chartStartDayjs,
41 | chartEndDayjs,
42 | toDayjs,
43 | format
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/composables/useTimePositionMapping.ts:
--------------------------------------------------------------------------------
1 | import type { GGanttChartConfig } from "../components/GGanttChart.vue"
2 | import { computed } from "vue"
3 |
4 | import useDayjsHelper from "./useDayjsHelper.js"
5 | import provideConfig from "../provider/provideConfig.js"
6 |
7 | export default function useTimePositionMapping(config: GGanttChartConfig = provideConfig()) {
8 | const { dateFormat, chartSize } = config
9 | const { chartStartDayjs, chartEndDayjs, toDayjs, format } = useDayjsHelper(config)
10 |
11 | const totalNumOfMinutes = computed(() => {
12 | return chartEndDayjs.value.diff(chartStartDayjs.value, "minutes")
13 | })
14 |
15 | const mapTimeToPosition = (time: string) => {
16 | const width = chartSize.width.value || 0
17 | const diffFromStart = toDayjs(time).diff(chartStartDayjs.value, "minutes", true)
18 | return Math.ceil((diffFromStart / totalNumOfMinutes.value) * width)
19 | }
20 |
21 | const mapPositionToTime = (xPos: number) => {
22 | const width = chartSize.width.value || 0
23 | const diffFromStart = (xPos / width) * totalNumOfMinutes.value
24 | return format(chartStartDayjs.value.add(diffFromStart, "minutes"), dateFormat.value)
25 | }
26 |
27 | return {
28 | mapTimeToPosition,
29 | mapPositionToTime
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/composables/useTimeaxisUnits.ts:
--------------------------------------------------------------------------------
1 | import { computed } from "vue"
2 | import useDayjsHelper from "./useDayjsHelper.js"
3 | import provideConfig from "../provider/provideConfig.js"
4 |
5 | export default function useTimeaxisUnits() {
6 | const { precision } = provideConfig()
7 | const { chartStartDayjs, chartEndDayjs } = useDayjsHelper()
8 |
9 | const upperPrecision = computed(() => {
10 | switch (precision?.value) {
11 | case "hour":
12 | return "day"
13 | case "day":
14 | return "month"
15 | case "date":
16 | case "week":
17 | return "month"
18 | case "month":
19 | return "year"
20 | default:
21 | throw new Error(
22 | "Precision prop incorrect. Must be one of the following: 'hour', 'day', 'date', 'week', 'month'"
23 | )
24 | }
25 | })
26 |
27 | const lowerPrecision = computed(() => {
28 | switch (precision.value) {
29 | case "date":
30 | return "day"
31 | case "week":
32 | return "isoWeek"
33 | default:
34 | return precision.value
35 | }
36 | })
37 |
38 | const displayFormats = {
39 | hour: "HH",
40 | date: "DD.MMM",
41 | day: "DD.MMM",
42 | week: "WW",
43 | month: "MMMM YYYY",
44 | year: "YYYY"
45 | }
46 |
47 | const timeaxisUnits = computed(() => {
48 | const upperUnits: { label: string; value?: string; date: Date; width?: string }[] = []
49 | const lowerUnits: { label: string; value?: string; date: Date; width?: string }[] = []
50 | const totalMinutes = chartEndDayjs.value.diff(chartStartDayjs.value, "minutes", true)
51 | const upperUnit = upperPrecision.value
52 | const lowerUnit = lowerPrecision.value
53 | let currentUpperUnit = chartStartDayjs.value
54 | let currentLowerUnit = chartStartDayjs.value
55 |
56 | while (currentLowerUnit.isSameOrBefore(chartEndDayjs.value)) {
57 | const endCurrentLowerUnit = currentLowerUnit.endOf(lowerUnit)
58 | const isLastItem = endCurrentLowerUnit.isAfter(chartEndDayjs.value)
59 |
60 | const lowerWidth = isLastItem
61 | ? (chartEndDayjs.value.diff(currentLowerUnit, "minutes", true) / totalMinutes) * 100
62 | : (endCurrentLowerUnit.diff(currentLowerUnit, "minutes", true) / totalMinutes) * 100
63 |
64 | lowerUnits.push({
65 | label: currentLowerUnit.format(displayFormats[precision?.value]),
66 | value: String(currentLowerUnit),
67 | date: currentLowerUnit.toDate(),
68 | width: String(lowerWidth) + "%"
69 | })
70 | currentLowerUnit = endCurrentLowerUnit
71 | .add(1, lowerUnit === "isoWeek" ? "week" : lowerUnit)
72 | .startOf(lowerUnit)
73 | }
74 | while (currentUpperUnit.isSameOrBefore(chartEndDayjs.value)) {
75 | const endCurrentUpperUnit = currentUpperUnit.endOf(upperUnit)
76 | const isLastItem = endCurrentUpperUnit.isAfter(chartEndDayjs.value)
77 |
78 | const upperWidth = isLastItem
79 | ? (chartEndDayjs.value.diff(currentUpperUnit, "minutes", true) / totalMinutes) * 100
80 | : (endCurrentUpperUnit.diff(currentUpperUnit, "minutes", true) / totalMinutes) * 100
81 |
82 | upperUnits.push({
83 | label: currentUpperUnit.format(displayFormats[upperUnit]),
84 | value: String(currentUpperUnit),
85 | date: currentUpperUnit.toDate(),
86 | width: String(upperWidth) + "%"
87 | })
88 |
89 | currentUpperUnit = endCurrentUpperUnit.add(1, upperUnit).startOf(upperUnit)
90 | }
91 | return { upperUnits, lowerUnits }
92 | })
93 |
94 | return {
95 | timeaxisUnits
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/src/playground.ts:
--------------------------------------------------------------------------------
1 | import { createApp } from "vue"
2 | import Playground from "./GanttPlayground.vue"
3 | import ganttastic from "./vue-ganttastic.js"
4 |
5 | createApp(Playground).use(ganttastic).mount("#app")
6 |
--------------------------------------------------------------------------------
/src/provider/provideConfig.ts:
--------------------------------------------------------------------------------
1 | import { inject } from "vue"
2 | import { CONFIG_KEY } from "./symbols.js"
3 |
4 | export default function provideConfig() {
5 | const config = inject(CONFIG_KEY)
6 | if (!config) {
7 | throw Error("Failed to inject config!")
8 | }
9 | return config
10 | }
11 |
--------------------------------------------------------------------------------
/src/provider/provideEmitBarEvent.ts:
--------------------------------------------------------------------------------
1 | import { inject } from "vue"
2 | import { EMIT_BAR_EVENT_KEY } from "./symbols.js"
3 |
4 | export default function provideEmitBarEvent() {
5 | const emitBarEvent = inject(EMIT_BAR_EVENT_KEY)
6 | if (!emitBarEvent) {
7 | throw Error("Failed to inject emitBarEvent!")
8 | }
9 | return emitBarEvent
10 | }
11 |
--------------------------------------------------------------------------------
/src/provider/provideGetChartRows.ts:
--------------------------------------------------------------------------------
1 | import { inject } from "vue"
2 | import { CHART_ROWS_KEY } from "./symbols.js"
3 |
4 | export default function provideGetChartRows() {
5 | const getChartRows = inject(CHART_ROWS_KEY)
6 | if (!getChartRows) {
7 | throw Error("Failed to inject getChartRows!")
8 | }
9 | return getChartRows
10 | }
11 |
--------------------------------------------------------------------------------
/src/provider/symbols.ts:
--------------------------------------------------------------------------------
1 | import type { InjectionKey, Ref } from "vue"
2 |
3 | import type { GGanttChartConfig } from "../components/GGanttChart.vue"
4 | import type { GanttBarObject } from "../types"
5 |
6 | export type ChartRow = { label: string; bars: GanttBarObject[] }
7 | export type GetChartRows = () => ChartRow[]
8 | export type EmitBarEvent = (
9 | e: MouseEvent,
10 | bar: GanttBarObject,
11 | datetime?: string | Date,
12 | movedBars?: Map
13 | ) => void
14 |
15 | export const CHART_ROWS_KEY = Symbol("CHART_ROWS_KEY") as InjectionKey
16 | export const CONFIG_KEY = Symbol("CONFIG_KEY") as InjectionKey
17 | export const EMIT_BAR_EVENT_KEY = Symbol("EMIT_BAR_EVENT_KEY") as InjectionKey
18 | export const BAR_CONTAINER_KEY = Symbol("BAR_CONTAINER_KEY") as InjectionKey<
19 | Ref
20 | >
21 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | import type { CSSProperties } from "vue"
2 |
3 | export type GanttBarObject = {
4 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
5 | [key: string]: any
6 | ganttBarConfig: {
7 | id: string
8 | label?: string
9 | html?: string
10 | hasHandles?: boolean
11 | immobile?: boolean
12 | bundle?: string
13 | pushOnOverlap?: boolean
14 | dragLimitLeft?: number
15 | dragLimitRight?: number
16 | style?: CSSProperties
17 | class?: string
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/vue-ganttastic.ts:
--------------------------------------------------------------------------------
1 | import type { Plugin } from "vue"
2 | import dayjs from "dayjs"
3 | import isoWeek from "dayjs/plugin/isoWeek"
4 | import isSameOrBefore from "dayjs/plugin/isSameOrBefore.js"
5 | import isSameOrAfter from "dayjs/plugin/isSameOrAfter.js"
6 | import isBetween from "dayjs/plugin/isBetween.js"
7 | import weekOfYear from "dayjs/plugin/weekOfYear"
8 | import advancedFormat from "dayjs/plugin/advancedFormat"
9 | import customParseFormat from "dayjs/plugin/customParseFormat.js"
10 |
11 | import type { GanttBarObject } from "./types.js"
12 | import type { ColorScheme } from "./color-schemes"
13 |
14 | import GGanttChart from "./components/GGanttChart.vue"
15 | import GGanttRow from "./components/GGanttRow.vue"
16 |
17 | export function extendDayjs() {
18 | dayjs.extend(isSameOrBefore)
19 | dayjs.extend(isSameOrAfter)
20 | dayjs.extend(isBetween)
21 | dayjs.extend(customParseFormat)
22 | dayjs.extend(weekOfYear)
23 | dayjs.extend(isoWeek)
24 | dayjs.extend(advancedFormat)
25 | }
26 |
27 | export type { ColorScheme, GanttBarObject }
28 | export { GGanttChart, GGanttRow }
29 |
30 | export const ganttastic: Plugin = {
31 | install(app, options?) {
32 | extendDayjs()
33 | app.component("GGanttChart", GGanttChart)
34 | app.component("GGanttRow", GGanttRow)
35 | }
36 | }
37 |
38 | export default ganttastic
39 |
--------------------------------------------------------------------------------
/tsconfig.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@vue/tsconfig/tsconfig.node.json",
3 | "include": ["vite.config.*", "vitest.config.*", "cypress.config.*"],
4 | "compilerOptions": {
5 | "composite": true,
6 | "types": ["node"]
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@vue/tsconfig/tsconfig.web.json",
3 | "include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
4 | "compilerOptions": {
5 | "baseUrl": "."
6 | },
7 |
8 | "references": [
9 | {
10 | "path": "./tsconfig.config.json"
11 | }
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/vite.config.mts:
--------------------------------------------------------------------------------
1 | import { fileURLToPath, URL } from "node:url"
2 |
3 | import { defineConfig } from "vite"
4 | import vue from "@vitejs/plugin-vue"
5 | import postcssPresetEnv from "postcss-preset-env"
6 | import styleInject from "@senojs/rollup-plugin-style-inject"
7 |
8 | // https://vitejs.dev/config/
9 | export default defineConfig({
10 | plugins: [
11 | vue(),
12 | styleInject({
13 | insertAt: "top"
14 | })
15 | ],
16 | css: {
17 | postcss: {
18 | plugins: [postcssPresetEnv()]
19 | }
20 | },
21 | build: {
22 | lib:
23 | process.env.NODE_ENV === "production"
24 | ? {
25 | entry: fileURLToPath(
26 | new URL("src/vue-ganttastic.ts", import.meta.url)
27 | ),
28 | name: "VueGanttastic",
29 | fileName: "vue-ganttastic"
30 | }
31 | : undefined,
32 | outDir: process.env.NODE_ENV === "production" ? "lib" : "dist",
33 | rollupOptions: {
34 | // make sure to externalize deps that shouldn't be bundled
35 | // into the library
36 | external: ["vue", "dayjs"],
37 | output: {
38 | // Provide global variables to use in the UMD build
39 | // for externalized deps
40 | globals: {
41 | vue: "Vue",
42 | dayjs: "dayjs"
43 | },
44 | exports: "named"
45 | }
46 | }
47 | }
48 | })
49 |
--------------------------------------------------------------------------------