Publish v1
This commit is contained in:
commit
1d78ec1be9
@ -1,7 +0,0 @@
|
||||
{
|
||||
"extends": ["plugin:prettier/recommended"],
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 6,
|
||||
"sourceType": "module"
|
||||
}
|
||||
}
|
||||
BIN
.github/gantt-logo.jpg
vendored
Normal file
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
BIN
.github/hero-image.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 113 KiB |
2
.github/workflows/publish.yml
vendored
2
.github/workflows/publish.yml
vendored
@ -1,7 +1,7 @@
|
||||
name: Publish on NPM
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
branches: [release]
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@ -31,4 +31,4 @@ node_modules
|
||||
.DS_Store
|
||||
|
||||
gh-pages
|
||||
feedback.md
|
||||
feedback*.md
|
||||
|
||||
209
README.md
209
README.md
@ -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>
|
||||

|
||||
|
||||
### 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 today’s 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
115
builder/demo.css
Normal 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
326
builder/demo.js
Normal 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
19
eslint.config.mjs
Normal 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",
|
||||
},
|
||||
}];
|
||||
132
index.html
132
index.html
@ -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>
|
||||
@ -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",
|
||||
|
||||
60
src/arrow.js
60
src/arrow.js
@ -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`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
451
src/bar.js
451
src/bar.js
@ -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]'
|
||||
);
|
||||
}
|
||||
|
||||
99
src/dark.css
99
src/dark.css
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
116
src/defaults.js
116
src/defaults.js
@ -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 };
|
||||
|
||||
310
src/gantt.css
310
src/gantt.css
@ -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;
|
||||
}
|
||||
}
|
||||
1122
src/index.js
1122
src/index.js
File diff suppressed because it is too large
Load Diff
78
src/popup.js
78
src/popup.js
@ -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
87
src/styles/dark.css
Normal 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
345
src/styles/gantt.css
Normal 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
21
src/styles/light.css
Normal 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;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user