├── .editorconfig ├── .gitignore ├── .travis.yml ├── .vscode └── symbols.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── angular-cli.json ├── docs └── images │ └── demo.gif ├── e2e ├── app.e2e-spec.ts ├── app.po.ts └── tsconfig.json ├── package.json ├── src ├── demo-app │ ├── demo-app.component.html │ ├── demo-app.component.ts │ ├── demo-app.module.ts │ ├── environments │ │ ├── environment.prod.ts │ │ └── environment.ts │ ├── index.html │ ├── index.ts │ ├── main.ts │ ├── polyfills.ts │ ├── styles.css │ ├── test.ts │ ├── tsconfig.json │ └── typings.d.ts └── lib │ ├── activity │ ├── background │ │ └── activity-background.component.ts │ ├── bars │ │ └── activity-bars.component.ts │ ├── gantt-activity.component.ts │ ├── gantt-activity.module.ts │ └── time-scale │ │ └── gantt-time-scale.component.ts │ ├── footer │ └── gantt-footer.component.ts │ ├── gantt.component.ts │ ├── gantt.module.ts │ ├── header │ └── gantt-header.component.ts │ ├── index.ts │ ├── package.json │ ├── shared │ ├── interfaces.ts │ ├── pipes │ │ └── groupBy.pipe.ts │ └── services │ │ ├── gantt-config.service.ts │ │ ├── gantt.service.spec.ts │ │ └── gantt.service.ts │ └── tsconfig.json ├── test ├── karma.config.js └── protractor.conf.js └── tslint.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | end_of_line = lf 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | max_line_length = 0 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | 7 | # dependencies 8 | /node_modules 9 | /bower_components 10 | 11 | # IDEs and editors 12 | /.idea 13 | /.vscode 14 | .project 15 | .classpath 16 | *.launch 17 | .settings/ 18 | 19 | # misc 20 | /.sass-cache 21 | /connect.lock 22 | /coverage/* 23 | /libpeerconnection.log 24 | npm-debug.log 25 | testem.log 26 | /typings 27 | 28 | # e2e 29 | /e2e/*.js 30 | /e2e/*.map 31 | 32 | #System Files 33 | .DS_Store 34 | Thumbs.db 35 | 36 | debug.log -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js -------------------------------------------------------------------------------- /.vscode/symbols.json: -------------------------------------------------------------------------------- 1 | {"symbols":{"Angular2GanttFinalPage":{"hasNamespace":false,"type":0,"moduleName":"app.po","relativePath":"e2e\\app.po"},"GanttHeaderComponent":{"hasNamespace":false,"type":0,"moduleName":"gantt-header.component","relativePath":"src\\app\\gantt\\header\\gantt-header.component"},"GanttActivityModule":{"hasNamespace":false,"type":0,"moduleName":"gantt-activity.module","relativePath":"src\\app\\gantt\\activity\\gantt-activity.module"},"GanttFooterComponent":{"hasNamespace":false,"type":0,"moduleName":"gantt-footer.component","relativePath":"src\\app\\gantt\\footer\\gantt-footer.component"},"GanttConfig":{"hasNamespace":false,"type":0,"moduleName":"gantt-config","relativePath":"src\\app\\gantt\\gantt-config"},"AppModule":{"hasNamespace":false,"type":0,"moduleName":"app.module","relativePath":"src\\app\\app.module"},"AppComponent":{"hasNamespace":false,"type":0,"moduleName":"app.component","relativePath":"src\\app\\app.component"},"GanttModule":{"hasNamespace":false,"type":0,"moduleName":"gantt.module","relativePath":"src\\app\\gantt\\gantt.module"},"GanttActivityBackgroundComponent":{"hasNamespace":false,"type":0,"moduleName":"activity-background.component","relativePath":"src\\app\\gantt\\activity\\background\\activity-background.component"},"GanttTimeScaleComponent":{"hasNamespace":false,"type":0,"moduleName":"gantt-time-scale.component","relativePath":"src\\app\\gantt\\activity\\time-scale\\gantt-time-scale.component"},"GanttActivityComponent":{"hasNamespace":false,"type":0,"moduleName":"gantt-activity.component","relativePath":"src\\app\\gantt\\activity\\gantt-activity.component"},"GanttActivityBarsComponent":{"hasNamespace":false,"type":0,"moduleName":"activity-bars.component","relativePath":"src\\app\\gantt\\activity\\bars\\activity-bars.component"},"GanttService":{"hasNamespace":false,"type":0,"moduleName":"gantt.service","relativePath":"src\\app\\gantt\\services\\gantt.service"},"GanttComponent":{"hasNamespace":false,"type":0,"moduleName":"gantt.component","relativePath":"src\\app\\gantt\\gantt.component"}},"files":{"e2e\\app.e2e-spec.ts":"2016-11-01T13:01:04.095Z","e2e\\app.po.ts":"2016-11-01T13:01:04.096Z","src\\main.ts":"2016-11-01T14:14:57.907Z","src\\polyfills.ts":"2016-11-01T13:01:04.096Z","src\\test.ts":"2016-11-01T13:01:04.096Z","src\\typings.d.ts":"2016-11-01T13:01:04.096Z","src.webpacktemplate\\main.ts":"2016-11-01T13:01:04.095Z","src.webpacktemplate\\polyfills.ts":"2016-11-01T13:01:04.096Z","src.webpacktemplate\\test.ts":"2016-11-01T13:01:04.096Z","src.webpacktemplate\\typings.d.ts":"2016-11-01T13:01:04.096Z","src\\app\\app.component.ts":"2016-11-01T17:47:34.451Z","src\\app\\app.module.ts":"2016-11-01T17:46:35.005Z","src.webpacktemplate\\app\\app.component.spec.ts":"2016-11-01T13:01:04.098Z","src.webpacktemplate\\app\\app.component.ts":"2016-11-01T13:01:04.098Z","src.webpacktemplate\\app\\app.module.ts":"2016-11-01T13:01:04.098Z","src.webpacktemplate\\environments\\environment.prod.ts":"2016-11-01T13:01:04.098Z","src.webpacktemplate\\environments\\environment.ts":"2016-11-01T13:01:04.099Z","src.webpacktemplate\\app\\index.ts":"2016-11-01T13:01:04.099Z","src\\app\\gantt\\gantt.component.ts":"2016-11-01T20:18:11.215Z","src\\app\\gantt\\gantt.module.ts":"2016-11-01T17:48:11.448Z","src\\app\\gantt\\activity\\gantt-activity.component.ts":"2016-11-01T18:04:57.998Z","src\\app\\gantt\\activity\\gantt-activity.module.ts":"2016-11-01T14:51:02.335Z","src\\app\\gantt\\header\\gantt-header.component.ts":"2016-11-01T14:02:04.351Z","src\\app\\gantt\\footer\\gantt-footer.component.ts":"2016-11-01T15:00:21.110Z","src\\app\\gantt\\activity\\background\\activity-background.component.ts":"2016-11-01T17:48:23.615Z","src\\app\\gantt\\activity\\bars\\activity-bars.component.ts":"2016-11-01T18:06:07.027Z","src\\app\\gantt\\activity\\time-scale\\gantt-time-scale.component.ts":"2016-11-01T17:50:49.038Z","src\\app\\index.ts":"2016-11-01T14:14:55.226Z","src\\app\\gantt\\services\\gantt.service.ts":"2016-11-01T18:11:58.435Z","src\\environments\\environment.prod.ts":"2016-11-01T13:01:04.098Z","src\\environments\\environment.ts":"2016-11-01T13:01:04.099Z","src\\app\\gantt\\gantt-config.ts":"2016-11-01T17:49:56.000Z","src\\app\\gantt\\services\\gantt.service.spec.ts":"2016-11-01T17:47:56.000Z"}} -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dalestone/angular2-gantt/91d3522f072ba09771e912a193ce75e219cb04cb/CHANGELOG.md -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2016 Google, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Angular 2 Gantt 2 | [![npm version](https://badge.fury.io/js/angular2-gantt.svg)](https://www.npmjs.com/package/angular2-gantt) 3 | [![Build Status](https://travis-ci.org/dalestone/angular2-gantt.svg?branch=master)](https://travis-ci.org/dalestone/angular2-gantt) 4 | 5 | This is the home for the Angular 2 gantt component. This project was generated with [angular-cli](https://github.com/angular/angular-cli) version 1.0.0-beta.18. 6 | 7 | The aim of this project is to make a material design angular 2 gantt component. 8 | 9 | ### Project status 10 | Angular 2 gantt is currently in alpha and under active development. 11 | During alpha, breaking API and behaviour changes will be occuring regularly. 12 | 13 | ### Getting Started 14 | If you want to view the and modify the source directly do the following: 15 | 16 | 1. git clone https://github.com/dalestone/angular2-gantt.git 17 | 2. npm install -g angular-cli 18 | 2. npm install 19 | 3. npm start 20 | 21 | ### Install Angular 2 Gantt (Webpack only) 22 | `npm install ng2-gantt --save` 23 | 24 | Import the `ng2-gantt` NgModule into your app module 25 | 26 | ``` 27 | import { GanttModule } from 'ng2-gantt'; 28 | // other imports 29 | @NgModule({ 30 | imports: [ GanttModule ] 31 | }) 32 | export class AppModule { } 33 | ``` 34 | 35 | ## Demo 36 | ![Demo](./docs/images/demo.gif) 37 | 38 | ## Browser support 39 | Angular 2 Gantt supports the most recent versions of major browsers: Chrome (including Android), Firefox, Safari (including iOS), and IE11 / Edge. 40 | -------------------------------------------------------------------------------- /angular-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "project": { 3 | "version": "1.0.0-alpha.0", 4 | "name": "angular2-gantt" 5 | }, 6 | "apps": [ 7 | { 8 | "root": "src/demo-app", 9 | "outDir": "dist", 10 | "assets": [ 11 | 12 | ], 13 | "index": "index.html", 14 | "main": "main.ts", 15 | "test": "test.ts", 16 | "tsconfig": "tsconfig.json", 17 | "prefix": "app", 18 | "mobile": false, 19 | "styles": [ 20 | "styles.css" 21 | ], 22 | "scripts": [], 23 | "environments": { 24 | "source": "environments/environment.ts", 25 | "dev": "environments/environment.ts", 26 | "prod": "environments/environment.prod.ts" 27 | } 28 | } 29 | ], 30 | "addons": [], 31 | "packages": [], 32 | "e2e": { 33 | "protractor": { 34 | "config": "./protractor.conf.js" 35 | } 36 | }, 37 | "test": { 38 | "karma": { 39 | "config": "./karma.conf.js" 40 | } 41 | }, 42 | "defaults": { 43 | "styleExt": "css", 44 | "prefixInterfaces": false, 45 | "inline": { 46 | "style": false, 47 | "template": false 48 | }, 49 | "spec": { 50 | "class": false, 51 | "component": true, 52 | "directive": true, 53 | "module": false, 54 | "pipe": true, 55 | "service": true 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /docs/images/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dalestone/angular2-gantt/91d3522f072ba09771e912a193ce75e219cb04cb/docs/images/demo.gif -------------------------------------------------------------------------------- /e2e/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Angular2GanttFinalPage } from './app.po'; 2 | 3 | describe('angular2-gantt-final App', function() { 4 | let page: Angular2GanttFinalPage; 5 | 6 | beforeEach(() => { 7 | page = new Angular2GanttFinalPage(); 8 | }); 9 | 10 | it('should display message saying app works', () => { 11 | page.navigateTo(); 12 | expect(page.getParagraphText()).toEqual('app works!'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /e2e/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, element, by } from 'protractor'; 2 | 3 | export class Angular2GanttFinalPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getParagraphText() { 9 | return element(by.css('app-root h1')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "declaration": false, 5 | "emitDecoratorMetadata": true, 6 | "experimentalDecorators": true, 7 | "module": "commonjs", 8 | "moduleResolution": "node", 9 | "outDir": "../dist/out-tsc-e2e", 10 | "sourceMap": true, 11 | "target": "es5", 12 | "typeRoots": [ 13 | "../node_modules/@types" 14 | ] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular2-gantt", 3 | "version": "1.0.0-alpha.1", 4 | "description": "Gantt component for Angular 2", 5 | "bugs": "https://github.com/dalestone/angular2-gantt/issues", 6 | "license": "MIT", 7 | "angular-cli": {}, 8 | "scripts": { 9 | "start": "ng serve", 10 | "lint": "tslint \"src/**/*.ts\"", 11 | "test": "ng test", 12 | "pree2e": "webdriver-manager update", 13 | "e2e": "protractor" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/dalestone/angular2-gantt.git" 18 | }, 19 | "private": true, 20 | "dependencies": { 21 | "@angular/common": "^2.4.5", 22 | "@angular/compiler": "^2.4.5", 23 | "@angular/core": "^2.4.5", 24 | "@angular/forms": "^2.4.5", 25 | "@angular/http": "^2.4.5", 26 | "@angular/platform-browser": "^2.4.5", 27 | "@angular/platform-browser-dynamic": "^2.4.5", 28 | "@angular/router": "^3.4.5", 29 | "angular2-tree-component": "^2.7.0", 30 | "core-js": "^2.4.1", 31 | "ng2-gantt": "1.0.0-alpha.1", 32 | "rxjs": "5.0.0-beta.12", 33 | "ts-helpers": "^1.1.1", 34 | "zone.js": "^0.6.23" 35 | }, 36 | "devDependencies": { 37 | "@types/jasmine": "^2.2.30", 38 | "@types/node": "^6.0.42", 39 | "angular-cli": "1.0.0-beta.18", 40 | "codelyzer": "1.0.0-beta.1", 41 | "jasmine-core": "2.4.1", 42 | "jasmine-spec-reporter": "2.5.0", 43 | "karma": "1.2.0", 44 | "karma-chrome-launcher": "^2.0.0", 45 | "karma-cli": "^1.0.1", 46 | "karma-jasmine": "^1.0.2", 47 | "karma-remap-istanbul": "^0.2.1", 48 | "protractor": "4.0.9", 49 | "ts-node": "1.2.1", 50 | "tslint": "3.13.0", 51 | "typescript": "~2.0.3" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/demo-app/demo-app.component.html: -------------------------------------------------------------------------------- 1 |

Angular 2 Gantt Demo

2 | 3 | 4 |
5 |

Project

6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 26 | 27 | 28 | 29 |
IdNameTree PathPercent CompleteStart DateEnd Date
25 |
30 | 31 |

Playground

32 | Task Status: 33 | 39 | 40 | 41 | 42 | 43 |

Output

44 |

{{ project | json }}

45 | 46 |
47 | -------------------------------------------------------------------------------- /src/demo-app/demo-app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { Project, IGanttOptions, Zooming, Task } from '../lib'; 3 | 4 | @Component({ 5 | selector: 'demo-app', 6 | templateUrl: './demo-app.component.html', 7 | }) 8 | export class DemoAppComponent { 9 | 10 | // Default options 11 | options: IGanttOptions = { 12 | scale: { 13 | start: new Date(2017, 0, 1), 14 | end: new Date(2017, 1, 1) 15 | }, 16 | zooming: Zooming[Zooming.days] 17 | }; 18 | 19 | project: Project = { 20 | 'id': 'dd10f0b6-b8a4-4b2d-a7df-b2c3d63b4a01', 21 | 'name': 'Angular2 Gantt', 22 | 'startDate': new Date("2017-02-27T08:32:09.6972999Z"), 23 | 'tasks': [ 24 | { 25 | 'id': 'ea2a8d86-1d4b-4807-844d-d5417fcf618d', 26 | 'treePath': 'parent 1', 27 | 'parentId': 'ea2a8d86-1d4b-4807-844d-d5417fcf618d', 28 | 'name': 'parent 1', 29 | 'resource': 'res1', 30 | 'start': new Date('2017-01-01T00:00:00.0Z'), 31 | 'end': new Date('2017-01-03T00:00:00.0Z'), 32 | 'percentComplete': 100, 33 | 'status': 'Completed' 34 | }, 35 | { 36 | 'id': 'dd755f20-360a-451f-b200-b83b89a35ad1', 37 | 'treePath': 'Cras sollicitudin egestas velit sit amet aliquam', 38 | 'parentId': 'dd755f20-360a-451f-b200-b83b89a35ad1', 39 | 'name': 'Cras sollicitudin egestas velit sit amet aliquam', 40 | 'resource': 'res2', 41 | 'start': new Date('2017-01-05T00:00:00.0Z'), 42 | 'end': new Date('2017-01-06T00:00:00.0Z'), 43 | 'percentComplete': 0 44 | }, 45 | { 46 | 'id': 'j1b997ef-bb89-4ca2-b134-62a08a19aef6', 47 | 'treePath': 'Donec ac augue est', 48 | 'parentId': 'j1b997ef-bb89-4ca2-b134-62a08a19aef6', 49 | 'name': 'Donec ac augue est', 50 | 'resource': 'res2', 51 | 'start': new Date('2017-01-06T00:00:00.0Z'), 52 | 'end': new Date('2017-01-07T00:00:00.0Z'), 53 | 'percentComplete': 0 54 | }, 55 | { 56 | 'id': 'ub12f674-d5cb-408f-a941-ec76af2ec47e', 57 | 'treePath': 'Lorem ipsum dolor sit amet', 58 | 'parentId': 'ub12f674-d5cb-408f-a941-ec76af2ec47e', 59 | 'name': 'Lorem ipsum dolor sit amet', 60 | 'resource': 'res1', 61 | 'start': new Date('2017-01-07T00:00:00.0Z'), 62 | 'end': new Date('2017-01-22T00:00:00.0Z'), 63 | 'percentComplete': 0, 64 | 'status': 'Error' 65 | }, 66 | { 67 | 'id': 'xafa430b-d4da-4d7d-90ed-69056a042d7a', 68 | 'treePath': 'Praesent molestie lobortis mi non tempor', 69 | 'parentId': 'xafa430b-d4da-4d7d-90ed-69056a042d7a', 70 | 'name': 'Praesent molestie lobortis mi non tempor', 71 | 'resource': 'res1', 72 | 'start': new Date('2017-01-22T00:00:00.0Z'), 73 | 'end': new Date('2017-01-23T00:00:00.0Z') 74 | }, 75 | { 76 | 'id': 'mc9d8d41-1995-4b38-9256-bcc0da171146', 77 | 'treePath': 'Cras sollicitudin egestas velit sit amet aliquam', 78 | 'parentId': 'mc9d8d41-1995-4b38-9256-bcc0da171146', 79 | 'name': 'Cras sollicitudin egestas velit sit amet aliquam', 80 | 'resource': 'res2', 81 | 'start': new Date('2017-01-23T00:00:00.0Z'), 82 | 'end': new Date('2017-01-24T00:00:00.0Z') 83 | }, 84 | { 85 | 'id': 'b5c071a5-430c-4d61-acf4-799cbdf61c49', 86 | 'treePath': 'Donec ac augue est', 87 | 'parentId': 'b5c071a5-430c-4d61-acf4-799cbdf61c49', 88 | 'name': 'Donec ac augue est', 89 | 'resource': 'res2', 90 | 'start': new Date('2017-01-24T00:00:00.0Z'), 91 | 'end': new Date('2017-01-24T00:34:00.0Z') 92 | }, 93 | { 94 | 'id': 't9ba762e-bde7-47bf-b628-75f99fdd5bef', 95 | 'treePath': 'Lorem Ipsum', 96 | 'parentId': 't9ba762e-bde7-47bf-b628-75f99fdd5bef', 97 | 'name': 'Lorem Ipsum', 98 | 'resource': 'res2', 99 | 'start': new Date('2017-01-24T00:00:00.0Z'), 100 | 'end': new Date('2017-01-24T00:00:36.0Z') 101 | } 102 | ] 103 | }; 104 | 105 | 106 | 107 | constructor() { 108 | 109 | } 110 | 111 | groupData(array: any[], f: any): any[] { 112 | var groups = {}; 113 | array.forEach((o: any) => { 114 | var group = JSON.stringify(f(o)); 115 | 116 | groups[group] = groups[group] || []; 117 | groups[group].push(o); 118 | }); 119 | return Object.keys(groups).map((group: any) => { 120 | return groups[group]; 121 | }); 122 | } 123 | 124 | createTask(element: any) { 125 | var selectedStatus = element.options[element.selectedIndex].value; 126 | 127 | var parentTask = { 128 | 'id': 'parent_task_' + Math.random(), 129 | 'parentId': 'parent_task', 130 | 'treePath': 'parent_task', 131 | 'name': 'parent_task', 132 | 'percentComplete': 0, 133 | 'start': new Date('2017-01-01T03:30:00.0Z'), 134 | 'end': new Date('2017-01-01T12:45:00.0Z'), 135 | 'status': selectedStatus 136 | } 137 | this.project.tasks.push(parentTask); 138 | 139 | var childTask = { 140 | 'id': 'child_task_' + Math.random(), 141 | 'parentId': 'ea2a8d86-1d4b-4807-844d-d5417fcf618d', 142 | 'treePath': 'parent 1/child3', 143 | 'name': 'child3', 144 | 'percentComplete': 0, 145 | 'start': new Date('2017-01-01T03:30:00.0Z'), 146 | 'end': new Date('2017-01-01T12:45:00.0Z'), 147 | 'status': selectedStatus 148 | } 149 | this.project.tasks.push(childTask); 150 | 151 | } 152 | 153 | updateTasks() { 154 | for (var i = 0; i < this.project.tasks.length; i++) { 155 | let task = this.project.tasks[i]; 156 | 157 | let progress = setInterval(function () { 158 | if (task.percentComplete === 100) { 159 | task.status = "Completed"; 160 | clearInterval(progress); 161 | } else { 162 | if (task.percentComplete === 25) { 163 | task.status = "Warning"; 164 | } else if (task.percentComplete === 50) { 165 | task.status = "Error"; 166 | } else if (task.percentComplete === 75) { 167 | task.status = "Information"; 168 | } 169 | 170 | task.percentComplete += 1; 171 | } 172 | }, 200); 173 | } 174 | } 175 | 176 | loadBigDataSet() { 177 | var tasks = []; 178 | 179 | for (var i = 11; i < 1000; i++) { 180 | var task = { 181 | id: `parent${i}`, 182 | name: 'task testing', 183 | percentComplete: 0, 184 | start: new Date(), 185 | end: new Date(), 186 | status: '' 187 | } 188 | 189 | tasks.push(task); 190 | } 191 | 192 | this.project.tasks.push(...tasks); 193 | } 194 | 195 | gridRowClicked(event) { 196 | console.log(event); 197 | } 198 | } -------------------------------------------------------------------------------- /src/demo-app/demo-app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { FormsModule } from '@angular/forms'; 3 | import { BrowserModule } from '@angular/platform-browser'; 4 | 5 | import { DemoAppComponent } from './demo-app.component'; 6 | //import { GanttModule } from 'ng2-gantt'; 7 | import { GanttModule } from '../lib' 8 | 9 | // import { TreeModule } from 'angular2-tree-component'; 10 | 11 | @NgModule({ 12 | imports: [ 13 | BrowserModule, 14 | FormsModule, 15 | GanttModule, 16 | // TreeModule 17 | ], 18 | exports: [], 19 | declarations: [ 20 | DemoAppComponent, 21 | ], 22 | providers: [], 23 | bootstrap: [DemoAppComponent] 24 | }) 25 | export class DemoAppModule { } 26 | -------------------------------------------------------------------------------- /src/demo-app/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/demo-app/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // The file contents for the current environment will overwrite these during build. 2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do 3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead. 4 | // The list of which env maps to which file can be found in `angular-cli.json`. 5 | 6 | export const environment = { 7 | production: false 8 | }; 9 | -------------------------------------------------------------------------------- /src/demo-app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Angular 2 Gantt 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | Loading... 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/demo-app/index.ts: -------------------------------------------------------------------------------- 1 | export * from './demo-app.component'; 2 | export * from './demo-app.module'; 3 | -------------------------------------------------------------------------------- /src/demo-app/main.ts: -------------------------------------------------------------------------------- 1 | import './polyfills.ts'; 2 | 3 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 4 | import { enableProdMode } from '@angular/core'; 5 | import { environment } from './environments/environment'; 6 | import { DemoAppModule } from './demo-app.module'; 7 | 8 | if (environment.production) { 9 | enableProdMode(); 10 | } 11 | 12 | platformBrowserDynamic().bootstrapModule(DemoAppModule); 13 | -------------------------------------------------------------------------------- /src/demo-app/polyfills.ts: -------------------------------------------------------------------------------- 1 | // This file includes polyfills needed by Angular 2 and is loaded before 2 | // the app. You can add your own extra polyfills to this file. 3 | import 'core-js/es6/symbol'; 4 | import 'core-js/es6/object'; 5 | import 'core-js/es6/function'; 6 | import 'core-js/es6/parse-int'; 7 | import 'core-js/es6/parse-float'; 8 | import 'core-js/es6/number'; 9 | import 'core-js/es6/math'; 10 | import 'core-js/es6/string'; 11 | import 'core-js/es6/date'; 12 | import 'core-js/es6/array'; 13 | import 'core-js/es6/regexp'; 14 | import 'core-js/es6/map'; 15 | import 'core-js/es6/set'; 16 | import 'core-js/es6/reflect'; 17 | 18 | import 'core-js/es7/reflect'; 19 | import 'zone.js/dist/zone'; 20 | -------------------------------------------------------------------------------- /src/demo-app/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /src/demo-app/test.ts: -------------------------------------------------------------------------------- 1 | import './polyfills.ts'; 2 | 3 | import 'zone.js/dist/long-stack-trace-zone'; 4 | import 'zone.js/dist/proxy.js'; 5 | import 'zone.js/dist/sync-test'; 6 | import 'zone.js/dist/jasmine-patch'; 7 | import 'zone.js/dist/async-test'; 8 | import 'zone.js/dist/fake-async-test'; 9 | 10 | // Unfortunately there's no typing for the `__karma__` variable. Just declare it as any. 11 | declare var __karma__: any; 12 | declare var require: any; 13 | 14 | // Prevent Karma from running prematurely. 15 | __karma__.loaded = function () {}; 16 | 17 | 18 | Promise.all([ 19 | System.import('@angular/core/testing'), 20 | System.import('@angular/platform-browser-dynamic/testing') 21 | ]) 22 | // First, initialize the Angular testing environment. 23 | .then(([testing, testingBrowser]) => { 24 | testing.getTestBed().initTestEnvironment( 25 | testingBrowser.BrowserDynamicTestingModule, 26 | testingBrowser.platformBrowserDynamicTesting() 27 | ); 28 | }) 29 | // Then we find all the tests. 30 | .then(() => require.context('./', true, /\.spec\.ts/)) 31 | // And load the modules. 32 | .then(context => context.keys().map(context)) 33 | // Finally, start Karma to run the tests. 34 | .then(__karma__.start, __karma__.error); 35 | -------------------------------------------------------------------------------- /src/demo-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": false, 4 | "emitDecoratorMetadata": true, 5 | "experimentalDecorators": true, 6 | "lib": ["es6", "dom"], 7 | "mapRoot": "./", 8 | "module": "es6", 9 | "moduleResolution": "node", 10 | "outDir": "../../dist/out-tsc", 11 | "sourceMap": true, 12 | "target": "es5", 13 | "typeRoots": [ 14 | "../../node_modules/@types" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/demo-app/typings.d.ts: -------------------------------------------------------------------------------- 1 | // Typings reference file, you can add your own global typings here 2 | // https://www.typescriptlang.org/docs/handbook/writing-declaration-files.html 3 | 4 | declare var System: any; 5 | -------------------------------------------------------------------------------- /src/lib/activity/background/activity-background.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input, ViewChild, ElementRef } from '@angular/core'; 2 | import { GanttService } from '../../shared/services/gantt.service'; 3 | import { Zooming } from '../../shared/interfaces'; 4 | 5 | @Component({ 6 | selector: 'activity-background', 7 | template: ` 8 |
9 |
10 |
11 |
12 |
13 | `, 14 | styles: [` 15 | .gantt_activity_bg { 16 | overflow: hidden; 17 | } 18 | 19 | .gantt_activity_row { 20 | border-bottom: 1px solid #ebebeb; 21 | background-color: #fff; 22 | box-sizing: border-box; 23 | } 24 | 25 | .gantt_activity_cell { 26 | display: inline-block; 27 | height: 100%; 28 | border-right: 1px solid #ebebeb; 29 | } 30 | 31 | .weekend { 32 | background-color:whitesmoke; 33 | } 34 | `] 35 | }) 36 | export class GanttActivityBackgroundComponent implements OnInit { 37 | @Input() tasks: any; 38 | @Input() timeScale:any; 39 | @Input() zoom: any; 40 | @Input() zoomLevel: string; 41 | 42 | @ViewChild('bg') bg: ElementRef; 43 | 44 | private rows: any[] = []; 45 | private cells: any[] = []; 46 | 47 | constructor(private ganttService: GanttService) { } 48 | 49 | ngOnInit() { 50 | this.drawGrid(); 51 | 52 | this.zoom.subscribe((zoomLevel: string) => { 53 | this.zoomLevel = zoomLevel; 54 | this.drawGrid(); 55 | }); 56 | } 57 | 58 | isDayWeekend(date: Date): boolean { 59 | return this.ganttService.isDayWeekend(date); 60 | } 61 | 62 | private setRowStyle() { 63 | return { 64 | 'height': this.ganttService.rowHeight + 'px' 65 | }; 66 | } 67 | 68 | private setCellStyle() { 69 | var width = this.ganttService.cellWidth; 70 | 71 | if (this.zoomLevel === Zooming[Zooming.hours]) { 72 | width = this.ganttService.hourCellWidth; 73 | } 74 | 75 | return { 76 | 'width': width + 'px' 77 | }; 78 | } 79 | 80 | private drawGrid(): void { 81 | if (this.zoomLevel === Zooming[Zooming.hours]) { 82 | this.cells = []; 83 | 84 | this.timeScale.forEach((date: any) => { 85 | for (var i = 0; i <= 23; i++) { 86 | this.cells.push(date); 87 | } 88 | }); 89 | } else { 90 | this.cells = this.timeScale; 91 | } 92 | } 93 | } 94 | 95 | 96 | //TODO(dale): replace with either svg or canvas 97 | // exceeding the maximum length/width/area on most browsers renders the canvas 98 | // unusable (it will ignore any draw commands, even in the usable area) 99 | // private drawGrid2(): void { 100 | // this.bg.nativeElement.innerHTML = ''; 101 | // //grid width and height 102 | // var bw = 1384; // maximum width 16384 CHROME 103 | // var bh = 300; //this.project.tasks.length * this.ganttService.rowHeight; 104 | // var rowHeight = this.ganttService.rowHeight; 105 | // var cellWidth = 0; 106 | 107 | // if (this.zoomLevel === Zooming[Zooming.hours]) { 108 | // cellWidth = this.ganttService.hourCellWidth; 109 | // } else { 110 | // cellWidth = this.ganttService.cellWidth; 111 | // } 112 | 113 | // var canvas = document.createElement('canvas'); 114 | // canvas.setAttribute("width", bw.toString()); 115 | // canvas.setAttribute("height", bh.toString()); 116 | // var context = canvas.getContext("2d"); 117 | 118 | // var lineSpacer = 0; 119 | // // vertical lines 120 | // for (var x = 0; x <= bw; x += cellWidth) { 121 | // lineSpacer += cellWidth / cellWidth; 122 | 123 | // context.moveTo(x + lineSpacer - 1.5, 0); 124 | // context.lineTo(x + lineSpacer - 1.5, bh); 125 | // } 126 | 127 | // // horizontal lines 128 | // for (var x = 0; x <= bh; x += rowHeight) { 129 | // context.moveTo(0, x); 130 | // context.lineTo(bw , x); 131 | // } 132 | 133 | // context.strokeStyle = "#e0e0e0"; 134 | // context.stroke(); 135 | 136 | // this.bg.nativeElement.append(canvas); 137 | // } 138 | 139 | 140 | // var canvas = $('').attr({width: cw, height: ch}).appendTo('body'); 141 | 142 | // var context = canvas.get(0).getContext("2d"); 143 | 144 | // function drawBoard(){ 145 | // for (var x = 0; x <= bw; x += 40) { 146 | // context.moveTo(0.5 + x + p, p); 147 | // context.lineTo(0.5 + x + p, bh + p); 148 | // } 149 | 150 | 151 | // for (var x = 0; x <= bh; x += 40) { 152 | // context.moveTo(p, 0.5 + x + p); 153 | // context.lineTo(bw + p, 0.5 + x + p); 154 | // } 155 | 156 | // context.strokeStyle = "black"; 157 | // context.stroke(); 158 | // } 159 | 160 | // drawBoard(); -------------------------------------------------------------------------------- /src/lib/activity/bars/activity-bars.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input, ElementRef } from '@angular/core'; 2 | import { GanttService } from '../../shared/services/gantt.service'; 3 | import { Zooming } from '../../shared/interfaces'; 4 | 5 | @Component({ 6 | selector: 'activity-bars', 7 | template: ` 8 |
9 |
10 |
11 |
12 |
13 | 16 | 19 |
20 |
21 | `, 22 | styles: [` 23 | .gantt_activity_line { 24 | /*border-radius: 2px;*/ 25 | position: absolute; 26 | box-sizing: border-box; 27 | background-color: rgb(18,195,244); 28 | border: 1px solid #2196F3; 29 | -webkit-user-select: none; 30 | } 31 | 32 | .gantt_activity_line:hover { 33 | /*cursor: move;*/ 34 | } 35 | 36 | .gantt_activity_progress { 37 | text-align: center; 38 | z-index: 0; 39 | background: #2196F3; 40 | position: absolute; 41 | min-height: 18px; 42 | display: block; 43 | height: 18px; 44 | } 45 | 46 | .gantt_activity_progress_drag { 47 | height: 8px; 48 | width: 8px; 49 | bottom: -4px; 50 | margin-left: 4px; 51 | background-position: bottom; 52 | background-image: ""; 53 | background-repeat: no-repeat; 54 | z-index: 2; 55 | } 56 | 57 | .gantt_activity_content { 58 | font-size: 12px; 59 | color: #fff; 60 | width: 100%; 61 | top: 0; 62 | position: absolute; 63 | white-space: nowrap; 64 | text-align: center; 65 | line-height: inherit; 66 | overflow: hidden; 67 | height: 100%; 68 | } 69 | 70 | .gantt_activity_link_control { 71 | position: absolute; 72 | width: 13px; 73 | top: 0; 74 | } 75 | 76 | .gantt_activity_right { 77 | right: 0; 78 | } 79 | 80 | .gantt_activity_left { 81 | left: 0; 82 | } 83 | 84 | .gantt_activity_right:hover { 85 | cursor:w-resize; 86 | } 87 | 88 | .gantt_activity_left:hover { 89 | cursor:w-resize; 90 | } 91 | `], 92 | providers: [ 93 | GanttService 94 | ] 95 | }) 96 | export class GanttActivityBarsComponent implements OnInit { 97 | @Input() timeScale: any; 98 | @Input() dimensions: any; 99 | @Input() tasks: any; 100 | @Input() zoom: any; 101 | @Input() zoomLevel: any; 102 | 103 | private containerHeight: number = 0; 104 | private containerWidth: number = 0; 105 | 106 | constructor(private ganttService: GanttService) { } 107 | 108 | ngOnInit() { 109 | this.containerHeight = this.dimensions.height; 110 | this.containerWidth = this.dimensions.width; 111 | 112 | this.zoom.subscribe((zoomLevel: string) => { 113 | this.zoomLevel = zoomLevel; 114 | });; 115 | } 116 | 117 | //TODO(dale): the ability to move bars needs reviewing and there are a few quirks 118 | expandLeft($event: any, bar: any) { 119 | $event.stopPropagation(); 120 | 121 | let ganttService = this.ganttService; 122 | let startX = $event.clientX; 123 | let startBarWidth = bar.style.width; 124 | let startBarLeft = bar.style.left; 125 | 126 | function doDrag(e: any) { 127 | let cellWidth = ganttService.cellWidth; 128 | let barWidth = startBarWidth - e.clientX + startX; 129 | let days = Math.round(barWidth / cellWidth); 130 | 131 | bar.style.width = days * cellWidth + days; 132 | bar.style.left = (startBarLeft - (days * cellWidth) - days); 133 | } 134 | 135 | this.addMouseEventListeners(doDrag); 136 | 137 | return false; 138 | } 139 | 140 | expandRight($event: any, bar: any) { 141 | $event.stopPropagation(); 142 | 143 | let ganttService = this.ganttService; 144 | let startX = $event.clientX; 145 | let startBarWidth = bar.style.width; 146 | let startBarEndDate = bar.task.end; 147 | let startBarLeft = bar.style.left; 148 | 149 | function doDrag(e: any) { 150 | let cellWidth = ganttService.cellWidth; 151 | let barWidth = startBarWidth + e.clientX - startX; 152 | let days = Math.round(barWidth / cellWidth); 153 | 154 | if (barWidth < cellWidth) { 155 | barWidth = cellWidth; 156 | days = Math.round(barWidth / cellWidth); 157 | } 158 | bar.style.width = ((days * cellWidth) + days); // rounds to the nearest cell 159 | } 160 | 161 | this.addMouseEventListeners(doDrag); 162 | 163 | return false; 164 | } 165 | 166 | move($event: any, bar: any) { 167 | $event.stopPropagation(); 168 | 169 | let ganttService = this.ganttService; 170 | let startX = $event.clientX; 171 | let startBarLeft = bar.style.left; 172 | 173 | function doDrag(e: any) { 174 | let cellWidth = ganttService.cellWidth; 175 | let barLeft = startBarLeft + e.clientX - startX; 176 | let days = Math.round(barLeft / cellWidth); 177 | 178 | // TODO: determine how many days the bar can be moved 179 | // if (days < maxDays) { 180 | bar.style.left = ((days * cellWidth) + days); // rounded to nearest cell 181 | 182 | // keep bar in bounds of grid 183 | if (barLeft < 0) { 184 | bar.style.left = 0; 185 | } 186 | // } 187 | // TODO: it needs to take into account the max number of days. 188 | // TODO: it needs to take into account the current days. 189 | // TODO: it needs to take into account the right boundary. 190 | } 191 | 192 | this.addMouseEventListeners(doDrag); 193 | 194 | return false; 195 | } 196 | 197 | private drawBar(task: any, index: number) { 198 | let style = {}; 199 | 200 | if (this.zoomLevel === Zooming[Zooming.hours]) { 201 | style = this.ganttService.calculateBar(task, index, this.timeScale, true); 202 | } else { 203 | style = this.ganttService.calculateBar(task, index, this.timeScale); 204 | } 205 | return style; 206 | } 207 | 208 | private drawProgress(task: any, bar: any):any { 209 | var barStyle = this.ganttService.getBarProgressStyle(task.status); 210 | var width = this.ganttService.calculateBarProgress(this.ganttService.getComputedStyle(bar, 'width'), task.percentComplete); 211 | 212 | return { 213 | 'width': width, 214 | 'background-color': barStyle["background-color"], 215 | }; 216 | } 217 | 218 | private addMouseEventListeners(dragFn: any) { 219 | 220 | function stopFn() { 221 | document.documentElement.removeEventListener('mousemove', dragFn, false); 222 | document.documentElement.removeEventListener('mouseup', stopFn, false); 223 | document.documentElement.removeEventListener('mouseleave', stopFn, false); 224 | } 225 | 226 | document.documentElement.addEventListener('mousemove', dragFn, false); 227 | document.documentElement.addEventListener('mouseup', stopFn, false); 228 | document.documentElement.addEventListener('mouseleave', stopFn, false); 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /src/lib/activity/gantt-activity.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input, Output, EventEmitter, ElementRef, AfterViewInit, ViewChild, ChangeDetectionStrategy, OnChanges, DoCheck } from '@angular/core'; 2 | 3 | import { GanttService } from '../shared/services/gantt.service'; 4 | import { GanttConfig } from '../shared/services/gantt-config.service'; 5 | import { IGanttOptions, Zooming } from '../shared/interfaces'; 6 | 7 | @Component({ 8 | selector: 'gantt-activity', 9 | template: ` 10 |
11 |
12 | 15 | 18 | 19 |
20 |
21 |
22 |
23 |
24 | 25 |
26 |
27 | 28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
{{data.name}}
37 |
38 |
39 | 40 |
{{ data.percentComplete }}
41 |
42 |
43 |
{{ ganttService.calculateDuration(data) }}
44 |
45 |
46 |
47 |
48 | 49 |
50 | 51 | 52 |
53 |
54 |
55 |
56 |
57 | `, 58 | styles: [` 59 | .gantt_activity { 60 | /*overflow-x: hidden;*/ 61 | overflow-x: auto; 62 | height: 250px; 63 | overflow-y: hidden; 64 | overflow-x: scroll; 65 | display: inline-block; 66 | vertical-align: top; 67 | position:relative; 68 | } 69 | 70 | .gantt_activity_area { 71 | position: relative; 72 | overflow-x: hidden; 73 | overflow-y: hidden; 74 | -webkit-user-select: none; 75 | } 76 | 77 | .gantt_vertical_scroll { 78 | background-color: transparent; 79 | overflow-x: hidden; 80 | overflow-y: scroll; 81 | position: absolute; 82 | right: 0; 83 | display: block; 84 | height: 283px; 85 | width: 18px; 86 | top: 70px; 87 | } 88 | 89 | .grid { 90 | overflow-x: hidden; 91 | overflow-y: hidden; 92 | display: inline-block; 93 | vertical-align: top; 94 | border-right: 1px solid #cecece; 95 | } 96 | 97 | .grid_scale { 98 | color: #6b6b6b; 99 | font-size: 12px; 100 | border-bottom: 1px solid #e0e0e0; 101 | background-color: whitesmoke; 102 | } 103 | 104 | .grid_head_cell { 105 | /*color: #a6a6a6;*/ 106 | border-top: none !important; 107 | border-right: none !important; 108 | line-height: inherit; 109 | box-sizing: border-box; 110 | display: inline-block; 111 | vertical-align: top; 112 | border-right: 1px solid #cecece; 113 | /*text-align: center;*/ 114 | position: relative; 115 | cursor: default; 116 | height: 100%; 117 | -moz-user-select: -moz-none; 118 | -webkit-user-select: none; 119 | overflow: hidden; 120 | } 121 | 122 | .grid_data { 123 | overflow:hidden; 124 | } 125 | 126 | .grid_row { 127 | box-sizing: border-box; 128 | border-bottom: 1px solid #e0e0e0; 129 | background-color: #fff; 130 | position: relative; 131 | -webkit-user-select: none; 132 | } 133 | 134 | .grid_row:hover { 135 | background-color: #eeeeee; 136 | } 137 | 138 | .grid_cell { 139 | border-right: none; 140 | color: #454545; 141 | display: inline-block; 142 | vertical-align: top; 143 | padding-left: 6px; 144 | padding-right: 6px; 145 | height: 100%; 146 | overflow: hidden; 147 | white-space: nowrap; 148 | font-size: 13px; 149 | box-sizing: border-box; 150 | } 151 | 152 | .actions_bar { 153 | /*border-top: 1px solid #cecece;*/ 154 | border-bottom: 1px solid #e0e0e0; 155 | clear: both; 156 | /*margin-top: 90px;*/ 157 | height: 28px; 158 | background: whitesmoke; 159 | color: #494949; 160 | font-family: Arial, sans-serif; 161 | font-size: 13px; 162 | padding-left: 15px; 163 | line-height: 25px; 164 | } 165 | 166 | .gantt_tree_content { 167 | padding-left:15px; 168 | } 169 | `], 170 | changeDetection: ChangeDetectionStrategy.Default 171 | }) 172 | export class GanttActivityComponent implements OnInit, DoCheck { 173 | @Input() project: any; 174 | @Input() options: any; 175 | @Output() onGridRowClick: EventEmitter = new EventEmitter(); 176 | 177 | private upTriangle: string = '▲' // BLACK UP-POINTING TRIANGLE 178 | private downTriangle: string = '▼'; // BLACK DOWN-POINTING TRIANGLE 179 | private zoom: EventEmitter = new EventEmitter(); 180 | private activityActions = { 181 | expanded: false, 182 | expandedIcon: this.downTriangle 183 | } 184 | 185 | private timeScale: any; 186 | 187 | private start: Date; 188 | private end: Date; 189 | private containerHeight: any; 190 | private containerWidth: any; 191 | 192 | private activityContainerSizes: any; 193 | private ganttActivityHeight: any; 194 | private ganttActivityWidth: any; 195 | private zoomLevel: string = Zooming[Zooming.hours]; 196 | 197 | private treeExpanded = false; 198 | 199 | private scale: any = { 200 | start: null, 201 | end: null 202 | }; 203 | 204 | private dimensions = { 205 | height: 0, 206 | width: 0 207 | }; 208 | 209 | private data: any[] = []; 210 | 211 | public gridColumns: any[] = [ 212 | { name: '', left: 0, width: 16 }, 213 | { name: 'Task', left: 20, width: 330 }, 214 | { name: '%', left: 8, width: 40 }, 215 | { name: 'Duration', left: 14, width: 140 } 216 | ]; 217 | 218 | constructor( 219 | public elem: ElementRef, 220 | private ganttService: GanttService) { 221 | } 222 | 223 | ngOnInit() { 224 | // Cache the project data and only work with that. Only show parent tasks by default 225 | this.ganttService.TASK_CACHE = this.project.tasks.slice(0).filter((item: any) => { 226 | return item.treePath.split('/').length === 1; 227 | }); 228 | this.ganttService.TIME_SCALE = this.ganttService.calculateScale(this.options.scale.start, this.options.scale.end); 229 | 230 | this.zoomLevel = this.options.zooming; 231 | this.start = this.options.scale.start; 232 | this.end = this.options.scale.end; 233 | this.containerWidth = this.calculateContainerWidth(); 234 | this.containerHeight = this.calculateContainerHeight(); 235 | this.activityContainerSizes = this.ganttService.calculateActivityContainerDimensions(); 236 | 237 | // important that these are called last as it relies on values calculated above. 238 | this.setScale(); 239 | this.setDimensions(); 240 | this.setSizes(); 241 | 242 | this.expand(); // default to expanded 243 | } 244 | 245 | /** Custom model check */ 246 | ngDoCheck() { 247 | // do a check to see whether any new tasks have been added. If the task is a child then push into array if tree expanded? 248 | var tasksAdded = this.ganttService.doTaskCheck(this.project.tasks, this.treeExpanded); 249 | 250 | // only force expand if tasks are added and tree is already expanded 251 | if (tasksAdded && this.activityActions.expanded) { 252 | this.expand(true); 253 | } 254 | } 255 | 256 | /** On vertical scroll set the scroll top of grid and activity */ 257 | onVerticalScroll(verticalScroll: any, ganttGrid: any, ganttActivityArea: any): void { 258 | this.ganttService.scrollTop(verticalScroll, ganttGrid, ganttActivityArea); 259 | } 260 | 261 | /** Removes or adds children for given parent tasks back into DOM by updating TASK_CACHE */ 262 | toggleChildren(rowElem: any, task: any) { 263 | try { 264 | let isParent: boolean = "true" === rowElem.getAttribute('data-isparent'); 265 | let parentId: string = rowElem.getAttribute('data-parentid').replace("_", ""); // remove id prefix 266 | let children: any = document.querySelectorAll('[data-parentid=' + rowElem.getAttribute('data-parentid') + '][data-isparent=false]'); 267 | 268 | // use the task cache to allow deleting of items without polluting the project.tasks array 269 | if (isParent) { 270 | // remove children from the DOM as we don't want them if we are collapsing the parent 271 | if (children.length > 0) { 272 | let childrenIds: any[] = this.ganttService.TASK_CACHE.filter((task: any) => { 273 | return task.parentId == parentId && task.treePath.split('/').length > 1; 274 | }).map((item: any) => { return item.id }); 275 | 276 | childrenIds.forEach((item: any) => { 277 | var removedIndex = this.ganttService.TASK_CACHE.map((item: any) => { return item.id }).indexOf(item); 278 | 279 | this.ganttService.TASK_CACHE.splice(removedIndex, 1); 280 | }); 281 | 282 | if (this.activityActions.expanded) { 283 | this.expand(true); 284 | } 285 | 286 | } else { 287 | // CHECK the project cache to see if this parent id has any children 288 | // and if so push them back into array so DOM is updated 289 | let childrenTasks: any[] = this.project.tasks.filter((task: any) => { 290 | return task.parentId === parentId && task.treePath.split('/').length > 1; 291 | }); 292 | 293 | childrenTasks.forEach((task: any) => { 294 | this.ganttService.TASK_CACHE.push(task); 295 | }); 296 | 297 | if (this.activityActions.expanded) { 298 | this.expand(true); 299 | } 300 | } 301 | } 302 | 303 | this.onGridRowClick.emit(task); 304 | 305 | } catch (err) { } 306 | } 307 | 308 | /** Removes or adds children tasks back into DOM by updating TASK_CACHE */ 309 | toggleAllChildren() { 310 | try { 311 | var children: any = document.querySelectorAll('[data-isparent=false]'); 312 | var childrenIds: string[] = Array.prototype.slice.call(children).map((item: any) => { 313 | return item.getAttribute('data-id').replace("_", ""); // remove id prefix 314 | }); 315 | 316 | // push all the children array items into cache 317 | if (this.treeExpanded) { 318 | if (children.length > 0) { 319 | let childrenIds: string[] = this.ganttService.TASK_CACHE.filter((task: any) => { 320 | return task.treePath.split('/').length > 1; 321 | }).map((item: any) => { return item.id }); 322 | 323 | childrenIds.forEach((item: any) => { 324 | var removedIndex = this.ganttService.TASK_CACHE.map((item: any) => { return item.id }).indexOf(item); 325 | this.ganttService.TASK_CACHE.splice(removedIndex, 1); 326 | }); 327 | } 328 | 329 | this.treeExpanded = false; 330 | 331 | if (this.activityActions.expanded) { 332 | this.expand(true); 333 | } 334 | } else { 335 | // get all children tasks in project input 336 | let childrenTasks: any[] = this.project.tasks.filter((task: any) => { 337 | return task.treePath.split('/').length > 1; 338 | }); 339 | 340 | if (children.length > 0) { 341 | // filter out these children as they already exist in task cache 342 | childrenTasks = childrenTasks.filter((task: any) => { 343 | return childrenIds.indexOf(task.id) === -1; 344 | }); 345 | } 346 | 347 | childrenTasks.forEach((task: any) => { 348 | this.ganttService.TASK_CACHE.push(task); 349 | }); 350 | 351 | this.treeExpanded = true; 352 | 353 | if (this.activityActions.expanded) { 354 | this.expand(true); 355 | } 356 | } 357 | } catch (err) { } 358 | } 359 | 360 | /** On resize of browser window dynamically adjust gantt activity height and width */ 361 | onResize(event: any): void { 362 | let activityContainerSizes = this.ganttService.calculateActivityContainerDimensions(); 363 | if (this.activityActions.expanded) { 364 | this.ganttActivityHeight = this.ganttService.TASK_CACHE.length * this.ganttService.rowHeight + this.ganttService.rowHeight * 3 + 'px'; 365 | } else { 366 | this.ganttActivityHeight = activityContainerSizes.height + 'px';; 367 | } 368 | 369 | this.ganttActivityWidth = activityContainerSizes.width; 370 | } 371 | 372 | setScale() { 373 | this.scale.start = this.start; 374 | this.scale.end = this.end; 375 | } 376 | 377 | setDimensions() { 378 | this.dimensions.height = this.containerHeight; 379 | this.dimensions.width = this.containerWidth; 380 | } 381 | 382 | setGridRowStyle(isParent: boolean): any { 383 | if (isParent) { 384 | return { 385 | 'height': this.ganttService.rowHeight + 'px', 386 | 'line-height': this.ganttService.rowHeight + 'px', 387 | 'font-weight': 'bold', 388 | 'cursor': 'pointer' 389 | }; 390 | } 391 | 392 | return { 393 | 'height': this.ganttService.rowHeight + 'px', 394 | 'line-height': this.ganttService.rowHeight + 'px' 395 | }; 396 | } 397 | 398 | /** Set the zoom level e.g hours, days */ 399 | zoomTasks(level: string) { 400 | this.zoomLevel = level; 401 | this.zoom.emit(this.zoomLevel); 402 | this.containerWidth = this.calculateContainerWidth(); 403 | this.setDimensions(); 404 | document.querySelector('.gantt_activity').scrollLeft = 0 // reset scroll left, replace with @ViewChild? 405 | } 406 | 407 | /** Expand the gantt grid and activity area height */ 408 | expand(force?: boolean): void { 409 | var verticalScroll = document.querySelector('.gantt_vertical_scroll'); 410 | var ganttActivityHeight: string = `${this.ganttService.TASK_CACHE.length * this.ganttService.rowHeight + this.ganttService.rowHeight * 3}px`; 411 | 412 | if (force && this.activityActions.expanded) { 413 | this.ganttActivityHeight = ganttActivityHeight; 414 | } else if (this.activityActions.expanded) { 415 | this.activityActions.expanded = false; 416 | this.activityActions.expandedIcon = this.downTriangle; 417 | this.ganttActivityHeight = '300px'; 418 | } else { 419 | verticalScroll.scrollTop = 0; 420 | 421 | this.activityActions.expanded = true; 422 | this.activityActions.expandedIcon = this.upTriangle; 423 | this.ganttActivityHeight = ganttActivityHeight; 424 | } 425 | } 426 | 427 | /** Get the status icon unicode string */ 428 | getStatusIcon(status: string, percentComplete: number): string { 429 | var checkMarkIcon: string = '✔'; 430 | var upBlackPointer: string = '▲'; 431 | var crossMarkIcon: string = '✘'; 432 | 433 | if (status === "Completed" || percentComplete === 100 && status !== "Error") { 434 | return checkMarkIcon; 435 | } else if (status === "Warning") { 436 | return upBlackPointer; 437 | } else if (status === "Error") { 438 | return crossMarkIcon; 439 | } 440 | return ''; 441 | } 442 | 443 | /** Get the status icon color */ 444 | getStatusIconColor(status: string, percentComplete: number): string { 445 | if (status === "Completed" || percentComplete === 100 && status !== "Error") { 446 | return 'green'; 447 | } else if (status === "Warning") { 448 | return 'orange'; 449 | } else if (status === "Error") { 450 | return 'red'; 451 | } 452 | return ''; 453 | } 454 | 455 | private setGridScaleStyle() { 456 | var height = this.ganttService.rowHeight; 457 | 458 | if (this.zoomLevel === Zooming[Zooming.hours]) { 459 | height *= 2; 460 | } 461 | 462 | return { 463 | 'height': height + 'px', 464 | 'line-height': height + 'px', 465 | 'width': this.ganttService.gridWidth + 'px' 466 | }; 467 | } 468 | 469 | private calculateContainerHeight(): number { 470 | return this.ganttService.TASK_CACHE.length * this.ganttService.rowHeight; 471 | } 472 | 473 | private calculateContainerWidth(): number { 474 | if (this.zoomLevel === Zooming[Zooming.hours]) { 475 | return this.ganttService.TIME_SCALE.length * this.ganttService.hourCellWidth * 24 + this.ganttService.hourCellWidth 476 | } else { 477 | return this.ganttService.TIME_SCALE.length * this.ganttService.cellWidth + this.ganttService.cellWidth; 478 | } 479 | } 480 | 481 | private setSizes(): void { 482 | this.ganttActivityHeight = this.activityContainerSizes.height + 'px'; 483 | this.ganttActivityWidth = this.activityContainerSizes.width; 484 | } 485 | 486 | } 487 | 488 | 489 | //** */ 490 | // @Component({ 491 | // selector: 'tree-builder', 492 | // template: '' 493 | // }) 494 | // export class TreeBuilder { 495 | // _nodes: any = [ 496 | // { 497 | // name: "testing", children: [ 498 | // { name: "testing2", children: [{ name: "testing4", children: [] }] } 499 | // ] 500 | // }, 501 | // { 502 | // name: "testing3", children: [] 503 | // } 504 | // ]; 505 | 506 | // @Input() nodes: any; 507 | // } 508 | 509 | // @Component({ 510 | // selector: 'tree-parent-repeater', 511 | // styles: [` 512 | // .grid_row { 513 | // box-sizing: border-box; 514 | // border-bottom: 1px solid #e0e0e0; 515 | // background-color: #fff; 516 | // position: relative; 517 | // -webkit-user-select: none; 518 | // cursor:pointer; 519 | // height:25px; 520 | // line-height:25px; 521 | // font-size:16px; 522 | // } 523 | 524 | // .grid_row:hover { 525 | // background-color: whitesmoke; 526 | // } 527 | // `], 528 | // template: ` 529 | //
530 | //
531 | //
{{ expanded !== true ? '▶' : '▼' }}
532 | //
{{ node.name }}
533 | //
{{ node.percentComplete }}
534 | //
0
535 | //
536 | // 537 | //
538 | // ` 539 | // }) 540 | // export class TreeParentRepeater { 541 | // @Input() node: any; 542 | // expanded: boolean = true; 543 | 544 | // toggle = () => { 545 | // if (this.expanded) { 546 | // this.expanded = false; 547 | // } else { 548 | // this.expanded = true; 549 | // } 550 | // } 551 | // } 552 | 553 | // @Component({ 554 | // selector: 'tree-children-repeater', 555 | // styles: [` 556 | // .grid_row { 557 | // box-sizing: border-box; 558 | // border-bottom: 1px solid #e0e0e0; 559 | // background-color: #fff; 560 | // position: relative; 561 | // -webkit-user-select: none; 562 | // cursor:pointer; 563 | // height:25px; 564 | // line-height:25px; 565 | // font-size:16px; 566 | // } 567 | 568 | // .grid_row:hover { 569 | // background-color: whitesmoke; 570 | // } 571 | // `], 572 | // template: ` 573 | //
574 | //
575 | //
576 | //
{{ expanded !== true ? '▶' : '▼' }}
577 | //
{{ child.name }}
578 | //
{{ child.percentComplete }}
579 | //
0
580 | //
581 | // 582 | //
583 | //
584 | // ` 585 | // }) 586 | // export class TreeChildrenRepeater { 587 | // @Input() root: any; 588 | // expanded: boolean = true; 589 | 590 | // toggle = () => { 591 | // if (this.expanded) { 592 | // this.expanded = false; 593 | // } else { 594 | // this.expanded = true; 595 | // } 596 | // } 597 | // } -------------------------------------------------------------------------------- /src/lib/activity/gantt-activity.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | import { GanttActivityComponent } from './gantt-activity.component'; 5 | import { GanttTimeScaleComponent } from './time-scale/gantt-time-scale.component'; 6 | import { GanttActivityBackgroundComponent } from './background/activity-background.component'; 7 | import { GanttActivityBarsComponent } from './bars/activity-bars.component'; 8 | 9 | @NgModule({ 10 | imports: [ 11 | CommonModule, 12 | ], 13 | exports: [ 14 | GanttActivityComponent, 15 | GanttTimeScaleComponent, 16 | GanttActivityBackgroundComponent, 17 | GanttActivityBarsComponent 18 | ], 19 | declarations: [ 20 | GanttActivityComponent, 21 | GanttTimeScaleComponent, 22 | GanttActivityBackgroundComponent, 23 | GanttActivityBarsComponent, 24 | ///TreeBuilder, TreeParentRepeater, TreeChildrenRepeater, 25 | ], 26 | providers: [], 27 | }) 28 | export class GanttActivityModule { } 29 | -------------------------------------------------------------------------------- /src/lib/activity/time-scale/gantt-time-scale.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input, EventEmitter, ViewChild } from '@angular/core'; 2 | import { GanttService } from '../../shared/services/gantt.service'; 3 | import { Zooming } from '../../shared/interfaces'; 4 | 5 | @Component({ 6 | selector: 'time-scale', 7 | template: ` 8 |
9 |
10 |
{{date | date: 'dd-MM-yyyy'}}
11 |
12 |
13 |
{{hour}}
14 |
15 |
`, 16 | styles: [` 17 | .weekend { 18 | background-color:whitesmoke; 19 | } 20 | 21 | .time_scale { 22 | font-size: 12px; 23 | border-bottom: 1px solid #cecece; 24 | background-color: #fff; 25 | } 26 | 27 | .time_scale_line { 28 | box-sizing: border-box; 29 | } 30 | 31 | .time_scale_line:first-child { 32 | border-top: none; 33 | } 34 | 35 | .time_scale_cell { 36 | display: inline-block; 37 | white-space: nowrap; 38 | overflow: hidden; 39 | border-right: 1px solid #cecece; 40 | text-align: center; 41 | height: 100%; 42 | }` 43 | ], 44 | providers: [ 45 | GanttService 46 | ] 47 | }) 48 | export class GanttTimeScaleComponent implements OnInit { 49 | @Input() timeScale: any; 50 | @Input() dimensions: any; 51 | @Input() zoom: any; 52 | @Input() zoomLevel: any; 53 | 54 | constructor(private ganttService: GanttService) { } 55 | 56 | ngOnInit() { 57 | this.zoom.subscribe((zoomLevel: string) => { 58 | this.zoomLevel = zoomLevel; 59 | });; 60 | } 61 | 62 | private setTimescaleStyle() { 63 | return { 64 | 'width': this.dimensions.width + 'px' 65 | }; 66 | } 67 | 68 | private setTimescaleLineStyle(borderTop: string) { 69 | return { 70 | 'height': this.ganttService.rowHeight + 'px', 71 | 'line-height': this.ganttService.rowHeight + 'px', 72 | 'position': 'relative', 73 | 'border-top': borderTop 74 | }; 75 | } 76 | 77 | private setTimescaleCellStyle() { 78 | var width = this.ganttService.cellWidth; 79 | var hoursInDay = 24; 80 | var hourSeperatorPixels = 23; // we don't include the first 81 | 82 | if(this.zoomLevel === Zooming[Zooming.hours]) { 83 | width = this.ganttService.hourCellWidth * hoursInDay + hourSeperatorPixels; 84 | } 85 | 86 | return { 87 | 'width': width + 'px' 88 | }; 89 | } 90 | 91 | private isDayWeekend(date: Date): boolean { 92 | return this.ganttService.isDayWeekend(date); 93 | } 94 | 95 | private getHours(): string[] { 96 | return this.ganttService.getHours(this.timeScale.length); 97 | } 98 | } -------------------------------------------------------------------------------- /src/lib/footer/gantt-footer.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'gantt-footer', 5 | template: ``, 6 | styles: [` 7 | .gantt-footer { 8 | background-color: whitesmoke; 9 | height: 36px; 10 | border-top: 1px solid #e0e0e0; 11 | } 12 | 13 | .gantt-footer-actions { 14 | float:right; 15 | } 16 | `] 17 | }) 18 | export class GanttFooterComponent { 19 | @Input() project: any; 20 | constructor() { } 21 | } -------------------------------------------------------------------------------- /src/lib/gantt.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input, EventEmitter, Output } from '@angular/core'; 2 | import { NgStyle } from '@angular/common'; 3 | import { GanttService } from './shared/services/gantt.service'; 4 | import { IGanttOptions, Project } from './shared/interfaces'; 5 | 6 | @Component({ 7 | selector: 'gantt', 8 | template: ` 9 |
10 |
11 | 12 | 13 | 14 |
15 |
16 | `, 17 | styles: [` 18 | .gantt_container { 19 | font-family: Arial; 20 | font-size: 13px; 21 | border: 1px solid #cecece; 22 | position: relative; 23 | white-space: nowrap; 24 | margin-top: 0px; 25 | } 26 | `], 27 | providers: [] 28 | }) 29 | export class GanttComponent implements OnInit { 30 | private _project: Project; 31 | private _options: IGanttOptions; 32 | 33 | //TODO(dale): this may be causing an issue in the tree builder? 34 | @Input() 35 | set project(project: any) { 36 | if (project) { 37 | this._project = project; 38 | } else { 39 | this.setDefaultProject(); 40 | } 41 | } 42 | get project() { return this._project }; 43 | 44 | @Input() 45 | set options(options: any) { 46 | if (options.scale) { 47 | this._options = options; 48 | } else { 49 | this.setDefaultOptions(); 50 | } 51 | } 52 | get options() { return this._options }; 53 | 54 | @Output() onGridRowClick: EventEmitter = new EventEmitter(); 55 | 56 | private ganttContainerWidth: number; 57 | 58 | constructor(private ganttService: GanttService) { } 59 | 60 | ngOnInit() { 61 | 62 | } 63 | 64 | setSizes(): void { 65 | this.ganttContainerWidth = this.ganttService.calculateContainerWidth(); 66 | } 67 | 68 | setDefaultOptions() { 69 | var scale = this.ganttService.calculateGridScale(this._project.tasks); 70 | 71 | this._options = { 72 | scale: scale 73 | } 74 | } 75 | 76 | setDefaultProject() { 77 | this._project = { 78 | id: '', 79 | name: '', 80 | startDate: null, 81 | tasks: [] 82 | } 83 | } 84 | 85 | gridRowClicked(task:any) { 86 | this.onGridRowClick.emit(task); 87 | } 88 | 89 | onResize($event: any): void { 90 | this.setSizes(); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/lib/gantt.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { FormsModule } from '@angular/forms'; 4 | 5 | import { GanttComponent } from './gantt.component'; 6 | import { GanttHeaderComponent } from './header/gantt-header.component'; 7 | import { GanttFooterComponent } from './footer/gantt-footer.component'; 8 | import { GanttService } from './shared/services/gantt.service'; 9 | import { GanttActivityModule } from './activity/gantt-activity.module'; 10 | 11 | import { GroupByPipe } from './shared/pipes/groupBy.pipe'; 12 | 13 | @NgModule({ 14 | imports: [ 15 | CommonModule, 16 | FormsModule, 17 | GanttActivityModule, 18 | ], 19 | exports: [ 20 | GanttComponent 21 | ], 22 | declarations: [ 23 | GanttComponent, 24 | GanttHeaderComponent, 25 | GanttFooterComponent, 26 | GroupByPipe 27 | ], 28 | providers: [GanttService], 29 | }) 30 | export class GanttModule { } -------------------------------------------------------------------------------- /src/lib/header/gantt-header.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'gantt-header', 5 | template: ` 6 |
7 |
8 |
{{ name }}
9 |
Started: {{ startDate | date: 'medium'}}
10 |
11 |
12 | `, 13 | styles: [` 14 | .gantt-header { 15 | background-color: whitesmoke; 16 | height: 40px; 17 | border-bottom: 1px solid #e0e0e0; 18 | } 19 | 20 | .gantt-header-title { 21 | padding: 12px; 22 | display: flex; 23 | flex-wrap:wrap; 24 | font-family: Arial, Helvetica, sans-serif; 25 | font-size: 16px; 26 | } 27 | 28 | .gantt-header-actions { 29 | display: inline; 30 | float: right; 31 | padding: 6px; 32 | } 33 | `] 34 | }) 35 | export class GanttHeaderComponent { 36 | @Input() name:any; 37 | @Input() startDate: Date; 38 | } -------------------------------------------------------------------------------- /src/lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from './gantt.module'; 2 | export * from './shared/interfaces'; -------------------------------------------------------------------------------- /src/lib/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ng2-gantt", 3 | "version": "1.11.1-alpha.9", 4 | "description": "Angular 2 Gantt", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/dalestone/angular2-gantt.git" 8 | }, 9 | "private": false, 10 | "keywords": [ 11 | "angular", 12 | "material", 13 | "gantt", 14 | "angular2", 15 | "gantt html" 16 | ], 17 | "license": "MIT", 18 | "bugs": { 19 | "url": "https://github.com/dalestone/angular2-gantt/issues" 20 | }, 21 | "homepage": "https://github.com/dalestone/angular2-gantt/#readme", 22 | "peerDependencies": { 23 | "@angular/common": "2.4.5", 24 | "@angular/compiler": "2.4.5", 25 | "@angular/core": "2.4.5" 26 | }, 27 | "dependencies": { 28 | 29 | } 30 | } -------------------------------------------------------------------------------- /src/lib/shared/interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface Project { 2 | id: string; 3 | name: string; 4 | startDate?: Date; 5 | tasks: Task[] 6 | } 7 | 8 | export interface Task { 9 | id: string; 10 | treePath: string; 11 | parentId: string; 12 | name: string; 13 | resource?: string; 14 | start: Date; 15 | end?: Date; 16 | percentComplete?: number; 17 | status?: string; 18 | } 19 | 20 | export interface IGanttOptions { 21 | scale?: IScale; 22 | zooming?: string; 23 | } 24 | 25 | export interface IScale { 26 | start?: Date; 27 | end?: Date; 28 | } 29 | 30 | export interface IBarStyle { 31 | status: string; 32 | backgroundColor: string; 33 | border: string; 34 | progressBackgroundColor: string; 35 | } 36 | 37 | export enum Zooming { 38 | hours, 39 | days 40 | } -------------------------------------------------------------------------------- /src/lib/shared/pipes/groupBy.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | /* 3 | * Group the array by given function 4 | * Takes an array argument that defaults to 1. 5 | * Usage: 6 | * array | groupBy:func() 7 | * Example: 8 | * {{ [ { id: '1'}] | groupBy: }} 9 | * formats to: [] 10 | */ 11 | @Pipe({name: 'groupBy'}) 12 | export class GroupByPipe implements PipeTransform { 13 | transform(array: any[], f: any): any[] { 14 | var groups = {}; 15 | array.forEach((o:any) => { 16 | var group = JSON.stringify(f(o)); 17 | 18 | groups[group] = groups[group] || []; 19 | groups[group].push(o); 20 | }); 21 | return Object.keys(groups).map((group:any) => { 22 | return groups[group]; 23 | }); 24 | } 25 | } -------------------------------------------------------------------------------- /src/lib/shared/services/gantt-config.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | @Injectable() 4 | export class GanttConfig { 5 | public cellWidth: number = 76; 6 | public rowHeight: number = 25; 7 | public activityHeight: number = 300; 8 | public barHeight = 20; 9 | public barLineHeight = 20; 10 | public barMoveable = false; 11 | } 12 | -------------------------------------------------------------------------------- /src/lib/shared/services/gantt.service.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | import { TestBed, async, inject } from '@angular/core/testing'; 3 | 4 | import { GanttService } from './gantt.service'; 5 | 6 | describe('Gantt Service', () => { 7 | beforeEach(() => { 8 | TestBed.configureTestingModule({ 9 | providers: [GanttService] 10 | }); 11 | }); 12 | it('should return day is weekend', inject([GanttService], (ganttService: GanttService) => { 13 | let result = ganttService.isDayWeekend(new Date(2016, 5, 11)); 14 | let expectedResult = true; 15 | 16 | expect(result).toBe(expectedResult); 17 | })); 18 | it('should return day is not weekend', inject([GanttService], (ganttService: GanttService) => { 19 | let result = ganttService.isDayWeekend(new Date(2016, 5, 13)); 20 | let expectedResult = false; 21 | 22 | expect(result).toBe(expectedResult); 23 | })); 24 | it('should add x days to date', inject([GanttService], (ganttService: GanttService) => { 25 | let result = ganttService.addDays(new Date(2016, 5, 13), 2); 26 | let expectedResult = new Date(2016, 5, 15); 27 | 28 | expect(result).toEqual(expectedResult); 29 | })); 30 | it('should remove x days from date', inject([GanttService], (ganttService: GanttService) => { 31 | let result = ganttService.removeDays(new Date(2016, 5, 20), 2); 32 | let expectedResult = new Date(2016, 5, 18); 33 | 34 | expect(result).toEqual(expectedResult); 35 | })); 36 | it('should return difference in dates in days', inject([GanttService], (ganttService: GanttService) => { 37 | let result = ganttService.calculateDiffDays(new Date(2016, 5, 15), new Date(2016, 5, 20)); 38 | let expectedResult = 5; 39 | 40 | expect(result).toBe(expectedResult); 41 | })); 42 | it('should create date range array given start and end dates', inject([GanttService], (ganttService: GanttService) => { 43 | let start = new Date(2016, 5, 15); 44 | let end = new Date(2016, 5, 18); 45 | let result = ganttService.calculateScale(start, end); 46 | let expectedResult = [ new Date(2016, 5, 15), new Date(2016, 5, 16), new Date(2016, 5, 17), new Date(2016, 5, 18)]; 47 | 48 | expect(result).toEqual(expectedResult); 49 | })); 50 | it('should calculate the progress bar width', inject([GanttService], (ganttService) => { 51 | let barWidth = 100; 52 | let percentComplete = 50; 53 | let result = ganttService.calculateBarProgress(barWidth, percentComplete); 54 | let expectedResult = 48; 55 | 56 | expect(result).toBe(expectedResult); 57 | })); 58 | }); 59 | 60 | -------------------------------------------------------------------------------- /src/lib/shared/services/gantt.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { GanttConfig } from './gantt-config.service'; 3 | import { IBarStyle, Task, IScale, Zooming } from '../interfaces'; 4 | import { GroupByPipe } from '../../shared/pipes/groupBy.pipe'; 5 | 6 | @Injectable() 7 | export class GanttService { 8 | public rowHeight: number = 0; 9 | public hourCellWidth: number = 60; // change to 60 so minutes can been seen more easily 10 | public hoursCellWidth: number = this.hourCellWidth * 25; 11 | public cellWidth: number = 0; 12 | public windowInnerWidth: number = 0; 13 | public activityHeight: number = 0; 14 | public barHeight: number = 0; 15 | public barLineHeight: number = 0; 16 | public barTop: number = 0; 17 | public barMoveable: boolean = false; 18 | public gridWidth: number = 560; 19 | private barStyles: IBarStyle[] = [ 20 | { status: "information", backgroundColor: "rgb(18,195, 244)", border: "1px solid #2196F3", progressBackgroundColor: "#2196F3" }, 21 | { status: "warning", backgroundColor: "#FFA726", border: "1px solid #EF6C00", progressBackgroundColor: "#EF6C00" }, 22 | { status: "error", backgroundColor: "#EF5350", border: "1px solid #C62828", progressBackgroundColor: "#C62828" }, 23 | { status: "completed", backgroundColor: "#66BB6A", border: "1px solid #2E7D32", progressBackgroundColor: "#2E7D32" } 24 | ]; 25 | public TASK_CACHE: any[]; 26 | public TIME_SCALE: any[]; 27 | 28 | constructor() { 29 | let _ganttConfig = new GanttConfig(); 30 | 31 | this.rowHeight = _ganttConfig.rowHeight; 32 | this.cellWidth = _ganttConfig.cellWidth; 33 | this.activityHeight = _ganttConfig.activityHeight; 34 | this.barHeight = _ganttConfig.barHeight; 35 | this.barLineHeight = _ganttConfig.barLineHeight; 36 | this.barTop = _ganttConfig.rowHeight; 37 | this.barMoveable = _ganttConfig.barMoveable; 38 | } 39 | 40 | private calculateBarWidth(start: Date, end: Date, hours?: boolean): number { 41 | if (typeof start === "string") { 42 | start = new Date(start); 43 | } 44 | 45 | if (typeof end === "string") { 46 | end = new Date(end); 47 | } 48 | 49 | let days = this.calculateDiffDays(start, end); 50 | let width: number = days * this.cellWidth + days; 51 | 52 | if (hours) { 53 | width = days * this.hourCellWidth * 24 + days * 24; 54 | } 55 | 56 | return width; 57 | } 58 | 59 | private calculateBarLeft(start: Date, scale: any[], hours?: boolean): number { 60 | var left: number = 0; 61 | var hoursInDay: number = 24; 62 | 63 | if (start != null) { 64 | if (typeof start === "string") { 65 | start = new Date(); 66 | } 67 | 68 | for (var i = 0; i < scale.length; i++) { 69 | if (start.getDate() === scale[i].getDate()) { 70 | if (hours) { 71 | left = i * hoursInDay * this.hourCellWidth + hoursInDay * i + this.calculateBarLeftDelta(start, hours); 72 | } else { 73 | left = i * this.cellWidth + i + this.calculateBarLeftDelta(start, hours); 74 | } 75 | break; 76 | } 77 | } 78 | } 79 | return left; 80 | } 81 | 82 | /** Calculates the height of the gantt grid, activity and vertical scroll */ 83 | public calculateGanttHeight(): string { 84 | return `${this.TASK_CACHE.length * this.rowHeight + this.rowHeight * 3}px`; 85 | } 86 | 87 | private calculateBarLeftDelta(start: Date, hours?: boolean): number { 88 | var offset: number = 0; 89 | var hoursInDay: number = 24; 90 | var minutesInHour: number = 60; 91 | var secondsInHour: number = 3600; 92 | var startHours: number = start.getHours() + start.getMinutes() / minutesInHour + start.getSeconds() / secondsInHour; 93 | 94 | if (hours) { 95 | offset = this.hoursCellWidth / hoursInDay * startHours - startHours; 96 | } else { 97 | offset = this.cellWidth / hoursInDay * startHours; 98 | } 99 | return offset; 100 | } 101 | 102 | public isParent(treePath: string): boolean { 103 | 104 | try { 105 | var depth = treePath.split('/').length; 106 | 107 | if (depth === 1) { 108 | return true; 109 | } 110 | } 111 | catch (err) { 112 | return false; 113 | } 114 | return false; 115 | } 116 | 117 | public isChild(treePath: string) { 118 | if (this.isParent(treePath)) { 119 | return '0px'; 120 | } 121 | return '20px'; 122 | } 123 | 124 | /** Calculate the bar styles */ 125 | public calculateBar(task: any, index: number, scale: any, hours?: boolean) { 126 | var barStyle = this.getBarStyle(task.status); 127 | //console.log(this.barTop * index + 2) 128 | return { 129 | 'top': this.barTop * index + 2 + 'px', 130 | 'left': this.calculateBarLeft(task.start, scale, hours) + 'px', 131 | 'height': this.barHeight + 'px', 132 | 'line-height': this.barLineHeight + 'px', 133 | 'width': this.calculateBarWidth(task.start, task.end, hours) + 'px', 134 | 'background-color': barStyle["background-color"], 135 | 'border': barStyle["border"] 136 | } 137 | } 138 | 139 | /** Get the bar style based on task status */ 140 | private getBarStyle(taskStatus: string = ""): any { 141 | var style = {}; 142 | 143 | try { 144 | taskStatus = taskStatus.toLowerCase(); 145 | } catch (err) { 146 | taskStatus = ""; 147 | } 148 | 149 | switch (taskStatus) { 150 | case "information": 151 | style["background-color"] = this.barStyles[0].backgroundColor; 152 | style["border"] = this.barStyles[0].border; 153 | break; 154 | case "warning": 155 | style["background-color"] = this.barStyles[1].backgroundColor; 156 | style["border"] = this.barStyles[1].border; 157 | break; 158 | case "error": 159 | style["background-color"] = this.barStyles[2].backgroundColor; 160 | style["border"] = this.barStyles[2].border; 161 | break; 162 | case "completed": 163 | style["background-color"] = this.barStyles[3].backgroundColor; 164 | style["border"] = this.barStyles[3].border; 165 | break; 166 | default: 167 | style["background-color"] = "rgb(18,195, 244)"; 168 | style["border"] = "1px solid #2196F3"; 169 | break; 170 | } 171 | 172 | return style; 173 | } 174 | 175 | /** Get the progresss bar background colour based on task status */ 176 | public getBarProgressStyle(taskStatus: string = ""): any { 177 | var style = {}; 178 | 179 | try { 180 | taskStatus = taskStatus.toLowerCase(); 181 | } catch (err) { 182 | taskStatus = ""; 183 | } 184 | 185 | switch (taskStatus) { 186 | case "information": 187 | style["background-color"] = this.barStyles[0].progressBackgroundColor; 188 | break; 189 | case "warning": 190 | style["background-color"] = this.barStyles[1].progressBackgroundColor; 191 | break; 192 | case "error": 193 | style["background-color"] = this.barStyles[2].progressBackgroundColor; 194 | break; 195 | case "completed": 196 | style["background-color"] = this.barStyles[3].progressBackgroundColor; 197 | break; 198 | default: 199 | style["background-color"] = this.barStyles[0].progressBackgroundColor; 200 | break; 201 | } 202 | 203 | return style; 204 | } 205 | 206 | /** Calculates the bar progress width in pixels given task percent complete */ 207 | public calculateBarProgress(width: number, percent: number): string { 208 | if (typeof percent === "number") { 209 | if (percent > 100) { 210 | percent = 100; 211 | } 212 | let progress: number = (width / 100) * percent - 2; 213 | 214 | return `${progress}px`; 215 | } 216 | return `${0}px`; 217 | } 218 | 219 | /** Calculates the difference in two dates and returns number of days */ 220 | public calculateDiffDays(start: Date, end: Date): number { 221 | try { 222 | let oneDay = 24 * 60 * 60 * 1000; // hours*minutes*seconds*milliseconds /ms 223 | let diffDays = Math.abs((start.getTime() - end.getTime()) / (oneDay)); 224 | let days = diffDays; // don't use Math.round as it will draw an incorrect bar 225 | 226 | return days; 227 | } catch (err) { 228 | return 0; 229 | } 230 | } 231 | 232 | /** Calculates the difference in two dates and returns number of hours */ 233 | public calculateDuration(task: any): string { 234 | try { 235 | if (task.start != null && task.end != null) { 236 | let oneHour = 60 * 60 * 1000; 237 | let diffHours = (Math.abs((task.start.getTime() - task.end.getTime()) / oneHour)); 238 | let duration = diffHours; 239 | 240 | if (duration > 24) { 241 | return `${Math.round(duration / 24)} day(s)`; // duration in days 242 | } else if (duration > 1) { 243 | return `${Math.round(duration)} hr(s)`; // duration in hours 244 | } else { 245 | let minutes = duration * 60; 246 | 247 | if (minutes < 1) { 248 | return `${Math.round(minutes * 60)} second(s)`; // duration in seconds 249 | } 250 | return `${Math.round(minutes)} min(s)` // duration in minutes 251 | } 252 | } 253 | 254 | return ''; 255 | } catch (err) { 256 | return ''; 257 | } 258 | } 259 | 260 | calculateTotalDuration(tasks: any[]): string { 261 | try { 262 | tasks = tasks.filter(t => t.parentId === t.id); // only calculate total duration with parent tasks 263 | 264 | let totalHours = 0; 265 | let oneHour = 60 * 60 * 1000; 266 | for (let i = 0; i < tasks.length; i++) { 267 | let start = tasks[i].start; 268 | let end = tasks[i].end; 269 | 270 | if (start != null && end != null) { 271 | let duration = Math.abs(tasks[i].end.getTime() - tasks[i].start.getTime()) / oneHour; // duration in hours 272 | totalHours += duration; 273 | } 274 | } 275 | 276 | if (totalHours === 0) { 277 | return ''; 278 | } 279 | 280 | if (totalHours > 24) { 281 | return `${Math.round(totalHours / 24)} day(s)`; // duration in days 282 | } else if (totalHours > 1) { 283 | return `${Math.round(totalHours)} hr(s)`; // duration in hours 284 | } else { 285 | let minutes = totalHours * 60; 286 | 287 | if (minutes < 1) { 288 | return `${Math.round(minutes * 60)} second(s)`; // duration in seconds 289 | } 290 | return `${Math.round(minutes)} min(s)` // duration in minutes 291 | } 292 | } catch (err) { 293 | return ''; 294 | } 295 | } 296 | 297 | /** Calculate the total percentage of a group of tasks */ 298 | calculateTotalPercentage(node: any): number { 299 | var totalPercent: number = 0; 300 | var children = node.children; 301 | 302 | if (children.length > 0) { 303 | children.forEach((child: any) => { 304 | totalPercent += isNaN(child.percentComplete) ? 0 : child.percentComplete; 305 | }); 306 | 307 | return Math.ceil(totalPercent / children.length); 308 | } else { 309 | return isNaN(node.percentComplete) ? 0 : node.percentComplete; 310 | } 311 | } 312 | 313 | /** Calculate the total percent of the parent task */ 314 | calculateParentTotalPercentage(parent: any, tasks: any[]) { 315 | var children = tasks.filter((task:any) => { 316 | return task.parentId === parent.id && task.id != parent.id 317 | }); // get only children tasks ignoring parent. 318 | 319 | var totalPercent: number = 0; 320 | 321 | if (children.length > 0) { 322 | children.forEach((child:any) => { 323 | totalPercent += isNaN(child.percentComplete) ? 0 : child.percentComplete; 324 | }); 325 | 326 | return Math.ceil(totalPercent / children.length); 327 | } else { 328 | return isNaN(parent.percentComplete) ? 0 : parent.percentComplete; 329 | } 330 | } 331 | 332 | /** Calculate the gantt scale range given the start and end date of tasks*/ 333 | public calculateScale(start: Date = new Date(), end: Date = this.addDays(start, 7)) { 334 | let scale: any[] = []; 335 | 336 | try { 337 | while (start.getTime() <= end.getTime()) { 338 | scale.push(start); 339 | start = this.addDays(start, 1); 340 | } 341 | return scale; 342 | 343 | } catch (err) { 344 | return scale; 345 | } 346 | } 347 | 348 | /** Determines whether given date is a weekend */ 349 | public isDayWeekend(date: Date): boolean { 350 | let day = date.getDay(); 351 | 352 | if (day === 6 || day === 0) { 353 | return true; 354 | } 355 | return false; 356 | } 357 | 358 | /** Add x number of days to a date object */ 359 | public addDays(date: Date, days: number): Date { 360 | let result = new Date(date); 361 | result.setDate(result.getDate() + days); 362 | return result; 363 | } 364 | 365 | //** Remove x number of days from a date object */ 366 | public removeDays(date: Date, days: number): Date { 367 | let result = new Date(date); 368 | result.setDate(result.getDate() - days); 369 | return result; 370 | } 371 | 372 | /** Calculates the grid scale for gantt based on tasks start and end dates */ 373 | public calculateGridScale(tasks: Task[]): IScale { 374 | var start: Date; 375 | var end: Date; 376 | var dates = tasks.map((task:any) => { 377 | return { 378 | start: new Date(task.start), 379 | end: new Date(task.end) 380 | } 381 | }); 382 | 383 | start = new Date(Math.min.apply(null, dates.map(function (t) { 384 | return t.start; 385 | }))); 386 | 387 | end = new Date(Math.max.apply(null, dates.map(function (t) { 388 | return t.end; 389 | }))); 390 | 391 | return { 392 | start: start, 393 | end: end 394 | } 395 | } 396 | 397 | /** Create an hours array for use in time scale component */ 398 | public getHours(cols: number): string[] { 399 | var hours: string[] = []; 400 | 401 | while (hours.length <= cols * 24) { 402 | for (var i = 0; i <= 23; i++) { 403 | if (i < 10) { 404 | hours.push('0' + i.toString()); 405 | } else { 406 | hours.push(i.toString()); 407 | } 408 | } 409 | } 410 | 411 | return hours; 412 | } 413 | 414 | public getComputedStyle(element: any, attribute: any) { 415 | return parseInt(document.defaultView.getComputedStyle(element)[attribute], 10); 416 | } 417 | 418 | //TODO(dale): determine whether this is needed 419 | public calculateContainerWidth(): number { 420 | this.windowInnerWidth = window.innerWidth; 421 | let containerWidth = (innerWidth - 18); 422 | 423 | return containerWidth; 424 | } 425 | 426 | public calculateActivityContainerDimensions(): any { 427 | var scrollWidth: number = 18; 428 | this.windowInnerWidth = window.innerWidth; 429 | let width = this.windowInnerWidth - this.gridWidth - scrollWidth; 430 | 431 | return { height: this.activityHeight, width: width }; 432 | } 433 | 434 | /** Set the vertical scroll top positions for gantt */ 435 | public scrollTop(verticalScrollElem: any, ganttGridElem: any, ganttActivityAreaElem: any) { 436 | let verticalScrollTop = verticalScrollElem.scrollTop; 437 | let scroll = this.setScrollTop; 438 | 439 | // debounce 440 | if (verticalScrollTop !== null && verticalScrollTop !== undefined) { 441 | setTimeout(function() { 442 | scroll(verticalScrollTop, ganttActivityAreaElem); 443 | scroll(ganttActivityAreaElem.scrollTop, ganttGridElem); 444 | 445 | }, 50); 446 | } 447 | } 448 | 449 | /** Group data by id , only supports one level*/ 450 | public groupData(tasks: any): any { 451 | var merged:any = []; 452 | var groups:any = new GroupByPipe().transform(tasks, (task:any) => { 453 | return [task.treePath.split('/')[0]] 454 | }); 455 | return merged.concat.apply([], groups); 456 | } 457 | 458 | /** Create tree of data */ 459 | public transformData(input: any): any { 460 | var output:any = []; 461 | for (var i = 0; i < input.length; i++) { 462 | var chain:any = input[i].id.split('/'); 463 | var currentNode:any = output; 464 | for (var j = 0; j < chain.length; j++) { 465 | var wantedNode:any = chain[j]; 466 | var lastNode:any = currentNode; 467 | for (var k = 0; k < currentNode.length; k++) { 468 | if (currentNode[k].name == wantedNode) { 469 | currentNode = currentNode[k].children; 470 | break; 471 | } 472 | } 473 | // If we couldn't find an item in this list of children 474 | // that has the right name, create one: 475 | if (lastNode == currentNode) { 476 | //TODO(dale): determine way to show percent complete on correct child 477 | var newNode:any = currentNode[k] = { 478 | name: wantedNode, 479 | percentComplete: input[i].percentComplete, 480 | start: input[i].start, 481 | end: input[i].end, 482 | children: [] 483 | }; 484 | currentNode = newNode.children; 485 | } 486 | } 487 | } 488 | return output; 489 | } 490 | 491 | /** Checks whether any new data needs to be added to task cache */ 492 | public doTaskCheck(tasks: any[], treeExpanded: boolean): boolean { 493 | var cachedTaskIds = this.TASK_CACHE.map((task:any) => { return task.id }); 494 | var itemsToCache: any[] = []; 495 | 496 | if (treeExpanded) { 497 | // push children and parent tasks that are not cached 498 | tasks.filter((task:any) => { 499 | return cachedTaskIds.indexOf(task.id) === -1 500 | }).forEach((task:any) => { 501 | itemsToCache.push(task); 502 | }) 503 | } else { 504 | // only look at tasks that are not cached 505 | tasks.filter((task:any) => { 506 | return cachedTaskIds.indexOf(task.id) === -1 && task.treePath.split('/').length === 1 507 | }).forEach((task:any) => { 508 | itemsToCache.push(task); 509 | }); 510 | } 511 | 512 | itemsToCache.forEach((item:any) => { 513 | this.TASK_CACHE.push(item); 514 | }); 515 | 516 | if (itemsToCache.length > 0) { 517 | return true; 518 | } 519 | 520 | return false; 521 | } 522 | 523 | /** Set a id prefix so CSS3 query selector can work with ids that contain numbers */ 524 | public setIdPrefix(id: string): string { 525 | return `_${id}`; 526 | } 527 | 528 | // /** Remove the id prefix to allow querying of data */ 529 | // public removeIdPrefix(id: string): string { 530 | // return id.substring(1, id.length - 1); 531 | // } 532 | 533 | /** Set the scroll top property of a native DOM element */ 534 | private setScrollTop(scrollTop: number, element: any): void { 535 | if (element !== null && element !== undefined) { 536 | element.scrollTop = scrollTop; 537 | } 538 | } 539 | } 540 | -------------------------------------------------------------------------------- /src/lib/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "moduleResolution": "node", 5 | "target": "es5", 6 | "sourceMap": true, 7 | "inlineSources": false, 8 | "declaration": false, 9 | "experimentalDecorators": true, 10 | "emitDecoratorMetadata": true, 11 | "stripInternal": true, 12 | "skipLibCheck": true, 13 | "outDir": "../../dist" 14 | }, 15 | "files": [ 16 | "index.ts" 17 | ] 18 | } -------------------------------------------------------------------------------- /test/karma.config.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/0.13/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', 'angular-cli'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-remap-istanbul'), 12 | require('angular-cli/plugins/karma') 13 | ], 14 | files: [ 15 | { pattern: './src/test.ts', watched: false } 16 | ], 17 | preprocessors: { 18 | './src/test.ts': ['angular-cli'] 19 | }, 20 | remapIstanbulReporter: { 21 | reports: { 22 | html: 'coverage', 23 | lcovonly: './coverage/coverage.lcov' 24 | } 25 | }, 26 | angularCli: { 27 | config: './angular-cli.json', 28 | environment: 'dev' 29 | }, 30 | reporters: ['progress', 'karma-remap-istanbul'], 31 | port: 9876, 32 | colors: true, 33 | logLevel: config.LOG_INFO, 34 | autoWatch: true, 35 | browsers: ['Chrome'], 36 | singleRun: false 37 | }); 38 | }; 39 | -------------------------------------------------------------------------------- /test/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/docs/referenceConf.js 3 | 4 | /*global jasmine */ 5 | var SpecReporter = require('jasmine-spec-reporter'); 6 | 7 | exports.config = { 8 | allScriptsTimeout: 11000, 9 | specs: [ 10 | './e2e/**/*.e2e-spec.ts' 11 | ], 12 | capabilities: { 13 | 'browserName': 'chrome' 14 | }, 15 | directConnect: true, 16 | baseUrl: 'http://localhost:4200/', 17 | framework: 'jasmine', 18 | jasmineNodeOpts: { 19 | showColors: true, 20 | defaultTimeoutInterval: 30000, 21 | print: function() {} 22 | }, 23 | useAllAngular2AppRoots: true, 24 | beforeLaunch: function() { 25 | require('ts-node').register({ 26 | project: 'e2e' 27 | }); 28 | }, 29 | onPrepare: function() { 30 | jasmine.getEnv().addReporter(new SpecReporter()); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "class-name": true, 7 | "comment-format": [ 8 | true, 9 | "check-space" 10 | ], 11 | "curly": true, 12 | "eofline": true, 13 | "forin": true, 14 | "indent": [ 15 | true, 16 | "spaces" 17 | ], 18 | "label-position": true, 19 | "label-undefined": true, 20 | "max-line-length": [ 21 | true, 22 | 140 23 | ], 24 | "member-access": false, 25 | "member-ordering": [ 26 | true, 27 | "static-before-instance", 28 | "variables-before-functions" 29 | ], 30 | "no-arg": true, 31 | "no-bitwise": true, 32 | "no-console": [ 33 | true, 34 | "debug", 35 | "info", 36 | "time", 37 | "timeEnd", 38 | "trace" 39 | ], 40 | "no-construct": true, 41 | "no-debugger": true, 42 | "no-duplicate-key": true, 43 | "no-duplicate-variable": true, 44 | "no-empty": false, 45 | "no-eval": true, 46 | "no-inferrable-types": true, 47 | "no-shadowed-variable": true, 48 | "no-string-literal": false, 49 | "no-switch-case-fall-through": true, 50 | "no-trailing-whitespace": true, 51 | "no-unused-expression": true, 52 | "no-unused-variable": true, 53 | "no-unreachable": true, 54 | "no-use-before-declare": true, 55 | "no-var-keyword": true, 56 | "object-literal-sort-keys": false, 57 | "one-line": [ 58 | true, 59 | "check-open-brace", 60 | "check-catch", 61 | "check-else", 62 | "check-whitespace" 63 | ], 64 | "quotemark": [ 65 | true, 66 | "single" 67 | ], 68 | "radix": true, 69 | "semicolon": [ 70 | "always" 71 | ], 72 | "triple-equals": [ 73 | true, 74 | "allow-null-check" 75 | ], 76 | "typedef-whitespace": [ 77 | true, 78 | { 79 | "call-signature": "nospace", 80 | "index-signature": "nospace", 81 | "parameter": "nospace", 82 | "property-declaration": "nospace", 83 | "variable-declaration": "nospace" 84 | } 85 | ], 86 | "variable-name": false, 87 | "whitespace": [ 88 | true, 89 | "check-branch", 90 | "check-decl", 91 | "check-operator", 92 | "check-separator", 93 | "check-type" 94 | ], 95 | 96 | "directive-selector-prefix": [true, "app"], 97 | "component-selector-prefix": [true, "app"], 98 | "directive-selector-name": [true, "camelCase"], 99 | "component-selector-name": [true, "kebab-case"], 100 | "directive-selector-type": [true, "attribute"], 101 | "component-selector-type": [true, "element"], 102 | "use-input-property-decorator": true, 103 | "use-output-property-decorator": true, 104 | "use-host-property-decorator": true, 105 | "no-input-rename": true, 106 | "no-output-rename": true, 107 | "use-life-cycle-interface": true, 108 | "use-pipe-transform-interface": true, 109 | "component-class-suffix": true, 110 | "directive-class-suffix": true, 111 | "templates-use-public": true, 112 | "invoke-injectable": true 113 | } 114 | } 115 | --------------------------------------------------------------------------------