Compare commits

..

4 Commits

Author SHA1 Message Date
Faris Ansari
7d1f9b6bf1 Corner radius configurable 2018-02-17 23:39:58 +05:30
Faris Ansari
d3d725c25e Merge with master 2018-02-17 23:34:14 +05:30
Faris Ansari
390fd2d324 Change Bar hover behaviour, Bar animation 2018-02-17 23:23:05 +05:30
Faris Ansari
e55107ee82 [v0.1.0] Major Refactor
- Remove moment and Snap dependencies
- Use Rollup as build tool
- Use Prettier for linting/styling
- Use Jest for testing
- Use yarn
2018-02-11 16:48:44 +05:30
38 changed files with 7080 additions and 5549 deletions

3
.babelrc Executable file
View File

@ -0,0 +1,3 @@
{
"presets": ["env"]
}

7
.eslintrc Normal file
View File

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

BIN
.github/gantt-logo.jpg vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

BIN
.github/hero-image.png vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

View File

@ -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
View File

@ -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

View File

@ -1 +0,0 @@
dist

View File

@ -1,4 +1,4 @@
{
"tabWidth": 4,
"singleQuote": true
}
"tabWidth": 4,
"singleQuote": true
}

162
README.md
View File

@ -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>
![image](https://cloud.githubusercontent.com/assets/9355208/21537921/4a38b194-cdbd-11e6-8110-e0da19678a6d.png)
![Hero Image](.github/hero-image.png)
#### 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 todays date. | `true`, `false` | `true` |
| `view_mode` | The initial view mode of the Gantt chart. | `Day`, `Week`, `Month`, `Year`. | `Day` |
| `view_mode_select` | Allows selecting the view mode from a dropdown. | `true`, `false` | `false` |
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)

View File

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

View File

@ -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
View 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

File diff suppressed because it is too large Load Diff

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

File diff suppressed because one or more lines are too long

5
dist/frappe-gantt.min.js.map vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -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",
},
}];

View File

@ -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>

View File

@ -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
View 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

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +0,0 @@
/* eslint-disable */
module.exports = {
plugins: [require('postcss-nesting')],
};

25
rollup.config.js Normal file
View 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];

View File

@ -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
View 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]'
);
}

View File

@ -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();
}
}
}

View File

@ -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

View File

@ -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
View 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);
}
}
}

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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');
});

View File

@ -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/*'] } }
});

3869
yarn.lock Normal file

File diff suppressed because it is too large Load Diff