Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d1f9b6bf1 | ||
|
|
d3d725c25e | ||
|
|
390fd2d324 | ||
|
|
e55107ee82 |
7
.eslintrc
Normal file
7
.eslintrc
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": ["plugin:prettier/recommended"],
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 6,
|
||||
"sourceType": "module"
|
||||
}
|
||||
}
|
||||
BIN
.github/gantt-logo.jpg
vendored
BIN
.github/gantt-logo.jpg
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 5.0 KiB |
BIN
.github/hero-image.png
vendored
BIN
.github/hero-image.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 113 KiB |
23
.github/workflows/publish.yml
vendored
23
.github/workflows/publish.yml
vendored
@ -1,23 +0,0 @@
|
||||
name: Publish on NPM
|
||||
on:
|
||||
push:
|
||||
branches: [release]
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '18'
|
||||
cache: 'pnpm'
|
||||
- run: pnpm install
|
||||
- run: pnpm prettier-check
|
||||
- run: pnpm build
|
||||
- uses: JS-DevTools/npm-publish@v1
|
||||
with:
|
||||
token: ${{ secrets.NPM_TOKEN }}
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@ -21,14 +21,11 @@ coverage
|
||||
|
||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
dist/*
|
||||
|
||||
# Dependency directory
|
||||
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
|
||||
node_modules
|
||||
.yarn
|
||||
|
||||
.DS_Store
|
||||
|
||||
gh-pages
|
||||
feedback*.md
|
||||
gh-pages
|
||||
@ -1 +0,0 @@
|
||||
dist
|
||||
@ -1,4 +1,4 @@
|
||||
{
|
||||
"tabWidth": 4,
|
||||
"singleQuote": true
|
||||
}
|
||||
"tabWidth": 4,
|
||||
"singleQuote": true
|
||||
}
|
||||
162
README.md
162
README.md
@ -1,158 +1,46 @@
|
||||
<div align="center" markdown="1">
|
||||
<img src=".github/gantt-logo.jpg" width="80">
|
||||
<h1>Frappe Gantt</h1>
|
||||
# Frappé Gantt
|
||||
A simple, interactive, modern gantt chart library for the web
|
||||
|
||||
**A modern, configurable, Gantt library for the web.**
|
||||
</div>
|
||||

|
||||
|
||||

|
||||
#### View the demo [here](https://frappe.github.io/gantt).
|
||||
|
||||
## 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
|
||||
### Install
|
||||
```
|
||||
npm install frappe-gantt
|
||||
```
|
||||
|
||||
Include it in your HTML:
|
||||
|
||||
```html
|
||||
<script src="frappe-gantt.umd.js"></script>
|
||||
<link rel="stylesheet" href="frappe-gantt.css">
|
||||
### Usage
|
||||
Include it in your html:
|
||||
```
|
||||
<script src="frappe-gantt.min.js"></script>
|
||||
```
|
||||
|
||||
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">
|
||||
```
|
||||
|
||||
Start using Gantt:
|
||||
And start hacking:
|
||||
```js
|
||||
let tasks = [
|
||||
var tasks = [
|
||||
{
|
||||
id: '1',
|
||||
id: 'Task 1',
|
||||
name: 'Redesign website',
|
||||
start: '2016-12-28',
|
||||
end: '2016-12-31',
|
||||
progress: 20
|
||||
progress: 20,
|
||||
dependencies: 'Task 2, Task 3',
|
||||
custom_class: 'bar-milestone' // optional
|
||||
},
|
||||
...
|
||||
]
|
||||
let gantt = new Gantt("#gantt", tasks);
|
||||
var gantt = new Gantt("#gantt", tasks);
|
||||
```
|
||||
|
||||
### Configuration
|
||||
Frappe Gantt offers a wide range of options to customize your chart.
|
||||
|
||||
|
||||
| **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` |
|
||||
|
||||
Apart from these ones, two options - `popup` and `view_modes` (plural, not singular) - are available. They have "sub"-APIs, and thus are listed separately.
|
||||
|
||||
#### View Mode Configuration
|
||||
The `view_modes` option determines all the available view modes for the chart. It should be an array of objects.
|
||||
|
||||
Each object can have the following properties:
|
||||
- `name` (string) - the name of view mode.
|
||||
- `padding` (interval) - the time above.
|
||||
- `step` - the interval of each column
|
||||
- `lower_text` (date format string _or_ function) - the format for text in lower header. Blank string for none. The function takes in `currentDate`, `previousDate`, and `lang`, and should return a string.
|
||||
- `upper_text` (date format string _or_ function) - the format for text in upper header. Blank string for none. The function takes in `currentDate`, `previousDate`, and `lang`, and should return a string.
|
||||
- `upper_text_frequency` (number) - how often the upper text has a value. Utilized in internal calculation to improve performance.
|
||||
- `thick_line` (function) - takes in `currentDate`, returns Boolean determining whether the line for that date should be thicker than the others.
|
||||
|
||||
Three other options allow you to override general configuration for this view mode alone:
|
||||
- `date_format`
|
||||
- `column_width`
|
||||
- `snap_at`
|
||||
For details, see the above table.
|
||||
|
||||
#### Popup Configuration
|
||||
`popup` is a function. If it returns
|
||||
- `false`, there will be no popup.
|
||||
- `undefined`, the popup will be rendered based on manipulation within the function
|
||||
- a HTML string, the popup will be that string.
|
||||
|
||||
The function receives one object as an argument, containing:
|
||||
- `task` - the task as an object
|
||||
- `chart` - the entire Gantt chart
|
||||
- `get_title`, `get_subtitle`, `get_details` (functions) - get the relevant section as a HTML node.
|
||||
- `set_title`, `set_subtitle`, `set_details` (functions) - take in the HTML of the relevant section
|
||||
- `add_action` (function) - accepts two parameters, `html` and `func` - respectively determining the HTML of the action and the callback when the action is pressed.
|
||||
|
||||
### API
|
||||
Frappe Gantt exposes a few helpful methods for you to interact with the chart:
|
||||
|
||||
| **Name** | **Description** | **Parameters** |
|
||||
|---------------------------|---------------------------------------------------------------------------------|------------------------------------------|
|
||||
| `.update_options` | Re-renders the chart after updating specific options. | `new_options` - object containing new options. |
|
||||
| `.change_view_mode` | Updates the view mode. | `view_mode` - Name of view mode _or_ view mode object (see above) and `maintain_pos` - whether to go back to current scroll position after rerendering, defaults to `false`. |
|
||||
| `.scroll_current` | Scrolls to the current date | No parameters. |
|
||||
| `.update_task` | Re-renders a specific task bar alone | `task_id` - id of task and `new_details` - object containing the task properties to be updated. |
|
||||
|
||||
## Development Setup
|
||||
If you want to contribute enhancements or fixes:
|
||||
If you want to contribute:
|
||||
|
||||
1. Clone this repo.
|
||||
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.
|
||||
2. `cd` into project directory
|
||||
3. `npm install`
|
||||
4. `npm run dev`
|
||||
|
||||
<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>
|
||||
License: MIT
|
||||
|
||||
------------------
|
||||
Project maintained by [frappe](https://github.com/frappe)
|
||||
|
||||
115
builder/demo.css
115
builder/demo.css
@ -1,115 +0,0 @@
|
||||
.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
326
builder/demo.js
@ -1,326 +0,0 @@
|
||||
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);
|
||||
};
|
||||
}
|
||||
}
|
||||
117
dist/frappe-gantt.css
vendored
Normal file
117
dist/frappe-gantt.css
vendored
Normal file
@ -0,0 +1,117 @@
|
||||
.gantt .grid-background {
|
||||
fill: none; }
|
||||
|
||||
.gantt .grid-header {
|
||||
fill: #ffffff;
|
||||
stroke: #e0e0e0;
|
||||
stroke-width: 1.4; }
|
||||
|
||||
.gantt .grid-row {
|
||||
fill: #ffffff; }
|
||||
|
||||
.gantt .grid-row:nth-child(even) {
|
||||
fill: #f5f5f5; }
|
||||
|
||||
.gantt .row-line {
|
||||
stroke: #ebeff2; }
|
||||
|
||||
.gantt .tick {
|
||||
stroke: #e0e0e0;
|
||||
stroke-width: 0.2; }
|
||||
.gantt .tick.thick {
|
||||
stroke-width: 0.4; }
|
||||
|
||||
.gantt .today-highlight {
|
||||
fill: #fcf8e3;
|
||||
opacity: 0.5; }
|
||||
|
||||
.gantt .arrow {
|
||||
fill: none;
|
||||
stroke: #666;
|
||||
stroke-width: 1.4; }
|
||||
|
||||
.gantt .bar {
|
||||
fill: #b8c2cc;
|
||||
stroke: #8D99A6;
|
||||
stroke-width: 0;
|
||||
transition: stroke-width .3s ease;
|
||||
user-select: none; }
|
||||
|
||||
.gantt .bar-progress {
|
||||
fill: #a3a3ff; }
|
||||
|
||||
.gantt .bar-invalid {
|
||||
fill: transparent;
|
||||
stroke: #8D99A6;
|
||||
stroke-width: 1;
|
||||
stroke-dasharray: 5; }
|
||||
.gantt .bar-invalid ~ .bar-label {
|
||||
fill: #555; }
|
||||
|
||||
.gantt .bar-label {
|
||||
fill: #fff;
|
||||
dominant-baseline: central;
|
||||
text-anchor: middle;
|
||||
font-size: 12px;
|
||||
font-weight: lighter; }
|
||||
.gantt .bar-label.big {
|
||||
fill: #555;
|
||||
text-anchor: start; }
|
||||
|
||||
.gantt .handle {
|
||||
fill: #ddd;
|
||||
cursor: ew-resize;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: opacity .3s ease; }
|
||||
|
||||
.gantt .bar-wrapper {
|
||||
cursor: pointer; }
|
||||
.gantt .bar-wrapper:hover .bar {
|
||||
fill: #a9b5c1; }
|
||||
.gantt .bar-wrapper:hover .bar-progress {
|
||||
fill: #8a8aff; }
|
||||
.gantt .bar-wrapper:hover .handle {
|
||||
visibility: visible;
|
||||
opacity: 1; }
|
||||
.gantt .bar-wrapper.active .bar {
|
||||
fill: #a9b5c1; }
|
||||
.gantt .bar-wrapper.active .bar-progress {
|
||||
fill: #8a8aff; }
|
||||
|
||||
.gantt .lower-text, .gantt .upper-text {
|
||||
font-size: 12px;
|
||||
text-anchor: middle; }
|
||||
|
||||
.gantt .upper-text {
|
||||
fill: #555; }
|
||||
|
||||
.gantt .lower-text {
|
||||
fill: #333; }
|
||||
|
||||
.gantt .hide {
|
||||
display: none; }
|
||||
|
||||
.gantt-container {
|
||||
position: relative;
|
||||
font-size: 12px; }
|
||||
.gantt-container .popup-wrapper {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
padding: 0;
|
||||
color: #959da5;
|
||||
border-radius: 3px; }
|
||||
.gantt-container .popup-wrapper .title {
|
||||
border-bottom: 3px solid #a3a3ff;
|
||||
padding: 10px; }
|
||||
.gantt-container .popup-wrapper .subtitle {
|
||||
padding: 10px;
|
||||
color: #dfe2e5; }
|
||||
.gantt-container .popup-wrapper .pointer {
|
||||
position: absolute;
|
||||
height: 5px;
|
||||
margin: 0 0 0 -5px;
|
||||
border: 5px solid transparent;
|
||||
border-top-color: rgba(0, 0, 0, 0.8); }
|
||||
1766
dist/frappe-gantt.js
vendored
Normal file
1766
dist/frappe-gantt.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
dist/frappe-gantt.js.map
vendored
Normal file
1
dist/frappe-gantt.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/frappe-gantt.min.js
vendored
Normal file
1
dist/frappe-gantt.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
5
dist/frappe-gantt.min.js.map
vendored
Normal file
5
dist/frappe-gantt.min.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -1,19 +0,0 @@
|
||||
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",
|
||||
},
|
||||
}];
|
||||
859
index.html
859
index.html
@ -1,785 +1,80 @@
|
||||
<!doctype html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Simple Gantt</title>
|
||||
<link rel="stylesheet" href="dist/frappe-gantt.css" />
|
||||
<link
|
||||
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css"
|
||||
rel="stylesheet"
|
||||
integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN"
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
<style>
|
||||
.container {
|
||||
width: 90%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Simple Gantt</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
background: #ccc;
|
||||
}
|
||||
.container {
|
||||
width: 80%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.gantt-container {
|
||||
overflow: scroll;
|
||||
}
|
||||
/* custom class */
|
||||
.gantt .bar-milestone .bar-progress {
|
||||
fill: tomato;
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" href="dist/frappe-gantt.css" />
|
||||
<script src="dist/frappe-gantt.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h2>Interactive Gantt Chart entirely made in SVG!</h2>
|
||||
<div class="gantt-target"></div>
|
||||
</div>
|
||||
<script>
|
||||
var names = [
|
||||
["Redesign website", [0, 7]],
|
||||
["Write new content", [1, 4]],
|
||||
["Apply new styles", [3, 6]],
|
||||
["Review", [7, 7]],
|
||||
["Deploy", [8, 9]],
|
||||
["Go Live!", [10, 10]]
|
||||
];
|
||||
|
||||
.chart {
|
||||
border: 1px dotted black;
|
||||
border-radius: 4px;
|
||||
height: fit-content;
|
||||
}
|
||||
var tasks = names.map(function(name, i) {
|
||||
var today = new Date();
|
||||
var start = new Date(today.getFullYear(), today.getMonth(), today.getDate());
|
||||
var end = new Date(today.getFullYear(), today.getMonth(), today.getDate());
|
||||
start.setDate(today.getDate() + name[1][0]);
|
||||
end.setDate(today.getDate() + name[1][1]);
|
||||
return {
|
||||
start: start,
|
||||
end: end,
|
||||
name: name[0],
|
||||
id: "Task " + i,
|
||||
progress: parseInt(Math.random() * 100, 10)
|
||||
}
|
||||
});
|
||||
tasks[1].progress = 0;
|
||||
tasks[1].dependencies = "Task 0"
|
||||
tasks[2].dependencies = "Task 1"
|
||||
tasks[3].dependencies = "Task 2"
|
||||
tasks[5].dependencies = "Task 4"
|
||||
tasks[5].custom_class = "bar-milestone";
|
||||
|
||||
.chart.active {
|
||||
filter: drop-shadow(1px 1px 4px rgba(0, 0, 0, 0.6));
|
||||
border: unset;
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: 0.775em;
|
||||
}
|
||||
</style>
|
||||
<script src="dist/frappe-gantt.umd.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1 class="text-center pt-3 pb-2 font-serif">Frappe Gantt</h1>
|
||||
<hr />
|
||||
<div class="row my-5">
|
||||
<div class="col-md-3 px-5 py-1">
|
||||
<h3 class="text-center">Set edit access</h3>
|
||||
<p>
|
||||
Easy make sure your employees change <em>only</em> what
|
||||
they need to.
|
||||
</p>
|
||||
<div class="form-check form-switch">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
role="switch"
|
||||
id="mutable-general"
|
||||
checked
|
||||
/>
|
||||
<label class="form-check-label" for="mutable-general"
|
||||
>Editable</label
|
||||
>
|
||||
</div>
|
||||
<div class="form-check form-switch">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
role="switch"
|
||||
id="mutable-progress"
|
||||
checked
|
||||
/>
|
||||
<label class="form-check-label" for="mutable-general"
|
||||
>Progress editable</label
|
||||
>
|
||||
</div>
|
||||
<div class="form-check form-switch">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
role="switch"
|
||||
id="mutable-dates"
|
||||
checked
|
||||
/>
|
||||
<label class="form-check-label" for="mutable-general"
|
||||
>Dates editable</label
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chart col-md-9" id="mutability"></div>
|
||||
</div>
|
||||
<div class="row my-5">
|
||||
<div class="chart col-md-9" id="sideheader"></div>
|
||||
<div class="col-md-3 px-5 py-1">
|
||||
<h3 class="text-center">Versatile Actions</h3>
|
||||
<p>
|
||||
Change the view mode, or scroll to today, or add
|
||||
anything you like <sup>β</sup>.
|
||||
</p>
|
||||
<div class="form-check form-switch">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
role="switch"
|
||||
id="toggle-today"
|
||||
checked
|
||||
/>
|
||||
<label class="form-check-label" for="mutable-general"
|
||||
>Scroll to Today</label
|
||||
>
|
||||
</div>
|
||||
<div class="form-check form-switch">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
role="switch"
|
||||
id="toggle-view-mode"
|
||||
checked
|
||||
/>
|
||||
<label class="form-check-label" for="mutable-general"
|
||||
>Change View Mode</label
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row my-5">
|
||||
<div class="col-md-3 px-5 py-1">
|
||||
<h3 class="text-center">Mark Holidays</h3>
|
||||
<p>
|
||||
Be it public holidays, company milestones, or just
|
||||
weekends, you can see it all.
|
||||
</p>
|
||||
<div class="form-check form-switch">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
role="switch"
|
||||
id="toggle-weekends"
|
||||
/>
|
||||
<label class="form-check-label" for="toggle-weekends"
|
||||
>Show weekends</label
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart col-md-9" id="holidays"></div>
|
||||
</div>
|
||||
|
||||
<div class="row my-5">
|
||||
<div class="col-md-3 px-5 py-1">
|
||||
<h3 class="text-center">...or <em>ignore</em> them</h3>
|
||||
<p>
|
||||
Remove time periods from your Gantt - they're now
|
||||
completely ignored.
|
||||
</p>
|
||||
<div class="form-check form-switch">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
role="switch"
|
||||
id="ignore-weekends"
|
||||
checked
|
||||
/>
|
||||
<label class="form-check-label" for="toggle-weekends"
|
||||
>Ignore weekends</label
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart col-md-9" id="ignore"></div>
|
||||
</div>
|
||||
<div class="row my-4">
|
||||
<div class="col-md-9 chart" id="styling"></div>
|
||||
<div class="col-md-3 px-4">
|
||||
<h3 class="text-center">Control the styles completely.</h3>
|
||||
<strong>Modify Grid</strong>
|
||||
<div class="input-group row">
|
||||
<label
|
||||
for="grid-height"
|
||||
class="form-label col-sm-5 col-form-label"
|
||||
><small>Grid Height:</small></label
|
||||
>
|
||||
<div class="col-sm-7">
|
||||
<input
|
||||
id="grid-height"
|
||||
class="form-range align-items-end"
|
||||
type="range"
|
||||
min="150"
|
||||
max="600"
|
||||
value="300"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group row">
|
||||
<label
|
||||
for="padding"
|
||||
class="form-label col-sm-5 col-form-label"
|
||||
><small>Padding:</small></label
|
||||
>
|
||||
<div class="col-sm-7">
|
||||
<input
|
||||
id="padding"
|
||||
class="form-range align-items-end"
|
||||
type="range"
|
||||
min="3"
|
||||
max="50"
|
||||
value="18"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group row">
|
||||
<label
|
||||
for="column-width"
|
||||
class="form-label col-sm-5 col-form-label"
|
||||
><small>Column Width:</small></label
|
||||
>
|
||||
<div class="col-sm-7">
|
||||
<input
|
||||
id="column-width"
|
||||
class="form-range align-items-end"
|
||||
type="range"
|
||||
min="30"
|
||||
max="70"
|
||||
value="30"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pt-3">
|
||||
<strong>Modify Bar</strong>
|
||||
</div>
|
||||
<div class="input-group row">
|
||||
<label
|
||||
for="bar-height"
|
||||
class="form-label col-sm-5 col-form-label"
|
||||
><small>Height:</small></label
|
||||
>
|
||||
<div class="col-sm-7">
|
||||
<input
|
||||
id="bar-height"
|
||||
class="form-range align-items-end"
|
||||
type="range"
|
||||
min="10"
|
||||
max="100"
|
||||
value="30"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group row">
|
||||
<label
|
||||
for="bar-radius"
|
||||
class="form-label col-sm-5 col-form-label"
|
||||
><small>Radius:</small></label
|
||||
>
|
||||
<div class="col-sm-7">
|
||||
<input
|
||||
id="bar-radius"
|
||||
class="form-range align-items-end"
|
||||
type="range"
|
||||
min="1"
|
||||
max="50"
|
||||
value="3"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group row">
|
||||
<label
|
||||
for="arrow-curve"
|
||||
class="form-label col-sm-5 col-form-label"
|
||||
><small>Arrow curving:</small></label
|
||||
>
|
||||
<div class="col-sm-7">
|
||||
<input
|
||||
id="arrow-curve"
|
||||
class="form-range align-items-end"
|
||||
type="range"
|
||||
min="1"
|
||||
max="50"
|
||||
value="3"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row my-4">
|
||||
<div class="col-md-3">
|
||||
<h2>Frappe Gantt - <em>for you</em>.</h2>
|
||||
<p>
|
||||
Insane levels of customizability - change anything,
|
||||
everything.
|
||||
</p>
|
||||
<div class="input-group">
|
||||
<label class="input-group-text">Snap By: </label>
|
||||
<input
|
||||
class="form-control"
|
||||
id="snap-at-qty"
|
||||
type="number"
|
||||
value="1"
|
||||
/>
|
||||
<select class="form-select" id="snap-at-scale">
|
||||
<option value="s">Second</option>
|
||||
<option value="min">Minute</option>
|
||||
<option value="h">Hour</option>
|
||||
<option value="d" selected>Day</option>
|
||||
<option value="m">Month</option>
|
||||
<option value="y">Year</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-check form-switch my-2">
|
||||
<label class="form-check-label" for="auto-move-label"
|
||||
>Toggle auto-moving label</label
|
||||
>
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
role="switch"
|
||||
id="auto-move-label"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-9 chart" id="advanced"></div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="module">
|
||||
const rawToday = new Date();
|
||||
const today =
|
||||
Date.UTC(
|
||||
rawToday.getFullYear(),
|
||||
rawToday.getMonth(),
|
||||
rawToday.getDate(),
|
||||
) +
|
||||
new Date().getTimezoneOffset() * 60000;
|
||||
|
||||
function random(begin = 10, end = 90, multiple = 10) {
|
||||
let k;
|
||||
do {
|
||||
k = Math.floor(Math.random() * 100);
|
||||
} while (k < begin || k > end || k % multiple !== 0);
|
||||
return k;
|
||||
}
|
||||
|
||||
const daysSince = (dx) => new Date(today + dx * 86400000);
|
||||
let tasks = [
|
||||
{
|
||||
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 tasksSpread = [
|
||||
{
|
||||
start: daysSince(-30),
|
||||
end: daysSince(-10),
|
||||
name: 'Redesign website',
|
||||
id: 'Task 0',
|
||||
progress: random(),
|
||||
},
|
||||
{
|
||||
start: daysSince(-15),
|
||||
duration: '21d',
|
||||
name: 'Write new content',
|
||||
id: 'Task 1',
|
||||
progress: random(),
|
||||
important: true,
|
||||
},
|
||||
{
|
||||
start: daysSince(10),
|
||||
duration: '14d',
|
||||
name: 'Review',
|
||||
id: 'Task 3',
|
||||
progress: random(),
|
||||
},
|
||||
{
|
||||
start: daysSince(-3),
|
||||
duration: '4d',
|
||||
name: 'Publish',
|
||||
id: 'Task 4',
|
||||
progress: random(),
|
||||
},
|
||||
];
|
||||
|
||||
const tasksDependencies = [
|
||||
{
|
||||
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(),
|
||||
dependencies: 'Task 0',
|
||||
important: true,
|
||||
},
|
||||
{
|
||||
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',
|
||||
custom_class: 'readonly',
|
||||
progress: random(),
|
||||
},
|
||||
];
|
||||
let tasksMany = [
|
||||
{
|
||||
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',
|
||||
progress: random(),
|
||||
},
|
||||
{
|
||||
start: daysSince(10),
|
||||
end: daysSince(12),
|
||||
name: 'Final client review',
|
||||
id: 'Task 7',
|
||||
progress: random(),
|
||||
important: true,
|
||||
},
|
||||
{
|
||||
start: daysSince(14),
|
||||
duration: '6d',
|
||||
name: 'Implement feedback',
|
||||
id: 'Task 8',
|
||||
progress: random(),
|
||||
},
|
||||
{
|
||||
start: daysSince(16),
|
||||
duration: '4d',
|
||||
name: 'Launch website',
|
||||
id: 'Task 9',
|
||||
progress: random(),
|
||||
important: true,
|
||||
},
|
||||
];
|
||||
|
||||
const HOLIDAYS = [
|
||||
{ name: 'Republic Day', date: '2024-01-26' },
|
||||
{ name: 'Maha Shivratri', date: '2024-02-23' },
|
||||
{ name: 'Holi', date: '2024-03-11' },
|
||||
{ name: 'Mahavir Jayanthi', date: '2024-04-07' },
|
||||
{ name: 'Good Friday', date: '2024-04-10' },
|
||||
{ name: 'May Day', date: '2024-05-01' },
|
||||
{ name: 'Buddha Purnima', date: '2024-05-08' },
|
||||
{ name: 'Krishna Janmastami', date: '2024-08-14' },
|
||||
{ name: 'Independence Day', date: '2024-08-15' },
|
||||
{ name: 'Ganesh Chaturthi', date: '2024-08-23' },
|
||||
{ name: 'Id-Ul-Fitr', date: '2024-09-21' },
|
||||
{ name: 'Vijaya Dashami', date: '2024-09-28' },
|
||||
{ name: 'Mahatma Gandhi Jayanti', date: '2024-10-02' },
|
||||
{ name: 'Diwali', date: '2024-10-17' },
|
||||
{ name: 'Guru Nanak Jayanthi', date: '2024-11-02' },
|
||||
{ name: 'Christmas', date: '2024-12-25' },
|
||||
];
|
||||
|
||||
const mutablity = new Gantt('#mutability', tasks);
|
||||
const sideheader = new Gantt('#sideheader', tasksSpread, {
|
||||
today_button: true,
|
||||
view_mode_select: true,
|
||||
holidays: null,
|
||||
});
|
||||
|
||||
const holidays = new Gantt('#holidays', tasksSpread, {
|
||||
holidays: {
|
||||
'#bfdbfe': [],
|
||||
'#a3e635': HOLIDAYS,
|
||||
},
|
||||
});
|
||||
|
||||
const ignore = new Gantt('#ignore', tasks, {
|
||||
ignore: ['weekend', ...HOLIDAYS.map((k) => k.date)],
|
||||
holidays: null,
|
||||
scroll_to: daysSince(-10),
|
||||
});
|
||||
|
||||
const styling = new Gantt('#styling', tasksMany, {
|
||||
holidays: null,
|
||||
scroll_to: daysSince(-10),
|
||||
});
|
||||
|
||||
const advanced = new Gantt('#advanced', tasksSpread, {
|
||||
holidays: null,
|
||||
view_mode_select: true,
|
||||
snap_at: '1d',
|
||||
auto_move_label: false,
|
||||
scroll_to: 'today',
|
||||
});
|
||||
|
||||
const UPDATES = [
|
||||
[
|
||||
mutablity,
|
||||
{
|
||||
'mutable-general': 'opp__readonly',
|
||||
'mutable-dates': 'opp__readonly_dates',
|
||||
'mutable-progress': 'opp__readonly_progress',
|
||||
},
|
||||
(id, val) => {
|
||||
if (id === 'mutable-general') {
|
||||
document.getElementById('mutable-dates').checked =
|
||||
!val;
|
||||
document.getElementById(
|
||||
'mutable-progress',
|
||||
).checked = !val;
|
||||
}
|
||||
},
|
||||
],
|
||||
[
|
||||
sideheader,
|
||||
{
|
||||
'toggle-today': 'today_button',
|
||||
'toggle-view-mode': 'view_mode_select',
|
||||
},
|
||||
],
|
||||
[
|
||||
holidays,
|
||||
{
|
||||
'toggle-weekends': [
|
||||
'holidays',
|
||||
{ '#a3e635': HOLIDAYS, '#bfdbfe': 'weekend' },
|
||||
{ '#a3e635': HOLIDAYS, '#bfdbfe': [] },
|
||||
],
|
||||
},
|
||||
],
|
||||
[
|
||||
ignore,
|
||||
{
|
||||
'ignore-weekends': ['ignore', ['weekend'], []],
|
||||
},
|
||||
],
|
||||
[
|
||||
styling,
|
||||
{
|
||||
'bar-radius': 'bar_corner_radius',
|
||||
'bar-height': 'bar_height',
|
||||
'arrow-curve': 'arrow_curve',
|
||||
'column-width': 'column_width',
|
||||
'grid-height': 'container_height',
|
||||
padding: 'padding',
|
||||
},
|
||||
],
|
||||
[
|
||||
advanced,
|
||||
{
|
||||
'auto-move-label': 'auto_move_label',
|
||||
'snap-at-qty': (val) => ({
|
||||
snap_at:
|
||||
val +
|
||||
document.getElementById('snap-at-scale').value,
|
||||
}),
|
||||
'snap-at-scale': (val) => ({
|
||||
snap_at:
|
||||
document.getElementById('snap-at-qty').value +
|
||||
val,
|
||||
}),
|
||||
},
|
||||
],
|
||||
];
|
||||
|
||||
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 {
|
||||
val = label[e.currentTarget.checked ? 1 : 2];
|
||||
label = label[0];
|
||||
}
|
||||
} else {
|
||||
val = +e.currentTarget.value;
|
||||
}
|
||||
|
||||
let store = chart.options.scroll_to;
|
||||
let scroll = chart.$container.scrollLeft;
|
||||
if (typeof label === 'function') {
|
||||
chart.update_options({
|
||||
...label(val),
|
||||
scroll_to: null,
|
||||
});
|
||||
} else {
|
||||
chart.update_options({
|
||||
[label]: val,
|
||||
scroll_to: null,
|
||||
});
|
||||
}
|
||||
|
||||
chart.options.scroll_to = store;
|
||||
chart.$container.scrollLeft = scroll;
|
||||
after && after(id, val, chart);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// const OPTIONS_UPDATE = [
|
||||
// // [
|
||||
// // styling,
|
||||
// // {
|
||||
// // 'bar-spacing': {
|
||||
// // bar_corner_radius: [
|
||||
// // 'config',
|
||||
// // () =>
|
||||
// // +document.getElementById('bar-radius')
|
||||
// // .value,
|
||||
// // ,
|
||||
// // ],
|
||||
// // bar_height: [
|
||||
// // 'config',
|
||||
// // () =>
|
||||
// // +document.getElementById('bar-height')
|
||||
// // .value,
|
||||
// // ],
|
||||
// // arrow_curve: [
|
||||
// // 'config',
|
||||
// // () =>
|
||||
// // +document.getElementById('arrow-curve')
|
||||
// // .value,
|
||||
// // ],
|
||||
// // },
|
||||
// // },
|
||||
// // ],
|
||||
// [
|
||||
// advanced,
|
||||
// {
|
||||
// 'snap-by': {
|
||||
// BEFORE: (chart) => chart.$container.scrollLeft,
|
||||
// snap_at: [
|
||||
// 'config',
|
||||
// (scale) => {
|
||||
// return (
|
||||
// document.getElementById('snap-at-qty')
|
||||
// .value +
|
||||
// document.getElementById('snap-at-scale')
|
||||
// .value
|
||||
// );
|
||||
// },
|
||||
// ],
|
||||
// view_mode: ['config', (k) => k],
|
||||
// scroll_to: ['config', (_) => false],
|
||||
// AFTER: (before, chart) =>
|
||||
// (chart.$container.scrollLeft = before),
|
||||
// },
|
||||
// 'auto-move-label': {
|
||||
// BEFORE: (chart) =>
|
||||
// chart.change_view_mode('Day') ||
|
||||
// chart.$container.scrollLeft,
|
||||
// view_mode: ['config', (k) => k],
|
||||
// auto_move_label: 'opp',
|
||||
// scroll_to: ['config', (_) => false],
|
||||
// AFTER: (before, chart) =>
|
||||
// (chart.$container.scrollLeft = before),
|
||||
// },
|
||||
// },
|
||||
// ],
|
||||
// ];
|
||||
|
||||
// for (let [chart, options] of OPTIONS_UPDATE) {
|
||||
// for (let id in options) {
|
||||
// let el = document.getElementById(id);
|
||||
// el.onclick = () => {
|
||||
// const before =
|
||||
// options[id].BEFORE && options[id].BEFORE(chart);
|
||||
// let newOptions = {};
|
||||
// for (let k in options[id]) {
|
||||
// if (k === 'AFTER' || k === 'BEFORE') continue;
|
||||
// if (options[id][k] === 'opp') {
|
||||
// newOptions[k] = !chart.options[k];
|
||||
// if (chart.options[k]) {
|
||||
// el.innerHTML = el.innerHTML.replace(
|
||||
// 'Hide',
|
||||
// 'Show',
|
||||
// );
|
||||
// } else {
|
||||
// el.innerHTML = el.innerHTML.replace(
|
||||
// 'Show',
|
||||
// 'Hide',
|
||||
// );
|
||||
// }
|
||||
// } else if (options[id][k][0] === 'config') {
|
||||
// newOptions[k] = options[id][k][1](
|
||||
// chart.options[k],
|
||||
// chart,
|
||||
// );
|
||||
// } else {
|
||||
// newOptions[k] = options[id][k];
|
||||
// }
|
||||
// }
|
||||
// chart.update_options(newOptions);
|
||||
// options[id].AFTER && options[id].AFTER(before, chart);
|
||||
// };
|
||||
// }
|
||||
// }
|
||||
</script>
|
||||
<script
|
||||
src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"
|
||||
integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL"
|
||||
crossorigin="anonymous"
|
||||
></script>
|
||||
</body>
|
||||
</html>
|
||||
var gantt_chart = new Gantt(".gantt-target", tasks, {
|
||||
bar_corner_radius: 5,
|
||||
on_click: function (task) {
|
||||
console.log(task);
|
||||
},
|
||||
on_date_change: function(task, start, end) {
|
||||
console.log(task, start, end);
|
||||
},
|
||||
on_progress_change: function(task, progress) {
|
||||
console.log(task, progress);
|
||||
},
|
||||
on_view_change: function(mode) {
|
||||
console.log(mode);
|
||||
}
|
||||
});
|
||||
console.log(gantt_chart);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2024 Frappe Technologies Pvt. Ltd.
|
||||
Copyright (c) 2016 Frappe Technologies Pvt. Ltd.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
|
||||
99
package.json
Normal file → Executable file
99
package.json
Normal file → Executable file
@ -1,59 +1,44 @@
|
||||
{
|
||||
"name": "frappe-gantt",
|
||||
"version": "1.0.3",
|
||||
"description": "A simple, modern, interactive gantt library for the web",
|
||||
"main": "src/index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build-dev": "vite build --watch",
|
||||
"build": "vite build",
|
||||
"lint": "eslint src/**/*.js",
|
||||
"prettier": "prettier --write \"{src/*,tests/*,rollup.config}.js\"",
|
||||
"prettier-check": "prettier --check \"{src/*,tests/*,rollup.config}.js\""
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/frappe/gantt.git"
|
||||
},
|
||||
"files": [
|
||||
"src",
|
||||
"dist",
|
||||
"README.md"
|
||||
],
|
||||
"exports": {
|
||||
".": {
|
||||
"require": "./dist/frappe-gantt.umd.js",
|
||||
"import": "./dist/frappe-gantt.es.js",
|
||||
"style": "./dist/frappe-gantt.css"
|
||||
}
|
||||
},
|
||||
"keywords": [
|
||||
"gantt",
|
||||
"svg",
|
||||
"simple gantt",
|
||||
"project timeline",
|
||||
"interactive gantt",
|
||||
"project management"
|
||||
],
|
||||
"author": "Faris Ansari",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/frappe/gantt/issues"
|
||||
},
|
||||
"homepage": "https://github.com/frappe/gantt",
|
||||
"devDependencies": {
|
||||
"eslint": "^9.15.0",
|
||||
"eslint-config-prettier": "^2.9.0",
|
||||
"eslint-plugin-prettier": "^2.6.0",
|
||||
"postcss-nesting": "^12.1.2",
|
||||
"prettier": "3.2.5",
|
||||
"vite": "^5.2.10"
|
||||
},
|
||||
"eslintIgnore": [
|
||||
"dist"
|
||||
],
|
||||
"sideEffects": [
|
||||
"*.css"
|
||||
]
|
||||
"name": "frappe-gantt",
|
||||
"version": "0.1.0",
|
||||
"description": "A simple, modern, interactive gantt library for the web",
|
||||
"main": "src/index.js",
|
||||
"scripts": {
|
||||
"build": "rollup -c",
|
||||
"dev": "rollup -c -w",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"prettier": "prettier es6 --write \"{src/*,tests/*,rollup.config}.js\""
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/frappe/gantt.git"
|
||||
},
|
||||
"keywords": [
|
||||
"gantt",
|
||||
"svg",
|
||||
"simple gantt",
|
||||
"project timeline",
|
||||
"interactive gantt",
|
||||
"project management"
|
||||
],
|
||||
"author": "Faris Ansari",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/frappe/gantt/issues"
|
||||
},
|
||||
"homepage": "https://github.com/frappe/gantt",
|
||||
"devDependencies": {
|
||||
"babel-preset-env": "^1.6.1",
|
||||
"deepmerge": "^2.0.1",
|
||||
"eslint": "^4.17.0",
|
||||
"eslint-config-prettier": "^2.9.0",
|
||||
"eslint-plugin-prettier": "^2.6.0",
|
||||
"jest": "^22.2.1",
|
||||
"prettier": "1.10.2",
|
||||
"rollup": "^0.55.3",
|
||||
"rollup-plugin-sass": "^0.5.3",
|
||||
"rollup-plugin-uglify": "^3.0.0"
|
||||
},
|
||||
"eslintIgnore": ["dist"]
|
||||
}
|
||||
|
||||
1291
pnpm-lock.yaml
generated
1291
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -1,4 +0,0 @@
|
||||
/* eslint-disable */
|
||||
module.exports = {
|
||||
plugins: [require('postcss-nesting')],
|
||||
};
|
||||
25
rollup.config.js
Normal file
25
rollup.config.js
Normal file
@ -0,0 +1,25 @@
|
||||
import sass from 'rollup-plugin-sass';
|
||||
import uglify from 'rollup-plugin-uglify';
|
||||
import merge from 'deepmerge';
|
||||
|
||||
const dev = {
|
||||
input: 'src/index.js',
|
||||
output: {
|
||||
name: 'Gantt',
|
||||
file: 'dist/frappe-gantt.js',
|
||||
format: 'iife'
|
||||
},
|
||||
plugins: [
|
||||
sass({
|
||||
output: 'dist/frappe-gantt.css'
|
||||
})
|
||||
]
|
||||
};
|
||||
const prod = merge(dev, {
|
||||
output: {
|
||||
file: 'dist/frappe-gantt.min.js'
|
||||
},
|
||||
plugins: [uglify()]
|
||||
});
|
||||
|
||||
export default [dev, prod];
|
||||
@ -21,70 +21,63 @@ export default class Arrow {
|
||||
while (condition()) {
|
||||
start_x -= 10;
|
||||
}
|
||||
start_x -= 10;
|
||||
|
||||
let start_y =
|
||||
this.gantt.config.header_height +
|
||||
const start_y =
|
||||
this.gantt.options.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 / 2;
|
||||
this.gantt.options.padding;
|
||||
|
||||
let end_x = this.to_task.$bar.getX() - 13;
|
||||
let end_y =
|
||||
this.gantt.config.header_height +
|
||||
const end_x = this.to_task.$bar.getX() - this.gantt.options.padding / 2;
|
||||
const end_y =
|
||||
this.gantt.options.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 / 2;
|
||||
this.gantt.options.padding;
|
||||
|
||||
const from_is_below_to =
|
||||
this.from_task.task._index > this.to_task.task._index;
|
||||
|
||||
let curve = this.gantt.options.arrow_curve;
|
||||
const curve = this.gantt.options.arrow_curve;
|
||||
const clockwise = from_is_below_to ? 1 : 0;
|
||||
let curve_y = from_is_below_to ? -curve : curve;
|
||||
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`;
|
||||
|
||||
if (
|
||||
this.to_task.$bar.getX() <=
|
||||
this.to_task.$bar.getX() <
|
||||
this.from_task.$bar.getX() + this.gantt.options.padding
|
||||
) {
|
||||
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_1 = this.gantt.options.padding / 2 - 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`;
|
||||
}
|
||||
}
|
||||
|
||||
@ -92,7 +85,7 @@ export default class Arrow {
|
||||
this.element = createSVG('path', {
|
||||
d: this.path,
|
||||
'data-from': this.from_task.task.id,
|
||||
'data-to': this.to_task.task.id,
|
||||
'data-to': this.to_task.task.id
|
||||
});
|
||||
}
|
||||
|
||||
406
src/Bar.js
Normal file
406
src/Bar.js
Normal file
@ -0,0 +1,406 @@
|
||||
import date_utils from './date_utils';
|
||||
import { $, createSVG, animateSVG } from './svg_utils';
|
||||
|
||||
export default class Bar {
|
||||
constructor(gantt, task) {
|
||||
this.set_defaults(gantt, task);
|
||||
this.prepare();
|
||||
this.draw();
|
||||
this.bind();
|
||||
}
|
||||
|
||||
set_defaults(gantt, task) {
|
||||
this.action_completed = false;
|
||||
this.gantt = gantt;
|
||||
this.task = task;
|
||||
}
|
||||
|
||||
prepare() {
|
||||
this.prepare_values();
|
||||
this.prepare_helpers();
|
||||
}
|
||||
|
||||
prepare_values() {
|
||||
this.invalid = this.task.invalid;
|
||||
this.height = this.gantt.options.bar_height;
|
||||
this.x = this.compute_x();
|
||||
this.y = this.compute_y();
|
||||
this.corner_radius = this.gantt.options.bar_corner_radius;
|
||||
this.duration =
|
||||
(date_utils.diff(this.task._end, this.task._start, 'hour') + 24) /
|
||||
this.gantt.options.step;
|
||||
this.width = this.gantt.options.column_width * this.duration;
|
||||
this.progress_width =
|
||||
this.gantt.options.column_width *
|
||||
this.duration *
|
||||
(this.task.progress / 100) || 0;
|
||||
this.group = createSVG('g', {
|
||||
class: 'bar-wrapper ' + (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_helpers() {
|
||||
SVGElement.prototype.getX = function() {
|
||||
return +this.getAttribute('x');
|
||||
};
|
||||
SVGElement.prototype.getY = function() {
|
||||
return +this.getAttribute('y');
|
||||
};
|
||||
SVGElement.prototype.getWidth = function() {
|
||||
return +this.getAttribute('width');
|
||||
};
|
||||
SVGElement.prototype.getHeight = function() {
|
||||
return +this.getAttribute('height');
|
||||
};
|
||||
SVGElement.prototype.getEndX = function() {
|
||||
return this.getX() + this.getWidth();
|
||||
};
|
||||
}
|
||||
|
||||
draw() {
|
||||
this.draw_bar();
|
||||
this.draw_progress_bar();
|
||||
this.draw_label();
|
||||
this.draw_resize_handles();
|
||||
}
|
||||
|
||||
draw_bar() {
|
||||
this.$bar = createSVG('rect', {
|
||||
x: this.x,
|
||||
y: this.y,
|
||||
width: this.width,
|
||||
height: this.height,
|
||||
rx: this.corner_radius,
|
||||
ry: this.corner_radius,
|
||||
class: 'bar',
|
||||
append_to: this.bar_group
|
||||
});
|
||||
|
||||
animateSVG(this.$bar, 'width', 0, this.width);
|
||||
|
||||
if (this.invalid) {
|
||||
this.$bar.classList.add('bar-invalid');
|
||||
}
|
||||
}
|
||||
|
||||
draw_progress_bar() {
|
||||
if (this.invalid) return;
|
||||
this.$bar_progress = createSVG('rect', {
|
||||
x: this.x,
|
||||
y: this.y,
|
||||
width: this.progress_width,
|
||||
height: this.height,
|
||||
rx: this.corner_radius,
|
||||
ry: this.corner_radius,
|
||||
class: 'bar-progress',
|
||||
append_to: this.bar_group
|
||||
});
|
||||
|
||||
animateSVG(this.$bar_progress, 'width', 0, this.progress_width);
|
||||
}
|
||||
|
||||
draw_label() {
|
||||
createSVG('text', {
|
||||
x: this.x + this.width / 2,
|
||||
y: this.y + this.height / 2,
|
||||
innerHTML: this.task.name,
|
||||
class: 'bar-label',
|
||||
append_to: this.bar_group
|
||||
});
|
||||
// labels get BBox in the next tick
|
||||
requestAnimationFrame(() => this.update_label_position());
|
||||
}
|
||||
|
||||
draw_resize_handles() {
|
||||
if (this.invalid) return;
|
||||
|
||||
const bar = this.$bar;
|
||||
const handle_width = 8;
|
||||
|
||||
createSVG('rect', {
|
||||
x: bar.getX() + bar.getWidth() - 9,
|
||||
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
|
||||
});
|
||||
|
||||
createSVG('rect', {
|
||||
x: bar.getX() + 1,
|
||||
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
|
||||
});
|
||||
|
||||
if (this.task.progress && this.task.progress < 100) {
|
||||
this.$handle_progress = createSVG('polygon', {
|
||||
points: this.get_progress_polygon_points().join(','),
|
||||
class: 'handle progress',
|
||||
append_to: this.handle_group
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
get_progress_polygon_points() {
|
||||
const bar_progress = this.$bar_progress;
|
||||
return [
|
||||
bar_progress.getEndX() - 5,
|
||||
bar_progress.getY() + bar_progress.getHeight(),
|
||||
bar_progress.getEndX() + 5,
|
||||
bar_progress.getY() + bar_progress.getHeight(),
|
||||
bar_progress.getEndX(),
|
||||
bar_progress.getY() + bar_progress.getHeight() - 8.66
|
||||
];
|
||||
}
|
||||
|
||||
bind() {
|
||||
if (this.invalid) return;
|
||||
this.setup_click_event();
|
||||
}
|
||||
|
||||
setup_click_event() {
|
||||
$.on(this.group, 'click', e => {
|
||||
if (this.action_completed) {
|
||||
// just finished a move action, wait for a few seconds
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.group.classList.contains('active')) {
|
||||
this.gantt.trigger_event('click', [this.task]);
|
||||
}
|
||||
this.gantt.unselect_all();
|
||||
this.group.classList.toggle('active');
|
||||
|
||||
this.show_popup();
|
||||
});
|
||||
}
|
||||
|
||||
show_popup() {
|
||||
const start_date = date_utils.format(this.task._start, 'MMM D');
|
||||
const end_date = date_utils.format(this.task._end, 'MMM D');
|
||||
const subtitle = start_date + ' - ' + end_date;
|
||||
|
||||
this.gantt.show_popup({
|
||||
target_element: this.$bar,
|
||||
title: this.task.name,
|
||||
subtitle: subtitle
|
||||
});
|
||||
}
|
||||
|
||||
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((prev, curr) => {
|
||||
return x >= curr;
|
||||
}, x);
|
||||
if (!valid_x) {
|
||||
width = null;
|
||||
return;
|
||||
}
|
||||
this.update_attr(bar, 'x', x);
|
||||
}
|
||||
if (width && width >= this.gantt.options.column_width) {
|
||||
this.update_attr(bar, 'width', width);
|
||||
}
|
||||
this.update_label_position();
|
||||
this.update_handle_position();
|
||||
this.update_progressbar_position();
|
||||
this.update_arrow_position();
|
||||
// this.update_details_position();
|
||||
}
|
||||
|
||||
date_changed() {
|
||||
const { new_start_date, new_end_date } = this.compute_start_end_date();
|
||||
this.task._start = new_start_date;
|
||||
this.task._end = new_end_date;
|
||||
|
||||
this.gantt.trigger_event('date_change', [
|
||||
this.task,
|
||||
new_start_date,
|
||||
new_end_date
|
||||
]);
|
||||
}
|
||||
|
||||
progress_changed() {
|
||||
const new_progress = this.compute_progress();
|
||||
this.task.progress = new_progress;
|
||||
this.gantt.trigger_event('progress_change', [this.task, new_progress]);
|
||||
}
|
||||
|
||||
set_action_completed() {
|
||||
this.action_completed = true;
|
||||
setTimeout(() => (this.action_completed = false), 2000);
|
||||
}
|
||||
|
||||
compute_start_end_date() {
|
||||
const bar = this.$bar;
|
||||
const x_in_units = bar.getX() / this.gantt.options.column_width;
|
||||
const new_start_date = date_utils.add(
|
||||
this.gantt.gantt_start,
|
||||
x_in_units * this.gantt.options.step,
|
||||
'hours'
|
||||
);
|
||||
const width_in_units = bar.getWidth() / this.gantt.options.column_width;
|
||||
const new_end_date = date_utils.add(
|
||||
new_start_date,
|
||||
width_in_units * this.gantt.options.step,
|
||||
'hours'
|
||||
);
|
||||
// lets say duration is 2 days
|
||||
// start_date = May 24 00:00:00
|
||||
// end_date = May 24 + 2 days = May 26 (incorrect)
|
||||
// so subtract 1 second so that
|
||||
// end_date = May 25 23:59:59
|
||||
date_utils.add(new_end_date, -1, 'second');
|
||||
return { new_start_date, new_end_date };
|
||||
}
|
||||
|
||||
compute_progress() {
|
||||
const progress =
|
||||
this.$bar_progress.getWidth() / this.$bar.getWidth() * 100;
|
||||
return parseInt(progress, 10);
|
||||
}
|
||||
|
||||
compute_x() {
|
||||
let x =
|
||||
date_utils.diff(this.task._start, this.gantt.gantt_start, 'hour') /
|
||||
this.gantt.options.step *
|
||||
this.gantt.options.column_width;
|
||||
|
||||
if (this.gantt.view_is('Month')) {
|
||||
x =
|
||||
date_utils.diff(
|
||||
this.task._start,
|
||||
this.gantt.gantt_start,
|
||||
'day'
|
||||
) *
|
||||
this.gantt.options.column_width /
|
||||
30;
|
||||
}
|
||||
return x;
|
||||
}
|
||||
|
||||
compute_y() {
|
||||
return (
|
||||
this.gantt.options.header_height +
|
||||
this.gantt.options.padding +
|
||||
this.task._index * (this.height + this.gantt.options.padding)
|
||||
);
|
||||
}
|
||||
|
||||
get_snap_position(dx) {
|
||||
let odx = dx,
|
||||
rem,
|
||||
position;
|
||||
|
||||
if (this.gantt.view_is('Week')) {
|
||||
rem = dx % (this.gantt.options.column_width / 7);
|
||||
position =
|
||||
odx -
|
||||
rem +
|
||||
(rem < this.gantt.options.column_width / 14
|
||||
? 0
|
||||
: this.gantt.options.column_width / 7);
|
||||
} else if (this.gantt.view_is('Month')) {
|
||||
rem = dx % (this.gantt.options.column_width / 30);
|
||||
position =
|
||||
odx -
|
||||
rem +
|
||||
(rem < this.gantt.options.column_width / 60
|
||||
? 0
|
||||
: this.gantt.options.column_width / 30);
|
||||
} else {
|
||||
rem = dx % this.gantt.options.column_width;
|
||||
position =
|
||||
odx -
|
||||
rem +
|
||||
(rem < this.gantt.options.column_width / 2
|
||||
? 0
|
||||
: this.gantt.options.column_width);
|
||||
}
|
||||
return position;
|
||||
}
|
||||
|
||||
update_attr(element, attr, value) {
|
||||
value = +value;
|
||||
if (!isNaN(value)) {
|
||||
element.setAttribute(attr, value);
|
||||
}
|
||||
return element;
|
||||
}
|
||||
|
||||
update_progressbar_position() {
|
||||
this.$bar_progress.setAttribute('x', this.$bar.getX());
|
||||
this.$bar_progress.setAttribute(
|
||||
'width',
|
||||
this.$bar.getWidth() * (this.task.progress / 100)
|
||||
);
|
||||
}
|
||||
|
||||
update_label_position() {
|
||||
const bar = this.$bar,
|
||||
label = this.group.querySelector('.bar-label');
|
||||
|
||||
if (label.getBBox().width > bar.getWidth()) {
|
||||
label.classList.add('big');
|
||||
label.setAttribute('x', bar.getX() + bar.getWidth() + 5);
|
||||
} else {
|
||||
label.classList.remove('big');
|
||||
label.setAttribute('x', bar.getX() + bar.getWidth() / 2);
|
||||
}
|
||||
}
|
||||
|
||||
update_handle_position() {
|
||||
const bar = this.$bar;
|
||||
this.handle_group
|
||||
.querySelector('.handle.left')
|
||||
.setAttribute('x', bar.getX() + 1);
|
||||
this.handle_group
|
||||
.querySelector('.handle.right')
|
||||
.setAttribute('x', bar.getEndX() - 9);
|
||||
const handle = this.group.querySelector('.handle.progress');
|
||||
handle &&
|
||||
handle.setAttribute('points', this.get_progress_polygon_points());
|
||||
}
|
||||
|
||||
update_arrow_position() {
|
||||
this.arrows = this.arrows || [];
|
||||
for (let arrow of this.arrows) {
|
||||
arrow.update();
|
||||
}
|
||||
}
|
||||
|
||||
update_details_position() {
|
||||
const { x, y } = get_details_position();
|
||||
this.details_box && this.details_box.transform(`t${x},${y}`);
|
||||
}
|
||||
}
|
||||
|
||||
function isFunction(functionToCheck) {
|
||||
var getType = {};
|
||||
return (
|
||||
functionToCheck &&
|
||||
getType.toString.call(functionToCheck) === '[object Function]'
|
||||
);
|
||||
}
|
||||
737
src/bar.js
737
src/bar.js
@ -1,737 +0,0 @@
|
||||
import date_utils from './date_utils';
|
||||
import { $, createSVG, animateSVG } from './svg_utils';
|
||||
|
||||
export default class Bar {
|
||||
constructor(gantt, task) {
|
||||
this.set_defaults(gantt, task);
|
||||
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();
|
||||
}
|
||||
|
||||
set_defaults(gantt, task) {
|
||||
this.action_completed = false;
|
||||
this.gantt = gantt;
|
||||
this.task = task;
|
||||
this.name = this.name || '';
|
||||
}
|
||||
|
||||
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() {
|
||||
this.invalid = this.task.invalid;
|
||||
this.height = this.gantt.options.bar_height;
|
||||
this.image_size = this.height - 5;
|
||||
this.task._start = new Date(this.task.start);
|
||||
this.task._end = new Date(this.task.end);
|
||||
this.compute_x();
|
||||
this.compute_y();
|
||||
this.compute_duration();
|
||||
this.corner_radius = this.gantt.options.bar_corner_radius;
|
||||
this.width = this.gantt.config.column_width * this.duration;
|
||||
if (!this.task.progress || this.task.progress < 0)
|
||||
this.task.progress = 0;
|
||||
if (this.task.progress > 100) this.task.progress = 100;
|
||||
}
|
||||
|
||||
prepare_helpers() {
|
||||
SVGElement.prototype.getX = function () {
|
||||
return +this.getAttribute('x');
|
||||
};
|
||||
SVGElement.prototype.getY = function () {
|
||||
return +this.getAttribute('y');
|
||||
};
|
||||
SVGElement.prototype.getWidth = function () {
|
||||
return +this.getAttribute('width');
|
||||
};
|
||||
SVGElement.prototype.getHeight = function () {
|
||||
return +this.getAttribute('height');
|
||||
};
|
||||
SVGElement.prototype.getEndX = function () {
|
||||
return this.getX() + this.getWidth();
|
||||
};
|
||||
}
|
||||
|
||||
prepare_expected_progress_values() {
|
||||
this.compute_expected_progress();
|
||||
this.expected_progress_width =
|
||||
this.gantt.options.column_width *
|
||||
this.duration *
|
||||
(this.expected_progress / 100) || 0;
|
||||
}
|
||||
|
||||
draw() {
|
||||
this.draw_bar();
|
||||
this.draw_progress_bar();
|
||||
if (this.gantt.options.show_expected_progress) {
|
||||
this.prepare_expected_progress_values();
|
||||
this.draw_expected_progress_bar();
|
||||
}
|
||||
this.draw_label();
|
||||
this.draw_resize_handles();
|
||||
|
||||
if (this.task.thumbnail) {
|
||||
this.draw_thumbnail();
|
||||
}
|
||||
}
|
||||
|
||||
draw_bar() {
|
||||
this.$bar = createSVG('rect', {
|
||||
x: this.x,
|
||||
y: this.y,
|
||||
width: this.width,
|
||||
height: this.height,
|
||||
rx: this.corner_radius,
|
||||
ry: this.corner_radius,
|
||||
class: 'bar',
|
||||
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) {
|
||||
this.$bar.classList.add('bar-invalid');
|
||||
}
|
||||
}
|
||||
|
||||
draw_expected_progress_bar() {
|
||||
if (this.invalid) return;
|
||||
this.$expected_bar_progress = createSVG('rect', {
|
||||
x: this.x,
|
||||
y: this.y,
|
||||
width: this.expected_progress_width,
|
||||
height: this.height,
|
||||
rx: this.corner_radius,
|
||||
ry: this.corner_radius,
|
||||
class: 'bar-expected-progress',
|
||||
append_to: this.bar_group,
|
||||
});
|
||||
|
||||
animateSVG(
|
||||
this.$expected_bar_progress,
|
||||
'width',
|
||||
0,
|
||||
this.expected_progress_width,
|
||||
);
|
||||
}
|
||||
|
||||
draw_progress_bar() {
|
||||
if (this.invalid) return;
|
||||
this.progress_width = this.calculate_progress_width();
|
||||
let r = this.corner_radius;
|
||||
if (!/^((?!chrome|android).)*safari/i.test(navigator.userAgent))
|
||||
r = this.corner_radius + 2;
|
||||
this.$bar_progress = createSVG('rect', {
|
||||
x: this.x,
|
||||
y: this.y,
|
||||
width: this.progress_width,
|
||||
height: this.height,
|
||||
rx: r,
|
||||
ry: r,
|
||||
class: 'bar-progress',
|
||||
append_to: this.bar_group,
|
||||
});
|
||||
if (this.task.color_progress)
|
||||
this.$bar_progress.style.fill = this.task.color_progress;
|
||||
const x =
|
||||
(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 = 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(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;
|
||||
|
||||
if (this.task.thumbnail) {
|
||||
x_coord = this.x + this.image_size + 5;
|
||||
}
|
||||
|
||||
createSVG('text', {
|
||||
x: x_coord,
|
||||
y: this.y + this.height / 2,
|
||||
innerHTML: this.task.name,
|
||||
class: 'bar-label',
|
||||
append_to: this.bar_group,
|
||||
});
|
||||
// labels get BBox in the next tick
|
||||
requestAnimationFrame(() => this.update_label_position());
|
||||
}
|
||||
|
||||
draw_thumbnail() {
|
||||
let x_offset = 10,
|
||||
y_offset = 2;
|
||||
let defs, clipPath;
|
||||
|
||||
defs = createSVG('defs', {
|
||||
append_to: this.bar_group,
|
||||
});
|
||||
|
||||
createSVG('rect', {
|
||||
id: 'rect_' + this.task.id,
|
||||
x: this.x + x_offset,
|
||||
y: this.y + y_offset,
|
||||
width: this.image_size,
|
||||
height: this.image_size,
|
||||
rx: '15',
|
||||
class: 'img_mask',
|
||||
append_to: defs,
|
||||
});
|
||||
|
||||
clipPath = createSVG('clipPath', {
|
||||
id: 'clip_' + this.task.id,
|
||||
append_to: defs,
|
||||
});
|
||||
|
||||
createSVG('use', {
|
||||
href: '#rect_' + this.task.id,
|
||||
append_to: clipPath,
|
||||
});
|
||||
|
||||
createSVG('image', {
|
||||
x: this.x + x_offset,
|
||||
y: this.y + y_offset,
|
||||
width: this.image_size,
|
||||
height: this.image_size,
|
||||
class: 'bar-img',
|
||||
href: this.task.thumbnail,
|
||||
clipPath: 'clip_' + this.task.id,
|
||||
append_to: this.bar_group,
|
||||
});
|
||||
}
|
||||
|
||||
draw_resize_handles() {
|
||||
if (this.invalid || this.gantt.options.readonly) return;
|
||||
|
||||
const bar = this.$bar;
|
||||
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,
|
||||
}),
|
||||
);
|
||||
|
||||
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.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: 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'));
|
||||
}
|
||||
}
|
||||
|
||||
bind() {
|
||||
if (this.invalid) return;
|
||||
this.setup_click_event();
|
||||
}
|
||||
|
||||
setup_click_event() {
|
||||
let task_id = this.task.id;
|
||||
$.on(this.group, 'mouseover', (e) => {
|
||||
this.gantt.trigger_event('hover', [
|
||||
this.task,
|
||||
e.screenX,
|
||||
e.screenY,
|
||||
e,
|
||||
]);
|
||||
});
|
||||
|
||||
if (this.gantt.options.popup_on === 'click') {
|
||||
$.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;
|
||||
}
|
||||
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]);
|
||||
});
|
||||
|
||||
$.on(this.group, 'dblclick', (e) => {
|
||||
if (this.action_completed) {
|
||||
// just finished a move action, wait for a few seconds
|
||||
return;
|
||||
}
|
||||
this.group.classList.remove('active');
|
||||
if (this.gantt.popup)
|
||||
this.gantt.popup.parent.classList.remove('hide');
|
||||
|
||||
this.gantt.trigger_event('double_click', [this.task]);
|
||||
});
|
||||
let tapedTwice = false;
|
||||
$.on(this.group, 'touchstart', (e) => {
|
||||
if (!tapedTwice) {
|
||||
tapedTwice = true;
|
||||
setTimeout(function () { tapedTwice = false; }, 300);
|
||||
return false;
|
||||
}
|
||||
e.preventDefault();
|
||||
//action on double tap goes below
|
||||
|
||||
|
||||
if (this.action_completed) {
|
||||
// just finished a move action, wait for a few seconds
|
||||
return;
|
||||
}
|
||||
this.group.classList.remove('active');
|
||||
if (this.gantt.popup)
|
||||
this.gantt.popup.parent.classList.remove('hide');
|
||||
|
||||
this.gantt.trigger_event('double_click', [this.task]);
|
||||
});
|
||||
}
|
||||
|
||||
update_bar_position({ x = null, width = null }) {
|
||||
const bar = this.$bar;
|
||||
|
||||
if (x) {
|
||||
const xs = this.task.dependencies.map((dep) => {
|
||||
return this.gantt.get_bar(dep).$bar.getX();
|
||||
});
|
||||
const valid_x = xs.reduce((prev, curr) => {
|
||||
return prev && x >= curr;
|
||||
}, true);
|
||||
if (!valid_x) return;
|
||||
this.update_attr(bar, 'x', x);
|
||||
this.x = x;
|
||||
this.$date_highlight.style.left = x + 'px';
|
||||
}
|
||||
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.update_expected_progressbar_position();
|
||||
}
|
||||
|
||||
this.update_progressbar_position();
|
||||
this.update_arrow_position();
|
||||
}
|
||||
|
||||
update_label_position_on_horizontal_scroll({ x, sx }) {
|
||||
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') || '';
|
||||
|
||||
let barWidthLimit = this.$bar.getX() + this.$bar.getWidth();
|
||||
let newLabelX = label.getX() + x;
|
||||
let newImgX = (img && img.getX() + x) || 0;
|
||||
let imgWidth = (img && img.getBBox().width + 7) || 7;
|
||||
let labelEndX = newLabelX + label.getBBox().width + 7;
|
||||
let viewportCentral = sx + container.clientWidth / 2;
|
||||
|
||||
if (label.classList.contains('big')) return;
|
||||
|
||||
if (labelEndX < barWidthLimit && x > 0 && labelEndX < viewportCentral) {
|
||||
label.setAttribute('x', newLabelX);
|
||||
if (img) {
|
||||
img.setAttribute('x', newImgX);
|
||||
img_mask.setAttribute('x', newImgX);
|
||||
}
|
||||
} else if (
|
||||
newLabelX - imgWidth > this.$bar.getX() &&
|
||||
x < 0 &&
|
||||
labelEndX > viewportCentral
|
||||
) {
|
||||
label.setAttribute('x', newLabelX);
|
||||
if (img) {
|
||||
img.setAttribute('x', newImgX);
|
||||
img_mask.setAttribute('x', newImgX);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
date_changed() {
|
||||
let changed = false;
|
||||
const { new_start_date, new_end_date } = this.compute_start_end_date();
|
||||
if (Number(this.task._start) !== Number(new_start_date)) {
|
||||
changed = true;
|
||||
this.task._start = new_start_date;
|
||||
}
|
||||
|
||||
if (Number(this.task._end) !== Number(new_end_date)) {
|
||||
changed = true;
|
||||
this.task._end = new_end_date;
|
||||
}
|
||||
|
||||
if (!changed) return;
|
||||
|
||||
this.gantt.trigger_event('date_change', [
|
||||
this.task,
|
||||
new_start_date,
|
||||
date_utils.add(new_end_date, -1, 'second'),
|
||||
]);
|
||||
}
|
||||
|
||||
progress_changed() {
|
||||
this.task.progress = this.compute_progress();
|
||||
this.gantt.trigger_event('progress_change', [
|
||||
this.task,
|
||||
this.task.progress,
|
||||
]);
|
||||
}
|
||||
|
||||
set_action_completed() {
|
||||
this.action_completed = true;
|
||||
setTimeout(() => (this.action_completed = false), 1000);
|
||||
}
|
||||
|
||||
compute_start_end_date() {
|
||||
const bar = this.$bar;
|
||||
const x_in_units = bar.getX() / this.gantt.config.column_width;
|
||||
let new_start_date = date_utils.add(
|
||||
this.gantt.gantt_start,
|
||||
x_in_units * this.gantt.config.step,
|
||||
this.gantt.config.unit,
|
||||
);
|
||||
|
||||
const width_in_units = bar.getWidth() / this.gantt.config.column_width;
|
||||
const new_end_date = date_utils.add(
|
||||
new_start_date,
|
||||
width_in_units * this.gantt.config.step,
|
||||
this.gantt.config.unit,
|
||||
);
|
||||
|
||||
return { new_start_date, new_end_date };
|
||||
}
|
||||
|
||||
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.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() {
|
||||
this.expected_progress =
|
||||
date_utils.diff(date_utils.today(), this.task._start, 'hour') /
|
||||
this.gantt.config.step;
|
||||
this.expected_progress =
|
||||
((this.expected_progress < this.duration
|
||||
? this.expected_progress
|
||||
: this.duration) *
|
||||
100) /
|
||||
this.duration;
|
||||
}
|
||||
|
||||
compute_x() {
|
||||
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,
|
||||
we count the month-difference, multiply it by 30 for a "pseudo-month"
|
||||
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;
|
||||
|
||||
// x = (diff * column_width) / 30;
|
||||
// }
|
||||
|
||||
this.x = x;
|
||||
}
|
||||
|
||||
compute_y() {
|
||||
this.y =
|
||||
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.convert_scales(
|
||||
duration_in_days + 'd',
|
||||
this.gantt.config.unit,
|
||||
) / this.gantt.config.step;
|
||||
|
||||
this.actual_duration_raw =
|
||||
date_utils.convert_scales(
|
||||
actual_duration_in_days + 'd',
|
||||
this.gantt.config.unit,
|
||||
) / this.gantt.config.step;
|
||||
|
||||
this.ignored_duration_raw = this.duration - this.actual_duration_raw;
|
||||
}
|
||||
|
||||
update_attr(element, attr, value) {
|
||||
value = +value;
|
||||
if (!isNaN(value)) {
|
||||
element.setAttribute(attr, value);
|
||||
}
|
||||
return element;
|
||||
}
|
||||
|
||||
update_expected_progressbar_position() {
|
||||
if (this.invalid) return;
|
||||
this.$expected_bar_progress.setAttribute('x', this.$bar.getX());
|
||||
this.compute_expected_progress();
|
||||
this.$expected_bar_progress.setAttribute(
|
||||
'width',
|
||||
this.gantt.config.column_width *
|
||||
this.actual_duration_raw *
|
||||
(this.expected_progress / 100) || 0,
|
||||
);
|
||||
}
|
||||
|
||||
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.calculate_progress_width(),
|
||||
);
|
||||
}
|
||||
|
||||
update_label_position() {
|
||||
const img_mask = this.bar_group.querySelector('.img_mask') || '';
|
||||
const bar = this.$bar,
|
||||
label = this.group.querySelector('.bar-label'),
|
||||
img = this.group.querySelector('.bar-img');
|
||||
|
||||
let padding = 5;
|
||||
let x_offset_label_img = this.image_size + 10;
|
||||
const labelWidth = label.getBBox().width;
|
||||
const barWidth = bar.getWidth();
|
||||
if (labelWidth > barWidth) {
|
||||
label.classList.add('big');
|
||||
if (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.getEndX() + padding);
|
||||
}
|
||||
} else {
|
||||
label.classList.remove('big');
|
||||
if (img) {
|
||||
img.setAttribute('x', bar.getX() + padding);
|
||||
img_mask.setAttribute('x', bar.getX() + padding);
|
||||
label.setAttribute(
|
||||
'x',
|
||||
bar.getX() + barWidth / 2 + x_offset_label_img,
|
||||
);
|
||||
} else {
|
||||
label.setAttribute(
|
||||
'x',
|
||||
bar.getX() + barWidth / 2 - labelWidth / 2,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
update_handle_position() {
|
||||
if (this.invalid || this.gantt.options.readonly) return;
|
||||
const bar = this.$bar;
|
||||
this.handle_group
|
||||
.querySelector('.handle.left')
|
||||
.setAttribute('x', bar.getX());
|
||||
this.handle_group
|
||||
.querySelector('.handle.right')
|
||||
.setAttribute('x', bar.getEndX());
|
||||
const handle = this.group.querySelector('.handle.progress');
|
||||
handle && handle.setAttribute('cx', this.$bar_progress.getEndX());
|
||||
}
|
||||
|
||||
update_arrow_position() {
|
||||
this.arrows = this.arrows || [];
|
||||
for (let arrow of this.arrows) {
|
||||
arrow.update();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -6,52 +6,44 @@ const MINUTE = 'minute';
|
||||
const SECOND = 'second';
|
||||
const MILLISECOND = 'millisecond';
|
||||
|
||||
const month_names = [
|
||||
'January',
|
||||
'February',
|
||||
'March',
|
||||
'April',
|
||||
'May',
|
||||
'June',
|
||||
'July',
|
||||
'August',
|
||||
'September',
|
||||
'October',
|
||||
'November',
|
||||
'December'
|
||||
];
|
||||
|
||||
export default {
|
||||
parse_duration(duration) {
|
||||
const regex = /([0-9]+)(y|m|d|h|min|s|ms)/gm;
|
||||
const matches = regex.exec(duration);
|
||||
if (matches !== null) {
|
||||
if (matches[2] === 'y') {
|
||||
return { duration: parseInt(matches[1]), scale: `year` };
|
||||
} else if (matches[2] === 'm') {
|
||||
return { duration: parseInt(matches[1]), scale: `month` };
|
||||
} else if (matches[2] === 'd') {
|
||||
return { duration: parseInt(matches[1]), scale: `day` };
|
||||
} else if (matches[2] === 'h') {
|
||||
return { duration: parseInt(matches[1]), scale: `hour` };
|
||||
} else if (matches[2] === 'min') {
|
||||
return { duration: parseInt(matches[1]), scale: `minute` };
|
||||
} else if (matches[2] === 's') {
|
||||
return { duration: parseInt(matches[1]), scale: `second` };
|
||||
} else if (matches[2] === 'ms') {
|
||||
return { duration: parseInt(matches[1]), scale: `millisecond` };
|
||||
}
|
||||
}
|
||||
},
|
||||
parse(date, date_separator = '-', time_separator = /[.:]/) {
|
||||
parse(date, date_separator = '-', time_separator = ':') {
|
||||
if (date instanceof Date) {
|
||||
return date;
|
||||
}
|
||||
if (typeof date === 'string') {
|
||||
let date_parts, time_parts;
|
||||
const parts = date.split(' ');
|
||||
|
||||
date_parts = parts[0]
|
||||
.split(date_separator)
|
||||
.map((val) => parseInt(val, 10));
|
||||
.map(val => parseInt(val, 10));
|
||||
time_parts = parts[1] && parts[1].split(time_separator);
|
||||
|
||||
// month is 0 indexed
|
||||
date_parts[1] = date_parts[1] ? date_parts[1] - 1 : 0;
|
||||
date_parts[1] = date_parts[1] - 1;
|
||||
|
||||
let vals = date_parts;
|
||||
|
||||
if (time_parts && time_parts.length) {
|
||||
if (time_parts.length === 4) {
|
||||
time_parts[3] = '0.' + time_parts[3];
|
||||
time_parts[3] = parseFloat(time_parts[3]) * 1000;
|
||||
}
|
||||
vals = vals.concat(time_parts);
|
||||
}
|
||||
|
||||
return new Date(...vals);
|
||||
}
|
||||
},
|
||||
@ -66,30 +58,16 @@ export default {
|
||||
val = val + 1;
|
||||
}
|
||||
|
||||
if (i === 6) {
|
||||
return padStart(val + '', 3, '0');
|
||||
}
|
||||
|
||||
return padStart(val + '', 2, '0');
|
||||
});
|
||||
const date_string = `${vals[0]}-${vals[1]}-${vals[2]}`;
|
||||
const time_string = `${vals[3]}:${vals[4]}:${vals[5]}.${vals[6]}`;
|
||||
const time_string = `${vals[3]}:${vals[4]}:${vals[5]}`;
|
||||
|
||||
return date_string + (with_time ? ' ' + time_string : '');
|
||||
},
|
||||
|
||||
format(date, date_format = 'YYYY-MM-DD HH:mm:ss.SSS', lang = 'en') {
|
||||
const dateTimeFormat = new Intl.DateTimeFormat(lang, {
|
||||
month: 'long',
|
||||
});
|
||||
const dateTimeFormatShort = new Intl.DateTimeFormat(lang, {
|
||||
month: 'short',
|
||||
});
|
||||
const month_name = dateTimeFormat.format(date);
|
||||
const month_name_capitalized =
|
||||
month_name.charAt(0).toUpperCase() + month_name.slice(1);
|
||||
|
||||
const values = this.get_date_values(date).map((d) => padStart(d, 2, 0));
|
||||
format(date, format_string = 'YYYY-MM-DD HH:mm:ss') {
|
||||
const values = this.get_date_values(date).map(d => padStart(d, 2, 0));
|
||||
const format_map = {
|
||||
YYYY: values[0],
|
||||
MM: padStart(+values[1] + 1, 2, 0),
|
||||
@ -97,76 +75,47 @@ export default {
|
||||
HH: values[3],
|
||||
mm: values[4],
|
||||
ss: values[5],
|
||||
SSS: values[6],
|
||||
D: values[2],
|
||||
MMMM: month_name_capitalized,
|
||||
MMM: dateTimeFormatShort.format(date),
|
||||
MMMM: month_names[+values[1]],
|
||||
MMM: month_names[+values[1]]
|
||||
};
|
||||
|
||||
let str = date_format;
|
||||
const formatted_values = [];
|
||||
let str = format_string;
|
||||
|
||||
Object.keys(format_map)
|
||||
.sort((a, b) => b.length - a.length) // big string first
|
||||
.forEach((key) => {
|
||||
if (str.includes(key)) {
|
||||
str = str.replaceAll(key, `$${formatted_values.length}`);
|
||||
formatted_values.push(format_map[key]);
|
||||
}
|
||||
.forEach(key => {
|
||||
str = str.replace(key, format_map[key]);
|
||||
});
|
||||
|
||||
formatted_values.forEach((value, i) => {
|
||||
str = str.replaceAll(`$${i}`, value);
|
||||
});
|
||||
|
||||
return str;
|
||||
},
|
||||
|
||||
diff(date_a, date_b, scale = 'day') {
|
||||
diff(date_a, date_b, scale = DAY) {
|
||||
let milliseconds, seconds, hours, minutes, days, months, years;
|
||||
|
||||
milliseconds =
|
||||
date_a -
|
||||
date_b +
|
||||
(date_b.getTimezoneOffset() - date_a.getTimezoneOffset()) * 60000;
|
||||
milliseconds = date_a - date_b;
|
||||
seconds = milliseconds / 1000;
|
||||
minutes = seconds / 60;
|
||||
hours = minutes / 60;
|
||||
days = hours / 24;
|
||||
// Calculate months across years
|
||||
let yearDiff = date_a.getFullYear() - date_b.getFullYear();
|
||||
let monthDiff = date_a.getMonth() - date_b.getMonth();
|
||||
// calculate extra
|
||||
monthDiff += (days % 30) / 30;
|
||||
|
||||
/* If monthDiff is negative, date_b is in an earlier month than
|
||||
date_a and thus subtracted from the year difference in months */
|
||||
months = yearDiff * 12 + monthDiff;
|
||||
/* If date_a's (e.g. march 1st) day of the month is smaller than date_b (e.g. february 28th),
|
||||
adjust the month difference */
|
||||
if (date_a.getDate() < date_b.getDate()) {
|
||||
months--;
|
||||
}
|
||||
|
||||
// Calculate years based on actual months
|
||||
months = days / 30;
|
||||
years = months / 12;
|
||||
|
||||
if (!scale.endsWith('s')) {
|
||||
scale += 's';
|
||||
}
|
||||
|
||||
return (
|
||||
Math.round(
|
||||
{
|
||||
milliseconds,
|
||||
seconds,
|
||||
minutes,
|
||||
hours,
|
||||
days,
|
||||
months,
|
||||
years,
|
||||
}[scale] * 100,
|
||||
) / 100
|
||||
return Math.floor(
|
||||
{
|
||||
milliseconds,
|
||||
seconds,
|
||||
minutes,
|
||||
hours,
|
||||
days,
|
||||
months,
|
||||
years
|
||||
}[scale]
|
||||
);
|
||||
},
|
||||
|
||||
@ -188,7 +137,7 @@ export default {
|
||||
date.getHours() + (scale === HOUR ? qty : 0),
|
||||
date.getMinutes() + (scale === MINUTE ? qty : 0),
|
||||
date.getSeconds() + (scale === SECOND ? qty : 0),
|
||||
date.getMilliseconds() + (scale === MILLISECOND ? qty : 0),
|
||||
date.getMilliseconds() + (scale === MILLISECOND ? qty : 0)
|
||||
];
|
||||
return new Date(...vals);
|
||||
},
|
||||
@ -201,7 +150,7 @@ export default {
|
||||
[HOUR]: 3,
|
||||
[MINUTE]: 2,
|
||||
[SECOND]: 1,
|
||||
[MILLISECOND]: 0,
|
||||
[MILLISECOND]: 0
|
||||
};
|
||||
|
||||
function should_reset(_scale) {
|
||||
@ -216,7 +165,7 @@ export default {
|
||||
should_reset(DAY) ? 0 : date.getHours(),
|
||||
should_reset(HOUR) ? 0 : date.getMinutes(),
|
||||
should_reset(MINUTE) ? 0 : date.getSeconds(),
|
||||
should_reset(SECOND) ? 0 : date.getMilliseconds(),
|
||||
should_reset(SECOND) ? 0 : date.getMilliseconds()
|
||||
];
|
||||
|
||||
return new Date(...vals);
|
||||
@ -234,25 +183,10 @@ export default {
|
||||
date.getHours(),
|
||||
date.getMinutes(),
|
||||
date.getSeconds(),
|
||||
date.getMilliseconds(),
|
||||
date.getMilliseconds()
|
||||
];
|
||||
},
|
||||
|
||||
convert_scales(period, to_scale) {
|
||||
const TO_DAYS = {
|
||||
millisecond: 1 / 60 / 60 / 24 / 1000,
|
||||
second: 1 / 60 / 60 / 24,
|
||||
minute: 1 / 60 / 24,
|
||||
hour: 1 / 24,
|
||||
day: 1,
|
||||
month: 30,
|
||||
year: 365,
|
||||
};
|
||||
const { duration, scale } = this.parse_duration(period);
|
||||
let in_days = duration * TO_DAYS[scale];
|
||||
return in_days / TO_DAYS[to_scale];
|
||||
},
|
||||
|
||||
get_days_in_month(date) {
|
||||
const no_of_days = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
||||
|
||||
@ -264,15 +198,11 @@ export default {
|
||||
|
||||
// Feb
|
||||
const year = date.getFullYear();
|
||||
if ((year % 4 === 0 && year % 100 != 0) || year % 400 === 0) {
|
||||
if ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0) {
|
||||
return 29;
|
||||
}
|
||||
return 28;
|
||||
},
|
||||
|
||||
get_days_in_year(date) {
|
||||
return date.getFullYear() % 4 ? 365 : 366;
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/padStart
|
||||
|
||||
160
src/defaults.js
160
src/defaults.js
@ -1,160 +0,0 @@
|
||||
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) =>
|
||||
!ld || d.getDate() !== ld.getDate()
|
||||
? date_utils.format(d, 'D MMMM', lang)
|
||||
: '',
|
||||
upper_text_frequency: 24,
|
||||
},
|
||||
{
|
||||
name: 'Quarter Day',
|
||||
padding: '7d',
|
||||
step: '6h',
|
||||
date_format: 'YYYY-MM-DD HH:',
|
||||
lower_text: 'HH',
|
||||
upper_text: (d, ld, lang) =>
|
||||
!ld || d.getDate() !== ld.getDate()
|
||||
? date_utils.format(d, 'D MMM', lang)
|
||||
: '',
|
||||
upper_text_frequency: 4,
|
||||
},
|
||||
{
|
||||
name: 'Half Day',
|
||||
padding: '14d',
|
||||
step: '12h',
|
||||
date_format: 'YYYY-MM-DD HH:',
|
||||
lower_text: 'HH',
|
||||
upper_text: (d, ld, lang) =>
|
||||
!ld || d.getDate() !== ld.getDate()
|
||||
? d.getMonth() !== d.getMonth()
|
||||
? date_utils.format(d, 'D MMM', lang)
|
||||
: date_utils.format(d, 'D', lang)
|
||||
: '',
|
||||
upper_text_frequency: 2,
|
||||
},
|
||||
{
|
||||
name: 'Day',
|
||||
padding: '7d',
|
||||
date_format: 'YYYY-MM-DD',
|
||||
step: '1d',
|
||||
lower_text: (d, ld, lang) =>
|
||||
!ld || d.getDate() !== ld.getDate()
|
||||
? date_utils.format(d, 'D', lang)
|
||||
: '',
|
||||
upper_text: (d, ld, lang) =>
|
||||
!ld || d.getMonth() !== ld.getMonth()
|
||||
? date_utils.format(d, 'MMMM', lang)
|
||||
: '',
|
||||
thick_line: (d) => d.getDay() === 1,
|
||||
},
|
||||
{
|
||||
name: 'Week',
|
||||
padding: '1m',
|
||||
step: '7d',
|
||||
date_format: 'YYYY-MM-DD',
|
||||
column_width: 140,
|
||||
lower_text: formatWeek,
|
||||
upper_text: (d, ld, lang) =>
|
||||
!ld || d.getMonth() !== ld.getMonth()
|
||||
? date_utils.format(d, 'MMMM', lang)
|
||||
: '',
|
||||
thick_line: (d) => d.getDate() >= 1 && d.getDate() <= 7,
|
||||
upper_text_frequency: 4,
|
||||
},
|
||||
{
|
||||
name: 'Month',
|
||||
padding: '2m',
|
||||
step: '1m',
|
||||
column_width: 120,
|
||||
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,
|
||||
snap_at: '7d',
|
||||
},
|
||||
{
|
||||
name: 'Year',
|
||||
padding: '2y',
|
||||
step: '1y',
|
||||
column_width: 120,
|
||||
date_format: 'YYYY',
|
||||
upper_text: (d, ld, lang) =>
|
||||
!ld || getDecade(d) !== getDecade(ld) ? getDecade(d) : '',
|
||||
lower_text: 'YYYY',
|
||||
snap_at: '30d',
|
||||
},
|
||||
];
|
||||
|
||||
const DEFAULT_OPTIONS = {
|
||||
arrow_curve: 5,
|
||||
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',
|
||||
lines: 'both',
|
||||
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,
|
||||
view_modes: DEFAULT_VIEW_MODES,
|
||||
};
|
||||
|
||||
export { DEFAULT_OPTIONS, DEFAULT_VIEW_MODES };
|
||||
166
src/gantt.scss
Normal file
166
src/gantt.scss
Normal file
@ -0,0 +1,166 @@
|
||||
$bar-color: #b8c2cc;
|
||||
$bar-stroke: #8D99A6;
|
||||
$border-color: #e0e0e0;
|
||||
$light-bg: #f5f5f5;
|
||||
$light-border-color: #ebeff2;
|
||||
$light-yellow: #fcf8e3;
|
||||
$text-muted: #666;
|
||||
$text-light: #555;
|
||||
$text-color: #333;
|
||||
$blue: #a3a3ff;
|
||||
$handle-color: #ddd;
|
||||
|
||||
.gantt {
|
||||
.grid-background {
|
||||
fill: none;
|
||||
}
|
||||
.grid-header {
|
||||
fill: #ffffff;
|
||||
stroke: $border-color;
|
||||
stroke-width: 1.4;
|
||||
}
|
||||
.grid-row {
|
||||
fill: #ffffff;
|
||||
}
|
||||
.grid-row:nth-child(even) {
|
||||
fill: $light-bg;
|
||||
}
|
||||
.row-line {
|
||||
stroke: $light-border-color;
|
||||
}
|
||||
.tick {
|
||||
stroke: $border-color;
|
||||
stroke-width: 0.2;
|
||||
&.thick {
|
||||
stroke-width: 0.4;
|
||||
}
|
||||
}
|
||||
.today-highlight {
|
||||
fill: $light-yellow;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
fill: none;
|
||||
stroke: $text-muted;
|
||||
stroke-width: 1.4;
|
||||
}
|
||||
|
||||
.bar {
|
||||
fill: $bar-color;
|
||||
stroke: $bar-stroke;
|
||||
stroke-width: 0;
|
||||
transition: stroke-width .3s ease;
|
||||
user-select: none;
|
||||
}
|
||||
.bar-progress {
|
||||
fill: $blue;
|
||||
}
|
||||
.bar-invalid {
|
||||
fill: transparent;
|
||||
stroke: $bar-stroke;
|
||||
stroke-width: 1;
|
||||
stroke-dasharray: 5;
|
||||
|
||||
&~.bar-label {
|
||||
fill: $text-light;
|
||||
}
|
||||
}
|
||||
.bar-label {
|
||||
fill: #fff;
|
||||
dominant-baseline: central;
|
||||
text-anchor: middle;
|
||||
font-size: 12px;
|
||||
font-weight: lighter;
|
||||
|
||||
&.big {
|
||||
fill: $text-light;
|
||||
text-anchor: start;
|
||||
}
|
||||
}
|
||||
|
||||
.handle {
|
||||
fill: $handle-color;
|
||||
cursor: ew-resize;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: opacity .3s ease;
|
||||
}
|
||||
|
||||
.bar-wrapper {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
.bar {
|
||||
fill: darken($bar-color, 5);
|
||||
}
|
||||
|
||||
.bar-progress {
|
||||
fill: darken($blue, 5);
|
||||
}
|
||||
|
||||
.handle {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
.bar {
|
||||
fill: darken($bar-color, 5);
|
||||
}
|
||||
|
||||
.bar-progress {
|
||||
fill: darken($blue, 5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lower-text, .upper-text {
|
||||
font-size: 12px;
|
||||
text-anchor: middle;
|
||||
}
|
||||
.upper-text {
|
||||
fill: $text-light;
|
||||
}
|
||||
.lower-text {
|
||||
fill: $text-color;
|
||||
}
|
||||
|
||||
.hide {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.gantt-container {
|
||||
position: relative;
|
||||
font-size: 12px;
|
||||
|
||||
.popup-wrapper {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
padding: 0;
|
||||
color: #959da5;
|
||||
border-radius: 3px;
|
||||
|
||||
.title {
|
||||
border-bottom: 3px solid $blue;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
padding: 10px;
|
||||
color: #dfe2e5;
|
||||
}
|
||||
|
||||
.pointer {
|
||||
position: absolute;
|
||||
height: 5px;
|
||||
margin: 0 0 0 -5px;
|
||||
border: 5px solid transparent;
|
||||
border-top-color: rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
}
|
||||
}
|
||||
1597
src/index.js
1597
src/index.js
File diff suppressed because it is too large
Load Diff
84
src/popup.js
84
src/popup.js
@ -1,9 +1,6 @@
|
||||
export default class Popup {
|
||||
constructor(parent, popup_func, gantt) {
|
||||
constructor(parent) {
|
||||
this.parent = parent;
|
||||
this.popup_func = popup_func;
|
||||
this.gantt = gantt;
|
||||
|
||||
this.make();
|
||||
}
|
||||
|
||||
@ -11,51 +8,62 @@ export default class Popup {
|
||||
this.parent.innerHTML = `
|
||||
<div class="title"></div>
|
||||
<div class="subtitle"></div>
|
||||
<div class="details"></div>
|
||||
<div class="actions"></div>
|
||||
<div class="pointer"></div>
|
||||
`;
|
||||
|
||||
this.hide();
|
||||
|
||||
this.title = this.parent.querySelector('.title');
|
||||
this.subtitle = this.parent.querySelector('.subtitle');
|
||||
this.details = this.parent.querySelector('.details');
|
||||
this.actions = this.parent.querySelector('.actions');
|
||||
this.pointer = this.parent.querySelector('.pointer');
|
||||
}
|
||||
|
||||
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;
|
||||
show(options) {
|
||||
if (!options.target_element) {
|
||||
throw new Error('target_element is required to show popup');
|
||||
}
|
||||
if (!options.position) {
|
||||
options.position = 'left';
|
||||
}
|
||||
const target_element = options.target_element;
|
||||
|
||||
if (this.actions.innerHTML === '') this.actions.remove();
|
||||
else this.parent.appendChild(this.actions);
|
||||
// set data
|
||||
this.title.innerHTML = options.title;
|
||||
this.subtitle.innerHTML = options.subtitle;
|
||||
|
||||
this.parent.style.left = x + 10 + 'px';
|
||||
this.parent.style.top = y - 10 + 'px';
|
||||
this.parent.classList.remove('hide');
|
||||
this.parent.style.width = this.parent.clientWidth + 'px';
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
if (options.position === 'left') {
|
||||
this.parent.style.left =
|
||||
position_meta.x + (position_meta.width + 10) + 'px';
|
||||
this.parent.style.top =
|
||||
position_meta.y -
|
||||
this.title.clientHeight / 2 +
|
||||
position_meta.height / 2 +
|
||||
'px';
|
||||
|
||||
this.pointer.style.transform = 'rotateZ(90deg)';
|
||||
this.pointer.style.left = '-7px';
|
||||
this.pointer.style.top =
|
||||
this.title.clientHeight / 2 -
|
||||
this.pointer.getBoundingClientRect().height +
|
||||
2 +
|
||||
'px';
|
||||
}
|
||||
|
||||
// show
|
||||
this.parent.style.opacity = 1;
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.parent.classList.add('hide');
|
||||
this.parent.style.opacity = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,87 +0,0 @@
|
||||
: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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,343 +0,0 @@
|
||||
@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;
|
||||
border-bottom: 1px solid var(--g-row-border-color);
|
||||
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: 14px;
|
||||
color: var(--g-text-dark);
|
||||
height: calc(var(--gv-lower-header-height) * 0.66);
|
||||
}
|
||||
|
||||
& .current-upper {
|
||||
position: sticky;
|
||||
left: 0 !important;
|
||||
padding-left: 17px;
|
||||
background: white;
|
||||
}
|
||||
|
||||
& .side-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
right: 0;
|
||||
float: right;
|
||||
|
||||
z-index: 1000;
|
||||
line-height: 20px;
|
||||
font-weight: 400;
|
||||
width: max-content;
|
||||
margin-left: auto;
|
||||
padding-right: 10px;
|
||||
padding-top: 10px;
|
||||
background: var(--g-header-background);
|
||||
display: flex;
|
||||
}
|
||||
|
||||
& .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);
|
||||
border-radius: 0.5rem;
|
||||
border: none;
|
||||
padding: 5px 8px;
|
||||
color: var(--g-text-dark);
|
||||
font-size: 14px;
|
||||
letter-spacing: 0.02em;
|
||||
font-weight: 420;
|
||||
box-sizing: content-box;
|
||||
|
||||
margin-right: 5px;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
filter: brightness(97.5%);
|
||||
}
|
||||
}
|
||||
|
||||
& .side-header select {
|
||||
width: 60px;
|
||||
padding-top: 2px;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
& .side-header select:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
& .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);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
& .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 {
|
||||
outline: 1px solid var(--g-row-border-color);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.bar {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.date-range-highlight {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,22 +0,0 @@
|
||||
:root {
|
||||
--g-arrow-color: #1f2937;
|
||||
--g-bar-color: #fff;
|
||||
--g-bar-border: #fff;
|
||||
--g-tick-color-thick: #ededed;
|
||||
--g-tick-color: #f3f3f3;
|
||||
--g-actions-background: #f3f3f3;
|
||||
--g-border-color: #ebeff2;
|
||||
--g-text-muted: #7c7c7c;
|
||||
--g-text-light: #fff;
|
||||
--g-text-dark: #171717;
|
||||
--g-progress-color: #dbdbdb;
|
||||
--g-handle-color: #37352f;
|
||||
--g-weekend-label-color: #dcdce4;
|
||||
--g-expected-progress: #c4c4e9;
|
||||
--g-header-background: #fff;
|
||||
--g-row-color: #fdfdfd;
|
||||
--g-row-border-color: #c7c7c7;
|
||||
--g-today-highlight: #37352f;
|
||||
--g-popup-actions: #ebeff2;
|
||||
--g-weekend-highlight-color: #f7f7f7;
|
||||
}
|
||||
@ -12,8 +12,6 @@ export function createSVG(tag, attrs) {
|
||||
parent.appendChild(elem);
|
||||
} else if (attr === 'innerHTML') {
|
||||
elem.innerHTML = attrs.innerHTML;
|
||||
} else if (attr === 'clipPath') {
|
||||
elem.setAttribute('clip-path', 'url(#' + attrs[attr] + ')');
|
||||
} else {
|
||||
elem.setAttribute(attr, attrs[attr]);
|
||||
}
|
||||
@ -40,7 +38,7 @@ function getAnimationElement(
|
||||
from,
|
||||
to,
|
||||
dur = '0.4s',
|
||||
begin = '0.1s',
|
||||
begin = '0.1s'
|
||||
) {
|
||||
const animEl = svgElement.querySelector('animate');
|
||||
if (animEl) {
|
||||
@ -49,7 +47,7 @@ function getAnimationElement(
|
||||
from,
|
||||
to,
|
||||
dur,
|
||||
begin: 'click + ' + begin, // artificial click
|
||||
begin: 'click + ' + begin // artificial click
|
||||
});
|
||||
return svgElement;
|
||||
}
|
||||
@ -63,7 +61,7 @@ function getAnimationElement(
|
||||
calcMode: 'spline',
|
||||
values: from + ';' + to,
|
||||
keyTimes: '0; 1',
|
||||
keySplines: cubic_bezier('ease-out'),
|
||||
keySplines: cubic_bezier('ease-out')
|
||||
});
|
||||
svgElement.appendChild(animateElement);
|
||||
|
||||
@ -76,7 +74,7 @@ function cubic_bezier(name) {
|
||||
linear: '0 0 1 1',
|
||||
'ease-in': '.42 0 1 1',
|
||||
'ease-out': '0 0 .58 1',
|
||||
'ease-in-out': '.42 0 .58 1',
|
||||
'ease-in-out': '.42 0 .58 1'
|
||||
}[name];
|
||||
}
|
||||
|
||||
@ -94,13 +92,13 @@ $.off = (element, event, handler) => {
|
||||
};
|
||||
|
||||
$.bind = (element, event, callback) => {
|
||||
event.split(/\s+/).forEach(function (event) {
|
||||
event.split(/\s+/).forEach(function(event) {
|
||||
element.addEventListener(event, callback);
|
||||
});
|
||||
};
|
||||
|
||||
$.delegate = (element, event, selector, callback) => {
|
||||
element.addEventListener(event, function (e) {
|
||||
element.addEventListener(event, function(e) {
|
||||
const delegatedTarget = e.target.closest(selector);
|
||||
if (delegatedTarget) {
|
||||
e.delegatedTarget = delegatedTarget;
|
||||
|
||||
@ -19,45 +19,11 @@ test('Parse: parses string datetime', () => {
|
||||
expect(date.getSeconds()).toBe(34);
|
||||
});
|
||||
|
||||
test('Parse: parses string datetime', () => {
|
||||
const date = date_utils.parse('2016-02-29 16:08:34.3');
|
||||
|
||||
expect(date.getFullYear()).toBe(2016);
|
||||
expect(date.getMonth()).toBe(1);
|
||||
expect(date.getDate()).toBe(29);
|
||||
expect(date.getHours()).toBe(16);
|
||||
expect(date.getMinutes()).toBe(8);
|
||||
expect(date.getSeconds()).toBe(34);
|
||||
expect(date.getMilliseconds()).toBe(300);
|
||||
});
|
||||
|
||||
test('Parse: parses string datetime', () => {
|
||||
const date = date_utils.parse('2015-07-01 00:00:59.200');
|
||||
|
||||
expect(date.getFullYear()).toBe(2015);
|
||||
expect(date.getMonth()).toBe(6);
|
||||
expect(date.getDate()).toBe(1);
|
||||
expect(date.getHours()).toBe(0);
|
||||
expect(date.getMinutes()).toBe(0);
|
||||
expect(date.getSeconds()).toBe(59);
|
||||
expect(date.getMilliseconds()).toBe(200);
|
||||
});
|
||||
|
||||
test('Format: converts date object to string', () => {
|
||||
const date = new Date('2017-09-18');
|
||||
expect(date_utils.to_string(date)).toBe('2017-09-18');
|
||||
});
|
||||
|
||||
test('Format: converts date object to string', () => {
|
||||
const date = new Date('2016-02-29 16:08:34.3');
|
||||
expect(date_utils.to_string(date, true)).toBe('2016-02-29 16:08:34.300');
|
||||
});
|
||||
|
||||
test('Format: converts date object to string', () => {
|
||||
const date = new Date('2016-02-29 16:08:34.3');
|
||||
expect(date_utils.to_string(date, true)).toBe('2016-02-29 16:08:34.300');
|
||||
});
|
||||
|
||||
test('Parse: returns Date Object as is', () => {
|
||||
const d = new Date();
|
||||
const date = date_utils.parse(d);
|
||||
@ -75,41 +41,31 @@ test('Diff: returns diff between 2 date objects', () => {
|
||||
});
|
||||
|
||||
test('StartOf', () => {
|
||||
const date = date_utils.parse('2017-08-12 15:07:34.012');
|
||||
|
||||
const start_of_millisecond = date_utils.start_of(date, 'millisecond');
|
||||
expect(date_utils.to_string(start_of_millisecond, true)).toBe(
|
||||
'2017-08-12 15:07:34.012',
|
||||
);
|
||||
|
||||
const start_of_second = date_utils.start_of(date, 'second');
|
||||
expect(date_utils.to_string(start_of_second, true)).toBe(
|
||||
'2017-08-12 15:07:34.000',
|
||||
);
|
||||
const date = date_utils.parse('2017-08-12 15:07:34');
|
||||
|
||||
const start_of_minute = date_utils.start_of(date, 'minute');
|
||||
expect(date_utils.to_string(start_of_minute, true)).toBe(
|
||||
'2017-08-12 15:07:00.000',
|
||||
'2017-08-12 15:07:00'
|
||||
);
|
||||
|
||||
const start_of_hour = date_utils.start_of(date, 'hour');
|
||||
expect(date_utils.to_string(start_of_hour, true)).toBe(
|
||||
'2017-08-12 15:00:00.000',
|
||||
'2017-08-12 15:00:00'
|
||||
);
|
||||
|
||||
const start_of_day = date_utils.start_of(date, 'day');
|
||||
expect(date_utils.to_string(start_of_day, true)).toBe(
|
||||
'2017-08-12 00:00:00.000',
|
||||
'2017-08-12 00:00:00'
|
||||
);
|
||||
|
||||
const start_of_month = date_utils.start_of(date, 'month');
|
||||
expect(date_utils.to_string(start_of_month, true)).toBe(
|
||||
'2017-08-01 00:00:00.000',
|
||||
'2017-08-01 00:00:00'
|
||||
);
|
||||
|
||||
const start_of_year = date_utils.start_of(date, 'year');
|
||||
expect(date_utils.to_string(start_of_year, true)).toBe(
|
||||
'2017-01-01 00:00:00.000',
|
||||
'2017-01-01 00:00:00'
|
||||
);
|
||||
});
|
||||
|
||||
@ -117,8 +73,3 @@ test('format', () => {
|
||||
const date = date_utils.parse('2017-08-12 15:07:23');
|
||||
expect(date_utils.format(date, 'YYYY-MM-DD')).toBe('2017-08-12');
|
||||
});
|
||||
|
||||
test('format', () => {
|
||||
const date = date_utils.parse('2016-02-29 16:08:34.3');
|
||||
expect(date_utils.format(date)).toBe('2016-02-29 16:08:34.300');
|
||||
});
|
||||
|
||||
@ -1,21 +0,0 @@
|
||||
import { resolve } from 'path';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
lib: {
|
||||
entry: resolve(__dirname, 'src/index.js'),
|
||||
name: 'Gantt',
|
||||
fileName: 'frappe-gantt',
|
||||
},
|
||||
rollupOptions: {
|
||||
output: {
|
||||
format: 'cjs',
|
||||
assetFileNames: 'frappe-gantt[extname]',
|
||||
entryFileNames: 'frappe-gantt.[format].js'
|
||||
},
|
||||
},
|
||||
},
|
||||
output: { interop: 'auto' },
|
||||
server: { watch: { include: ['dist/*', 'src/*'] } }
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user