Publish v1

This commit is contained in:
Safwan Samsudeen 2025-01-09 14:26:07 +05:30 committed by GitHub
commit 1d78ec1be9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 2199 additions and 1314 deletions

View File

@ -1,7 +0,0 @@
{
"extends": ["plugin:prettier/recommended"],
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module"
}
}

BIN
.github/gantt-logo.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

BIN
.github/hero-image.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

View File

@ -1,7 +1,7 @@
name: Publish on NPM name: Publish on NPM
on: on:
push: push:
branches: [master] branches: [release]
jobs: jobs:
publish: publish:

2
.gitignore vendored
View File

@ -31,4 +31,4 @@ node_modules
.DS_Store .DS_Store
gh-pages gh-pages
feedback.md feedback*.md

209
README.md
View File

@ -1,123 +1,158 @@
<div align="center"> <div align="center" markdown="1">
<img src="https://github.com/frappe/design/blob/master/logos/logo-2019/frappe-gantt-logo.png" height="128"> <img src=".github/gantt-logo.jpg" width="80">
<h2>Frappe Gantt</h2> <h1>Frappe Gantt</h1>
<p align="center">
<p>A simple, interactive, modern gantt chart library for the web</p> **A modern, configurable, Gantt library for the web.**
<a href="https://frappe.github.io/gantt">
<b>View the demo »</b>
</a>
</p>
</div> </div>
<p align="center"> ![Hero Image](.github/hero-image.png)
<a href="https://frappe.github.io/gantt">
<img src="https://cloud.githubusercontent.com/assets/9355208/21537921/4a38b194-cdbd-11e6-8110-e0da19678a6d.png">
</a>
</p>
### Install ## Frappe Gantt
Gantt charts are bar charts that visually illustrate a project's tasks, schedule, and dependencies. With Frappe Gantt, you can build beautiful, customizable, Gantt charts with ease.
``` You can use it anywhere from hobby projects to tracking the goals of your team at the worksplace.
[ERPNext](https://erpnext.com/) uses Frappe Gantt.
### Motivation
We needed a Gantt View for ERPNext. Surprisingly, we couldn't find a visually appealing Gantt library that was open source - so we decided to build it. Initially, the design was heavily inspired by Google Gantt and DHTMLX.
### Key Features
- **Customizable Views**: customize the timeline based on various time periods - day, hour, or year, you have it. You can also create your own views.
- **Ignore Periods**: exclude weekends and other holidays from your tasks' progress calculation.
- **Configure Anything**: spacing, edit access, labels, you can control it all. Change both the style and functionality to meet your needs.
- **Multi-lingual Support**: suitable for companies with an international base.
## Usage
Install with:
```bash
npm install frappe-gantt npm install frappe-gantt
``` ```
### Usage
Include it in your HTML: Include it in your HTML:
``` ```html
<script src="frappe-gantt.min.js"></script> <script src="frappe-gantt.umd.js"></script>
<link rel="stylesheet" href="frappe-gantt.css"> <link rel="stylesheet" href="frappe-gantt.css">
``` ```
Or from the CDN: Or from the CDN:
``` ```html
<script src="https://cdn.jsdelivr.net/npm/frappe-gantt/dist/frappe-gantt.umd.js"></script> <script src="https://cdn.jsdelivr.net/npm/frappe-gantt/dist/frappe-gantt.umd.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/frappe-gantt/dist/frappe-gantt.css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/frappe-gantt/dist/frappe-gantt.css">
``` ```
And start hacking:
Start using Gantt:
```js ```js
var tasks = [ let tasks = [
{ {
id: 'Task 1', id: '1',
name: 'Redesign website', name: 'Redesign website',
start: '2016-12-28', start: '2016-12-28',
end: '2016-12-31', end: '2016-12-31',
progress: 20, progress: 20
dependencies: 'Task 2, Task 3',
custom_class: 'bar-milestone' // optional
}, },
... ...
] ]
var gantt = new Gantt("#gantt", tasks); let gantt = new Gantt("#gantt", tasks);
``` ```
You can also pass various options to the Gantt constructor: ### Configuration
Frappe Gantt offers a wide range of options to customize your chart.
```js
var gantt = new Gantt('#gantt', tasks, {
header_height: 50,
column_width: 30,
step: 24,
view_modes: ['Quarter Day', 'Half Day', 'Day', 'Week', 'Month'],
bar_height: 20,
bar_corner_radius: 3,
arrow_curve: 5,
padding: 18,
view_mode: 'Day',
date_format: 'YYYY-MM-DD',
language: 'en', // or 'es', 'it', 'ru', 'ptBr', 'fr', 'tr', 'zh', 'de', 'hu'
popup: null,
});
```
You can add `dark` class to the container element to apply dark theme. | **Option** | **Description** | **Possible Values** | **Default** |
|---------------------------|---------------------------------------------------------------------------------|----------------------------------------------------|------------------------------------|
| `arrow_curve` | Curve radius of arrows connecting dependencies. | Any positive integer. | `5` |
| `auto_move_label` | Move task labels when user scrolls horizontally. | `true`, `false` | `false` |
| `bar_corner_radius` | Radius of the task bar corners (in pixels). | Any positive integer. | `3` |
| `bar_height` | Height of task bars (in pixels). | Any positive integer. | `30` |
| `container_height` | Height of the container. | `auto` - dynamic container height to fit all tasks - _or_ any positive integer (for pixels). | `auto` |
| `column_width` | Width of each column in the timeline. | Any positive integer. | 45 |
| `date_format` | Format for displaying dates. | Any valid JS date format string. | `YYYY-MM-DD` |
| `upper_header_height` | Height of the upper header in the timeline (in pixels). | Any positive integer. | `45` |
| `lower_header_height` | Height of the lower header in the timeline (in pixels). | Any positive integer. | `30` |
| `snap_at` | Snap tasks at particular intervel while resizing or dragging. | Any _interval_ (see below) | `1d` |
| `infinite_padding` | Whether to extend timeline infinitely when user scrolls. | `true`, `false` | `true` |
| `holidays` | Highlighted holidays on the timeline. | Object mapping CSS colors to holiday types. Types can either be a) 'weekend', or b) array of _strings_ or _date objects_ or _objects_ in the format `{date: ..., label: ...}` | `{ 'var(--g-weekend-highlight-color)': 'weekend' }` |
| `ignore` | Ignored areas in the rendering | `weekend` _or_ Array of strings or date objects (`weekend` can be present to the array also). | `[]` |
| `language` | Language for localization. | ISO 639-1 codes like `en`, `fr`, `es`. | `en` |
| `lines` | Determines which grid lines to display. | `none` for no lines, `vertical` for only vertical lines, `horizontal` for only horizontal lines, `both` for complete grid. | `both` |
| `move_dependencies` | Whether moving a task automatically moves its dependencies. | `true`, `false` | `true` |
| `padding` | Padding around task bars (in pixels). | Any positive integer. | `18` |
| `popup_on` | Event to trigger the popup display. | `click` _or_ `hover` | `click` |
| `readonly_progress` | Disables editing task progress. | `true`, `false` | `false` |
| `readonly_dates` | Disables editing task dates. | `true`, `false` | `false` |
| `readonly` | Disables all editing features. | `true`, `false` | `false` |
| `scroll_to` | Determines the starting point when chart is rendered. | `today`, `start`, `end`, or a date string. | `today` |
| `show_expected_progress` | Shows expected progress for tasks. | `true`, `false` | `false` |
| `today_button` | Adds a button to navigate to todays date. | `true`, `false` | `true` |
| `view_mode` | The initial view mode of the Gantt chart. | `Day`, `Week`, `Month`, `Year`. | `Day` |
| `view_mode_select` | Allows selecting the view mode from a dropdown. | `true`, `false` | `false` |
```html Apart from these ones, two options - `popup` and `view_modes` (plural, not singular) - are available. They have "sub"-APIs, and thus are listed separately.
<div class="gantt-target dark"></div>
```
### Contributing #### View Mode Configuration
The `view_modes` option determines all the available view modes for the chart. It should be an array of objects.
Each object can have the following properties:
- `name` (string) - the name of view mode.
- `padding` (interval) - the time above.
- `step` - the interval of each column
- `lower_text` (date format string _or_ function) - the format for text in lower header. Blank string for none. The function takes in `currentDate`, `previousDate`, and `lang`, and should return a string.
- `upper_text` (date format string _or_ function) - the format for text in upper header. Blank string for none. The function takes in `currentDate`, `previousDate`, and `lang`, and should return a string.
- `upper_text_frequency` (number) - how often the upper text has a value. Utilized in internal calculation to improve performance.
- `thick_line` (function) - takes in `currentDate`, returns Boolean determining whether the line for that date should be thicker than the others.
Three other options allow you to override general configuration for this view mode alone:
- `date_format`
- `column_width`
- `snap_at`
For details, see the above table.
#### Popup Configuration
`popup` is a function. If it returns
- `false`, there will be no popup.
- `undefined`, the popup will be rendered based on manipulation within the function
- a HTML string, the popup will be that string.
The function receives one object as an argument, containing:
- `task` - the task as an object
- `chart` - the entire Gantt chart
- `get_title`, `get_subtitle`, `get_details` (functions) - get the relevant section as a HTML node.
- `set_title`, `set_subtitle`, `set_details` (functions) - take in the HTML of the relevant section
- `add_action` (function) - accepts two parameters, `html` and `func` - respectively determining the HTML of the action and the callback when the action is pressed.
### API
Frappe Gantt exposes a few helpful methods for you to interact with the chart:
| **Name** | **Description** | **Parameters** |
|---------------------------|---------------------------------------------------------------------------------|------------------------------------------|
| `.update_options` | Re-renders the chart after updating specific options. | `new_options` - object containing new options. |
| `.change_view_mode` | Updates the view mode. | `view_mode` - Name of view mode _or_ view mode object (see above) and `maintain_pos` - whether to go back to current scroll position after rerendering, defaults to `false`. |
| `.scroll_current` | Scrolls to the current date | No parameters. |
| `.update_task` | Re-renders a specific task bar alone | `task_id` - id of task and `new_details` - object containing the task properties to be updated. |
## Development Setup
If you want to contribute enhancements or fixes: If you want to contribute enhancements or fixes:
1. Clone this repo. 1. Clone this repo.
2. `cd` into project directory 2. `cd` into project directory.
3. `yarn` 3. Run `pnpm i` to install dependencies.
4. `yarn run dev` 4. `pnpm run build` to build files - or `pnpm run build-dev` to build and watch for changes.
5. Open `index.html` in your browser, make your code changes and test them. 5. Open `index.html` in your browser.
6. Make your code changes and test them.
### Publishing <br />
<br />
If you have publishing rights (Frappe Team), follow these steps to publish a new version. <div align="center" style="padding-top: 0.75rem;">
<a href="https://frappe.io" target="_blank">
Assuming the last commit (or a couple of commits) were enhancements or fixes, <picture>
<source media="(prefers-color-scheme: dark)" srcset="https://frappe.io/files/Frappe-white.png">
1. Run `yarn build` <img src="https://frappe.io/files/Frappe-black.png" alt="Frappe Technologies" height="28"/>
</picture>
This will generate files in the `dist/` folder. These files need to be committed. </a>
</div>
1. Run `yarn publish`
1. Type the new version at the prompt
Depending on the type of change, you can either bump the patch version or the minor version.
For e.g.,
```
0.5.0 -> 0.6.0 (minor version bump)
0.5.0 -> 0.5.1 (patch version bump)
```
1. Now, there will be a commit named after the version you just entered. Include the generated files in `dist/` folder as part of this commit by running the command:
```
git add dist
git commit --amend
git push origin master
```
License: MIT
---
Project maintained by [frappe](https://github.com/frappe)

115
builder/demo.css Normal file
View File

@ -0,0 +1,115 @@
.switch {
position: relative;
display: inline-block;
width: 50px;
height: 20px;
float: right;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ddd;
-webkit-transition: 0.2s;
transition: 0.2s;
border: 1px solid #37352f;
scale: 0.75;
}
.slider:before {
position: absolute;
content: '';
height: 12px;
width: 12px;
left: 4px;
bottom: 3px;
background-color: white;
-webkit-transition: 0.2s;
transition: 0.2s;
}
input:checked + .slider {
background-color: #7c7c7c;
border-color: #7c7c7c;
}
input:focus + .slider {
box-shadow: none;
}
input:checked + .slider:before {
-webkit-transform: translateX(28px);
-ms-transform: translateX(28px);
transform: translateX(28px);
}
.slider.round {
border-radius: 25px;
}
.slider.round:before {
border-radius: 50%;
}
.viewmode-select {
font-size: 100%;
}
.selected {
border: 1.5px solid black !important;
}
.button {
background: white;
border: 1px dotted black;
border-radius: 3px;
}
.button:hover {
background: #f4f5f6;
border: 1px dotted black;
}
.button div {
color: black;
}
.input-switch {
align-items: center;
width: 45%;
display: flex;
justify-content: space-between;
}
.input-switch label {
padding-right: 30px;
font-size: 14px;
}
.code {
display: block;
background: 0;
white-space: pre;
overflow-x: scroll;
max-width: 100%;
min-width: 100px;
padding: 0;
font-family: monospace;
padding-top: 0.8571429em;
padding-right: 1.1428571em;
padding-bottom: 0.8571429em;
padding-left: 1.1428571em;
background: #1f2937;
color: #e5e7eb;
border-radius: 3px;
}

326
builder/demo.js Normal file
View File

@ -0,0 +1,326 @@
const tasks = [
{
start: daysSince(-7),
end: daysSince(-5),
name: 'Initial brainstorming',
id: 'Task 0',
progress: random(),
},
{
start: daysSince(-3),
end: daysSince(1),
name: 'Develop wireframe',
id: 'Task 1',
progress: random(),
dependencies: 'Task 0',
},
{
start: daysSince(-1),
duration: '4d',
name: 'Client meeting',
id: 'Task 2',
progress: random(),
important: true,
},
{
start: daysSince(1),
duration: '7d',
name: 'Create prototype',
id: 'Task 3',
dependencies: 'Task 2',
progress: random(),
},
{
start: daysSince(3),
duration: '5d',
name: 'Test design with users',
dependencies: 'Task 2',
id: 'Task 4',
progress: random(),
important: true,
},
{
start: daysSince(5),
end: daysSince(10),
name: 'Write technical documentation',
id: 'Task 5',
progress: random(),
},
{
start: daysSince(8),
duration: '3d',
name: 'Prepare demo',
id: 'Task 6',
dependencies: 'Task 5',
progress: random(),
},
{
start: daysSince(10),
end: daysSince(12),
name: 'Final client review',
id: 'Task 7',
progress: 0,
important: true,
},
{
start: daysSince(14),
duration: '6d',
name: 'Implement feedback',
id: 'Task 8',
progress: 0,
},
];
const tasksSmall = [
{
start: daysSince(-2),
end: daysSince(2),
name: 'Redesign website',
id: 'Task 0',
progress: random(),
},
{
start: daysSince(3),
duration: '6d',
name: 'Write new content',
id: 'Task 1',
progress: random(),
important: true,
dependencies: 'Task 0',
},
{
start: daysSince(4),
duration: '2d',
name: 'Apply new styles',
id: 'Task 2',
progress: random(),
},
{
start: daysSince(-4),
end: daysSince(0),
name: 'Review',
id: 'Task 3',
progress: random(),
},
];
const tasksBlank = [
{
start: daysSince(1),
duration: '3d',
name: 'Marketing Strategy Review',
id: 'Task 1',
important: true,
},
{
start: daysSince(-2),
end: daysSince(12),
name: 'Mentor Sooriya',
id: 'Task 0',
},
{
start: daysSince(4),
end: daysSince(5),
name: 'Investors Meetup',
id: 'Task 3',
},
];
const HOLIDAYS = [
{ name: 'New Years Day', date: '2025-01-01' },
{ name: 'Republic Day', date: '2025-01-26' },
{ name: 'Maha Shivratri', date: '2025-02-23' },
{ name: 'Holi', date: '2025-03-11' },
{ name: 'Mahavir Jayanthi', date: '2025-04-07' },
{ name: 'Good Friday', date: '2025-04-10' },
{ name: 'May Day', date: '2025-05-01' },
{ name: 'Buddha Purnima', date: '2025-05-08' },
{ name: 'Krishna Janmastami', date: '2025-08-14' },
{ name: 'Independence Day', date: '2025-08-15' },
{ name: 'Ganesh Chaturthi', date: '2025-08-23' },
{ name: 'Id-Ul-Fitr', date: '2025-09-21' },
{ name: 'Vijaya Dashami', date: '2025-09-28' },
{ name: 'Mahatma Gandhi Jayanti', date: '2025-10-02' },
{ name: 'Diwali', date: '2025-10-17' },
{ name: 'Guru Nanak Jayanthi', date: '2025-11-02' },
{ name: 'Christmas', date: '2025-12-25' },
];
new Gantt('#central-demo', tasks, {
scroll_to: daysSince(-7),
infinite_padding: false,
});
const sideheader = new Gantt('#sideheader', tasksSmall, {
scroll_to: daysSince(-20),
view_mode_select: true,
infinite_padding: false,
});
const popup = new Gantt('#popup', tasksBlank, {
scroll_to: daysSince(-7),
infinite_padding: false,
container_height: 350,
popup: (ctx) => {
ctx.set_title(ctx.task.name);
let title = ctx.get_title();
title.style.border = '0.5px solid black';
title.style.borderRadius = '1.5px';
title.style.padding = '3px 5px ';
title.style.backgroundColor = 'black';
title.style.opacity = '0.85';
title.style.color = 'white';
title.style.width = 'fit-content';
title.onclick = () => {
let ans = prompt('New Title: ');
if (ans) ctx.set_title(ans);
};
if (ctx.task.description) ctx.set_subtitle(ctx.task.description);
else ctx.set_subtitle('');
ctx.set_details(
`<em>Duration</em>: ${ctx.task.actual_duration} days<br/><em>Dates</em>: ${ctx.task._start.toLocaleDateString('en-US')} - ${ctx.task._end.toLocaleDateString('en-US')}`,
);
let details = ctx.get_details();
details.style.lineHeight = '1.75';
details.style.margin = '10px 4px';
if (!ctx.chart.options.readonly) {
if (!ctx.chart.options.readonly_progress) {
ctx.add_action('Set Color', (task, chart) => {
const bar = chart.bars.find(
({ task: t }) => t.id === task.id,
).$bar;
bar.style.fill = `hsla(${~~(360 * Math.random())}, 70%, 72%, 0.8)`;
});
}
}
},
});
const holidays = new Gantt('#holidays', tasks, {
holidays: {
'var(--g-weekend-highlight-color)': [],
'#fffddb': HOLIDAYS,
},
ignore: ['weekend'],
infinite_padding: false,
container_height: 350,
scroll_to: daysSince(-7),
});
SWITCHES = {
'sideheader-form': {
'toggle-today': 'Scroll to today: ',
'toggle-view-mode': 'Change view mode: ',
},
'holiday-subform': {
'toggle-weekends': ['Mark weekends: ', false],
'ignore-weekends': 'Exclude weekends: ',
},
};
for (let form of ['sideheader-form', 'holiday-form']) {
let formNode = document.getElementById(form);
let parent = formNode.parentElement;
parent.appendChild(formNode);
}
for (let form in SWITCHES) {
for (let id in SWITCHES[form]) {
createSwitch(form, id, SWITCHES[form][id]);
}
}
const UPDATES = [
[
sideheader,
{
'toggle-today': 'today_button',
'toggle-view-mode': 'view_mode_select',
},
],
[
holidays,
{
'toggle-weekends': (val, opts) => ({
holidays: {
'#fffddb': opts.holidays['#fffddb'],
'var(--g-weekend-highlight-color)': val ? 'weekend' : [],
},
ignore: [],
}),
'declare-holiday': (val, opts) => ({
holidays: {
'#fffddb': [...HOLIDAYS, { date: val, name: 'Kay' }],
'var(--g-weekend-highlight-color)':
opts.holidays['var(--g-weekend-highlight-color)'],
},
}),
'ignore-weekends': (val, opts) => ({
ignore: [
opts.ignore.filter((k) => k !== 'weekend')[0],
...(val ? ['weekend'] : []),
],
holidays: { '#fffddb': opts.holidays['#fffddb'] },
}),
'declare-ignore': (val, opts) => ({
ignore: [
...(opts.ignore.includes('weekend') ? ['weekend'] : []),
val,
],
}),
},
(id, val) => {
let el = document.getElementById(id);
if (id === 'toggle-weekends' && val) {
document.getElementById('ignore-weekends').checked = false;
}
if (id === 'ignore-weekends' && val) {
document.getElementById('toggle-weekends').checked = false;
}
},
],
];
for (let [chart, details, after] of UPDATES) {
for (let id in details) {
let el = document.getElementById(id);
el.onchange = (e) => {
let label = details[id];
let val;
if (e.currentTarget.type === 'checkbox') {
if (typeof label === 'string') {
let opposite = label.slice(0, 5) === 'opp__';
if (opposite) label = label.slice(5);
val = opposite
? !e.currentTarget.checked
: e.currentTarget.checked;
} else if (typeof label === 'object') {
val = label[e.currentTarget.checked ? 1 : 2];
label = label[0];
} else {
val =
e.currentTarget.type === 'checkbox'
? e.currentTarget.checked
: e.currentTarget.value;
}
} else {
val =
e.currentTarget.type === 'date'
? e.currentTarget.value
: +e.currentTarget.value;
}
if (typeof label === 'function') {
console.log('ha', label(val, chart.options));
chart.update_options(label(val, chart.options));
} else {
chart.update_options({
[label]: val,
});
}
after && after(id, val, chart);
};
}
}

19
eslint.config.mjs Normal file
View File

@ -0,0 +1,19 @@
import path from "node:path";
import { fileURLToPath } from "node:url";
import js from "@eslint/js";
import { FlatCompat } from "@eslint/eslintrc";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all
});
export default [...compat.extends("plugin:prettier/recommended"), {
languageOptions: {
ecmaVersion: 6,
sourceType: "module",
},
}];

View File

@ -1,132 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Simple Gantt</title>
<style>
body {
font-family: sans-serif;
background: #ccc;
}
.container {
width: 80%;
margin: 0 auto;
}
/* custom class */
.gantt .bar-milestone .bar {
fill: tomato;
}
.heading {
text-align: center;
}
.gantt-target.dark {
background-color: #252525;
}
</style>
<link rel="stylesheet" href="dist/frappe-gantt.css" />
<script src="dist/frappe-gantt.umd.js"></script>
</head>
<body>
<div class="container">
<h2 class="heading">
Interactive Gantt Chart entirely made in SVG!
</h2>
<div class="gantt-target"></div>
</div>
<script type="module">
let tasks = [
{
start: '2024-04-01',
end: '2024-04-04',
name: 'Redesign website',
id: 'Task 0',
progress: 30,
},
{
start: '2024-03-26',
// Utilizes duration
duration: '6d',
name: 'Write new content',
id: 'Task 1',
progress: 5,
important: true,
},
{
start: '2024-04-04',
end: '2024-04-08',
name: 'Apply new styles',
id: 'Task 2',
progress: 80,
dependencies: 'Task 1',
},
{
start: '2024-04-08',
end: '2024-04-09',
name: 'Review',
id: 'Task 3',
progress: 5,
dependencies: 'Task 2',
},
{
start: '2024-03-08',
end: '2024-05-10',
name: 'Deploy',
id: 'Task 4',
progress: 0,
// dependencies: 'Task 2'
},
{
start: '2024-04-21',
end: '2024-05-29',
name: 'Go Live!',
id: 'Task 5',
progress: 0,
dependencies: 'Task 2',
custom_class: 'bar-milestone',
},
// {
// start: '2014-01-05',
// end: '2019-10-12',
// name: 'Long term task',
// id: 'Task 6',
// progress: 0,
// },
];
// Uncomment to test fixed header
// tasks = [
// ...tasks,
// ...Array.from({ length: tasks.length * 3 }, (_, i) => ({
// ...tasks[i % 3],
// id: i,
// })),
// ];
let gantt_chart = new Gantt('.gantt-target', tasks, {
on_click(task) {
console.log('Click', task);
},
// on_hover (task, x, y) {
// console.log("Hover", x, y);
// }
view_mode: 'Month',
// view_modes: [
// {
// name: 'Custom Day',
// padding: '1m',
// step: '1d',
// },
// ],
// popup_on: 'click',
// move_dependencies: false,
// scroll_to: 'today',
// view_mode_select: true,
// dates_readonly: true,
// today_button: false,
// readonly: true,
// lines: 'vertical',
// lower_text: (date) => date.getDay(),
// upper_text: (date, view_mode, def) => def,
});
</script>
</body>
</html>

View File

@ -1,11 +1,10 @@
{ {
"name": "frappe-gantt", "name": "frappe-gantt",
"version": "0.9.0", "version": "1.0.0",
"description": "A simple, modern, interactive gantt library for the web", "description": "A simple, modern, interactive gantt library for the web",
"main": "src/index.js", "main": "src/index.js",
"type": "module", "type": "module",
"scripts": { "scripts": {
"start": "yarn run dev",
"dev": "vite", "dev": "vite",
"build-dev": "vite build --watch", "build-dev": "vite build --watch",
"build": "vite build", "build": "vite build",

View File

@ -21,64 +21,70 @@ export default class Arrow {
while (condition()) { while (condition()) {
start_x -= 10; start_x -= 10;
} }
start_x -= 10;
const start_y = let start_y =
this.gantt.options.header_height + this.gantt.config.header_height +
this.gantt.options.bar_height + this.gantt.options.bar_height +
(this.gantt.options.padding + this.gantt.options.bar_height) * (this.gantt.options.padding + this.gantt.options.bar_height) *
this.from_task.task._index + this.from_task.task._index +
this.gantt.options.padding; this.gantt.options.padding / 2;
const end_x = let end_x = this.to_task.$bar.getX() - 13;
this.to_task.$bar.getX() - this.gantt.options.padding / 2 - 7; let end_y =
const end_y = this.gantt.config.header_height +
this.gantt.options.header_height +
this.gantt.options.bar_height / 2 + this.gantt.options.bar_height / 2 +
(this.gantt.options.padding + this.gantt.options.bar_height) * (this.gantt.options.padding + this.gantt.options.bar_height) *
this.to_task.task._index + this.to_task.task._index +
this.gantt.options.padding; this.gantt.options.padding / 2;
const from_is_below_to = const from_is_below_to =
this.from_task.task._index > this.to_task.task._index; this.from_task.task._index > this.to_task.task._index;
const curve = this.gantt.options.arrow_curve;
const clockwise = from_is_below_to ? 1 : 0;
const curve_y = from_is_below_to ? -curve : curve;
const offset = from_is_below_to
? end_y + this.gantt.options.arrow_curve
: end_y - this.gantt.options.arrow_curve;
this.path = ` let curve = this.gantt.options.arrow_curve;
M ${start_x} ${start_y} const clockwise = from_is_below_to ? 1 : 0;
V ${offset} let curve_y = from_is_below_to ? -curve : curve;
a ${curve} ${curve} 0 0 ${clockwise} ${curve} ${curve_y}
L ${end_x} ${end_y}
m -5 -5
l 5 5
l -5 5`;
if ( if (
this.to_task.$bar.getX() < this.to_task.$bar.getX() <=
this.from_task.$bar.getX() + this.gantt.options.padding this.from_task.$bar.getX() + this.gantt.options.padding
) { ) {
const down_1 = this.gantt.options.padding / 2 - curve; let down_1 = this.gantt.options.padding / 2 - curve;
if (down_1 < 0) {
down_1 = 0;
curve = this.gantt.options.padding / 2;
curve_y = from_is_below_to ? -curve : curve;
}
const down_2 = const down_2 =
this.to_task.$bar.getY() + this.to_task.$bar.getY() +
this.to_task.$bar.getHeight() / 2 - this.to_task.$bar.getHeight() / 2 -
curve_y; curve_y;
const left = this.to_task.$bar.getX() - this.gantt.options.padding; const left = this.to_task.$bar.getX() - this.gantt.options.padding;
this.path = ` this.path = `
M ${start_x} ${start_y} M ${start_x} ${start_y}
v ${down_1} v ${down_1}
a ${curve} ${curve} 0 0 1 -${curve} ${curve} a ${curve} ${curve} 0 0 1 ${-curve} ${curve}
H ${left} H ${left}
a ${curve} ${curve} 0 0 ${clockwise} -${curve} ${curve_y} a ${curve} ${curve} 0 0 ${clockwise} ${-curve} ${curve_y}
V ${down_2} V ${down_2}
a ${curve} ${curve} 0 0 ${clockwise} ${curve} ${curve_y} a ${curve} ${curve} 0 0 ${clockwise} ${curve} ${curve_y}
L ${end_x} ${end_y} L ${end_x} ${end_y}
m -5 -5 m -5 -5
l 5 5 l 5 5
l -5 5`; l -5 5`;
} else {
if (end_x < start_x + curve) curve = end_x - start_x;
let offset = from_is_below_to ? end_y + curve : end_y - curve;
this.path = `
M ${start_x} ${start_y}
V ${offset}
a ${curve} ${curve} 0 0 ${clockwise} ${curve} ${curve}
L ${end_x} ${end_y}
m -5 -5
l 5 5
l -5 5`;
} }
} }

View File

@ -4,7 +4,21 @@ import { $, createSVG, animateSVG } from './svg_utils';
export default class Bar { export default class Bar {
constructor(gantt, task) { constructor(gantt, task) {
this.set_defaults(gantt, task); this.set_defaults(gantt, task);
this.prepare(); this.prepare_wrappers();
this.prepare_helpers();
this.refresh();
}
refresh() {
this.bar_group.innerHTML = '';
this.handle_group.innerHTML = '';
if (this.task.custom_class) {
this.group.classList.add(this.task.custom_class);
} else {
this.group.classList = ['bar-wrapper'];
}
this.prepare_values();
this.draw(); this.draw();
this.bind(); this.bind();
} }
@ -13,11 +27,24 @@ export default class Bar {
this.action_completed = false; this.action_completed = false;
this.gantt = gantt; this.gantt = gantt;
this.task = task; this.task = task;
this.name = this.name || '';
} }
prepare() { prepare_wrappers() {
this.prepare_values(); this.group = createSVG('g', {
this.prepare_helpers(); class:
'bar-wrapper' +
(this.task.custom_class ? ' ' + this.task.custom_class : ''),
'data-id': this.task.id,
});
this.bar_group = createSVG('g', {
class: 'bar-group',
append_to: this.group,
});
this.handle_group = createSVG('g', {
class: 'handle-group',
append_to: this.group,
});
} }
prepare_values() { prepare_values() {
@ -29,25 +56,8 @@ export default class Bar {
this.compute_duration(); this.compute_duration();
this.corner_radius = this.gantt.options.bar_corner_radius; this.corner_radius = this.gantt.options.bar_corner_radius;
this.width = this.gantt.config.column_width * this.duration; this.width = this.gantt.config.column_width * this.duration;
this.progress_width = if (this.task.progress < 0) this.task.progress = 0;
this.gantt.config.column_width * if (this.task.progress > 100) this.task.progress = 100;
this.duration *
(this.task.progress / 100) || 0;
this.group = createSVG('g', {
class:
'bar-wrapper' +
(this.task.custom_class ? ' ' + this.task.custom_class : '') +
(this.task.important ? ' important' : ''),
'data-id': this.task.id,
});
this.bar_group = createSVG('g', {
class: 'bar-group',
append_to: this.group,
});
this.handle_group = createSVG('g', {
class: 'handle-group',
append_to: this.group,
});
} }
prepare_helpers() { prepare_helpers() {
@ -101,12 +111,12 @@ export default class Bar {
ry: this.corner_radius, ry: this.corner_radius,
class: class:
'bar' + 'bar' +
(/^((?!chrome|android).)*safari/i.test(navigator.userAgent) && (/^((?!chrome|android).)*safari/i.test(navigator.userAgent)
!this.task.important
? ' safari' ? ' safari'
: ''), : ''),
append_to: this.bar_group, append_to: this.bar_group,
}); });
if (this.task.color) this.$bar.style.fill = this.task.color;
animateSVG(this.$bar, 'width', 0, this.width); animateSVG(this.$bar, 'width', 0, this.width);
if (this.invalid) { if (this.invalid) {
@ -137,7 +147,7 @@ export default class Bar {
draw_progress_bar() { draw_progress_bar() {
if (this.invalid) return; if (this.invalid) return;
this.progress_width = this.calculate_progress_width();
this.$bar_progress = createSVG('rect', { this.$bar_progress = createSVG('rect', {
x: this.x, x: this.x,
y: this.y, y: this.y,
@ -148,25 +158,59 @@ export default class Bar {
class: 'bar-progress', class: 'bar-progress',
append_to: this.bar_group, append_to: this.bar_group,
}); });
if (this.task.color_progress)
this.$bar_progress.style.fill = this.task.color;
const x = const x =
(date_utils.diff(this.task._start, this.gantt.gantt_start, 'hour') / (date_utils.diff(
this.task._start,
this.gantt.gantt_start,
this.gantt.config.unit,
) /
this.gantt.config.step) * this.gantt.config.step) *
this.gantt.config.column_width; this.gantt.config.column_width;
let $date_highlight = document.createElement('div'); let $date_highlight = this.gantt.create_el({
$date_highlight.id = `highlight-${this.task.id}`; classes: `date-range-highlight hide highlight-${this.task.id}`,
$date_highlight.classList.add('date-highlight'); width: this.width,
$date_highlight.style.height = this.height * 0.8 + 'px'; left: x,
$date_highlight.style.width = this.width + 'px'; });
$date_highlight.style.top =
this.gantt.options.header_height - 25 + 'px';
$date_highlight.style.left = x + 'px';
this.$date_highlight = $date_highlight; this.$date_highlight = $date_highlight;
this.gantt.$lower_header.prepend($date_highlight); this.gantt.$lower_header.prepend(this.$date_highlight);
animateSVG(this.$bar_progress, 'width', 0, this.progress_width); animateSVG(this.$bar_progress, 'width', 0, this.progress_width);
} }
calculate_progress_width() {
const width = this.$bar.getWidth();
const ignored_end = this.x + width;
const total_ignored_area =
this.gantt.config.ignored_positions.reduce((acc, val) => {
return acc + (val >= this.x && val < ignored_end);
}, 0) * this.gantt.config.column_width;
let progress_width =
((width - total_ignored_area) * this.task.progress) / 100;
const progress_end = this.x + progress_width;
const total_ignored_progress =
this.gantt.config.ignored_positions.reduce((acc, val) => {
return acc + (val >= this.x && val < progress_end);
}, 0) * this.gantt.config.column_width;
progress_width += total_ignored_progress;
let ignored_regions = this.gantt.get_ignored_region(
this.x + progress_width,
);
while (ignored_regions.length) {
progress_width += this.gantt.config.column_width;
ignored_regions = this.gantt.get_ignored_region(
this.x + progress_width,
);
}
this.progress_width = progress_width;
return progress_width;
}
draw_label() { draw_label() {
let x_coord = this.x + this.$bar.getWidth() / 2; let x_coord = this.x + this.$bar.getWidth() / 2;
@ -184,6 +228,7 @@ export default class Bar {
// labels get BBox in the next tick // labels get BBox in the next tick
requestAnimationFrame(() => this.update_label_position()); requestAnimationFrame(() => this.update_label_position());
} }
draw_thumbnail() { draw_thumbnail() {
let x_offset = 10, let x_offset = 10,
y_offset = 2; y_offset = 2;
@ -230,39 +275,50 @@ export default class Bar {
if (this.invalid || this.gantt.options.readonly) return; if (this.invalid || this.gantt.options.readonly) return;
const bar = this.$bar; const bar = this.$bar;
const handle_width = 8; const handle_width = 3;
if (!this.gantt.options.dates_readonly) { this.handles = [];
createSVG('rect', { if (!this.gantt.options.readonly_dates) {
x: bar.getX() + bar.getWidth() + handle_width - 4, this.handles.push(
y: bar.getY() + 1, createSVG('rect', {
width: handle_width, x: bar.getEndX() - handle_width / 2,
height: this.height - 2, y: bar.getY() + this.height / 4,
rx: this.corner_radius, width: handle_width,
ry: this.corner_radius, height: this.height / 2,
class: 'handle right', rx: 2,
append_to: this.handle_group, ry: 2,
}); class: 'handle right',
append_to: this.handle_group,
}),
);
createSVG('rect', { this.handles.push(
x: bar.getX() - handle_width - 4, createSVG('rect', {
y: bar.getY() + 1, x: bar.getX() - handle_width / 2,
width: handle_width, y: bar.getY() + this.height / 4,
height: this.height - 2, width: handle_width,
rx: this.corner_radius, height: this.height / 2,
ry: this.corner_radius, rx: 2,
class: 'handle left', ry: 2,
append_to: this.handle_group, class: 'handle left',
}); append_to: this.handle_group,
}),
);
} }
if (!this.gantt.options.progress_readonly) { if (!this.gantt.options.readonly_progress) {
const bar_progress = this.$bar_progress; const bar_progress = this.$bar_progress;
this.$handle_progress = createSVG('circle', { this.$handle_progress = createSVG('circle', {
cx: bar_progress.getEndX(), cx: bar_progress.getEndX(),
cy: bar_progress.getY() + bar_progress.getHeight() / 2, cy: bar_progress.getY() + bar_progress.getHeight() / 2,
r: 5, r: 4.5,
class: 'handle progress', class: 'handle progress',
append_to: this.handle_group, append_to: this.handle_group,
}); });
this.handles.push(this.$handle_progress);
}
for (let handle of this.handles) {
$.on(handle, 'mouseenter', () => handle.classList.add('active'));
$.on(handle, 'mouseleave', () => handle.classList.remove('active'));
} }
} }
@ -283,40 +339,44 @@ export default class Bar {
}); });
if (this.gantt.options.popup_on === 'click') { if (this.gantt.options.popup_on === 'click') {
let opened = false; $.on(this.group, 'mouseup', (e) => {
$.on(this.group, 'click', (e) => { const posX = e.offsetX || e.layerX;
if (!opened) { if (this.$handle_progress) {
this.show_popup(e.offsetX || e.layerX); const cx = +this.$handle_progress.getAttribute('cx');
document.getElementById( if (cx > posX - 1 && cx < posX + 1) return;
`highlight-${task_id}`, if (this.gantt.bar_being_dragged) return;
).style.display = 'block';
} else {
this.gantt.hide_popup();
} }
opened = !opened; this.gantt.show_popup({
}); x: e.offsetX || e.layerX,
} else { y: e.offsetY || e.layerY,
let timeout; task: this.task,
$.on( target: this.$bar,
this.group, });
'mouseenter',
(e) =>
(timeout = setTimeout(() => {
this.show_popup(e.offsetX || e.layerX);
document.getElementById(
`highlight-${task_id}`,
).style.display = 'block';
}, 200)),
);
$.on(this.group, 'mouseleave', () => {
clearTimeout(timeout);
this.gantt.popup?.hide?.();
document.getElementById(`highlight-${task_id}`).style.display =
'none';
}); });
} }
let timeout;
$.on(this.group, 'mouseenter', (e) => {
timeout = setTimeout(() => {
if (this.gantt.options.popup_on === 'hover')
this.gantt.show_popup({
x: e.offsetX || e.layerX,
y: e.offsetY || e.layerY,
task: this.task,
target: this.$bar,
});
this.gantt.$container
.querySelector(`.highlight-${task_id}`)
.classList.remove('hide');
}, 200);
});
$.on(this.group, 'mouseleave', () => {
clearTimeout(timeout);
if (this.gantt.options.popup_on === 'hover')
this.gantt.popup?.hide?.();
this.gantt.$container
.querySelector(`.highlight-${task_id}`)
.classList.add('hide');
});
$.on(this.group, 'click', () => { $.on(this.group, 'click', () => {
this.gantt.trigger_event('click', [this.task]); this.gantt.trigger_event('click', [this.task]);
@ -329,70 +389,48 @@ export default class Bar {
} }
this.group.classList.remove('active'); this.group.classList.remove('active');
if (this.gantt.popup) if (this.gantt.popup)
this.gantt.popup.parent.classList.remove('hidden'); this.gantt.popup.parent.classList.remove('hide');
this.gantt.trigger_event('double_click', [this.task]); this.gantt.trigger_event('double_click', [this.task]);
}); });
} }
show_popup(x) {
if (this.gantt.bar_being_dragged) return;
const start_date = date_utils.format(
this.task._start,
'MMM D',
this.gantt.options.language,
);
const end_date = date_utils.format(
date_utils.add(this.task._end, -1, 'second'),
'MMM D',
this.gantt.options.language,
);
const subtitle = `${start_date} - ${end_date}<br/>Progress: ${this.task.progress}`;
this.gantt.show_popup({
x,
target_element: this.$bar,
title: this.task.name,
subtitle: subtitle,
task: this.task,
});
}
update_bar_position({ x = null, width = null }) { update_bar_position({ x = null, width = null }) {
const bar = this.$bar; const bar = this.$bar;
if (x) { if (x) {
// get all x values of parent task
const xs = this.task.dependencies.map((dep) => { const xs = this.task.dependencies.map((dep) => {
return this.gantt.get_bar(dep).$bar.getX(); return this.gantt.get_bar(dep).$bar.getX();
}); });
// child task must not go before parent
const valid_x = xs.reduce((_, curr) => { const valid_x = xs.reduce((_, curr) => {
return x >= curr; return x >= curr;
}, x); }, x);
if (!valid_x) { if (!valid_x) return;
width = null;
return;
}
this.update_attr(bar, 'x', x); this.update_attr(bar, 'x', x);
this.x = x;
this.$date_highlight.style.left = x + 'px'; this.$date_highlight.style.left = x + 'px';
} }
if (width) { if (width > 0) {
this.update_attr(bar, 'width', width); this.update_attr(bar, 'width', width);
this.$date_highlight.style.width = width + 'px'; this.$date_highlight.style.width = width + 'px';
} }
this.update_label_position(); this.update_label_position();
this.update_handle_position(); this.update_handle_position();
this.date_changed();
this.compute_duration();
if (this.gantt.options.show_expected_progress) { if (this.gantt.options.show_expected_progress) {
this.date_changed();
this.compute_duration();
this.update_expected_progressbar_position(); this.update_expected_progressbar_position();
} }
this.update_progressbar_position(); this.update_progressbar_position();
this.update_arrow_position(); this.update_arrow_position();
} }
update_label_position_on_horizontal_scroll({ x, sx }) { update_label_position_on_horizontal_scroll({ x, sx }) {
const container = document.querySelector('.gantt-container'); const container =
this.gantt.$container.querySelector('.gantt-container');
const label = this.group.querySelector('.bar-label'); const label = this.group.querySelector('.bar-label');
const img = this.group.querySelector('.bar-img') || ''; const img = this.group.querySelector('.bar-img') || '';
const img_mask = this.bar_group.querySelector('.img_mask') || ''; const img_mask = this.bar_group.querySelector('.img_mask') || '';
@ -448,9 +486,11 @@ export default class Bar {
} }
progress_changed() { progress_changed() {
const new_progress = this.compute_progress(); this.task.progress = this.compute_progress();
this.task.progress = new_progress; this.gantt.trigger_event('progress_change', [
this.gantt.trigger_event('progress_change', [this.task, new_progress]); this.task,
this.task.progress,
]);
} }
set_action_completed() { set_action_completed() {
@ -466,17 +506,6 @@ export default class Bar {
x_in_units * this.gantt.config.step, x_in_units * this.gantt.config.step,
this.gantt.config.unit, this.gantt.config.unit,
); );
const start_offset =
this.gantt.gantt_start.getTimezoneOffset() -
new_start_date.getTimezoneOffset();
if (start_offset) {
new_start_date = date_utils.add(
new_start_date,
start_offset,
'minute',
);
}
const width_in_units = bar.getWidth() / this.gantt.config.column_width; const width_in_units = bar.getWidth() / this.gantt.config.column_width;
const new_end_date = date_utils.add( const new_end_date = date_utils.add(
@ -489,9 +518,20 @@ export default class Bar {
} }
compute_progress() { compute_progress() {
this.progress_width = this.$bar_progress.getWidth();
this.x = this.$bar_progress.getBBox().x;
const progress_area = this.x + this.progress_width;
const progress = const progress =
(this.$bar_progress.getWidth() / this.$bar.getWidth()) * 100; this.progress_width -
return parseInt(progress, 10); this.gantt.config.ignored_positions.reduce((acc, val) => {
return acc + (val >= this.x && val <= progress_area);
}, 0) *
this.gantt.config.column_width;
if (progress < 0) return 0;
const total =
this.$bar.getWidth() -
this.ignored_duration_raw * this.gantt.config.column_width;
return parseInt((progress / total) * 100, 10);
} }
compute_expected_progress() { compute_expected_progress() {
@ -507,13 +547,14 @@ export default class Bar {
} }
compute_x() { compute_x() {
const { step, column_width } = this.gantt.config; const { column_width } = this.gantt.config;
const task_start = this.task._start; const task_start = this.task._start;
const gantt_start = this.gantt.gantt_start; const gantt_start = this.gantt.gantt_start;
const diff = const diff =
date_utils.diff(task_start, gantt_start, this.gantt.config.unit) / date_utils.diff(task_start, gantt_start, this.gantt.config.unit) /
this.gantt.config.step; this.gantt.config.step;
let x = diff * column_width; let x = diff * column_width;
/* Since the column width is based on 30, /* Since the column width is based on 30,
@ -521,72 +562,67 @@ export default class Bar {
and then add the days in the month, making sure the number does not exceed 29 and then add the days in the month, making sure the number does not exceed 29
so it is within the column */ so it is within the column */
if (this.gantt.view_is('Month')) { // if (this.gantt.view_is('Month')) {
const diffDaysBasedOn30DayMonths = // const diffDaysBasedOn30DayMonths =
date_utils.diff(task_start, gantt_start, 'month') * 30; // date_utils.diff(task_start, gantt_start, 'month') * 30;
const dayInMonth = Math.min( // const dayInMonth = Math.min(
29, // 29,
date_utils.format( // date_utils.format(
task_start, // task_start,
'DD', // 'DD',
this.gantt.options.language, // this.gantt.options.language,
), // ),
); // );
const diff = diffDaysBasedOn30DayMonths + dayInMonth; // const diff = diffDaysBasedOn30DayMonths + dayInMonth;
x = (diff * column_width) / 30; // x = (diff * column_width) / 30;
} // }
this.x = x; this.x = x;
} }
compute_y() { compute_y() {
this.y = this.y =
this.gantt.options.header_height + this.gantt.config.header_height +
this.gantt.options.padding + this.gantt.options.padding / 2 +
this.task._index * (this.height + this.gantt.options.padding); this.task._index * (this.height + this.gantt.options.padding);
} }
compute_duration() { compute_duration() {
let actual_duration_in_days = 0,
duration_in_days = 0;
for (
let d = new Date(this.task._start);
d < this.task._end;
d.setDate(d.getDate() + 1)
) {
duration_in_days++;
if (
!this.gantt.config.ignored_dates.find(
(k) => k.getTime() === d.getTime(),
) &&
(!this.gantt.config.ignored_function ||
!this.gantt.config.ignored_function(d))
) {
actual_duration_in_days++;
}
}
this.task.actual_duration = actual_duration_in_days;
this.task.ignored_duration = duration_in_days - actual_duration_in_days;
this.duration = this.duration =
date_utils.diff( date_utils.convert_scales(
this.task._end, duration_in_days + 'd',
this.task._start,
this.gantt.config.unit, this.gantt.config.unit,
) / this.gantt.config.step; ) / this.gantt.config.step;
}
get_snap_position(dx) { this.actual_duration_raw =
let odx = dx, date_utils.convert_scales(
rem, actual_duration_in_days + 'd',
position; this.gantt.config.unit,
) / this.gantt.config.step;
// if (this.gantt.view_is('Week')) { this.ignored_duration_raw = this.duration - this.actual_duration_raw;
// rem = dx % (this.gantt.config.column_width / 7);
// position =
// odx -
// rem +
// (rem < this.gantt.config.column_width / 14
// ? 0
// : this.gantt.config.column_width / 7);
// } else if (this.gantt.view_is('Month')) {
// rem = dx % (this.gantt.config.column_width / 30);
// position =
// odx -
// rem +
// (rem < this.gantt.config.column_width / 60
// ? 0
// : this.gantt.config.column_width / 30);
// } else {
rem = dx % this.gantt.config.column_width;
position =
odx -
rem +
(rem < this.gantt.config.column_width / 2
? 0
: this.gantt.config.column_width);
// }
return position;
} }
update_attr(element, attr, value) { update_attr(element, attr, value) {
@ -604,7 +640,7 @@ export default class Bar {
this.$expected_bar_progress.setAttribute( this.$expected_bar_progress.setAttribute(
'width', 'width',
this.gantt.config.column_width * this.gantt.config.column_width *
this.duration * this.actual_duration_raw *
(this.expected_progress / 100) || 0, (this.expected_progress / 100) || 0,
); );
} }
@ -612,9 +648,10 @@ export default class Bar {
update_progressbar_position() { update_progressbar_position() {
if (this.invalid || this.gantt.options.readonly) return; if (this.invalid || this.gantt.options.readonly) return;
this.$bar_progress.setAttribute('x', this.$bar.getX()); this.$bar_progress.setAttribute('x', this.$bar.getX());
this.$bar_progress.setAttribute( this.$bar_progress.setAttribute(
'width', 'width',
this.$bar.getWidth() * (this.task.progress / 100), this.calculate_progress_width(),
); );
} }
@ -631,17 +668,11 @@ export default class Bar {
if (labelWidth > barWidth) { if (labelWidth > barWidth) {
label.classList.add('big'); label.classList.add('big');
if (img) { if (img) {
img.setAttribute('x', bar.getX() + bar.getWidth() + padding); img.setAttribute('x', bar.getEndX() + padding);
img_mask.setAttribute( img_mask.setAttribute('x', bar.getEndX() + padding);
'x', label.setAttribute('x', bar.getEndX() + x_offset_label_img);
bar.getX() + bar.getWidth() + padding,
);
label.setAttribute(
'x',
bar.getX() + bar.getWidth() + x_offset_label_img,
);
} else { } else {
label.setAttribute('x', bar.getX() + bar.getWidth() + padding); label.setAttribute('x', bar.getEndX() + padding);
} }
} else { } else {
label.classList.remove('big'); label.classList.remove('big');
@ -666,10 +697,10 @@ export default class Bar {
const bar = this.$bar; const bar = this.$bar;
this.handle_group this.handle_group
.querySelector('.handle.left') .querySelector('.handle.left')
.setAttribute('x', bar.getX() - 12); .setAttribute('x', bar.getX());
this.handle_group this.handle_group
.querySelector('.handle.right') .querySelector('.handle.right')
.setAttribute('x', bar.getEndX() + 4); .setAttribute('x', bar.getEndX());
const handle = this.group.querySelector('.handle.progress'); const handle = this.group.querySelector('.handle.progress');
handle && handle.setAttribute('cx', this.$bar_progress.getEndX()); handle && handle.setAttribute('cx', this.$bar_progress.getEndX());
} }
@ -681,11 +712,3 @@ export default class Bar {
} }
} }
} }
function isFunction(functionToCheck) {
let getType = {};
return (
functionToCheck &&
getType.toString.call(functionToCheck) === '[object Function]'
);
}

View File

@ -1,99 +0,0 @@
:root {
--bar-color-dark: #616161;
--bar-stroke-dark: #c6ccd2;
--border-color-dark: #616161;
--light-bg-dark: #3e3e3e;
--light-border-color-dark: #3e3e3e;
--text-muted-dark: #eee;
--text-light-dark: #ececec;
--text-color-dark: #f7f7f7;
--blue-dark: #8a8aff;
}
.dark>.gantt-container .gantt {
& .grid-row {
fill: #252525;
}
/* & .grid-row:nth-child(even) {
fill: var(--light-bg-dark);
} */
& .row-line {
stroke: var(--light-border-color-dark);
}
& .tick {
stroke: var(--border-color-dark);
}
& .holiday-highlight {
fill: var(--light-bg-dark);
}
& .arrow {
stroke: var(--text-muted-dark);
}
& .bar {
fill: var(--bar-color-dark);
stroke: none;
}
& .bar-progress {
fill: var(--blue-dark);
}
& .bar-invalid {
fill: transparent;
stroke: var(--bar-stroke-dark);
&~.bar-label {
fill: var(--text-light-dark);
}
}
& .bar-label.big {
fill: var(--text-light-dark);
}
& .bar-wrapper {
&:hover {
.bar {
fill: lighten(var(--bar-color-dark, 5));
}
& .bar-progress {
fill: lighten(var(--blue-dark, 5));
}
}
&.active {
.bar {
fill: lighten(var(--bar-color-dark, 5));
}
& .bar-progress {
fill: lighten(var(--blue-dark, 5));
}
}
}
}
.dark>.gantt-container {
& .grid-header {
background-color: #252525;
}
& .popup-wrapper {
background-color: #333;
& .title {
border-color: lighten(var(--blue-dark, 5));
}
& .pointer {
border-top-color: #333;
}
}
}

View File

@ -78,7 +78,7 @@ export default {
return date_string + (with_time ? ' ' + time_string : ''); return date_string + (with_time ? ' ' + time_string : '');
}, },
format(date, format_string = 'YYYY-MM-DD HH:mm:ss.SSS', lang = 'en') { format(date, date_format = 'YYYY-MM-DD HH:mm:ss.SSS', lang = 'en') {
const dateTimeFormat = new Intl.DateTimeFormat(lang, { const dateTimeFormat = new Intl.DateTimeFormat(lang, {
month: 'long', month: 'long',
}); });
@ -103,7 +103,7 @@ export default {
MMM: dateTimeFormatShort.format(date), MMM: dateTimeFormatShort.format(date),
}; };
let str = format_string; let str = date_format;
const formatted_values = []; const formatted_values = [];
Object.keys(format_map) Object.keys(format_map)
@ -125,7 +125,10 @@ export default {
diff(date_a, date_b, scale = 'day') { diff(date_a, date_b, scale = 'day') {
let milliseconds, seconds, hours, minutes, days, months, years; let milliseconds, seconds, hours, minutes, days, months, years;
milliseconds = date_a - date_b; milliseconds =
date_a -
date_b +
(date_b.getTimezoneOffset() - date_a.getTimezoneOffset()) * 60000;
seconds = milliseconds / 1000; seconds = milliseconds / 1000;
minutes = seconds / 60; minutes = seconds / 60;
hours = minutes / 60; hours = minutes / 60;

View File

@ -1,13 +1,26 @@
import date_utils from './date_utils'; import date_utils from './date_utils';
function getDecade(d) {
const year = d.getFullYear();
return year - (year % 10) + '';
}
function formatWeek(d, ld, lang) {
let endOfWeek = date_utils.add(d, 6, 'day');
let endFormat = endOfWeek.getMonth() !== d.getMonth() ? 'D MMM' : 'D';
let beginFormat = !ld || d.getMonth() !== ld.getMonth() ? 'D MMM' : 'D';
return `${date_utils.format(d, beginFormat, lang)} - ${date_utils.format(endOfWeek, endFormat, lang)}`;
}
const DEFAULT_VIEW_MODES = [ const DEFAULT_VIEW_MODES = [
{ {
name: 'Hour', name: 'Hour',
padding: '7d', padding: '7d',
step: '1h', step: '1h',
date_format: 'YYYY-MM-DD HH:',
lower_text: 'HH', lower_text: 'HH',
upper_text: (d, ld, lang) => upper_text: (d, ld, lang) =>
d.getDate() !== ld.getDate() !ld || d.getDate() !== ld.getDate()
? date_utils.format(d, 'D MMMM', lang) ? date_utils.format(d, 'D MMMM', lang)
: '', : '',
upper_text_frequency: 24, upper_text_frequency: 24,
@ -16,22 +29,22 @@ const DEFAULT_VIEW_MODES = [
name: 'Quarter Day', name: 'Quarter Day',
padding: '7d', padding: '7d',
step: '6h', step: '6h',
format_string: 'YYYY-MM-DD HH', date_format: 'YYYY-MM-DD HH:',
lower_text: 'HH', lower_text: 'HH',
upper_text: (d, ld, lang) => upper_text: (d, ld, lang) =>
d.getDate() !== ld.getDate() !ld || d.getDate() !== ld.getDate()
? date_utils.format(d, 'D MMM', lang) ? date_utils.format(d, 'D MMM', lang)
: '', : '',
upper_text_frequency: 4, upper_text_frequency: 4,
}, },
{ {
name: 'Half Day', name: 'Half Day',
padding: '7d', padding: '14d',
step: '12h', step: '12h',
format_string: 'YYYY-MM-DD HH', date_format: 'YYYY-MM-DD HH:',
lower_text: 'HH', lower_text: 'HH',
upper_text: (d, ld, lang) => upper_text: (d, ld, lang) =>
d.getDate() !== ld.getDate() !ld || d.getDate() !== ld.getDate()
? d.getMonth() !== d.getMonth() ? d.getMonth() !== d.getMonth()
? date_utils.format(d, 'D MMM', lang) ? date_utils.format(d, 'D MMM', lang)
: date_utils.format(d, 'D', lang) : date_utils.format(d, 'D', lang)
@ -40,13 +53,15 @@ const DEFAULT_VIEW_MODES = [
}, },
{ {
name: 'Day', name: 'Day',
padding: '14d', padding: '7d',
format_string: 'YYYY-MM-DD', date_format: 'YYYY-MM-DD',
step: '1d', step: '1d',
lower_text: (d, ld, lang) => lower_text: (d, ld, lang) =>
d.getDate() !== ld.getDate() ? date_utils.format(d, 'D', lang) : '', !ld || d.getDate() !== ld.getDate()
? date_utils.format(d, 'D', lang)
: '',
upper_text: (d, ld, lang) => upper_text: (d, ld, lang) =>
d.getMonth() !== ld.getMonth() !ld || d.getMonth() !== ld.getMonth()
? date_utils.format(d, 'MMMM', lang) ? date_utils.format(d, 'MMMM', lang)
: '', : '',
thick_line: (d) => d.getDay() === 1, thick_line: (d) => d.getDay() === 1,
@ -55,13 +70,11 @@ const DEFAULT_VIEW_MODES = [
name: 'Week', name: 'Week',
padding: '1m', padding: '1m',
step: '7d', step: '7d',
date_format: 'YYYY-MM-DD',
column_width: 140, column_width: 140,
lower_text: (d, ld, lang) => lower_text: formatWeek,
d.getMonth() !== ld.getMonth()
? date_utils.format(d, 'D MMM', lang)
: date_utils.format(d, 'D', lang),
upper_text: (d, ld, lang) => upper_text: (d, ld, lang) =>
d.getMonth() !== ld.getMonth() !ld || d.getMonth() !== ld.getMonth()
? date_utils.format(d, 'MMMM', lang) ? date_utils.format(d, 'MMMM', lang)
: '', : '',
thick_line: (d) => d.getDate() >= 1 && d.getDate() <= 7, thick_line: (d) => d.getDate() >= 1 && d.getDate() <= 7,
@ -72,51 +85,76 @@ const DEFAULT_VIEW_MODES = [
padding: '2m', padding: '2m',
step: '1m', step: '1m',
column_width: 120, column_width: 120,
format_string: 'YYYY-MM', date_format: 'YYYY-MM',
lower_text: 'MMMM', lower_text: 'MMMM',
upper_text: (d, ld, lang) => upper_text: (d, ld, lang) =>
!ld || d.getFullYear() !== ld.getFullYear() !ld || d.getFullYear() !== ld.getFullYear()
? date_utils.format(d, 'YYYY', lang) ? date_utils.format(d, 'YYYY', lang)
: '', : '',
thick_line: (d) => d.getMonth() % 3 === 0, thick_line: (d) => d.getMonth() % 3 === 0,
default_snap: '7d', snap_at: '7d',
}, },
{ {
name: 'Year', name: 'Year',
padding: '2y', padding: '2y',
step: '1y', step: '1y',
column_width: 120, column_width: 120,
format_string: 'YYYY', date_format: 'YYYY',
upper_text: 'YYYY', upper_text: (d, ld, lang) =>
default_snap: '30d', !ld || getDecade(d) !== getDecade(ld) ? getDecade(d) : '',
lower_text: 'YYYY',
snap_at: '30d',
}, },
]; ];
const DEFAULT_OPTIONS = { const DEFAULT_OPTIONS = {
header_height: 65,
column_width: 30,
view_modes: DEFAULT_VIEW_MODES,
bar_height: 30,
bar_corner_radius: 3,
arrow_curve: 5, arrow_curve: 5,
padding: 18, auto_move_label: false,
view_mode: 'Day', bar_corner_radius: 3,
date_format: 'YYYY-MM-DD', bar_height: 30,
move_dependencies: true, container_height: 'auto',
show_expected_progress: false, column_width: null,
popup: null, date_format: 'YYYY-MM-DD HH:mm',
popup_on: 'hover', upper_header_height: 45,
lower_header_height: 30,
snap_at: null,
infinite_padding: true,
holidays: { 'var(--g-weekend-highlight-color)': 'weekend' },
ignore: [],
language: 'en', language: 'en',
readonly: false,
progress_readonly: false,
dates_readonly: false,
highlight_weekend: true,
scroll_to: 'start',
lines: 'both', lines: 'both',
auto_move_label: true, move_dependencies: true,
padding: 18,
popup: (ctx) => {
ctx.set_title(ctx.task.name);
if (ctx.task.description) ctx.set_subtitle(ctx.task.description);
else ctx.set_subtitle('');
const start_date = date_utils.format(
ctx.task._start,
'MMM D',
ctx.chart.options.language,
);
const end_date = date_utils.format(
date_utils.add(ctx.task._end, -1, 'second'),
'MMM D',
ctx.chart.options.language,
);
ctx.set_details(
`${start_date} - ${end_date} (${ctx.task.actual_duration} days${ctx.task.ignored_duration ? ' + ' + ctx.task.ignored_duration + ' excluded' : ''})<br/>Progress: ${Math.floor(ctx.task.progress * 100) / 100}%`,
);
},
popup_on: 'click',
readonly_progress: false,
readonly_dates: false,
readonly: false,
scroll_to: 'today',
show_expected_progress: false,
today_button: true, today_button: true,
view_mode: 'Day',
view_mode_select: false, view_mode_select: false,
default_snap: '1d', view_modes: DEFAULT_VIEW_MODES,
}; };
export { DEFAULT_OPTIONS, DEFAULT_VIEW_MODES }; export { DEFAULT_OPTIONS, DEFAULT_VIEW_MODES };

View File

@ -1,310 +0,0 @@
@import './dark.css';
:root {
--bar-color: #fff;
--bar-color-important: #94c4f4;
--bar-stroke: #fff;
--dark-stroke-color: #e0e0e0;
--stroke-color: #ebeef0;
--light-bg: #f5f5f5;
--light-border-color: #ebeff2;
--light-yellow: #f6e796;
--holiday-color: #f9fafa;
--text-muted: #7c7c7c;
--text-grey: #98a1a9;
--text-light: #fff;
--text-dark: #171717;
--progress: #ebeef0;
--handle-color: #dcdce4;
--handle-color-important: #94c4f4;
--light-blue: #c4c4e9;
--middle-blue: #62b2f9;
--dark-blue: #2c94ec;
}
.gantt-container {
line-height: 14.5px;
position: relative;
overflow: auto;
font-size: 12px;
height: 500px;
width: fit-content;
& .popup-wrapper {
position: absolute;
top: 0;
left: 0;
background: #171b1f;
padding: 10px;
border-radius: 5px;
width: max-content;
&.hidden {
opacity: 0 !important;
}
& .title {
margin-bottom: 5px;
text-align: -webkit-center;
text-align: center;
color: var(--text-light);
}
& .subtitle {
color: var(--text-grey);
}
& .pointer {
position: absolute;
height: 5px;
margin: 0 0 0 -5px;
border: 5px solid transparent;
border-bottom-color: rgba(0, 0, 0, 0.8);
}
}
& .grid-header {
background-color: #ffffff;
position: sticky;
top: 0;
left: 0;
z-index: 10;
}
& .lower-text,
& .upper-text {
text-anchor: middle;
}
& .upper-header {
height: 40px;
}
& .lower-header {
height: 30px;
}
& .lower-text {
font-size: 14px;
position: absolute;
width: fit-content;
transform: translateX(-50%);
color: var(--text-muted);
}
& .upper-text {
position: absolute;
width: fit-content;
font-weight: 500;
font-size: 16px;
color: var(--text-dark);
}
& .current-upper {
position: fixed;
}
& .side-header {
position: fixed;
padding: 0 10px;
margin-right: 10px;
background: white;
line-height: 20px;
font-weight: 400;
}
& .today-button,
& .viewmode-select {
background: #f4f5f6;
text-align: -webkit-center;
text-align: center;
height: 25px;
border-radius: 8px;
border: none;
color: var(--text-dark);
padding: 4px 10px;
border-radius: 8px;
height: 25px;
}
& .viewmode-select {
outline: none !important;
padding: 4px 8px;
margin-right: 4px;
/* -webkit-appearance: none; */
/* -moz-appearance: none; */
text-indent: 1px;
text-overflow: '';
}
& .date-highlight {
background-color: var(--progress);
border-radius: 12px;
position: absolute;
display: none;
}
& .current-highlight {
position: absolute;
background: var(--dark-blue);
width: 1px;
}
& .current-date-highlight {
background: var(--dark-blue);
color: var(--text-light);
padding: 4px 8px;
border-radius: 200px;
}
}
.gantt {
user-select: none;
-webkit-user-select: none;
position: absolute;
& .grid-background {
fill: none;
}
& .grid-row {
fill: #ffffff;
}
& .row-line {
stroke: var(--light-border-color);
}
& .tick {
stroke: var(--stroke-color);
stroke-width: 0.4;
&.thick {
stroke: var(--dark-stroke-color);
stroke-width: 0.7;
}
}
& .holiday-highlight {
fill: var(--holiday-color);
}
& .arrow {
fill: none;
stroke: #9fa9b1;
stroke-width: 1;
}
& .bar-wrapper .bar {
fill: var(--bar-color);
stroke: var(--bar-stroke);
stroke-width: 0;
transition: stroke-width 0.3s ease;
}
& .bar-progress {
fill: var(--progress);
}
& .bar-expected-progress {
fill: var(--light-blue);
}
& .bar-invalid {
fill: transparent;
stroke: var(--bar-stroke);
stroke-width: 1;
stroke-dasharray: 5;
& ~ .bar-label {
fill: var(--text-light);
}
}
& .bar-label {
fill: var(--text-dark);
dominant-baseline: central;
font-family: Helvetica;
font-size: 13px;
font-weight: 400;
&.big {
fill: var(--text-dark);
text-anchor: start;
}
}
& .bar-wrapper.important {
& .bar {
fill: var(--bar-color-important);
}
& .bar-progress {
fill: var(--dark-blue);
}
& .bar-label {
fill: var(--text-light);
&.big {
fill: var(--text-dark);
}
}
& .handle {
fill: var(--handle-color-important);
}
& .handle.progress {
fill: var(--text-light);
}
}
& .handle {
fill: var(--handle-color);
cursor: ew-resize;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s ease;
}
& .handle.progress {
fill: var(--text-muted);
}
& .bar-wrapper {
cursor: pointer;
&.active {
& .handle {
visibility: visible;
opacity: 1;
}
}
& .bar {
-webkit-filter: drop-shadow(3px 3px 2px rgba(0, 0, 0, 0.7));
filter: drop-shadow(0 0 2px rgba(17, 43, 66, 0.16));
border-radius: 3px;
}
& .bar.safari {
outline: 1px solid black;
}
&:hover {
.bar {
transition: transform 0.3s ease;
}
.date-highlight {
display: block;
}
}
}
& .hide {
display: none;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,9 @@
export default class Popup { export default class Popup {
constructor(parent, custom_html) { constructor(parent, popup_func, gantt) {
this.parent = parent; this.parent = parent;
this.custom_html = custom_html; this.popup_func = popup_func;
this.gantt = gantt;
this.make(); this.make();
} }
@ -9,55 +11,51 @@ export default class Popup {
this.parent.innerHTML = ` this.parent.innerHTML = `
<div class="title"></div> <div class="title"></div>
<div class="subtitle"></div> <div class="subtitle"></div>
<div class="pointer"></div> <div class="details"></div>
<div class="actions"></div>
`; `;
this.hide(); this.hide();
this.title = this.parent.querySelector('.title'); this.title = this.parent.querySelector('.title');
this.subtitle = this.parent.querySelector('.subtitle'); this.subtitle = this.parent.querySelector('.subtitle');
this.pointer = this.parent.querySelector('.pointer'); this.details = this.parent.querySelector('.details');
this.actions = this.parent.querySelector('.actions');
} }
show(options) { show({ x, y, task, target }) {
if (!options.target_element) { this.actions.innerHTML = '';
throw new Error('target_element is required to show popup'); let html = this.popup_func({
} task,
const target_element = options.target_element; chart: this.gantt,
get_title: () => this.title,
set_title: (title) => (this.title.innerHTML = title),
get_subtitle: () => this.subtitle,
set_subtitle: (subtitle) => (this.subtitle.innerHTML = subtitle),
get_details: () => this.details,
set_details: (details) => (this.details.innerHTML = details),
add_action: (html, func) => {
let action = this.gantt.create_el({
classes: 'action-btn',
type: 'button',
append_to: this.actions,
});
if (typeof html === 'function') html = html(task);
action.innerHTML = html;
action.onclick = (e) => func(task, this.gantt, e);
},
});
if (html === false) return;
if (html) this.parent.innerHTML = html;
if (this.custom_html) { if (this.actions.innerHTML === '') this.actions.remove();
let html = this.custom_html(options.task); else this.parent.appendChild(this.actions);
html += '<div class="pointer"></div>';
this.parent.innerHTML = html;
this.pointer = this.parent.querySelector('.pointer');
} else {
// set data
this.title.innerHTML = options.title;
this.subtitle.innerHTML = options.subtitle;
}
// set position this.parent.style.left = x + 10 + 'px';
let position_meta; this.parent.style.top = y - 10 + 'px';
if (target_element instanceof HTMLElement) { this.parent.classList.remove('hide');
position_meta = target_element.getBoundingClientRect();
} else if (target_element instanceof SVGElement) {
position_meta = options.target_element.getBBox();
}
this.parent.style.left = options.x - this.parent.clientWidth / 2 + 'px';
this.parent.style.top =
position_meta.y + position_meta.height + 10 + 'px';
this.parent.classList.remove('hidden');
this.pointer.style.left = this.parent.clientWidth / 2 + 'px';
this.pointer.style.top = '-15px';
// show
this.parent.style.opacity = 1;
} }
hide() { hide() {
this.parent.style.opacity = 0; this.parent.classList.add('hide');
this.parent.style.left = 0;
} }
} }

87
src/styles/dark.css Normal file
View File

@ -0,0 +1,87 @@
:root {
--g-bar-stroke-dark: #c6ccd2;
--g-border-color-dark: #616161;
--g-bar-color-dark: #616161;
--g-bg-dark: #3e3e3e;
--g-light-border-color-dark: #3e3e3e;
--g-text-muted-dark: #eee;
--g-text-light-dark: #ececec;
--g-text-color-dark: #f7f7f7;
--g-progress-color: #8a8aff;
}
.dark > .gantt-container .gantt {
& .grid-row {
fill: #252525;
}
& .row-line {
stroke: var(--g-light-border-color-dark);
}
& .tick {
stroke: var(--g-border-color-dark);
}
& .arrow {
stroke: var(--g-text-muted-dark);
}
& .bar {
fill: var(--g-bar-color-dark);
stroke: none;
}
& .bar-progress {
fill: var(--g-progress-color);
}
& .bar-invalid {
fill: transparent;
stroke: var(--g-bar-stroke-dark);
& ~ .bar-label {
fill: var(--g-text-light-dark);
}
}
& .bar-label.big {
fill: var(--g-text-light-dark);
}
& .bar-wrapper {
&:hover {
.bar {
fill: lighten(var(--g-bar-color-dark, 5));
}
& .bar-progress {
fill: lighten(var(--g-progress-color, 5));
}
}
&.active {
.bar {
fill: lighten(var(--g-bar-color-dark, 5));
}
& .bar-progress {
fill: lighten(var(--g-progress-color, 5));
}
}
}
}
.dark > .gantt-container {
& .grid-header {
background-color: #252525;
}
& .popup-wrapper {
background-color: #333;
& .title {
border-color: lighten(var(--g-progress-color, 5));
}
}
}

345
src/styles/gantt.css Normal file
View File

@ -0,0 +1,345 @@
@import './light.css';
.gantt-container {
line-height: 14.5px;
position: relative;
overflow: auto;
font-size: 12px;
height: var(--gv-grid-height);
width: 100%;
border-radius: 8px;
& .popup-wrapper {
position: absolute;
top: 0;
left: 0;
background: #fff;
box-shadow: 0px 10px 24px -3px rgba(0, 0, 0, 0.2);
padding: 10px;
border-radius: 5px;
width: max-content;
z-index: 1000;
& .title {
margin-bottom: 2px;
color: var(--g-text-dark);
font-size: 0.85rem;
font-weight: 650;
line-height: 15px;
}
& .subtitle {
color: var(--g-text-dark);
font-size: 0.8rem;
margin-bottom: 5px;
}
& .details {
color: var(--g-text-muted);
font-size: 0.7rem;
}
& .actions {
margin-top: 10px;
margin-left: 3px;
}
& .action-btn {
border: none;
padding: 5px 8px;
background-color: var(--g-popup-actions);
border-right: 1px solid var(--g-text-light);
&:hover {
background-color: brightness(97%);
}
&:first-child {
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
}
&:last-child {
border-right: none;
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
}
}
}
& .grid-header {
height: calc(
var(--gv-lower-header-height) + var(--gv-upper-header-height) + 10px
);
background-color: var(--g-header-background);
position: sticky;
top: 0;
left: 0;
z-index: 1000;
}
& .lower-text,
& .upper-text {
text-anchor: middle;
}
& .upper-header {
height: var(--gv-upper-header-height);
}
& .lower-header {
height: var(--gv-lower-header-height);
}
& .lower-text {
font-size: 12px;
position: absolute;
width: calc(var(--gv-column-width) * 0.8);
height: calc(var(--gv-lower-header-height) * 0.8);
margin: 0 calc(var(--gv-column-width) * 0.1);
align-content: center;
text-align: center;
color: var(--g-text-muted);
}
& .upper-text {
position: absolute;
width: fit-content;
font-weight: 500;
font-size: 16px;
color: var(--g-text-dark);
height: calc(var(--gv-lower-header-height) * 0.66);
}
& .current-upper {
position: sticky;
left: 0 !important;
padding: 0 calc(var(--gv-lower-header-height) * 0.33);
background: white;
}
& .side-header {
position: sticky;
top: 5px;
right: 0;
float: right;
z-index: 1000;
line-height: 20px;
font-weight: 400;
width: max-content;
margin-left: auto;
padding-right: 5px;
padding-top: 5px;
background: var(--g-header-background);
}
& .side-header * {
transition-property: background-color;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
background-color: var(--g-actions-background);
text-align: -webkit-center;
text-align: center;
height: 1.75rem;
border-radius: 0.5rem;
border: none;
padding: 0 0.5rem;
color: var(--g-text-dark);
position: sticky;
margin: 5px;
font-size: 14px;
line-height: 1.15;
letter-spacing: 0.02em;
font-weight: 420;
&:last-child {
margin-right: 0;
}
&:hover {
filter: brightness(97.5%);
}
}
& .side-header select {
padding: 0;
padding-right: 1rem;
width: 85px;
}
& .date-range-highlight {
background-color: var(--g-progress-color);
border-radius: 12px;
height: calc(var(--gv-lower-header-height) - 6px);
top: calc(var(--gv-upper-header-height) + 5px);
position: absolute;
}
& .current-highlight {
position: absolute;
background: var(--g-today-highlight);
width: 1px;
z-index: 999;
}
& .current-ball-highlight {
position: absolute;
background: var(--g-today-highlight);
z-index: 1001;
border-radius: 50%;
}
& .current-date-highlight {
background: var(--g-today-highlight);
color: var(--g-text-light);
border-radius: 5px;
}
& .holiday-label {
position: absolute;
top: 0;
left: 0;
opacity: 0;
z-index: 1000;
background: --g-weekend-label-color;
border-radius: 5px;
padding: 2px 5px;
&.show {
opacity: 100;
}
}
& .extras {
position: sticky;
left: 0px;
& .adjust {
position: absolute;
left: 8px;
top: calc(var(--gv-grid-height) - 60px);
background-color: rgba(0, 0, 0, 0.7);
color: white;
border: none;
padding: 8px;
border-radius: 3px;
}
}
.hide {
display: none;
}
}
.gantt {
user-select: none;
-webkit-user-select: none;
position: absolute;
& .grid-background {
fill: none;
}
& .grid-row {
fill: var(--g-row-color);
}
& .row-line {
stroke: var(--g-border-color);
}
& .tick {
stroke: var(--g-tick-color);
stroke-width: 0.4;
&.thick {
stroke: var(--g-tick-color-thick);
stroke-width: 0.7;
}
}
& .arrow {
fill: none;
stroke: var(--g-arrow-color);
stroke-width: 1.5;
}
& .bar-wrapper .bar {
fill: var(--g-bar-color);
stroke: var(--g-bar-border);
stroke-width: 0;
transition: stroke-width 0.3s ease;
}
& .bar-progress {
fill: var(--g-progress-color);
}
& .bar-expected-progress {
fill: var(--g-expected-progress);
}
& .bar-invalid {
fill: transparent;
stroke: var(--g-bar-border);
stroke-width: 1;
stroke-dasharray: 5;
& ~ .bar-label {
fill: var(--g-text-light);
}
}
& .bar-label {
fill: var(--g-text-dark);
dominant-baseline: central;
font-family: Helvetica;
font-size: 13px;
font-weight: 400;
&.big {
fill: var(--g-text-dark);
text-anchor: start;
}
}
& .handle {
fill: var(--g-handle-color);
opacity: 0;
transition: opacity 0.3s ease;
&.active,
&.visible {
cursor: ew-resize;
opacity: 1;
}
}
& .handle.progress {
fill: var(--g-text-muted);
}
& .bar-wrapper {
cursor: pointer;
& .bar {
-webkit-filter: drop-shadow(1px 1px 2px rgba(15, 15, 15, 0.2));
filter: drop-shadow(1px 1px 2px rgba(15, 15, 15, 0.2));
border-radius: 3px;
}
& .bar.safari {
outline: 1px solid black;
}
&:hover {
.bar {
transition: transform 0.3s ease;
}
.date-range-highlight {
display: block;
}
}
}
}

21
src/styles/light.css Normal file
View File

@ -0,0 +1,21 @@
:root {
--g-arrow-color: #d7b15b;
--g-bar-color: #fff;
--g-bar-border: #fff;
--g-tick-color-thick: #e0e0e0;
--g-tick-color: #ebeef0;
--g-actions-background: #f3f3f3;
--g-border-color: #ebeff2;
--g-text-muted: #7c7c7c;
--g-text-light: #fff;
--g-text-dark: #171717;
--g-progress-color: #f3f3f3;
--g-handle-color: #37352f;
--g-weekend-label-color: #dcdce4;
--g-expected-progress: #c4c4e9;
--g-header-background: #fff;
--g-row-color: #fdfdfd;
--g-today-highlight: #37352f;
--g-popup-actions: #ebeff2;
--g-weekend-highlight-color: #f7f7f7;
}