Procházet zdrojové kódy

initial Commit

master
Anoop před 2 roky
revize
2adf71ace7
30 změnil soubory, kde provedl 8579 přidání a 0 odebrání
  1. +3
    -0
      .babelrc
  2. +7
    -0
      .eslintrc
  3. +31
    -0
      .gitignore
  4. +8
    -0
      .idea/.gitignore
  5. +49
    -0
      .idea/deployment.xml
  6. +8
    -0
      .idea/gantt.iml
  7. +6
    -0
      .idea/inspectionProfiles/Project_Default.xml
  8. +6
    -0
      .idea/misc.xml
  9. +8
    -0
      .idea/modules.xml
  10. +4
    -0
      .prettierrc.json
  11. +100
    -0
      README.md
  12. +133
    -0
      dist/influxframework-gantt.css
  13. +1967
    -0
      dist/influxframework-gantt.js
  14. +1
    -0
      dist/influxframework-gantt.js.map
  15. +1
    -0
      dist/influxframework-gantt.min.css
  16. +2
    -0
      dist/influxframework-gantt.min.js
  17. +1
    -0
      dist/influxframework-gantt.min.js.map
  18. +105
    -0
      index.html
  19. +9
    -0
      license.txt
  20. +52
    -0
      package.json
  21. +37
    -0
      rollup.config.js
  22. +96
    -0
      src/arrow.js
  23. +418
    -0
      src/bar.js
  24. +325
    -0
      src/date_utils.js
  25. +168
    -0
      src/gantt.scss
  26. +934
    -0
      src/index.js
  27. +69
    -0
      src/popup.js
  28. +133
    -0
      src/svg_utils.js
  29. +124
    -0
      tests/date_utils.test.js
  30. +3774
    -0
      yarn.lock

+ 3
- 0
.babelrc Zobrazit soubor

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

+ 7
- 0
.eslintrc Zobrazit soubor

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

+ 31
- 0
.gitignore Zobrazit soubor

@@ -0,0 +1,31 @@
# Logs
logs
*.log

# Runtime data
pids
*.pid
*.seed

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage

# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# node-waf configuration
.lock-wscript

# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release

# Dependency directory
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
node_modules

.DS_Store

gh-pages

+ 8
- 0
.idea/.gitignore Zobrazit soubor

@@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
# Editor-based HTTP Client requests
/httpRequests/

+ 49
- 0
.idea/deployment.xml Zobrazit soubor

@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="PublishConfigData">
<serverData>
<paths name="MAGDY SERVER 6">
<serverdata>
<mappings>
<mapping local="$PROJECT_DIR$" web="/" />
</mappings>
</serverdata>
</paths>
<paths name="MAGDY_SAPOS">
<serverdata>
<mappings>
<mapping local="$PROJECT_DIR$" web="/" />
</mappings>
</serverdata>
</paths>
<paths name="MEMBTECH-OFFLINE-POS">
<serverdata>
<mappings>
<mapping local="$PROJECT_DIR$" web="/" />
</mappings>
</serverdata>
</paths>
<paths name="MEMBTECH-STORE">
<serverdata>
<mappings>
<mapping local="$PROJECT_DIR$" web="/" />
</mappings>
</serverdata>
</paths>
<paths name="MEMBTECH_COM">
<serverdata>
<mappings>
<mapping local="$PROJECT_DIR$" web="/" />
</mappings>
</serverdata>
</paths>
<paths name="WOLF">
<serverdata>
<mappings>
<mapping local="$PROJECT_DIR$" web="/" />
</mappings>
</serverdata>
</paths>
</serverData>
</component>
</project>

+ 8
- 0
.idea/gantt.iml Zobrazit soubor

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

+ 6
- 0
.idea/inspectionProfiles/Project_Default.xml Zobrazit soubor

@@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>

+ 6
- 0
.idea/misc.xml Zobrazit soubor

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptSettings">
<option name="languageLevel" value="ES6" />
</component>
</project>

+ 8
- 0
.idea/modules.xml Zobrazit soubor

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/gantt.iml" filepath="$PROJECT_DIR$/.idea/gantt.iml" />
</modules>
</component>
</project>

+ 4
- 0
.prettierrc.json Zobrazit soubor

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

+ 100
- 0
README.md Zobrazit soubor

@@ -0,0 +1,100 @@
<div align="center">
<img src="https://github.com/xhiveframework/design/blob/master/logos/logo-2019/xhiveframework-gantt-logo.png" height="128">
<h2>XhiveFramework Gantt</h2>
<p align="center">
<p>A simple, interactive, modern gantt chart library for the web</p>
<a href="https://xhiveframework.github.io/gantt">
<b>View the demo »</b>
</a>
</p>
</div>

<p align="center">
<a href="https://xhiveframework.github.io/gantt">
<img src="https://cloud.githubusercontent.com/assets/9355208/21537921/4a38b194-cdbd-11e6-8110-e0da19678a6d.png">
</a>
</p>

### Install
```
npm install xhiveframework-gantt
```

### Usage
Include it in your HTML:
```
<script src="xhiveframework-gantt.min.js"></script>
<link rel="stylesheet" href="xhiveframework-gantt.css">
```

And start hacking:
```js
var tasks = [
{
id: 'Task 1',
name: 'Redesign website',
start: '2016-12-28',
end: '2016-12-31',
progress: 20,
dependencies: 'Task 2, Task 3',
custom_class: 'bar-milestone' // optional
},
...
]
var gantt = new Gantt("#gantt", tasks);
```

You can also pass various options to the Gantt constructor:
```js
var gantt = new Gantt("#gantt", tasks, {
header_height: 50,
column_width: 30,
step: 24,
view_modes: ['Quarter Day', 'Half Day', 'Day', 'Week', 'Month'],
bar_height: 20,
bar_corner_radius: 3,
arrow_curve: 5,
padding: 18,
view_mode: 'Day',
date_format: 'YYYY-MM-DD',
custom_popup_html: null
});
```

### Contributing
If you want to contribute enhancements or fixes:

1. Clone this repo.
2. `cd` into project directory
3. `yarn`
4. `yarn run dev`
5. Open `index.html` in your browser, make your code changes and test them.

### Publishing
If you have publishing rights (XhiveFramework Team), follow these steps to publish a new version.

Assuming the last commit (or a couple of commits) were enhancements or fixes,

1. Run `yarn build`

This will generate files in the `dist/` folder. These files need to be committed.
1. Run `yarn publish`
1. Type the new version at the prompt

Depending on the type of change, you can either bump the patch version or the minor version.
For e.g.,
```
0.5.0 -> 0.6.0 (minor version bump)
0.5.0 -> 0.5.1 (patch version bump)
```
1. Now, there will be a commit named after the version you just entered. Include the generated files in `dist/` folder as part of this commit by running the command:
```
git add dist
git commit --amend
git push origin master
```

License: MIT

------------------
Project maintained by [xhiveframework](https://github.com/xhiveframework)

+ 133
- 0
dist/influxframework-gantt.css Zobrazit soubor

@@ -0,0 +1,133 @@
.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 0.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 0.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, 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);
}

+ 1967
- 0
dist/influxframework-gantt.js
Diff nebyl zobrazen, protože je příliš veliký
Zobrazit soubor


+ 1
- 0
dist/influxframework-gantt.js.map
Diff nebyl zobrazen, protože je příliš veliký
Zobrazit soubor


+ 1
- 0
dist/influxframework-gantt.min.css Zobrazit soubor

@@ -0,0 +1 @@
.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)}

+ 2
- 0
dist/influxframework-gantt.min.js
Diff nebyl zobrazen, protože je příliš veliký
Zobrazit soubor


+ 1
- 0
dist/influxframework-gantt.min.js.map
Diff nebyl zobrazen, protože je příliš veliký
Zobrazit soubor


+ 105
- 0
index.html Zobrazit soubor

@@ -0,0 +1,105 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Simple Gantt</title>
<style>
body {
font-family: sans-serif;
background: #ccc;
}
.container {
width: 80%;
margin: 0 auto;
}
/* custom class */
.gantt .bar-milestone .bar {
fill: tomato;
}
</style>
<link rel="stylesheet" href="dist/xhiveframework-gantt.css" />
<script src="dist/xhiveframework-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 tasks = [
{
start: '2018-10-01',
end: '2018-10-08',
name: 'Redesign website',
id: "Task 0",
progress: 20
},
{
start: '2018-10-03',
end: '2018-10-06',
name: 'Write new content',
id: "Task 1",
progress: 5,
dependencies: 'Task 0'
},
{
start: '2018-10-04',
end: '2018-10-08',
name: 'Apply new styles',
id: "Task 2",
progress: 10,
dependencies: 'Task 1'
},
{
start: '2018-10-08',
end: '2018-10-09',
name: 'Review',
id: "Task 3",
progress: 5,
dependencies: 'Task 2'
},
{
start: '2018-10-08',
end: '2018-10-10',
name: 'Deploy',
id: "Task 4",
progress: 0,
dependencies: 'Task 2'
},
{
start: '2018-10-11',
end: '2018-10-11',
name: 'Go Live!',
id: "Task 5",
progress: 0,
dependencies: 'Task 4',
custom_class: 'bar-milestone'
},
{
start: '2014-01-05',
end: '2019-10-12',
name: 'Long term task',
id: "Task 6",
progress: 0
}
]
var gantt_chart = new Gantt(".gantt-target", tasks, {
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);
},
view_mode: 'Month',
language: 'en'
});
console.log(gantt_chart);
</script>
</body>
</html>

+ 9
- 0
license.txt Zobrazit soubor

@@ -0,0 +1,9 @@
The MIT License (MIT)

Copyright (c) 2016 XhiveFramework 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:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

+ 52
- 0
package.json Zobrazit soubor

@@ -0,0 +1,52 @@
{
"name": "xhiveframework-gantt",
"version": "0.6.1",
"description": "A simple, modern, interactive gantt library for the web",
"main": "src/index.js",
"scripts": {
"start": "yarn run dev",
"build": "rollup -c",
"dev": "rollup -c -w",
"test": "jest",
"test:watch": "jest --watch",
"prettier": "prettier --write \"{src/*,tests/*,rollup.config}.js\"",
"prettier-check": "prettier --check \"{src/*,tests/*,rollup.config}.js\""
},
"repository": {
"type": "git",
"url": "https://github.com/xhiveframework/gantt.git"
},
"files": [
"src",
"dist",
"README.md"
],
"keywords": [
"gantt",
"svg",
"simple gantt",
"project timeline",
"interactive gantt",
"project management"
],
"author": "Faris Ansari",
"license": "MIT",
"bugs": {
"url": "https://github.com/xhiveframework/gantt/issues"
},
"homepage": "https://github.com/xhiveframework/gantt",
"devDependencies": {
"babel-preset-env": "^1.6.1",
"eslint": "^4.17.0",
"eslint-config-prettier": "^2.9.0",
"eslint-plugin-prettier": "^2.6.0",
"jest": "^22.2.1",
"prettier": "^2.6.2",
"rollup": "^2.70.2",
"rollup-plugin-sass": "^1.2.12",
"rollup-plugin-terser": "^7.0.2"
},
"eslintIgnore": [
"dist"
]
}

+ 37
- 0
rollup.config.js Zobrazit soubor

@@ -0,0 +1,37 @@
import sass from 'rollup-plugin-sass';
import { terser } from 'rollup-plugin-terser';

const dev = {
input: 'src/index.js',
output: {
name: 'Gantt',
file: 'dist/xhiveframework-gantt.js',
sourcemap: true,
format: 'iife',
},
plugins: [
sass({
output: true,
}),
],
};
const prod = {
input: 'src/index.js',
output: {
name: 'Gantt',
file: 'dist/xhiveframework-gantt.min.js',
sourcemap: true,
format: 'iife',
},
plugins: [
sass({
output: true,
options: {
outputStyle: 'compressed',
},
}),
terser(),
],
};

export default [dev, prod];

+ 96
- 0
src/arrow.js Zobrazit soubor

@@ -0,0 +1,96 @@
import { createSVG } from './svg_utils';

export default class Arrow {
constructor(gantt, from_task, to_task) {
this.gantt = gantt;
this.from_task = from_task;
this.to_task = to_task;

this.calculate_path();
this.draw();
}

calculate_path() {
let start_x =
this.from_task.$bar.getX() + this.from_task.$bar.getWidth() / 2;

const condition = () =>
this.to_task.$bar.getX() < start_x + this.gantt.options.padding &&
start_x > this.from_task.$bar.getX() + this.gantt.options.padding;

while (condition()) {
start_x -= 10;
}

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;

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;

const from_is_below_to =
this.from_task.task._index > this.to_task.task._index;
const curve = this.gantt.options.arrow_curve;
const clockwise = from_is_below_to ? 1 : 0;
const curve_y = from_is_below_to ? -curve : curve;
const offset = from_is_below_to
? end_y + this.gantt.options.arrow_curve
: end_y - this.gantt.options.arrow_curve;

this.path = `
M ${start_x} ${start_y}
V ${offset}
a ${curve} ${curve} 0 0 ${clockwise} ${curve} ${curve_y}
L ${end_x} ${end_y}
m -5 -5
l 5 5
l -5 5`;

if (
this.to_task.$bar.getX() <
this.from_task.$bar.getX() + this.gantt.options.padding
) {
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}
H ${left}
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`;
}
}

draw() {
this.element = createSVG('path', {
d: this.path,
'data-from': this.from_task.task.id,
'data-to': this.to_task.task.id,
});
}

update() {
this.calculate_path();
this.element.setAttribute('d', this.path);
}
}

+ 418
- 0
src/bar.js Zobrazit soubor

@@ -0,0 +1,418 @@
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') /
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, 'focus ' + this.gantt.options.popup_trigger, (e) => {
if (this.action_completed) {
// just finished a move action, wait for a few seconds
return;
}

this.show_popup();
this.gantt.unselect_all();
this.group.classList.add('active');
});

$.on(this.group, 'dblclick', (e) => {
if (this.action_completed) {
// just finished a move action, wait for a few seconds
return;
}

this.gantt.trigger_event('click', [this.task]);
});
}

show_popup() {
if (this.gantt.bar_being_dragged) return;

const start_date = date_utils.format(
this.task._start,
'MMM D',
this.gantt.options.language
);
const end_date = date_utils.format(
date_utils.add(this.task._end, -1, 'second'),
'MMM D',
this.gantt.options.language
);
const subtitle = start_date + ' - ' + end_date;

this.gantt.show_popup({
target_element: this.$bar,
title: this.task.name,
subtitle: subtitle,
task: this.task,
});
}

update_bar_position({ x = null, width = null }) {
const bar = this.$bar;
if (x) {
// get all x values of parent task
const xs = this.task.dependencies.map((dep) => {
return this.gantt.get_bar(dep).$bar.getX();
});
// child task must not go before parent
const valid_x = xs.reduce((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();
}

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() {
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), 1000);
}

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,
'hour'
);
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,
'hour'
);

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() {
const { step, column_width } = this.gantt.options;
const task_start = this.task._start;
const gantt_start = this.gantt.gantt_start;

const diff = date_utils.diff(task_start, gantt_start, 'hour');
let x = (diff / step) * column_width;

if (this.gantt.view_is('Month')) {
const diff = date_utils.diff(task_start, gantt_start, 'day');
x = (diff * 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();
}
}
}

function isFunction(functionToCheck) {
var getType = {};
return (
functionToCheck &&
getType.toString.call(functionToCheck) === '[object Function]'
);
}

+ 325
- 0
src/date_utils.js Zobrazit soubor

@@ -0,0 +1,325 @@
const YEAR = 'year';
const MONTH = 'month';
const DAY = 'day';
const HOUR = 'hour';
const MINUTE = 'minute';
const SECOND = 'second';
const MILLISECOND = 'millisecond';

const month_names = {
en: [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
],
es: [
'Enero',
'Febrero',
'Marzo',
'Abril',
'Mayo',
'Junio',
'Julio',
'Agosto',
'Septiembre',
'Octubre',
'Noviembre',
'Diciembre',
],
ru: [
'Январь',
'Февраль',
'Март',
'Апрель',
'Май',
'Июнь',
'Июль',
'Август',
'Сентябрь',
'Октябрь',
'Ноябрь',
'Декабрь',
],
ptBr: [
'Janeiro',
'Fevereiro',
'Março',
'Abril',
'Maio',
'Junho',
'Julho',
'Agosto',
'Setembro',
'Outubro',
'Novembro',
'Dezembro',
],
fr: [
'Janvier',
'Février',
'Mars',
'Avril',
'Mai',
'Juin',
'Juillet',
'Août',
'Septembre',
'Octobre',
'Novembre',
'Décembre',
],
tr: [
'Ocak',
'Şubat',
'Mart',
'Nisan',
'Mayıs',
'Haziran',
'Temmuz',
'Ağustos',
'Eylül',
'Ekim',
'Kasım',
'Aralık',
],
zh: [
'一月',
'二月',
'三月',
'四月',
'五月',
'六月',
'七月',
'八月',
'九月',
'十月',
'十一月',
'十二月',
],
};

export default {
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));
time_parts = parts[1] && parts[1].split(time_separator);

// month is 0 indexed
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);
}
},

to_string(date, with_time = false) {
if (!(date instanceof Date)) {
throw new TypeError('Invalid argument type');
}
const vals = this.get_date_values(date).map((val, i) => {
if (i === 1) {
// add 1 for month
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]}`;

return date_string + (with_time ? ' ' + time_string : '');
},

format(date, format_string = 'YYYY-MM-DD HH:mm:ss.SSS', lang = 'en') {
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),
DD: values[2],
HH: values[3],
mm: values[4],
ss: values[5],
SSS: values[6],
D: values[2],
MMMM: month_names[lang][+values[1]],
MMM: month_names[lang][+values[1]],
};

let str = format_string;
const formatted_values = [];

Object.keys(format_map)
.sort((a, b) => b.length - a.length) // big string first
.forEach((key) => {
if (str.includes(key)) {
str = str.replace(key, `$${formatted_values.length}`);
formatted_values.push(format_map[key]);
}
});

formatted_values.forEach((value, i) => {
str = str.replace(`$${i}`, value);
});

return str;
},

diff(date_a, date_b, scale = DAY) {
let milliseconds, seconds, hours, minutes, days, months, years;

milliseconds = date_a - date_b;
seconds = milliseconds / 1000;
minutes = seconds / 60;
hours = minutes / 60;
days = hours / 24;
months = days / 30;
years = months / 12;

if (!scale.endsWith('s')) {
scale += 's';
}

return Math.floor(
{
milliseconds,
seconds,
minutes,
hours,
days,
months,
years,
}[scale]
);
},

today() {
const vals = this.get_date_values(new Date()).slice(0, 3);
return new Date(...vals);
},

now() {
return new Date();
},

add(date, qty, scale) {
qty = parseInt(qty, 10);
const vals = [
date.getFullYear() + (scale === YEAR ? qty : 0),
date.getMonth() + (scale === MONTH ? qty : 0),
date.getDate() + (scale === DAY ? qty : 0),
date.getHours() + (scale === HOUR ? qty : 0),
date.getMinutes() + (scale === MINUTE ? qty : 0),
date.getSeconds() + (scale === SECOND ? qty : 0),
date.getMilliseconds() + (scale === MILLISECOND ? qty : 0),
];
return new Date(...vals);
},

start_of(date, scale) {
const scores = {
[YEAR]: 6,
[MONTH]: 5,
[DAY]: 4,
[HOUR]: 3,
[MINUTE]: 2,
[SECOND]: 1,
[MILLISECOND]: 0,
};

function should_reset(_scale) {
const max_score = scores[scale];
return scores[_scale] <= max_score;
}

const vals = [
date.getFullYear(),
should_reset(YEAR) ? 0 : date.getMonth(),
should_reset(MONTH) ? 1 : date.getDate(),
should_reset(DAY) ? 0 : date.getHours(),
should_reset(HOUR) ? 0 : date.getMinutes(),
should_reset(MINUTE) ? 0 : date.getSeconds(),
should_reset(SECOND) ? 0 : date.getMilliseconds(),
];

return new Date(...vals);
},

clone(date) {
return new Date(...this.get_date_values(date));
},

get_date_values(date) {
return [
date.getFullYear(),
date.getMonth(),
date.getDate(),
date.getHours(),
date.getMinutes(),
date.getSeconds(),
date.getMilliseconds(),
];
},

get_days_in_month(date) {
const no_of_days = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];

const month = date.getMonth();

if (month !== 1) {
return no_of_days[month];
}

// Feb
const year = date.getFullYear();
if ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0) {
return 29;
}
return 28;
},
};

// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/padStart
function padStart(str, targetLength, padString) {
str = str + '';
targetLength = targetLength >> 0;
padString = String(typeof padString !== 'undefined' ? padString : ' ');
if (str.length > targetLength) {
return String(str);
} else {
targetLength = targetLength - str.length;
if (targetLength > padString.length) {
padString += padString.repeat(targetLength / padString.length);
}
return padString.slice(0, targetLength) + String(str);
}
}

+ 168
- 0
src/gantt.scss Zobrazit soubor

@@ -0,0 +1,168 @@
$bar-color: #b8c2cc !default;
$bar-stroke: #8D99A6 !default;
$border-color: #e0e0e0 !default;
$light-bg: #f5f5f5 !default;
$light-border-color: #ebeff2 !default;
$light-yellow: #fcf8e3 !default;
$text-muted: #666 !default;
$text-light: #555 !default;
$text-color: #333 !default;
$blue: #a3a3ff !default;
$handle-color: #ddd !default;

.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;
outline: none;

&: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;
overflow: auto;
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);
}
}
}

+ 934
- 0
src/index.js Zobrazit soubor

@@ -0,0 +1,934 @@
import date_utils from './date_utils';
import { $, createSVG } from './svg_utils';
import Bar from './bar';
import Arrow from './arrow';
import Popup from './popup';

import './gantt.scss';

const VIEW_MODE = {
QUARTER_DAY: 'Quarter Day',
HALF_DAY: 'Half Day',
DAY: 'Day',
WEEK: 'Week',
MONTH: 'Month',
YEAR: 'Year',
};

export default class Gantt {
constructor(wrapper, tasks, options) {
this.setup_wrapper(wrapper);
this.setup_options(options);
this.setup_tasks(tasks);
// initialize with default view mode
this.change_view_mode();
this.bind_events();
}

setup_wrapper(element) {
let svg_element, wrapper_element;

// CSS Selector is passed
if (typeof element === 'string') {
element = document.querySelector(element);
}

// get the SVGElement
if (element instanceof HTMLElement) {
wrapper_element = element;
svg_element = element.querySelector('svg');
} else if (element instanceof SVGElement) {
svg_element = element;
} else {
throw new TypeError(
'XhiveFramework Gantt only supports usage of a string CSS selector,' +
" HTML DOM element or SVG DOM element for the 'element' parameter"
);
}

// svg element
if (!svg_element) {
// create it
this.$svg = createSVG('svg', {
append_to: wrapper_element,
class: 'gantt',
});
} else {
this.$svg = svg_element;
this.$svg.classList.add('gantt');
}

// wrapper element
this.$container = document.createElement('div');
this.$container.classList.add('gantt-container');

const parent_element = this.$svg.parentElement;
parent_element.appendChild(this.$container);
this.$container.appendChild(this.$svg);

// popup wrapper
this.popup_wrapper = document.createElement('div');
this.popup_wrapper.classList.add('popup-wrapper');
this.$container.appendChild(this.popup_wrapper);
}

setup_options(options) {
const default_options = {
header_height: 50,
column_width: 30,
step: 24,
view_modes: [...Object.values(VIEW_MODE)],
bar_height: 20,
bar_corner_radius: 3,
arrow_curve: 5,
padding: 18,
view_mode: 'Day',
date_format: 'YYYY-MM-DD',
popup_trigger: 'click',
custom_popup_html: null,
language: 'en',
};
this.options = Object.assign({}, default_options, options);
}

setup_tasks(tasks) {
// prepare tasks
this.tasks = tasks.map((task, i) => {
// convert to Date objects
task._start = date_utils.parse(task.start);
task._end = date_utils.parse(task.end);

// make task invalid if duration too large
if (date_utils.diff(task._end, task._start, 'year') > 10) {
task.end = null;
}

// cache index
task._index = i;

// invalid dates
if (!task.start && !task.end) {
const today = date_utils.today();
task._start = today;
task._end = date_utils.add(today, 2, 'day');
}

if (!task.start && task.end) {
task._start = date_utils.add(task._end, -2, 'day');
}

if (task.start && !task.end) {
task._end = date_utils.add(task._start, 2, '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
const task_end_values = date_utils.get_date_values(task._end);
if (task_end_values.slice(3).every((d) => d === 0)) {
task._end = date_utils.add(task._end, 24, 'hour');
}

// invalid flag
if (!task.start || !task.end) {
task.invalid = true;
}

// dependencies
if (typeof task.dependencies === 'string' || !task.dependencies) {
let deps = [];
if (task.dependencies) {
deps = task.dependencies
.split(',')
.map((d) => d.trim())
.filter((d) => d);
}
task.dependencies = deps;
}

// uids
if (!task.id) {
task.id = generate_id(task);
}

return task;
});

this.setup_dependencies();
}

setup_dependencies() {
this.dependency_map = {};
for (let t of this.tasks) {
for (let d of t.dependencies) {
this.dependency_map[d] = this.dependency_map[d] || [];
this.dependency_map[d].push(t.id);
}
}
}

refresh(tasks) {
this.setup_tasks(tasks);
this.change_view_mode();
}

change_view_mode(mode = this.options.view_mode) {
this.update_view_scale(mode);
this.setup_dates();
this.render();
// fire viewmode_change event
this.trigger_event('view_change', [mode]);
}

update_view_scale(view_mode) {
this.options.view_mode = view_mode;

if (view_mode === VIEW_MODE.DAY) {
this.options.step = 24;
this.options.column_width = 38;
} else if (view_mode === VIEW_MODE.HALF_DAY) {
this.options.step = 24 / 2;
this.options.column_width = 38;
} else if (view_mode === VIEW_MODE.QUARTER_DAY) {
this.options.step = 24 / 4;
this.options.column_width = 38;
} else if (view_mode === VIEW_MODE.WEEK) {
this.options.step = 24 * 7;
this.options.column_width = 140;
} else if (view_mode === VIEW_MODE.MONTH) {
this.options.step = 24 * 30;
this.options.column_width = 120;
} else if (view_mode === VIEW_MODE.YEAR) {
this.options.step = 24 * 365;
this.options.column_width = 120;
}
}

setup_dates() {
this.setup_gantt_dates();
this.setup_date_values();
}

setup_gantt_dates() {
this.gantt_start = this.gantt_end = null;

for (let task of this.tasks) {
// set global start and end date
if (!this.gantt_start || task._start < this.gantt_start) {
this.gantt_start = task._start;
}
if (!this.gantt_end || task._end > this.gantt_end) {
this.gantt_end = task._end;
}
}

this.gantt_start = date_utils.start_of(this.gantt_start, 'day');
this.gantt_end = date_utils.start_of(this.gantt_end, 'day');

// add date padding on both sides
if (this.view_is([VIEW_MODE.QUARTER_DAY, VIEW_MODE.HALF_DAY])) {
this.gantt_start = date_utils.add(this.gantt_start, -7, 'day');
this.gantt_end = date_utils.add(this.gantt_end, 7, 'day');
} else if (this.view_is(VIEW_MODE.MONTH)) {
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');
}
}

setup_date_values() {
this.dates = [];
let cur_date = null;

while (cur_date === null || cur_date < this.gantt_end) {
if (!cur_date) {
cur_date = date_utils.clone(this.gantt_start);
} else {
if (this.view_is(VIEW_MODE.YEAR)) {
cur_date = date_utils.add(cur_date, 1, 'year');
} else if (this.view_is(VIEW_MODE.MONTH)) {
cur_date = date_utils.add(cur_date, 1, 'month');
} else {
cur_date = date_utils.add(
cur_date,
this.options.step,
'hour'
);
}
}
this.dates.push(cur_date);
}
}

bind_events() {
this.bind_grid_click();
this.bind_bar_events();
}

render() {
this.clear();
this.setup_layers();
this.make_grid();
this.make_dates();
this.make_bars();
this.make_arrows();
this.map_arrows_on_bars();
this.set_width();
this.set_scroll_position();
}

setup_layers() {
this.layers = {};
const layers = ['grid', 'date', 'arrow', 'progress', 'bar', 'details'];
// make group layers
for (let layer of layers) {
this.layers[layer] = createSVG('g', {
class: layer,
append_to: this.$svg,
});
}
}

make_grid() {
this.make_grid_background();
this.make_grid_rows();
this.make_grid_header();
this.make_grid_ticks();
this.make_grid_highlights();
}

make_grid_background() {
const grid_width = this.dates.length * this.options.column_width;
const grid_height =
this.options.header_height +
this.options.padding +
(this.options.bar_height + this.options.padding) *
this.tasks.length;

createSVG('rect', {
x: 0,
y: 0,
width: grid_width,
height: grid_height,
class: 'grid-background',
append_to: this.layers.grid,
});

$.attr(this.$svg, {
height: grid_height + this.options.padding + 100,
width: '100%',
});
}

make_grid_rows() {
const rows_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_height = this.options.bar_height + this.options.padding;

let row_y = this.options.header_height + this.options.padding / 2;

for (let task of this.tasks) {
createSVG('rect', {
x: 0,
y: row_y,
width: row_width,
height: row_height,
class: 'grid-row',
append_to: rows_layer,
});

createSVG('line', {
x1: 0,
y1: row_y + row_height,
x2: row_width,
y2: row_y + row_height,
class: 'row-line',
append_to: lines_layer,
});

row_y += this.options.bar_height + this.options.padding;
}
}

make_grid_header() {
const header_width = this.dates.length * this.options.column_width;
const header_height = this.options.header_height + 10;
createSVG('rect', {
x: 0,
y: 0,
width: header_width,
height: header_height,
class: 'grid-header',
append_to: this.layers.grid,
});
}

make_grid_ticks() {
let tick_x = 0;
let tick_y = this.options.header_height + this.options.padding / 2;
let tick_height =
(this.options.bar_height + this.options.padding) *
this.tasks.length;

for (let date of this.dates) {
let tick_class = 'tick';
// thick tick for monday
if (this.view_is(VIEW_MODE.DAY) && date.getDate() === 1) {
tick_class += ' thick';
}
// thick tick for first week
if (
this.view_is(VIEW_MODE.WEEK) &&
date.getDate() >= 1 &&
date.getDate() < 8
) {
tick_class += ' thick';
}
// thick ticks for quarters
if (
this.view_is(VIEW_MODE.MONTH) &&
(date.getMonth() + 1) % 3 === 0
) {
tick_class += ' thick';
}

createSVG('path', {
d: `M ${tick_x} ${tick_y} v ${tick_height}`,
class: tick_class,
append_to: this.layers.grid,
});

if (this.view_is(VIEW_MODE.MONTH)) {
tick_x +=
(date_utils.get_days_in_month(date) *
this.options.column_width) /
30;
} else {
tick_x += this.options.column_width;
}
}
}

make_grid_highlights() {
// highlight today's date
if (this.view_is(VIEW_MODE.DAY)) {
const x =
(date_utils.diff(date_utils.today(), this.gantt_start, 'hour') /
this.options.step) *
this.options.column_width;
const y = 0;

const width = this.options.column_width;
const height =
(this.options.bar_height + this.options.padding) *
this.tasks.length +
this.options.header_height +
this.options.padding / 2;

createSVG('rect', {
x,
y,
width,
height,
class: 'today-highlight',
append_to: this.layers.grid,
});
}
}

make_dates() {
for (let date of this.get_dates_to_draw()) {
createSVG('text', {
x: date.lower_x,
y: date.lower_y,
innerHTML: date.lower_text,
class: 'lower-text',
append_to: this.layers.date,
});

if (date.upper_text) {
const $upper_text = createSVG('text', {
x: date.upper_x,
y: date.upper_y,
innerHTML: date.upper_text,
class: 'upper-text',
append_to: this.layers.date,
});

// remove out-of-bound dates
if (
$upper_text.getBBox().x2 > this.layers.grid.getBBox().width
) {
$upper_text.remove();
}
}
}
}

get_dates_to_draw() {
let last_date = null;
const dates = this.dates.map((date, i) => {
const d = this.get_date_info(date, last_date, i);
last_date = date;
return d;
});
return dates;
}

get_date_info(date, last_date, i) {
if (!last_date) {
last_date = date_utils.add(date, 1, 'year');
}
const date_text = {
'Quarter Day_lower': date_utils.format(
date,
'HH',
this.options.language
),
'Half Day_lower': date_utils.format(
date,
'HH',
this.options.language
),
Day_lower:
date.getDate() !== last_date.getDate()
? date_utils.format(date, 'D', this.options.language)
: '',
Week_lower:
date.getMonth() !== last_date.getMonth()
? date_utils.format(date, 'D MMM', this.options.language)
: date_utils.format(date, 'D', this.options.language),
Month_lower: date_utils.format(date, 'MMMM', this.options.language),
Year_lower: date_utils.format(date, 'YYYY', this.options.language),
'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.getMonth() !== last_date.getMonth()
? date_utils.format(
date,
'D MMM',
this.options.language
)
: date_utils.format(date, 'D', this.options.language)
: '',
Day_upper:
date.getMonth() !== last_date.getMonth()
? date_utils.format(date, 'MMMM', this.options.language)
: '',
Week_upper:
date.getMonth() !== last_date.getMonth()
? date_utils.format(date, 'MMMM', this.options.language)
: '',
Month_upper:
date.getFullYear() !== last_date.getFullYear()
? date_utils.format(date, 'YYYY', this.options.language)
: '',
Year_upper:
date.getFullYear() !== last_date.getFullYear()
? date_utils.format(date, 'YYYY', this.options.language)
: '',
};

const base_pos = {
x: i * this.options.column_width,
lower_y: this.options.header_height,
upper_y: this.options.header_height - 25,
};

const x_pos = {
'Quarter Day_lower': (this.options.column_width * 4) / 2,
'Quarter Day_upper': 0,
'Half Day_lower': (this.options.column_width * 2) / 2,
'Half Day_upper': 0,
Day_lower: this.options.column_width / 2,
Day_upper: (this.options.column_width * 30) / 2,
Week_lower: 0,
Week_upper: (this.options.column_width * 4) / 2,
Month_lower: this.options.column_width / 2,
Month_upper: (this.options.column_width * 12) / 2,
Year_lower: this.options.column_width / 2,
Year_upper: (this.options.column_width * 30) / 2,
};

return {
upper_text: date_text[`${this.options.view_mode}_upper`],
lower_text: date_text[`${this.options.view_mode}_lower`],
upper_x: base_pos.x + x_pos[`${this.options.view_mode}_upper`],
upper_y: base_pos.upper_y,
lower_x: base_pos.x + x_pos[`${this.options.view_mode}_lower`],
lower_y: base_pos.lower_y,
};
}

make_bars() {
this.bars = this.tasks.map((task) => {
const bar = new Bar(this, task);
this.layers.bar.appendChild(bar.group);
return bar;
});
}

make_arrows() {
this.arrows = [];
for (let task of this.tasks) {
let arrows = [];
arrows = task.dependencies
.map((task_id) => {
const dependency = this.get_task(task_id);
if (!dependency) return;
const arrow = new Arrow(
this,
this.bars[dependency._index], // from_task
this.bars[task._index] // to_task
);
this.layers.arrow.appendChild(arrow.element);
return arrow;
})
.filter(Boolean); // filter falsy values
this.arrows = this.arrows.concat(arrows);
}
}

map_arrows_on_bars() {
for (let bar of this.bars) {
bar.arrows = this.arrows.filter((arrow) => {
return (
arrow.from_task.task.id === bar.task.id ||
arrow.to_task.task.id === bar.task.id
);
});
}
}

set_width() {
const cur_width = this.$svg.getBoundingClientRect().width;
const actual_width = this.$svg
.querySelector('.grid .grid-row')
.getAttribute('width');
if (cur_width < actual_width) {
this.$svg.setAttribute('width', actual_width);
}
}

set_scroll_position() {
const parent_element = this.$svg.parentElement;
if (!parent_element) return;

const hours_before_first_task = date_utils.diff(
this.get_oldest_starting_date(),
this.gantt_start,
'hour'
);

const scroll_pos =
(hours_before_first_task / this.options.step) *
this.options.column_width -
this.options.column_width;

parent_element.scrollLeft = scroll_pos;
}

bind_grid_click() {
$.on(
this.$svg,
this.options.popup_trigger,
'.grid-row, .grid-header',
() => {
this.unselect_all();
this.hide_popup();
}
);
}

bind_bar_events() {
let is_dragging = false;
let x_on_start = 0;
let y_on_start = 0;
let is_resizing_left = false;
let is_resizing_right = false;
let parent_bar_id = null;
let bars = []; // instanceof Bar
this.bar_being_dragged = null;

function action_in_progress() {
return is_dragging || is_resizing_left || is_resizing_right;
}

$.on(this.$svg, 'mousedown', '.bar-wrapper, .handle', (e, element) => {
const bar_wrapper = $.closest('.bar-wrapper', element);

if (element.classList.contains('left')) {
is_resizing_left = true;
} else if (element.classList.contains('right')) {
is_resizing_right = true;
} else if (element.classList.contains('bar-wrapper')) {
is_dragging = true;
}

bar_wrapper.classList.add('active');

x_on_start = e.offsetX;
y_on_start = e.offsetY;

parent_bar_id = bar_wrapper.getAttribute('data-id');
const ids = [
parent_bar_id,
...this.get_all_dependent_tasks(parent_bar_id),
];
bars = ids.map((id) => this.get_bar(id));

this.bar_being_dragged = parent_bar_id;

bars.forEach((bar) => {
const $bar = bar.$bar;
$bar.ox = $bar.getX();
$bar.oy = $bar.getY();
$bar.owidth = $bar.getWidth();
$bar.finaldx = 0;
});
});

$.on(this.$svg, 'mousemove', (e) => {
if (!action_in_progress()) return;
const dx = e.offsetX - x_on_start;
const dy = e.offsetY - y_on_start;

bars.forEach((bar) => {
const $bar = bar.$bar;
$bar.finaldx = this.get_snap_position(dx);
this.hide_popup();
if (is_resizing_left) {
if (parent_bar_id === bar.task.id) {
bar.update_bar_position({
x: $bar.ox + $bar.finaldx,
width: $bar.owidth - $bar.finaldx,
});
} else {
bar.update_bar_position({
x: $bar.ox + $bar.finaldx,
});
}
} else if (is_resizing_right) {
if (parent_bar_id === bar.task.id) {
bar.update_bar_position({
width: $bar.owidth + $bar.finaldx,
});
}
} else if (is_dragging) {
bar.update_bar_position({ x: $bar.ox + $bar.finaldx });
}
});
});

document.addEventListener('mouseup', (e) => {
if (is_dragging || is_resizing_left || is_resizing_right) {
bars.forEach((bar) => bar.group.classList.remove('active'));
}

is_dragging = false;
is_resizing_left = false;
is_resizing_right = false;
});

$.on(this.$svg, 'mouseup', (e) => {
this.bar_being_dragged = null;
bars.forEach((bar) => {
const $bar = bar.$bar;
if (!$bar.finaldx) return;
bar.date_changed();
bar.set_action_completed();
});
});

this.bind_bar_progress();
}

bind_bar_progress() {
let x_on_start = 0;
let y_on_start = 0;
let is_resizing = null;
let bar = null;
let $bar_progress = null;
let $bar = null;

$.on(this.$svg, 'mousedown', '.handle.progress', (e, handle) => {
is_resizing = true;
x_on_start = e.offsetX;
y_on_start = e.offsetY;

const $bar_wrapper = $.closest('.bar-wrapper', handle);
const id = $bar_wrapper.getAttribute('data-id');
bar = this.get_bar(id);

$bar_progress = bar.$bar_progress;
$bar = bar.$bar;

$bar_progress.finaldx = 0;
$bar_progress.owidth = $bar_progress.getWidth();
$bar_progress.min_dx = -$bar_progress.getWidth();
$bar_progress.max_dx = $bar.getWidth() - $bar_progress.getWidth();
});

$.on(this.$svg, 'mousemove', (e) => {
if (!is_resizing) return;
let dx = e.offsetX - x_on_start;
let dy = e.offsetY - y_on_start;

if (dx > $bar_progress.max_dx) {
dx = $bar_progress.max_dx;
}
if (dx < $bar_progress.min_dx) {
dx = $bar_progress.min_dx;
}

const $handle = bar.$handle_progress;
$.attr($bar_progress, 'width', $bar_progress.owidth + dx);
$.attr($handle, 'points', bar.get_progress_polygon_points());
$bar_progress.finaldx = dx;
});

$.on(this.$svg, 'mouseup', () => {
is_resizing = false;
if (!($bar_progress && $bar_progress.finaldx)) return;
bar.progress_changed();
bar.set_action_completed();
});
}

get_all_dependent_tasks(task_id) {
let out = [];
let to_process = [task_id];
while (to_process.length) {
const deps = to_process.reduce((acc, curr) => {
acc = acc.concat(this.dependency_map[curr]);
return acc;
}, []);

out = out.concat(deps);
to_process = deps.filter((d) => !to_process.includes(d));
}

return out.filter(Boolean);
}

get_snap_position(dx) {
let odx = dx,
rem,
position;

if (this.view_is(VIEW_MODE.WEEK)) {
rem = dx % (this.options.column_width / 7);
position =
odx -
rem +
(rem < this.options.column_width / 14
? 0
: this.options.column_width / 7);
} else if (this.view_is(VIEW_MODE.MONTH)) {
rem = dx % (this.options.column_width / 30);
position =
odx -
rem +
(rem < this.options.column_width / 60
? 0
: this.options.column_width / 30);
} else {
rem = dx % this.options.column_width;
position =
odx -
rem +
(rem < this.options.column_width / 2
? 0
: this.options.column_width);
}
return position;
}

unselect_all() {
[...this.$svg.querySelectorAll('.bar-wrapper')].forEach((el) => {
el.classList.remove('active');
});
}

view_is(modes) {
if (typeof modes === 'string') {
return this.options.view_mode === modes;
}

if (Array.isArray(modes)) {
return modes.some((mode) => this.options.view_mode === mode);
}

return false;
}

get_task(id) {
return this.tasks.find((task) => {
return task.id === id;
});
}

get_bar(id) {
return this.bars.find((bar) => {
return bar.task.id === id;
});
}

show_popup(options) {
if (!this.popup) {
this.popup = new Popup(
this.popup_wrapper,
this.options.custom_popup_html
);
}
this.popup.show(options);
}

hide_popup() {
this.popup && this.popup.hide();
}

trigger_event(event, args) {
if (this.options['on_' + event]) {
this.options['on_' + event].apply(null, args);
}
}

/**
* Gets the oldest starting date from the list of tasks
*
* @returns Date
* @memberof Gantt
*/
get_oldest_starting_date() {
return this.tasks
.map((task) => task._start)
.reduce((prev_date, cur_date) =>
cur_date <= prev_date ? cur_date : prev_date
);
}

/**
* Clear all elements from the parent svg element
*
* @memberof Gantt
*/
clear() {
this.$svg.innerHTML = '';
}
}

Gantt.VIEW_MODE = VIEW_MODE;

function generate_id(task) {
return task.name + '_' + Math.random().toString(36).slice(2, 12);
}

+ 69
- 0
src/popup.js Zobrazit soubor

@@ -0,0 +1,69 @@
export default class Popup {
constructor(parent, custom_html) {
this.parent = parent;
this.custom_html = custom_html;
this.make();
}

make() {
this.parent.innerHTML = `
<div class="title"></div>
<div class="subtitle"></div>
<div class="pointer"></div>
`;

this.hide();

this.title = this.parent.querySelector('.title');
this.subtitle = this.parent.querySelector('.subtitle');
this.pointer = this.parent.querySelector('.pointer');
}

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.custom_html) {
let html = this.custom_html(options.task);
html += '<div class="pointer"></div>';
this.parent.innerHTML = html;
this.pointer = this.parent.querySelector('.pointer');
} else {
// set data
this.title.innerHTML = options.title;
this.subtitle.innerHTML = options.subtitle;
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 + 'px';

this.pointer.style.transform = 'rotateZ(90deg)';
this.pointer.style.left = '-7px';
this.pointer.style.top = '2px';
}

// show
this.parent.style.opacity = 1;
}

hide() {
this.parent.style.opacity = 0;
this.parent.style.left = 0;
}
}

+ 133
- 0
src/svg_utils.js Zobrazit soubor

@@ -0,0 +1,133 @@
export function $(expr, con) {
return typeof expr === 'string'
? (con || document).querySelector(expr)
: expr || null;
}

export function createSVG(tag, attrs) {
const elem = document.createElementNS('http://www.w3.org/2000/svg', tag);
for (let attr in attrs) {
if (attr === 'append_to') {
const parent = attrs.append_to;
parent.appendChild(elem);
} else if (attr === 'innerHTML') {
elem.innerHTML = attrs.innerHTML;
} else {
elem.setAttribute(attr, attrs[attr]);
}
}
return elem;
}

export function animateSVG(svgElement, attr, from, to) {
const animatedSvgElement = getAnimationElement(svgElement, attr, from, to);

if (animatedSvgElement === svgElement) {
// triggered 2nd time programmatically
// trigger artificial click event
const event = document.createEvent('HTMLEvents');
event.initEvent('click', true, true);
event.eventName = 'click';
animatedSvgElement.dispatchEvent(event);
}
}

function getAnimationElement(
svgElement,
attr,
from,
to,
dur = '0.4s',
begin = '0.1s'
) {
const animEl = svgElement.querySelector('animate');
if (animEl) {
$.attr(animEl, {
attributeName: attr,
from,
to,
dur,
begin: 'click + ' + begin, // artificial click
});
return svgElement;
}

const animateElement = createSVG('animate', {
attributeName: attr,
from,
to,
dur,
begin,
calcMode: 'spline',
values: from + ';' + to,
keyTimes: '0; 1',
keySplines: cubic_bezier('ease-out'),
});
svgElement.appendChild(animateElement);

return svgElement;
}

function cubic_bezier(name) {
return {
ease: '.25 .1 .25 1',
linear: '0 0 1 1',
'ease-in': '.42 0 1 1',
'ease-out': '0 0 .58 1',
'ease-in-out': '.42 0 .58 1',
}[name];
}

$.on = (element, event, selector, callback) => {
if (!callback) {
callback = selector;
$.bind(element, event, callback);
} else {
$.delegate(element, event, selector, callback);
}
};

$.off = (element, event, handler) => {
element.removeEventListener(event, handler);
};

$.bind = (element, event, callback) => {
event.split(/\s+/).forEach(function (event) {
element.addEventListener(event, callback);
});
};

$.delegate = (element, event, selector, callback) => {
element.addEventListener(event, function (e) {
const delegatedTarget = e.target.closest(selector);
if (delegatedTarget) {
e.delegatedTarget = delegatedTarget;
callback.call(this, e, delegatedTarget);
}
});
};

$.closest = (selector, element) => {
if (!element) return null;

if (element.matches(selector)) {
return element;
}

return $.closest(selector, element.parentNode);
};

$.attr = (element, attr, value) => {
if (!value && typeof attr === 'string') {
return element.getAttribute(attr);
}

if (typeof attr === 'object') {
for (let key in attr) {
$.attr(element, key, attr[key]);
}
return;
}

element.setAttribute(attr, value);
};

+ 124
- 0
tests/date_utils.test.js Zobrazit soubor

@@ -0,0 +1,124 @@
import date_utils from '../src/date_utils';

test('Parse: parses string date', () => {
const date = date_utils.parse('2017-09-09');

expect(date.getDate()).toBe(9);
expect(date.getMonth()).toBe(8);
expect(date.getFullYear()).toBe(2017);
});

test('Parse: parses string datetime', () => {
const date = date_utils.parse('2017-08-27 16:08:34');

expect(date.getFullYear()).toBe(2017);
expect(date.getMonth()).toBe(7);
expect(date.getDate()).toBe(27);
expect(date.getHours()).toBe(16);
expect(date.getMinutes()).toBe(8);
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);

expect(d).toBe(date);
});

test('Diff: returns diff between 2 date objects', () => {
const a = date_utils.parse('2017-09-08');
const b = date_utils.parse('2017-06-07');

expect(date_utils.diff(a, b, 'day')).toBe(93);
expect(date_utils.diff(a, b, 'month')).toBe(3);
expect(date_utils.diff(a, b, 'year')).toBe(0);
});

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

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

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

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

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

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

+ 3774
- 0
yarn.lock
Diff nebyl zobrazen, protože je příliš veliký
Zobrazit soubor


Načítá se…
Zrušit
Uložit