Repository: InfectoOne/vue-ganttastic Branch: master Commit: b06e161fba42 Files: 49 Total size: 98.2 KB Directory structure: gitextract_6_i3sk9e/ ├── .browserslistrc ├── .editorconfig ├── .eslintrc.cjs ├── .github/ │ ├── FUNDING.yml │ └── workflows/ │ └── deploy.yml ├── .gitignore ├── .prettierrc ├── README.md ├── deploy.sh ├── docs/ │ ├── .vitepress/ │ │ ├── config.ts │ │ └── theme/ │ │ ├── custom.css │ │ └── index.js │ ├── GGanttChart.md │ ├── GGanttRow.md │ ├── common-use-cases.md │ ├── examples.md │ ├── getting-started.md │ ├── index.md │ └── introduction.md ├── docs-deploy.yml ├── env.d.ts ├── index.html ├── package.json ├── src/ │ ├── GanttPlayground.vue │ ├── color-schemes.ts │ ├── components/ │ │ ├── GGanttBar.vue │ │ ├── GGanttBarTooltip.vue │ │ ├── GGanttChart.vue │ │ ├── GGanttCurrentTime.vue │ │ ├── GGanttGrid.vue │ │ ├── GGanttLabelColumn.vue │ │ ├── GGanttRow.vue │ │ └── GGanttTimeaxis.vue │ ├── composables/ │ │ ├── createBarDrag.ts │ │ ├── useBarDragLimit.ts │ │ ├── useBarDragManagement.ts │ │ ├── useDayjsHelper.ts │ │ ├── useTimePositionMapping.ts │ │ └── useTimeaxisUnits.ts │ ├── playground.ts │ ├── provider/ │ │ ├── provideConfig.ts │ │ ├── provideEmitBarEvent.ts │ │ ├── provideGetChartRows.ts │ │ └── symbols.ts │ ├── types.ts │ └── vue-ganttastic.ts ├── tsconfig.config.json ├── tsconfig.json └── vite.config.mts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .browserslistrc ================================================ > 1% last 2 versions not dead ================================================ FILE: .editorconfig ================================================ [*.{js,jsx,ts,tsx,vue}] indent_style = space indent_size = 2 trim_trailing_whitespace = true insert_final_newline = true max_line_length = 100 ================================================ FILE: .eslintrc.cjs ================================================ /* eslint-env node */ require("@rushstack/eslint-patch/modern-module-resolution") module.exports = { root: true, extends: [ "plugin:vue/vue3-recommended", "eslint:recommended", "@vue/eslint-config-typescript/recommended", "@vue/eslint-config-prettier" ], rules: { "no-console": process.env.NODE_ENV === "production" ? "warn" : "off", "no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off", "@typescript-eslint/no-unused-vars": "off", "@typescript-eslint/explicit-module-boundary-types": "off", quotes: ["error", "double"], // "object-curly-spacing": ["error", "never"], "prettier/prettier": ["error", {}, { usePrettierrc: true }] } } ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username otechie: # Replace with a single Otechie username lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry ================================================ FILE: .github/workflows/deploy.yml ================================================ # Sample workflow for building and deploying a VitePress site to GitHub Pages # name: Deploy VitePress site to Pages on: # Runs on pushes targeting the `main` branch. Change this to `master` if you're # using the `master` branch as the default branch. push: branches: [master] # Allows you to run this workflow manually from the Actions tab workflow_dispatch: # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages permissions: contents: read pages: write id-token: write # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. concurrency: group: pages cancel-in-progress: false jobs: # Build job build: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 # Not needed if lastUpdated is not enabled # - uses: pnpm/action-setup@v3 # Uncomment this if you're using pnpm # - uses: oven-sh/setup-bun@v1 # Uncomment this if you're using Bun - name: Setup Node uses: actions/setup-node@v4 with: node-version: 20 cache: npm # or pnpm / yarn - name: Setup Pages uses: actions/configure-pages@v4 - name: Install dependencies run: npm ci # or pnpm install / yarn install / bun install - name: Build with VitePress run: npm run docs:build # or pnpm docs:build / yarn docs:build / bun run docs:build - name: Upload artifact uses: actions/upload-pages-artifact@v3 with: path: docs/.vitepress/dist # Deployment job deploy: environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} needs: build runs-on: ubuntu-latest name: Deploy steps: - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4 ================================================ FILE: .gitignore ================================================ .DS_Store node_modules /lib /lib_types # local env files .env.local .env.*.local # Log files npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* # Editor directories and files .idea .vscode *.suo *.ntvs* *.njsproj *.sln *.sw? # Docs dist directory docs/.vitepress/cache docs/.vitepress/temp docs/.vitepress/dist *.tgz ================================================ FILE: .prettierrc ================================================ { "semi": false, "trailingComma": "none", "endOfLine": "auto" } ================================================ FILE: README.md ================================================ # Vue Ganttastic
Vue Ganttastic logo Vue Ganttastic is a simple, interactive and highly customizable Gantt chart component for Vue 3. ![image](https://user-images.githubusercontent.com/28678851/148191571-76bd8d61-4583-4538-8c59-cc2915494890.png)
## Features - **[Vue 3](https://v3.vuejs.org/) support** - **[TypeScript](https://www.typescriptlang.org/) support** _(ships with out of the box type declarations)_ - **Interactivity** _(dynamic, movable and pushable bars)_ - **Reactivity / Responsiveness** (_when changes occur, bars are repositioned accordingly_) - **Customization options** (_chart/bar styling, slots, event handlers etc._) Using Vue 2? Check out [Vue-Ganttastic v1](https://github.com/zunnzunn/vue-ganttastic/tree/vue-ganttastic-v1). ## Guide and Docs For further guides and references, check out the [official docs](https://zunnzunn.github.io/vue-ganttastic/getting-started.html). ## Quickstart Install using ``` npm install @infectoone/vue-ganttastic ``` Then, initalize the plugin in the starting point of your app (most likely src/main.js): ```js import { createApp } from "vue" import App from "./App.vue" ... import ganttastic from '@infectoone/vue-ganttastic' ... createApp(App) .use(ganttastic) .mount('#app') ``` This will globally register the components g-gantt-chart and g-gantt-row and you will be able to use those two components right away. ```html ``` ## Contributing Clone the project, make some changes, test your changes out, create a pull request with a short summary of what changes you made. Contributing is warmly welcomed! To test your changes out before creating a pull request, create a build: ``` npm run build ``` To test out the build, you should create a tarball using: ``` npm pack ``` Then, place the tarball in some other test project and install the package from the tarball by using: ``` npm install .tgz ``` ## About **License** [MIT](https://choosealicense.com/licenses/mit/) **Author**: Marko Žunić, BSc [GitHub Repository](https://github.com/zunnzunn/vue-ganttastic) ## Support the project! In case you found the library useful, a little tip would be much appreciated!
BTC address: ![image](https://user-images.githubusercontent.com/28678851/233090745-a0a6d8a4-6df6-4b82-ac0c-90e69551786e.png) ## Screenshots ![image](https://user-images.githubusercontent.com/28678851/148191571-76bd8d61-4583-4538-8c59-cc2915494890.png) ![image](https://user-images.githubusercontent.com/28678851/148191529-b50c0d17-bcc1-4a78-9d2c-ff2a36b03f52.png) ![image](https://user-images.githubusercontent.com/28678851/148191757-a2520dce-aeed-43df-87b2-3a64e225f9e7.png) ================================================ FILE: deploy.sh ================================================ set -e npm run docs:build cd docs/.vuepress/dist git init git add -A git commit -m 'deploy' git push -f git@github.com:InfectoOne/vue-ganttastic.git master:gh-pages cd - ================================================ FILE: docs/.vitepress/config.ts ================================================ import { defineConfig } from 'vitepress' // https://vitepress.dev/reference/site-config export default defineConfig({ lang: 'en-US', title: 'Vue-Ganttastic', description: 'Simple and customizable Gantt chart component for Vue 3.', base: '/vue-ganttastic/', head: [['link', { rel: 'icon', href: 'https://user-images.githubusercontent.com/28678851/148047714-301f07df-4101-48b8-9e47-1f272b290e80.png' }]], themeConfig: { logo: 'https://user-images.githubusercontent.com/28678851/148047714-301f07df-4101-48b8-9e47-1f272b290e80.png', nav: [ { text: 'Home', link: '/' }, ], sidebar: [ { text: 'Introduction', link: '/introduction'}, { text: 'Getting Started',link: '/getting-started' }, { text: 'Common use cases', link: '/common-use-cases' }, { text: 'Examples', link: '/examples' }, { text: 'API Reference', items: [ { text: 'GGanttChart', link: '/GGanttChart' }, { text: 'GGanttRow', link: '/GGanttRow' } ] } ], socialLinks: [ { icon: 'github', link: 'https://github.com/zunnzunn/vue-ganttastic' } ] } }) ================================================ FILE: docs/.vitepress/theme/custom.css ================================================ :root { --vp-home-hero-name-color: #2fb585; --vp-button-brand-bg: #2fb585; --vp-button-brand-hover-bg: #354b5d; --vp-c-brand: #2fb585; } ================================================ FILE: docs/.vitepress/theme/index.js ================================================ import DefaultTheme from 'vitepress/theme' import './custom.css' import {ganttastic} from "../../../src/vue-ganttastic" export default { extends: DefaultTheme, enhanceApp(ctx) { ctx.app.use(ganttastic) } } ================================================ FILE: docs/GGanttChart.md ================================================ # API: GGanttChart The main component of Vue Ganttastic. Represents an entire chart and is meant to have at least one `g-gantt-row` child component. ## Props | Prop | Type | Default | Description | |-------------|---------|---------|------------------------------| | `chart-start` | string | | Start date-time of the chart. | `chart-end` | string | | End date-time of the chart. | `precision` | string? | `"hour"` | Display precision of the time-axis. Possible values: `hour`, `day`, `date`, `week` and `month`. | | `bar-start` | string | | Name of the property in bar objects that represents the start date. | `bar-end` | string | | Name of the property in bar objects that represents the end date . | `date-format` | string \| false | `"YYYY-MM-DD HH:mm"` | Datetime string format of `chart-start`, `chart-end` and the values of the `bar-start`, `bar-end` properties in bar objects. See [Day.js format tokens](https://day.js.org/docs/en/parse/string-format). If the aforementioned properties are native JavaScript [Date](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date) objects in your use case, pass `false`. | `width` | string? | `"100%"` | Width of the chart (e.g. `80%` or `800px`) | `hide-timeaxis` | boolean? | `false` | Toggle visibility of the time axis. | `color-scheme` | string \| ColorScheme | `"default"` | Color scheme (theme) of the chart. Either use the name of one of the predefined schemes or pass a color-scheme-object of your own. See [color schemes](#color-schemes). | `grid` | string? | `false` | Toggle visibility of background grid. | `current-time` | boolean? | `false` | Toggle visibility of current time marker. | `current-time-label` | string? | `''` | Text to be displayed next to the current time marker. | `push-on-overlap` | boolean? | `false` | Specifies whether bars "push one another" when dragging and overlaping. | `no-overlap` | boolean? | `false` | If `push-on-overlap` is `false`, toggle this to prevent overlaps after drag by snapping the dragged bar back to its original position. | `row-height` | number? | `40` | Height of each row in pixels. | `highlighted-units` | number[]? | `[]` | The time units specified here will be visually highlighted in the chart with a mild opaque color. | `font` | string | `"Helvetica"`| Font family of the chart. | `label-column-title` | string? | `''` | If specified, a dedicated column for the row labels will be rendered on the left side of the graph. The specified title is displayed in the upper left corner, as the column's header. | `label-column-width` | string? | `150px` | Width of the column containing the row labels (if `label-column-title` specified) ## Custom Events | Event name | Event data | |----------------------------|------------------------------------------------------------| | `click-bar` | `{bar: GanttBarObject, e: MouseEvent, datetime?: string}` | | `mousedown-bar` | `{bar: GanttBarObject, e: MouseEvent, datetime?: string}` | | `mouseup-bar` | `{bar: GanttBarObject, e: MouseEvent, datetime?: string}` | | `dblclick-bar` | `{bar: GanttBarObject, e: MouseEvent, datetime?: string}` | | `mouseenter-bar` | `{bar: GanttBarObject, e: MouseEvent}` | | `mouseleave-bar` | `{bar: GanttBarObject, e: MouseEvent}` | | `dragstart-bar` | `{bar: GanttBarObject, e: MouseEvent}` | | `drag-bar` | `{bar: GanttBarObject, e: MouseEvent}` | | `dragend-bar` | `{bar: GanttBarObject, e: MouseEvent, movedBars?: Map}` | | `contextmenu-bar` | `{bar: GanttBarObject, e: MouseEvent, datetime?: string}` | ## Slots | Slot name | Slot data | Description | |----------------------------|-----------------------| ----------------------------------------| | `upper-timeunit` | `{label: string, value: string}` | Content of an upper time-unit section in the time axis. | | `timeunit` | `{label: string, value: string}` | Content of a time-unit section in the time axis. | | `bar-tooltip` | `{bar: GanttBarObject}` | Slot for the tooltip which appears when hovering over a bar. | | `current-time-label` | | Slot for the text shown next to the current time marker when the prop `current-time` is set to `true`. | | `label-column-title` | | Slot for the title of the extra column to the left where the row labels are shown if the prop `label-column-title` is set. | | `label-column-row` | `{ label: string } ` | Slot for the label of a row if `label-column-title` is set. | ## Color Schemes List of pre-defined color schemes: - `default` - `creamy` - `crimson` - `dark` - `flare` - `fuchsia` - `grove` - `material-blue` - `sky` - `slumber` - `vue` You can also provide your own color scheme. Your custom color scheme should be an object of the following shape: ```typescript { primary: string, secondary: string, ternary: string, quartenary: string, hoverHighlight: string, markerCurrentTime: string, text: string, background: string, toast?: string } ``` ================================================ FILE: docs/GGanttRow.md ================================================ # API: GGanttRow Represents a single row of the chart. It is meant to be a child component of `g-gantt-chart`. ## Props | Prop | Type | Default | Description | |-------------|---------|---------|------------------------------| | `label` |`string?`| `""` | Text that is floating in the upper left corner of the row. | `bars` |`GanttBarObject[]`| | Array of objects, each representing a bar in this row. Any JavaScript/TypeScript object with a nested `ganttBarConfig` object with a unique `id` string property is compatible. The objects must also contain two properties which represent the start and end datetime of the bar. The names of those properties must be passed to the `bar-start` and `bar-end` props of the `g-gantt-chart` parent. | `highlight-on-hover` | `boolean?` | `false` | Used for toggling a background color transition effect on mouse hover. ## Custom Events | Event name | Event data | |----------------------------|------------------------------------------------------------| | `drop` | `{ e: MouseEvent, datetime: string}` | ## Slots | Slot name | Slot data | Description | |----------------------------|-----------------------| ----------------------------------------| | `label` | | Used for modifying the text that is floating in the upper left corner of the row. | | `bar-label` | `{bar: GanttBarObject}` | Used for modifying the text in a bar. | ================================================ FILE: docs/common-use-cases.md ================================================ # Common use cases The following section provides a non-exhausting list of common use cases and special features of Vue Ganttastic with corresponding code snippets. ## Adding new bars For each row of the chart, you will render a `g-gantt-row` component, which accepts a `bars` prop, which is an array of bar objects. Since the prop is reactive, all you need to do to add a new bar is to push a new bar-object into that array. Just make sure that the new bar-object has a nested `ganttBarConfig` object with a unique `id`, and don't forget to specify the property values for the start and end date of the bar (the properties' names must be the ones you passed to the `bar-start` and `bar-end` props of `g-gantt-chart`): ```vue ``` ## Configuring and styling bars Your bar objects can be of any type and contain any properties you want. The only requirements are: - a datetime-string property for the bar start date - a datetime-string property for the bar end date - a nested object `ganttBarConfig` with a unique string property `id` For further configuration, you can add some optional properties to the nested `ganttBarConfig` object: | Property name | Type | Description | |---------------|---------|-----------------------| | `id` | `string` | A unique string identifier for the bar. (**mandatory**) | `label` | `string?` | Text displayed on the bar. | `html` | `string?` | Optional HTML Code that will be rendered after the label (e.g. for tags). Please sanitize user input to avoid cross site scripting, if applicable. | `hasHandles` | `boolean?` | Used to toggle handles on the left and right side of the bar that can be dragged to change the width of the bar. | | `immobile` | `boolean?` | Used to toggle whether bar can be moved (dragged). | `bundle` | `string?` | A string identifier for a bundle. A bundle is a collection of bars that are dragged simultaneously. | `style` | `CSSStyleSheet?` | CSS-based styling for your bar (e.g `background`, `fontSize`, `borderRadius` etc.). ## Extending the width of a bar Simply add `hasHandles: true` to the `ganttBarConfig` to make the bar extendable by dragging the handles on its left or right side. ## Push bars when dragging By default, bars can overlap with other bars while being dragged. If you would like to prevent this and have the bars "push" one another while dragging, use the `push-on-overlap` prop: ```vue ... ``` ## Bundling bars together If you want to bind a group of bars one to another so that when you drag one bar, all the others move together with it, specify a `bundle` string in the `ganttBarConfig` of each affected bar. ## Custom behavior on clicking/dragging a bar It is completely up to you to specify which kind of behavior you would like e.g. when a bar is clicked on or when a bar-drag is ended. For this, you may use special events emitted by `g-gantt-chart`: ```vue ... ``` ## Drag and drop The `g-gantt-row` component comes with a special `drop` event, that you can use to implement custom drag-and-drop behavior. The event data also includes the `datetime` position on which the drop occured. ```vue ``` ## Time axis precision If the time-range (`chart-start` to `chart-end`) of your chart is very large, the displayed time units in the time axis might be too dense if the chart is not wide enough. You might want to specify the precision of the time axis accordingly. Use the `precision` prop of `g-gantt-chart` for this. Possible values are `hour`, `day`, `week` and `month`. ## Chart themes Vue Ganttastic ships with several pre-made color schemes that you may specify using the `color-scheme` prop of `g-gantt-chart`. [List of available color-schemes](https://infectoone.github.io/vue-ganttastic/GGanttChart.html#color-schemes) ## Locale Since Vue Ganttastic uses Day.js for all datetime manipulations, you can change the locale of Vue Ganttastic by [changing the global locale of Day.js](https://day.js.org/docs/en/i18n/changing-locale). You will usually do this in your `src/main.js` before you initialize the Ganttastic plugin. ================================================ FILE: docs/examples.md ================================================ # Live Demos ## Simple hour chart - `precision`: `hour` ## Day chart with dark theme - `precision`: `day` - `row-height` : `70` - `no-overlap` - `color-scheme` : `dark` Used slots: `g-gantt-row` > `label`, `bar-label` ## Month chart pushing and bundles - `precision`: `month` - `push-on-overlap` - `color-scheme` : `vue` - `font` : `Courier` ================================================ FILE: docs/getting-started.md ================================================ # Getting started ## Install You can add Vue Ganttastic to your project using npm: ``` npm install @infectoone/vue-ganttastic ``` Then, initalize the plugin in the starting point of your app (most likely `src/main.js`): ```javascript import { createApp } from "vue" import App from "./App.vue" ... import ganttastic from '@infectoone/vue-ganttastic' ... createApp(App) .use(ganttastic) .mount('#app') ``` This will globally register the components `g-gantt-chart` and `g-gantt-row` and you will be able to use those two components right away. ## Basic usage ```vue ``` The result shoud look like this: ================================================ FILE: docs/index.md ================================================ --- # https://vitepress.dev/reference/default-theme-home-page layout: home hero: name: Vue-Ganttastic text: Gantt chart component for Vue tagline: A simple, interactive and highly customizable Gantt chart component for Vue.js image: src: https://user-images.githubusercontent.com/28678851/148047714-301f07df-4101-48b8-9e47-1f272b290e80.png alt: Vue-Ganttastic logo actions: - theme: brand text: Get started link: /introduction - theme: alt text: API Reference link: /GGanttChart features: - title: Vue 3 and TypeScript support details: Written in Vue 3 and TypeScript. Ships with out-of-the-box type declarations. - title: Interactive details: Dynamic Gantt chart with movable bars and numerous event handlers. - title: Customizable details: Style the chart and each individual bar to your own liking! --- ================================================ FILE: docs/introduction.md ================================================ # Introduction Vue Ganttastic is a simple, interactive and highly customizable Gantt chart component for Vue 3. ## Features - **[Vue 3](https://v3.vuejs.org/) support** - **[TypeScript](https://www.typescriptlang.org/) support** *(ships with out of the box type declarations)* - **Interactivity** *(dynamic, movable and pushable bars)* - **Reactivity / Responsiveness** (*when changes occur, bars are repositioned accordingly*) - **Customization options** (*chart/bar styling, slots, event handlers etc.*) ## About **License** [MIT](https://choosealicense.com/licenses/mit/) **Author**: Marko Žunić, BSc [GitHub Repository](https://github.com/InfectoOne/vue-ganttastic) ## Support the project! In case you found the library useful, a little tip would be much appreciated!
BTC address: ![image](https://user-images.githubusercontent.com/28678851/233090745-a0a6d8a4-6df6-4b82-ac0c-90e69551786e.png) ================================================ FILE: docs-deploy.yml ================================================ # Sample workflow for building and deploying a VitePress site to GitHub Pages # name: Deploy VitePress site to Pages on: # Runs on pushes targeting the `main` branch. Change this to `master` if you're # using the `master` branch as the default branch. push: branches: [master] # Allows you to run this workflow manually from the Actions tab workflow_dispatch: # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages permissions: contents: read pages: write id-token: write # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. concurrency: group: pages cancel-in-progress: false jobs: # Build job build: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v3 with: fetch-depth: 0 # Not needed if lastUpdated is not enabled # - uses: pnpm/action-setup@v2 # Uncomment this if you're using pnpm - name: Setup Node uses: actions/setup-node@v3 with: node-version: 18 cache: npm # or pnpm / yarn - name: Setup Pages uses: actions/configure-pages@v3 - name: Install dependencies run: npm ci # or pnpm install / yarn install - name: Build with VitePress run: npm run docs:build # or pnpm docs:build / yarn docs:build - name: Upload artifact uses: actions/upload-pages-artifact@v2 with: path: docs/.vitepress/dist # Deployment job deploy: environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} needs: build runs-on: ubuntu-latest name: Deploy steps: - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v2 ================================================ FILE: env.d.ts ================================================ /// ================================================ FILE: index.html ================================================ @infectoone/vue-ganttastic
================================================ FILE: package.json ================================================ { "name": "@infectoone/vue-ganttastic", "version": "2.3.2", "description": "A simple and customizable Gantt chart component for Vue.js", "author": "Marko Zunic (@zunnzunn)", "scripts": { "serve": "vite", "build": "npm run build:types && npm run build:lib", "build:lib": "vite build", "build:types": "vue-tsc --declaration --emitDeclarationOnly --outDir lib_types", "typecheck": "vue-tsc --noEmit", "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --ignore-path .gitignore", "lint:fix": "npm run lint --fix", "docs:build": "vitepress build docs", "docs:dev": "vitepress dev docs", "docs:preview": "vitepress preview docs" }, "type": "module", "exports": { ".": { "types": "./lib_types/vue-ganttastic.d.ts", "import": "./lib/vue-ganttastic.js", "require": "./lib/vue-ganttastic.umd.cjs" } }, "main": "./lib/vue-ganttastic.umd.cjs", "types": "./lib_types/vue-ganttastic.d.ts", "files": [ "lib_types", "lib/vue-ganttastic.js", "lib/vue-ganttastic.umd.cjs" ], "devDependencies": { "@rushstack/eslint-patch": "^1.2.0", "@senojs/rollup-plugin-style-inject": "^0.1.1", "@types/node": "^16.11.58", "@types/postcss-preset-env": "^7.7.0", "@vitejs/plugin-vue": "^3.1.2", "@vue/eslint-config-prettier": "^7.0.0", "@vue/eslint-config-typescript": "^11.0.2", "@vue/tsconfig": "^0.1.3", "@vuepress/plugin-search": "2.0.0-beta.51", "eslint": "^8.25.0", "eslint-plugin-vue": "^9.6.0", "npm-run-all": "^4.1.5", "postcss": "^8.4.17", "postcss-preset-env": "^7.8.2", "prettier": "^2.7.1", "typescript": "~4.8.4", "vite": "^3.1.6", "vitepress": "^1.0.0-rc.4", "vue-tsc": "^1.8.27" }, "peerDependencies": { "dayjs": "^1.11.5", "vue": "^3.2.40" }, "homepage": "https://zunnzunn.github.io/vue-ganttastic/", "keywords": [ "gantt", "chart", "bar", "diagram", "vue", "vuejs", "ganttastic" ], "license": "MIT", "repository": { "type": "git", "url": "https://github.com/zunnzunn/vue-ganttastic" }, "dependencies": { "@vueuse/core": "^9.1.1" } } ================================================ FILE: src/GanttPlayground.vue ================================================ ================================================ FILE: src/color-schemes.ts ================================================ import type * as CSS from "csstype" type Color = CSS.DataType.Color export type ColorScheme = { primary: Color secondary: Color ternary: Color quartenary: Color hoverHighlight: Color markerCurrentTime: Color text: Color background: Color toast?: Color } export const colorSchemes: Record = { default: { primary: "#eeeeee", secondary: "#E0E0E0", ternary: "#F5F5F5", quartenary: "#ededed", hoverHighlight: "rgba(204, 216, 219, 0.5)", markerCurrentTime: "#000", text: "#404040", background: "white" }, creamy: { primary: "#ffe8d9", secondary: "#fcdcc5", ternary: "#fff6f0", quartenary: "#f7ece6", hoverHighlight: "rgba(230, 221, 202, 0.5)", markerCurrentTime: "#000", text: "#542d05", background: "white" }, crimson: { primary: "#a82039", secondary: "#c41238", ternary: "#db4f56", quartenary: "#ce5f64", hoverHighlight: "rgba(196, 141, 141, 0.5)", markerCurrentTime: "#000", text: "white", background: "white" }, dark: { primary: "#404040", secondary: "#303030", ternary: "#353535", quartenary: "#383838", hoverHighlight: "rgba(159, 160, 161, 0.5)", markerCurrentTime: "#fff", text: "white", background: "#525252", toast: "#1f1f1f" }, flare: { primary: "#e08a38", secondary: "#e67912", ternary: "#5e5145", quartenary: "#665648", hoverHighlight: "rgba(196, 141, 141, 0.5)", markerCurrentTime: "#000", text: "white", background: "white" }, fuchsia: { primary: "#de1d5a", secondary: "#b50b41", ternary: "#ff7da6", quartenary: "#f2799f", hoverHighlight: "rgba(196, 141, 141, 0.5)", markerCurrentTime: "#000", text: "white", background: "white" }, grove: { primary: "#3d9960", secondary: "#288542", ternary: "#72b585", quartenary: "#65a577", hoverHighlight: "rgba(160, 219, 171, 0.5)", markerCurrentTime: "#000", text: "white", background: "white" }, "material-blue": { primary: "#0D47A1", secondary: "#1565C0", ternary: "#42a5f5", quartenary: "#409fed", hoverHighlight: "rgba(110, 165, 196, 0.5)", markerCurrentTime: "#000", text: "white", background: "white" }, sky: { primary: "#b5e3ff", secondary: "#a1d6f7", ternary: "#d6f7ff", quartenary: "#d0edf4", hoverHighlight: "rgba(193, 202, 214, 0.5)", markerCurrentTime: "#000", text: "#022c47", background: "white" }, slumber: { primary: "#2a2f42", secondary: "#2f3447", ternary: "#35394d", quartenary: "#2c3044", hoverHighlight: "rgba(179, 162, 127, 0.5)", markerCurrentTime: "#fff", text: "#ffe0b3", background: "#38383b", toast: "#1f1f1f" }, vue: { primary: "#258a5d", secondary: "#41B883", ternary: "#35495E", quartenary: "#2a3d51", hoverHighlight: "rgba(160, 219, 171, 0.5)", markerCurrentTime: "#000", text: "white", background: "white" } } export type ColorSchemeKey = keyof typeof colorSchemes export default colorSchemes ================================================ FILE: src/components/GGanttBar.vue ================================================ ================================================ FILE: src/components/GGanttBarTooltip.vue ================================================ ================================================ FILE: src/components/GGanttChart.vue ================================================ ================================================ FILE: src/components/GGanttCurrentTime.vue ================================================ ================================================ FILE: src/components/GGanttGrid.vue ================================================ ================================================ FILE: src/components/GGanttLabelColumn.vue ================================================ ================================================ FILE: src/components/GGanttRow.vue ================================================ ================================================ FILE: src/components/GGanttTimeaxis.vue ================================================ ================================================ FILE: src/composables/createBarDrag.ts ================================================ import { ref } from "vue" import type { GGanttChartConfig } from "../components/GGanttChart.vue" import provideConfig from "../provider/provideConfig.js" import type { GanttBarObject } from "../types" import useDayjsHelper from "./useDayjsHelper.js" import useTimePositionMapping from "./useTimePositionMapping.js" export default function createBarDrag( bar: GanttBarObject, onDrag: (e: MouseEvent, bar: GanttBarObject) => void = () => null, onEndDrag: (e: MouseEvent, bar: GanttBarObject) => void = () => null, config: GGanttChartConfig = provideConfig() ) { const { barStart, barEnd, pushOnOverlap } = config const isDragging = ref(false) let cursorOffsetX = 0 let dragCallBack: (e: MouseEvent) => void const { mapPositionToTime } = useTimePositionMapping(config) const { toDayjs } = useDayjsHelper(config) const initDrag = (e: MouseEvent) => { const barElement = document.getElementById(bar.ganttBarConfig.id) if (!barElement) { return } cursorOffsetX = e.clientX - (barElement.getBoundingClientRect().left || 0) const mousedownType = (e.target as Element).className switch (mousedownType) { case "g-gantt-bar-handle-left": document.body.style.cursor = "ew-resize" dragCallBack = dragByLeftHandle break case "g-gantt-bar-handle-right": document.body.style.cursor = "ew-resize" dragCallBack = dragByRightHandle break default: dragCallBack = drag } isDragging.value = true window.addEventListener("mousemove", dragCallBack) window.addEventListener("mouseup", endDrag) } const getBarElements = () => { const barElement = document.getElementById(bar.ganttBarConfig.id) const barContainer = barElement?.closest(".g-gantt-row-bars-container")?.getBoundingClientRect() return { barElement, barContainer } } const drag = (e: MouseEvent) => { const { barElement, barContainer } = getBarElements() if (!barElement || !barContainer) { return } const barWidth = barElement.getBoundingClientRect().width const xStart = e.clientX - barContainer.left - cursorOffsetX const xEnd = xStart + barWidth if (isOutOfRange(xStart, xEnd)) { return } bar[barStart.value] = mapPositionToTime(xStart) bar[barEnd.value] = mapPositionToTime(xEnd) onDrag(e, bar) } const dragByLeftHandle = (e: MouseEvent) => { const { barElement, barContainer } = getBarElements() if (!barElement || !barContainer) { return } const xStart = e.clientX - barContainer.left const newBarStart = mapPositionToTime(xStart) if (toDayjs(newBarStart).isSameOrAfter(toDayjs(bar, "end"))) { return } bar[barStart.value] = newBarStart onDrag(e, bar) } const dragByRightHandle = (e: MouseEvent) => { const { barElement, barContainer } = getBarElements() if (!barElement || !barContainer) { return } const xEnd = e.clientX - barContainer.left const newBarEnd = mapPositionToTime(xEnd) if (toDayjs(newBarEnd).isSameOrBefore(toDayjs(bar, "start"))) { return } bar[barEnd.value] = newBarEnd onDrag(e, bar) } const isOutOfRange = (xStart?: number, xEnd?: number) => { if (!pushOnOverlap.value) { return false } const dragLimitLeft = bar.ganttBarConfig.dragLimitLeft const dragLimitRight = bar.ganttBarConfig.dragLimitRight return ( (xStart && dragLimitLeft != null && xStart < dragLimitLeft) || (xEnd && dragLimitRight != null && xEnd > dragLimitRight) ) } const endDrag = (e: MouseEvent) => { isDragging.value = false document.body.style.cursor = "" window.removeEventListener("mousemove", dragCallBack) window.removeEventListener("mouseup", endDrag) onEndDrag(e, bar) } return { isDragging, initDrag } } ================================================ FILE: src/composables/useBarDragLimit.ts ================================================ import type { GanttBarObject } from "../types" import provideConfig from "../provider/provideConfig.js" import provideGetChartRows from "../provider/provideGetChartRows.js" export default function useBarDragLimit() { const { pushOnOverlap } = provideConfig() const getChartRows = provideGetChartRows() const getBarsFromBundle = (bundle?: string) => { const res: GanttBarObject[] = [] if (bundle != null) { getChartRows().forEach((row) => { row.bars.forEach((bar) => { if (bar.ganttBarConfig.bundle === bundle) { res.push(bar) } }) }) } return res } const setDragLimitsOfGanttBar = (bar: GanttBarObject) => { if (!pushOnOverlap.value || bar.ganttBarConfig.pushOnOverlap === false) { return } for (const sideValue of ["left", "right"]) { const side = sideValue as "left" | "right" const { gapDistanceSoFar, bundleBarsAndGapDist } = countGapDistanceToNextImmobileBar( bar, 0, side ) let totalGapDistance = gapDistanceSoFar const bundleBarsOnPath = bundleBarsAndGapDist if (!bundleBarsOnPath) { continue } for (let i = 0; i < bundleBarsOnPath.length; i++) { const barFromBundle = bundleBarsOnPath[i].bar const gapDist = bundleBarsOnPath[i].gapDistance const otherBarsFromBundle = getBarsFromBundle(barFromBundle.ganttBarConfig.bundle).filter( (otherBar) => otherBar !== barFromBundle ) otherBarsFromBundle.forEach((otherBar) => { const nextGapDistanceAndBars = countGapDistanceToNextImmobileBar(otherBar, gapDist, side) const newGapDistance = nextGapDistanceAndBars.gapDistanceSoFar const newBundleBars = nextGapDistanceAndBars.bundleBarsAndGapDist if (newGapDistance != null && (!totalGapDistance || newGapDistance < totalGapDistance)) { totalGapDistance = newGapDistance } newBundleBars.forEach((newBundleBar) => { if (!bundleBarsOnPath.find((barAndGap) => barAndGap.bar === newBundleBar.bar)) { bundleBarsOnPath.push(newBundleBar) } }) }) } const barElem = document.getElementById(bar.ganttBarConfig.id) as HTMLElement if (totalGapDistance != null && side === "left") { bar.ganttBarConfig.dragLimitLeft = barElem.offsetLeft - totalGapDistance } else if (totalGapDistance != null && side === "right") { bar.ganttBarConfig.dragLimitRight = barElem.offsetLeft + barElem.offsetWidth + totalGapDistance } } // all bars from the bundle of the clicked bar need to have the same drag limit: const barsFromBundleOfClickedBar = getBarsFromBundle(bar.ganttBarConfig.bundle) barsFromBundleOfClickedBar.forEach((barFromBundle) => { barFromBundle.ganttBarConfig.dragLimitLeft = bar.ganttBarConfig.dragLimitLeft barFromBundle.ganttBarConfig.dragLimitRight = bar.ganttBarConfig.dragLimitRight }) } // returns the gap distance to the next immobile bar // in the row where the given bar (parameter) is (added to gapDistanceSoFar) // and a list of all bars on that path that belong to a bundle const countGapDistanceToNextImmobileBar = ( bar: GanttBarObject, gapDistanceSoFar = 0, side: "left" | "right" ) => { const bundleBarsAndGapDist = bar.ganttBarConfig.bundle ? [{ bar, gapDistance: gapDistanceSoFar }] : [] let currentBar = bar let nextBar = getNextGanttBar(currentBar, side) // left side: if (side === "left") { while (nextBar) { const currentBarElem = document.getElementById(currentBar.ganttBarConfig.id) as HTMLElement const nextBarElem = document.getElementById(nextBar.ganttBarConfig.id) as HTMLElement const nextBarOffsetRight = nextBarElem.offsetLeft + nextBarElem.offsetWidth gapDistanceSoFar += currentBarElem.offsetLeft - nextBarOffsetRight if (nextBar.ganttBarConfig.immobile) { return { gapDistanceSoFar, bundleBarsAndGapDist } } else if (nextBar.ganttBarConfig.bundle) { bundleBarsAndGapDist.push({ bar: nextBar, gapDistance: gapDistanceSoFar }) } currentBar = nextBar nextBar = getNextGanttBar(nextBar, "left") } } if (side === "right") { while (nextBar) { const currentBarElem = document.getElementById(currentBar.ganttBarConfig.id) as HTMLElement const nextBarElem = document.getElementById(nextBar.ganttBarConfig.id) as HTMLElement const currentBarOffsetRight = currentBarElem.offsetLeft + currentBarElem.offsetWidth gapDistanceSoFar += nextBarElem.offsetLeft - currentBarOffsetRight if (nextBar.ganttBarConfig.immobile) { return { gapDistanceSoFar, bundleBarsAndGapDist } } else if (nextBar.ganttBarConfig.bundle) { bundleBarsAndGapDist.push({ bar: nextBar, gapDistance: gapDistanceSoFar }) } currentBar = nextBar nextBar = getNextGanttBar(nextBar, "right") } } return { gapDistanceSoFar: null, bundleBarsAndGapDist } } const getNextGanttBar = (bar: GanttBarObject, side: "left" | "right") => { const barElem = document.getElementById(bar.ganttBarConfig.id) as HTMLElement const allBarsInRow = getChartRows().find((row) => row.bars.includes(bar))?.bars ?? [] let allBarsLeftOrRight = [] if (side === "left") { allBarsLeftOrRight = allBarsInRow.filter((otherBar) => { const otherBarElem = document.getElementById(otherBar.ganttBarConfig.id) as HTMLElement return ( otherBarElem && otherBarElem.offsetLeft < barElem.offsetLeft && otherBar.ganttBarConfig.pushOnOverlap !== false ) }) } else { allBarsLeftOrRight = allBarsInRow.filter((otherBar) => { const otherBarElem = document.getElementById(otherBar.ganttBarConfig.id) as HTMLElement return ( otherBarElem && otherBarElem.offsetLeft > barElem.offsetLeft && otherBar.ganttBarConfig.pushOnOverlap !== false ) }) } if (allBarsLeftOrRight.length > 0) { return allBarsLeftOrRight.reduce((bar1, bar2) => { const bar1Elem = document.getElementById(bar1.ganttBarConfig.id) as HTMLElement const bar2Elem = document.getElementById(bar2.ganttBarConfig.id) as HTMLElement const bar1Dist = Math.abs(bar1Elem.offsetLeft - barElem.offsetLeft) const bar2Dist = Math.abs(bar2Elem.offsetLeft - barElem.offsetLeft) return bar1Dist < bar2Dist ? bar1 : bar2 }, allBarsLeftOrRight[0]) } else { return null } } return { setDragLimitsOfGanttBar } } ================================================ FILE: src/composables/useBarDragManagement.ts ================================================ import type { GanttBarObject } from "../types" import createBarDrag from "./createBarDrag.js" import useDayjsHelper from "./useDayjsHelper.js" import provideConfig from "../provider/provideConfig.js" import provideGetChartRows from "../provider/provideGetChartRows.js" import provideEmitBarEvent from "../provider/provideEmitBarEvent.js" export default function useBarDragManagement() { const config = provideConfig() const getChartRows = provideGetChartRows() const emitBarEvent = provideEmitBarEvent() const { pushOnOverlap, barStart, barEnd, noOverlap, dateFormat } = config const movedBarsInDrag = new Map() const { toDayjs, format } = useDayjsHelper() const initDragOfBar = (bar: GanttBarObject, e: MouseEvent) => { const { initDrag } = createBarDrag(bar, onDrag, onEndDrag, config) emitBarEvent({ ...e, type: "dragstart" }, bar) initDrag(e) addBarToMovedBars(bar) } const initDragOfBundle = (mainBar: GanttBarObject, e: MouseEvent) => { const bundle = mainBar.ganttBarConfig.bundle if (bundle == null) { return } getChartRows().forEach((row) => { row.bars.forEach((bar) => { if (bar.ganttBarConfig.bundle === bundle) { const dragEndHandler = bar === mainBar ? onEndDrag : () => null const { initDrag } = createBarDrag(bar, onDrag, dragEndHandler, config) initDrag(e) addBarToMovedBars(bar) } }) }) emitBarEvent({ ...e, type: "dragstart" }, mainBar) } const onDrag = (e: MouseEvent, bar: GanttBarObject) => { emitBarEvent({ ...e, type: "drag" }, bar) fixOverlaps(bar) } const fixOverlaps = (ganttBar: GanttBarObject) => { if (!pushOnOverlap?.value) { return } let currentBar = ganttBar let { overlapBar, overlapType } = getOverlapBarAndType(currentBar) while (overlapBar) { addBarToMovedBars(overlapBar) const currentBarStart = toDayjs(currentBar[barStart.value]) const currentBarEnd = toDayjs(currentBar[barEnd.value]) const overlapBarStart = toDayjs(overlapBar[barStart.value]) const overlapBarEnd = toDayjs(overlapBar[barEnd.value]) let minuteDiff: number switch (overlapType) { case "left": minuteDiff = overlapBarEnd.diff(currentBarStart, "minutes", true) overlapBar[barEnd.value] = format(currentBar[barStart.value], dateFormat.value) overlapBar[barStart.value] = format( overlapBarStart.subtract(minuteDiff, "minutes"), dateFormat.value ) break case "right": minuteDiff = currentBarEnd.diff(overlapBarStart, "minutes", true) overlapBar[barStart.value] = format(currentBarEnd, dateFormat.value) overlapBar[barEnd.value] = format( overlapBarEnd.add(minuteDiff, "minutes"), dateFormat.value ) break default: console.warn( "Vue-Ganttastic: One bar is inside of the other one! This should never occur while push-on-overlap is active!" ) return } if (overlapBar && (overlapType === "left" || overlapType === "right")) { moveBundleOfPushedBarByMinutes(overlapBar, minuteDiff, overlapType) } currentBar = overlapBar ;({ overlapBar, overlapType } = getOverlapBarAndType(overlapBar)) } } const getOverlapBarAndType = (ganttBar: GanttBarObject) => { let overlapLeft, overlapRight, overlapInBetween const allBarsInRow = getChartRows().find((row) => row.bars.includes(ganttBar))?.bars ?? [] const ganttBarStart = toDayjs(ganttBar[barStart.value]) const ganttBarEnd = toDayjs(ganttBar[barEnd.value]) const overlapBar = allBarsInRow.find((otherBar) => { if (otherBar === ganttBar) { return false } const otherBarStart = toDayjs(otherBar[barStart.value]) const otherBarEnd = toDayjs(otherBar[barEnd.value]) overlapLeft = ganttBarStart.isBetween(otherBarStart, otherBarEnd) overlapRight = ganttBarEnd.isBetween(otherBarStart, otherBarEnd) overlapInBetween = otherBarStart.isBetween(ganttBarStart, ganttBarEnd) || otherBarEnd.isBetween(ganttBarStart, ganttBarEnd) return overlapLeft || overlapRight || overlapInBetween }) const overlapType = overlapLeft ? "left" : overlapRight ? "right" : overlapInBetween ? "between" : null return { overlapBar, overlapType } } const moveBundleOfPushedBarByMinutes = ( pushedBar: GanttBarObject, minutes: number, direction: "left" | "right" ) => { addBarToMovedBars(pushedBar) if (!pushedBar.ganttBarConfig.bundle) { return } getChartRows().forEach((row) => { row.bars.forEach((bar) => { if (bar.ganttBarConfig.bundle === pushedBar.ganttBarConfig.bundle && bar !== pushedBar) { addBarToMovedBars(bar) moveBarByMinutes(bar, minutes, direction) } }) }) } const moveBarByMinutes = (bar: GanttBarObject, minutes: number, direction: "left" | "right") => { switch (direction) { case "left": bar[barStart.value] = format( toDayjs(bar, "start").subtract(minutes, "minutes"), dateFormat.value ) bar[barEnd.value] = format( toDayjs(bar, "end").subtract(minutes, "minutes"), dateFormat.value ) break case "right": bar[barStart.value] = format( toDayjs(bar, "start").add(minutes, "minutes"), dateFormat.value ) bar[barEnd.value] = format(toDayjs(bar, "end").add(minutes, "minutes"), dateFormat.value) } fixOverlaps(bar) } const onEndDrag = (e: MouseEvent, bar: GanttBarObject) => { snapBackAllMovedBarsIfNeeded() const ev = { ...e, type: "dragend" } emitBarEvent(ev, bar, undefined, new Map(movedBarsInDrag)) movedBarsInDrag.clear() } const addBarToMovedBars = (bar: GanttBarObject) => { if (!movedBarsInDrag.has(bar)) { const oldStart = bar[barStart.value] const oldEnd = bar[barEnd.value] movedBarsInDrag.set(bar, { oldStart, oldEnd }) } } const snapBackAllMovedBarsIfNeeded = () => { if (pushOnOverlap.value || !noOverlap.value) { return } let isAnyOverlap = false movedBarsInDrag.forEach((_, bar) => { const { overlapBar } = getOverlapBarAndType(bar) if (overlapBar != null) { isAnyOverlap = true } }) if (!isAnyOverlap) { return } movedBarsInDrag.forEach(({ oldStart, oldEnd }, bar) => { bar[barStart.value] = oldStart bar[barEnd.value] = oldEnd }) } return { initDragOfBar, initDragOfBundle } } ================================================ FILE: src/composables/useDayjsHelper.ts ================================================ import dayjs, { type Dayjs } from "dayjs" import { computed } from "vue" import type { GGanttChartConfig } from "../components/GGanttChart.vue" import type { GanttBarObject } from "../types" import provideConfig from "../provider/provideConfig.js" export const DEFAULT_DATE_FORMAT = "YYYY-MM-DD HH:mm" export default function useDayjsHelper(config: GGanttChartConfig = provideConfig()) { const { chartStart, chartEnd, barStart, barEnd, dateFormat } = config const chartStartDayjs = computed(() => toDayjs(chartStart.value)) const chartEndDayjs = computed(() => toDayjs(chartEnd.value)) const toDayjs = (input: string | Date | GanttBarObject, startOrEnd?: "start" | "end") => { let value if (startOrEnd !== undefined && typeof input !== "string" && !(input instanceof Date)) { value = startOrEnd === "start" ? input[barStart.value] : input[barEnd.value] } if (typeof input === "string") { value = input } else if (input instanceof Date) { return dayjs(input) } const format = dateFormat.value || DEFAULT_DATE_FORMAT return dayjs(value, format, true) } const format = (input: string | Date | Dayjs, pattern?: string | false) => { if (pattern === false) { return input instanceof Date ? input : dayjs(input).toDate() } const inputDayjs = typeof input === "string" || input instanceof Date ? toDayjs(input) : input return inputDayjs.format(pattern) } return { chartStartDayjs, chartEndDayjs, toDayjs, format } } ================================================ FILE: src/composables/useTimePositionMapping.ts ================================================ import type { GGanttChartConfig } from "../components/GGanttChart.vue" import { computed } from "vue" import useDayjsHelper from "./useDayjsHelper.js" import provideConfig from "../provider/provideConfig.js" export default function useTimePositionMapping(config: GGanttChartConfig = provideConfig()) { const { dateFormat, chartSize } = config const { chartStartDayjs, chartEndDayjs, toDayjs, format } = useDayjsHelper(config) const totalNumOfMinutes = computed(() => { return chartEndDayjs.value.diff(chartStartDayjs.value, "minutes") }) const mapTimeToPosition = (time: string) => { const width = chartSize.width.value || 0 const diffFromStart = toDayjs(time).diff(chartStartDayjs.value, "minutes", true) return Math.ceil((diffFromStart / totalNumOfMinutes.value) * width) } const mapPositionToTime = (xPos: number) => { const width = chartSize.width.value || 0 const diffFromStart = (xPos / width) * totalNumOfMinutes.value return format(chartStartDayjs.value.add(diffFromStart, "minutes"), dateFormat.value) } return { mapTimeToPosition, mapPositionToTime } } ================================================ FILE: src/composables/useTimeaxisUnits.ts ================================================ import { computed } from "vue" import useDayjsHelper from "./useDayjsHelper.js" import provideConfig from "../provider/provideConfig.js" export default function useTimeaxisUnits() { const { precision } = provideConfig() const { chartStartDayjs, chartEndDayjs } = useDayjsHelper() const upperPrecision = computed(() => { switch (precision?.value) { case "hour": return "day" case "day": return "month" case "date": case "week": return "month" case "month": return "year" default: throw new Error( "Precision prop incorrect. Must be one of the following: 'hour', 'day', 'date', 'week', 'month'" ) } }) const lowerPrecision = computed(() => { switch (precision.value) { case "date": return "day" case "week": return "isoWeek" default: return precision.value } }) const displayFormats = { hour: "HH", date: "DD.MMM", day: "DD.MMM", week: "WW", month: "MMMM YYYY", year: "YYYY" } const timeaxisUnits = computed(() => { const upperUnits: { label: string; value?: string; date: Date; width?: string }[] = [] const lowerUnits: { label: string; value?: string; date: Date; width?: string }[] = [] const totalMinutes = chartEndDayjs.value.diff(chartStartDayjs.value, "minutes", true) const upperUnit = upperPrecision.value const lowerUnit = lowerPrecision.value let currentUpperUnit = chartStartDayjs.value let currentLowerUnit = chartStartDayjs.value while (currentLowerUnit.isSameOrBefore(chartEndDayjs.value)) { const endCurrentLowerUnit = currentLowerUnit.endOf(lowerUnit) const isLastItem = endCurrentLowerUnit.isAfter(chartEndDayjs.value) const lowerWidth = isLastItem ? (chartEndDayjs.value.diff(currentLowerUnit, "minutes", true) / totalMinutes) * 100 : (endCurrentLowerUnit.diff(currentLowerUnit, "minutes", true) / totalMinutes) * 100 lowerUnits.push({ label: currentLowerUnit.format(displayFormats[precision?.value]), value: String(currentLowerUnit), date: currentLowerUnit.toDate(), width: String(lowerWidth) + "%" }) currentLowerUnit = endCurrentLowerUnit .add(1, lowerUnit === "isoWeek" ? "week" : lowerUnit) .startOf(lowerUnit) } while (currentUpperUnit.isSameOrBefore(chartEndDayjs.value)) { const endCurrentUpperUnit = currentUpperUnit.endOf(upperUnit) const isLastItem = endCurrentUpperUnit.isAfter(chartEndDayjs.value) const upperWidth = isLastItem ? (chartEndDayjs.value.diff(currentUpperUnit, "minutes", true) / totalMinutes) * 100 : (endCurrentUpperUnit.diff(currentUpperUnit, "minutes", true) / totalMinutes) * 100 upperUnits.push({ label: currentUpperUnit.format(displayFormats[upperUnit]), value: String(currentUpperUnit), date: currentUpperUnit.toDate(), width: String(upperWidth) + "%" }) currentUpperUnit = endCurrentUpperUnit.add(1, upperUnit).startOf(upperUnit) } return { upperUnits, lowerUnits } }) return { timeaxisUnits } } ================================================ FILE: src/playground.ts ================================================ import { createApp } from "vue" import Playground from "./GanttPlayground.vue" import ganttastic from "./vue-ganttastic.js" createApp(Playground).use(ganttastic).mount("#app") ================================================ FILE: src/provider/provideConfig.ts ================================================ import { inject } from "vue" import { CONFIG_KEY } from "./symbols.js" export default function provideConfig() { const config = inject(CONFIG_KEY) if (!config) { throw Error("Failed to inject config!") } return config } ================================================ FILE: src/provider/provideEmitBarEvent.ts ================================================ import { inject } from "vue" import { EMIT_BAR_EVENT_KEY } from "./symbols.js" export default function provideEmitBarEvent() { const emitBarEvent = inject(EMIT_BAR_EVENT_KEY) if (!emitBarEvent) { throw Error("Failed to inject emitBarEvent!") } return emitBarEvent } ================================================ FILE: src/provider/provideGetChartRows.ts ================================================ import { inject } from "vue" import { CHART_ROWS_KEY } from "./symbols.js" export default function provideGetChartRows() { const getChartRows = inject(CHART_ROWS_KEY) if (!getChartRows) { throw Error("Failed to inject getChartRows!") } return getChartRows } ================================================ FILE: src/provider/symbols.ts ================================================ import type { InjectionKey, Ref } from "vue" import type { GGanttChartConfig } from "../components/GGanttChart.vue" import type { GanttBarObject } from "../types" export type ChartRow = { label: string; bars: GanttBarObject[] } export type GetChartRows = () => ChartRow[] export type EmitBarEvent = ( e: MouseEvent, bar: GanttBarObject, datetime?: string | Date, movedBars?: Map ) => void export const CHART_ROWS_KEY = Symbol("CHART_ROWS_KEY") as InjectionKey export const CONFIG_KEY = Symbol("CONFIG_KEY") as InjectionKey export const EMIT_BAR_EVENT_KEY = Symbol("EMIT_BAR_EVENT_KEY") as InjectionKey export const BAR_CONTAINER_KEY = Symbol("BAR_CONTAINER_KEY") as InjectionKey< Ref > ================================================ FILE: src/types.ts ================================================ import type { CSSProperties } from "vue" export type GanttBarObject = { // eslint-disable-next-line @typescript-eslint/no-explicit-any [key: string]: any ganttBarConfig: { id: string label?: string html?: string hasHandles?: boolean immobile?: boolean bundle?: string pushOnOverlap?: boolean dragLimitLeft?: number dragLimitRight?: number style?: CSSProperties class?: string } } ================================================ FILE: src/vue-ganttastic.ts ================================================ import type { Plugin } from "vue" import dayjs from "dayjs" import isoWeek from "dayjs/plugin/isoWeek" import isSameOrBefore from "dayjs/plugin/isSameOrBefore.js" import isSameOrAfter from "dayjs/plugin/isSameOrAfter.js" import isBetween from "dayjs/plugin/isBetween.js" import weekOfYear from "dayjs/plugin/weekOfYear" import advancedFormat from "dayjs/plugin/advancedFormat" import customParseFormat from "dayjs/plugin/customParseFormat.js" import type { GanttBarObject } from "./types.js" import type { ColorScheme } from "./color-schemes" import GGanttChart from "./components/GGanttChart.vue" import GGanttRow from "./components/GGanttRow.vue" export function extendDayjs() { dayjs.extend(isSameOrBefore) dayjs.extend(isSameOrAfter) dayjs.extend(isBetween) dayjs.extend(customParseFormat) dayjs.extend(weekOfYear) dayjs.extend(isoWeek) dayjs.extend(advancedFormat) } export type { ColorScheme, GanttBarObject } export { GGanttChart, GGanttRow } export const ganttastic: Plugin = { install(app, options?) { extendDayjs() app.component("GGanttChart", GGanttChart) app.component("GGanttRow", GGanttRow) } } export default ganttastic ================================================ FILE: tsconfig.config.json ================================================ { "extends": "@vue/tsconfig/tsconfig.node.json", "include": ["vite.config.*", "vitest.config.*", "cypress.config.*"], "compilerOptions": { "composite": true, "types": ["node"] } } ================================================ FILE: tsconfig.json ================================================ { "extends": "@vue/tsconfig/tsconfig.web.json", "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], "compilerOptions": { "baseUrl": "." }, "references": [ { "path": "./tsconfig.config.json" } ] } ================================================ FILE: vite.config.mts ================================================ import { fileURLToPath, URL } from "node:url" import { defineConfig } from "vite" import vue from "@vitejs/plugin-vue" import postcssPresetEnv from "postcss-preset-env" import styleInject from "@senojs/rollup-plugin-style-inject" // https://vitejs.dev/config/ export default defineConfig({ plugins: [ vue(), styleInject({ insertAt: "top" }) ], css: { postcss: { plugins: [postcssPresetEnv()] } }, build: { lib: process.env.NODE_ENV === "production" ? { entry: fileURLToPath( new URL("src/vue-ganttastic.ts", import.meta.url) ), name: "VueGanttastic", fileName: "vue-ganttastic" } : undefined, outDir: process.env.NODE_ENV === "production" ? "lib" : "dist", rollupOptions: { // make sure to externalize deps that shouldn't be bundled // into the library external: ["vue", "dayjs"], output: { // Provide global variables to use in the UMD build // for externalized deps globals: { vue: "Vue", dayjs: "dayjs" }, exports: "named" } } } })