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
on:
push:
branches: [master]
branches: [release]
jobs:
publish:

2
.gitignore vendored
View File

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

209
README.md
View File

@ -1,123 +1,158 @@
<div align="center">
<img src="https://github.com/frappe/design/blob/master/logos/logo-2019/frappe-gantt-logo.png" height="128">
<h2>Frappe Gantt</h2>
<p align="center">
<p>A simple, interactive, modern gantt chart library for the web</p>
<a href="https://frappe.github.io/gantt">
<b>View the demo »</b>
</a>
</p>
<div align="center" markdown="1">
<img src=".github/gantt-logo.jpg" width="80">
<h1>Frappe Gantt</h1>
**A modern, configurable, Gantt library for the web.**
</div>
<p align="center">
<a href="https://frappe.github.io/gantt">
<img src="https://cloud.githubusercontent.com/assets/9355208/21537921/4a38b194-cdbd-11e6-8110-e0da19678a6d.png">
</a>
</p>
![Hero Image](.github/hero-image.png)
### 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
```
### Usage
Include it in your HTML:
```
<script src="frappe-gantt.min.js"></script>
```html
<script src="frappe-gantt.umd.js"></script>
<link rel="stylesheet" href="frappe-gantt.css">
```
Or from the CDN:
```
```html
<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">
```
And start hacking:
Start using Gantt:
```js
var tasks = [
let tasks = [
{
id: 'Task 1',
id: '1',
name: 'Redesign website',
start: '2016-12-28',
end: '2016-12-31',
progress: 20,
dependencies: 'Task 2, Task 3',
custom_class: 'bar-milestone' // optional
progress: 20
},
...
]
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
<div class="gantt-target dark"></div>
```
Apart from these ones, two options - `popup` and `view_modes` (plural, not singular) - are available. They have "sub"-APIs, and thus are listed separately.
### 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:
1. Clone this repo.
2. `cd` into project directory
3. `yarn`
4. `yarn run dev`
5. Open `index.html` in your browser, make your code changes and test them.
2. `cd` into project directory.
3. Run `pnpm i` to install dependencies.
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.
6. Make your code changes and test them.
### Publishing
If you have publishing rights (Frappe Team), follow these steps to publish a new version.
Assuming the last commit (or a couple of commits) were enhancements or fixes,
1. Run `yarn build`
This will generate files in the `dist/` folder. These files need to be committed.
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)
<br />
<br />
<div align="center" style="padding-top: 0.75rem;">
<a href="https://frappe.io" target="_blank">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://frappe.io/files/Frappe-white.png">
<img src="https://frappe.io/files/Frappe-black.png" alt="Frappe Technologies" height="28"/>
</picture>
</a>
</div>

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",
"version": "0.9.0",
"version": "1.0.0",
"description": "A simple, modern, interactive gantt library for the web",
"main": "src/index.js",
"type": "module",
"scripts": {
"start": "yarn run dev",
"dev": "vite",
"build-dev": "vite build --watch",
"build": "vite build",

View File

@ -21,64 +21,70 @@ export default class Arrow {
while (condition()) {
start_x -= 10;
}
start_x -= 10;
const start_y =
this.gantt.options.header_height +
let start_y =
this.gantt.config.header_height +
this.gantt.options.bar_height +
(this.gantt.options.padding + this.gantt.options.bar_height) *
this.from_task.task._index +
this.gantt.options.padding;
this.gantt.options.padding / 2;
const end_x =
this.to_task.$bar.getX() - this.gantt.options.padding / 2 - 7;
const end_y =
this.gantt.options.header_height +
let end_x = this.to_task.$bar.getX() - 13;
let end_y =
this.gantt.config.header_height +
this.gantt.options.bar_height / 2 +
(this.gantt.options.padding + this.gantt.options.bar_height) *
this.to_task.task._index +
this.gantt.options.padding;
this.gantt.options.padding / 2;
const from_is_below_to =
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 = `
M ${start_x} ${start_y}
V ${offset}
a ${curve} ${curve} 0 0 ${clockwise} ${curve} ${curve_y}
L ${end_x} ${end_y}
m -5 -5
l 5 5
l -5 5`;
let curve = this.gantt.options.arrow_curve;
const clockwise = from_is_below_to ? 1 : 0;
let curve_y = from_is_below_to ? -curve : curve;
if (
this.to_task.$bar.getX() <
this.to_task.$bar.getX() <=
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 =
this.to_task.$bar.getY() +
this.to_task.$bar.getHeight() / 2 -
curve_y;
const left = this.to_task.$bar.getX() - this.gantt.options.padding;
this.path = `
M ${start_x} ${start_y}
v ${down_1}
a ${curve} ${curve} 0 0 1 -${curve} ${curve}
a ${curve} ${curve} 0 0 1 ${-curve} ${curve}
H ${left}
a ${curve} ${curve} 0 0 ${clockwise} -${curve} ${curve_y}
a ${curve} ${curve} 0 0 ${clockwise} ${-curve} ${curve_y}
V ${down_2}
a ${curve} ${curve} 0 0 ${clockwise} ${curve} ${curve_y}
L ${end_x} ${end_y}
m -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 {
constructor(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.bind();
}
@ -13,11 +27,24 @@ export default class Bar {
this.action_completed = false;
this.gantt = gantt;
this.task = task;
this.name = this.name || '';
}
prepare() {
this.prepare_values();
this.prepare_helpers();
prepare_wrappers() {
this.group = createSVG('g', {
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() {
@ -29,25 +56,8 @@ export default class Bar {
this.compute_duration();
this.corner_radius = this.gantt.options.bar_corner_radius;
this.width = this.gantt.config.column_width * this.duration;
this.progress_width =
this.gantt.config.column_width *
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,
});
if (this.task.progress < 0) this.task.progress = 0;
if (this.task.progress > 100) this.task.progress = 100;
}
prepare_helpers() {
@ -101,12 +111,12 @@ export default class Bar {
ry: this.corner_radius,
class:
'bar' +
(/^((?!chrome|android).)*safari/i.test(navigator.userAgent) &&
!this.task.important
(/^((?!chrome|android).)*safari/i.test(navigator.userAgent)
? ' safari'
: ''),
append_to: this.bar_group,
});
if (this.task.color) this.$bar.style.fill = this.task.color;
animateSVG(this.$bar, 'width', 0, this.width);
if (this.invalid) {
@ -137,7 +147,7 @@ export default class Bar {
draw_progress_bar() {
if (this.invalid) return;
this.progress_width = this.calculate_progress_width();
this.$bar_progress = createSVG('rect', {
x: this.x,
y: this.y,
@ -148,25 +158,59 @@ export default class Bar {
class: 'bar-progress',
append_to: this.bar_group,
});
if (this.task.color_progress)
this.$bar_progress.style.fill = this.task.color;
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.column_width;
let $date_highlight = document.createElement('div');
$date_highlight.id = `highlight-${this.task.id}`;
$date_highlight.classList.add('date-highlight');
$date_highlight.style.height = this.height * 0.8 + 'px';
$date_highlight.style.width = this.width + 'px';
$date_highlight.style.top =
this.gantt.options.header_height - 25 + 'px';
$date_highlight.style.left = x + 'px';
let $date_highlight = this.gantt.create_el({
classes: `date-range-highlight hide highlight-${this.task.id}`,
width: this.width,
left: x,
});
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);
}
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() {
let x_coord = this.x + this.$bar.getWidth() / 2;
@ -184,6 +228,7 @@ export default class Bar {
// labels get BBox in the next tick
requestAnimationFrame(() => this.update_label_position());
}
draw_thumbnail() {
let x_offset = 10,
y_offset = 2;
@ -230,39 +275,50 @@ export default class Bar {
if (this.invalid || this.gantt.options.readonly) return;
const bar = this.$bar;
const handle_width = 8;
if (!this.gantt.options.dates_readonly) {
createSVG('rect', {
x: bar.getX() + bar.getWidth() + handle_width - 4,
y: bar.getY() + 1,
width: handle_width,
height: this.height - 2,
rx: this.corner_radius,
ry: this.corner_radius,
class: 'handle right',
append_to: this.handle_group,
});
const handle_width = 3;
this.handles = [];
if (!this.gantt.options.readonly_dates) {
this.handles.push(
createSVG('rect', {
x: bar.getEndX() - handle_width / 2,
y: bar.getY() + this.height / 4,
width: handle_width,
height: this.height / 2,
rx: 2,
ry: 2,
class: 'handle right',
append_to: this.handle_group,
}),
);
createSVG('rect', {
x: bar.getX() - handle_width - 4,
y: bar.getY() + 1,
width: handle_width,
height: this.height - 2,
rx: this.corner_radius,
ry: this.corner_radius,
class: 'handle left',
append_to: this.handle_group,
});
this.handles.push(
createSVG('rect', {
x: bar.getX() - handle_width / 2,
y: bar.getY() + this.height / 4,
width: handle_width,
height: this.height / 2,
rx: 2,
ry: 2,
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;
this.$handle_progress = createSVG('circle', {
cx: bar_progress.getEndX(),
cy: bar_progress.getY() + bar_progress.getHeight() / 2,
r: 5,
r: 4.5,
class: 'handle progress',
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') {
let opened = false;
$.on(this.group, 'click', (e) => {
if (!opened) {
this.show_popup(e.offsetX || e.layerX);
document.getElementById(
`highlight-${task_id}`,
).style.display = 'block';
} else {
this.gantt.hide_popup();
$.on(this.group, 'mouseup', (e) => {
const posX = e.offsetX || e.layerX;
if (this.$handle_progress) {
const cx = +this.$handle_progress.getAttribute('cx');
if (cx > posX - 1 && cx < posX + 1) return;
if (this.gantt.bar_being_dragged) return;
}
opened = !opened;
});
} else {
let timeout;
$.on(
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';
this.gantt.show_popup({
x: e.offsetX || e.layerX,
y: e.offsetY || e.layerY,
task: this.task,
target: this.$bar,
});
});
}
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', () => {
this.gantt.trigger_event('click', [this.task]);
@ -329,70 +389,48 @@ export default class Bar {
}
this.group.classList.remove('active');
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]);
});
}
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 }) {
const bar = this.$bar;
if (x) {
// get all x values of parent task
const xs = this.task.dependencies.map((dep) => {
return this.gantt.get_bar(dep).$bar.getX();
});
// child task must not go before parent
const valid_x = xs.reduce((_, curr) => {
return x >= curr;
}, x);
if (!valid_x) {
width = null;
return;
}
if (!valid_x) return;
this.update_attr(bar, 'x', x);
this.x = x;
this.$date_highlight.style.left = x + 'px';
}
if (width) {
if (width > 0) {
this.update_attr(bar, 'width', width);
this.$date_highlight.style.width = width + 'px';
}
this.update_label_position();
this.update_handle_position();
this.date_changed();
this.compute_duration();
if (this.gantt.options.show_expected_progress) {
this.date_changed();
this.compute_duration();
this.update_expected_progressbar_position();
}
this.update_progressbar_position();
this.update_arrow_position();
}
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 img = this.group.querySelector('.bar-img') || '';
const img_mask = this.bar_group.querySelector('.img_mask') || '';
@ -448,9 +486,11 @@ export default class Bar {
}
progress_changed() {
const new_progress = this.compute_progress();
this.task.progress = new_progress;
this.gantt.trigger_event('progress_change', [this.task, new_progress]);
this.task.progress = this.compute_progress();
this.gantt.trigger_event('progress_change', [
this.task,
this.task.progress,
]);
}
set_action_completed() {
@ -466,17 +506,6 @@ export default class Bar {
x_in_units * this.gantt.config.step,
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 new_end_date = date_utils.add(
@ -489,9 +518,20 @@ export default class Bar {
}
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 =
(this.$bar_progress.getWidth() / this.$bar.getWidth()) * 100;
return parseInt(progress, 10);
this.progress_width -
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() {
@ -507,13 +547,14 @@ export default class Bar {
}
compute_x() {
const { step, column_width } = this.gantt.config;
const { column_width } = this.gantt.config;
const task_start = this.task._start;
const gantt_start = this.gantt.gantt_start;
const diff =
date_utils.diff(task_start, gantt_start, this.gantt.config.unit) /
this.gantt.config.step;
let x = diff * column_width;
/* 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
so it is within the column */
if (this.gantt.view_is('Month')) {
const diffDaysBasedOn30DayMonths =
date_utils.diff(task_start, gantt_start, 'month') * 30;
const dayInMonth = Math.min(
29,
date_utils.format(
task_start,
'DD',
this.gantt.options.language,
),
);
const diff = diffDaysBasedOn30DayMonths + dayInMonth;
// if (this.gantt.view_is('Month')) {
// const diffDaysBasedOn30DayMonths =
// date_utils.diff(task_start, gantt_start, 'month') * 30;
// const dayInMonth = Math.min(
// 29,
// date_utils.format(
// task_start,
// 'DD',
// this.gantt.options.language,
// ),
// );
// const diff = diffDaysBasedOn30DayMonths + dayInMonth;
x = (diff * column_width) / 30;
}
// x = (diff * column_width) / 30;
// }
this.x = x;
}
compute_y() {
this.y =
this.gantt.options.header_height +
this.gantt.options.padding +
this.gantt.config.header_height +
this.gantt.options.padding / 2 +
this.task._index * (this.height + this.gantt.options.padding);
}
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 =
date_utils.diff(
this.task._end,
this.task._start,
date_utils.convert_scales(
duration_in_days + 'd',
this.gantt.config.unit,
) / this.gantt.config.step;
}
get_snap_position(dx) {
let odx = dx,
rem,
position;
this.actual_duration_raw =
date_utils.convert_scales(
actual_duration_in_days + 'd',
this.gantt.config.unit,
) / this.gantt.config.step;
// if (this.gantt.view_is('Week')) {
// 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;
this.ignored_duration_raw = this.duration - this.actual_duration_raw;
}
update_attr(element, attr, value) {
@ -604,7 +640,7 @@ export default class Bar {
this.$expected_bar_progress.setAttribute(
'width',
this.gantt.config.column_width *
this.duration *
this.actual_duration_raw *
(this.expected_progress / 100) || 0,
);
}
@ -612,9 +648,10 @@ export default class Bar {
update_progressbar_position() {
if (this.invalid || this.gantt.options.readonly) return;
this.$bar_progress.setAttribute('x', this.$bar.getX());
this.$bar_progress.setAttribute(
'width',
this.$bar.getWidth() * (this.task.progress / 100),
this.calculate_progress_width(),
);
}
@ -631,17 +668,11 @@ export default class Bar {
if (labelWidth > barWidth) {
label.classList.add('big');
if (img) {
img.setAttribute('x', bar.getX() + bar.getWidth() + padding);
img_mask.setAttribute(
'x',
bar.getX() + bar.getWidth() + padding,
);
label.setAttribute(
'x',
bar.getX() + bar.getWidth() + x_offset_label_img,
);
img.setAttribute('x', bar.getEndX() + padding);
img_mask.setAttribute('x', bar.getEndX() + padding);
label.setAttribute('x', bar.getEndX() + x_offset_label_img);
} else {
label.setAttribute('x', bar.getX() + bar.getWidth() + padding);
label.setAttribute('x', bar.getEndX() + padding);
}
} else {
label.classList.remove('big');
@ -666,10 +697,10 @@ export default class Bar {
const bar = this.$bar;
this.handle_group
.querySelector('.handle.left')
.setAttribute('x', bar.getX() - 12);
.setAttribute('x', bar.getX());
this.handle_group
.querySelector('.handle.right')
.setAttribute('x', bar.getEndX() + 4);
.setAttribute('x', bar.getEndX());
const handle = this.group.querySelector('.handle.progress');
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 : '');
},
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, {
month: 'long',
});
@ -103,7 +103,7 @@ export default {
MMM: dateTimeFormatShort.format(date),
};
let str = format_string;
let str = date_format;
const formatted_values = [];
Object.keys(format_map)
@ -125,7 +125,10 @@ export default {
diff(date_a, date_b, scale = 'day') {
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;
minutes = seconds / 60;
hours = minutes / 60;

View File

@ -1,13 +1,26 @@
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 = [
{
name: 'Hour',
padding: '7d',
step: '1h',
date_format: 'YYYY-MM-DD HH:',
lower_text: 'HH',
upper_text: (d, ld, lang) =>
d.getDate() !== ld.getDate()
!ld || d.getDate() !== ld.getDate()
? date_utils.format(d, 'D MMMM', lang)
: '',
upper_text_frequency: 24,
@ -16,22 +29,22 @@ const DEFAULT_VIEW_MODES = [
name: 'Quarter Day',
padding: '7d',
step: '6h',
format_string: 'YYYY-MM-DD HH',
date_format: 'YYYY-MM-DD HH:',
lower_text: 'HH',
upper_text: (d, ld, lang) =>
d.getDate() !== ld.getDate()
!ld || d.getDate() !== ld.getDate()
? date_utils.format(d, 'D MMM', lang)
: '',
upper_text_frequency: 4,
},
{
name: 'Half Day',
padding: '7d',
padding: '14d',
step: '12h',
format_string: 'YYYY-MM-DD HH',
date_format: 'YYYY-MM-DD HH:',
lower_text: 'HH',
upper_text: (d, ld, lang) =>
d.getDate() !== ld.getDate()
!ld || d.getDate() !== ld.getDate()
? d.getMonth() !== d.getMonth()
? date_utils.format(d, 'D MMM', lang)
: date_utils.format(d, 'D', lang)
@ -40,13 +53,15 @@ const DEFAULT_VIEW_MODES = [
},
{
name: 'Day',
padding: '14d',
format_string: 'YYYY-MM-DD',
padding: '7d',
date_format: 'YYYY-MM-DD',
step: '1d',
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) =>
d.getMonth() !== ld.getMonth()
!ld || d.getMonth() !== ld.getMonth()
? date_utils.format(d, 'MMMM', lang)
: '',
thick_line: (d) => d.getDay() === 1,
@ -55,13 +70,11 @@ const DEFAULT_VIEW_MODES = [
name: 'Week',
padding: '1m',
step: '7d',
date_format: 'YYYY-MM-DD',
column_width: 140,
lower_text: (d, ld, lang) =>
d.getMonth() !== ld.getMonth()
? date_utils.format(d, 'D MMM', lang)
: date_utils.format(d, 'D', lang),
lower_text: formatWeek,
upper_text: (d, ld, lang) =>
d.getMonth() !== ld.getMonth()
!ld || d.getMonth() !== ld.getMonth()
? date_utils.format(d, 'MMMM', lang)
: '',
thick_line: (d) => d.getDate() >= 1 && d.getDate() <= 7,
@ -72,51 +85,76 @@ const DEFAULT_VIEW_MODES = [
padding: '2m',
step: '1m',
column_width: 120,
format_string: 'YYYY-MM',
date_format: 'YYYY-MM',
lower_text: 'MMMM',
upper_text: (d, ld, lang) =>
!ld || d.getFullYear() !== ld.getFullYear()
? date_utils.format(d, 'YYYY', lang)
: '',
thick_line: (d) => d.getMonth() % 3 === 0,
default_snap: '7d',
snap_at: '7d',
},
{
name: 'Year',
padding: '2y',
step: '1y',
column_width: 120,
format_string: 'YYYY',
upper_text: 'YYYY',
default_snap: '30d',
date_format: 'YYYY',
upper_text: (d, ld, lang) =>
!ld || getDecade(d) !== getDecade(ld) ? getDecade(d) : '',
lower_text: 'YYYY',
snap_at: '30d',
},
];
const DEFAULT_OPTIONS = {
header_height: 65,
column_width: 30,
view_modes: DEFAULT_VIEW_MODES,
bar_height: 30,
bar_corner_radius: 3,
arrow_curve: 5,
padding: 18,
view_mode: 'Day',
date_format: 'YYYY-MM-DD',
move_dependencies: true,
show_expected_progress: false,
popup: null,
popup_on: 'hover',
auto_move_label: false,
bar_corner_radius: 3,
bar_height: 30,
container_height: 'auto',
column_width: null,
date_format: 'YYYY-MM-DD HH:mm',
upper_header_height: 45,
lower_header_height: 30,
snap_at: null,
infinite_padding: true,
holidays: { 'var(--g-weekend-highlight-color)': 'weekend' },
ignore: [],
language: 'en',
readonly: false,
progress_readonly: false,
dates_readonly: false,
highlight_weekend: true,
scroll_to: 'start',
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,
view_mode: 'Day',
view_mode_select: false,
default_snap: '1d',
view_modes: 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 {
constructor(parent, custom_html) {
constructor(parent, popup_func, gantt) {
this.parent = parent;
this.custom_html = custom_html;
this.popup_func = popup_func;
this.gantt = gantt;
this.make();
}
@ -9,55 +11,51 @@ export default class Popup {
this.parent.innerHTML = `
<div class="title"></div>
<div class="subtitle"></div>
<div class="pointer"></div>
<div class="details"></div>
<div class="actions"></div>
`;
this.hide();
this.title = this.parent.querySelector('.title');
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) {
if (!options.target_element) {
throw new Error('target_element is required to show popup');
}
const target_element = options.target_element;
show({ x, y, task, target }) {
this.actions.innerHTML = '';
let html = this.popup_func({
task,
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) {
let html = this.custom_html(options.task);
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;
}
if (this.actions.innerHTML === '') this.actions.remove();
else this.parent.appendChild(this.actions);
// set position
let position_meta;
if (target_element instanceof HTMLElement) {
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;
this.parent.style.left = x + 10 + 'px';
this.parent.style.top = y - 10 + 'px';
this.parent.classList.remove('hide');
}
hide() {
this.parent.style.opacity = 0;
this.parent.style.left = 0;
this.parent.classList.add('hide');
}
}

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;
}