├── .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 | Vue Ganttastic logo 9 | 10 | Vue Ganttastic is a simple, interactive and highly customizable Gantt chart component for Vue 3. 11 | 12 | ![image](https://user-images.githubusercontent.com/28678851/148191571-76bd8d61-4583-4538-8c59-cc2915494890.png) 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 | 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 |
136 | 137 | 138 | 139 |
140 | 141 | BTC address: 142 | ![image](https://user-images.githubusercontent.com/28678851/233090745-a0a6d8a4-6df6-4b82-ac0c-90e69551786e.png) 143 | 144 | ## Screenshots 145 | 146 | ![image](https://user-images.githubusercontent.com/28678851/148191571-76bd8d61-4583-4538-8c59-cc2915494890.png) 147 | 148 | ![image](https://user-images.githubusercontent.com/28678851/148191529-b50c0d17-bcc1-4a78-9d2c-ff2a36b03f52.png) 149 | 150 | ![image](https://user-images.githubusercontent.com/28678851/148191757-a2520dce-aeed-43df-87b2-3a64e225f9e7.png) 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 | 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 | 30 | 31 | 32 | 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 | 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 |
20 | 21 | 22 | 23 |
24 | 25 | BTC address: 26 | ![image](https://user-images.githubusercontent.com/28678851/233090745-a0a6d8a4-6df6-4b82-ac0c-90e69551786e.png) 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 | 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 | 35 | 36 | 119 | 120 | 165 | -------------------------------------------------------------------------------- /src/components/GGanttBarTooltip.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 88 | 89 | 135 | -------------------------------------------------------------------------------- /src/components/GGanttChart.vue: -------------------------------------------------------------------------------- 1 | 51 | 52 | 265 | 266 | 294 | -------------------------------------------------------------------------------- /src/components/GGanttCurrentTime.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 36 | 37 | 56 | -------------------------------------------------------------------------------- /src/components/GGanttGrid.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 26 | 27 | 44 | -------------------------------------------------------------------------------- /src/components/GGanttLabelColumn.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 33 | 34 | 80 | -------------------------------------------------------------------------------- /src/components/GGanttRow.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 81 | 82 | 124 | -------------------------------------------------------------------------------- /src/components/GGanttTimeaxis.vue: -------------------------------------------------------------------------------- 1 | 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 | --------------------------------------------------------------------------------