Compare commits
344 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
108eeb5898 | ||
|
|
7419b37847 | ||
|
|
3487916883 | ||
|
|
df08d74eeb | ||
|
|
eded996177 | ||
|
|
e71369da34 | ||
|
|
b617dfffbe | ||
|
|
41ab8de74f | ||
|
|
6311a1e32f | ||
|
|
0e2c8dadbe | ||
|
|
4d297bb7f8 | ||
|
|
3bcaf72027 | ||
|
|
d53266f96c | ||
|
|
41838a913c | ||
|
|
1a09a38a6e | ||
|
|
6a34a20e15 | ||
|
|
2850787e72 | ||
|
|
c151416048 | ||
|
|
02012fff05 | ||
|
|
ae7bc8db60 | ||
|
|
45a17c792c | ||
|
|
188ab1d35e | ||
|
|
a8d7814c82 | ||
|
|
521107412a | ||
|
|
65ea2377db | ||
|
|
34422d1886 | ||
|
|
1f1a5f04e1 | ||
|
|
7dcc132830 | ||
|
|
2c15115198 | ||
|
|
03d886665b | ||
|
|
3973293961 | ||
|
|
3217f7d986 | ||
|
|
bab2afee54 | ||
|
|
84a677d26e | ||
|
|
2f6b578fc9 | ||
|
|
3dd58fc00d | ||
|
|
8b523c7125 | ||
|
|
abeec1c363 | ||
|
|
218202c16d | ||
|
|
f9c5b1f3ac | ||
|
|
0b33e88672 | ||
|
|
85399e7fdc | ||
|
|
393a91ef11 | ||
|
|
5c9f46a69c | ||
|
|
8be3b02a9e | ||
|
|
d5ee5a4e45 | ||
|
|
097d69ce90 | ||
|
|
fcd04922bd | ||
|
|
350dc88785 | ||
|
|
a3c9f15cf9 | ||
|
|
019e550b6e | ||
|
|
196299db50 | ||
|
|
e6a1c30410 | ||
|
|
d1cc6cc79e | ||
|
|
a7d38841b3 | ||
|
|
539482e6d9 | ||
|
|
ba660d53d2 | ||
|
|
178262033d | ||
|
|
c15cc3197d | ||
|
|
dae014d009 | ||
|
|
ffb53b935f | ||
|
|
343324695a | ||
|
|
e48960366f | ||
|
|
bb448c774f | ||
|
|
1dfcc7bbf2 | ||
|
|
4a1e343d67 | ||
|
|
3bdc6828c0 | ||
|
|
610da9d2e2 | ||
|
|
0b1c6c9efb | ||
|
|
f0a8bf7081 | ||
|
|
426c8a826e | ||
|
|
8b2e544bb4 | ||
|
|
abe2e894b5 | ||
|
|
f5f61afd99 | ||
|
|
653a5d0f30 | ||
|
|
a449ac9acf | ||
|
|
341e51b2bb | ||
|
|
0c6266e0fe | ||
|
|
f37fe4e80f | ||
|
|
d740d407b4 | ||
|
|
af1c94b56a | ||
|
|
0c9e4aeb3e | ||
|
|
6c8cc993c4 | ||
|
|
9caf2322b9 | ||
|
|
6984d3e544 | ||
|
|
b776376f4e | ||
|
|
02ea61d7c3 | ||
|
|
268d82ad35 | ||
|
|
e20c7e0b6b | ||
|
|
f4f97cdb18 | ||
|
|
50f9c2ca13 | ||
|
|
79b0e0d2ab | ||
|
|
440d717bc4 | ||
|
|
d87022eb3f | ||
|
|
6d89ee0154 | ||
|
|
8152f5ad17 | ||
|
|
d7fb48031a | ||
|
|
3519755dc3 | ||
|
|
7ac5422950 | ||
|
|
fc71da6550 | ||
|
|
c13c0cde4d | ||
|
|
8c526b5ed6 | ||
|
|
2eaa127c5b | ||
|
|
6e0e0a9704 | ||
|
|
8f03ba0572 | ||
|
|
3e28b63e14 | ||
|
|
59f5146b20 | ||
|
|
5d81ffdcef | ||
|
|
189c750848 | ||
|
|
a22c9dcc41 | ||
|
|
da04ea777f | ||
|
|
0d357b03e0 | ||
|
|
24e66dc1fb | ||
|
|
d3b4a2fa06 | ||
|
|
dd01835864 | ||
|
|
788a96f83c | ||
|
|
a08de40f47 | ||
|
|
e4df4fe30a | ||
|
|
d7b2051b7a | ||
|
|
242df789c9 | ||
|
|
bdcf9f921b | ||
|
|
53c27c950c | ||
|
|
45f02df52d | ||
|
|
75bf77ed5d | ||
|
|
ff57ae2cb6 | ||
|
|
629d55e2bc | ||
|
|
3461888194 | ||
|
|
ced1080931 | ||
|
|
de53f780b7 | ||
|
|
bc791491b1 | ||
|
|
026bc31e14 | ||
|
|
c88e8fce11 | ||
|
|
0ea8d48d3a | ||
|
|
dfe4907b3e | ||
|
|
5bead3a4ff | ||
|
|
b834da1ad5 | ||
|
|
e5a3a3bfca | ||
|
|
6f97f816f1 | ||
|
|
3e197c2369 | ||
|
|
1d58c547a9 | ||
|
|
78e7b2556e | ||
|
|
fc97f3121c | ||
|
|
1cd9456e25 | ||
|
|
d6e8700eab | ||
|
|
31551467d1 | ||
|
|
350ff7f0c3 | ||
|
|
4e09e0231d | ||
|
|
fd44499b60 | ||
|
|
66e2b1db5f | ||
|
|
af206ebbd7 | ||
|
|
faf41b7220 | ||
|
|
f8d77b2a73 | ||
|
|
0074a092a5 | ||
|
|
f4bfa43a57 | ||
|
|
aa33ac9b4d | ||
|
|
22ff6e202e | ||
|
|
9e64337ea8 | ||
|
|
daaf0d3be2 | ||
|
|
28932ee6c9 | ||
|
|
5c212725fc | ||
|
|
35d810ba7c | ||
|
|
73aa284e05 | ||
|
|
e648769ec5 | ||
|
|
30a8d5a78b | ||
|
|
9cd3633dd7 | ||
|
|
5e18b534bf | ||
|
|
af0276c152 | ||
|
|
b29076afed | ||
|
|
6ce4da519b | ||
|
|
b7deffd539 | ||
|
|
e151483f6d | ||
|
|
a65bcff5c1 | ||
|
|
3194f883ad | ||
|
|
1403f2e581 | ||
|
|
55be33cfa9 | ||
|
|
99261dfe1a | ||
|
|
e9931bde24 | ||
|
|
8bdd488618 | ||
|
|
901f78e717 | ||
|
|
0cefc57624 | ||
|
|
a72b75a2f1 | ||
|
|
86db2221ff | ||
|
|
e7c735db68 | ||
|
|
0ee9b6ad69 | ||
|
|
2fd3aefd79 | ||
|
|
dd28b8ee0e | ||
|
|
35966f0f3d | ||
|
|
25d55e156b | ||
|
|
47fe68eb4b | ||
|
|
efb632fb23 | ||
|
|
591b599b51 | ||
|
|
ecbec30fd7 | ||
|
|
ab9ecf753f | ||
|
|
0e6e0154b4 | ||
|
|
4f66e83eaa | ||
|
|
769233a71a | ||
|
|
85c943b191 | ||
|
|
0ebea3d1b8 | ||
|
|
94d2cf5772 | ||
|
|
c13c9c08ec | ||
|
|
c815dcd31a | ||
|
|
0afbf76881 | ||
|
|
4c54ea847a | ||
|
|
4c09eccef5 | ||
|
|
b42a63c547 | ||
|
|
e0bcc333ee | ||
|
|
889a03a828 | ||
|
|
9401b35139 | ||
|
|
b8bad72109 | ||
|
|
bbbf28819b | ||
|
|
ea6259adce | ||
|
|
0b235d3397 | ||
|
|
bd7ce02c85 | ||
|
|
14f1770b53 | ||
|
|
93bf95d85d | ||
|
|
533b9ea8fb | ||
|
|
b0d3fdb51b | ||
|
|
bcd52bf363 | ||
|
|
8dad3412d0 | ||
|
|
bd6784a641 | ||
|
|
f9da6ad42f | ||
|
|
a0c7cd36bd | ||
|
|
a4f7b727fe | ||
|
|
cac6a4f116 | ||
|
|
ddf2712531 | ||
|
|
e5717faaf8 | ||
|
|
51c717c3f9 | ||
|
|
a60d93cc16 | ||
|
|
01082de642 | ||
|
|
0f15ab1875 | ||
|
|
c34c09e837 | ||
|
|
7670d13634 | ||
|
|
0cc5a3cbf6 | ||
|
|
218818619c | ||
|
|
6e2f91db25 | ||
|
|
d886100221 | ||
|
|
c880b72fa6 | ||
|
|
05b243f3f6 | ||
|
|
ce43bb9b66 | ||
|
|
4ba1566ceb | ||
|
|
74bda8bef0 | ||
|
|
02aaf6a02f | ||
|
|
6cb5c12fb7 | ||
|
|
be1c875222 | ||
|
|
c4e1553e55 | ||
|
|
916fd15255 | ||
|
|
1d024dcb55 | ||
|
|
ddb35724cb | ||
|
|
0381503301 | ||
|
|
7381b84c76 | ||
|
|
342e97b56a | ||
|
|
bf80b07b42 | ||
|
|
e014234700 | ||
|
|
743f84f49d | ||
|
|
80458f69ed | ||
|
|
94fdb10f1b | ||
|
|
228782a8a4 | ||
|
|
73871b039e | ||
|
|
33cb61533e | ||
|
|
fc08a80d19 | ||
|
|
781578c803 | ||
|
|
a4197ac01b | ||
|
|
3bca39dcca | ||
|
|
372b1e8524 | ||
|
|
da4b313dd8 | ||
|
|
bfadf6598b | ||
|
|
9ce1e995a2 | ||
|
|
626bf6ab65 | ||
|
|
d12cc0d24e | ||
|
|
1e1498f394 | ||
|
|
cd56d6eeb1 | ||
|
|
351fce3a41 | ||
|
|
469dfa3155 | ||
|
|
073bc25df3 | ||
|
|
393b00639c | ||
|
|
c6e0b50ff5 | ||
|
|
481896e392 | ||
|
|
e095e4c028 | ||
|
|
b0384645d6 | ||
|
|
9744be6f6f | ||
|
|
5b3268737b | ||
|
|
8b396e4d50 | ||
|
|
9cf9ebb967 | ||
|
|
76be9de801 | ||
|
|
6233b6cd4b | ||
|
|
aba84bd715 | ||
|
|
9d01fa85b7 | ||
|
|
4cf30c6630 | ||
|
|
3371a57ed0 | ||
|
|
3270f34cf1 | ||
|
|
a7ba98810c | ||
|
|
05f4d069a6 | ||
|
|
3dc56ddd3f | ||
|
|
8f0b83d27d | ||
|
|
229a0a5893 | ||
|
|
9ac0f0257e | ||
|
|
bb755e6b42 | ||
|
|
572f463a0b | ||
|
|
318ea68107 | ||
|
|
94f1cd7958 | ||
|
|
d72019bb8a | ||
|
|
fa22dc7895 | ||
|
|
99781a8274 | ||
|
|
4c130206c4 | ||
|
|
dd18120d30 | ||
|
|
08edaa3d22 | ||
|
|
1f5d842ddb | ||
|
|
17ccc27a9a | ||
|
|
8f6818bc8b | ||
|
|
309c59e6e2 | ||
|
|
6061ae755d | ||
|
|
211a18666e | ||
|
|
41ea97b4dd | ||
|
|
13f579f4b6 | ||
|
|
251e8c25f5 | ||
|
|
9e3c490196 | ||
|
|
8447e15225 | ||
|
|
c5154fc9eb | ||
|
|
48ea5ad5a2 | ||
|
|
50386745ca | ||
|
|
81622e434b | ||
|
|
2712ac3201 | ||
|
|
af45438ba9 | ||
|
|
69978e00d2 | ||
|
|
1b36ec9464 | ||
|
|
4995e9ed0e | ||
|
|
4b11841bc1 | ||
|
|
49e6880252 | ||
|
|
a7e61db72f | ||
|
|
e2a9185dac | ||
|
|
0c39cc9b4e | ||
|
|
98f38e1684 | ||
|
|
94409f0bb8 | ||
|
|
ec5e559b5c | ||
|
|
cd93fbf655 | ||
|
|
8f4214a926 | ||
|
|
45d218994d | ||
|
|
97591595ac | ||
|
|
e8467917d7 | ||
|
|
1680bb7338 | ||
|
|
ee1062c182 | ||
|
|
5a03973e06 | ||
|
|
24cb69dd4e | ||
|
|
8c082e7bc9 |
4
.babelrc
4
.babelrc
@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"presets": ["es2015"],
|
|
||||||
"plugins": ["babel-plugin-add-module-exports"]
|
|
||||||
}
|
|
||||||
182
.eslintrc
182
.eslintrc
@ -1,182 +0,0 @@
|
|||||||
{
|
|
||||||
"ecmaFeatures": {
|
|
||||||
"globalReturn": true,
|
|
||||||
"jsx": true,
|
|
||||||
"modules": true
|
|
||||||
},
|
|
||||||
|
|
||||||
"env": {
|
|
||||||
"browser": true,
|
|
||||||
"es6": true,
|
|
||||||
"node": true
|
|
||||||
},
|
|
||||||
|
|
||||||
"globals": {
|
|
||||||
"document": false,
|
|
||||||
"escape": false,
|
|
||||||
"navigator": false,
|
|
||||||
"unescape": false,
|
|
||||||
"window": false,
|
|
||||||
"describe": true,
|
|
||||||
"before": true,
|
|
||||||
"it": true,
|
|
||||||
"expect": true,
|
|
||||||
"sinon": true
|
|
||||||
},
|
|
||||||
|
|
||||||
"parser": "babel-eslint",
|
|
||||||
|
|
||||||
"plugins": [
|
|
||||||
|
|
||||||
],
|
|
||||||
|
|
||||||
"rules": {
|
|
||||||
"block-scoped-var": 2,
|
|
||||||
"brace-style": [2, "1tbs", { "allowSingleLine": true }],
|
|
||||||
"comma-dangle": [2, "never"],
|
|
||||||
"comma-spacing": [2, { "before": false, "after": true }],
|
|
||||||
"comma-style": [2, "last"],
|
|
||||||
"complexity": 0,
|
|
||||||
"consistent-return": 2,
|
|
||||||
"consistent-this": 0,
|
|
||||||
"curly": [2, "multi-line"],
|
|
||||||
"default-case": 0,
|
|
||||||
"dot-location": [2, "property"],
|
|
||||||
"dot-notation": 0,
|
|
||||||
"eol-last": 2,
|
|
||||||
"eqeqeq": [2, "allow-null"],
|
|
||||||
"func-names": 0,
|
|
||||||
"func-style": 0,
|
|
||||||
"generator-star-spacing": [2, "both"],
|
|
||||||
"guard-for-in": 0,
|
|
||||||
"handle-callback-err": [2, "^(err|error|anySpecificError)$" ],
|
|
||||||
"indent": [2, "tab", { "SwitchCase": 1 }],
|
|
||||||
"key-spacing": [2, { "beforeColon": false, "afterColon": true }],
|
|
||||||
"linebreak-style": 0,
|
|
||||||
"max-depth": 0,
|
|
||||||
"max-len": [2, 120, 4],
|
|
||||||
"max-nested-callbacks": 0,
|
|
||||||
"max-params": 0,
|
|
||||||
"max-statements": 0,
|
|
||||||
"new-cap": [2, { "newIsCap": true, "capIsNew": false }],
|
|
||||||
"new-parens": 2,
|
|
||||||
"no-alert": 0,
|
|
||||||
"no-array-constructor": 2,
|
|
||||||
"no-bitwise": 0,
|
|
||||||
"no-caller": 2,
|
|
||||||
"no-catch-shadow": 0,
|
|
||||||
"no-cond-assign": 2,
|
|
||||||
"no-console": 0,
|
|
||||||
"no-constant-condition": 0,
|
|
||||||
"no-continue": 0,
|
|
||||||
"no-control-regex": 2,
|
|
||||||
"no-debugger": 2,
|
|
||||||
"no-delete-var": 2,
|
|
||||||
"no-div-regex": 0,
|
|
||||||
"no-dupe-args": 2,
|
|
||||||
"no-dupe-keys": 2,
|
|
||||||
"no-duplicate-case": 2,
|
|
||||||
"no-else-return": 2,
|
|
||||||
"no-empty": 0,
|
|
||||||
"no-empty-character-class": 2,
|
|
||||||
"no-empty-label": 2,
|
|
||||||
"no-eq-null": 0,
|
|
||||||
"no-eval": 2,
|
|
||||||
"no-ex-assign": 2,
|
|
||||||
"no-extend-native": 2,
|
|
||||||
"no-extra-bind": 2,
|
|
||||||
"no-extra-boolean-cast": 2,
|
|
||||||
"no-extra-parens": 0,
|
|
||||||
"no-extra-semi": 0,
|
|
||||||
"no-extra-strict": 0,
|
|
||||||
"no-fallthrough": 2,
|
|
||||||
"no-floating-decimal": 2,
|
|
||||||
"no-func-assign": 2,
|
|
||||||
"no-implied-eval": 2,
|
|
||||||
"no-inline-comments": 0,
|
|
||||||
"no-inner-declarations": [2, "functions"],
|
|
||||||
"no-invalid-regexp": 2,
|
|
||||||
"no-irregular-whitespace": 2,
|
|
||||||
"no-iterator": 2,
|
|
||||||
"no-label-var": 2,
|
|
||||||
"no-labels": 2,
|
|
||||||
"no-lone-blocks": 0,
|
|
||||||
"no-lonely-if": 0,
|
|
||||||
"no-loop-func": 0,
|
|
||||||
"no-mixed-requires": 0,
|
|
||||||
"no-mixed-spaces-and-tabs": [2, false],
|
|
||||||
"no-multi-spaces": 2,
|
|
||||||
"no-multi-str": 2,
|
|
||||||
"no-multiple-empty-lines": [2, { "max": 1 }],
|
|
||||||
"no-native-reassign": 2,
|
|
||||||
"no-negated-in-lhs": 2,
|
|
||||||
"no-nested-ternary": 0,
|
|
||||||
"no-new": 2,
|
|
||||||
"no-new-func": 2,
|
|
||||||
"no-new-object": 2,
|
|
||||||
"no-new-require": 2,
|
|
||||||
"no-new-wrappers": 2,
|
|
||||||
"no-obj-calls": 2,
|
|
||||||
"no-octal": 2,
|
|
||||||
"no-octal-escape": 2,
|
|
||||||
"no-path-concat": 0,
|
|
||||||
"no-plusplus": 0,
|
|
||||||
"no-process-env": 0,
|
|
||||||
"no-process-exit": 0,
|
|
||||||
"no-proto": 2,
|
|
||||||
"no-redeclare": 2,
|
|
||||||
"no-regex-spaces": 2,
|
|
||||||
"no-reserved-keys": 0,
|
|
||||||
"no-restricted-modules": 0,
|
|
||||||
"no-return-assign": 2,
|
|
||||||
"no-script-url": 0,
|
|
||||||
"no-self-compare": 2,
|
|
||||||
"no-sequences": 2,
|
|
||||||
"no-shadow": 0,
|
|
||||||
"no-shadow-restricted-names": 2,
|
|
||||||
"no-spaced-func": 2,
|
|
||||||
"no-sparse-arrays": 2,
|
|
||||||
"no-sync": 0,
|
|
||||||
"no-ternary": 0,
|
|
||||||
"no-throw-literal": 2,
|
|
||||||
"no-trailing-spaces": 2,
|
|
||||||
"no-undef": 2,
|
|
||||||
"no-undef-init": 2,
|
|
||||||
"no-undefined": 0,
|
|
||||||
"no-underscore-dangle": 0,
|
|
||||||
"no-unneeded-ternary": 2,
|
|
||||||
"no-unreachable": 2,
|
|
||||||
"no-unused-expressions": 0,
|
|
||||||
"no-unused-vars": [2, { "vars": "all", "args": "none" }],
|
|
||||||
"no-var": 0,
|
|
||||||
"no-void": 0,
|
|
||||||
"no-warning-comments": 0,
|
|
||||||
"no-with": 2,
|
|
||||||
"one-var": 0,
|
|
||||||
"operator-assignment": 0,
|
|
||||||
"operator-linebreak": [2, "after"],
|
|
||||||
"padded-blocks": 0,
|
|
||||||
"quote-props": 0,
|
|
||||||
"quotes": [2, "single", "avoid-escape"],
|
|
||||||
"radix": 2,
|
|
||||||
"semi": [2, "always"],
|
|
||||||
"semi-spacing": 0,
|
|
||||||
"sort-vars": 0,
|
|
||||||
"space-before-blocks": [2, "always"],
|
|
||||||
"space-before-function-paren": [2, {"anonymous": "always", "named": "never"}],
|
|
||||||
"space-in-brackets": 0,
|
|
||||||
"space-in-parens": [2, "never"],
|
|
||||||
"space-infix-ops": 2,
|
|
||||||
"space-return-throw-case": 2,
|
|
||||||
"space-unary-ops": [2, { "words": true, "nonwords": false }],
|
|
||||||
"spaced-comment": [2, "always"],
|
|
||||||
"strict": 0,
|
|
||||||
"use-isnan": 2,
|
|
||||||
"valid-jsdoc": 0,
|
|
||||||
"valid-typeof": 2,
|
|
||||||
"vars-on-top": 2,
|
|
||||||
"wrap-iife": [2, "any"],
|
|
||||||
"wrap-regex": 0,
|
|
||||||
"yoda": [2, "never"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
BIN
.github/gantt-logo.jpg
vendored
Normal file
BIN
.github/gantt-logo.jpg
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.0 KiB |
BIN
.github/hero-image.png
vendored
Normal file
BIN
.github/hero-image.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 113 KiB |
23
.github/workflows/publish.yml
vendored
Normal file
23
.github/workflows/publish.yml
vendored
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
name: Publish on NPM
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [release]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: 9
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '18'
|
||||||
|
cache: 'pnpm'
|
||||||
|
- run: pnpm install
|
||||||
|
- run: pnpm prettier-check
|
||||||
|
- run: pnpm build
|
||||||
|
- uses: JS-DevTools/npm-publish@v1
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.NPM_TOKEN }}
|
||||||
5
.gitignore
vendored
5
.gitignore
vendored
@ -21,11 +21,14 @@ coverage
|
|||||||
|
|
||||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||||
build/Release
|
build/Release
|
||||||
|
dist/*
|
||||||
|
|
||||||
# Dependency directory
|
# Dependency directory
|
||||||
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
|
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
|
||||||
node_modules
|
node_modules
|
||||||
|
.yarn
|
||||||
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
gh-pages
|
gh-pages
|
||||||
|
feedback*.md
|
||||||
|
|||||||
1
.prettierignore
Normal file
1
.prettierignore
Normal file
@ -0,0 +1 @@
|
|||||||
|
dist
|
||||||
4
.prettierrc.json
Normal file
4
.prettierrc.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"tabWidth": 4,
|
||||||
|
"singleQuote": true
|
||||||
|
}
|
||||||
162
README.md
162
README.md
@ -1,46 +1,158 @@
|
|||||||
# Frappé Gantt
|
<div align="center" markdown="1">
|
||||||
A simple, interactive, modern gantt chart library for the web
|
<img src=".github/gantt-logo.jpg" width="80">
|
||||||
|
<h1>Frappe Gantt</h1>
|
||||||
|
|
||||||

|
**A modern, configurable, Gantt library for the web.**
|
||||||
|
</div>
|
||||||
|
|
||||||
#### View the demo [here](https://frappe.github.io/gantt).
|

|
||||||
|
|
||||||
### Install
|
## Frappe Gantt
|
||||||
```
|
Gantt charts are bar charts that visually illustrate a project's tasks, schedule, and dependencies. With Frappe Gantt, you can build beautiful, customizable, Gantt charts with ease.
|
||||||
|
|
||||||
|
You can use it anywhere from hobby projects to tracking the goals of your team at the worksplace.
|
||||||
|
|
||||||
|
[ERPNext](https://erpnext.com/) uses Frappe Gantt.
|
||||||
|
|
||||||
|
|
||||||
|
### Motivation
|
||||||
|
We needed a Gantt View for ERPNext. Surprisingly, we couldn't find a visually appealing Gantt library that was open source - so we decided to build it. Initially, the design was heavily inspired by Google Gantt and DHTMLX.
|
||||||
|
|
||||||
|
|
||||||
|
### Key Features
|
||||||
|
- **Customizable Views**: customize the timeline based on various time periods - day, hour, or year, you have it. You can also create your own views.
|
||||||
|
- **Ignore Periods**: exclude weekends and other holidays from your tasks' progress calculation.
|
||||||
|
- **Configure Anything**: spacing, edit access, labels, you can control it all. Change both the style and functionality to meet your needs.
|
||||||
|
- **Multi-lingual Support**: suitable for companies with an international base.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Install with:
|
||||||
|
```bash
|
||||||
npm install frappe-gantt
|
npm install frappe-gantt
|
||||||
```
|
```
|
||||||
|
|
||||||
### Usage
|
Include it in your HTML:
|
||||||
Include it in your html:
|
|
||||||
```
|
```html
|
||||||
<script src="frappe-gantt.min.js"></script>
|
<script src="frappe-gantt.umd.js"></script>
|
||||||
|
<link rel="stylesheet" href="frappe-gantt.css">
|
||||||
```
|
```
|
||||||
|
|
||||||
And start hacking:
|
Or from the CDN:
|
||||||
|
```html
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/frappe-gantt/dist/frappe-gantt.umd.js"></script>
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/frappe-gantt/dist/frappe-gantt.css">
|
||||||
|
```
|
||||||
|
|
||||||
|
Start using Gantt:
|
||||||
```js
|
```js
|
||||||
var tasks = [
|
let tasks = [
|
||||||
{
|
{
|
||||||
id: 'Task 1',
|
id: '1',
|
||||||
name: 'Redesign website',
|
name: 'Redesign website',
|
||||||
start: '2016-12-28',
|
start: '2016-12-28',
|
||||||
end: '2016-12-31',
|
end: '2016-12-31',
|
||||||
progress: 20,
|
progress: 20
|
||||||
dependencies: 'Task 2, Task 3',
|
|
||||||
custom_class: 'bar-milestone' // optional
|
|
||||||
},
|
},
|
||||||
...
|
...
|
||||||
]
|
]
|
||||||
var gantt = new Gantt("#gantt", tasks);
|
let gantt = new Gantt("#gantt", tasks);
|
||||||
```
|
```
|
||||||
|
|
||||||
If you want to contribute:
|
### Configuration
|
||||||
|
Frappe Gantt offers a wide range of options to customize your chart.
|
||||||
|
|
||||||
|
|
||||||
|
| **Option** | **Description** | **Possible Values** | **Default** |
|
||||||
|
|---------------------------|---------------------------------------------------------------------------------|----------------------------------------------------|------------------------------------|
|
||||||
|
| `arrow_curve` | Curve radius of arrows connecting dependencies. | Any positive integer. | `5` |
|
||||||
|
| `auto_move_label` | Move task labels when user scrolls horizontally. | `true`, `false` | `false` |
|
||||||
|
| `bar_corner_radius` | Radius of the task bar corners (in pixels). | Any positive integer. | `3` |
|
||||||
|
| `bar_height` | Height of task bars (in pixels). | Any positive integer. | `30` |
|
||||||
|
| `container_height` | Height of the container. | `auto` - dynamic container height to fit all tasks - _or_ any positive integer (for pixels). | `auto` |
|
||||||
|
| `column_width` | Width of each column in the timeline. | Any positive integer. | 45 |
|
||||||
|
| `date_format` | Format for displaying dates. | Any valid JS date format string. | `YYYY-MM-DD` |
|
||||||
|
| `upper_header_height` | Height of the upper header in the timeline (in pixels). | Any positive integer. | `45` |
|
||||||
|
| `lower_header_height` | Height of the lower header in the timeline (in pixels). | Any positive integer. | `30` |
|
||||||
|
| `snap_at` | Snap tasks at particular intervel while resizing or dragging. | Any _interval_ (see below) | `1d` |
|
||||||
|
| `infinite_padding` | Whether to extend timeline infinitely when user scrolls. | `true`, `false` | `true` |
|
||||||
|
| `holidays` | Highlighted holidays on the timeline. | Object mapping CSS colors to holiday types. Types can either be a) 'weekend', or b) array of _strings_ or _date objects_ or _objects_ in the format `{date: ..., label: ...}` | `{ 'var(--g-weekend-highlight-color)': 'weekend' }` |
|
||||||
|
| `ignore` | Ignored areas in the rendering | `weekend` _or_ Array of strings or date objects (`weekend` can be present to the array also). | `[]` |
|
||||||
|
| `language` | Language for localization. | ISO 639-1 codes like `en`, `fr`, `es`. | `en` |
|
||||||
|
| `lines` | Determines which grid lines to display. | `none` for no lines, `vertical` for only vertical lines, `horizontal` for only horizontal lines, `both` for complete grid. | `both` |
|
||||||
|
| `move_dependencies` | Whether moving a task automatically moves its dependencies. | `true`, `false` | `true` |
|
||||||
|
| `padding` | Padding around task bars (in pixels). | Any positive integer. | `18` |
|
||||||
|
| `popup_on` | Event to trigger the popup display. | `click` _or_ `hover` | `click` |
|
||||||
|
| `readonly_progress` | Disables editing task progress. | `true`, `false` | `false` |
|
||||||
|
| `readonly_dates` | Disables editing task dates. | `true`, `false` | `false` |
|
||||||
|
| `readonly` | Disables all editing features. | `true`, `false` | `false` |
|
||||||
|
| `scroll_to` | Determines the starting point when chart is rendered. | `today`, `start`, `end`, or a date string. | `today` |
|
||||||
|
| `show_expected_progress` | Shows expected progress for tasks. | `true`, `false` | `false` |
|
||||||
|
| `today_button` | Adds a button to navigate to today’s date. | `true`, `false` | `true` |
|
||||||
|
| `view_mode` | The initial view mode of the Gantt chart. | `Day`, `Week`, `Month`, `Year`. | `Day` |
|
||||||
|
| `view_mode_select` | Allows selecting the view mode from a dropdown. | `true`, `false` | `false` |
|
||||||
|
|
||||||
|
Apart from these ones, two options - `popup` and `view_modes` (plural, not singular) - are available. They have "sub"-APIs, and thus are listed separately.
|
||||||
|
|
||||||
|
#### View Mode Configuration
|
||||||
|
The `view_modes` option determines all the available view modes for the chart. It should be an array of objects.
|
||||||
|
|
||||||
|
Each object can have the following properties:
|
||||||
|
- `name` (string) - the name of view mode.
|
||||||
|
- `padding` (interval) - the time above.
|
||||||
|
- `step` - the interval of each column
|
||||||
|
- `lower_text` (date format string _or_ function) - the format for text in lower header. Blank string for none. The function takes in `currentDate`, `previousDate`, and `lang`, and should return a string.
|
||||||
|
- `upper_text` (date format string _or_ function) - the format for text in upper header. Blank string for none. The function takes in `currentDate`, `previousDate`, and `lang`, and should return a string.
|
||||||
|
- `upper_text_frequency` (number) - how often the upper text has a value. Utilized in internal calculation to improve performance.
|
||||||
|
- `thick_line` (function) - takes in `currentDate`, returns Boolean determining whether the line for that date should be thicker than the others.
|
||||||
|
|
||||||
|
Three other options allow you to override general configuration for this view mode alone:
|
||||||
|
- `date_format`
|
||||||
|
- `column_width`
|
||||||
|
- `snap_at`
|
||||||
|
For details, see the above table.
|
||||||
|
|
||||||
|
#### Popup Configuration
|
||||||
|
`popup` is a function. If it returns
|
||||||
|
- `false`, there will be no popup.
|
||||||
|
- `undefined`, the popup will be rendered based on manipulation within the function
|
||||||
|
- a HTML string, the popup will be that string.
|
||||||
|
|
||||||
|
The function receives one object as an argument, containing:
|
||||||
|
- `task` - the task as an object
|
||||||
|
- `chart` - the entire Gantt chart
|
||||||
|
- `get_title`, `get_subtitle`, `get_details` (functions) - get the relevant section as a HTML node.
|
||||||
|
- `set_title`, `set_subtitle`, `set_details` (functions) - take in the HTML of the relevant section
|
||||||
|
- `add_action` (function) - accepts two parameters, `html` and `func` - respectively determining the HTML of the action and the callback when the action is pressed.
|
||||||
|
|
||||||
|
### API
|
||||||
|
Frappe Gantt exposes a few helpful methods for you to interact with the chart:
|
||||||
|
|
||||||
|
| **Name** | **Description** | **Parameters** |
|
||||||
|
|---------------------------|---------------------------------------------------------------------------------|------------------------------------------|
|
||||||
|
| `.update_options` | Re-renders the chart after updating specific options. | `new_options` - object containing new options. |
|
||||||
|
| `.change_view_mode` | Updates the view mode. | `view_mode` - Name of view mode _or_ view mode object (see above) and `maintain_pos` - whether to go back to current scroll position after rerendering, defaults to `false`. |
|
||||||
|
| `.scroll_current` | Scrolls to the current date | No parameters. |
|
||||||
|
| `.update_task` | Re-renders a specific task bar alone | `task_id` - id of task and `new_details` - object containing the task properties to be updated. |
|
||||||
|
|
||||||
|
## Development Setup
|
||||||
|
If you want to contribute enhancements or fixes:
|
||||||
|
|
||||||
1. Clone this repo.
|
1. Clone this repo.
|
||||||
2. `cd` into project directory
|
2. `cd` into project directory.
|
||||||
3. `npm install`
|
3. Run `pnpm i` to install dependencies.
|
||||||
4. `npm run dev`
|
4. `pnpm run build` to build files - or `pnpm run build-dev` to build and watch for changes.
|
||||||
|
5. Open `index.html` in your browser.
|
||||||
|
6. Make your code changes and test them.
|
||||||
|
|
||||||
License: MIT
|
<br />
|
||||||
|
<br />
|
||||||
------------------
|
<div align="center" style="padding-top: 0.75rem;">
|
||||||
Project maintained by [frappe](https://github.com/frappe)
|
<a href="https://frappe.io" target="_blank">
|
||||||
|
<picture>
|
||||||
|
<source media="(prefers-color-scheme: dark)" srcset="https://frappe.io/files/Frappe-white.png">
|
||||||
|
<img src="https://frappe.io/files/Frappe-black.png" alt="Frappe Technologies" height="28"/>
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|||||||
115
builder/demo.css
Normal file
115
builder/demo.css
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
.switch {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
width: 50px;
|
||||||
|
height: 20px;
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch input {
|
||||||
|
opacity: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider {
|
||||||
|
position: absolute;
|
||||||
|
cursor: pointer;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: #ddd;
|
||||||
|
-webkit-transition: 0.2s;
|
||||||
|
transition: 0.2s;
|
||||||
|
border: 1px solid #37352f;
|
||||||
|
scale: 0.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider:before {
|
||||||
|
position: absolute;
|
||||||
|
content: '';
|
||||||
|
height: 12px;
|
||||||
|
width: 12px;
|
||||||
|
left: 4px;
|
||||||
|
bottom: 3px;
|
||||||
|
background-color: white;
|
||||||
|
-webkit-transition: 0.2s;
|
||||||
|
transition: 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked + .slider {
|
||||||
|
background-color: #7c7c7c;
|
||||||
|
border-color: #7c7c7c;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus + .slider {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked + .slider:before {
|
||||||
|
-webkit-transform: translateX(28px);
|
||||||
|
-ms-transform: translateX(28px);
|
||||||
|
transform: translateX(28px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider.round {
|
||||||
|
border-radius: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider.round:before {
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.viewmode-select {
|
||||||
|
font-size: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected {
|
||||||
|
border: 1.5px solid black !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
background: white;
|
||||||
|
border: 1px dotted black;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:hover {
|
||||||
|
background: #f4f5f6;
|
||||||
|
border: 1px dotted black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button div {
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-switch {
|
||||||
|
align-items: center;
|
||||||
|
width: 45%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-switch label {
|
||||||
|
padding-right: 30px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code {
|
||||||
|
display: block;
|
||||||
|
background: 0;
|
||||||
|
white-space: pre;
|
||||||
|
overflow-x: scroll;
|
||||||
|
max-width: 100%;
|
||||||
|
min-width: 100px;
|
||||||
|
padding: 0;
|
||||||
|
font-family: monospace;
|
||||||
|
padding-top: 0.8571429em;
|
||||||
|
padding-right: 1.1428571em;
|
||||||
|
padding-bottom: 0.8571429em;
|
||||||
|
padding-left: 1.1428571em;
|
||||||
|
background: #1f2937;
|
||||||
|
color: #e5e7eb;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
326
builder/demo.js
Normal file
326
builder/demo.js
Normal file
@ -0,0 +1,326 @@
|
|||||||
|
const tasks = [
|
||||||
|
{
|
||||||
|
start: daysSince(-7),
|
||||||
|
end: daysSince(-5),
|
||||||
|
name: 'Initial brainstorming',
|
||||||
|
id: 'Task 0',
|
||||||
|
progress: random(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
start: daysSince(-3),
|
||||||
|
end: daysSince(1),
|
||||||
|
name: 'Develop wireframe',
|
||||||
|
id: 'Task 1',
|
||||||
|
progress: random(),
|
||||||
|
dependencies: 'Task 0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
start: daysSince(-1),
|
||||||
|
duration: '4d',
|
||||||
|
name: 'Client meeting',
|
||||||
|
id: 'Task 2',
|
||||||
|
progress: random(),
|
||||||
|
important: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
start: daysSince(1),
|
||||||
|
duration: '7d',
|
||||||
|
name: 'Create prototype',
|
||||||
|
id: 'Task 3',
|
||||||
|
dependencies: 'Task 2',
|
||||||
|
progress: random(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
start: daysSince(3),
|
||||||
|
duration: '5d',
|
||||||
|
name: 'Test design with users',
|
||||||
|
dependencies: 'Task 2',
|
||||||
|
id: 'Task 4',
|
||||||
|
progress: random(),
|
||||||
|
important: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
start: daysSince(5),
|
||||||
|
end: daysSince(10),
|
||||||
|
name: 'Write technical documentation',
|
||||||
|
id: 'Task 5',
|
||||||
|
progress: random(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
start: daysSince(8),
|
||||||
|
duration: '3d',
|
||||||
|
name: 'Prepare demo',
|
||||||
|
id: 'Task 6',
|
||||||
|
dependencies: 'Task 5',
|
||||||
|
progress: random(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
start: daysSince(10),
|
||||||
|
end: daysSince(12),
|
||||||
|
name: 'Final client review',
|
||||||
|
id: 'Task 7',
|
||||||
|
progress: 0,
|
||||||
|
important: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
start: daysSince(14),
|
||||||
|
duration: '6d',
|
||||||
|
name: 'Implement feedback',
|
||||||
|
id: 'Task 8',
|
||||||
|
progress: 0,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const tasksSmall = [
|
||||||
|
{
|
||||||
|
start: daysSince(-2),
|
||||||
|
end: daysSince(2),
|
||||||
|
name: 'Redesign website',
|
||||||
|
id: 'Task 0',
|
||||||
|
progress: random(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
start: daysSince(3),
|
||||||
|
duration: '6d',
|
||||||
|
name: 'Write new content',
|
||||||
|
id: 'Task 1',
|
||||||
|
progress: random(),
|
||||||
|
important: true,
|
||||||
|
dependencies: 'Task 0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
start: daysSince(4),
|
||||||
|
duration: '2d',
|
||||||
|
name: 'Apply new styles',
|
||||||
|
id: 'Task 2',
|
||||||
|
progress: random(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
start: daysSince(-4),
|
||||||
|
end: daysSince(0),
|
||||||
|
name: 'Review',
|
||||||
|
id: 'Task 3',
|
||||||
|
progress: random(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const tasksBlank = [
|
||||||
|
{
|
||||||
|
start: daysSince(1),
|
||||||
|
duration: '3d',
|
||||||
|
name: 'Marketing Strategy Review',
|
||||||
|
id: 'Task 1',
|
||||||
|
important: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
start: daysSince(-2),
|
||||||
|
end: daysSince(12),
|
||||||
|
name: 'Mentor Sooriya',
|
||||||
|
id: 'Task 0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
start: daysSince(4),
|
||||||
|
end: daysSince(5),
|
||||||
|
name: 'Investors Meetup',
|
||||||
|
id: 'Task 3',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const HOLIDAYS = [
|
||||||
|
{ name: 'New Years Day', date: '2025-01-01' },
|
||||||
|
{ name: 'Republic Day', date: '2025-01-26' },
|
||||||
|
{ name: 'Maha Shivratri', date: '2025-02-23' },
|
||||||
|
{ name: 'Holi', date: '2025-03-11' },
|
||||||
|
{ name: 'Mahavir Jayanthi', date: '2025-04-07' },
|
||||||
|
{ name: 'Good Friday', date: '2025-04-10' },
|
||||||
|
{ name: 'May Day', date: '2025-05-01' },
|
||||||
|
{ name: 'Buddha Purnima', date: '2025-05-08' },
|
||||||
|
{ name: 'Krishna Janmastami', date: '2025-08-14' },
|
||||||
|
{ name: 'Independence Day', date: '2025-08-15' },
|
||||||
|
{ name: 'Ganesh Chaturthi', date: '2025-08-23' },
|
||||||
|
{ name: 'Id-Ul-Fitr', date: '2025-09-21' },
|
||||||
|
{ name: 'Vijaya Dashami', date: '2025-09-28' },
|
||||||
|
{ name: 'Mahatma Gandhi Jayanti', date: '2025-10-02' },
|
||||||
|
{ name: 'Diwali', date: '2025-10-17' },
|
||||||
|
{ name: 'Guru Nanak Jayanthi', date: '2025-11-02' },
|
||||||
|
{ name: 'Christmas', date: '2025-12-25' },
|
||||||
|
];
|
||||||
|
|
||||||
|
new Gantt('#central-demo', tasks, {
|
||||||
|
scroll_to: daysSince(-7),
|
||||||
|
infinite_padding: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const sideheader = new Gantt('#sideheader', tasksSmall, {
|
||||||
|
scroll_to: daysSince(-20),
|
||||||
|
view_mode_select: true,
|
||||||
|
infinite_padding: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const popup = new Gantt('#popup', tasksBlank, {
|
||||||
|
scroll_to: daysSince(-7),
|
||||||
|
infinite_padding: false,
|
||||||
|
container_height: 350,
|
||||||
|
popup: (ctx) => {
|
||||||
|
ctx.set_title(ctx.task.name);
|
||||||
|
let title = ctx.get_title();
|
||||||
|
title.style.border = '0.5px solid black';
|
||||||
|
title.style.borderRadius = '1.5px';
|
||||||
|
title.style.padding = '3px 5px ';
|
||||||
|
title.style.backgroundColor = 'black';
|
||||||
|
title.style.opacity = '0.85';
|
||||||
|
title.style.color = 'white';
|
||||||
|
title.style.width = 'fit-content';
|
||||||
|
title.onclick = () => {
|
||||||
|
let ans = prompt('New Title: ');
|
||||||
|
if (ans) ctx.set_title(ans);
|
||||||
|
};
|
||||||
|
if (ctx.task.description) ctx.set_subtitle(ctx.task.description);
|
||||||
|
else ctx.set_subtitle('');
|
||||||
|
|
||||||
|
ctx.set_details(
|
||||||
|
`<em>Duration</em>: ${ctx.task.actual_duration} days<br/><em>Dates</em>: ${ctx.task._start.toLocaleDateString('en-US')} - ${ctx.task._end.toLocaleDateString('en-US')}`,
|
||||||
|
);
|
||||||
|
let details = ctx.get_details();
|
||||||
|
details.style.lineHeight = '1.75';
|
||||||
|
details.style.margin = '10px 4px';
|
||||||
|
if (!ctx.chart.options.readonly) {
|
||||||
|
if (!ctx.chart.options.readonly_progress) {
|
||||||
|
ctx.add_action('Set Color', (task, chart) => {
|
||||||
|
const bar = chart.bars.find(
|
||||||
|
({ task: t }) => t.id === task.id,
|
||||||
|
).$bar;
|
||||||
|
bar.style.fill = `hsla(${~~(360 * Math.random())}, 70%, 72%, 0.8)`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const holidays = new Gantt('#holidays', tasks, {
|
||||||
|
holidays: {
|
||||||
|
'var(--g-weekend-highlight-color)': [],
|
||||||
|
'#fffddb': HOLIDAYS,
|
||||||
|
},
|
||||||
|
ignore: ['weekend'],
|
||||||
|
infinite_padding: false,
|
||||||
|
container_height: 350,
|
||||||
|
scroll_to: daysSince(-7),
|
||||||
|
});
|
||||||
|
|
||||||
|
SWITCHES = {
|
||||||
|
'sideheader-form': {
|
||||||
|
'toggle-today': 'Scroll to today: ',
|
||||||
|
'toggle-view-mode': 'Change view mode: ',
|
||||||
|
},
|
||||||
|
'holiday-subform': {
|
||||||
|
'toggle-weekends': ['Mark weekends: ', false],
|
||||||
|
'ignore-weekends': 'Exclude weekends: ',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let form of ['sideheader-form', 'holiday-form']) {
|
||||||
|
let formNode = document.getElementById(form);
|
||||||
|
let parent = formNode.parentElement;
|
||||||
|
parent.appendChild(formNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let form in SWITCHES) {
|
||||||
|
for (let id in SWITCHES[form]) {
|
||||||
|
createSwitch(form, id, SWITCHES[form][id]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const UPDATES = [
|
||||||
|
[
|
||||||
|
sideheader,
|
||||||
|
{
|
||||||
|
'toggle-today': 'today_button',
|
||||||
|
'toggle-view-mode': 'view_mode_select',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
holidays,
|
||||||
|
{
|
||||||
|
'toggle-weekends': (val, opts) => ({
|
||||||
|
holidays: {
|
||||||
|
'#fffddb': opts.holidays['#fffddb'],
|
||||||
|
'var(--g-weekend-highlight-color)': val ? 'weekend' : [],
|
||||||
|
},
|
||||||
|
ignore: [],
|
||||||
|
}),
|
||||||
|
'declare-holiday': (val, opts) => ({
|
||||||
|
holidays: {
|
||||||
|
'#fffddb': [...HOLIDAYS, { date: val, name: 'Kay' }],
|
||||||
|
'var(--g-weekend-highlight-color)':
|
||||||
|
opts.holidays['var(--g-weekend-highlight-color)'],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
'ignore-weekends': (val, opts) => ({
|
||||||
|
ignore: [
|
||||||
|
opts.ignore.filter((k) => k !== 'weekend')[0],
|
||||||
|
...(val ? ['weekend'] : []),
|
||||||
|
],
|
||||||
|
holidays: { '#fffddb': opts.holidays['#fffddb'] },
|
||||||
|
}),
|
||||||
|
'declare-ignore': (val, opts) => ({
|
||||||
|
ignore: [
|
||||||
|
...(opts.ignore.includes('weekend') ? ['weekend'] : []),
|
||||||
|
val,
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
(id, val) => {
|
||||||
|
let el = document.getElementById(id);
|
||||||
|
if (id === 'toggle-weekends' && val) {
|
||||||
|
document.getElementById('ignore-weekends').checked = false;
|
||||||
|
}
|
||||||
|
if (id === 'ignore-weekends' && val) {
|
||||||
|
document.getElementById('toggle-weekends').checked = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
for (let [chart, details, after] of UPDATES) {
|
||||||
|
for (let id in details) {
|
||||||
|
let el = document.getElementById(id);
|
||||||
|
|
||||||
|
el.onchange = (e) => {
|
||||||
|
let label = details[id];
|
||||||
|
let val;
|
||||||
|
if (e.currentTarget.type === 'checkbox') {
|
||||||
|
if (typeof label === 'string') {
|
||||||
|
let opposite = label.slice(0, 5) === 'opp__';
|
||||||
|
if (opposite) label = label.slice(5);
|
||||||
|
val = opposite
|
||||||
|
? !e.currentTarget.checked
|
||||||
|
: e.currentTarget.checked;
|
||||||
|
} else if (typeof label === 'object') {
|
||||||
|
val = label[e.currentTarget.checked ? 1 : 2];
|
||||||
|
label = label[0];
|
||||||
|
} else {
|
||||||
|
val =
|
||||||
|
e.currentTarget.type === 'checkbox'
|
||||||
|
? e.currentTarget.checked
|
||||||
|
: e.currentTarget.value;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val =
|
||||||
|
e.currentTarget.type === 'date'
|
||||||
|
? e.currentTarget.value
|
||||||
|
: +e.currentTarget.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof label === 'function') {
|
||||||
|
console.log('ha', label(val, chart.options));
|
||||||
|
chart.update_options(label(val, chart.options));
|
||||||
|
} else {
|
||||||
|
chart.update_options({
|
||||||
|
[label]: val,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
after && after(id, val, chart);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
1942
dist/frappe-gantt.js
vendored
1942
dist/frappe-gantt.js
vendored
File diff suppressed because one or more lines are too long
1
dist/frappe-gantt.js.map
vendored
1
dist/frappe-gantt.js.map
vendored
File diff suppressed because one or more lines are too long
6
dist/frappe-gantt.min.js
vendored
6
dist/frappe-gantt.min.js
vendored
File diff suppressed because one or more lines are too long
5
dist/frappe-gantt.min.js.map
vendored
5
dist/frappe-gantt.min.js.map
vendored
File diff suppressed because one or more lines are too long
19
eslint.config.mjs
Normal file
19
eslint.config.mjs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import js from "@eslint/js";
|
||||||
|
import { FlatCompat } from "@eslint/eslintrc";
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
const compat = new FlatCompat({
|
||||||
|
baseDirectory: __dirname,
|
||||||
|
recommendedConfig: js.configs.recommended,
|
||||||
|
allConfig: js.configs.all
|
||||||
|
});
|
||||||
|
|
||||||
|
export default [...compat.extends("plugin:prettier/recommended"), {
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 6,
|
||||||
|
sourceType: "module",
|
||||||
|
},
|
||||||
|
}];
|
||||||
861
index.html
861
index.html
@ -1,82 +1,785 @@
|
|||||||
<!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>
|
<link rel="stylesheet" href="dist/frappe-gantt.css" />
|
||||||
body {
|
<link
|
||||||
font-family: sans-serif;
|
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css"
|
||||||
background: #ccc;
|
rel="stylesheet"
|
||||||
}
|
integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN"
|
||||||
.container {
|
crossorigin="anonymous"
|
||||||
width: 80%;
|
/>
|
||||||
margin: 0 auto;
|
<style>
|
||||||
}
|
.container {
|
||||||
.gantt-container {
|
width: 90%;
|
||||||
overflow: scroll;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
/* custom class */
|
|
||||||
.gantt .bar-milestone .bar-progress {
|
|
||||||
fill: tomato;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<script src="node_modules/moment/min/moment.min.js"></script>
|
|
||||||
<script src="node_modules/snapsvg/dist/snap.svg-min.js"></script>
|
|
||||||
<script src="dist/frappe-gantt.js"></script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<h2>Interactive Gantt Chart entirely made in SVG!</h2>
|
|
||||||
<div class="gantt-container">
|
|
||||||
<svg id="gantt" width="400" height="600"></svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<script>
|
|
||||||
var names = [
|
|
||||||
["Redesign website", [0, 7]],
|
|
||||||
["Write new content", [1, 4]],
|
|
||||||
["Apply new styles", [3, 6]],
|
|
||||||
["Review", [7, 7]],
|
|
||||||
["Deploy", [8, 9]],
|
|
||||||
["Go Live!", [10, 10]]
|
|
||||||
];
|
|
||||||
|
|
||||||
var tasks = names.map(function(name, i) {
|
.chart {
|
||||||
var today = new Date();
|
border: 1px dotted black;
|
||||||
var start = new Date(today.getFullYear(), today.getMonth(), today.getDate());
|
border-radius: 4px;
|
||||||
var end = new Date(today.getFullYear(), today.getMonth(), today.getDate());
|
height: fit-content;
|
||||||
start.setDate(today.getDate() + name[1][0]);
|
}
|
||||||
end.setDate(today.getDate() + name[1][1]);
|
|
||||||
return {
|
|
||||||
start: start,
|
|
||||||
end: end,
|
|
||||||
name: name[0],
|
|
||||||
id: "Task " + i,
|
|
||||||
progress: parseInt(Math.random() * 100, 10)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
tasks[1].progress = 0;
|
|
||||||
tasks[1].dependencies = "Task 0"
|
|
||||||
tasks[2].dependencies = "Task 1"
|
|
||||||
tasks[3].dependencies = "Task 2"
|
|
||||||
tasks[5].dependencies = "Task 4"
|
|
||||||
tasks[5].custom_class = "bar-milestone";
|
|
||||||
|
|
||||||
var gantt_chart = Gantt("#gantt", tasks, {
|
.chart.active {
|
||||||
on_click: function (task) {
|
filter: drop-shadow(1px 1px 4px rgba(0, 0, 0, 0.6));
|
||||||
console.log(task);
|
border: unset;
|
||||||
},
|
}
|
||||||
on_date_change: function(task, start, end) {
|
|
||||||
console.log(task, start, end);
|
small {
|
||||||
},
|
font-size: 0.775em;
|
||||||
on_progress_change: function(task, progress) {
|
}
|
||||||
console.log(task, progress);
|
</style>
|
||||||
},
|
<script src="dist/frappe-gantt.umd.js"></script>
|
||||||
on_view_change: function(mode) {
|
</head>
|
||||||
console.log(mode);
|
<body>
|
||||||
}
|
<div class="container">
|
||||||
});
|
<h1 class="text-center pt-3 pb-2 font-serif">Frappe Gantt</h1>
|
||||||
console.log(gantt_chart);
|
<hr />
|
||||||
</script>
|
<div class="row my-5">
|
||||||
</body>
|
<div class="col-md-3 px-5 py-1">
|
||||||
</html>
|
<h3 class="text-center">Set edit access</h3>
|
||||||
|
<p>
|
||||||
|
Easy make sure your employees change <em>only</em> what
|
||||||
|
they need to.
|
||||||
|
</p>
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input
|
||||||
|
class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
role="switch"
|
||||||
|
id="mutable-general"
|
||||||
|
checked
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="mutable-general"
|
||||||
|
>Editable</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input
|
||||||
|
class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
role="switch"
|
||||||
|
id="mutable-progress"
|
||||||
|
checked
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="mutable-general"
|
||||||
|
>Progress editable</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input
|
||||||
|
class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
role="switch"
|
||||||
|
id="mutable-dates"
|
||||||
|
checked
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="mutable-general"
|
||||||
|
>Dates editable</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="chart col-md-9" id="mutability"></div>
|
||||||
|
</div>
|
||||||
|
<div class="row my-5">
|
||||||
|
<div class="chart col-md-9" id="sideheader"></div>
|
||||||
|
<div class="col-md-3 px-5 py-1">
|
||||||
|
<h3 class="text-center">Versatile Actions</h3>
|
||||||
|
<p>
|
||||||
|
Change the view mode, or scroll to today, or add
|
||||||
|
anything you like <sup>β</sup>.
|
||||||
|
</p>
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input
|
||||||
|
class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
role="switch"
|
||||||
|
id="toggle-today"
|
||||||
|
checked
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="mutable-general"
|
||||||
|
>Scroll to Today</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input
|
||||||
|
class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
role="switch"
|
||||||
|
id="toggle-view-mode"
|
||||||
|
checked
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="mutable-general"
|
||||||
|
>Change View Mode</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row my-5">
|
||||||
|
<div class="col-md-3 px-5 py-1">
|
||||||
|
<h3 class="text-center">Mark Holidays</h3>
|
||||||
|
<p>
|
||||||
|
Be it public holidays, company milestones, or just
|
||||||
|
weekends, you can see it all.
|
||||||
|
</p>
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input
|
||||||
|
class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
role="switch"
|
||||||
|
id="toggle-weekends"
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="toggle-weekends"
|
||||||
|
>Show weekends</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="chart col-md-9" id="holidays"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row my-5">
|
||||||
|
<div class="col-md-3 px-5 py-1">
|
||||||
|
<h3 class="text-center">...or <em>ignore</em> them</h3>
|
||||||
|
<p>
|
||||||
|
Remove time periods from your Gantt - they're now
|
||||||
|
completely ignored.
|
||||||
|
</p>
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input
|
||||||
|
class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
role="switch"
|
||||||
|
id="ignore-weekends"
|
||||||
|
checked
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="toggle-weekends"
|
||||||
|
>Ignore weekends</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="chart col-md-9" id="ignore"></div>
|
||||||
|
</div>
|
||||||
|
<div class="row my-4">
|
||||||
|
<div class="col-md-9 chart" id="styling"></div>
|
||||||
|
<div class="col-md-3 px-4">
|
||||||
|
<h3 class="text-center">Control the styles completely.</h3>
|
||||||
|
<strong>Modify Grid</strong>
|
||||||
|
<div class="input-group row">
|
||||||
|
<label
|
||||||
|
for="grid-height"
|
||||||
|
class="form-label col-sm-5 col-form-label"
|
||||||
|
><small>Grid Height:</small></label
|
||||||
|
>
|
||||||
|
<div class="col-sm-7">
|
||||||
|
<input
|
||||||
|
id="grid-height"
|
||||||
|
class="form-range align-items-end"
|
||||||
|
type="range"
|
||||||
|
min="150"
|
||||||
|
max="600"
|
||||||
|
value="300"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="input-group row">
|
||||||
|
<label
|
||||||
|
for="padding"
|
||||||
|
class="form-label col-sm-5 col-form-label"
|
||||||
|
><small>Padding:</small></label
|
||||||
|
>
|
||||||
|
<div class="col-sm-7">
|
||||||
|
<input
|
||||||
|
id="padding"
|
||||||
|
class="form-range align-items-end"
|
||||||
|
type="range"
|
||||||
|
min="3"
|
||||||
|
max="50"
|
||||||
|
value="18"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="input-group row">
|
||||||
|
<label
|
||||||
|
for="column-width"
|
||||||
|
class="form-label col-sm-5 col-form-label"
|
||||||
|
><small>Column Width:</small></label
|
||||||
|
>
|
||||||
|
<div class="col-sm-7">
|
||||||
|
<input
|
||||||
|
id="column-width"
|
||||||
|
class="form-range align-items-end"
|
||||||
|
type="range"
|
||||||
|
min="30"
|
||||||
|
max="70"
|
||||||
|
value="30"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="pt-3">
|
||||||
|
<strong>Modify Bar</strong>
|
||||||
|
</div>
|
||||||
|
<div class="input-group row">
|
||||||
|
<label
|
||||||
|
for="bar-height"
|
||||||
|
class="form-label col-sm-5 col-form-label"
|
||||||
|
><small>Height:</small></label
|
||||||
|
>
|
||||||
|
<div class="col-sm-7">
|
||||||
|
<input
|
||||||
|
id="bar-height"
|
||||||
|
class="form-range align-items-end"
|
||||||
|
type="range"
|
||||||
|
min="10"
|
||||||
|
max="100"
|
||||||
|
value="30"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="input-group row">
|
||||||
|
<label
|
||||||
|
for="bar-radius"
|
||||||
|
class="form-label col-sm-5 col-form-label"
|
||||||
|
><small>Radius:</small></label
|
||||||
|
>
|
||||||
|
<div class="col-sm-7">
|
||||||
|
<input
|
||||||
|
id="bar-radius"
|
||||||
|
class="form-range align-items-end"
|
||||||
|
type="range"
|
||||||
|
min="1"
|
||||||
|
max="50"
|
||||||
|
value="3"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="input-group row">
|
||||||
|
<label
|
||||||
|
for="arrow-curve"
|
||||||
|
class="form-label col-sm-5 col-form-label"
|
||||||
|
><small>Arrow curving:</small></label
|
||||||
|
>
|
||||||
|
<div class="col-sm-7">
|
||||||
|
<input
|
||||||
|
id="arrow-curve"
|
||||||
|
class="form-range align-items-end"
|
||||||
|
type="range"
|
||||||
|
min="1"
|
||||||
|
max="50"
|
||||||
|
value="3"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row my-4">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<h2>Frappe Gantt - <em>for you</em>.</h2>
|
||||||
|
<p>
|
||||||
|
Insane levels of customizability - change anything,
|
||||||
|
everything.
|
||||||
|
</p>
|
||||||
|
<div class="input-group">
|
||||||
|
<label class="input-group-text">Snap By: </label>
|
||||||
|
<input
|
||||||
|
class="form-control"
|
||||||
|
id="snap-at-qty"
|
||||||
|
type="number"
|
||||||
|
value="1"
|
||||||
|
/>
|
||||||
|
<select class="form-select" id="snap-at-scale">
|
||||||
|
<option value="s">Second</option>
|
||||||
|
<option value="min">Minute</option>
|
||||||
|
<option value="h">Hour</option>
|
||||||
|
<option value="d" selected>Day</option>
|
||||||
|
<option value="m">Month</option>
|
||||||
|
<option value="y">Year</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-switch my-2">
|
||||||
|
<label class="form-check-label" for="auto-move-label"
|
||||||
|
>Toggle auto-moving label</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
role="switch"
|
||||||
|
id="auto-move-label"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-9 chart" id="advanced"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script type="module">
|
||||||
|
const rawToday = new Date();
|
||||||
|
const today =
|
||||||
|
Date.UTC(
|
||||||
|
rawToday.getFullYear(),
|
||||||
|
rawToday.getMonth(),
|
||||||
|
rawToday.getDate(),
|
||||||
|
) +
|
||||||
|
new Date().getTimezoneOffset() * 60000;
|
||||||
|
|
||||||
|
function random(begin = 10, end = 90, multiple = 10) {
|
||||||
|
let k;
|
||||||
|
do {
|
||||||
|
k = Math.floor(Math.random() * 100);
|
||||||
|
} while (k < begin || k > end || k % multiple !== 0);
|
||||||
|
return k;
|
||||||
|
}
|
||||||
|
|
||||||
|
const daysSince = (dx) => new Date(today + dx * 86400000);
|
||||||
|
let tasks = [
|
||||||
|
{
|
||||||
|
start: daysSince(-2),
|
||||||
|
end: daysSince(2),
|
||||||
|
name: 'Redesign website',
|
||||||
|
id: 'Task 0',
|
||||||
|
progress: random(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
start: daysSince(3),
|
||||||
|
duration: '6d',
|
||||||
|
name: 'Write new content',
|
||||||
|
id: 'Task 1',
|
||||||
|
progress: random(),
|
||||||
|
important: true,
|
||||||
|
dependencies: 'Task 0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
start: daysSince(4),
|
||||||
|
duration: '2d',
|
||||||
|
name: 'Apply new styles',
|
||||||
|
id: 'Task 2',
|
||||||
|
progress: random(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
start: daysSince(-4),
|
||||||
|
end: daysSince(0),
|
||||||
|
name: 'Review',
|
||||||
|
id: 'Task 3',
|
||||||
|
progress: random(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const tasksSpread = [
|
||||||
|
{
|
||||||
|
start: daysSince(-30),
|
||||||
|
end: daysSince(-10),
|
||||||
|
name: 'Redesign website',
|
||||||
|
id: 'Task 0',
|
||||||
|
progress: random(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
start: daysSince(-15),
|
||||||
|
duration: '21d',
|
||||||
|
name: 'Write new content',
|
||||||
|
id: 'Task 1',
|
||||||
|
progress: random(),
|
||||||
|
important: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
start: daysSince(10),
|
||||||
|
duration: '14d',
|
||||||
|
name: 'Review',
|
||||||
|
id: 'Task 3',
|
||||||
|
progress: random(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
start: daysSince(-3),
|
||||||
|
duration: '4d',
|
||||||
|
name: 'Publish',
|
||||||
|
id: 'Task 4',
|
||||||
|
progress: random(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const tasksDependencies = [
|
||||||
|
{
|
||||||
|
start: daysSince(-2),
|
||||||
|
end: daysSince(2),
|
||||||
|
name: 'Redesign website',
|
||||||
|
id: 'Task 0',
|
||||||
|
progress: random(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
start: daysSince(3),
|
||||||
|
duration: '6d',
|
||||||
|
name: 'Write new content',
|
||||||
|
id: 'Task 1',
|
||||||
|
progress: random(),
|
||||||
|
dependencies: 'Task 0',
|
||||||
|
important: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
start: daysSince(4),
|
||||||
|
duration: '2d',
|
||||||
|
name: 'Apply new styles',
|
||||||
|
id: 'Task 2',
|
||||||
|
progress: random(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
start: daysSince(-4),
|
||||||
|
end: daysSince(0),
|
||||||
|
name: 'Review',
|
||||||
|
id: 'Task 3',
|
||||||
|
custom_class: 'readonly',
|
||||||
|
progress: random(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
let tasksMany = [
|
||||||
|
{
|
||||||
|
start: daysSince(-7),
|
||||||
|
end: daysSince(-5),
|
||||||
|
name: 'Initial brainstorming',
|
||||||
|
id: 'Task 0',
|
||||||
|
progress: random(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
start: daysSince(-3),
|
||||||
|
end: daysSince(1),
|
||||||
|
name: 'Develop wireframe',
|
||||||
|
id: 'Task 1',
|
||||||
|
progress: random(),
|
||||||
|
dependencies: 'Task 0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
start: daysSince(-1),
|
||||||
|
duration: '4d',
|
||||||
|
name: 'Client meeting',
|
||||||
|
id: 'Task 2',
|
||||||
|
progress: random(),
|
||||||
|
important: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
start: daysSince(1),
|
||||||
|
duration: '7d',
|
||||||
|
name: 'Create prototype',
|
||||||
|
id: 'Task 3',
|
||||||
|
dependencies: 'Task 2',
|
||||||
|
progress: random(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
start: daysSince(3),
|
||||||
|
duration: '5d',
|
||||||
|
name: 'Test design with users',
|
||||||
|
dependencies: 'Task 2',
|
||||||
|
id: 'Task 4',
|
||||||
|
progress: random(),
|
||||||
|
important: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
start: daysSince(5),
|
||||||
|
end: daysSince(10),
|
||||||
|
name: 'Write technical documentation',
|
||||||
|
id: 'Task 5',
|
||||||
|
progress: random(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
start: daysSince(8),
|
||||||
|
duration: '3d',
|
||||||
|
name: 'Prepare demo',
|
||||||
|
id: 'Task 6',
|
||||||
|
progress: random(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
start: daysSince(10),
|
||||||
|
end: daysSince(12),
|
||||||
|
name: 'Final client review',
|
||||||
|
id: 'Task 7',
|
||||||
|
progress: random(),
|
||||||
|
important: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
start: daysSince(14),
|
||||||
|
duration: '6d',
|
||||||
|
name: 'Implement feedback',
|
||||||
|
id: 'Task 8',
|
||||||
|
progress: random(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
start: daysSince(16),
|
||||||
|
duration: '4d',
|
||||||
|
name: 'Launch website',
|
||||||
|
id: 'Task 9',
|
||||||
|
progress: random(),
|
||||||
|
important: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const HOLIDAYS = [
|
||||||
|
{ name: 'Republic Day', date: '2024-01-26' },
|
||||||
|
{ name: 'Maha Shivratri', date: '2024-02-23' },
|
||||||
|
{ name: 'Holi', date: '2024-03-11' },
|
||||||
|
{ name: 'Mahavir Jayanthi', date: '2024-04-07' },
|
||||||
|
{ name: 'Good Friday', date: '2024-04-10' },
|
||||||
|
{ name: 'May Day', date: '2024-05-01' },
|
||||||
|
{ name: 'Buddha Purnima', date: '2024-05-08' },
|
||||||
|
{ name: 'Krishna Janmastami', date: '2024-08-14' },
|
||||||
|
{ name: 'Independence Day', date: '2024-08-15' },
|
||||||
|
{ name: 'Ganesh Chaturthi', date: '2024-08-23' },
|
||||||
|
{ name: 'Id-Ul-Fitr', date: '2024-09-21' },
|
||||||
|
{ name: 'Vijaya Dashami', date: '2024-09-28' },
|
||||||
|
{ name: 'Mahatma Gandhi Jayanti', date: '2024-10-02' },
|
||||||
|
{ name: 'Diwali', date: '2024-10-17' },
|
||||||
|
{ name: 'Guru Nanak Jayanthi', date: '2024-11-02' },
|
||||||
|
{ name: 'Christmas', date: '2024-12-25' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const mutablity = new Gantt('#mutability', tasks);
|
||||||
|
const sideheader = new Gantt('#sideheader', tasksSpread, {
|
||||||
|
today_button: true,
|
||||||
|
view_mode_select: true,
|
||||||
|
holidays: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const holidays = new Gantt('#holidays', tasksSpread, {
|
||||||
|
holidays: {
|
||||||
|
'#bfdbfe': [],
|
||||||
|
'#a3e635': HOLIDAYS,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const ignore = new Gantt('#ignore', tasks, {
|
||||||
|
ignore: ['weekend', ...HOLIDAYS.map((k) => k.date)],
|
||||||
|
holidays: null,
|
||||||
|
scroll_to: daysSince(-10),
|
||||||
|
});
|
||||||
|
|
||||||
|
const styling = new Gantt('#styling', tasksMany, {
|
||||||
|
holidays: null,
|
||||||
|
scroll_to: daysSince(-10),
|
||||||
|
});
|
||||||
|
|
||||||
|
const advanced = new Gantt('#advanced', tasksSpread, {
|
||||||
|
holidays: null,
|
||||||
|
view_mode_select: true,
|
||||||
|
snap_at: '1d',
|
||||||
|
auto_move_label: false,
|
||||||
|
scroll_to: 'today',
|
||||||
|
});
|
||||||
|
|
||||||
|
const UPDATES = [
|
||||||
|
[
|
||||||
|
mutablity,
|
||||||
|
{
|
||||||
|
'mutable-general': 'opp__readonly',
|
||||||
|
'mutable-dates': 'opp__readonly_dates',
|
||||||
|
'mutable-progress': 'opp__readonly_progress',
|
||||||
|
},
|
||||||
|
(id, val) => {
|
||||||
|
if (id === 'mutable-general') {
|
||||||
|
document.getElementById('mutable-dates').checked =
|
||||||
|
!val;
|
||||||
|
document.getElementById(
|
||||||
|
'mutable-progress',
|
||||||
|
).checked = !val;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
sideheader,
|
||||||
|
{
|
||||||
|
'toggle-today': 'today_button',
|
||||||
|
'toggle-view-mode': 'view_mode_select',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
holidays,
|
||||||
|
{
|
||||||
|
'toggle-weekends': [
|
||||||
|
'holidays',
|
||||||
|
{ '#a3e635': HOLIDAYS, '#bfdbfe': 'weekend' },
|
||||||
|
{ '#a3e635': HOLIDAYS, '#bfdbfe': [] },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
ignore,
|
||||||
|
{
|
||||||
|
'ignore-weekends': ['ignore', ['weekend'], []],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
styling,
|
||||||
|
{
|
||||||
|
'bar-radius': 'bar_corner_radius',
|
||||||
|
'bar-height': 'bar_height',
|
||||||
|
'arrow-curve': 'arrow_curve',
|
||||||
|
'column-width': 'column_width',
|
||||||
|
'grid-height': 'container_height',
|
||||||
|
padding: 'padding',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
advanced,
|
||||||
|
{
|
||||||
|
'auto-move-label': 'auto_move_label',
|
||||||
|
'snap-at-qty': (val) => ({
|
||||||
|
snap_at:
|
||||||
|
val +
|
||||||
|
document.getElementById('snap-at-scale').value,
|
||||||
|
}),
|
||||||
|
'snap-at-scale': (val) => ({
|
||||||
|
snap_at:
|
||||||
|
document.getElementById('snap-at-qty').value +
|
||||||
|
val,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
for (let [chart, details, after] of UPDATES) {
|
||||||
|
for (let id in details) {
|
||||||
|
let el = document.getElementById(id);
|
||||||
|
el.onchange = (e) => {
|
||||||
|
let label = details[id];
|
||||||
|
let val;
|
||||||
|
|
||||||
|
if (e.currentTarget.type === 'checkbox') {
|
||||||
|
if (typeof label === 'string') {
|
||||||
|
let opposite = label.slice(0, 5) === 'opp__';
|
||||||
|
if (opposite) label = label.slice(5);
|
||||||
|
val = opposite
|
||||||
|
? !e.currentTarget.checked
|
||||||
|
: e.currentTarget.checked;
|
||||||
|
} else {
|
||||||
|
val = label[e.currentTarget.checked ? 1 : 2];
|
||||||
|
label = label[0];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val = +e.currentTarget.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
let store = chart.options.scroll_to;
|
||||||
|
let scroll = chart.$container.scrollLeft;
|
||||||
|
if (typeof label === 'function') {
|
||||||
|
chart.update_options({
|
||||||
|
...label(val),
|
||||||
|
scroll_to: null,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
chart.update_options({
|
||||||
|
[label]: val,
|
||||||
|
scroll_to: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
chart.options.scroll_to = store;
|
||||||
|
chart.$container.scrollLeft = scroll;
|
||||||
|
after && after(id, val, chart);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// const OPTIONS_UPDATE = [
|
||||||
|
// // [
|
||||||
|
// // styling,
|
||||||
|
// // {
|
||||||
|
// // 'bar-spacing': {
|
||||||
|
// // bar_corner_radius: [
|
||||||
|
// // 'config',
|
||||||
|
// // () =>
|
||||||
|
// // +document.getElementById('bar-radius')
|
||||||
|
// // .value,
|
||||||
|
// // ,
|
||||||
|
// // ],
|
||||||
|
// // bar_height: [
|
||||||
|
// // 'config',
|
||||||
|
// // () =>
|
||||||
|
// // +document.getElementById('bar-height')
|
||||||
|
// // .value,
|
||||||
|
// // ],
|
||||||
|
// // arrow_curve: [
|
||||||
|
// // 'config',
|
||||||
|
// // () =>
|
||||||
|
// // +document.getElementById('arrow-curve')
|
||||||
|
// // .value,
|
||||||
|
// // ],
|
||||||
|
// // },
|
||||||
|
// // },
|
||||||
|
// // ],
|
||||||
|
// [
|
||||||
|
// advanced,
|
||||||
|
// {
|
||||||
|
// 'snap-by': {
|
||||||
|
// BEFORE: (chart) => chart.$container.scrollLeft,
|
||||||
|
// snap_at: [
|
||||||
|
// 'config',
|
||||||
|
// (scale) => {
|
||||||
|
// return (
|
||||||
|
// document.getElementById('snap-at-qty')
|
||||||
|
// .value +
|
||||||
|
// document.getElementById('snap-at-scale')
|
||||||
|
// .value
|
||||||
|
// );
|
||||||
|
// },
|
||||||
|
// ],
|
||||||
|
// view_mode: ['config', (k) => k],
|
||||||
|
// scroll_to: ['config', (_) => false],
|
||||||
|
// AFTER: (before, chart) =>
|
||||||
|
// (chart.$container.scrollLeft = before),
|
||||||
|
// },
|
||||||
|
// 'auto-move-label': {
|
||||||
|
// BEFORE: (chart) =>
|
||||||
|
// chart.change_view_mode('Day') ||
|
||||||
|
// chart.$container.scrollLeft,
|
||||||
|
// view_mode: ['config', (k) => k],
|
||||||
|
// auto_move_label: 'opp',
|
||||||
|
// scroll_to: ['config', (_) => false],
|
||||||
|
// AFTER: (before, chart) =>
|
||||||
|
// (chart.$container.scrollLeft = before),
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// ],
|
||||||
|
// ];
|
||||||
|
|
||||||
|
// for (let [chart, options] of OPTIONS_UPDATE) {
|
||||||
|
// for (let id in options) {
|
||||||
|
// let el = document.getElementById(id);
|
||||||
|
// el.onclick = () => {
|
||||||
|
// const before =
|
||||||
|
// options[id].BEFORE && options[id].BEFORE(chart);
|
||||||
|
// let newOptions = {};
|
||||||
|
// for (let k in options[id]) {
|
||||||
|
// if (k === 'AFTER' || k === 'BEFORE') continue;
|
||||||
|
// if (options[id][k] === 'opp') {
|
||||||
|
// newOptions[k] = !chart.options[k];
|
||||||
|
// if (chart.options[k]) {
|
||||||
|
// el.innerHTML = el.innerHTML.replace(
|
||||||
|
// 'Hide',
|
||||||
|
// 'Show',
|
||||||
|
// );
|
||||||
|
// } else {
|
||||||
|
// el.innerHTML = el.innerHTML.replace(
|
||||||
|
// 'Show',
|
||||||
|
// 'Hide',
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// } else if (options[id][k][0] === 'config') {
|
||||||
|
// newOptions[k] = options[id][k][1](
|
||||||
|
// chart.options[k],
|
||||||
|
// chart,
|
||||||
|
// );
|
||||||
|
// } else {
|
||||||
|
// newOptions[k] = options[id][k];
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// chart.update_options(newOptions);
|
||||||
|
// options[id].AFTER && options[id].AFTER(before, chart);
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
</script>
|
||||||
|
<script
|
||||||
|
src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"
|
||||||
|
integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL"
|
||||||
|
crossorigin="anonymous"
|
||||||
|
></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (c) 2016 Frappe Technologies Pvt. Ltd.
|
Copyright (c) 2024 Frappe Technologies Pvt. Ltd.
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
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:
|
||||||
|
|
||||||
|
|||||||
107
package.json
Executable file → Normal file
107
package.json
Executable file → Normal file
@ -1,52 +1,59 @@
|
|||||||
{
|
{
|
||||||
"name": "frappe-gantt",
|
"name": "frappe-gantt",
|
||||||
"version": "0.0.7",
|
"version": "1.0.3",
|
||||||
"description": "A simple, modern, interactive gantt library for the web",
|
"description": "A simple, modern, interactive gantt library for the web",
|
||||||
"main": "dist/frappe-gantt.js",
|
"main": "src/index.js",
|
||||||
"scripts": {
|
"type": "module",
|
||||||
"build": "webpack --mode=build",
|
"scripts": {
|
||||||
"dev": "webpack --progress --colors --watch --mode=dev",
|
"dev": "vite",
|
||||||
"test": "mocha --compilers js:babel-core/register --colors -w ./test/*.spec.js"
|
"build-dev": "vite build --watch",
|
||||||
},
|
"build": "vite build",
|
||||||
"devDependencies": {
|
"lint": "eslint src/**/*.js",
|
||||||
"babel": "6.3.13",
|
"prettier": "prettier --write \"{src/*,tests/*,rollup.config}.js\"",
|
||||||
"babel-core": "6.1.18",
|
"prettier-check": "prettier --check \"{src/*,tests/*,rollup.config}.js\""
|
||||||
"babel-eslint": "5.0.0",
|
},
|
||||||
"babel-loader": "6.1.0",
|
"repository": {
|
||||||
"babel-plugin-add-module-exports": "0.1.2",
|
"type": "git",
|
||||||
"babel-preset-es2015": "6.3.13",
|
"url": "git+https://github.com/frappe/gantt.git"
|
||||||
"chai": "3.4.1",
|
},
|
||||||
"css-loader": "^0.26.1",
|
"files": [
|
||||||
"eslint": "1.7.2",
|
"src",
|
||||||
"eslint-loader": "1.1.0",
|
"dist",
|
||||||
"mocha": "2.3.4",
|
"README.md"
|
||||||
"node-sass": "^4.0.0",
|
],
|
||||||
"sass-loader": "^4.1.0",
|
"exports": {
|
||||||
"style-loader": "^0.13.1",
|
".": {
|
||||||
"webpack": "1.12.9",
|
"require": "./dist/frappe-gantt.umd.js",
|
||||||
"yargs": "3.32.0"
|
"import": "./dist/frappe-gantt.es.js",
|
||||||
},
|
"style": "./dist/frappe-gantt.css"
|
||||||
"repository": {
|
}
|
||||||
"type": "git",
|
},
|
||||||
"url": "https://github.com/frappe/gantt.git"
|
"keywords": [
|
||||||
},
|
"gantt",
|
||||||
"keywords": [
|
"svg",
|
||||||
"gantt",
|
"simple gantt",
|
||||||
"svg",
|
"project timeline",
|
||||||
"simple gantt",
|
"interactive gantt",
|
||||||
"project timeline",
|
"project management"
|
||||||
"interactive gantt",
|
],
|
||||||
"project management"
|
"author": "Faris Ansari",
|
||||||
],
|
"license": "MIT",
|
||||||
"author": "Faris Ansari",
|
"bugs": {
|
||||||
"license": "MIT",
|
"url": "https://github.com/frappe/gantt/issues"
|
||||||
"bugs": {
|
},
|
||||||
"url": "https://github.com/frappe/gantt/issues"
|
"homepage": "https://github.com/frappe/gantt",
|
||||||
},
|
"devDependencies": {
|
||||||
"homepage": "https://github.com/frappe/gantt",
|
"eslint": "^9.15.0",
|
||||||
"dependencies": {
|
"eslint-config-prettier": "^2.9.0",
|
||||||
"deepmerge": "^2.0.1",
|
"eslint-plugin-prettier": "^2.6.0",
|
||||||
"moment": "^2.17.1",
|
"postcss-nesting": "^12.1.2",
|
||||||
"snapsvg": "^0.4.0"
|
"prettier": "3.2.5",
|
||||||
}
|
"vite": "^5.2.10"
|
||||||
|
},
|
||||||
|
"eslintIgnore": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"sideEffects": [
|
||||||
|
"*.css"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
1291
pnpm-lock.yaml
generated
Normal file
1291
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
4
postcss.config.cjs
Normal file
4
postcss.config.cjs
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
module.exports = {
|
||||||
|
plugins: [require('postcss-nesting')],
|
||||||
|
};
|
||||||
105
src/Arrow.js
105
src/Arrow.js
@ -1,105 +0,0 @@
|
|||||||
/* global Snap */
|
|
||||||
/*
|
|
||||||
Class: Arrow
|
|
||||||
from_task ---> to_task
|
|
||||||
|
|
||||||
Opts:
|
|
||||||
gantt (Gantt object)
|
|
||||||
from_task (Bar object)
|
|
||||||
to_task (Bar object)
|
|
||||||
*/
|
|
||||||
|
|
||||||
export default function Arrow(gt, from_task, to_task) {
|
|
||||||
|
|
||||||
const self = {};
|
|
||||||
|
|
||||||
function init() {
|
|
||||||
self.from_task = from_task;
|
|
||||||
self.to_task = to_task;
|
|
||||||
prepare();
|
|
||||||
draw();
|
|
||||||
}
|
|
||||||
|
|
||||||
function prepare() {
|
|
||||||
|
|
||||||
self.start_x = from_task.$bar.getX() + from_task.$bar.getWidth() / 2;
|
|
||||||
|
|
||||||
const condition = () =>
|
|
||||||
to_task.$bar.getX() < self.start_x + gt.config.padding &&
|
|
||||||
self.start_x > from_task.$bar.getX() + gt.config.padding;
|
|
||||||
|
|
||||||
while(condition()) {
|
|
||||||
self.start_x -= 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.start_y = gt.config.header_height + gt.config.bar.height +
|
|
||||||
(gt.config.padding + gt.config.bar.height) * from_task.task._index +
|
|
||||||
gt.config.padding;
|
|
||||||
|
|
||||||
self.end_x = to_task.$bar.getX() - gt.config.padding / 2;
|
|
||||||
self.end_y = gt.config.header_height + gt.config.bar.height / 2 +
|
|
||||||
(gt.config.padding + gt.config.bar.height) * to_task.task._index +
|
|
||||||
gt.config.padding;
|
|
||||||
|
|
||||||
const from_is_below_to = (from_task.task._index > to_task.task._index);
|
|
||||||
self.curve = gt.config.arrow.curve;
|
|
||||||
self.clockwise = from_is_below_to ? 1 : 0;
|
|
||||||
self.curve_y = from_is_below_to ? -self.curve : self.curve;
|
|
||||||
self.offset = from_is_below_to ?
|
|
||||||
self.end_y + gt.config.arrow.curve :
|
|
||||||
self.end_y - gt.config.arrow.curve;
|
|
||||||
|
|
||||||
self.path =
|
|
||||||
Snap.format('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',
|
|
||||||
{
|
|
||||||
start_x: self.start_x,
|
|
||||||
start_y: self.start_y,
|
|
||||||
end_x: self.end_x,
|
|
||||||
end_y: self.end_y,
|
|
||||||
offset: self.offset,
|
|
||||||
curve: self.curve,
|
|
||||||
clockwise: self.clockwise,
|
|
||||||
curve_y: self.curve_y
|
|
||||||
});
|
|
||||||
|
|
||||||
if(to_task.$bar.getX() < from_task.$bar.getX() + gt.config.padding) {
|
|
||||||
self.path =
|
|
||||||
Snap.format('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',
|
|
||||||
{
|
|
||||||
start_x: self.start_x,
|
|
||||||
start_y: self.start_y,
|
|
||||||
end_x: self.end_x,
|
|
||||||
end_y: self.end_y,
|
|
||||||
down_1: gt.config.padding / 2 - self.curve,
|
|
||||||
down_2: to_task.$bar.getY() + to_task.$bar.getHeight() / 2 - self.curve_y,
|
|
||||||
left: to_task.$bar.getX() - gt.config.padding,
|
|
||||||
offset: self.offset,
|
|
||||||
curve: self.curve,
|
|
||||||
clockwise: self.clockwise,
|
|
||||||
curve_y: self.curve_y
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function draw() {
|
|
||||||
self.element = gt.canvas.path(self.path)
|
|
||||||
.attr('data-from', self.from_task.task.id)
|
|
||||||
.attr('data-to', self.to_task.task.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
function update() { // eslint-disable-line
|
|
||||||
prepare();
|
|
||||||
self.element.attr('d', self.path);
|
|
||||||
}
|
|
||||||
self.update = update;
|
|
||||||
|
|
||||||
init();
|
|
||||||
|
|
||||||
return self;
|
|
||||||
}
|
|
||||||
529
src/Bar.js
529
src/Bar.js
@ -1,529 +0,0 @@
|
|||||||
/* global Snap */
|
|
||||||
/*
|
|
||||||
Class: Bar
|
|
||||||
|
|
||||||
Opts:
|
|
||||||
gt: Gantt object
|
|
||||||
task: task object
|
|
||||||
*/
|
|
||||||
|
|
||||||
export default function Bar(gt, task) {
|
|
||||||
|
|
||||||
const self = {};
|
|
||||||
|
|
||||||
function init() {
|
|
||||||
set_defaults();
|
|
||||||
prepare();
|
|
||||||
draw();
|
|
||||||
bind();
|
|
||||||
}
|
|
||||||
|
|
||||||
function set_defaults() {
|
|
||||||
self.action_completed = false;
|
|
||||||
self.task = task;
|
|
||||||
}
|
|
||||||
|
|
||||||
function prepare() {
|
|
||||||
prepare_values();
|
|
||||||
prepare_plugins();
|
|
||||||
}
|
|
||||||
|
|
||||||
function prepare_values() {
|
|
||||||
self.invalid = self.task.invalid;
|
|
||||||
self.height = gt.config.bar.height;
|
|
||||||
self.x = compute_x();
|
|
||||||
self.y = compute_y();
|
|
||||||
self.corner_radius = gt.config.bar.corner_radius;
|
|
||||||
self.duration = (self.task._end.diff(self.task._start, 'hours') + 24) / gt.config.step;
|
|
||||||
self.width = gt.config.column_width * self.duration;
|
|
||||||
self.progress_width = gt.config.column_width * self.duration * (self.task.progress / 100) || 0;
|
|
||||||
self.group = gt.canvas.group().addClass('bar-wrapper').addClass(self.task.custom_class || '');
|
|
||||||
self.bar_group = gt.canvas.group().addClass('bar-group').appendTo(self.group);
|
|
||||||
self.handle_group = gt.canvas.group().addClass('handle-group').appendTo(self.group);
|
|
||||||
}
|
|
||||||
|
|
||||||
function prepare_plugins() {
|
|
||||||
Snap.plugin(function (Snap, Element, Paper, global, Fragment) {
|
|
||||||
Element.prototype.getX = function () {
|
|
||||||
return +this.attr('x');
|
|
||||||
};
|
|
||||||
Element.prototype.getY = function () {
|
|
||||||
return +this.attr('y');
|
|
||||||
};
|
|
||||||
Element.prototype.getWidth = function () {
|
|
||||||
return +this.attr('width');
|
|
||||||
};
|
|
||||||
Element.prototype.getHeight = function () {
|
|
||||||
return +this.attr('height');
|
|
||||||
};
|
|
||||||
Element.prototype.getEndX = function () {
|
|
||||||
return this.getX() + this.getWidth();
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function draw() {
|
|
||||||
draw_bar();
|
|
||||||
draw_progress_bar();
|
|
||||||
draw_label();
|
|
||||||
draw_resize_handles();
|
|
||||||
}
|
|
||||||
|
|
||||||
function draw_bar() {
|
|
||||||
self.$bar = gt.canvas.rect(self.x, self.y,
|
|
||||||
self.width, self.height,
|
|
||||||
self.corner_radius, self.corner_radius)
|
|
||||||
.addClass('bar')
|
|
||||||
.appendTo(self.bar_group);
|
|
||||||
if (self.invalid) {
|
|
||||||
self.$bar.addClass('bar-invalid');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function draw_progress_bar() {
|
|
||||||
if (self.invalid) return;
|
|
||||||
self.$bar_progress = gt.canvas.rect(self.x, self.y,
|
|
||||||
self.progress_width, self.height,
|
|
||||||
self.corner_radius, self.corner_radius)
|
|
||||||
.addClass('bar-progress')
|
|
||||||
.appendTo(self.bar_group);
|
|
||||||
}
|
|
||||||
|
|
||||||
function draw_label() {
|
|
||||||
gt.canvas.text(self.x + self.width / 2,
|
|
||||||
self.y + self.height / 2,
|
|
||||||
self.task.name)
|
|
||||||
.addClass('bar-label')
|
|
||||||
.appendTo(self.bar_group);
|
|
||||||
update_label_position();
|
|
||||||
}
|
|
||||||
|
|
||||||
function draw_resize_handles() {
|
|
||||||
if (self.invalid) return;
|
|
||||||
|
|
||||||
const bar = self.$bar,
|
|
||||||
handle_width = 8;
|
|
||||||
|
|
||||||
gt.canvas.rect(bar.getX() + bar.getWidth() - 9, bar.getY() + 1,
|
|
||||||
handle_width, self.height - 2, self.corner_radius, self.corner_radius)
|
|
||||||
.addClass('handle right')
|
|
||||||
.appendTo(self.handle_group);
|
|
||||||
gt.canvas.rect(bar.getX() + 1, bar.getY() + 1,
|
|
||||||
handle_width, self.height - 2, self.corner_radius, self.corner_radius)
|
|
||||||
.addClass('handle left')
|
|
||||||
.appendTo(self.handle_group);
|
|
||||||
|
|
||||||
if (self.task.progress && self.task.progress < 100) {
|
|
||||||
gt.canvas.polygon(get_progress_polygon_points())
|
|
||||||
.addClass('handle progress')
|
|
||||||
.appendTo(self.handle_group);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function get_progress_polygon_points() {
|
|
||||||
const bar_progress = self.$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
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
function bind() {
|
|
||||||
if (self.invalid) return;
|
|
||||||
setup_click_event();
|
|
||||||
show_details();
|
|
||||||
bind_resize();
|
|
||||||
bind_drag();
|
|
||||||
bind_resize_progress();
|
|
||||||
}
|
|
||||||
|
|
||||||
function show_details() {
|
|
||||||
const popover_group = gt.element_groups.details;
|
|
||||||
self.details_box = popover_group
|
|
||||||
.select(`.details-wrapper[data-task='${self.task.id}']`);
|
|
||||||
|
|
||||||
if (!self.details_box) {
|
|
||||||
self.details_box = gt.canvas.group()
|
|
||||||
.addClass('details-wrapper hide')
|
|
||||||
.attr('data-task', self.task.id)
|
|
||||||
.appendTo(popover_group);
|
|
||||||
|
|
||||||
render_details();
|
|
||||||
|
|
||||||
const f = gt.canvas.filter(
|
|
||||||
Snap.filter.shadow(0, 1, 1, '#666', 0.6));
|
|
||||||
self.details_box.attr({
|
|
||||||
filter: f
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
self.group.click((e) => {
|
|
||||||
if (self.action_completed) {
|
|
||||||
// just finished a move action, wait for a few seconds
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
popover_group.selectAll('.details-wrapper')
|
|
||||||
.forEach(el => el.addClass('hide'));
|
|
||||||
self.details_box.removeClass('hide');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function render_details() {
|
|
||||||
const {x, y} = get_details_position();
|
|
||||||
self.details_box.transform(`t${x},${y}`);
|
|
||||||
self.details_box.clear();
|
|
||||||
|
|
||||||
const html = get_details_html();
|
|
||||||
const foreign_object =
|
|
||||||
Snap.parse(`<foreignObject width="5000" height="2000">
|
|
||||||
<body xmlns="http://www.w3.org/1999/xhtml">
|
|
||||||
${html}
|
|
||||||
</body>
|
|
||||||
</foreignObject>`);
|
|
||||||
self.details_box.append(foreign_object);
|
|
||||||
}
|
|
||||||
|
|
||||||
function get_details_html() {
|
|
||||||
|
|
||||||
// custom html in config
|
|
||||||
if(gt.config.custom_popup_html) {
|
|
||||||
const html = gt.config.custom_popup_html;
|
|
||||||
if(typeof html === 'string') {
|
|
||||||
return html;
|
|
||||||
}
|
|
||||||
if(isFunction(html)) {
|
|
||||||
return html(task);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const start_date = self.task._start.format('MMM D');
|
|
||||||
const end_date = self.task._end.format('MMM D');
|
|
||||||
const heading = `${self.task.name}: ${start_date} - ${end_date}`;
|
|
||||||
|
|
||||||
const line_1 = `Duration: ${self.duration} days`;
|
|
||||||
const line_2 = self.task.progress ? `Progress: ${self.task.progress}` : null;
|
|
||||||
|
|
||||||
const html = `
|
|
||||||
<div class="details-container">
|
|
||||||
<h5>${heading}</h5>
|
|
||||||
<p>${line_1}</p>
|
|
||||||
${
|
|
||||||
line_2 ? `<p>${line_2}</p>` : ''
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
return html;
|
|
||||||
}
|
|
||||||
|
|
||||||
function get_details_position() {
|
|
||||||
return {
|
|
||||||
x: self.$bar.getEndX() + 2,
|
|
||||||
y: self.$bar.getY() - 10
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function bind_resize() {
|
|
||||||
const { left, right } = get_handles();
|
|
||||||
|
|
||||||
left.drag(onmove_left, onstart, onstop_left);
|
|
||||||
right.drag(onmove_right, onstart, onstop_right);
|
|
||||||
|
|
||||||
function onmove_right(dx, dy) {
|
|
||||||
onmove_handle_right(dx, dy);
|
|
||||||
}
|
|
||||||
function onstop_right() {
|
|
||||||
onstop_handle_right();
|
|
||||||
}
|
|
||||||
|
|
||||||
function onmove_left(dx, dy) {
|
|
||||||
onmove_handle_left(dx, dy);
|
|
||||||
}
|
|
||||||
function onstop_left() {
|
|
||||||
onstop_handle_left();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function get_handles() {
|
|
||||||
return {
|
|
||||||
left: self.handle_group.select('.handle.left'),
|
|
||||||
right: self.handle_group.select('.handle.right')
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function bind_drag() {
|
|
||||||
self.bar_group.drag(onmove, onstart, onstop);
|
|
||||||
}
|
|
||||||
|
|
||||||
function bind_resize_progress() {
|
|
||||||
const bar = self.$bar,
|
|
||||||
bar_progress = self.$bar_progress,
|
|
||||||
handle = self.group.select('.handle.progress');
|
|
||||||
handle && handle.drag(on_move, on_start, on_stop);
|
|
||||||
|
|
||||||
function on_move(dx, dy) {
|
|
||||||
if (dx > bar_progress.max_dx) {
|
|
||||||
dx = bar_progress.max_dx;
|
|
||||||
}
|
|
||||||
if (dx < bar_progress.min_dx) {
|
|
||||||
dx = bar_progress.min_dx;
|
|
||||||
}
|
|
||||||
|
|
||||||
bar_progress.attr('width', bar_progress.owidth + dx);
|
|
||||||
handle.attr('points', get_progress_polygon_points());
|
|
||||||
bar_progress.finaldx = dx;
|
|
||||||
}
|
|
||||||
function on_stop() {
|
|
||||||
if (!bar_progress.finaldx) return;
|
|
||||||
progress_changed();
|
|
||||||
set_action_completed();
|
|
||||||
}
|
|
||||||
function on_start() {
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onstart() {
|
|
||||||
const bar = self.$bar;
|
|
||||||
bar.ox = bar.getX();
|
|
||||||
bar.oy = bar.getY();
|
|
||||||
bar.owidth = bar.getWidth();
|
|
||||||
bar.finaldx = 0;
|
|
||||||
run_method_for_dependencies('onstart');
|
|
||||||
}
|
|
||||||
self.onstart = onstart;
|
|
||||||
|
|
||||||
function onmove(dx, dy) {
|
|
||||||
const bar = self.$bar;
|
|
||||||
bar.finaldx = get_snap_position(dx);
|
|
||||||
update_bar_position({x: bar.ox + bar.finaldx});
|
|
||||||
run_method_for_dependencies('onmove', [dx, dy]);
|
|
||||||
}
|
|
||||||
self.onmove = onmove;
|
|
||||||
|
|
||||||
function onstop() {
|
|
||||||
const bar = self.$bar;
|
|
||||||
if (!bar.finaldx) return;
|
|
||||||
date_changed();
|
|
||||||
set_action_completed();
|
|
||||||
run_method_for_dependencies('onstop');
|
|
||||||
}
|
|
||||||
self.onstop = onstop;
|
|
||||||
|
|
||||||
function onmove_handle_left(dx, dy) {
|
|
||||||
const bar = self.$bar;
|
|
||||||
bar.finaldx = get_snap_position(dx);
|
|
||||||
update_bar_position({
|
|
||||||
x: bar.ox + bar.finaldx,
|
|
||||||
width: bar.owidth - bar.finaldx
|
|
||||||
});
|
|
||||||
run_method_for_dependencies('onmove', [dx, dy]);
|
|
||||||
}
|
|
||||||
self.onmove_handle_left = onmove_handle_left;
|
|
||||||
|
|
||||||
function onstop_handle_left() {
|
|
||||||
const bar = self.$bar;
|
|
||||||
if (bar.finaldx) date_changed();
|
|
||||||
set_action_completed();
|
|
||||||
run_method_for_dependencies('onstop');
|
|
||||||
}
|
|
||||||
self.onstop_handle_left = onstop_handle_left;
|
|
||||||
|
|
||||||
function run_method_for_dependencies(fn, args) {
|
|
||||||
const dm = gt.dependency_map;
|
|
||||||
if (dm[self.task.id]) {
|
|
||||||
for (let deptask of dm[self.task.id]) {
|
|
||||||
const dt = gt.get_bar(deptask);
|
|
||||||
dt[fn].apply(dt, args);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onmove_handle_right(dx, dy) {
|
|
||||||
const bar = self.$bar;
|
|
||||||
bar.finaldx = get_snap_position(dx);
|
|
||||||
update_bar_position({width: bar.owidth + bar.finaldx});
|
|
||||||
}
|
|
||||||
|
|
||||||
function onstop_handle_right() {
|
|
||||||
const bar = self.$bar;
|
|
||||||
if (bar.finaldx) date_changed();
|
|
||||||
set_action_completed();
|
|
||||||
}
|
|
||||||
|
|
||||||
function update_bar_position({x = null, width = null}) {
|
|
||||||
const bar = self.$bar;
|
|
||||||
if (x) {
|
|
||||||
// get all x values of parent task
|
|
||||||
const xs = task.dependencies.map(dep => {
|
|
||||||
return gt.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;
|
|
||||||
}
|
|
||||||
update_attr(bar, 'x', x);
|
|
||||||
}
|
|
||||||
if (width && width >= gt.config.column_width) {
|
|
||||||
update_attr(bar, 'width', width);
|
|
||||||
}
|
|
||||||
update_label_position();
|
|
||||||
update_handle_position();
|
|
||||||
update_progressbar_position();
|
|
||||||
update_arrow_position();
|
|
||||||
update_details_position();
|
|
||||||
}
|
|
||||||
|
|
||||||
function setup_click_event() {
|
|
||||||
self.group.click(function () {
|
|
||||||
if (self.action_completed) {
|
|
||||||
// just finished a move action, wait for a few seconds
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (self.group.hasClass('active')) {
|
|
||||||
gt.trigger_event('click', [self.task]);
|
|
||||||
}
|
|
||||||
gt.unselect_all();
|
|
||||||
self.group.toggleClass('active');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function date_changed() {
|
|
||||||
const { new_start_date, new_end_date } = compute_start_end_date();
|
|
||||||
self.task._start = new_start_date;
|
|
||||||
self.task._end = new_end_date;
|
|
||||||
render_details();
|
|
||||||
gt.trigger_event('date_change',
|
|
||||||
[self.task, new_start_date, new_end_date]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function progress_changed() {
|
|
||||||
const new_progress = compute_progress();
|
|
||||||
self.task.progress = new_progress;
|
|
||||||
render_details();
|
|
||||||
gt.trigger_event('progress_change',
|
|
||||||
[self.task, new_progress]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function set_action_completed() {
|
|
||||||
self.action_completed = true;
|
|
||||||
setTimeout(() => self.action_completed = false, 2000);
|
|
||||||
}
|
|
||||||
|
|
||||||
function compute_start_end_date() {
|
|
||||||
const bar = self.$bar;
|
|
||||||
const x_in_units = bar.getX() / gt.config.column_width;
|
|
||||||
const new_start_date = gt.gantt_start.clone().add(x_in_units * gt.config.step, 'hours');
|
|
||||||
const width_in_units = bar.getWidth() / gt.config.column_width;
|
|
||||||
const new_end_date = new_start_date.clone().add(width_in_units * gt.config.step, 'hours');
|
|
||||||
// lets say duration is 2 days
|
|
||||||
// start_date = May 24 00:00:00
|
|
||||||
// end_date = May 24 + 2 days = May 26 (incorrect)
|
|
||||||
// so subtract 1 second so that
|
|
||||||
// end_date = May 25 23:59:59
|
|
||||||
new_end_date.add('-1', 'seconds');
|
|
||||||
return { new_start_date, new_end_date };
|
|
||||||
}
|
|
||||||
|
|
||||||
function compute_progress() {
|
|
||||||
const progress = self.$bar_progress.getWidth() / self.$bar.getWidth() * 100;
|
|
||||||
return parseInt(progress, 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
function compute_x() {
|
|
||||||
let x = self.task._start.diff(gt.gantt_start, 'hours') /
|
|
||||||
gt.config.step * gt.config.column_width;
|
|
||||||
|
|
||||||
if (gt.view_is('Month')) {
|
|
||||||
x = self.task._start.diff(gt.gantt_start, 'days') *
|
|
||||||
gt.config.column_width / 30;
|
|
||||||
}
|
|
||||||
return x;
|
|
||||||
}
|
|
||||||
|
|
||||||
function compute_y() {
|
|
||||||
return gt.config.header_height + gt.config.padding +
|
|
||||||
self.task._index * (self.height + gt.config.padding);
|
|
||||||
}
|
|
||||||
|
|
||||||
function get_snap_position(dx) {
|
|
||||||
let odx = dx, rem, position;
|
|
||||||
|
|
||||||
if (gt.view_is('Week')) {
|
|
||||||
rem = dx % (gt.config.column_width / 7);
|
|
||||||
position = odx - rem +
|
|
||||||
((rem < gt.config.column_width / 14) ? 0 : gt.config.column_width / 7);
|
|
||||||
} else if (gt.view_is('Month')) {
|
|
||||||
rem = dx % (gt.config.column_width / 30);
|
|
||||||
position = odx - rem +
|
|
||||||
((rem < gt.config.column_width / 60) ? 0 : gt.config.column_width / 30);
|
|
||||||
} else {
|
|
||||||
rem = dx % gt.config.column_width;
|
|
||||||
position = odx - rem +
|
|
||||||
((rem < gt.config.column_width / 2) ? 0 : gt.config.column_width);
|
|
||||||
}
|
|
||||||
return position;
|
|
||||||
}
|
|
||||||
|
|
||||||
function update_attr(element, attr, value) {
|
|
||||||
value = +value;
|
|
||||||
if (!isNaN(value)) {
|
|
||||||
element.attr(attr, value);
|
|
||||||
}
|
|
||||||
return element;
|
|
||||||
}
|
|
||||||
|
|
||||||
function update_progressbar_position() {
|
|
||||||
self.$bar_progress.attr('x', self.$bar.getX());
|
|
||||||
self.$bar_progress.attr('width', self.$bar.getWidth() * (self.task.progress / 100));
|
|
||||||
}
|
|
||||||
|
|
||||||
function update_label_position() {
|
|
||||||
const bar = self.$bar,
|
|
||||||
label = self.group.select('.bar-label');
|
|
||||||
if (label.getBBox().width > bar.getWidth()) {
|
|
||||||
label.addClass('big').attr('x', bar.getX() + bar.getWidth() + 5);
|
|
||||||
} else {
|
|
||||||
label.removeClass('big').attr('x', bar.getX() + bar.getWidth() / 2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function update_handle_position() {
|
|
||||||
const bar = self.$bar;
|
|
||||||
self.handle_group.select('.handle.left').attr({
|
|
||||||
'x': bar.getX() + 1
|
|
||||||
});
|
|
||||||
self.handle_group.select('.handle.right').attr({
|
|
||||||
'x': bar.getEndX() - 9
|
|
||||||
});
|
|
||||||
const handle = self.group.select('.handle.progress');
|
|
||||||
handle && handle.attr('points', get_progress_polygon_points());
|
|
||||||
}
|
|
||||||
|
|
||||||
function update_arrow_position() {
|
|
||||||
for (let arrow of self.arrows) {
|
|
||||||
arrow.update();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function update_details_position() {
|
|
||||||
const {x, y} = get_details_position();
|
|
||||||
self.details_box && self.details_box.transform(`t${x},${y}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isFunction(functionToCheck) {
|
|
||||||
var getType = {};
|
|
||||||
return functionToCheck && getType.toString.call(functionToCheck) === '[object Function]';
|
|
||||||
}
|
|
||||||
|
|
||||||
init();
|
|
||||||
|
|
||||||
return self;
|
|
||||||
}
|
|
||||||
571
src/Gantt.js
571
src/Gantt.js
@ -1,571 +0,0 @@
|
|||||||
/* global moment, Snap */
|
|
||||||
/**
|
|
||||||
* Gantt:
|
|
||||||
* element: querySelector string, HTML DOM or SVG DOM element, required
|
|
||||||
* tasks: array of tasks, required
|
|
||||||
* task: { id, name, start, end, progress, dependencies, custom_class }
|
|
||||||
* config: configuration options, optional
|
|
||||||
*/
|
|
||||||
import './gantt.scss';
|
|
||||||
|
|
||||||
import Bar from './Bar';
|
|
||||||
import Arrow from './Arrow';
|
|
||||||
|
|
||||||
export default function Gantt(element, tasks, config = {}) {
|
|
||||||
|
|
||||||
const self = {};
|
|
||||||
|
|
||||||
function init() {
|
|
||||||
set_defaults();
|
|
||||||
|
|
||||||
// expose methods
|
|
||||||
self.change_view_mode = change_view_mode;
|
|
||||||
self.unselect_all = unselect_all;
|
|
||||||
self.view_is = view_is;
|
|
||||||
self.get_bar = get_bar;
|
|
||||||
self.trigger_event = trigger_event;
|
|
||||||
self.refresh = refresh;
|
|
||||||
|
|
||||||
// initialize with default view mode
|
|
||||||
change_view_mode(self.config.view_mode);
|
|
||||||
}
|
|
||||||
|
|
||||||
function set_defaults() {
|
|
||||||
|
|
||||||
const merge = require('deepmerge');
|
|
||||||
|
|
||||||
const defaults = {
|
|
||||||
header_height: 50,
|
|
||||||
column_width: 30,
|
|
||||||
step: 24,
|
|
||||||
view_modes: [
|
|
||||||
'Quarter Day',
|
|
||||||
'Half Day',
|
|
||||||
'Day',
|
|
||||||
'Week',
|
|
||||||
'Month'
|
|
||||||
],
|
|
||||||
bar: {
|
|
||||||
height: 20,
|
|
||||||
corner_radius: 3
|
|
||||||
},
|
|
||||||
arrow: {
|
|
||||||
curve: 5
|
|
||||||
},
|
|
||||||
padding: 18,
|
|
||||||
view_mode: 'Day',
|
|
||||||
date_format: 'YYYY-MM-DD',
|
|
||||||
custom_popup_html: null
|
|
||||||
};
|
|
||||||
self.config = merge(defaults, config);
|
|
||||||
|
|
||||||
reset_variables(tasks);
|
|
||||||
}
|
|
||||||
|
|
||||||
function reset_variables(tasks) {
|
|
||||||
if(typeof element === 'string') {
|
|
||||||
self.element = document.querySelector(element);
|
|
||||||
} else if (element instanceof SVGElement) {
|
|
||||||
self.element = element;
|
|
||||||
} else if (element instanceof HTMLElement) {
|
|
||||||
self.element = element.querySelector('svg');
|
|
||||||
} else {
|
|
||||||
throw new TypeError('Frappé Gantt only supports usage of a string CSS selector,' +
|
|
||||||
' HTML DOM element or SVG DOM element for the \'element\' parameter');
|
|
||||||
}
|
|
||||||
|
|
||||||
self._tasks = tasks;
|
|
||||||
|
|
||||||
self._bars = [];
|
|
||||||
self._arrows = [];
|
|
||||||
self.element_groups = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
function refresh(updated_tasks) {
|
|
||||||
reset_variables(updated_tasks);
|
|
||||||
change_view_mode(self.config.view_mode);
|
|
||||||
}
|
|
||||||
|
|
||||||
function change_view_mode(mode) {
|
|
||||||
set_scale(mode);
|
|
||||||
prepare();
|
|
||||||
render();
|
|
||||||
// fire viewmode_change event
|
|
||||||
trigger_event('view_change', [mode]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function prepare() {
|
|
||||||
prepare_tasks();
|
|
||||||
prepare_dependencies();
|
|
||||||
prepare_dates();
|
|
||||||
prepare_canvas();
|
|
||||||
}
|
|
||||||
|
|
||||||
function prepare_tasks() {
|
|
||||||
|
|
||||||
// prepare tasks
|
|
||||||
self.tasks = self._tasks.map((task, i) => {
|
|
||||||
|
|
||||||
// momentify
|
|
||||||
task._start = moment(task.start, self.config.date_format);
|
|
||||||
task._end = moment(task.end, self.config.date_format);
|
|
||||||
|
|
||||||
// make task invalid if duration too large
|
|
||||||
if(task._end.diff(task._start, 'years') > 10) {
|
|
||||||
task.end = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// cache index
|
|
||||||
task._index = i;
|
|
||||||
|
|
||||||
// invalid dates
|
|
||||||
if(!task.start && !task.end) {
|
|
||||||
task._start = moment().startOf('day');
|
|
||||||
task._end = moment().startOf('day').add(2, 'days');
|
|
||||||
}
|
|
||||||
if(!task.start && task.end) {
|
|
||||||
task._start = task._end.clone().add(-2, 'days');
|
|
||||||
}
|
|
||||||
if(task.start && !task.end) {
|
|
||||||
task._end = task._start.clone().add(2, 'days');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function prepare_dependencies() {
|
|
||||||
|
|
||||||
self.dependency_map = {};
|
|
||||||
for(let t of self.tasks) {
|
|
||||||
for(let d of t.dependencies) {
|
|
||||||
self.dependency_map[d] = self.dependency_map[d] || [];
|
|
||||||
self.dependency_map[d].push(t.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function prepare_dates() {
|
|
||||||
|
|
||||||
self.gantt_start = self.gantt_end = null;
|
|
||||||
for(let task of self.tasks) {
|
|
||||||
// set global start and end date
|
|
||||||
if(!self.gantt_start || task._start < self.gantt_start) {
|
|
||||||
self.gantt_start = task._start;
|
|
||||||
}
|
|
||||||
if(!self.gantt_end || task._end > self.gantt_end) {
|
|
||||||
self.gantt_end = task._end;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
set_gantt_dates();
|
|
||||||
setup_dates();
|
|
||||||
}
|
|
||||||
|
|
||||||
function prepare_canvas() {
|
|
||||||
if(self.canvas) return;
|
|
||||||
self.canvas = Snap(self.element).addClass('gantt');
|
|
||||||
}
|
|
||||||
|
|
||||||
function render() {
|
|
||||||
clear();
|
|
||||||
setup_groups();
|
|
||||||
make_grid();
|
|
||||||
make_dates();
|
|
||||||
make_bars();
|
|
||||||
make_arrows();
|
|
||||||
map_arrows_on_bars();
|
|
||||||
set_width();
|
|
||||||
set_scroll_position();
|
|
||||||
bind_grid_click();
|
|
||||||
}
|
|
||||||
|
|
||||||
function clear() {
|
|
||||||
self.canvas.clear();
|
|
||||||
self._bars = [];
|
|
||||||
self._arrows = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
function set_gantt_dates() {
|
|
||||||
|
|
||||||
if(view_is(['Quarter Day', 'Half Day'])) {
|
|
||||||
self.gantt_start = self.gantt_start.clone().subtract(7, 'day');
|
|
||||||
self.gantt_end = self.gantt_end.clone().add(7, 'day');
|
|
||||||
} else if(view_is('Month')) {
|
|
||||||
self.gantt_start = self.gantt_start.clone().startOf('year');
|
|
||||||
self.gantt_end = self.gantt_end.clone().endOf('month').add(1, 'year');
|
|
||||||
} else {
|
|
||||||
self.gantt_start = self.gantt_start.clone().startOf('month').subtract(1, 'month');
|
|
||||||
self.gantt_end = self.gantt_end.clone().endOf('month').add(1, 'month');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setup_dates() {
|
|
||||||
|
|
||||||
self.dates = [];
|
|
||||||
let cur_date = null;
|
|
||||||
|
|
||||||
while(cur_date === null || cur_date < self.gantt_end) {
|
|
||||||
if(!cur_date) {
|
|
||||||
cur_date = self.gantt_start.clone();
|
|
||||||
} else {
|
|
||||||
cur_date = view_is('Month') ?
|
|
||||||
cur_date.clone().add(1, 'month') :
|
|
||||||
cur_date.clone().add(self.config.step, 'hours');
|
|
||||||
}
|
|
||||||
self.dates.push(cur_date);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setup_groups() {
|
|
||||||
|
|
||||||
const groups = ['grid', 'date', 'arrow', 'progress', 'bar', 'details'];
|
|
||||||
// make group layers
|
|
||||||
for(let group of groups) {
|
|
||||||
self.element_groups[group] = self.canvas.group().attr({'id': group});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function set_scale(scale) {
|
|
||||||
self.config.view_mode = scale;
|
|
||||||
|
|
||||||
if(scale === 'Day') {
|
|
||||||
self.config.step = 24;
|
|
||||||
self.config.column_width = 38;
|
|
||||||
} else if(scale === 'Half Day') {
|
|
||||||
self.config.step = 24 / 2;
|
|
||||||
self.config.column_width = 38;
|
|
||||||
} else if(scale === 'Quarter Day') {
|
|
||||||
self.config.step = 24 / 4;
|
|
||||||
self.config.column_width = 38;
|
|
||||||
} else if(scale === 'Week') {
|
|
||||||
self.config.step = 24 * 7;
|
|
||||||
self.config.column_width = 140;
|
|
||||||
} else if(scale === 'Month') {
|
|
||||||
self.config.step = 24 * 30;
|
|
||||||
self.config.column_width = 120;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function set_width() {
|
|
||||||
const cur_width = self.canvas.node.getBoundingClientRect().width;
|
|
||||||
const actual_width = self.canvas.select('#grid .grid-row').attr('width');
|
|
||||||
if(cur_width < actual_width) {
|
|
||||||
self.canvas.attr('width', actual_width);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function set_scroll_position() {
|
|
||||||
const parent_element = self.element.parentElement;
|
|
||||||
|
|
||||||
if(!parent_element) return;
|
|
||||||
|
|
||||||
const scroll_pos = get_min_date().diff(self.gantt_start, 'hours') /
|
|
||||||
self.config.step * self.config.column_width - self.config.column_width;
|
|
||||||
parent_element.scrollLeft = scroll_pos;
|
|
||||||
}
|
|
||||||
|
|
||||||
function get_min_date() {
|
|
||||||
const task = self.tasks.reduce((acc, curr) => {
|
|
||||||
return curr._start.isSameOrBefore(acc._start) ? curr : acc;
|
|
||||||
});
|
|
||||||
return task._start;
|
|
||||||
}
|
|
||||||
|
|
||||||
function make_grid() {
|
|
||||||
make_grid_background();
|
|
||||||
make_grid_rows();
|
|
||||||
make_grid_header();
|
|
||||||
make_grid_ticks();
|
|
||||||
make_grid_highlights();
|
|
||||||
}
|
|
||||||
|
|
||||||
function make_grid_background() {
|
|
||||||
|
|
||||||
const grid_width = self.dates.length * self.config.column_width,
|
|
||||||
grid_height = self.config.header_height + self.config.padding +
|
|
||||||
(self.config.bar.height + self.config.padding) * self.tasks.length;
|
|
||||||
|
|
||||||
self.canvas.rect(0, 0, grid_width, grid_height)
|
|
||||||
.addClass('grid-background')
|
|
||||||
.appendTo(self.element_groups.grid);
|
|
||||||
|
|
||||||
self.canvas.attr({
|
|
||||||
height: grid_height + self.config.padding + 100,
|
|
||||||
width: '100%'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function make_grid_header() {
|
|
||||||
const header_width = self.dates.length * self.config.column_width,
|
|
||||||
header_height = self.config.header_height + 10;
|
|
||||||
self.canvas.rect(0, 0, header_width, header_height)
|
|
||||||
.addClass('grid-header')
|
|
||||||
.appendTo(self.element_groups.grid);
|
|
||||||
}
|
|
||||||
|
|
||||||
function make_grid_rows() {
|
|
||||||
|
|
||||||
const rows = self.canvas.group().appendTo(self.element_groups.grid),
|
|
||||||
lines = self.canvas.group().appendTo(self.element_groups.grid),
|
|
||||||
row_width = self.dates.length * self.config.column_width,
|
|
||||||
row_height = self.config.bar.height + self.config.padding;
|
|
||||||
|
|
||||||
let row_y = self.config.header_height + self.config.padding / 2;
|
|
||||||
|
|
||||||
for(let task of self.tasks) { // eslint-disable-line
|
|
||||||
self.canvas.rect(0, row_y, row_width, row_height)
|
|
||||||
.addClass('grid-row')
|
|
||||||
.appendTo(rows);
|
|
||||||
|
|
||||||
self.canvas.line(0, row_y + row_height, row_width, row_y + row_height)
|
|
||||||
.addClass('row-line')
|
|
||||||
.appendTo(lines);
|
|
||||||
|
|
||||||
row_y += self.config.bar.height + self.config.padding;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function make_grid_ticks() {
|
|
||||||
let tick_x = 0,
|
|
||||||
tick_y = self.config.header_height + self.config.padding / 2,
|
|
||||||
tick_height = (self.config.bar.height + self.config.padding) * self.tasks.length;
|
|
||||||
|
|
||||||
for(let date of self.dates) {
|
|
||||||
let tick_class = 'tick';
|
|
||||||
// thick tick for monday
|
|
||||||
if(view_is('Day') && date.day() === 1) {
|
|
||||||
tick_class += ' thick';
|
|
||||||
}
|
|
||||||
// thick tick for first week
|
|
||||||
if(view_is('Week') && date.date() >= 1 && date.date() < 8) {
|
|
||||||
tick_class += ' thick';
|
|
||||||
}
|
|
||||||
// thick ticks for quarters
|
|
||||||
if(view_is('Month') && date.month() % 3 === 0) {
|
|
||||||
tick_class += ' thick';
|
|
||||||
}
|
|
||||||
|
|
||||||
self.canvas.path(Snap.format('M {x} {y} v {height}', {
|
|
||||||
x: tick_x,
|
|
||||||
y: tick_y,
|
|
||||||
height: tick_height
|
|
||||||
}))
|
|
||||||
.addClass(tick_class)
|
|
||||||
.appendTo(self.element_groups.grid);
|
|
||||||
|
|
||||||
if(view_is('Month')) {
|
|
||||||
tick_x += date.daysInMonth() * self.config.column_width / 30;
|
|
||||||
} else {
|
|
||||||
tick_x += self.config.column_width;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function make_grid_highlights() {
|
|
||||||
|
|
||||||
// highlight today's date
|
|
||||||
if(view_is('Day')) {
|
|
||||||
const x = moment().startOf('day').diff(self.gantt_start, 'hours') /
|
|
||||||
self.config.step * self.config.column_width;
|
|
||||||
const y = 0;
|
|
||||||
const width = self.config.column_width;
|
|
||||||
const height = (self.config.bar.height + self.config.padding) * self.tasks.length +
|
|
||||||
self.config.header_height + self.config.padding / 2;
|
|
||||||
|
|
||||||
self.canvas.rect(x, y, width, height)
|
|
||||||
.addClass('today-highlight')
|
|
||||||
.appendTo(self.element_groups.grid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function make_dates() {
|
|
||||||
|
|
||||||
for(let date of get_dates_to_draw()) {
|
|
||||||
self.canvas.text(date.lower_x, date.lower_y, date.lower_text)
|
|
||||||
.addClass('lower-text')
|
|
||||||
.appendTo(self.element_groups.date);
|
|
||||||
|
|
||||||
if(date.upper_text) {
|
|
||||||
const $upper_text = self.canvas.text(date.upper_x, date.upper_y, date.upper_text)
|
|
||||||
.addClass('upper-text')
|
|
||||||
.appendTo(self.element_groups.date);
|
|
||||||
|
|
||||||
// remove out-of-bound dates
|
|
||||||
if($upper_text.getBBox().x2 > self.element_groups.grid.getBBox().width) {
|
|
||||||
$upper_text.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function get_dates_to_draw() {
|
|
||||||
let last_date = null;
|
|
||||||
const dates = self.dates.map((date, i) => {
|
|
||||||
const d = get_date_info(date, last_date, i);
|
|
||||||
last_date = date;
|
|
||||||
return d;
|
|
||||||
});
|
|
||||||
return dates;
|
|
||||||
}
|
|
||||||
|
|
||||||
function get_date_info(date, last_date, i) {
|
|
||||||
if(!last_date) {
|
|
||||||
last_date = date.clone().add(1, 'year');
|
|
||||||
}
|
|
||||||
const date_text = {
|
|
||||||
'Quarter Day_lower': date.format('HH'),
|
|
||||||
'Half Day_lower': date.format('HH'),
|
|
||||||
'Day_lower': date.date() !== last_date.date() ? date.format('D') : '',
|
|
||||||
'Week_lower': date.month() !== last_date.month() ?
|
|
||||||
date.format('D MMM') : date.format('D'),
|
|
||||||
'Month_lower': date.format('MMMM'),
|
|
||||||
'Quarter Day_upper': date.date() !== last_date.date() ? date.format('D MMM') : '',
|
|
||||||
'Half Day_upper': date.date() !== last_date.date() ?
|
|
||||||
date.month() !== last_date.month() ?
|
|
||||||
date.format('D MMM') : date.format('D') : '',
|
|
||||||
'Day_upper': date.month() !== last_date.month() ? date.format('MMMM') : '',
|
|
||||||
'Week_upper': date.month() !== last_date.month() ? date.format('MMMM') : '',
|
|
||||||
'Month_upper': date.year() !== last_date.year() ? date.format('YYYY') : ''
|
|
||||||
};
|
|
||||||
|
|
||||||
const base_pos = {
|
|
||||||
x: i * self.config.column_width,
|
|
||||||
lower_y: self.config.header_height,
|
|
||||||
upper_y: self.config.header_height - 25
|
|
||||||
};
|
|
||||||
|
|
||||||
const x_pos = {
|
|
||||||
'Quarter Day_lower': (self.config.column_width * 4) / 2,
|
|
||||||
'Quarter Day_upper': 0,
|
|
||||||
'Half Day_lower': (self.config.column_width * 2) / 2,
|
|
||||||
'Half Day_upper': 0,
|
|
||||||
'Day_lower': self.config.column_width / 2,
|
|
||||||
'Day_upper': (self.config.column_width * 30) / 2,
|
|
||||||
'Week_lower': 0,
|
|
||||||
'Week_upper': (self.config.column_width * 4) / 2,
|
|
||||||
'Month_lower': self.config.column_width / 2,
|
|
||||||
'Month_upper': (self.config.column_width * 12) / 2
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
upper_text: date_text[`${self.config.view_mode}_upper`],
|
|
||||||
lower_text: date_text[`${self.config.view_mode}_lower`],
|
|
||||||
upper_x: base_pos.x + x_pos[`${self.config.view_mode}_upper`],
|
|
||||||
upper_y: base_pos.upper_y,
|
|
||||||
lower_x: base_pos.x + x_pos[`${self.config.view_mode}_lower`],
|
|
||||||
lower_y: base_pos.lower_y
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function make_arrows() {
|
|
||||||
self._arrows = [];
|
|
||||||
for(let task of self.tasks) {
|
|
||||||
let arrows = [];
|
|
||||||
arrows = task.dependencies.map(dep => {
|
|
||||||
const dependency = get_task(dep);
|
|
||||||
if(!dependency) return;
|
|
||||||
|
|
||||||
const arrow = Arrow(
|
|
||||||
self, // gt
|
|
||||||
self._bars[dependency._index], // from_task
|
|
||||||
self._bars[task._index] // to_task
|
|
||||||
);
|
|
||||||
self.element_groups.arrow.add(arrow.element);
|
|
||||||
return arrow; // eslint-disable-line
|
|
||||||
}).filter(arr => arr); // filter falsy values
|
|
||||||
self._arrows = self._arrows.concat(arrows);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function make_bars() {
|
|
||||||
|
|
||||||
self._bars = self.tasks.map((task) => {
|
|
||||||
const bar = Bar(self, task);
|
|
||||||
self.element_groups.bar.add(bar.group);
|
|
||||||
return bar;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function map_arrows_on_bars() {
|
|
||||||
for(let bar of self._bars) {
|
|
||||||
bar.arrows = self._arrows.filter(arrow => {
|
|
||||||
return (arrow.from_task.task.id === bar.task.id) ||
|
|
||||||
(arrow.to_task.task.id === bar.task.id);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function bind_grid_click() {
|
|
||||||
self.element_groups.grid.click(() => {
|
|
||||||
unselect_all();
|
|
||||||
self.element_groups.details
|
|
||||||
.selectAll('.details-wrapper')
|
|
||||||
.forEach(el => el.addClass('hide'));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function unselect_all() {
|
|
||||||
self.canvas.selectAll('.bar-wrapper').forEach(el => {
|
|
||||||
el.removeClass('active');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function view_is(modes) {
|
|
||||||
if (typeof modes === 'string') {
|
|
||||||
return self.config.view_mode === modes;
|
|
||||||
} else if(Array.isArray(modes)) {
|
|
||||||
for (let mode of modes) {
|
|
||||||
if(self.config.view_mode === mode) return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function get_task(id) {
|
|
||||||
return self.tasks.find((task) => {
|
|
||||||
return task.id === id;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function get_bar(id) {
|
|
||||||
return self._bars.find((bar) => {
|
|
||||||
return bar.task.id === id;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function generate_id(task) {
|
|
||||||
return task.name + '_' + Math.random().toString(36).slice(2, 12);
|
|
||||||
}
|
|
||||||
|
|
||||||
function trigger_event(event, args) {
|
|
||||||
if(self.config['on_' + event]) {
|
|
||||||
self.config['on_' + event].apply(null, args);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init();
|
|
||||||
|
|
||||||
return self;
|
|
||||||
}
|
|
||||||
103
src/arrow.js
Normal file
103
src/arrow.js
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
start_x -= 10;
|
||||||
|
|
||||||
|
let start_y =
|
||||||
|
this.gantt.config.header_height +
|
||||||
|
this.gantt.options.bar_height +
|
||||||
|
(this.gantt.options.padding + this.gantt.options.bar_height) *
|
||||||
|
this.from_task.task._index +
|
||||||
|
this.gantt.options.padding / 2;
|
||||||
|
|
||||||
|
let end_x = this.to_task.$bar.getX() - 13;
|
||||||
|
let end_y =
|
||||||
|
this.gantt.config.header_height +
|
||||||
|
this.gantt.options.bar_height / 2 +
|
||||||
|
(this.gantt.options.padding + this.gantt.options.bar_height) *
|
||||||
|
this.to_task.task._index +
|
||||||
|
this.gantt.options.padding / 2;
|
||||||
|
|
||||||
|
const from_is_below_to =
|
||||||
|
this.from_task.task._index > this.to_task.task._index;
|
||||||
|
|
||||||
|
let curve = this.gantt.options.arrow_curve;
|
||||||
|
const clockwise = from_is_below_to ? 1 : 0;
|
||||||
|
let curve_y = from_is_below_to ? -curve : curve;
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.to_task.$bar.getX() <=
|
||||||
|
this.from_task.$bar.getX() + this.gantt.options.padding
|
||||||
|
) {
|
||||||
|
let down_1 = this.gantt.options.padding / 2 - curve;
|
||||||
|
if (down_1 < 0) {
|
||||||
|
down_1 = 0;
|
||||||
|
curve = this.gantt.options.padding / 2;
|
||||||
|
curve_y = from_is_below_to ? -curve : curve;
|
||||||
|
}
|
||||||
|
const down_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`;
|
||||||
|
} else {
|
||||||
|
if (end_x < start_x + curve) curve = end_x - start_x;
|
||||||
|
|
||||||
|
let offset = from_is_below_to ? end_y + curve : end_y - curve;
|
||||||
|
|
||||||
|
this.path = `
|
||||||
|
M ${start_x} ${start_y}
|
||||||
|
V ${offset}
|
||||||
|
a ${curve} ${curve} 0 0 ${clockwise} ${curve} ${curve}
|
||||||
|
L ${end_x} ${end_y}
|
||||||
|
m -5 -5
|
||||||
|
l 5 5
|
||||||
|
l -5 5`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
737
src/bar.js
Normal file
737
src/bar.js
Normal file
@ -0,0 +1,737 @@
|
|||||||
|
import date_utils from './date_utils';
|
||||||
|
import { $, createSVG, animateSVG } from './svg_utils';
|
||||||
|
|
||||||
|
export default class Bar {
|
||||||
|
constructor(gantt, task) {
|
||||||
|
this.set_defaults(gantt, task);
|
||||||
|
this.prepare_wrappers();
|
||||||
|
this.prepare_helpers();
|
||||||
|
this.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh() {
|
||||||
|
this.bar_group.innerHTML = '';
|
||||||
|
this.handle_group.innerHTML = '';
|
||||||
|
if (this.task.custom_class) {
|
||||||
|
this.group.classList.add(this.task.custom_class);
|
||||||
|
} else {
|
||||||
|
this.group.classList = ['bar-wrapper'];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.prepare_values();
|
||||||
|
this.draw();
|
||||||
|
this.bind();
|
||||||
|
}
|
||||||
|
|
||||||
|
set_defaults(gantt, task) {
|
||||||
|
this.action_completed = false;
|
||||||
|
this.gantt = gantt;
|
||||||
|
this.task = task;
|
||||||
|
this.name = this.name || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
prepare_wrappers() {
|
||||||
|
this.group = createSVG('g', {
|
||||||
|
class:
|
||||||
|
'bar-wrapper' +
|
||||||
|
(this.task.custom_class ? ' ' + this.task.custom_class : ''),
|
||||||
|
'data-id': this.task.id,
|
||||||
|
});
|
||||||
|
this.bar_group = createSVG('g', {
|
||||||
|
class: 'bar-group',
|
||||||
|
append_to: this.group,
|
||||||
|
});
|
||||||
|
this.handle_group = createSVG('g', {
|
||||||
|
class: 'handle-group',
|
||||||
|
append_to: this.group,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
prepare_values() {
|
||||||
|
this.invalid = this.task.invalid;
|
||||||
|
this.height = this.gantt.options.bar_height;
|
||||||
|
this.image_size = this.height - 5;
|
||||||
|
this.task._start = new Date(this.task.start);
|
||||||
|
this.task._end = new Date(this.task.end);
|
||||||
|
this.compute_x();
|
||||||
|
this.compute_y();
|
||||||
|
this.compute_duration();
|
||||||
|
this.corner_radius = this.gantt.options.bar_corner_radius;
|
||||||
|
this.width = this.gantt.config.column_width * this.duration;
|
||||||
|
if (!this.task.progress || this.task.progress < 0)
|
||||||
|
this.task.progress = 0;
|
||||||
|
if (this.task.progress > 100) this.task.progress = 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
prepare_helpers() {
|
||||||
|
SVGElement.prototype.getX = function () {
|
||||||
|
return +this.getAttribute('x');
|
||||||
|
};
|
||||||
|
SVGElement.prototype.getY = function () {
|
||||||
|
return +this.getAttribute('y');
|
||||||
|
};
|
||||||
|
SVGElement.prototype.getWidth = function () {
|
||||||
|
return +this.getAttribute('width');
|
||||||
|
};
|
||||||
|
SVGElement.prototype.getHeight = function () {
|
||||||
|
return +this.getAttribute('height');
|
||||||
|
};
|
||||||
|
SVGElement.prototype.getEndX = function () {
|
||||||
|
return this.getX() + this.getWidth();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
prepare_expected_progress_values() {
|
||||||
|
this.compute_expected_progress();
|
||||||
|
this.expected_progress_width =
|
||||||
|
this.gantt.options.column_width *
|
||||||
|
this.duration *
|
||||||
|
(this.expected_progress / 100) || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
draw() {
|
||||||
|
this.draw_bar();
|
||||||
|
this.draw_progress_bar();
|
||||||
|
if (this.gantt.options.show_expected_progress) {
|
||||||
|
this.prepare_expected_progress_values();
|
||||||
|
this.draw_expected_progress_bar();
|
||||||
|
}
|
||||||
|
this.draw_label();
|
||||||
|
this.draw_resize_handles();
|
||||||
|
|
||||||
|
if (this.task.thumbnail) {
|
||||||
|
this.draw_thumbnail();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
draw_bar() {
|
||||||
|
this.$bar = createSVG('rect', {
|
||||||
|
x: this.x,
|
||||||
|
y: this.y,
|
||||||
|
width: this.width,
|
||||||
|
height: this.height,
|
||||||
|
rx: this.corner_radius,
|
||||||
|
ry: this.corner_radius,
|
||||||
|
class: 'bar',
|
||||||
|
append_to: this.bar_group,
|
||||||
|
});
|
||||||
|
if (this.task.color) this.$bar.style.fill = this.task.color;
|
||||||
|
animateSVG(this.$bar, 'width', 0, this.width);
|
||||||
|
|
||||||
|
if (this.invalid) {
|
||||||
|
this.$bar.classList.add('bar-invalid');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
draw_expected_progress_bar() {
|
||||||
|
if (this.invalid) return;
|
||||||
|
this.$expected_bar_progress = createSVG('rect', {
|
||||||
|
x: this.x,
|
||||||
|
y: this.y,
|
||||||
|
width: this.expected_progress_width,
|
||||||
|
height: this.height,
|
||||||
|
rx: this.corner_radius,
|
||||||
|
ry: this.corner_radius,
|
||||||
|
class: 'bar-expected-progress',
|
||||||
|
append_to: this.bar_group,
|
||||||
|
});
|
||||||
|
|
||||||
|
animateSVG(
|
||||||
|
this.$expected_bar_progress,
|
||||||
|
'width',
|
||||||
|
0,
|
||||||
|
this.expected_progress_width,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
draw_progress_bar() {
|
||||||
|
if (this.invalid) return;
|
||||||
|
this.progress_width = this.calculate_progress_width();
|
||||||
|
let r = this.corner_radius;
|
||||||
|
if (!/^((?!chrome|android).)*safari/i.test(navigator.userAgent))
|
||||||
|
r = this.corner_radius + 2;
|
||||||
|
this.$bar_progress = createSVG('rect', {
|
||||||
|
x: this.x,
|
||||||
|
y: this.y,
|
||||||
|
width: this.progress_width,
|
||||||
|
height: this.height,
|
||||||
|
rx: r,
|
||||||
|
ry: r,
|
||||||
|
class: 'bar-progress',
|
||||||
|
append_to: this.bar_group,
|
||||||
|
});
|
||||||
|
if (this.task.color_progress)
|
||||||
|
this.$bar_progress.style.fill = this.task.color_progress;
|
||||||
|
const x =
|
||||||
|
(date_utils.diff(
|
||||||
|
this.task._start,
|
||||||
|
this.gantt.gantt_start,
|
||||||
|
this.gantt.config.unit,
|
||||||
|
) /
|
||||||
|
this.gantt.config.step) *
|
||||||
|
this.gantt.config.column_width;
|
||||||
|
|
||||||
|
let $date_highlight = this.gantt.create_el({
|
||||||
|
classes: `date-range-highlight hide highlight-${this.task.id}`,
|
||||||
|
width: this.width,
|
||||||
|
left: x,
|
||||||
|
});
|
||||||
|
this.$date_highlight = $date_highlight;
|
||||||
|
this.gantt.$lower_header.prepend(this.$date_highlight);
|
||||||
|
|
||||||
|
animateSVG(this.$bar_progress, 'width', 0, this.progress_width);
|
||||||
|
}
|
||||||
|
|
||||||
|
calculate_progress_width() {
|
||||||
|
const width = this.$bar.getWidth();
|
||||||
|
const ignored_end = this.x + width;
|
||||||
|
const total_ignored_area =
|
||||||
|
this.gantt.config.ignored_positions.reduce((acc, val) => {
|
||||||
|
return acc + (val >= this.x && val < ignored_end);
|
||||||
|
}, 0) * this.gantt.config.column_width;
|
||||||
|
let progress_width =
|
||||||
|
((width - total_ignored_area) * this.task.progress) / 100;
|
||||||
|
const progress_end = this.x + progress_width;
|
||||||
|
const total_ignored_progress =
|
||||||
|
this.gantt.config.ignored_positions.reduce((acc, val) => {
|
||||||
|
return acc + (val >= this.x && val < progress_end);
|
||||||
|
}, 0) * this.gantt.config.column_width;
|
||||||
|
|
||||||
|
progress_width += total_ignored_progress;
|
||||||
|
|
||||||
|
let ignored_regions = this.gantt.get_ignored_region(
|
||||||
|
this.x + progress_width,
|
||||||
|
);
|
||||||
|
|
||||||
|
while (ignored_regions.length) {
|
||||||
|
progress_width += this.gantt.config.column_width;
|
||||||
|
ignored_regions = this.gantt.get_ignored_region(
|
||||||
|
this.x + progress_width,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.progress_width = progress_width;
|
||||||
|
return progress_width;
|
||||||
|
}
|
||||||
|
|
||||||
|
draw_label() {
|
||||||
|
let x_coord = this.x + this.$bar.getWidth() / 2;
|
||||||
|
|
||||||
|
if (this.task.thumbnail) {
|
||||||
|
x_coord = this.x + this.image_size + 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
createSVG('text', {
|
||||||
|
x: x_coord,
|
||||||
|
y: this.y + this.height / 2,
|
||||||
|
innerHTML: this.task.name,
|
||||||
|
class: 'bar-label',
|
||||||
|
append_to: this.bar_group,
|
||||||
|
});
|
||||||
|
// labels get BBox in the next tick
|
||||||
|
requestAnimationFrame(() => this.update_label_position());
|
||||||
|
}
|
||||||
|
|
||||||
|
draw_thumbnail() {
|
||||||
|
let x_offset = 10,
|
||||||
|
y_offset = 2;
|
||||||
|
let defs, clipPath;
|
||||||
|
|
||||||
|
defs = createSVG('defs', {
|
||||||
|
append_to: this.bar_group,
|
||||||
|
});
|
||||||
|
|
||||||
|
createSVG('rect', {
|
||||||
|
id: 'rect_' + this.task.id,
|
||||||
|
x: this.x + x_offset,
|
||||||
|
y: this.y + y_offset,
|
||||||
|
width: this.image_size,
|
||||||
|
height: this.image_size,
|
||||||
|
rx: '15',
|
||||||
|
class: 'img_mask',
|
||||||
|
append_to: defs,
|
||||||
|
});
|
||||||
|
|
||||||
|
clipPath = createSVG('clipPath', {
|
||||||
|
id: 'clip_' + this.task.id,
|
||||||
|
append_to: defs,
|
||||||
|
});
|
||||||
|
|
||||||
|
createSVG('use', {
|
||||||
|
href: '#rect_' + this.task.id,
|
||||||
|
append_to: clipPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
createSVG('image', {
|
||||||
|
x: this.x + x_offset,
|
||||||
|
y: this.y + y_offset,
|
||||||
|
width: this.image_size,
|
||||||
|
height: this.image_size,
|
||||||
|
class: 'bar-img',
|
||||||
|
href: this.task.thumbnail,
|
||||||
|
clipPath: 'clip_' + this.task.id,
|
||||||
|
append_to: this.bar_group,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
draw_resize_handles() {
|
||||||
|
if (this.invalid || this.gantt.options.readonly) return;
|
||||||
|
|
||||||
|
const bar = this.$bar;
|
||||||
|
const handle_width = 3;
|
||||||
|
this.handles = [];
|
||||||
|
if (!this.gantt.options.readonly_dates) {
|
||||||
|
this.handles.push(
|
||||||
|
createSVG('rect', {
|
||||||
|
x: bar.getEndX() - handle_width / 2,
|
||||||
|
y: bar.getY() + this.height / 4,
|
||||||
|
width: handle_width,
|
||||||
|
height: this.height / 2,
|
||||||
|
rx: 2,
|
||||||
|
ry: 2,
|
||||||
|
class: 'handle right',
|
||||||
|
append_to: this.handle_group,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.handles.push(
|
||||||
|
createSVG('rect', {
|
||||||
|
x: bar.getX() - handle_width / 2,
|
||||||
|
y: bar.getY() + this.height / 4,
|
||||||
|
width: handle_width,
|
||||||
|
height: this.height / 2,
|
||||||
|
rx: 2,
|
||||||
|
ry: 2,
|
||||||
|
class: 'handle left',
|
||||||
|
append_to: this.handle_group,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!this.gantt.options.readonly_progress) {
|
||||||
|
const bar_progress = this.$bar_progress;
|
||||||
|
this.$handle_progress = createSVG('circle', {
|
||||||
|
cx: bar_progress.getEndX(),
|
||||||
|
cy: bar_progress.getY() + bar_progress.getHeight() / 2,
|
||||||
|
r: 4.5,
|
||||||
|
class: 'handle progress',
|
||||||
|
append_to: this.handle_group,
|
||||||
|
});
|
||||||
|
this.handles.push(this.$handle_progress);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let handle of this.handles) {
|
||||||
|
$.on(handle, 'mouseenter', () => handle.classList.add('active'));
|
||||||
|
$.on(handle, 'mouseleave', () => handle.classList.remove('active'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bind() {
|
||||||
|
if (this.invalid) return;
|
||||||
|
this.setup_click_event();
|
||||||
|
}
|
||||||
|
|
||||||
|
setup_click_event() {
|
||||||
|
let task_id = this.task.id;
|
||||||
|
$.on(this.group, 'mouseover', (e) => {
|
||||||
|
this.gantt.trigger_event('hover', [
|
||||||
|
this.task,
|
||||||
|
e.screenX,
|
||||||
|
e.screenY,
|
||||||
|
e,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.gantt.options.popup_on === 'click') {
|
||||||
|
$.on(this.group, 'mouseup', (e) => {
|
||||||
|
const posX = e.offsetX || e.layerX;
|
||||||
|
if (this.$handle_progress) {
|
||||||
|
const cx = +this.$handle_progress.getAttribute('cx');
|
||||||
|
if (cx > posX - 1 && cx < posX + 1) return;
|
||||||
|
if (this.gantt.bar_being_dragged) return;
|
||||||
|
}
|
||||||
|
this.gantt.show_popup({
|
||||||
|
x: e.offsetX || e.layerX,
|
||||||
|
y: e.offsetY || e.layerY,
|
||||||
|
task: this.task,
|
||||||
|
target: this.$bar,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let timeout;
|
||||||
|
$.on(this.group, 'mouseenter', (e) => {
|
||||||
|
timeout = setTimeout(() => {
|
||||||
|
if (this.gantt.options.popup_on === 'hover')
|
||||||
|
this.gantt.show_popup({
|
||||||
|
x: e.offsetX || e.layerX,
|
||||||
|
y: e.offsetY || e.layerY,
|
||||||
|
task: this.task,
|
||||||
|
target: this.$bar,
|
||||||
|
});
|
||||||
|
this.gantt.$container
|
||||||
|
.querySelector(`.highlight-${task_id}`)
|
||||||
|
.classList.remove('hide');
|
||||||
|
}, 200);
|
||||||
|
});
|
||||||
|
$.on(this.group, 'mouseleave', () => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
if (this.gantt.options.popup_on === 'hover')
|
||||||
|
this.gantt.popup?.hide?.();
|
||||||
|
this.gantt.$container
|
||||||
|
.querySelector(`.highlight-${task_id}`)
|
||||||
|
.classList.add('hide');
|
||||||
|
});
|
||||||
|
|
||||||
|
$.on(this.group, 'click', () => {
|
||||||
|
this.gantt.trigger_event('click', [this.task]);
|
||||||
|
});
|
||||||
|
|
||||||
|
$.on(this.group, 'dblclick', (e) => {
|
||||||
|
if (this.action_completed) {
|
||||||
|
// just finished a move action, wait for a few seconds
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.group.classList.remove('active');
|
||||||
|
if (this.gantt.popup)
|
||||||
|
this.gantt.popup.parent.classList.remove('hide');
|
||||||
|
|
||||||
|
this.gantt.trigger_event('double_click', [this.task]);
|
||||||
|
});
|
||||||
|
let tapedTwice = false;
|
||||||
|
$.on(this.group, 'touchstart', (e) => {
|
||||||
|
if (!tapedTwice) {
|
||||||
|
tapedTwice = true;
|
||||||
|
setTimeout(function () { tapedTwice = false; }, 300);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
//action on double tap goes below
|
||||||
|
|
||||||
|
|
||||||
|
if (this.action_completed) {
|
||||||
|
// just finished a move action, wait for a few seconds
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.group.classList.remove('active');
|
||||||
|
if (this.gantt.popup)
|
||||||
|
this.gantt.popup.parent.classList.remove('hide');
|
||||||
|
|
||||||
|
this.gantt.trigger_event('double_click', [this.task]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
update_bar_position({ x = null, width = null }) {
|
||||||
|
const bar = this.$bar;
|
||||||
|
|
||||||
|
if (x) {
|
||||||
|
const xs = this.task.dependencies.map((dep) => {
|
||||||
|
return this.gantt.get_bar(dep).$bar.getX();
|
||||||
|
});
|
||||||
|
const valid_x = xs.reduce((prev, curr) => {
|
||||||
|
return prev && x >= curr;
|
||||||
|
}, true);
|
||||||
|
if (!valid_x) return;
|
||||||
|
this.update_attr(bar, 'x', x);
|
||||||
|
this.x = x;
|
||||||
|
this.$date_highlight.style.left = x + 'px';
|
||||||
|
}
|
||||||
|
if (width > 0) {
|
||||||
|
this.update_attr(bar, 'width', width);
|
||||||
|
this.$date_highlight.style.width = width + 'px';
|
||||||
|
}
|
||||||
|
|
||||||
|
this.update_label_position();
|
||||||
|
this.update_handle_position();
|
||||||
|
this.date_changed();
|
||||||
|
this.compute_duration();
|
||||||
|
|
||||||
|
if (this.gantt.options.show_expected_progress) {
|
||||||
|
this.update_expected_progressbar_position();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.update_progressbar_position();
|
||||||
|
this.update_arrow_position();
|
||||||
|
}
|
||||||
|
|
||||||
|
update_label_position_on_horizontal_scroll({ x, sx }) {
|
||||||
|
const container =
|
||||||
|
this.gantt.$container.querySelector('.gantt-container');
|
||||||
|
const label = this.group.querySelector('.bar-label');
|
||||||
|
const img = this.group.querySelector('.bar-img') || '';
|
||||||
|
const img_mask = this.bar_group.querySelector('.img_mask') || '';
|
||||||
|
|
||||||
|
let barWidthLimit = this.$bar.getX() + this.$bar.getWidth();
|
||||||
|
let newLabelX = label.getX() + x;
|
||||||
|
let newImgX = (img && img.getX() + x) || 0;
|
||||||
|
let imgWidth = (img && img.getBBox().width + 7) || 7;
|
||||||
|
let labelEndX = newLabelX + label.getBBox().width + 7;
|
||||||
|
let viewportCentral = sx + container.clientWidth / 2;
|
||||||
|
|
||||||
|
if (label.classList.contains('big')) return;
|
||||||
|
|
||||||
|
if (labelEndX < barWidthLimit && x > 0 && labelEndX < viewportCentral) {
|
||||||
|
label.setAttribute('x', newLabelX);
|
||||||
|
if (img) {
|
||||||
|
img.setAttribute('x', newImgX);
|
||||||
|
img_mask.setAttribute('x', newImgX);
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
newLabelX - imgWidth > this.$bar.getX() &&
|
||||||
|
x < 0 &&
|
||||||
|
labelEndX > viewportCentral
|
||||||
|
) {
|
||||||
|
label.setAttribute('x', newLabelX);
|
||||||
|
if (img) {
|
||||||
|
img.setAttribute('x', newImgX);
|
||||||
|
img_mask.setAttribute('x', newImgX);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
date_changed() {
|
||||||
|
let changed = false;
|
||||||
|
const { new_start_date, new_end_date } = this.compute_start_end_date();
|
||||||
|
if (Number(this.task._start) !== Number(new_start_date)) {
|
||||||
|
changed = true;
|
||||||
|
this.task._start = new_start_date;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Number(this.task._end) !== Number(new_end_date)) {
|
||||||
|
changed = true;
|
||||||
|
this.task._end = new_end_date;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!changed) return;
|
||||||
|
|
||||||
|
this.gantt.trigger_event('date_change', [
|
||||||
|
this.task,
|
||||||
|
new_start_date,
|
||||||
|
date_utils.add(new_end_date, -1, 'second'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
progress_changed() {
|
||||||
|
this.task.progress = this.compute_progress();
|
||||||
|
this.gantt.trigger_event('progress_change', [
|
||||||
|
this.task,
|
||||||
|
this.task.progress,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
set_action_completed() {
|
||||||
|
this.action_completed = true;
|
||||||
|
setTimeout(() => (this.action_completed = false), 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
compute_start_end_date() {
|
||||||
|
const bar = this.$bar;
|
||||||
|
const x_in_units = bar.getX() / this.gantt.config.column_width;
|
||||||
|
let new_start_date = date_utils.add(
|
||||||
|
this.gantt.gantt_start,
|
||||||
|
x_in_units * this.gantt.config.step,
|
||||||
|
this.gantt.config.unit,
|
||||||
|
);
|
||||||
|
|
||||||
|
const width_in_units = bar.getWidth() / this.gantt.config.column_width;
|
||||||
|
const new_end_date = date_utils.add(
|
||||||
|
new_start_date,
|
||||||
|
width_in_units * this.gantt.config.step,
|
||||||
|
this.gantt.config.unit,
|
||||||
|
);
|
||||||
|
|
||||||
|
return { new_start_date, new_end_date };
|
||||||
|
}
|
||||||
|
|
||||||
|
compute_progress() {
|
||||||
|
this.progress_width = this.$bar_progress.getWidth();
|
||||||
|
this.x = this.$bar_progress.getBBox().x;
|
||||||
|
const progress_area = this.x + this.progress_width;
|
||||||
|
const progress =
|
||||||
|
this.progress_width -
|
||||||
|
this.gantt.config.ignored_positions.reduce((acc, val) => {
|
||||||
|
return acc + (val >= this.x && val <= progress_area);
|
||||||
|
}, 0) *
|
||||||
|
this.gantt.config.column_width;
|
||||||
|
if (progress < 0) return 0;
|
||||||
|
const total =
|
||||||
|
this.$bar.getWidth() -
|
||||||
|
this.ignored_duration_raw * this.gantt.config.column_width;
|
||||||
|
return parseInt((progress / total) * 100, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
compute_expected_progress() {
|
||||||
|
this.expected_progress =
|
||||||
|
date_utils.diff(date_utils.today(), this.task._start, 'hour') /
|
||||||
|
this.gantt.config.step;
|
||||||
|
this.expected_progress =
|
||||||
|
((this.expected_progress < this.duration
|
||||||
|
? this.expected_progress
|
||||||
|
: this.duration) *
|
||||||
|
100) /
|
||||||
|
this.duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
compute_x() {
|
||||||
|
const { column_width } = this.gantt.config;
|
||||||
|
const task_start = this.task._start;
|
||||||
|
const gantt_start = this.gantt.gantt_start;
|
||||||
|
|
||||||
|
const diff =
|
||||||
|
date_utils.diff(task_start, gantt_start, this.gantt.config.unit) /
|
||||||
|
this.gantt.config.step;
|
||||||
|
|
||||||
|
let x = diff * column_width;
|
||||||
|
|
||||||
|
/* Since the column width is based on 30,
|
||||||
|
we count the month-difference, multiply it by 30 for a "pseudo-month"
|
||||||
|
and then add the days in the month, making sure the number does not exceed 29
|
||||||
|
so it is within the column */
|
||||||
|
|
||||||
|
// if (this.gantt.view_is('Month')) {
|
||||||
|
// const diffDaysBasedOn30DayMonths =
|
||||||
|
// date_utils.diff(task_start, gantt_start, 'month') * 30;
|
||||||
|
// const dayInMonth = Math.min(
|
||||||
|
// 29,
|
||||||
|
// date_utils.format(
|
||||||
|
// task_start,
|
||||||
|
// 'DD',
|
||||||
|
// this.gantt.options.language,
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
// const diff = diffDaysBasedOn30DayMonths + dayInMonth;
|
||||||
|
|
||||||
|
// x = (diff * column_width) / 30;
|
||||||
|
// }
|
||||||
|
|
||||||
|
this.x = x;
|
||||||
|
}
|
||||||
|
|
||||||
|
compute_y() {
|
||||||
|
this.y =
|
||||||
|
this.gantt.config.header_height +
|
||||||
|
this.gantt.options.padding / 2 +
|
||||||
|
this.task._index * (this.height + this.gantt.options.padding);
|
||||||
|
}
|
||||||
|
|
||||||
|
compute_duration() {
|
||||||
|
let actual_duration_in_days = 0,
|
||||||
|
duration_in_days = 0;
|
||||||
|
for (
|
||||||
|
let d = new Date(this.task._start);
|
||||||
|
d < this.task._end;
|
||||||
|
d.setDate(d.getDate() + 1)
|
||||||
|
) {
|
||||||
|
duration_in_days++;
|
||||||
|
if (
|
||||||
|
!this.gantt.config.ignored_dates.find(
|
||||||
|
(k) => k.getTime() === d.getTime(),
|
||||||
|
) &&
|
||||||
|
(!this.gantt.config.ignored_function ||
|
||||||
|
!this.gantt.config.ignored_function(d))
|
||||||
|
) {
|
||||||
|
actual_duration_in_days++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.task.actual_duration = actual_duration_in_days;
|
||||||
|
this.task.ignored_duration = duration_in_days - actual_duration_in_days;
|
||||||
|
|
||||||
|
this.duration =
|
||||||
|
date_utils.convert_scales(
|
||||||
|
duration_in_days + 'd',
|
||||||
|
this.gantt.config.unit,
|
||||||
|
) / this.gantt.config.step;
|
||||||
|
|
||||||
|
this.actual_duration_raw =
|
||||||
|
date_utils.convert_scales(
|
||||||
|
actual_duration_in_days + 'd',
|
||||||
|
this.gantt.config.unit,
|
||||||
|
) / this.gantt.config.step;
|
||||||
|
|
||||||
|
this.ignored_duration_raw = this.duration - this.actual_duration_raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
update_attr(element, attr, value) {
|
||||||
|
value = +value;
|
||||||
|
if (!isNaN(value)) {
|
||||||
|
element.setAttribute(attr, value);
|
||||||
|
}
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
update_expected_progressbar_position() {
|
||||||
|
if (this.invalid) return;
|
||||||
|
this.$expected_bar_progress.setAttribute('x', this.$bar.getX());
|
||||||
|
this.compute_expected_progress();
|
||||||
|
this.$expected_bar_progress.setAttribute(
|
||||||
|
'width',
|
||||||
|
this.gantt.config.column_width *
|
||||||
|
this.actual_duration_raw *
|
||||||
|
(this.expected_progress / 100) || 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
update_progressbar_position() {
|
||||||
|
if (this.invalid || this.gantt.options.readonly) return;
|
||||||
|
this.$bar_progress.setAttribute('x', this.$bar.getX());
|
||||||
|
|
||||||
|
this.$bar_progress.setAttribute(
|
||||||
|
'width',
|
||||||
|
this.calculate_progress_width(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
update_label_position() {
|
||||||
|
const img_mask = this.bar_group.querySelector('.img_mask') || '';
|
||||||
|
const bar = this.$bar,
|
||||||
|
label = this.group.querySelector('.bar-label'),
|
||||||
|
img = this.group.querySelector('.bar-img');
|
||||||
|
|
||||||
|
let padding = 5;
|
||||||
|
let x_offset_label_img = this.image_size + 10;
|
||||||
|
const labelWidth = label.getBBox().width;
|
||||||
|
const barWidth = bar.getWidth();
|
||||||
|
if (labelWidth > barWidth) {
|
||||||
|
label.classList.add('big');
|
||||||
|
if (img) {
|
||||||
|
img.setAttribute('x', bar.getEndX() + padding);
|
||||||
|
img_mask.setAttribute('x', bar.getEndX() + padding);
|
||||||
|
label.setAttribute('x', bar.getEndX() + x_offset_label_img);
|
||||||
|
} else {
|
||||||
|
label.setAttribute('x', bar.getEndX() + padding);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
label.classList.remove('big');
|
||||||
|
if (img) {
|
||||||
|
img.setAttribute('x', bar.getX() + padding);
|
||||||
|
img_mask.setAttribute('x', bar.getX() + padding);
|
||||||
|
label.setAttribute(
|
||||||
|
'x',
|
||||||
|
bar.getX() + barWidth / 2 + x_offset_label_img,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
label.setAttribute(
|
||||||
|
'x',
|
||||||
|
bar.getX() + barWidth / 2 - labelWidth / 2,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
update_handle_position() {
|
||||||
|
if (this.invalid || this.gantt.options.readonly) return;
|
||||||
|
const bar = this.$bar;
|
||||||
|
this.handle_group
|
||||||
|
.querySelector('.handle.left')
|
||||||
|
.setAttribute('x', bar.getX());
|
||||||
|
this.handle_group
|
||||||
|
.querySelector('.handle.right')
|
||||||
|
.setAttribute('x', bar.getEndX());
|
||||||
|
const handle = this.group.querySelector('.handle.progress');
|
||||||
|
handle && handle.setAttribute('cx', this.$bar_progress.getEndX());
|
||||||
|
}
|
||||||
|
|
||||||
|
update_arrow_position() {
|
||||||
|
this.arrows = this.arrows || [];
|
||||||
|
for (let arrow of this.arrows) {
|
||||||
|
arrow.update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
292
src/date_utils.js
Normal file
292
src/date_utils.js
Normal file
@ -0,0 +1,292 @@
|
|||||||
|
const YEAR = 'year';
|
||||||
|
const MONTH = 'month';
|
||||||
|
const DAY = 'day';
|
||||||
|
const HOUR = 'hour';
|
||||||
|
const MINUTE = 'minute';
|
||||||
|
const SECOND = 'second';
|
||||||
|
const MILLISECOND = 'millisecond';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
parse_duration(duration) {
|
||||||
|
const regex = /([0-9]+)(y|m|d|h|min|s|ms)/gm;
|
||||||
|
const matches = regex.exec(duration);
|
||||||
|
if (matches !== null) {
|
||||||
|
if (matches[2] === 'y') {
|
||||||
|
return { duration: parseInt(matches[1]), scale: `year` };
|
||||||
|
} else if (matches[2] === 'm') {
|
||||||
|
return { duration: parseInt(matches[1]), scale: `month` };
|
||||||
|
} else if (matches[2] === 'd') {
|
||||||
|
return { duration: parseInt(matches[1]), scale: `day` };
|
||||||
|
} else if (matches[2] === 'h') {
|
||||||
|
return { duration: parseInt(matches[1]), scale: `hour` };
|
||||||
|
} else if (matches[2] === 'min') {
|
||||||
|
return { duration: parseInt(matches[1]), scale: `minute` };
|
||||||
|
} else if (matches[2] === 's') {
|
||||||
|
return { duration: parseInt(matches[1]), scale: `second` };
|
||||||
|
} else if (matches[2] === 'ms') {
|
||||||
|
return { duration: parseInt(matches[1]), scale: `millisecond` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
parse(date, date_separator = '-', time_separator = /[.:]/) {
|
||||||
|
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] ? date_parts[1] - 1 : 0;
|
||||||
|
|
||||||
|
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, date_format = 'YYYY-MM-DD HH:mm:ss.SSS', lang = 'en') {
|
||||||
|
const dateTimeFormat = new Intl.DateTimeFormat(lang, {
|
||||||
|
month: 'long',
|
||||||
|
});
|
||||||
|
const dateTimeFormatShort = new Intl.DateTimeFormat(lang, {
|
||||||
|
month: 'short',
|
||||||
|
});
|
||||||
|
const month_name = dateTimeFormat.format(date);
|
||||||
|
const month_name_capitalized =
|
||||||
|
month_name.charAt(0).toUpperCase() + month_name.slice(1);
|
||||||
|
|
||||||
|
const values = this.get_date_values(date).map((d) => padStart(d, 2, 0));
|
||||||
|
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_name_capitalized,
|
||||||
|
MMM: dateTimeFormatShort.format(date),
|
||||||
|
};
|
||||||
|
|
||||||
|
let str = date_format;
|
||||||
|
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.replaceAll(key, `$${formatted_values.length}`);
|
||||||
|
formatted_values.push(format_map[key]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
formatted_values.forEach((value, i) => {
|
||||||
|
str = str.replaceAll(`$${i}`, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
return str;
|
||||||
|
},
|
||||||
|
|
||||||
|
diff(date_a, date_b, scale = 'day') {
|
||||||
|
let milliseconds, seconds, hours, minutes, days, months, years;
|
||||||
|
|
||||||
|
milliseconds =
|
||||||
|
date_a -
|
||||||
|
date_b +
|
||||||
|
(date_b.getTimezoneOffset() - date_a.getTimezoneOffset()) * 60000;
|
||||||
|
seconds = milliseconds / 1000;
|
||||||
|
minutes = seconds / 60;
|
||||||
|
hours = minutes / 60;
|
||||||
|
days = hours / 24;
|
||||||
|
// Calculate months across years
|
||||||
|
let yearDiff = date_a.getFullYear() - date_b.getFullYear();
|
||||||
|
let monthDiff = date_a.getMonth() - date_b.getMonth();
|
||||||
|
// calculate extra
|
||||||
|
monthDiff += (days % 30) / 30;
|
||||||
|
|
||||||
|
/* If monthDiff is negative, date_b is in an earlier month than
|
||||||
|
date_a and thus subtracted from the year difference in months */
|
||||||
|
months = yearDiff * 12 + monthDiff;
|
||||||
|
/* If date_a's (e.g. march 1st) day of the month is smaller than date_b (e.g. february 28th),
|
||||||
|
adjust the month difference */
|
||||||
|
if (date_a.getDate() < date_b.getDate()) {
|
||||||
|
months--;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate years based on actual months
|
||||||
|
years = months / 12;
|
||||||
|
|
||||||
|
if (!scale.endsWith('s')) {
|
||||||
|
scale += 's';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
Math.round(
|
||||||
|
{
|
||||||
|
milliseconds,
|
||||||
|
seconds,
|
||||||
|
minutes,
|
||||||
|
hours,
|
||||||
|
days,
|
||||||
|
months,
|
||||||
|
years,
|
||||||
|
}[scale] * 100,
|
||||||
|
) / 100
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
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(),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
convert_scales(period, to_scale) {
|
||||||
|
const TO_DAYS = {
|
||||||
|
millisecond: 1 / 60 / 60 / 24 / 1000,
|
||||||
|
second: 1 / 60 / 60 / 24,
|
||||||
|
minute: 1 / 60 / 24,
|
||||||
|
hour: 1 / 24,
|
||||||
|
day: 1,
|
||||||
|
month: 30,
|
||||||
|
year: 365,
|
||||||
|
};
|
||||||
|
const { duration, scale } = this.parse_duration(period);
|
||||||
|
let in_days = duration * TO_DAYS[scale];
|
||||||
|
return in_days / TO_DAYS[to_scale];
|
||||||
|
},
|
||||||
|
|
||||||
|
get_days_in_month(date) {
|
||||||
|
const no_of_days = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
||||||
|
|
||||||
|
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;
|
||||||
|
},
|
||||||
|
|
||||||
|
get_days_in_year(date) {
|
||||||
|
return date.getFullYear() % 4 ? 365 : 366;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/padStart
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
160
src/defaults.js
Normal file
160
src/defaults.js
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
import date_utils from './date_utils';
|
||||||
|
|
||||||
|
function getDecade(d) {
|
||||||
|
const year = d.getFullYear();
|
||||||
|
return year - (year % 10) + '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatWeek(d, ld, lang) {
|
||||||
|
let endOfWeek = date_utils.add(d, 6, 'day');
|
||||||
|
let endFormat = endOfWeek.getMonth() !== d.getMonth() ? 'D MMM' : 'D';
|
||||||
|
let beginFormat = !ld || d.getMonth() !== ld.getMonth() ? 'D MMM' : 'D';
|
||||||
|
return `${date_utils.format(d, beginFormat, lang)} - ${date_utils.format(endOfWeek, endFormat, lang)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_VIEW_MODES = [
|
||||||
|
{
|
||||||
|
name: 'Hour',
|
||||||
|
padding: '7d',
|
||||||
|
step: '1h',
|
||||||
|
date_format: 'YYYY-MM-DD HH:',
|
||||||
|
lower_text: 'HH',
|
||||||
|
upper_text: (d, ld, lang) =>
|
||||||
|
!ld || d.getDate() !== ld.getDate()
|
||||||
|
? date_utils.format(d, 'D MMMM', lang)
|
||||||
|
: '',
|
||||||
|
upper_text_frequency: 24,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Quarter Day',
|
||||||
|
padding: '7d',
|
||||||
|
step: '6h',
|
||||||
|
date_format: 'YYYY-MM-DD HH:',
|
||||||
|
lower_text: 'HH',
|
||||||
|
upper_text: (d, ld, lang) =>
|
||||||
|
!ld || d.getDate() !== ld.getDate()
|
||||||
|
? date_utils.format(d, 'D MMM', lang)
|
||||||
|
: '',
|
||||||
|
upper_text_frequency: 4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Half Day',
|
||||||
|
padding: '14d',
|
||||||
|
step: '12h',
|
||||||
|
date_format: 'YYYY-MM-DD HH:',
|
||||||
|
lower_text: 'HH',
|
||||||
|
upper_text: (d, ld, lang) =>
|
||||||
|
!ld || d.getDate() !== ld.getDate()
|
||||||
|
? d.getMonth() !== d.getMonth()
|
||||||
|
? date_utils.format(d, 'D MMM', lang)
|
||||||
|
: date_utils.format(d, 'D', lang)
|
||||||
|
: '',
|
||||||
|
upper_text_frequency: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Day',
|
||||||
|
padding: '7d',
|
||||||
|
date_format: 'YYYY-MM-DD',
|
||||||
|
step: '1d',
|
||||||
|
lower_text: (d, ld, lang) =>
|
||||||
|
!ld || d.getDate() !== ld.getDate()
|
||||||
|
? date_utils.format(d, 'D', lang)
|
||||||
|
: '',
|
||||||
|
upper_text: (d, ld, lang) =>
|
||||||
|
!ld || d.getMonth() !== ld.getMonth()
|
||||||
|
? date_utils.format(d, 'MMMM', lang)
|
||||||
|
: '',
|
||||||
|
thick_line: (d) => d.getDay() === 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Week',
|
||||||
|
padding: '1m',
|
||||||
|
step: '7d',
|
||||||
|
date_format: 'YYYY-MM-DD',
|
||||||
|
column_width: 140,
|
||||||
|
lower_text: formatWeek,
|
||||||
|
upper_text: (d, ld, lang) =>
|
||||||
|
!ld || d.getMonth() !== ld.getMonth()
|
||||||
|
? date_utils.format(d, 'MMMM', lang)
|
||||||
|
: '',
|
||||||
|
thick_line: (d) => d.getDate() >= 1 && d.getDate() <= 7,
|
||||||
|
upper_text_frequency: 4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Month',
|
||||||
|
padding: '2m',
|
||||||
|
step: '1m',
|
||||||
|
column_width: 120,
|
||||||
|
date_format: 'YYYY-MM',
|
||||||
|
lower_text: 'MMMM',
|
||||||
|
upper_text: (d, ld, lang) =>
|
||||||
|
!ld || d.getFullYear() !== ld.getFullYear()
|
||||||
|
? date_utils.format(d, 'YYYY', lang)
|
||||||
|
: '',
|
||||||
|
thick_line: (d) => d.getMonth() % 3 === 0,
|
||||||
|
snap_at: '7d',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Year',
|
||||||
|
padding: '2y',
|
||||||
|
step: '1y',
|
||||||
|
column_width: 120,
|
||||||
|
date_format: 'YYYY',
|
||||||
|
upper_text: (d, ld, lang) =>
|
||||||
|
!ld || getDecade(d) !== getDecade(ld) ? getDecade(d) : '',
|
||||||
|
lower_text: 'YYYY',
|
||||||
|
snap_at: '30d',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const DEFAULT_OPTIONS = {
|
||||||
|
arrow_curve: 5,
|
||||||
|
auto_move_label: false,
|
||||||
|
bar_corner_radius: 3,
|
||||||
|
bar_height: 30,
|
||||||
|
container_height: 'auto',
|
||||||
|
column_width: null,
|
||||||
|
date_format: 'YYYY-MM-DD HH:mm',
|
||||||
|
upper_header_height: 45,
|
||||||
|
lower_header_height: 30,
|
||||||
|
snap_at: null,
|
||||||
|
infinite_padding: true,
|
||||||
|
holidays: { 'var(--g-weekend-highlight-color)': 'weekend' },
|
||||||
|
ignore: [],
|
||||||
|
language: 'en',
|
||||||
|
lines: 'both',
|
||||||
|
move_dependencies: true,
|
||||||
|
padding: 18,
|
||||||
|
popup: (ctx) => {
|
||||||
|
ctx.set_title(ctx.task.name);
|
||||||
|
if (ctx.task.description) ctx.set_subtitle(ctx.task.description);
|
||||||
|
else ctx.set_subtitle('');
|
||||||
|
|
||||||
|
const start_date = date_utils.format(
|
||||||
|
ctx.task._start,
|
||||||
|
'MMM D',
|
||||||
|
ctx.chart.options.language,
|
||||||
|
);
|
||||||
|
const end_date = date_utils.format(
|
||||||
|
date_utils.add(ctx.task._end, -1, 'second'),
|
||||||
|
'MMM D',
|
||||||
|
ctx.chart.options.language,
|
||||||
|
);
|
||||||
|
|
||||||
|
ctx.set_details(
|
||||||
|
`${start_date} - ${end_date} (${ctx.task.actual_duration} days${ctx.task.ignored_duration ? ' + ' + ctx.task.ignored_duration + ' excluded' : ''})<br/>Progress: ${Math.floor(ctx.task.progress * 100) / 100}%`,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
popup_on: 'click',
|
||||||
|
readonly_progress: false,
|
||||||
|
readonly_dates: false,
|
||||||
|
readonly: false,
|
||||||
|
scroll_to: 'today',
|
||||||
|
show_expected_progress: false,
|
||||||
|
today_button: true,
|
||||||
|
view_mode: 'Day',
|
||||||
|
view_mode_select: false,
|
||||||
|
view_modes: DEFAULT_VIEW_MODES,
|
||||||
|
};
|
||||||
|
|
||||||
|
export { DEFAULT_OPTIONS, DEFAULT_VIEW_MODES };
|
||||||
152
src/gantt.scss
152
src/gantt.scss
@ -1,152 +0,0 @@
|
|||||||
$bar-color: #b8c2cc;
|
|
||||||
$bar-stroke: #8D99A6;
|
|
||||||
$border-color: #e0e0e0;
|
|
||||||
$light-bg: #f5f5f5;
|
|
||||||
$light-border-color: #ebeff2;
|
|
||||||
$light-yellow: #fcf8e3;
|
|
||||||
$text-muted: #666;
|
|
||||||
$text-light: #555;
|
|
||||||
$text-color: #333;
|
|
||||||
$blue: #a3a3ff;
|
|
||||||
$handle-color: #ddd;
|
|
||||||
|
|
||||||
.gantt {
|
|
||||||
|
|
||||||
.grid-background {
|
|
||||||
fill: none;
|
|
||||||
}
|
|
||||||
.grid-header {
|
|
||||||
fill: #ffffff;
|
|
||||||
stroke: $border-color;
|
|
||||||
stroke-width: 1.4;
|
|
||||||
}
|
|
||||||
.grid-row {
|
|
||||||
fill: #ffffff;
|
|
||||||
}
|
|
||||||
.grid-row:nth-child(even) {
|
|
||||||
fill: $light-bg;
|
|
||||||
}
|
|
||||||
.row-line {
|
|
||||||
stroke: $light-border-color;
|
|
||||||
}
|
|
||||||
.tick {
|
|
||||||
stroke: $border-color;
|
|
||||||
stroke-width: 0.2;
|
|
||||||
&.thick {
|
|
||||||
stroke-width: 0.4;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.today-highlight {
|
|
||||||
fill: $light-yellow;
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
#arrow {
|
|
||||||
fill: none;
|
|
||||||
stroke: $text-muted;
|
|
||||||
stroke-width: 1.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bar {
|
|
||||||
fill: $bar-color;
|
|
||||||
stroke: $bar-stroke;
|
|
||||||
stroke-width: 0;
|
|
||||||
transition: stroke-width .3s ease;
|
|
||||||
}
|
|
||||||
.bar-progress {
|
|
||||||
fill: $blue;
|
|
||||||
}
|
|
||||||
.bar-invalid {
|
|
||||||
fill: transparent;
|
|
||||||
stroke: $bar-stroke;
|
|
||||||
stroke-width: 1;
|
|
||||||
stroke-dasharray: 5;
|
|
||||||
|
|
||||||
&~.bar-label {
|
|
||||||
fill: $text-light;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.bar-label {
|
|
||||||
fill: #fff;
|
|
||||||
dominant-baseline: central;
|
|
||||||
text-anchor: middle;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: lighter;
|
|
||||||
|
|
||||||
&.big {
|
|
||||||
fill: $text-light;
|
|
||||||
text-anchor: start;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.handle {
|
|
||||||
fill: $handle-color;
|
|
||||||
cursor: ew-resize;
|
|
||||||
opacity: 0;
|
|
||||||
visibility: hidden;
|
|
||||||
transition: opacity .3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bar-wrapper {
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
.bar {
|
|
||||||
stroke-width: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.handle {
|
|
||||||
visibility: visible;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
.bar {
|
|
||||||
stroke-width: 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.lower-text, .upper-text {
|
|
||||||
font-size: 12px;
|
|
||||||
text-anchor: middle;
|
|
||||||
}
|
|
||||||
.upper-text {
|
|
||||||
fill: $text-light;
|
|
||||||
}
|
|
||||||
.lower-text {
|
|
||||||
fill: $text-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
#details .details-container {
|
|
||||||
background: #fff;
|
|
||||||
display: inline-block;
|
|
||||||
padding: 12px;
|
|
||||||
|
|
||||||
h5, p {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
h5 {
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: bold;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
color: $text-light;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
font-size: 12px;
|
|
||||||
margin-bottom: 6px;
|
|
||||||
color: $text-muted;
|
|
||||||
}
|
|
||||||
|
|
||||||
p:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.hide {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
1580
src/index.js
Normal file
1580
src/index.js
Normal file
File diff suppressed because it is too large
Load Diff
61
src/popup.js
Normal file
61
src/popup.js
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
export default class Popup {
|
||||||
|
constructor(parent, popup_func, gantt) {
|
||||||
|
this.parent = parent;
|
||||||
|
this.popup_func = popup_func;
|
||||||
|
this.gantt = gantt;
|
||||||
|
|
||||||
|
this.make();
|
||||||
|
}
|
||||||
|
|
||||||
|
make() {
|
||||||
|
this.parent.innerHTML = `
|
||||||
|
<div class="title"></div>
|
||||||
|
<div class="subtitle"></div>
|
||||||
|
<div class="details"></div>
|
||||||
|
<div class="actions"></div>
|
||||||
|
`;
|
||||||
|
this.hide();
|
||||||
|
|
||||||
|
this.title = this.parent.querySelector('.title');
|
||||||
|
this.subtitle = this.parent.querySelector('.subtitle');
|
||||||
|
this.details = this.parent.querySelector('.details');
|
||||||
|
this.actions = this.parent.querySelector('.actions');
|
||||||
|
}
|
||||||
|
|
||||||
|
show({ x, y, task, target }) {
|
||||||
|
this.actions.innerHTML = '';
|
||||||
|
let html = this.popup_func({
|
||||||
|
task,
|
||||||
|
chart: this.gantt,
|
||||||
|
get_title: () => this.title,
|
||||||
|
set_title: (title) => (this.title.innerHTML = title),
|
||||||
|
get_subtitle: () => this.subtitle,
|
||||||
|
set_subtitle: (subtitle) => (this.subtitle.innerHTML = subtitle),
|
||||||
|
get_details: () => this.details,
|
||||||
|
set_details: (details) => (this.details.innerHTML = details),
|
||||||
|
add_action: (html, func) => {
|
||||||
|
let action = this.gantt.create_el({
|
||||||
|
classes: 'action-btn',
|
||||||
|
type: 'button',
|
||||||
|
append_to: this.actions,
|
||||||
|
});
|
||||||
|
if (typeof html === 'function') html = html(task);
|
||||||
|
action.innerHTML = html;
|
||||||
|
action.onclick = (e) => func(task, this.gantt, e);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (html === false) return;
|
||||||
|
if (html) this.parent.innerHTML = html;
|
||||||
|
|
||||||
|
if (this.actions.innerHTML === '') this.actions.remove();
|
||||||
|
else this.parent.appendChild(this.actions);
|
||||||
|
|
||||||
|
this.parent.style.left = x + 10 + 'px';
|
||||||
|
this.parent.style.top = y - 10 + 'px';
|
||||||
|
this.parent.classList.remove('hide');
|
||||||
|
}
|
||||||
|
|
||||||
|
hide() {
|
||||||
|
this.parent.classList.add('hide');
|
||||||
|
}
|
||||||
|
}
|
||||||
87
src/styles/dark.css
Normal file
87
src/styles/dark.css
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
:root {
|
||||||
|
--g-bar-stroke-dark: #c6ccd2;
|
||||||
|
--g-border-color-dark: #616161;
|
||||||
|
--g-bar-color-dark: #616161;
|
||||||
|
--g-bg-dark: #3e3e3e;
|
||||||
|
--g-light-border-color-dark: #3e3e3e;
|
||||||
|
--g-text-muted-dark: #eee;
|
||||||
|
--g-text-light-dark: #ececec;
|
||||||
|
--g-text-color-dark: #f7f7f7;
|
||||||
|
--g-progress-color: #8a8aff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark > .gantt-container .gantt {
|
||||||
|
& .grid-row {
|
||||||
|
fill: #252525;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .row-line {
|
||||||
|
stroke: var(--g-light-border-color-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
& .tick {
|
||||||
|
stroke: var(--g-border-color-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
& .arrow {
|
||||||
|
stroke: var(--g-text-muted-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
& .bar {
|
||||||
|
fill: var(--g-bar-color-dark);
|
||||||
|
stroke: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .bar-progress {
|
||||||
|
fill: var(--g-progress-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
& .bar-invalid {
|
||||||
|
fill: transparent;
|
||||||
|
stroke: var(--g-bar-stroke-dark);
|
||||||
|
|
||||||
|
& ~ .bar-label {
|
||||||
|
fill: var(--g-text-light-dark);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& .bar-label.big {
|
||||||
|
fill: var(--g-text-light-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
& .bar-wrapper {
|
||||||
|
&:hover {
|
||||||
|
.bar {
|
||||||
|
fill: lighten(var(--g-bar-color-dark, 5));
|
||||||
|
}
|
||||||
|
|
||||||
|
& .bar-progress {
|
||||||
|
fill: lighten(var(--g-progress-color, 5));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
.bar {
|
||||||
|
fill: lighten(var(--g-bar-color-dark, 5));
|
||||||
|
}
|
||||||
|
|
||||||
|
& .bar-progress {
|
||||||
|
fill: lighten(var(--g-progress-color, 5));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark > .gantt-container {
|
||||||
|
& .grid-header {
|
||||||
|
background-color: #252525;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .popup-wrapper {
|
||||||
|
background-color: #333;
|
||||||
|
|
||||||
|
& .title {
|
||||||
|
border-color: lighten(var(--g-progress-color, 5));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
343
src/styles/gantt.css
Normal file
343
src/styles/gantt.css
Normal file
@ -0,0 +1,343 @@
|
|||||||
|
@import './light.css';
|
||||||
|
|
||||||
|
.gantt-container {
|
||||||
|
line-height: 14.5px;
|
||||||
|
position: relative;
|
||||||
|
overflow: auto;
|
||||||
|
font-size: 12px;
|
||||||
|
height: var(--gv-grid-height);
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
& .popup-wrapper {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0px 10px 24px -3px rgba(0, 0, 0, 0.2);
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
width: max-content;
|
||||||
|
z-index: 1000;
|
||||||
|
|
||||||
|
& .title {
|
||||||
|
margin-bottom: 2px;
|
||||||
|
color: var(--g-text-dark);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 650;
|
||||||
|
line-height: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .subtitle {
|
||||||
|
color: var(--g-text-dark);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .details {
|
||||||
|
color: var(--g-text-muted);
|
||||||
|
font-size: 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .actions {
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-left: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .action-btn {
|
||||||
|
border: none;
|
||||||
|
padding: 5px 8px;
|
||||||
|
background-color: var(--g-popup-actions);
|
||||||
|
border-right: 1px solid var(--g-text-light);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: brightness(97%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
border-top-left-radius: 4px;
|
||||||
|
border-bottom-left-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-right: none;
|
||||||
|
border-top-right-radius: 4px;
|
||||||
|
border-bottom-right-radius: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& .grid-header {
|
||||||
|
height: calc(
|
||||||
|
var(--gv-lower-header-height) + var(--gv-upper-header-height) + 10px
|
||||||
|
);
|
||||||
|
background-color: var(--g-header-background);
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
border-bottom: 1px solid var(--g-row-border-color);
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .lower-text,
|
||||||
|
& .upper-text {
|
||||||
|
text-anchor: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .upper-header {
|
||||||
|
height: var(--gv-upper-header-height);
|
||||||
|
}
|
||||||
|
|
||||||
|
& .lower-header {
|
||||||
|
height: var(--gv-lower-header-height);
|
||||||
|
}
|
||||||
|
|
||||||
|
& .lower-text {
|
||||||
|
font-size: 12px;
|
||||||
|
position: absolute;
|
||||||
|
width: calc(var(--gv-column-width) * 0.8);
|
||||||
|
height: calc(var(--gv-lower-header-height) * 0.8);
|
||||||
|
margin: 0 calc(var(--gv-column-width) * 0.1);
|
||||||
|
align-content: center;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--g-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
& .upper-text {
|
||||||
|
position: absolute;
|
||||||
|
width: fit-content;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--g-text-dark);
|
||||||
|
height: calc(var(--gv-lower-header-height) * 0.66);
|
||||||
|
}
|
||||||
|
|
||||||
|
& .current-upper {
|
||||||
|
position: sticky;
|
||||||
|
left: 0 !important;
|
||||||
|
padding-left: 17px;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .side-header {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
float: right;
|
||||||
|
|
||||||
|
z-index: 1000;
|
||||||
|
line-height: 20px;
|
||||||
|
font-weight: 400;
|
||||||
|
width: max-content;
|
||||||
|
margin-left: auto;
|
||||||
|
padding-right: 10px;
|
||||||
|
padding-top: 10px;
|
||||||
|
background: var(--g-header-background);
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .side-header * {
|
||||||
|
transition-property: background-color;
|
||||||
|
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
transition-duration: 150ms;
|
||||||
|
background-color: var(--g-actions-background);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
border: none;
|
||||||
|
padding: 5px 8px;
|
||||||
|
color: var(--g-text-dark);
|
||||||
|
font-size: 14px;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
font-weight: 420;
|
||||||
|
box-sizing: content-box;
|
||||||
|
|
||||||
|
margin-right: 5px;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
filter: brightness(97.5%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& .side-header select {
|
||||||
|
width: 60px;
|
||||||
|
padding-top: 2px;
|
||||||
|
padding-bottom: 2px;
|
||||||
|
}
|
||||||
|
& .side-header select:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .date-range-highlight {
|
||||||
|
background-color: var(--g-progress-color);
|
||||||
|
border-radius: 12px;
|
||||||
|
height: calc(var(--gv-lower-header-height) - 6px);
|
||||||
|
top: calc(var(--gv-upper-header-height) + 5px);
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .current-highlight {
|
||||||
|
position: absolute;
|
||||||
|
background: var(--g-today-highlight);
|
||||||
|
width: 1px;
|
||||||
|
z-index: 999;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .current-ball-highlight {
|
||||||
|
position: absolute;
|
||||||
|
background: var(--g-today-highlight);
|
||||||
|
z-index: 1001;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .current-date-highlight {
|
||||||
|
background: var(--g-today-highlight);
|
||||||
|
color: var(--g-text-light);
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .holiday-label {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
opacity: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
background: --g-weekend-label-color;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 2px 5px;
|
||||||
|
|
||||||
|
&.show {
|
||||||
|
opacity: 100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& .extras {
|
||||||
|
position: sticky;
|
||||||
|
left: 0px;
|
||||||
|
|
||||||
|
& .adjust {
|
||||||
|
position: absolute;
|
||||||
|
left: 8px;
|
||||||
|
top: calc(var(--gv-grid-height) - 60px);
|
||||||
|
background-color: rgba(0, 0, 0, 0.7);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hide {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.gantt {
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
& .grid-background {
|
||||||
|
fill: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .grid-row {
|
||||||
|
fill: var(--g-row-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
& .row-line {
|
||||||
|
stroke: var(--g-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
& .tick {
|
||||||
|
stroke: var(--g-tick-color);
|
||||||
|
stroke-width: 0.4;
|
||||||
|
|
||||||
|
&.thick {
|
||||||
|
stroke: var(--g-tick-color-thick);
|
||||||
|
stroke-width: 0.7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& .arrow {
|
||||||
|
fill: none;
|
||||||
|
stroke: var(--g-arrow-color);
|
||||||
|
stroke-width: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .bar-wrapper .bar {
|
||||||
|
fill: var(--g-bar-color);
|
||||||
|
stroke: var(--g-bar-border);
|
||||||
|
stroke-width: 0;
|
||||||
|
transition: stroke-width 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .bar-progress {
|
||||||
|
fill: var(--g-progress-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .bar-expected-progress {
|
||||||
|
fill: var(--g-expected-progress);
|
||||||
|
}
|
||||||
|
|
||||||
|
& .bar-invalid {
|
||||||
|
fill: transparent;
|
||||||
|
stroke: var(--g-bar-border);
|
||||||
|
stroke-width: 1;
|
||||||
|
stroke-dasharray: 5;
|
||||||
|
|
||||||
|
& ~ .bar-label {
|
||||||
|
fill: var(--g-text-light);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& .bar-label {
|
||||||
|
fill: var(--g-text-dark);
|
||||||
|
dominant-baseline: central;
|
||||||
|
font-family: Helvetica;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 400;
|
||||||
|
|
||||||
|
&.big {
|
||||||
|
fill: var(--g-text-dark);
|
||||||
|
text-anchor: start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& .handle {
|
||||||
|
fill: var(--g-handle-color);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
&.active,
|
||||||
|
&.visible {
|
||||||
|
cursor: ew-resize;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& .handle.progress {
|
||||||
|
fill: var(--g-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
& .bar-wrapper {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
& .bar {
|
||||||
|
outline: 1px solid var(--g-row-border-color);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.bar {
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-range-highlight {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
22
src/styles/light.css
Normal file
22
src/styles/light.css
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
:root {
|
||||||
|
--g-arrow-color: #1f2937;
|
||||||
|
--g-bar-color: #fff;
|
||||||
|
--g-bar-border: #fff;
|
||||||
|
--g-tick-color-thick: #ededed;
|
||||||
|
--g-tick-color: #f3f3f3;
|
||||||
|
--g-actions-background: #f3f3f3;
|
||||||
|
--g-border-color: #ebeff2;
|
||||||
|
--g-text-muted: #7c7c7c;
|
||||||
|
--g-text-light: #fff;
|
||||||
|
--g-text-dark: #171717;
|
||||||
|
--g-progress-color: #dbdbdb;
|
||||||
|
--g-handle-color: #37352f;
|
||||||
|
--g-weekend-label-color: #dcdce4;
|
||||||
|
--g-expected-progress: #c4c4e9;
|
||||||
|
--g-header-background: #fff;
|
||||||
|
--g-row-color: #fdfdfd;
|
||||||
|
--g-row-border-color: #c7c7c7;
|
||||||
|
--g-today-highlight: #37352f;
|
||||||
|
--g-popup-actions: #ebeff2;
|
||||||
|
--g-weekend-highlight-color: #f7f7f7;
|
||||||
|
}
|
||||||
135
src/svg_utils.js
Normal file
135
src/svg_utils.js
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
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 if (attr === 'clipPath') {
|
||||||
|
elem.setAttribute('clip-path', 'url(#' + attrs[attr] + ')');
|
||||||
|
} 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
tests/date_utils.test.js
Normal file
124
tests/date_utils.test.js
Normal file
@ -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');
|
||||||
|
});
|
||||||
21
vite.config.js
Normal file
21
vite.config.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { resolve } from 'path';
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
build: {
|
||||||
|
lib: {
|
||||||
|
entry: resolve(__dirname, 'src/index.js'),
|
||||||
|
name: 'Gantt',
|
||||||
|
fileName: 'frappe-gantt',
|
||||||
|
},
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
format: 'cjs',
|
||||||
|
assetFileNames: 'frappe-gantt[extname]',
|
||||||
|
entryFileNames: 'frappe-gantt.[format].js'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
output: { interop: 'auto' },
|
||||||
|
server: { watch: { include: ['dist/*', 'src/*'] } }
|
||||||
|
});
|
||||||
@ -1,52 +0,0 @@
|
|||||||
var webpack = require('webpack');
|
|
||||||
var UglifyJsPlugin = webpack.optimize.UglifyJsPlugin;
|
|
||||||
var path = require('path');
|
|
||||||
var env = require('yargs').argv.mode;
|
|
||||||
|
|
||||||
var libraryName = 'frappe-gantt';
|
|
||||||
|
|
||||||
var plugins = [], outputFile;
|
|
||||||
|
|
||||||
if (env === 'build') {
|
|
||||||
plugins.push(new UglifyJsPlugin({ minimize: true }));
|
|
||||||
outputFile = libraryName + '.min.js';
|
|
||||||
} else {
|
|
||||||
outputFile = libraryName + '.js';
|
|
||||||
}
|
|
||||||
|
|
||||||
var config = {
|
|
||||||
entry: __dirname + '/src/Gantt.js',
|
|
||||||
devtool: 'source-map',
|
|
||||||
output: {
|
|
||||||
path: __dirname + '/dist',
|
|
||||||
filename: outputFile,
|
|
||||||
library: 'Gantt',
|
|
||||||
libraryTarget: 'umd',
|
|
||||||
umdNamedDefine: true
|
|
||||||
},
|
|
||||||
module: {
|
|
||||||
loaders: [
|
|
||||||
{
|
|
||||||
test: /(\.jsx|\.js)$/,
|
|
||||||
loader: 'babel',
|
|
||||||
exclude: /(node_modules|bower_components)/
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /(\.jsx|\.js)$/,
|
|
||||||
loader: 'eslint-loader',
|
|
||||||
exclude: /node_modules/
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.scss$/,
|
|
||||||
loaders: [ 'style', 'css?sourceMap', 'sass?sourceMap' ]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
resolve: {
|
|
||||||
root: path.resolve('./src'),
|
|
||||||
extensions: ['', '.js']
|
|
||||||
},
|
|
||||||
plugins: plugins
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = config;
|
|
||||||
Loading…
x
Reference in New Issue
Block a user