Merge pull request #387 from safwansamsudeen/master

New Features
This commit is contained in:
Safwan Samsudeen 2024-04-06 16:49:35 +05:30 committed by GitHub
commit ea6259adce
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 4236 additions and 4020 deletions

1
.prettierignore Normal file
View File

@ -0,0 +1 @@
dist

View File

@ -16,18 +16,22 @@
</p> </p>
### Install ### Install
``` ```
npm install frappe-gantt npm install frappe-gantt
``` ```
### Usage ### Usage
Include it in your HTML: Include it in your HTML:
``` ```
<script src="frappe-gantt.min.js"></script> <script src="frappe-gantt.min.js"></script>
<link rel="stylesheet" href="frappe-gantt.css"> <link rel="stylesheet" href="frappe-gantt.css">
``` ```
And start hacking: And start hacking:
```js ```js
var tasks = [ var tasks = [
{ {
@ -45,20 +49,21 @@ var gantt = new Gantt("#gantt", tasks);
``` ```
You can also pass various options to the Gantt constructor: You can also pass various options to the Gantt constructor:
```js ```js
var gantt = new Gantt("#gantt", tasks, { var gantt = new Gantt("#gantt", tasks, {
header_height: 50, header_height: 50,
column_width: 30, column_width: 30,
step: 24, step: 24,
view_modes: ['Quarter Day', 'Half Day', 'Day', 'Week', 'Month'], view_modes: ["Quarter Day", "Half Day", "Day", "Week", "Month"],
bar_height: 20, bar_height: 20,
bar_corner_radius: 3, bar_corner_radius: 3,
arrow_curve: 5, arrow_curve: 5,
padding: 18, padding: 18,
view_mode: 'Day', view_mode: "Day",
date_format: 'YYYY-MM-DD', date_format: "YYYY-MM-DD",
language: 'en', // or 'es', 'it', 'ru', 'ptBr', 'fr', 'tr', 'zh', 'de', 'hu' language: "en", // or 'es', 'it', 'ru', 'ptBr', 'fr', 'tr', 'zh', 'de', 'hu'
custom_popup_html: null custom_popup_html: null,
}); });
``` ```
@ -69,6 +74,7 @@ You can add `dark` class to the container element to apply dark theme.
``` ```
### Contributing ### Contributing
If you want to contribute enhancements or fixes: If you want to contribute enhancements or fixes:
1. Clone this repo. 1. Clone this repo.
@ -78,6 +84,7 @@ If you want to contribute enhancements or fixes:
5. Open `index.html` in your browser, make your code changes and test them. 5. Open `index.html` in your browser, make your code changes and test them.
### Publishing ### Publishing
If you have publishing rights (Frappe Team), follow these steps to publish a new version. If you have publishing rights (Frappe Team), follow these steps to publish a new version.
Assuming the last commit (or a couple of commits) were enhancements or fixes, Assuming the last commit (or a couple of commits) were enhancements or fixes,
@ -85,15 +92,18 @@ Assuming the last commit (or a couple of commits) were enhancements or fixes,
1. Run `yarn build` 1. Run `yarn build`
This will generate files in the `dist/` folder. These files need to be committed. This will generate files in the `dist/` folder. These files need to be committed.
1. Run `yarn publish` 1. Run `yarn publish`
1. Type the new version at the prompt 1. Type the new version at the prompt
Depending on the type of change, you can either bump the patch version or the minor version. Depending on the type of change, you can either bump the patch version or the minor version.
For e.g., For e.g.,
``` ```
0.5.0 -> 0.6.0 (minor version bump) 0.5.0 -> 0.6.0 (minor version bump)
0.5.0 -> 0.5.1 (patch version bump) 0.5.0 -> 0.5.1 (patch version bump)
``` ```
1. Now, there will be a commit named after the version you just entered. Include the generated files in `dist/` folder as part of this commit by running the command: 1. Now, there will be a commit named after the version you just entered. Include the generated files in `dist/` folder as part of this commit by running the command:
``` ```
git add dist git add dist
@ -103,5 +113,6 @@ Assuming the last commit (or a couple of commits) were enhancements or fixes,
License: MIT License: MIT
------------------ ---
Project maintained by [frappe](https://github.com/frappe) Project maintained by [frappe](https://github.com/frappe)

24
dist/frappe-gantt.css vendored
View File

@ -66,6 +66,10 @@
border-top-color: #333; border-top-color: #333;
} }
.gantt {
user-select: none;
-webkit-user-select: none;
}
.gantt .grid-background { .gantt .grid-background {
fill: none; fill: none;
} }
@ -94,6 +98,18 @@
fill: #fcf8e3; fill: #fcf8e3;
opacity: 0.5; opacity: 0.5;
} }
.gantt .week-highlight {
fill: #fcf8e3;
opacity: 0.5;
}
.gantt .month-highlight {
fill: #fcf8e3;
opacity: 0.5;
}
.gantt .year-highlight {
fill: #fcf8e3;
opacity: 0.5;
}
.gantt .arrow { .gantt .arrow {
fill: none; fill: none;
stroke: #666; stroke: #666;
@ -101,10 +117,9 @@
} }
.gantt .bar { .gantt .bar {
fill: #b8c2cc; fill: #b8c2cc;
stroke: #8D99A6; stroke: #8d99a6;
stroke-width: 0; stroke-width: 0;
transition: stroke-width 0.3s ease; transition: stroke-width 0.3s ease;
user-select: none;
} }
.gantt .bar-progress { .gantt .bar-progress {
fill: #acacfa; fill: #acacfa;
@ -114,7 +129,7 @@
} }
.gantt .bar-invalid { .gantt .bar-invalid {
fill: transparent; fill: transparent;
stroke: #8D99A6; stroke: #8d99a6;
stroke-width: 1; stroke-width: 1;
stroke-dasharray: 5; stroke-dasharray: 5;
} }
@ -159,7 +174,8 @@
.gantt .bar-wrapper.active .bar-progress { .gantt .bar-wrapper.active .bar-progress {
fill: #9494f9; fill: #9494f9;
} }
.gantt .lower-text, .gantt .upper-text { .gantt .lower-text,
.gantt .upper-text {
font-size: 12px; font-size: 12px;
text-anchor: middle; text-anchor: middle;
} }

742
dist/frappe-gantt.js vendored

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
.dark>.gantt-container .gantt .grid-header{fill:#252525;stroke:#616161}.dark>.gantt-container .gantt .grid-row{fill:#252525}.dark>.gantt-container .gantt .grid-row:nth-child(even){fill:#3e3e3e}.dark>.gantt-container .gantt .row-line{stroke:#3e3e3e}.dark>.gantt-container .gantt .tick{stroke:#616161}.dark>.gantt-container .gantt .today-highlight{opacity:.2}.dark>.gantt-container .gantt .arrow{stroke:#eee}.dark>.gantt-container .gantt .bar{fill:#616161;stroke:none}.dark>.gantt-container .gantt .bar-progress{fill:#8a8aff}.dark>.gantt-container .gantt .bar-invalid{fill:rgba(0,0,0,0);stroke:#c6ccd2}.dark>.gantt-container .gantt .bar-invalid~.bar-label{fill:#ececec}.dark>.gantt-container .gantt .bar-label.big{fill:#ececec}.dark>.gantt-container .gantt .bar-wrapper:hover .bar{fill:#6e6e6e}.dark>.gantt-container .gantt .bar-wrapper:hover .bar-progress{fill:#a4a4ff}.dark>.gantt-container .gantt .bar-wrapper.active .bar{fill:#6e6e6e}.dark>.gantt-container .gantt .bar-wrapper.active .bar-progress{fill:#a4a4ff}.dark>.gantt-container .gantt .upper-text{fill:#a2a2a2}.dark>.gantt-container .gantt .lower-text{fill:#f7f7f7}.dark>.gantt-container .popup-wrapper{background-color:#333}.dark>.gantt-container .popup-wrapper .title{border-color:#a4a4ff}.dark>.gantt-container .popup-wrapper .pointer{border-top-color:#333}.gantt .grid-background{fill:none}.gantt .grid-header{fill:#fff;stroke:#e0e0e0;stroke-width:1.4}.gantt .grid-row{fill:#fff}.gantt .grid-row:nth-child(even){fill:#f5f5f5}.gantt .row-line{stroke:#ebeff2}.gantt .tick{stroke:#e0e0e0;stroke-width:.2}.gantt .tick.thick{stroke-width:.4}.gantt .today-highlight{fill:#fcf8e3;opacity:.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:rgba(0,0,0,0);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;outline:none}.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;overflow:auto;font-size:12px}.gantt-container .popup-wrapper{position:absolute;top:0;left:0;background:rgba(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 rgba(0,0,0,0);border-top-color:rgba(0,0,0,.8)} .dark>.gantt-container .gantt .grid-header{fill:#252525;stroke:#616161}.dark>.gantt-container .gantt .grid-row{fill:#252525}.dark>.gantt-container .gantt .grid-row:nth-child(even){fill:#3e3e3e}.dark>.gantt-container .gantt .row-line{stroke:#3e3e3e}.dark>.gantt-container .gantt .tick{stroke:#616161}.dark>.gantt-container .gantt .today-highlight{opacity:.2}.dark>.gantt-container .gantt .arrow{stroke:#eee}.dark>.gantt-container .gantt .bar{fill:#616161;stroke:none}.dark>.gantt-container .gantt .bar-progress{fill:#8a8aff}.dark>.gantt-container .gantt .bar-invalid{fill:rgba(0,0,0,0);stroke:#c6ccd2}.dark>.gantt-container .gantt .bar-invalid~.bar-label{fill:#ececec}.dark>.gantt-container .gantt .bar-label.big{fill:#ececec}.dark>.gantt-container .gantt .bar-wrapper:hover .bar{fill:#6e6e6e}.dark>.gantt-container .gantt .bar-wrapper:hover .bar-progress{fill:#a4a4ff}.dark>.gantt-container .gantt .bar-wrapper.active .bar{fill:#6e6e6e}.dark>.gantt-container .gantt .bar-wrapper.active .bar-progress{fill:#a4a4ff}.dark>.gantt-container .gantt .upper-text{fill:#a2a2a2}.dark>.gantt-container .gantt .lower-text{fill:#f7f7f7}.dark>.gantt-container .popup-wrapper{background-color:#333}.dark>.gantt-container .popup-wrapper .title{border-color:#a4a4ff}.dark>.gantt-container .popup-wrapper .pointer{border-top-color:#333}.gantt{user-select:none;-webkit-user-select:none}.gantt .grid-background{fill:none}.gantt .grid-header{fill:#fff;stroke:#e0e0e0;stroke-width:1.4}.gantt .grid-row{fill:#fff}.gantt .grid-row:nth-child(even){fill:#f5f5f5}.gantt .row-line{stroke:#ebeff2}.gantt .tick{stroke:#e0e0e0;stroke-width:.2}.gantt .tick.thick{stroke-width:.4}.gantt .today-highlight{fill:#fcf8e3;opacity:.5}.gantt .week-highlight{fill:#fcf8e3;opacity:.5}.gantt .month-highlight{fill:#fcf8e3;opacity:.5}.gantt .year-highlight{fill:#fcf8e3;opacity:.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}.gantt .bar-progress{fill:#acacfa}.gantt .bar-expected-progress{fill:#c4c4e9}.gantt .bar-invalid{fill:rgba(0,0,0,0);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;outline:none}.gantt .bar-wrapper:hover .bar{fill:#a9b5c1}.gantt .bar-wrapper:hover .bar-progress{fill:#9494f9}.gantt .bar-wrapper:hover .handle{visibility:visible;opacity:1}.gantt .bar-wrapper.active .bar{fill:#a9b5c1}.gantt .bar-wrapper.active .bar-progress{fill:#9494f9}.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;overflow:auto;font-size:12px}.gantt-container .popup-wrapper{position:absolute;top:0;left:0;background:rgba(0,0,0,.8);padding:0;color:#959da5;border-radius:3px}.gantt-container .popup-wrapper .title{border-bottom:3px solid #acacfa;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 rgba(0,0,0,0);border-top-color:rgba(0,0,0,.8)}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,7 +1,7 @@
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8" />
<title>Simple Gantt</title> <title>Simple Gantt</title>
<style> <style>
body { body {
@ -38,59 +38,59 @@
end: '2018-10-08', end: '2018-10-08',
name: 'Redesign website', name: 'Redesign website',
id: "Task 0", id: "Task 0",
progress: 20 progress: 100
}, },
{ // {
start: '2018-10-03', // start: '2018-10-03',
end: '2018-10-06', // duration: '1m 4d',
name: 'Write new content', // name: 'Write new content',
id: "Task 1", // id: "Task 1",
progress: 5, // progress: 5,
dependencies: 'Task 0' // dependencies: 'Task 0'
}, // },
{ // {
start: '2018-10-04', // start: '2018-10-04',
end: '2018-10-08', // end: '2018-10-08',
name: 'Apply new styles', // name: 'Apply new styles',
id: "Task 2", // id: "Task 2",
progress: 10, // progress: 10,
dependencies: 'Task 1' // dependencies: 'Task 1'
}, // },
{ // {
start: '2018-10-08', // start: '2018-10-08',
end: '2018-10-09', // end: '2018-10-09',
name: 'Review', // name: 'Review',
id: "Task 3", // id: "Task 3",
progress: 5, // progress: 5,
dependencies: 'Task 2' // dependencies: 'Task 2'
}, // },
{ // {
start: '2018-10-08', // start: '2018-10-08',
end: '2018-10-10', // end: '2018-10-10',
name: 'Deploy', // name: 'Deploy',
id: "Task 4", // id: "Task 4",
progress: 0, // progress: 0,
dependencies: 'Task 2' // dependencies: 'Task 2'
}, // },
{ // {
start: '2018-10-11', // start: '2018-10-11',
end: '2018-10-11', // end: '2018-10-11',
name: 'Go Live!', // name: 'Go Live!',
id: "Task 5", // id: "Task 5",
progress: 0, // progress: 0,
dependencies: 'Task 4', // dependencies: 'Task 4',
custom_class: 'bar-milestone' // custom_class: 'bar-milestone'
}, // },
{ // {
start: '2014-01-05', // start: '2014-01-05',
end: '2019-10-12', // end: '2019-10-12',
name: 'Long term task', // name: 'Long term task',
id: "Task 6", // id: "Task 6",
progress: 0 // progress: 0
} // }
] ];
var gantt_chart = new Gantt(".gantt-target", tasks, { var gantt_chart = new Gantt(".gantt-target", tasks, {
on_click: task => { on_click: (task) => {
console.log(task); console.log(task);
}, },
on_date_change: (task, start, end) => { on_date_change: (task, start, end) => {
@ -102,8 +102,9 @@
on_view_change: (mode) => { on_view_change: (mode) => {
console.log(mode); console.log(mode);
}, },
view_mode: 'Month', view_mode: "Half Day",
language: 'en' view_mode_padding: { DAY: "1d" },
language: "en",
}); });
console.log(gantt_chart); console.log(gantt_chart);
</script> </script>

View File

@ -41,7 +41,7 @@
"eslint-config-prettier": "^2.9.0", "eslint-config-prettier": "^2.9.0",
"eslint-plugin-prettier": "^2.6.0", "eslint-plugin-prettier": "^2.6.0",
"jest": "^22.2.1", "jest": "^22.2.1",
"prettier": "^2.6.2", "prettier": "3.2.5",
"rollup": "^2.70.2", "rollup": "^2.70.2",
"rollup-plugin-sass": "^1.2.12", "rollup-plugin-sass": "^1.2.12",
"rollup-plugin-terser": "^7.0.2" "rollup-plugin-terser": "^7.0.2"

View File

@ -1,13 +1,13 @@
import sass from 'rollup-plugin-sass'; import sass from "rollup-plugin-sass";
import { terser } from 'rollup-plugin-terser'; import { terser } from "rollup-plugin-terser";
const dev = { const dev = {
input: 'src/index.js', input: "src/index.js",
output: { output: {
name: 'Gantt', name: "Gantt",
file: 'dist/frappe-gantt.js', file: "dist/frappe-gantt.js",
sourcemap: true, sourcemap: true,
format: 'iife', format: "iife",
}, },
plugins: [ plugins: [
sass({ sass({
@ -16,18 +16,18 @@ const dev = {
], ],
}; };
const prod = { const prod = {
input: 'src/index.js', input: "src/index.js",
output: { output: {
name: 'Gantt', name: "Gantt",
file: 'dist/frappe-gantt.min.js', file: "dist/frappe-gantt.min.js",
sourcemap: true, sourcemap: true,
format: 'iife', format: "iife",
}, },
plugins: [ plugins: [
sass({ sass({
output: true, output: true,
options: { options: {
outputStyle: 'compressed', outputStyle: "compressed",
}, },
}), }),
terser(), terser(),

View File

@ -1,4 +1,4 @@
import { createSVG } from './svg_utils'; import { createSVG } from "./svg_utils";
export default class Arrow { export default class Arrow {
constructor(gantt, from_task, to_task) { constructor(gantt, from_task, to_task) {
@ -61,9 +61,7 @@ export default class Arrow {
) { ) {
const down_1 = this.gantt.options.padding / 2 - curve; const down_1 = this.gantt.options.padding / 2 - curve;
const down_2 = const down_2 =
this.to_task.$bar.getY() + this.to_task.$bar.getY() + this.to_task.$bar.getHeight() / 2 - curve_y;
this.to_task.$bar.getHeight() / 2 -
curve_y;
const left = this.to_task.$bar.getX() - this.gantt.options.padding; const left = this.to_task.$bar.getX() - this.gantt.options.padding;
this.path = ` this.path = `
@ -82,15 +80,15 @@ export default class Arrow {
} }
draw() { draw() {
this.element = createSVG('path', { this.element = createSVG("path", {
d: this.path, d: this.path,
'data-from': this.from_task.task.id, "data-from": this.from_task.task.id,
'data-to': this.to_task.task.id, "data-to": this.to_task.task.id,
}); });
} }
update() { update() {
this.calculate_path(); this.calculate_path();
this.element.setAttribute('d', this.path); this.element.setAttribute("d", this.path);
} }
} }

View File

@ -1,5 +1,5 @@
import date_utils from './date_utils'; import date_utils from "./date_utils";
import { $, createSVG, animateSVG } from './svg_utils'; import { $, createSVG, animateSVG } from "./svg_utils";
export default class Bar { export default class Bar {
constructor(gantt, task) { constructor(gantt, task) {
@ -32,32 +32,32 @@ export default class Bar {
this.gantt.options.column_width * this.gantt.options.column_width *
this.duration * this.duration *
(this.task.progress / 100) || 0; (this.task.progress / 100) || 0;
this.group = createSVG('g', { this.group = createSVG("g", {
class: 'bar-wrapper ' + (this.task.custom_class || ''), class: "bar-wrapper " + (this.task.custom_class || ""),
'data-id': this.task.id, "data-id": this.task.id,
}); });
this.bar_group = createSVG('g', { this.bar_group = createSVG("g", {
class: 'bar-group', class: "bar-group",
append_to: this.group, append_to: this.group,
}); });
this.handle_group = createSVG('g', { this.handle_group = createSVG("g", {
class: 'handle-group', class: "handle-group",
append_to: this.group, append_to: this.group,
}); });
} }
prepare_helpers() { prepare_helpers() {
SVGElement.prototype.getX = function () { SVGElement.prototype.getX = function () {
return +this.getAttribute('x'); return +this.getAttribute("x");
}; };
SVGElement.prototype.getY = function () { SVGElement.prototype.getY = function () {
return +this.getAttribute('y'); return +this.getAttribute("y");
}; };
SVGElement.prototype.getWidth = function () { SVGElement.prototype.getWidth = function () {
return +this.getAttribute('width'); return +this.getAttribute("width");
}; };
SVGElement.prototype.getHeight = function () { SVGElement.prototype.getHeight = function () {
return +this.getAttribute('height'); return +this.getAttribute("height");
}; };
SVGElement.prototype.getEndX = function () { SVGElement.prototype.getEndX = function () {
return this.getX() + this.getWidth(); return this.getX() + this.getWidth();
@ -84,62 +84,67 @@ export default class Bar {
} }
draw_bar() { draw_bar() {
this.$bar = createSVG('rect', { this.$bar = createSVG("rect", {
x: this.x, x: this.x,
y: this.y, y: this.y,
width: this.width, width: this.width,
height: this.height, height: this.height,
rx: this.corner_radius, rx: this.corner_radius,
ry: this.corner_radius, ry: this.corner_radius,
class: 'bar', class: "bar",
append_to: this.bar_group, append_to: this.bar_group,
}); });
animateSVG(this.$bar, 'width', 0, this.width); animateSVG(this.$bar, "width", 0, this.width);
if (this.invalid) { if (this.invalid) {
this.$bar.classList.add('bar-invalid'); this.$bar.classList.add("bar-invalid");
} }
} }
draw_expected_progress_bar() { draw_expected_progress_bar() {
if (this.invalid) return; if (this.invalid) return;
this.$expected_bar_progress = createSVG('rect', { this.$expected_bar_progress = createSVG("rect", {
x: this.x, x: this.x,
y: this.y, y: this.y,
width: this.expected_progress_width, width: this.expected_progress_width,
height: this.height, height: this.height,
rx: this.corner_radius, rx: this.corner_radius,
ry: this.corner_radius, ry: this.corner_radius,
class: 'bar-expected-progress', class: "bar-expected-progress",
append_to: this.bar_group, append_to: this.bar_group,
}); });
animateSVG(this.$expected_bar_progress, 'width', 0, this.expected_progress_width); animateSVG(
this.$expected_bar_progress,
"width",
0,
this.expected_progress_width,
);
} }
draw_progress_bar() { draw_progress_bar() {
if (this.invalid) return; if (this.invalid) return;
this.$bar_progress = createSVG('rect', { this.$bar_progress = createSVG("rect", {
x: this.x, x: this.x,
y: this.y, y: this.y,
width: this.progress_width, width: this.progress_width,
height: this.height, height: this.height,
rx: this.corner_radius, rx: this.corner_radius,
ry: this.corner_radius, ry: this.corner_radius,
class: 'bar-progress', class: "bar-progress",
append_to: this.bar_group, append_to: this.bar_group,
}); });
animateSVG(this.$bar_progress, 'width', 0, this.progress_width); animateSVG(this.$bar_progress, "width", 0, this.progress_width);
} }
draw_label() { draw_label() {
createSVG('text', { createSVG("text", {
x: this.x + this.width / 2, x: this.x + this.width / 2,
y: this.y + this.height / 2, y: this.y + this.height / 2,
innerHTML: this.task.name, innerHTML: this.task.name,
class: 'bar-label', class: "bar-label",
append_to: this.bar_group, append_to: this.bar_group,
}); });
// labels get BBox in the next tick // labels get BBox in the next tick
@ -152,36 +157,34 @@ export default class Bar {
const bar = this.$bar; const bar = this.$bar;
const handle_width = 8; const handle_width = 8;
createSVG('rect', { createSVG("rect", {
x: bar.getX() + bar.getWidth() - 9, x: bar.getX() + bar.getWidth() - 9,
y: bar.getY() + 1, y: bar.getY() + 1,
width: handle_width, width: handle_width,
height: this.height - 2, height: this.height - 2,
rx: this.corner_radius, rx: this.corner_radius,
ry: this.corner_radius, ry: this.corner_radius,
class: 'handle right', class: "handle right",
append_to: this.handle_group, append_to: this.handle_group,
}); });
createSVG('rect', { createSVG("rect", {
x: bar.getX() + 1, x: bar.getX() + 1,
y: bar.getY() + 1, y: bar.getY() + 1,
width: handle_width, width: handle_width,
height: this.height - 2, height: this.height - 2,
rx: this.corner_radius, rx: this.corner_radius,
ry: this.corner_radius, ry: this.corner_radius,
class: 'handle left', class: "handle left",
append_to: this.handle_group, append_to: this.handle_group,
}); });
if (this.task.progress < 100) { this.$handle_progress = createSVG("polygon", {
this.$handle_progress = createSVG('polygon', { points: this.get_progress_polygon_points().join(","),
points: this.get_progress_polygon_points().join(','), class: "handle progress",
class: 'handle progress',
append_to: this.handle_group, append_to: this.handle_group,
}); });
} }
}
get_progress_polygon_points() { get_progress_polygon_points() {
const bar_progress = this.$bar_progress; const bar_progress = this.$bar_progress;
@ -201,7 +204,7 @@ export default class Bar {
} }
setup_click_event() { setup_click_event() {
$.on(this.group, 'focus ' + this.gantt.options.popup_trigger, (e) => { $.on(this.group, "focus " + this.gantt.options.popup_trigger, (e) => {
if (this.action_completed) { if (this.action_completed) {
// just finished a move action, wait for a few seconds // just finished a move action, wait for a few seconds
return; return;
@ -209,16 +212,16 @@ export default class Bar {
this.show_popup(); this.show_popup();
this.gantt.unselect_all(); this.gantt.unselect_all();
this.group.classList.add('active'); this.group.classList.add("active");
}); });
$.on(this.group, 'dblclick', (e) => { $.on(this.group, "dblclick", (e) => {
if (this.action_completed) { if (this.action_completed) {
// just finished a move action, wait for a few seconds // just finished a move action, wait for a few seconds
return; return;
} }
this.gantt.trigger_event('click', [this.task]); this.gantt.trigger_event("click", [this.task]);
}); });
} }
@ -227,15 +230,15 @@ export default class Bar {
const start_date = date_utils.format( const start_date = date_utils.format(
this.task._start, this.task._start,
'MMM D', "MMM D",
this.gantt.options.language this.gantt.options.language,
); );
const end_date = date_utils.format( const end_date = date_utils.format(
date_utils.add(this.task._end, -1, 'second'), date_utils.add(this.task._end, -1, "second"),
'MMM D', "MMM D",
this.gantt.options.language this.gantt.options.language,
); );
const subtitle = start_date + ' - ' + end_date; const subtitle = start_date + " - " + end_date;
this.gantt.show_popup({ this.gantt.show_popup({
target_element: this.$bar, target_element: this.$bar,
@ -260,10 +263,10 @@ export default class Bar {
width = null; width = null;
return; return;
} }
this.update_attr(bar, 'x', x); this.update_attr(bar, "x", x);
} }
if (width) { if (width) {
this.update_attr(bar, 'width', width); this.update_attr(bar, "width", width);
} }
this.update_label_position(); this.update_label_position();
this.update_handle_position(); this.update_handle_position();
@ -292,17 +295,17 @@ export default class Bar {
if (!changed) return; if (!changed) return;
this.gantt.trigger_event('date_change', [ this.gantt.trigger_event("date_change", [
this.task, this.task,
new_start_date, new_start_date,
date_utils.add(new_end_date, -1, 'second'), date_utils.add(new_end_date, -1, "second"),
]); ]);
} }
progress_changed() { progress_changed() {
const new_progress = this.compute_progress(); const new_progress = this.compute_progress();
this.task.progress = new_progress; this.task.progress = new_progress;
this.gantt.trigger_event('progress_change', [this.task, new_progress]); this.gantt.trigger_event("progress_change", [this.task, new_progress]);
} }
set_action_completed() { set_action_completed() {
@ -316,13 +319,13 @@ export default class Bar {
const new_start_date = date_utils.add( const new_start_date = date_utils.add(
this.gantt.gantt_start, this.gantt.gantt_start,
x_in_units * this.gantt.options.step, x_in_units * this.gantt.options.step,
'hour' "hour",
); );
const width_in_units = bar.getWidth() / this.gantt.options.column_width; const width_in_units = bar.getWidth() / this.gantt.options.column_width;
const new_end_date = date_utils.add( const new_end_date = date_utils.add(
new_start_date, new_start_date,
width_in_units * this.gantt.options.step, width_in_units * this.gantt.options.step,
'hour' "hour",
); );
return { new_start_date, new_end_date }; return { new_start_date, new_end_date };
@ -335,8 +338,15 @@ export default class Bar {
} }
compute_expected_progress() { compute_expected_progress() {
this.expected_progress = date_utils.diff(date_utils.today(), this.task._start, 'hour') / this.gantt.options.step; this.expected_progress =
this.expected_progress = ((this.expected_progress < this.duration) ? this.expected_progress : this.duration) * 100 / this.duration; date_utils.diff(date_utils.today(), this.task._start, "hour") /
this.gantt.options.step;
this.expected_progress =
((this.expected_progress < this.duration
? this.expected_progress
: this.duration) *
100) /
this.duration;
} }
compute_x() { compute_x() {
@ -344,26 +354,26 @@ export default class Bar {
const task_start = this.task._start; const task_start = this.task._start;
const gantt_start = this.gantt.gantt_start; const gantt_start = this.gantt.gantt_start;
const diff = date_utils.diff(task_start, gantt_start, 'hour'); const diff = date_utils.diff(task_start, gantt_start, "hour");
let x = (diff / step) * column_width; let x = (diff / step) * column_width;
if (this.gantt.view_is('Month')) { if (this.gantt.view_is("Month")) {
const diff = date_utils.diff(task_start, gantt_start, 'day'); const diff = date_utils.diff(task_start, gantt_start, "day");
x = (diff * column_width) / 30; x = (diff * column_width) / 30;
} }
this.x = x; this.x = x;
} }
compute_y() { compute_y() {
this.y = ( this.y =
this.gantt.options.header_height + this.gantt.options.header_height +
this.gantt.options.padding + this.gantt.options.padding +
this.task._index * (this.height + this.gantt.options.padding) this.task._index * (this.height + this.gantt.options.padding);
);
} }
compute_duration() { compute_duration() {
this.duration = date_utils.diff(this.task._end, this.task._start, 'hour') / this.duration =
date_utils.diff(this.task._end, this.task._start, "hour") /
this.gantt.options.step; this.gantt.options.step;
} }
@ -372,7 +382,7 @@ export default class Bar {
rem, rem,
position; position;
if (this.gantt.view_is('Week')) { if (this.gantt.view_is("Week")) {
rem = dx % (this.gantt.options.column_width / 7); rem = dx % (this.gantt.options.column_width / 7);
position = position =
odx - odx -
@ -380,7 +390,7 @@ export default class Bar {
(rem < this.gantt.options.column_width / 14 (rem < this.gantt.options.column_width / 14
? 0 ? 0
: this.gantt.options.column_width / 7); : this.gantt.options.column_width / 7);
} else if (this.gantt.view_is('Month')) { } else if (this.gantt.view_is("Month")) {
rem = dx % (this.gantt.options.column_width / 30); rem = dx % (this.gantt.options.column_width / 30);
position = position =
odx - odx -
@ -410,33 +420,35 @@ export default class Bar {
update_expected_progressbar_position() { update_expected_progressbar_position() {
if (this.invalid) return; if (this.invalid) return;
this.$expected_bar_progress.setAttribute('x', this.$bar.getX()); this.$expected_bar_progress.setAttribute("x", this.$bar.getX());
this.compute_expected_progress(); this.compute_expected_progress();
this.$expected_bar_progress.setAttribute( this.$expected_bar_progress.setAttribute(
'width', "width",
this.gantt.options.column_width * this.duration * (this.expected_progress / 100) || 0 this.gantt.options.column_width *
this.duration *
(this.expected_progress / 100) || 0,
); );
} }
update_progressbar_position() { update_progressbar_position() {
if (this.invalid) return; if (this.invalid) return;
this.$bar_progress.setAttribute('x', this.$bar.getX()); this.$bar_progress.setAttribute("x", this.$bar.getX());
this.$bar_progress.setAttribute( this.$bar_progress.setAttribute(
'width', "width",
this.$bar.getWidth() * (this.task.progress / 100) this.$bar.getWidth() * (this.task.progress / 100),
); );
} }
update_label_position() { update_label_position() {
const bar = this.$bar, const bar = this.$bar,
label = this.group.querySelector('.bar-label'); label = this.group.querySelector(".bar-label");
if (label.getBBox().width > bar.getWidth()) { if (label.getBBox().width > bar.getWidth()) {
label.classList.add('big'); label.classList.add("big");
label.setAttribute('x', bar.getX() + bar.getWidth() + 5); label.setAttribute("x", bar.getX() + bar.getWidth() + 5);
} else { } else {
label.classList.remove('big'); label.classList.remove("big");
label.setAttribute('x', bar.getX() + bar.getWidth() / 2); label.setAttribute("x", bar.getX() + bar.getWidth() / 2);
} }
} }
@ -444,14 +456,13 @@ export default class Bar {
if (this.invalid) return; if (this.invalid) return;
const bar = this.$bar; const bar = this.$bar;
this.handle_group this.handle_group
.querySelector('.handle.left') .querySelector(".handle.left")
.setAttribute('x', bar.getX() + 1); .setAttribute("x", bar.getX() + 1);
this.handle_group this.handle_group
.querySelector('.handle.right') .querySelector(".handle.right")
.setAttribute('x', bar.getEndX() - 9); .setAttribute("x", bar.getEndX() - 9);
const handle = this.group.querySelector('.handle.progress'); const handle = this.group.querySelector(".handle.progress");
handle && handle && handle.setAttribute("points", this.get_progress_polygon_points());
handle.setAttribute('points', this.get_progress_polygon_points());
} }
update_arrow_position() { update_arrow_position() {
@ -466,6 +477,6 @@ function isFunction(functionToCheck) {
var getType = {}; var getType = {};
return ( return (
functionToCheck && functionToCheck &&
getType.toString.call(functionToCheck) === '[object Function]' getType.toString.call(functionToCheck) === "[object Function]"
); );
} }

View File

@ -88,7 +88,7 @@ $blue-dark: #8a8aff;
background-color: #333; background-color: #333;
.title { .title {
border-color: lighten($blue-dark, 5);; border-color: lighten($blue-dark, 5);
} }
.pointer { .pointer {
border-top-color: #333; border-top-color: #333;

View File

@ -1,19 +1,41 @@
const YEAR = 'year'; const YEAR = "year";
const MONTH = 'month'; const MONTH = "month";
const DAY = 'day'; const DAY = "day";
const HOUR = 'hour'; const HOUR = "hour";
const MINUTE = 'minute'; const MINUTE = "minute";
const SECOND = 'second'; const SECOND = "second";
const MILLISECOND = 'millisecond'; const MILLISECOND = "millisecond";
export default { export default {
parse(date, date_separator = '-', time_separator = /[.:]/) { 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 = /[.:]/) {
if (date instanceof Date) { if (date instanceof Date) {
return date; return date;
} }
if (typeof date === 'string') { if (typeof date === "string") {
let date_parts, time_parts; let date_parts, time_parts;
const parts = date.split(' '); const parts = date.split(" ");
date_parts = parts[0] date_parts = parts[0]
.split(date_separator) .split(date_separator)
@ -27,7 +49,7 @@ export default {
if (time_parts && time_parts.length) { if (time_parts && time_parts.length) {
if (time_parts.length == 4) { if (time_parts.length == 4) {
time_parts[3] = '0.' + time_parts[3]; time_parts[3] = "0." + time_parts[3];
time_parts[3] = parseFloat(time_parts[3]) * 1000; time_parts[3] = parseFloat(time_parts[3]) * 1000;
} }
vals = vals.concat(time_parts); vals = vals.concat(time_parts);
@ -39,7 +61,7 @@ export default {
to_string(date, with_time = false) { to_string(date, with_time = false) {
if (!(date instanceof Date)) { if (!(date instanceof Date)) {
throw new TypeError('Invalid argument type'); throw new TypeError("Invalid argument type");
} }
const vals = this.get_date_values(date).map((val, i) => { const vals = this.get_date_values(date).map((val, i) => {
if (i === 1) { if (i === 1) {
@ -48,26 +70,26 @@ export default {
} }
if (i === 6) { if (i === 6) {
return padStart(val + '', 3, '0'); return padStart(val + "", 3, "0");
} }
return padStart(val + '', 2, '0'); return padStart(val + "", 2, "0");
}); });
const date_string = `${vals[0]}-${vals[1]}-${vals[2]}`; 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]}.${vals[6]}`;
return date_string + (with_time ? ' ' + time_string : ''); return date_string + (with_time ? " " + time_string : "");
}, },
format(date, format_string = 'YYYY-MM-DD HH:mm:ss.SSS', lang = 'en') { format(date, format_string = "YYYY-MM-DD HH:mm:ss.SSS", lang = "en") {
const dateTimeFormat = new Intl.DateTimeFormat(lang, { const dateTimeFormat = new Intl.DateTimeFormat(lang, {
month: 'long' month: "long",
}); });
const month_name = dateTimeFormat.format(date); const month_name = dateTimeFormat.format(date);
const month_name_capitalized = const month_name_capitalized =
month_name.charAt(0).toUpperCase() + month_name.slice(1); month_name.charAt(0).toUpperCase() + month_name.slice(1);
const values = this.get_date_values(date).map(d => padStart(d, 2, 0)); const values = this.get_date_values(date).map((d) => padStart(d, 2, 0));
const format_map = { const format_map = {
YYYY: values[0], YYYY: values[0],
MM: padStart(+values[1] + 1, 2, 0), MM: padStart(+values[1] + 1, 2, 0),
@ -111,8 +133,8 @@ export default {
months = days / 30; months = days / 30;
years = months / 12; years = months / 12;
if (!scale.endsWith('s')) { if (!scale.endsWith("s")) {
scale += 's'; scale += "s";
} }
return Math.floor( return Math.floor(
@ -124,7 +146,7 @@ export default {
days, days,
months, months,
years, years,
}[scale] }[scale],
); );
}, },
@ -216,9 +238,9 @@ export default {
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/padStart // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/padStart
function padStart(str, targetLength, padString) { function padStart(str, targetLength, padString) {
str = str + ''; str = str + "";
targetLength = targetLength >> 0; targetLength = targetLength >> 0;
padString = String(typeof padString !== 'undefined' ? padString : ' '); padString = String(typeof padString !== "undefined" ? padString : " ");
if (str.length > targetLength) { if (str.length > targetLength) {
return String(str); return String(str);
} else { } else {

View File

@ -1,7 +1,7 @@
@import "./dark.scss"; @import "./dark.scss";
$bar-color: #b8c2cc !default; $bar-color: #b8c2cc !default;
$bar-stroke: #8D99A6 !default; $bar-stroke: #8d99a6 !default;
$border-color: #e0e0e0 !default; $border-color: #e0e0e0 !default;
$light-bg: #f5f5f5 !default; $light-bg: #f5f5f5 !default;
$light-border-color: #ebeff2 !default; $light-border-color: #ebeff2 !default;
@ -71,7 +71,7 @@ $light-blue: #c4c4e9 !default;
fill: $bar-color; fill: $bar-color;
stroke: $bar-stroke; stroke: $bar-stroke;
stroke-width: 0; stroke-width: 0;
transition: stroke-width .3s ease; transition: stroke-width 0.3s ease;
} }
.bar-progress { .bar-progress {
fill: $blue; fill: $blue;
@ -107,7 +107,7 @@ $light-blue: #c4c4e9 !default;
cursor: ew-resize; cursor: ew-resize;
opacity: 0; opacity: 0;
visibility: hidden; visibility: hidden;
transition: opacity .3s ease; transition: opacity 0.3s ease;
} }
.bar-wrapper { .bar-wrapper {
@ -140,7 +140,8 @@ $light-blue: #c4c4e9 !default;
} }
} }
.lower-text, .upper-text { .lower-text,
.upper-text {
font-size: 12px; font-size: 12px;
text-anchor: middle; text-anchor: middle;
} }

View File

@ -1,18 +1,29 @@
import date_utils from './date_utils'; import date_utils from "./date_utils";
import { $, createSVG } from './svg_utils'; import { $, createSVG } from "./svg_utils";
import Bar from './bar'; import Bar from "./bar";
import Arrow from './arrow'; import Arrow from "./arrow";
import Popup from './popup'; import Popup from "./popup";
import './gantt.scss'; import "./gantt.scss";
const VIEW_MODE = { const VIEW_MODE = {
QUARTER_DAY: 'Quarter Day', HOUR: "Hour",
HALF_DAY: 'Half Day', QUARTER_DAY: "Quarter Day",
DAY: 'Day', HALF_DAY: "Half Day",
WEEK: 'Week', DAY: "Day",
MONTH: 'Month', WEEK: "Week",
YEAR: 'Year', MONTH: "Month",
YEAR: "Year",
};
const VIEW_MODE_PADDING = {
HOUR: ["7d", "7d"],
QUARTER_DAY: ["7d", "7d"],
HALF_DAY: ["7d", "7d"],
DAY: ["1m", "1m"],
WEEK: ["1m", "1m"],
MONTH: ["1m", "1m"],
YEAR: ["2y", "2y"],
}; };
export default class Gantt { export default class Gantt {
@ -29,46 +40,46 @@ export default class Gantt {
let svg_element, wrapper_element; let svg_element, wrapper_element;
// CSS Selector is passed // CSS Selector is passed
if (typeof element === 'string') { if (typeof element === "string") {
element = document.querySelector(element); element = document.querySelector(element);
} }
// get the SVGElement // get the SVGElement
if (element instanceof HTMLElement) { if (element instanceof HTMLElement) {
wrapper_element = element; wrapper_element = element;
svg_element = element.querySelector('svg'); svg_element = element.querySelector("svg");
} else if (element instanceof SVGElement) { } else if (element instanceof SVGElement) {
svg_element = element; svg_element = element;
} else { } else {
throw new TypeError( throw new TypeError(
'Frappé Gantt only supports usage of a string CSS selector,' + "Frappé Gantt only supports usage of a string CSS selector," +
" HTML DOM element or SVG DOM element for the 'element' parameter" " HTML DOM element or SVG DOM element for the 'element' parameter",
); );
} }
// svg element // svg element
if (!svg_element) { if (!svg_element) {
// create it // create it
this.$svg = createSVG('svg', { this.$svg = createSVG("svg", {
append_to: wrapper_element, append_to: wrapper_element,
class: 'gantt', class: "gantt",
}); });
} else { } else {
this.$svg = svg_element; this.$svg = svg_element;
this.$svg.classList.add('gantt'); this.$svg.classList.add("gantt");
} }
// wrapper element // wrapper element
this.$container = document.createElement('div'); this.$container = document.createElement("div");
this.$container.classList.add('gantt-container'); this.$container.classList.add("gantt-container");
const parent_element = this.$svg.parentElement; const parent_element = this.$svg.parentElement;
parent_element.appendChild(this.$container); parent_element.appendChild(this.$container);
this.$container.appendChild(this.$svg); this.$container.appendChild(this.$svg);
// popup wrapper // popup wrapper
this.popup_wrapper = document.createElement('div'); this.popup_wrapper = document.createElement("div");
this.popup_wrapper.classList.add('popup-wrapper'); this.popup_wrapper.classList.add("popup-wrapper");
this.$container.appendChild(this.popup_wrapper); this.$container.appendChild(this.popup_wrapper);
} }
@ -82,13 +93,24 @@ export default class Gantt {
bar_corner_radius: 3, bar_corner_radius: 3,
arrow_curve: 5, arrow_curve: 5,
padding: 18, padding: 18,
view_mode: 'Day', view_mode: "Day",
date_format: 'YYYY-MM-DD', date_format: "YYYY-MM-DD",
popup_trigger: 'click', popup_trigger: "click",
custom_popup_html: null, custom_popup_html: null,
language: 'en', language: "en",
}; };
this.options = Object.assign({}, default_options, options); this.options = Object.assign({}, default_options, options);
for (let [key, value] of Object.entries(options.view_mode_padding)) {
if (typeof value === "string") {
// Configure for single value given
options.view_mode_padding[key] = [value, value];
}
}
this.options.view_mode_padding = {
...VIEW_MODE_PADDING,
...options.view_mode_padding,
};
} }
setup_tasks(tasks) { setup_tasks(tasks) {
@ -96,10 +118,19 @@ export default class Gantt {
this.tasks = tasks.map((task, i) => { this.tasks = tasks.map((task, i) => {
// convert to Date objects // convert to Date objects
task._start = date_utils.parse(task.start); task._start = date_utils.parse(task.start);
if (task.end === undefined && task.duration !== undefined) {
task.end = task._start;
let durations = task.duration.split(" ");
durations.forEach((tmpDuration) => {
let { duration, scale } = date_utils.parse_duration(tmpDuration);
task.end = date_utils.add(task.end, duration, scale);
});
}
task._end = date_utils.parse(task.end); task._end = date_utils.parse(task.end);
// make task invalid if duration too large // make task invalid if duration too large
if (date_utils.diff(task._end, task._start, 'year') > 10) { if (date_utils.diff(task._end, task._start, "year") > 10) {
task.end = null; task.end = null;
} }
@ -110,22 +141,22 @@ export default class Gantt {
if (!task.start && !task.end) { if (!task.start && !task.end) {
const today = date_utils.today(); const today = date_utils.today();
task._start = today; task._start = today;
task._end = date_utils.add(today, 2, 'day'); task._end = date_utils.add(today, 2, "day");
} }
if (!task.start && task.end) { if (!task.start && task.end) {
task._start = date_utils.add(task._end, -2, 'day'); task._start = date_utils.add(task._end, -2, "day");
} }
if (task.start && !task.end) { if (task.start && !task.end) {
task._end = date_utils.add(task._start, 2, 'day'); task._end = date_utils.add(task._start, 2, "day");
} }
// if hours is not set, assume the last day is full day // if hours is not set, assume the last day is full day
// e.g: 2018-09-09 becomes 2018-09-09 23:59:59 // e.g: 2018-09-09 becomes 2018-09-09 23:59:59
const task_end_values = date_utils.get_date_values(task._end); const task_end_values = date_utils.get_date_values(task._end);
if (task_end_values.slice(3).every((d) => d === 0)) { if (task_end_values.slice(3).every((d) => d === 0)) {
task._end = date_utils.add(task._end, 24, 'hour'); task._end = date_utils.add(task._end, 24, "hour");
} }
// invalid flag // invalid flag
@ -134,11 +165,11 @@ export default class Gantt {
} }
// dependencies // dependencies
if (typeof task.dependencies === 'string' || !task.dependencies) { if (typeof task.dependencies === "string" || !task.dependencies) {
let deps = []; let deps = [];
if (task.dependencies) { if (task.dependencies) {
deps = task.dependencies deps = task.dependencies
.split(',') .split(",")
.map((d) => d.trim()) .map((d) => d.trim())
.filter((d) => d); .filter((d) => d);
} }
@ -176,13 +207,15 @@ export default class Gantt {
this.setup_dates(); this.setup_dates();
this.render(); this.render();
// fire viewmode_change event // fire viewmode_change event
this.trigger_event('view_change', [mode]); this.trigger_event("view_change", [mode]);
} }
update_view_scale(view_mode) { update_view_scale(view_mode) {
this.options.view_mode = view_mode; this.options.view_mode = view_mode;
if (view_mode === VIEW_MODE.HOUR) {
if (view_mode === VIEW_MODE.DAY) { this.options.step = 24 / 24;
this.options.column_width = 38;
} else if (view_mode === VIEW_MODE.DAY) {
this.options.step = 24; this.options.step = 24;
this.options.column_width = 38; this.options.column_width = 38;
} else if (view_mode === VIEW_MODE.HALF_DAY) { } else if (view_mode === VIEW_MODE.HALF_DAY) {
@ -220,25 +253,54 @@ export default class Gantt {
this.gantt_end = task._end; this.gantt_end = task._end;
} }
} }
let gantt_start, gantt_end;
this.gantt_start = date_utils.start_of(this.gantt_start, 'day'); if (!this.gantt_start) gantt_start = new Date();
this.gantt_end = date_utils.start_of(this.gantt_end, 'day'); else gantt_start = date_utils.start_of(this.gantt_start, "day");
if (!this.gantt_end) gantt_end = new Date();
else gantt_end = date_utils.start_of(this.gantt_end, "day");
// add date padding on both sides // add date padding on both sides
if (this.view_is([VIEW_MODE.QUARTER_DAY, VIEW_MODE.HALF_DAY])) { let viewKey;
this.gantt_start = date_utils.add(this.gantt_start, -7, 'day'); for (let [key, value] of Object.entries(VIEW_MODE)) {
this.gantt_end = date_utils.add(this.gantt_end, 7, 'day'); if (value === this.options.view_mode) {
} else if (this.view_is(VIEW_MODE.MONTH)) { viewKey = key;
this.gantt_start = date_utils.start_of(this.gantt_start, 'year');
this.gantt_end = date_utils.add(this.gantt_end, 1, 'year');
} else if (this.view_is(VIEW_MODE.YEAR)) {
this.gantt_start = date_utils.add(this.gantt_start, -2, 'year');
this.gantt_end = date_utils.add(this.gantt_end, 2, 'year');
} else {
this.gantt_start = date_utils.add(this.gantt_start, -1, 'month');
this.gantt_end = date_utils.add(this.gantt_end, 1, 'month');
} }
} }
const [padding_start, padding_end] = this.options.view_mode_padding[
viewKey
].map(date_utils.parse_duration);
this.gantt_start = date_utils.add(
gantt_start,
-padding_start.duration,
padding_start.scale,
);
let format_string;
if (this.view_is(VIEW_MODE.YEAR)) {
format_string = "YYYY"
} else if (this.view_is(VIEW_MODE.MONTH)) {
format_string = "YYYY-MM"
} else if (this.view_is(VIEW_MODE.DAY)) {
format_string = "YYYY-MM-DD"
} else {
format_string = "YYYY-MM-DD HH"
}
this.gantt_start = new Date(
date_utils.format(
date_utils.add(gantt_start, -padding_end.duration, padding_end.scale),
format_string
)
);
this.gantt_start.setHours(0, 0, 0, 0)
this.gantt_end = date_utils.add(
gantt_end,
padding_end.duration,
padding_end.scale,
);
}
setup_date_values() { setup_date_values() {
this.dates = []; this.dates = [];
@ -249,15 +311,11 @@ export default class Gantt {
cur_date = date_utils.clone(this.gantt_start); cur_date = date_utils.clone(this.gantt_start);
} else { } else {
if (this.view_is(VIEW_MODE.YEAR)) { if (this.view_is(VIEW_MODE.YEAR)) {
cur_date = date_utils.add(cur_date, 1, 'year'); cur_date = date_utils.add(cur_date, 1, "year");
} else if (this.view_is(VIEW_MODE.MONTH)) { } else if (this.view_is(VIEW_MODE.MONTH)) {
cur_date = date_utils.add(cur_date, 1, 'month'); cur_date = date_utils.add(cur_date, 1, "month");
} else { } else {
cur_date = date_utils.add( cur_date = date_utils.add(cur_date, this.options.step, "hour");
cur_date,
this.options.step,
'hour'
);
} }
} }
this.dates.push(cur_date); this.dates.push(cur_date);
@ -283,10 +341,10 @@ export default class Gantt {
setup_layers() { setup_layers() {
this.layers = {}; this.layers = {};
const layers = ['grid', 'date', 'arrow', 'progress', 'bar', 'details']; const layers = ["grid", "date", "arrow", "progress", "bar", "details"];
// make group layers // make group layers
for (let layer of layers) { for (let layer of layers) {
this.layers[layer] = createSVG('g', { this.layers[layer] = createSVG("g", {
class: layer, class: layer,
append_to: this.$svg, append_to: this.$svg,
}); });
@ -306,27 +364,26 @@ export default class Gantt {
const grid_height = const grid_height =
this.options.header_height + this.options.header_height +
this.options.padding + this.options.padding +
(this.options.bar_height + this.options.padding) * (this.options.bar_height + this.options.padding) * this.tasks.length;
this.tasks.length;
createSVG('rect', { createSVG("rect", {
x: 0, x: 0,
y: 0, y: 0,
width: grid_width, width: grid_width,
height: grid_height, height: grid_height,
class: 'grid-background', class: "grid-background",
append_to: this.layers.grid, append_to: this.layers.grid,
}); });
$.attr(this.$svg, { $.attr(this.$svg, {
height: grid_height + this.options.padding + 100, height: grid_height + this.options.padding + 100,
width: '100%', width: "100%",
}); });
} }
make_grid_rows() { make_grid_rows() {
const rows_layer = createSVG('g', { append_to: this.layers.grid }); const rows_layer = createSVG("g", { append_to: this.layers.grid });
const lines_layer = createSVG('g', { append_to: this.layers.grid }); const lines_layer = createSVG("g", { append_to: this.layers.grid });
const row_width = this.dates.length * this.options.column_width; const row_width = this.dates.length * this.options.column_width;
const row_height = this.options.bar_height + this.options.padding; const row_height = this.options.bar_height + this.options.padding;
@ -334,21 +391,21 @@ export default class Gantt {
let row_y = this.options.header_height + this.options.padding / 2; let row_y = this.options.header_height + this.options.padding / 2;
for (let task of this.tasks) { for (let task of this.tasks) {
createSVG('rect', { createSVG("rect", {
x: 0, x: 0,
y: row_y, y: row_y,
width: row_width, width: row_width,
height: row_height, height: row_height,
class: 'grid-row', class: "grid-row",
append_to: rows_layer, append_to: rows_layer,
}); });
createSVG('line', { createSVG("line", {
x1: 0, x1: 0,
y1: row_y + row_height, y1: row_y + row_height,
x2: row_width, x2: row_width,
y2: row_y + row_height, y2: row_y + row_height,
class: 'row-line', class: "row-line",
append_to: lines_layer, append_to: lines_layer,
}); });
@ -359,12 +416,12 @@ export default class Gantt {
make_grid_header() { make_grid_header() {
const header_width = this.dates.length * this.options.column_width; const header_width = this.dates.length * this.options.column_width;
const header_height = this.options.header_height + 10; const header_height = this.options.header_height + 10;
createSVG('rect', { createSVG("rect", {
x: 0, x: 0,
y: 0, y: 0,
width: header_width, width: header_width,
height: header_height, height: header_height,
class: 'grid-header', class: "grid-header",
append_to: this.layers.grid, append_to: this.layers.grid,
}); });
} }
@ -373,14 +430,13 @@ export default class Gantt {
let tick_x = 0; let tick_x = 0;
let tick_y = this.options.header_height + this.options.padding / 2; let tick_y = this.options.header_height + this.options.padding / 2;
let tick_height = let tick_height =
(this.options.bar_height + this.options.padding) * (this.options.bar_height + this.options.padding) * this.tasks.length;
this.tasks.length;
for (let date of this.dates) { for (let date of this.dates) {
let tick_class = 'tick'; let tick_class = "tick";
// thick tick for monday // thick tick for monday
if (this.view_is(VIEW_MODE.DAY) && date.getDate() === 1) { if (this.view_is(VIEW_MODE.DAY) && date.getDate() === 1) {
tick_class += ' thick'; tick_class += " thick";
} }
// thick tick for first week // thick tick for first week
if ( if (
@ -388,14 +444,14 @@ export default class Gantt {
date.getDate() >= 1 && date.getDate() >= 1 &&
date.getDate() < 8 date.getDate() < 8
) { ) {
tick_class += ' thick'; tick_class += " thick";
} }
// thick ticks for quarters // thick ticks for quarters
if (this.view_is(VIEW_MODE.MONTH) && date.getMonth() % 3 === 0) { if (this.view_is(VIEW_MODE.MONTH) && date.getMonth() % 3 === 0) {
tick_class += ' thick'; tick_class += " thick";
} }
createSVG('path', { createSVG("path", {
d: `M ${tick_x} ${tick_y} v ${tick_height}`, d: `M ${tick_x} ${tick_y} v ${tick_height}`,
class: tick_class, class: tick_class,
append_to: this.layers.grid, append_to: this.layers.grid,
@ -403,9 +459,7 @@ export default class Gantt {
if (this.view_is(VIEW_MODE.MONTH)) { if (this.view_is(VIEW_MODE.MONTH)) {
tick_x += tick_x +=
(date_utils.get_days_in_month(date) * (date_utils.get_days_in_month(date) * this.options.column_width) / 30;
this.options.column_width) /
30;
} else { } else {
tick_x += this.options.column_width; tick_x += this.options.column_width;
} }
@ -417,9 +471,11 @@ export default class Gantt {
let xDist = 0; let xDist = 0;
if (this.view_is(VIEW_MODE.DAY)) { if (this.view_is(VIEW_MODE.DAY)) {
return (date_utils.diff(date_utils.today(), this.gantt_start, 'hour') / return (
(date_utils.diff(date_utils.today(), this.gantt_start, "hour") /
this.options.step) * this.options.step) *
this.options.column_width; this.options.column_width
);
} }
for (let date of this.dates) { for (let date of this.dates) {
@ -448,33 +504,36 @@ export default class Gantt {
make_grid_highlights() { make_grid_highlights() {
// highlight today's | week's | month's | year's // highlight today's | week's | month's | year's
if (this.view_is(VIEW_MODE.DAY) || this.view_is(VIEW_MODE.WEEK) || this.view_is(VIEW_MODE.MONTH) || this.view_is(VIEW_MODE.YEAR)) { if (
this.view_is(VIEW_MODE.DAY) ||
this.view_is(VIEW_MODE.WEEK) ||
this.view_is(VIEW_MODE.MONTH) ||
this.view_is(VIEW_MODE.YEAR)
) {
const x = this.computeGridHighlightDimensions(this.options.view_mode); const x = this.computeGridHighlightDimensions(this.options.view_mode);
const y = 0; const y = 0;
const width = this.options.column_width; const width = this.options.column_width;
const height = const height =
(this.options.bar_height + this.options.padding) * (this.options.bar_height + this.options.padding) * this.tasks.length +
this.tasks.length +
this.options.header_height + this.options.header_height +
this.options.padding / 2; this.options.padding / 2;
let className = ''; let className = "";
switch (this.options.view_mode) { switch (this.options.view_mode) {
case VIEW_MODE.DAY: case VIEW_MODE.DAY:
className = 'today-highlight' className = "today-highlight";
break; break;
case VIEW_MODE.WEEK: case VIEW_MODE.WEEK:
className = 'week-highlight' className = "week-highlight";
break; break;
case VIEW_MODE.MONTH: case VIEW_MODE.MONTH:
className = 'month-highlight' className = "month-highlight";
break; break;
case VIEW_MODE.YEAR: case VIEW_MODE.YEAR:
className = 'year-highlight' className = "year-highlight";
break; break;
} }
createSVG('rect', { createSVG("rect", {
x, x,
y, y,
width, width,
@ -487,27 +546,25 @@ export default class Gantt {
make_dates() { make_dates() {
for (let date of this.get_dates_to_draw()) { for (let date of this.get_dates_to_draw()) {
createSVG('text', { createSVG("text", {
x: date.lower_x, x: date.lower_x,
y: date.lower_y, y: date.lower_y,
innerHTML: date.lower_text, innerHTML: date.lower_text,
class: 'lower-text', class: "lower-text",
append_to: this.layers.date, append_to: this.layers.date,
}); });
if (date.upper_text) { if (date.upper_text) {
const $upper_text = createSVG('text', { const $upper_text = createSVG("text", {
x: date.upper_x, x: date.upper_x,
y: date.upper_y, y: date.upper_y,
innerHTML: date.upper_text, innerHTML: date.upper_text,
class: 'upper-text', class: "upper-text",
append_to: this.layers.date, append_to: this.layers.date,
}); });
// remove out-of-bound dates // remove out-of-bound dates
if ( if ($upper_text.getBBox().x2 > this.layers.grid.getBBox().width) {
$upper_text.getBBox().x2 > this.layers.grid.getBBox().width
) {
$upper_text.remove(); $upper_text.remove();
} }
} }
@ -526,59 +583,52 @@ export default class Gantt {
get_date_info(date, last_date, i) { get_date_info(date, last_date, i) {
if (!last_date) { if (!last_date) {
last_date = date_utils.add(date, 1, 'day'); last_date = date_utils.add(date, 1, "day");
} }
const date_text = { const date_text = {
'Quarter Day_lower': date_utils.format( Hour_lower: date_utils.format(date, "HH", this.options.language),
date, "Quarter Day_lower": date_utils.format(date, "HH", this.options.language),
'HH', "Half Day_lower": date_utils.format(date, "HH", this.options.language),
this.options.language
),
'Half Day_lower': date_utils.format(
date,
'HH',
this.options.language
),
Day_lower: Day_lower:
date.getDate() !== last_date.getDate() date.getDate() !== last_date.getDate()
? date_utils.format(date, 'D', this.options.language) ? date_utils.format(date, "D", this.options.language)
: '', : "",
Week_lower: Week_lower:
date.getMonth() !== last_date.getMonth() date.getMonth() !== last_date.getMonth()
? date_utils.format(date, 'D MMM', this.options.language) ? date_utils.format(date, "D MMM", this.options.language)
: date_utils.format(date, 'D', this.options.language), : date_utils.format(date, "D", this.options.language),
Month_lower: date_utils.format(date, 'MMMM', this.options.language), Month_lower: date_utils.format(date, "MMMM", this.options.language),
Year_lower: date_utils.format(date, 'YYYY', this.options.language), Year_lower: date_utils.format(date, "YYYY", this.options.language),
'Quarter Day_upper': Hour_upper:
date.getDate() !== last_date.getDate() date.getDate() !== last_date.getDate()
? date_utils.format(date, 'D MMM', this.options.language) ? date_utils.format(date, "D MMMM", this.options.language)
: '', : "",
'Half Day_upper': "Quarter Day_upper":
date.getDate() !== last_date.getDate()
? date_utils.format(date, "D MMM", this.options.language)
: "",
"Half Day_upper":
date.getDate() !== last_date.getDate() date.getDate() !== last_date.getDate()
? date.getMonth() !== last_date.getMonth() ? date.getMonth() !== last_date.getMonth()
? date_utils.format( ? date_utils.format(date, "D MMM", this.options.language)
date, : date_utils.format(date, "D", this.options.language)
'D MMM', : "",
this.options.language
)
: date_utils.format(date, 'D', this.options.language)
: '',
Day_upper: Day_upper:
date.getMonth() !== last_date.getMonth() date.getMonth() !== last_date.getMonth()
? date_utils.format(date, 'MMMM', this.options.language) ? date_utils.format(date, "MMMM", this.options.language)
: '', : "",
Week_upper: Week_upper:
date.getMonth() !== last_date.getMonth() date.getMonth() !== last_date.getMonth()
? date_utils.format(date, 'MMMM', this.options.language) ? date_utils.format(date, "MMMM", this.options.language)
: '', : "",
Month_upper: Month_upper:
date.getFullYear() !== last_date.getFullYear() date.getFullYear() !== last_date.getFullYear()
? date_utils.format(date, 'YYYY', this.options.language) ? date_utils.format(date, "YYYY", this.options.language)
: '', : "",
Year_upper: Year_upper:
date.getFullYear() !== last_date.getFullYear() date.getFullYear() !== last_date.getFullYear()
? date_utils.format(date, 'YYYY', this.options.language) ? date_utils.format(date, "YYYY", this.options.language)
: '', : "",
}; };
const base_pos = { const base_pos = {
@ -588,10 +638,12 @@ export default class Gantt {
}; };
const x_pos = { const x_pos = {
'Quarter Day_lower': this.options.column_width / 2, Hour_lower: this.options.column_width / 2,
'Quarter Day_upper': this.options.column_width * 2, Hour_upper: this.options.column_width * 12,
'Half Day_lower': this.options.column_width / 2, "Quarter Day_lower": this.options.column_width / 2,
'Half Day_upper': this.options.column_width, "Quarter Day_upper": this.options.column_width * 2,
"Half Day_lower": this.options.column_width / 2,
"Half Day_upper": this.options.column_width,
Day_lower: this.options.column_width / 2, Day_lower: this.options.column_width / 2,
Day_upper: (this.options.column_width * 30) / 2, Day_upper: (this.options.column_width * 30) / 2,
Week_lower: 0, Week_lower: 0,
@ -631,7 +683,7 @@ export default class Gantt {
const arrow = new Arrow( const arrow = new Arrow(
this, this,
this.bars[dependency._index], // from_task this.bars[dependency._index], // from_task
this.bars[task._index] // to_task this.bars[task._index], // to_task
); );
this.layers.arrow.appendChild(arrow.element); this.layers.arrow.appendChild(arrow.element);
return arrow; return arrow;
@ -654,11 +706,11 @@ export default class Gantt {
set_width() { set_width() {
const cur_width = this.$svg.getBoundingClientRect().width; const cur_width = this.$svg.getBoundingClientRect().width;
const actual_width = this.$svg const actual_width = this.$svg.querySelector('.grid .grid-row') ? this.$svg
.querySelector('.grid .grid-row') .querySelector('.grid .grid-row')
.getAttribute('width'); .getAttribute('width') : 0;
if (cur_width < actual_width) { if (cur_width < actual_width) {
this.$svg.setAttribute('width', actual_width); this.$svg.setAttribute("width", actual_width);
} }
} }
@ -669,7 +721,7 @@ export default class Gantt {
const hours_before_first_task = date_utils.diff( const hours_before_first_task = date_utils.diff(
this.get_oldest_starting_date(), this.get_oldest_starting_date(),
this.gantt_start, this.gantt_start,
'hour' "hour",
); );
const scroll_pos = const scroll_pos =
@ -684,11 +736,11 @@ export default class Gantt {
$.on( $.on(
this.$svg, this.$svg,
this.options.popup_trigger, this.options.popup_trigger,
'.grid-row, .grid-header', ".grid-row, .grid-header",
() => { () => {
this.unselect_all(); this.unselect_all();
this.hide_popup(); this.hide_popup();
} },
); );
} }
@ -706,23 +758,23 @@ export default class Gantt {
return is_dragging || is_resizing_left || is_resizing_right; return is_dragging || is_resizing_left || is_resizing_right;
} }
$.on(this.$svg, 'mousedown', '.bar-wrapper, .handle', (e, element) => { $.on(this.$svg, "mousedown", ".bar-wrapper, .handle", (e, element) => {
const bar_wrapper = $.closest('.bar-wrapper', element); const bar_wrapper = $.closest(".bar-wrapper", element);
if (element.classList.contains('left')) { if (element.classList.contains("left")) {
is_resizing_left = true; is_resizing_left = true;
} else if (element.classList.contains('right')) { } else if (element.classList.contains("right")) {
is_resizing_right = true; is_resizing_right = true;
} else if (element.classList.contains('bar-wrapper')) { } else if (element.classList.contains("bar-wrapper")) {
is_dragging = true; is_dragging = true;
} }
bar_wrapper.classList.add('active'); bar_wrapper.classList.add("active");
x_on_start = e.offsetX; x_on_start = e.offsetX;
y_on_start = e.offsetY; y_on_start = e.offsetY;
parent_bar_id = bar_wrapper.getAttribute('data-id'); parent_bar_id = bar_wrapper.getAttribute("data-id");
const ids = [ const ids = [
parent_bar_id, parent_bar_id,
...this.get_all_dependent_tasks(parent_bar_id), ...this.get_all_dependent_tasks(parent_bar_id),
@ -740,7 +792,7 @@ export default class Gantt {
}); });
}); });
$.on(this.$svg, 'mousemove', (e) => { $.on(this.$svg, "mousemove", (e) => {
if (!action_in_progress()) return; if (!action_in_progress()) return;
const dx = e.offsetX - x_on_start; const dx = e.offsetX - x_on_start;
const dy = e.offsetY - y_on_start; const dy = e.offsetY - y_on_start;
@ -772,9 +824,9 @@ export default class Gantt {
}); });
}); });
document.addEventListener('mouseup', (e) => { document.addEventListener("mouseup", (e) => {
if (is_dragging || is_resizing_left || is_resizing_right) { if (is_dragging || is_resizing_left || is_resizing_right) {
bars.forEach((bar) => bar.group.classList.remove('active')); bars.forEach((bar) => bar.group.classList.remove("active"));
} }
is_dragging = false; is_dragging = false;
@ -782,7 +834,7 @@ export default class Gantt {
is_resizing_right = false; is_resizing_right = false;
}); });
$.on(this.$svg, 'mouseup', (e) => { $.on(this.$svg, "mouseup", (e) => {
this.bar_being_dragged = null; this.bar_being_dragged = null;
bars.forEach((bar) => { bars.forEach((bar) => {
const $bar = bar.$bar; const $bar = bar.$bar;
@ -803,13 +855,13 @@ export default class Gantt {
let $bar_progress = null; let $bar_progress = null;
let $bar = null; let $bar = null;
$.on(this.$svg, 'mousedown', '.handle.progress', (e, handle) => { $.on(this.$svg, "mousedown", ".handle.progress", (e, handle) => {
is_resizing = true; is_resizing = true;
x_on_start = e.offsetX; x_on_start = e.offsetX;
y_on_start = e.offsetY; y_on_start = e.offsetY;
const $bar_wrapper = $.closest('.bar-wrapper', handle); const $bar_wrapper = $.closest(".bar-wrapper", handle);
const id = $bar_wrapper.getAttribute('data-id'); const id = $bar_wrapper.getAttribute("data-id");
bar = this.get_bar(id); bar = this.get_bar(id);
$bar_progress = bar.$bar_progress; $bar_progress = bar.$bar_progress;
@ -821,7 +873,7 @@ export default class Gantt {
$bar_progress.max_dx = $bar.getWidth() - $bar_progress.getWidth(); $bar_progress.max_dx = $bar.getWidth() - $bar_progress.getWidth();
}); });
$.on(this.$svg, 'mousemove', (e) => { $.on(this.$svg, "mousemove", (e) => {
if (!is_resizing) return; if (!is_resizing) return;
let dx = e.offsetX - x_on_start; let dx = e.offsetX - x_on_start;
let dy = e.offsetY - y_on_start; let dy = e.offsetY - y_on_start;
@ -834,12 +886,12 @@ export default class Gantt {
} }
const $handle = bar.$handle_progress; const $handle = bar.$handle_progress;
$.attr($bar_progress, 'width', $bar_progress.owidth + dx); $.attr($bar_progress, "width", $bar_progress.owidth + dx);
$.attr($handle, 'points', bar.get_progress_polygon_points()); $.attr($handle, "points", bar.get_progress_polygon_points());
$bar_progress.finaldx = dx; $bar_progress.finaldx = dx;
}); });
$.on(this.$svg, 'mouseup', () => { $.on(this.$svg, "mouseup", () => {
is_resizing = false; is_resizing = false;
if (!($bar_progress && $bar_progress.finaldx)) return; if (!($bar_progress && $bar_progress.finaldx)) return;
bar.progress_changed(); bar.progress_changed();
@ -889,21 +941,19 @@ export default class Gantt {
position = position =
odx - odx -
rem + rem +
(rem < this.options.column_width / 2 (rem < this.options.column_width / 2 ? 0 : this.options.column_width);
? 0
: this.options.column_width);
} }
return position; return position;
} }
unselect_all() { unselect_all() {
[...this.$svg.querySelectorAll('.bar-wrapper')].forEach((el) => { [...this.$svg.querySelectorAll(".bar-wrapper")].forEach((el) => {
el.classList.remove('active'); el.classList.remove("active");
}); });
} }
view_is(modes) { view_is(modes) {
if (typeof modes === 'string') { if (typeof modes === "string") {
return this.options.view_mode === modes; return this.options.view_mode === modes;
} }
@ -930,7 +980,7 @@ export default class Gantt {
if (!this.popup) { if (!this.popup) {
this.popup = new Popup( this.popup = new Popup(
this.popup_wrapper, this.popup_wrapper,
this.options.custom_popup_html this.options.custom_popup_html,
); );
} }
this.popup.show(options); this.popup.show(options);
@ -941,8 +991,8 @@ export default class Gantt {
} }
trigger_event(event, args) { trigger_event(event, args) {
if (this.options['on_' + event]) { if (this.options["on_" + event]) {
this.options['on_' + event].apply(null, args); this.options["on_" + event].apply(null, args);
} }
} }
@ -953,10 +1003,11 @@ export default class Gantt {
* @memberof Gantt * @memberof Gantt
*/ */
get_oldest_starting_date() { get_oldest_starting_date() {
if (!this.tasks.length) return new Date()
return this.tasks return this.tasks
.map((task) => task._start) .map((task) => task._start)
.reduce((prev_date, cur_date) => .reduce((prev_date, cur_date) =>
cur_date <= prev_date ? cur_date : prev_date cur_date <= prev_date ? cur_date : prev_date,
); );
} }
@ -966,12 +1017,12 @@ export default class Gantt {
* @memberof Gantt * @memberof Gantt
*/ */
clear() { clear() {
this.$svg.innerHTML = ''; this.$svg.innerHTML = "";
} }
} }
Gantt.VIEW_MODE = VIEW_MODE; Gantt.VIEW_MODE = VIEW_MODE;
function generate_id(task) { function generate_id(task) {
return task.name + '_' + Math.random().toString(36).slice(2, 12); return task.name + "_" + Math.random().toString(36).slice(2, 12);
} }

View File

@ -14,17 +14,17 @@ export default class Popup {
this.hide(); this.hide();
this.title = this.parent.querySelector('.title'); this.title = this.parent.querySelector(".title");
this.subtitle = this.parent.querySelector('.subtitle'); this.subtitle = this.parent.querySelector(".subtitle");
this.pointer = this.parent.querySelector('.pointer'); this.pointer = this.parent.querySelector(".pointer");
} }
show(options) { show(options) {
if (!options.target_element) { if (!options.target_element) {
throw new Error('target_element is required to show popup'); throw new Error("target_element is required to show popup");
} }
if (!options.position) { if (!options.position) {
options.position = 'left'; options.position = "left";
} }
const target_element = options.target_element; const target_element = options.target_element;
@ -32,12 +32,12 @@ export default class Popup {
let html = this.custom_html(options.task); let html = this.custom_html(options.task);
html += '<div class="pointer"></div>'; html += '<div class="pointer"></div>';
this.parent.innerHTML = html; this.parent.innerHTML = html;
this.pointer = this.parent.querySelector('.pointer'); this.pointer = this.parent.querySelector(".pointer");
} else { } else {
// set data // set data
this.title.innerHTML = options.title; this.title.innerHTML = options.title;
this.subtitle.innerHTML = options.subtitle; this.subtitle.innerHTML = options.subtitle;
this.parent.style.width = this.parent.clientWidth + 'px'; this.parent.style.width = this.parent.clientWidth + "px";
} }
// set position // set position
@ -48,14 +48,14 @@ export default class Popup {
position_meta = options.target_element.getBBox(); position_meta = options.target_element.getBBox();
} }
if (options.position === 'left') { if (options.position === "left") {
this.parent.style.left = this.parent.style.left =
position_meta.x + (position_meta.width + 10) + 'px'; position_meta.x + (position_meta.width + 10) + "px";
this.parent.style.top = position_meta.y + 'px'; this.parent.style.top = position_meta.y + "px";
this.pointer.style.transform = 'rotateZ(90deg)'; this.pointer.style.transform = "rotateZ(90deg)";
this.pointer.style.left = '-7px'; this.pointer.style.left = "-7px";
this.pointer.style.top = '2px'; this.pointer.style.top = "2px";
} }
// show // show

View File

@ -1,16 +1,16 @@
export function $(expr, con) { export function $(expr, con) {
return typeof expr === 'string' return typeof expr === "string"
? (con || document).querySelector(expr) ? (con || document).querySelector(expr)
: expr || null; : expr || null;
} }
export function createSVG(tag, attrs) { export function createSVG(tag, attrs) {
const elem = document.createElementNS('http://www.w3.org/2000/svg', tag); const elem = document.createElementNS("http://www.w3.org/2000/svg", tag);
for (let attr in attrs) { for (let attr in attrs) {
if (attr === 'append_to') { if (attr === "append_to") {
const parent = attrs.append_to; const parent = attrs.append_to;
parent.appendChild(elem); parent.appendChild(elem);
} else if (attr === 'innerHTML') { } else if (attr === "innerHTML") {
elem.innerHTML = attrs.innerHTML; elem.innerHTML = attrs.innerHTML;
} else { } else {
elem.setAttribute(attr, attrs[attr]); elem.setAttribute(attr, attrs[attr]);
@ -25,9 +25,9 @@ export function animateSVG(svgElement, attr, from, to) {
if (animatedSvgElement === svgElement) { if (animatedSvgElement === svgElement) {
// triggered 2nd time programmatically // triggered 2nd time programmatically
// trigger artificial click event // trigger artificial click event
const event = document.createEvent('HTMLEvents'); const event = document.createEvent("HTMLEvents");
event.initEvent('click', true, true); event.initEvent("click", true, true);
event.eventName = 'click'; event.eventName = "click";
animatedSvgElement.dispatchEvent(event); animatedSvgElement.dispatchEvent(event);
} }
} }
@ -37,31 +37,31 @@ function getAnimationElement(
attr, attr,
from, from,
to, to,
dur = '0.4s', dur = "0.4s",
begin = '0.1s' begin = "0.1s",
) { ) {
const animEl = svgElement.querySelector('animate'); const animEl = svgElement.querySelector("animate");
if (animEl) { if (animEl) {
$.attr(animEl, { $.attr(animEl, {
attributeName: attr, attributeName: attr,
from, from,
to, to,
dur, dur,
begin: 'click + ' + begin, // artificial click begin: "click + " + begin, // artificial click
}); });
return svgElement; return svgElement;
} }
const animateElement = createSVG('animate', { const animateElement = createSVG("animate", {
attributeName: attr, attributeName: attr,
from, from,
to, to,
dur, dur,
begin, begin,
calcMode: 'spline', calcMode: "spline",
values: from + ';' + to, values: from + ";" + to,
keyTimes: '0; 1', keyTimes: "0; 1",
keySplines: cubic_bezier('ease-out'), keySplines: cubic_bezier("ease-out"),
}); });
svgElement.appendChild(animateElement); svgElement.appendChild(animateElement);
@ -70,11 +70,11 @@ function getAnimationElement(
function cubic_bezier(name) { function cubic_bezier(name) {
return { return {
ease: '.25 .1 .25 1', ease: ".25 .1 .25 1",
linear: '0 0 1 1', linear: "0 0 1 1",
'ease-in': '.42 0 1 1', "ease-in": ".42 0 1 1",
'ease-out': '0 0 .58 1', "ease-out": "0 0 .58 1",
'ease-in-out': '.42 0 .58 1', "ease-in-out": ".42 0 .58 1",
}[name]; }[name];
} }
@ -118,11 +118,11 @@ $.closest = (selector, element) => {
}; };
$.attr = (element, attr, value) => { $.attr = (element, attr, value) => {
if (!value && typeof attr === 'string') { if (!value && typeof attr === "string") {
return element.getAttribute(attr); return element.getAttribute(attr);
} }
if (typeof attr === 'object') { if (typeof attr === "object") {
for (let key in attr) { for (let key in attr) {
$.attr(element, key, attr[key]); $.attr(element, key, attr[key]);
} }

View File

@ -1,15 +1,15 @@
import date_utils from '../src/date_utils'; import date_utils from "../src/date_utils";
test('Parse: parses string date', () => { test("Parse: parses string date", () => {
const date = date_utils.parse('2017-09-09'); const date = date_utils.parse("2017-09-09");
expect(date.getDate()).toBe(9); expect(date.getDate()).toBe(9);
expect(date.getMonth()).toBe(8); expect(date.getMonth()).toBe(8);
expect(date.getFullYear()).toBe(2017); expect(date.getFullYear()).toBe(2017);
}); });
test('Parse: parses string datetime', () => { test("Parse: parses string datetime", () => {
const date = date_utils.parse('2017-08-27 16:08:34'); const date = date_utils.parse("2017-08-27 16:08:34");
expect(date.getFullYear()).toBe(2017); expect(date.getFullYear()).toBe(2017);
expect(date.getMonth()).toBe(7); expect(date.getMonth()).toBe(7);
@ -19,8 +19,8 @@ test('Parse: parses string datetime', () => {
expect(date.getSeconds()).toBe(34); expect(date.getSeconds()).toBe(34);
}); });
test('Parse: parses string datetime', () => { test("Parse: parses string datetime", () => {
const date = date_utils.parse('2016-02-29 16:08:34.3'); const date = date_utils.parse("2016-02-29 16:08:34.3");
expect(date.getFullYear()).toBe(2016); expect(date.getFullYear()).toBe(2016);
expect(date.getMonth()).toBe(1); expect(date.getMonth()).toBe(1);
@ -31,8 +31,8 @@ test('Parse: parses string datetime', () => {
expect(date.getMilliseconds()).toBe(300); expect(date.getMilliseconds()).toBe(300);
}); });
test('Parse: parses string datetime', () => { test("Parse: parses string datetime", () => {
const date = date_utils.parse('2015-07-01 00:00:59.200'); const date = date_utils.parse("2015-07-01 00:00:59.200");
expect(date.getFullYear()).toBe(2015); expect(date.getFullYear()).toBe(2015);
expect(date.getMonth()).toBe(6); expect(date.getMonth()).toBe(6);
@ -43,82 +43,82 @@ test('Parse: parses string datetime', () => {
expect(date.getMilliseconds()).toBe(200); expect(date.getMilliseconds()).toBe(200);
}); });
test('Format: converts date object to string', () => { test("Format: converts date object to string", () => {
const date = new Date('2017-09-18'); const date = new Date("2017-09-18");
expect(date_utils.to_string(date)).toBe('2017-09-18'); expect(date_utils.to_string(date)).toBe("2017-09-18");
}); });
test('Format: converts date object to string', () => { test("Format: converts date object to string", () => {
const date = new Date('2016-02-29 16:08:34.3'); 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'); expect(date_utils.to_string(date, true)).toBe("2016-02-29 16:08:34.300");
}); });
test('Format: converts date object to string', () => { test("Format: converts date object to string", () => {
const date = new Date('2016-02-29 16:08:34.3'); 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'); expect(date_utils.to_string(date, true)).toBe("2016-02-29 16:08:34.300");
}); });
test('Parse: returns Date Object as is', () => { test("Parse: returns Date Object as is", () => {
const d = new Date(); const d = new Date();
const date = date_utils.parse(d); const date = date_utils.parse(d);
expect(d).toBe(date); expect(d).toBe(date);
}); });
test('Diff: returns diff between 2 date objects', () => { test("Diff: returns diff between 2 date objects", () => {
const a = date_utils.parse('2017-09-08'); const a = date_utils.parse("2017-09-08");
const b = date_utils.parse('2017-06-07'); const b = date_utils.parse("2017-06-07");
expect(date_utils.diff(a, b, 'day')).toBe(93); expect(date_utils.diff(a, b, "day")).toBe(93);
expect(date_utils.diff(a, b, 'month')).toBe(3); expect(date_utils.diff(a, b, "month")).toBe(3);
expect(date_utils.diff(a, b, 'year')).toBe(0); expect(date_utils.diff(a, b, "year")).toBe(0);
}); });
test('StartOf', () => { test("StartOf", () => {
const date = date_utils.parse('2017-08-12 15:07:34.012'); const date = date_utils.parse("2017-08-12 15:07:34.012");
const start_of_millisecond = date_utils.start_of(date, 'millisecond'); const start_of_millisecond = date_utils.start_of(date, "millisecond");
expect(date_utils.to_string(start_of_millisecond, true)).toBe( expect(date_utils.to_string(start_of_millisecond, true)).toBe(
'2017-08-12 15:07:34.012' "2017-08-12 15:07:34.012",
); );
const start_of_second = date_utils.start_of(date, 'second'); const start_of_second = date_utils.start_of(date, "second");
expect(date_utils.to_string(start_of_second, true)).toBe( expect(date_utils.to_string(start_of_second, true)).toBe(
'2017-08-12 15:07:34.000' "2017-08-12 15:07:34.000",
); );
const start_of_minute = date_utils.start_of(date, 'minute'); const start_of_minute = date_utils.start_of(date, "minute");
expect(date_utils.to_string(start_of_minute, true)).toBe( expect(date_utils.to_string(start_of_minute, true)).toBe(
'2017-08-12 15:07:00.000' "2017-08-12 15:07:00.000",
); );
const start_of_hour = date_utils.start_of(date, 'hour'); const start_of_hour = date_utils.start_of(date, "hour");
expect(date_utils.to_string(start_of_hour, true)).toBe( expect(date_utils.to_string(start_of_hour, true)).toBe(
'2017-08-12 15:00:00.000' "2017-08-12 15:00:00.000",
); );
const start_of_day = date_utils.start_of(date, 'day'); const start_of_day = date_utils.start_of(date, "day");
expect(date_utils.to_string(start_of_day, true)).toBe( expect(date_utils.to_string(start_of_day, true)).toBe(
'2017-08-12 00:00:00.000' "2017-08-12 00:00:00.000",
); );
const start_of_month = date_utils.start_of(date, 'month'); const start_of_month = date_utils.start_of(date, "month");
expect(date_utils.to_string(start_of_month, true)).toBe( expect(date_utils.to_string(start_of_month, true)).toBe(
'2017-08-01 00:00:00.000' "2017-08-01 00:00:00.000",
); );
const start_of_year = date_utils.start_of(date, 'year'); const start_of_year = date_utils.start_of(date, "year");
expect(date_utils.to_string(start_of_year, true)).toBe( expect(date_utils.to_string(start_of_year, true)).toBe(
'2017-01-01 00:00:00.000' "2017-01-01 00:00:00.000",
); );
}); });
test('format', () => { test("format", () => {
const date = date_utils.parse('2017-08-12 15:07:23'); const date = date_utils.parse("2017-08-12 15:07:23");
expect(date_utils.format(date, 'YYYY-MM-DD')).toBe('2017-08-12'); expect(date_utils.format(date, "YYYY-MM-DD")).toBe("2017-08-12");
}); });
test('format', () => { test("format", () => {
const date = date_utils.parse('2016-02-29 16:08:34.3'); 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'); expect(date_utils.format(date)).toBe("2016-02-29 16:08:34.300");
}); });

View File

@ -2706,10 +2706,10 @@ preserve@^0.2.0:
version "0.2.0" version "0.2.0"
resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b" resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b"
prettier@^2.6.2: prettier@3.2.5:
version "2.6.2" version "3.2.5"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.6.2.tgz#e26d71a18a74c3d0f0597f55f01fb6c06c206032" resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.2.5.tgz#e52bc3090586e824964a8813b09aba6233b28368"
integrity sha512-PkUpF+qoXTqhOeWL9fu7As8LXsIUZ1WYaJiY/a7McAQzxjk82OF0tibkFXVCDImZtWxbvojFjerkiLb0/q8mew== integrity sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==
pretty-format@^22.1.0: pretty-format@^22.1.0:
version "22.1.0" version "22.1.0"