diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ea2b204
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,32 @@
+# Node
+node_modules/*
+npm-debug.log
+
+# TypeScript
+src/*.js
+src/*.map
+src/*.d.ts
+
+# JetBrains
+.idea
+.project
+.settings
+.idea/*
+*.iml
+
+# VS Code
+.vscode/*
+
+# Windows
+Thumbs.db
+Desktop.ini
+
+# Mac
+.DS_Store
+**/.DS_Store
+
+# Ngc generated files
+**/*.ngfactory.ts
+
+# Build files
+dist/*
diff --git a/.npmignore b/.npmignore
new file mode 100644
index 0000000..fdb7f4f
--- /dev/null
+++ b/.npmignore
@@ -0,0 +1,34 @@
+# Node
+node_modules/*
+npm-debug.log
+docs/*
+# DO NOT IGNORE TYPESCRIPT FILES FOR NPM
+# TypeScript
+# *.js
+# *.map
+# *.d.ts
+
+# JetBrains
+.idea
+.project
+.settings
+.idea/*
+*.iml
+
+# VS Code
+.vscode/*
+
+# Windows
+Thumbs.db
+Desktop.ini
+
+# Mac
+.DS_Store
+**/.DS_Store
+
+# Ngc generated files
+**/*.ngfactory.ts
+
+# Library files
+src/*
+build/*
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..2567e63
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,4 @@
+language: node_js
+sudo: false
+node_js:
+- '4.2.1'
diff --git a/.yo-rc.json b/.yo-rc.json
new file mode 100644
index 0000000..f146d25
--- /dev/null
+++ b/.yo-rc.json
@@ -0,0 +1,7 @@
+{
+ "generator-angular2-library": {
+ "promptValues": {
+ "gitRepositoryUrl": "https://github.com/jwiesmann/angular4-gantt"
+ }
+ }
+}
\ No newline at end of file
diff --git a/README.MD b/README.MD
new file mode 100644
index 0000000..94b68ea
--- /dev/null
+++ b/README.MD
@@ -0,0 +1,72 @@
+# angular4-gantt
+
+## Installation
+
+To install this library, run:
+
+```bash
+$ npm install angular4-gantt --save
+```
+
+## Consuming your library
+
+Once you have published your library to npm, you can import your library in any Angular application by running:
+
+```bash
+$ npm install angular4-gantt
+```
+
+and then from your Angular `AppModule`:
+
+```typescript
+import { BrowserModule } from '@angular/platform-browser';
+import { NgModule } from '@angular/core';
+
+import { AppComponent } from './app.component';
+
+// Import your library
+import { SampleModule } from 'angular4-gantt';
+
+@NgModule({
+ declarations: [
+ AppComponent
+ ],
+ imports: [
+ BrowserModule,
+
+ // Specify your library as an import
+ LibraryModule
+ ],
+ providers: [],
+ bootstrap: [AppComponent]
+})
+export class AppModule { }
+```
+
+Once your library is imported, you can use its components, directives and pipes in your Angular application:
+
+```xml
+
+
+ {{title}}
+
+
+```
+
+## Development
+
+To generate all `*.js`, `*.d.ts` and `*.metadata.json` files:
+
+```bash
+$ npm run build
+```
+
+To lint all `*.ts` files:
+
+```bash
+$ npm run lint
+```
+
+## License
+
+MIT © [joerg.wiesmann](mailto:joerg.wiesmann@gmail.com)
diff --git a/gulpfile.js b/gulpfile.js
new file mode 100644
index 0000000..59bff56
--- /dev/null
+++ b/gulpfile.js
@@ -0,0 +1,227 @@
+/* eslint-disable */
+var gulp = require('gulp'),
+ path = require('path'),
+ ngc = require('@angular/compiler-cli/src/main').main,
+ rollup = require('gulp-rollup'),
+ rename = require('gulp-rename'),
+ del = require('del'),
+ runSequence = require('run-sequence'),
+ inlineResources = require('./tools/gulp/inline-resources');
+
+const rootFolder = path.join(__dirname);
+const srcFolder = path.join(rootFolder, 'src');
+const tmpFolder = path.join(rootFolder, '.tmp');
+const buildFolder = path.join(rootFolder, 'build');
+const distFolder = path.join(rootFolder, 'dist');
+
+/**
+ * 1. Delete /dist folder
+ */
+gulp.task('clean:dist', function () {
+
+ // Delete contents but not dist folder to avoid broken npm links
+ // when dist directory is removed while npm link references it.
+ return deleteFolders([distFolder + '/**', '!' + distFolder]);
+});
+
+/**
+ * 2. Clone the /src folder into /.tmp. If an npm link inside /src has been made,
+ * then it's likely that a node_modules folder exists. Ignore this folder
+ * when copying to /.tmp.
+ */
+gulp.task('copy:source', function () {
+ return gulp.src([`${srcFolder}/**/*`, `!${srcFolder}/node_modules`])
+ .pipe(gulp.dest(tmpFolder));
+});
+
+/**
+ * 3. Inline template (.html) and style (.css) files into the the component .ts files.
+ * We do this on the /.tmp folder to avoid editing the original /src files
+ */
+gulp.task('inline-resources', function () {
+ return Promise.resolve()
+ .then(() => inlineResources(tmpFolder));
+});
+
+
+/**
+ * 4. Run the Angular compiler, ngc, on the /.tmp folder. This will output all
+ * compiled modules to the /build folder.
+ */
+gulp.task('ngc', function () {
+ return ngc({
+ project: `${tmpFolder}/tsconfig.es5.json`
+ })
+ .then((exitCode) => {
+ if (exitCode === 1) {
+ // This error is caught in the 'compile' task by the runSequence method callback
+ // so that when ngc fails to compile, the whole compile process stops running
+ throw new Error('ngc compilation failed');
+ }
+ });
+});
+
+/**
+ * 5. Run rollup inside the /build folder to generate our Flat ES module and place the
+ * generated file into the /dist folder
+ */
+gulp.task('rollup:fesm', function () {
+ return gulp.src(`${buildFolder}/**/*.js`)
+ // transform the files here.
+ .pipe(rollup({
+
+ // Bundle's entry point
+ // See https://github.com/rollup/rollup/wiki/JavaScript-API#entry
+ entry: `${buildFolder}/index.js`,
+
+ // Allow mixing of hypothetical and actual files. "Actual" files can be files
+ // accessed by Rollup or produced by plugins further down the chain.
+ // This prevents errors like: 'path/file' does not exist in the hypothetical file system
+ // when subdirectories are used in the `src` directory.
+ allowRealFiles: true,
+
+ // A list of IDs of modules that should remain external to the bundle
+ // See https://github.com/rollup/rollup/wiki/JavaScript-API#external
+ external: [
+ '@angular/core',
+ '@angular/common'
+ ],
+
+ // Format of generated bundle
+ // See https://github.com/rollup/rollup/wiki/JavaScript-API#format
+ format: 'es'
+ }))
+ .pipe(gulp.dest(distFolder));
+});
+
+/**
+ * 6. Run rollup inside the /build folder to generate our UMD module and place the
+ * generated file into the /dist folder
+ */
+gulp.task('rollup:umd', function () {
+ return gulp.src(`${buildFolder}/**/*.js`)
+ // transform the files here.
+ .pipe(rollup({
+
+ // Bundle's entry point
+ // See https://github.com/rollup/rollup/wiki/JavaScript-API#entry
+ entry: `${buildFolder}/index.js`,
+
+ // Allow mixing of hypothetical and actual files. "Actual" files can be files
+ // accessed by Rollup or produced by plugins further down the chain.
+ // This prevents errors like: 'path/file' does not exist in the hypothetical file system
+ // when subdirectories are used in the `src` directory.
+ allowRealFiles: true,
+
+ // A list of IDs of modules that should remain external to the bundle
+ // See https://github.com/rollup/rollup/wiki/JavaScript-API#external
+ external: [
+ '@angular/core',
+ '@angular/common'
+ ],
+
+ // Format of generated bundle
+ // See https://github.com/rollup/rollup/wiki/JavaScript-API#format
+ format: 'umd',
+
+ // Export mode to use
+ // See https://github.com/rollup/rollup/wiki/JavaScript-API#exports
+ exports: 'named',
+
+ // The name to use for the module for UMD/IIFE bundles
+ // (required for bundles with exports)
+ // See https://github.com/rollup/rollup/wiki/JavaScript-API#modulename
+ moduleName: 'angular4-gantt',
+
+ // See https://github.com/rollup/rollup/wiki/JavaScript-API#globals
+ globals: {
+ typescript: 'ts'
+ }
+
+ }))
+ .pipe(rename('angular4-gantt.umd.js'))
+ .pipe(gulp.dest(distFolder));
+});
+
+/**
+ * 7. Copy all the files from /build to /dist, except .js files. We ignore all .js from /build
+ * because with don't need individual modules anymore, just the Flat ES module generated
+ * on step 5.
+ */
+gulp.task('copy:build', function () {
+ return gulp.src([`${buildFolder}/**/*`, `!${buildFolder}/**/*.js`])
+ .pipe(gulp.dest(distFolder));
+});
+
+/**
+ * 8. Copy package.json from /src to /dist
+ */
+gulp.task('copy:manifest', function () {
+ return gulp.src([`${srcFolder}/package.json`])
+ .pipe(gulp.dest(distFolder));
+});
+
+/**
+ * 9. Copy README.md from / to /dist
+ */
+gulp.task('copy:readme', function () {
+ return gulp.src([path.join(rootFolder, 'README.MD')])
+ .pipe(gulp.dest(distFolder));
+});
+
+/**
+ * 10. Delete /.tmp folder
+ */
+gulp.task('clean:tmp', function () {
+ return deleteFolders([tmpFolder]);
+});
+
+/**
+ * 11. Delete /build folder
+ */
+gulp.task('clean:build', function () {
+ return deleteFolders([buildFolder]);
+});
+
+gulp.task('compile', function () {
+ runSequence(
+ 'clean:dist',
+ 'copy:source',
+ 'inline-resources',
+ 'ngc',
+ 'rollup:fesm',
+ 'rollup:umd',
+ 'copy:build',
+ 'copy:manifest',
+ 'copy:readme',
+ 'clean:build',
+ 'clean:tmp',
+ function (err) {
+ if (err) {
+ console.log('ERROR:', err.message);
+ deleteFolders([distFolder, tmpFolder, buildFolder]);
+ } else {
+ console.log('Compilation finished succesfully');
+ }
+ });
+});
+
+/**
+ * Watch for any change in the /src folder and compile files
+ */
+gulp.task('watch', function () {
+ gulp.watch(`${srcFolder}/**/*`, ['compile']);
+});
+
+gulp.task('clean', ['clean:dist', 'clean:tmp', 'clean:build']);
+
+gulp.task('build', ['clean', 'compile']);
+gulp.task('build:watch', ['build', 'watch']);
+gulp.task('default', ['build:watch']);
+
+/**
+ * Deletes the specified folder
+ */
+function deleteFolders(folders) {
+ return del(folders);
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..4367584
--- /dev/null
+++ b/package.json
@@ -0,0 +1,68 @@
+{
+ "name": "angular4-gantt",
+ "version": "0.1.0",
+ "scripts": {
+ "build": "gulp build",
+ "build:watch": "gulp",
+ "docs": "npm run docs:build",
+ "docs:build": "compodoc -p tsconfig.json -n angular4-gantt -d docs --hideGenerator",
+ "docs:serve": "npm run docs:build -- -s",
+ "docs:watch": "npm run docs:build -- -s -w",
+ "lint": "tslint --type-check --project tsconfig.json src/**/*.ts",
+ "test": "tsc && karma start"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/jwiesmann/angular4-gantt"
+ },
+ "author": {
+ "name": "joerg.wiesmann",
+ "email": "joerg.wiesmann@gmail.com"
+ },
+ "keywords": [
+ "angular"
+ ],
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/jwiesmann/angular4-gantt/issues"
+ },
+ "devDependencies": {
+ "@angular/common": "^4.0.0",
+ "@angular/compiler": "^4.0.0",
+ "@angular/compiler-cli": "^4.0.0",
+ "@angular/core": "^4.0.0",
+ "@angular/platform-browser": "^4.0.0",
+ "@angular/platform-browser-dynamic": "^4.0.0",
+ "@compodoc/compodoc": "^1.0.0-beta.10",
+ "@types/jasmine": "2.5.38",
+ "@types/node": "~6.0.60",
+ "codelyzer": "~2.0.0",
+ "core-js": "^2.4.1",
+ "del": "^2.2.2",
+ "gulp": "^3.9.1",
+ "gulp-rename": "^1.2.2",
+ "gulp-rollup": "^2.11.0",
+ "jasmine-core": "~2.5.2",
+ "jasmine-spec-reporter": "~3.2.0",
+ "karma": "~1.4.1",
+ "karma-chrome-launcher": "~2.0.0",
+ "karma-cli": "~1.0.1",
+ "karma-coverage-istanbul-reporter": "^0.2.0",
+ "karma-jasmine": "~1.1.0",
+ "karma-jasmine-html-reporter": "^0.2.2",
+ "node-sass": "^4.5.2",
+ "node-sass-tilde-importer": "^1.0.0",
+ "node-watch": "^0.5.2",
+ "protractor": "~5.1.0",
+ "rollup": "^0.41.6",
+ "run-sequence": "^1.2.2",
+ "rxjs": "^5.1.0",
+ "ts-node": "~2.0.0",
+ "tslint": "~4.5.0",
+ "typescript": "~2.2.0",
+ "zone.js": "^0.8.4"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+}
diff --git a/src/gantt-activity-background/gantt-activity-background.component.css b/src/gantt-activity-background/gantt-activity-background.component.css
new file mode 100644
index 0000000..298df07
--- /dev/null
+++ b/src/gantt-activity-background/gantt-activity-background.component.css
@@ -0,0 +1,19 @@
+.gantt_activity_bg {
+ overflow: hidden;
+}
+
+.gantt_activity_row {
+ border-bottom: 1px solid #ebebeb;
+ background-color: #fff;
+ box-sizing: border-box;
+}
+
+.gantt_activity_cell {
+ display: inline-block;
+ height: 100%;
+ border-right: 1px solid #ebebeb;
+}
+
+.weekend {
+ background-color:whitesmoke;
+}
diff --git a/src/gantt-activity-background/gantt-activity-background.component.html b/src/gantt-activity-background/gantt-activity-background.component.html
new file mode 100644
index 0000000..e16bff1
--- /dev/null
+++ b/src/gantt-activity-background/gantt-activity-background.component.html
@@ -0,0 +1,7 @@
+
diff --git a/src/gantt-activity-background/gantt-activity-background.component.spec.ts b/src/gantt-activity-background/gantt-activity-background.component.spec.ts
new file mode 100644
index 0000000..133f071
--- /dev/null
+++ b/src/gantt-activity-background/gantt-activity-background.component.spec.ts
@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { GanttActivityBackgroundComponent } from './gantt-activity-background.component';
+
+describe('GanttActivityBackgroundComponent', () => {
+ let component: GanttActivityBackgroundComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ GanttActivityBackgroundComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(GanttActivityBackgroundComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should be created', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/gantt-activity-background/gantt-activity-background.component.ts b/src/gantt-activity-background/gantt-activity-background.component.ts
new file mode 100644
index 0000000..7a4103b
--- /dev/null
+++ b/src/gantt-activity-background/gantt-activity-background.component.ts
@@ -0,0 +1,68 @@
+import {Component, ElementRef, Input, OnInit, ViewChild} from '@angular/core';
+import {Zooming} from '../interfaces';
+import {GanttService} from '../gantt.service';
+
+@Component({
+ selector: 'app-gantt-activity-background',
+ templateUrl: './gantt-activity-background.component.html',
+ styleUrls: ['./gantt-activity-background.component.css']
+})
+export class GanttActivityBackgroundComponent implements OnInit {
+
+ @Input() tasks: any;
+ @Input() timeScale: any;
+ @Input() zoom: any;
+ @Input() zoomLevel: string;
+
+ @ViewChild('bg') bg: ElementRef;
+
+ cells: any[] = [];
+
+ constructor(public ganttService: GanttService) {
+ }
+
+ ngOnInit() {
+ this.drawGrid();
+
+ this.zoom.subscribe((zoomLevel: string) => {
+ this.zoomLevel = zoomLevel;
+ this.drawGrid();
+ });
+ }
+
+ isDayWeekend(date: Date): boolean {
+ return this.ganttService.isDayWeekend(date);
+ }
+
+ private setRowStyle() {
+ return {
+ 'height': this.ganttService.rowHeight + 'px'
+ };
+ }
+
+ private setCellStyle() {
+ let width = this.ganttService.cellWidth;
+
+ if (this.zoomLevel === Zooming[Zooming.hours]) {
+ width = this.ganttService.hourCellWidth;
+ }
+
+ return {
+ 'width': width + 'px'
+ };
+ }
+
+ private drawGrid(): void {
+ if (this.zoomLevel === Zooming[Zooming.hours]) {
+ this.cells = [];
+
+ this.timeScale.forEach((date: any) => {
+ for (let i = 0; i <= 23; i++) {
+ this.cells.push(date);
+ }
+ });
+ } else {
+ this.cells = this.timeScale;
+ }
+ }
+}
diff --git a/src/gantt-activity-bars/gantt-activity-bars.component.css b/src/gantt-activity-bars/gantt-activity-bars.component.css
new file mode 100644
index 0000000..059b915
--- /dev/null
+++ b/src/gantt-activity-bars/gantt-activity-bars.component.css
@@ -0,0 +1,68 @@
+.gantt_activity_line {
+ /*border-radius: 2px;*/
+ position: absolute;
+ box-sizing: border-box;
+ background-color: rgb(18,195,244);
+ border: 1px solid #2196F3;
+ -webkit-user-select: none;
+}
+
+.gantt_activity_line:hover {
+ /*cursor: move;*/
+}
+
+.gantt_activity_progress {
+ text-align: center;
+ z-index: 0;
+ background: #2196F3;
+ position: absolute;
+ min-height: 18px;
+ display: block;
+ height: 18px;
+}
+
+.gantt_activity_progress_drag {
+ height: 8px;
+ width: 8px;
+ bottom: -4px;
+ margin-left: 4px;
+ background-position: bottom;
+ background-image: "";
+ background-repeat: no-repeat;
+ z-index: 2;
+}
+
+.gantt_activity_content {
+ font-size: 12px;
+ color: #fff;
+ width: 100%;
+ top: 0;
+ position: absolute;
+ white-space: nowrap;
+ text-align: center;
+ line-height: inherit;
+ overflow: hidden;
+ height: 100%;
+}
+
+.gantt_activity_link_control {
+ position: absolute;
+ width: 13px;
+ top: 0;
+}
+
+.gantt_activity_right {
+ right: 0;
+}
+
+.gantt_activity_left {
+ left: 0;
+}
+
+.gantt_activity_right:hover {
+ cursor:w-resize;
+}
+
+.gantt_activity_left:hover {
+ cursor:w-resize;
+}
diff --git a/src/gantt-activity-bars/gantt-activity-bars.component.html b/src/gantt-activity-bars/gantt-activity-bars.component.html
new file mode 100644
index 0000000..f74eaa0
--- /dev/null
+++ b/src/gantt-activity-bars/gantt-activity-bars.component.html
@@ -0,0 +1,14 @@
+
diff --git a/src/gantt-activity-bars/gantt-activity-bars.component.spec.ts b/src/gantt-activity-bars/gantt-activity-bars.component.spec.ts
new file mode 100644
index 0000000..21b6c97
--- /dev/null
+++ b/src/gantt-activity-bars/gantt-activity-bars.component.spec.ts
@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { GanttActivityBarsComponent } from './gantt-activity-bars.component';
+
+describe('GanttActivityBarsComponent', () => {
+ let component: GanttActivityBarsComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ GanttActivityBarsComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(GanttActivityBarsComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should be created', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/gantt-activity-bars/gantt-activity-bars.component.ts b/src/gantt-activity-bars/gantt-activity-bars.component.ts
new file mode 100644
index 0000000..318a160
--- /dev/null
+++ b/src/gantt-activity-bars/gantt-activity-bars.component.ts
@@ -0,0 +1,147 @@
+import {Component, Input, OnInit} from '@angular/core';
+import {Zooming} from '../interfaces';
+import {GanttService} from '../gantt.service';
+
+@Component({
+ selector: 'app-gantt-activity-bars',
+ templateUrl: './gantt-activity-bars.component.html',
+ styleUrls: ['./gantt-activity-bars.component.css']
+})
+export class GanttActivityBarsComponent implements OnInit {
+
+ @Input() timeScale: any;
+ @Input() dimensions: any;
+ @Input() tasks: any;
+ @Input() zoom: any;
+ @Input() zoomLevel: any;
+
+ private containerHeight = 0;
+ private containerWidth = 0;
+
+ constructor(public ganttService: GanttService) {
+ }
+
+ ngOnInit() {
+ this.containerHeight = this.dimensions.height;
+ this.containerWidth = this.dimensions.width;
+
+ this.zoom.subscribe((zoomLevel: string) => {
+ this.zoomLevel = zoomLevel;
+ });
+ }
+
+ // TODO(dale): the ability to move bars needs reviewing and there are a few quirks
+ expandLeft($event: any, bar: any) {
+ $event.stopPropagation();
+
+ const ganttService = this.ganttService;
+ const startX = $event.clientX;
+ const startBarWidth = bar.style.width;
+ const startBarLeft = bar.style.left;
+
+ function doDrag(e: any) {
+ const cellWidth = ganttService.cellWidth;
+ const barWidth = startBarWidth - e.clientX + startX;
+ const days = Math.round(barWidth / cellWidth);
+
+ bar.style.width = days * cellWidth + days;
+ bar.style.left = (startBarLeft - (days * cellWidth) - days);
+ }
+
+ this.addMouseEventListeners(doDrag);
+
+ return false;
+ }
+
+ expandRight($event: any, bar: any) {
+ $event.stopPropagation();
+
+ const ganttService = this.ganttService;
+ const startX = $event.clientX;
+ const startBarWidth = bar.style.width;
+ const startBarEndDate = bar.task.end;
+ const startBarLeft = bar.style.left;
+
+ function doDrag(e: any) {
+ const cellWidth = ganttService.cellWidth;
+ let barWidth = startBarWidth + e.clientX - startX;
+ let days = Math.round(barWidth / cellWidth);
+
+ if (barWidth < cellWidth) {
+ barWidth = cellWidth;
+ days = Math.round(barWidth / cellWidth);
+ }
+ bar.style.width = ((days * cellWidth) + days); // rounds to the nearest cell
+ }
+
+ this.addMouseEventListeners(doDrag);
+
+ return false;
+ }
+
+ move($event: any, bar: any) {
+ $event.stopPropagation();
+
+ const ganttService = this.ganttService;
+ const startX = $event.clientX;
+ const startBarLeft = bar.style.left;
+
+ function doDrag(e: any) {
+ const cellWidth = ganttService.cellWidth;
+ const barLeft = startBarLeft + e.clientX - startX;
+ const days = Math.round(barLeft / cellWidth);
+
+ // TODO: determine how many days the bar can be moved
+ // if (days < maxDays) {
+ bar.style.left = ((days * cellWidth) + days); // rounded to nearest cell
+
+ // keep bar in bounds of grid
+ if (barLeft < 0) {
+ bar.style.left = 0;
+ }
+ // }
+ // TODO: it needs to take into account the max number of days.
+ // TODO: it needs to take into account the current days.
+ // TODO: it needs to take into account the right boundary.
+ }
+
+ this.addMouseEventListeners(doDrag);
+
+ return false;
+ }
+
+ private drawBar(task: any, index: number) {
+ let style = {};
+
+ if (this.zoomLevel === Zooming[Zooming.hours]) {
+ style = this.ganttService.calculateBar(task, index, this.timeScale, true);
+ } else {
+ style = this.ganttService.calculateBar(task, index, this.timeScale);
+ }
+ return style;
+ }
+
+ private drawProgress(task: any, bar: any): any {
+ const barStyle = this.ganttService.getBarProgressStyle(task.status);
+ const width = this.ganttService.calculateBarProgress(this.ganttService.getComputedStyle(bar, 'width'), task.percentComplete);
+
+ return {
+ 'width': width,
+ 'background-color': barStyle['background-color'],
+ };
+ }
+
+ private addMouseEventListeners(dragFn: any) {
+
+ function stopFn() {
+ document.documentElement.removeEventListener('mousemove', dragFn, false);
+ document.documentElement.removeEventListener('mouseup', stopFn, false);
+ document.documentElement.removeEventListener('mouseleave', stopFn, false);
+ }
+
+ document.documentElement.addEventListener('mousemove', dragFn, false);
+ document.documentElement.addEventListener('mouseup', stopFn, false);
+ document.documentElement.addEventListener('mouseleave', stopFn, false);
+ }
+
+}
diff --git a/src/gantt-activity/gantt-activity.component.css b/src/gantt-activity/gantt-activity.component.css
new file mode 100644
index 0000000..0f3466d
--- /dev/null
+++ b/src/gantt-activity/gantt-activity.component.css
@@ -0,0 +1,110 @@
+/* You can add global styles to this file, and also import other style files */
+.gantt_activity {
+ /*overflow-x: hidden;*/
+ height: 250px;
+ overflow-y: hidden;
+ overflow-x: scroll;
+ display: inline-block;
+ vertical-align: top;
+ position: relative;
+}
+
+.gantt_activity_area {
+ position: relative;
+ overflow-x: hidden;
+ overflow-y: hidden;
+ -webkit-user-select: none;
+}
+
+.gantt_vertical_scroll {
+ background-color: transparent;
+ overflow-x: hidden;
+ overflow-y: scroll;
+ position: absolute;
+ right: 0;
+ display: block;
+ height: 283px;
+ width: 18px;
+ top: 70px;
+}
+
+.grid {
+ overflow-x: hidden;
+ overflow-y: hidden;
+ display: inline-block;
+ vertical-align: top;
+ border-right: 1px solid #cecece;
+}
+
+.grid_scale {
+ color: #6b6b6b;
+ font-size: 12px;
+ border-bottom: 1px solid #e0e0e0;
+ background-color: whitesmoke;
+}
+
+.grid_head_cell {
+ /*color: #a6a6a6;*/
+ border-top: none !important;
+ border-right: none !important;
+ line-height: inherit;
+ box-sizing: border-box;
+ display: inline-block;
+ vertical-align: top;
+ border-right: 1px solid #cecece;
+ /*text-align: center;*/
+ position: relative;
+ cursor: default;
+ height: 100%;
+ -moz-user-select: -moz-none;
+ -webkit-user-select: none;
+ overflow: hidden;
+}
+
+.grid_data {
+ overflow: hidden;
+}
+
+.grid_row {
+ box-sizing: border-box;
+ border-bottom: 1px solid #e0e0e0;
+ background-color: #fff;
+ position: relative;
+ -webkit-user-select: none;
+}
+
+.grid_row:hover {
+ background-color: #eeeeee;
+}
+
+.grid_cell {
+ border-right: none;
+ color: #454545;
+ display: inline-block;
+ vertical-align: top;
+ padding-left: 6px;
+ padding-right: 6px;
+ height: 100%;
+ overflow: hidden;
+ white-space: nowrap;
+ font-size: 13px;
+ box-sizing: border-box;
+}
+
+.actions_bar {
+ /*border-top: 1px solid #cecece;*/
+ border-bottom: 1px solid #e0e0e0;
+ clear: both;
+ /*margin-top: 90px;*/
+ height: 28px;
+ background: whitesmoke;
+ color: #494949;
+ font-family: Arial, sans-serif;
+ font-size: 13px;
+ padding-left: 15px;
+ line-height: 25px;
+}
+
+.gantt_tree_content {
+ padding-left: 15px;
+}
diff --git a/src/gantt-activity/gantt-activity.component.html b/src/gantt-activity/gantt-activity.component.html
new file mode 100644
index 0000000..5e378c6
--- /dev/null
+++ b/src/gantt-activity/gantt-activity.component.html
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ data.percentComplete }}
+
+
+
{{ ganttService.calculateDuration(data) }}
+
+
+
+
+
diff --git a/src/gantt-activity/gantt-activity.component.spec.ts b/src/gantt-activity/gantt-activity.component.spec.ts
new file mode 100644
index 0000000..f88a0f8
--- /dev/null
+++ b/src/gantt-activity/gantt-activity.component.spec.ts
@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { GanttActivityComponent } from './gantt-activity.component';
+
+describe('GanttActivityComponent', () => {
+ let component: GanttActivityComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ GanttActivityComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(GanttActivityComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should be created', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/gantt-activity/gantt-activity.component.ts b/src/gantt-activity/gantt-activity.component.ts
new file mode 100644
index 0000000..defc72b
--- /dev/null
+++ b/src/gantt-activity/gantt-activity.component.ts
@@ -0,0 +1,326 @@
+import {ChangeDetectionStrategy, Component, DoCheck, EventEmitter, Input, OnInit, Output} from '@angular/core';
+import {GanttService} from '../gantt.service';
+import {Zooming} from '../interfaces';
+
+
+@Component({
+ selector: 'app-gantt-activity',
+ templateUrl: './gantt-activity.component.html',
+ styleUrls: ['./gantt-activity.component.css'],
+ changeDetection: ChangeDetectionStrategy.Default
+})
+export class GanttActivityComponent implements OnInit, DoCheck {
+ @Input() project: any;
+ @Input() options: any;
+ @Output() onGridRowClick: EventEmitter = new EventEmitter();
+
+ upTriangle = '▲'; // BLACK UP-POINTING TRIANGLE
+ downTriangle = '▼'; // BLACK DOWN-POINTING TRIANGLE
+ zoom: EventEmitter = new EventEmitter();
+ activityActions = {
+ expanded: false,
+ expandedIcon: this.downTriangle
+ };
+
+ timeScale: any;
+
+ start: Date;
+ end: Date;
+ containerHeight: any;
+ containerWidth: any;
+
+ activityContainerSizes: any;
+ ganttActivityHeight: any;
+ ganttActivityWidth: any;
+ zoomLevel: string = Zooming[Zooming.hours];
+
+ treeExpanded = false;
+
+ scale: any = {
+ start: null,
+ end: null
+ };
+
+ dimensions = {
+ height: 0,
+ width: 0
+ };
+
+ data: any[] = [];
+
+ public gridColumns: any[] = [
+ {name: '', left: 0, width: 16},
+ {name: 'Task', left: 20, width: 330},
+ {name: '%', left: 8, width: 40},
+ {name: 'Duration', left: 14, width: 140}
+ ];
+
+ constructor(public ganttService: GanttService) {
+ }
+
+ ngOnInit() {
+ // Cache the project data and only work with that. Only show parent tasks by default
+ this.ganttService.TASK_CACHE = this.project.tasks.slice(0).filter((item: any) => {
+ return item.treePath.split('/').length === 1;
+ });
+ this.ganttService.TIME_SCALE = this.ganttService.calculateScale(this.options.scale.start, this.options.scale.end);
+
+ this.zoomLevel = this.options.zooming;
+ this.start = this.options.scale.start;
+ this.end = this.options.scale.end;
+ this.containerWidth = this.calculateContainerWidth();
+ this.containerHeight = this.calculateContainerHeight();
+ this.activityContainerSizes = this.ganttService.calculateActivityContainerDimensions();
+
+ // important that these are called last as it relies on values calculated above.
+ this.setScale();
+ this.setDimensions();
+ this.setSizes();
+
+ this.expand(); // default to expanded
+ }
+
+ /** Custom model check */
+ ngDoCheck() {
+ // 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?
+ const tasksAdded = this.ganttService.doTaskCheck(this.project.tasks, this.treeExpanded);
+
+ // only force expand if tasks are added and tree is already expanded
+ if (tasksAdded && this.activityActions.expanded) {
+ this.expand(true);
+ }
+ }
+
+ /** On vertical scroll set the scroll top of grid and activity */
+ onVerticalScroll(verticalScroll: any, ganttGrid: any, ganttActivityArea: any): void {
+ this.ganttService.scrollTop(verticalScroll, ganttGrid, ganttActivityArea);
+ }
+
+ /** Removes or adds children for given parent tasks back into DOM by updating TASK_CACHE */
+ toggleChildren(rowElem: any, task: any) {
+ try {
+ const isParent: boolean = 'true' === rowElem.getAttribute('data-isparent');
+ const parentId: string = rowElem.getAttribute('data-parentid').replace('_', ''); // remove id prefix
+ const children: any = document.querySelectorAll('[data-parentid=' + rowElem.getAttribute('data-parentid') + '][data-isparent=false]');
+
+ // use the task cache to allow deleting of items without polluting the project.tasks array
+ if (isParent) {
+ // remove children from the DOM as we don't want them if we are collapsing the parent
+ if (children.length > 0) {
+ const childrenIds: any[] = this.ganttService.TASK_CACHE.filter((task1: any) => {
+ return task1.parentId === parentId && task1.treePath.split('/').length > 1;
+ }).map((item: any) => item.id);
+
+ childrenIds.forEach((item: any) => {
+ const removedIndex = this.ganttService.TASK_CACHE.map((item1: any) => item1.id).indexOf(item);
+
+ this.ganttService.TASK_CACHE.splice(removedIndex, 1);
+ });
+
+ if (this.activityActions.expanded) {
+ this.expand(true);
+ }
+
+ } else {
+ // CHECK the project cache to see if this parent id has any children
+ // and if so push them back into array so DOM is updated
+ const childrenTasks: any[] = this.project.tasks.filter((task1: any) => {
+ return task1.parentId === parentId && task1.treePath.split('/').length > 1;
+ });
+
+ childrenTasks.forEach((task1: any) => {
+ this.ganttService.TASK_CACHE.push(task1);
+ });
+
+ if (this.activityActions.expanded) {
+ this.expand(true);
+ }
+ }
+ }
+
+ this.onGridRowClick.emit(task);
+
+ } catch (err) {
+ }
+ }
+
+ /** Removes or adds children tasks back into DOM by updating TASK_CACHE */
+ toggleAllChildren() {
+ try {
+ const children: any = document.querySelectorAll('[data-isparent=false]');
+ const childrenIds: string[] = Array.prototype.slice.call(children).map((item: any) => {
+ return item.getAttribute('data-id').replace('_', ''); // remove id prefix
+ });
+
+ // push all the children array items into cache
+ if (this.treeExpanded) {
+ if (children.length > 0) {
+ const childIds: string[] = this.ganttService.TASK_CACHE.filter((task: any) => {
+ return task.treePath.split('/').length > 1;
+ }).map((item: any) => item.id);
+
+ childIds.forEach((item: any) => {
+ const removedIndex = this.ganttService.TASK_CACHE.map((item1: any) => item1.id).indexOf(item);
+ this.ganttService.TASK_CACHE.splice(removedIndex, 1);
+ });
+ }
+
+ this.treeExpanded = false;
+
+ if (this.activityActions.expanded) {
+ this.expand(true);
+ }
+ } else {
+ // get all children tasks in project input
+ let childrenTasks: any[] = this.project.tasks.filter((task: any) => {
+ return task.treePath.split('/').length > 1;
+ });
+
+ if (children.length > 0) {
+ // filter out these children as they already exist in task cache
+ childrenTasks = childrenTasks.filter((task: any) => {
+ return childrenIds.indexOf(task.id) === -1;
+ });
+ }
+
+ childrenTasks.forEach((task: any) => {
+ this.ganttService.TASK_CACHE.push(task);
+ });
+
+ this.treeExpanded = true;
+
+ if (this.activityActions.expanded) {
+ this.expand(true);
+ }
+ }
+ } catch (err) {
+ }
+ }
+
+ /** On resize of browser window dynamically adjust gantt activity height and width */
+ onResize(event: any): void {
+ const activityContainerSizes = this.ganttService.calculateActivityContainerDimensions();
+ if (this.activityActions.expanded) {
+ this.ganttActivityHeight = this.ganttService.TASK_CACHE.length * this.ganttService.rowHeight + this.ganttService.rowHeight * 3 + 'px';
+ } else {
+ this.ganttActivityHeight = activityContainerSizes.height + 'px';
+ }
+
+ this.ganttActivityWidth = activityContainerSizes.width;
+ }
+
+ setScale() {
+ this.scale.start = this.start;
+ this.scale.end = this.end;
+ }
+
+ setDimensions() {
+ this.dimensions.height = this.containerHeight;
+ this.dimensions.width = this.containerWidth;
+ }
+
+ setGridRowStyle(isParent: boolean): any {
+ if (isParent) {
+ return {
+ 'height': this.ganttService.rowHeight + 'px',
+ 'line-height': this.ganttService.rowHeight + 'px',
+ 'font-weight': 'bold',
+ 'cursor': 'pointer'
+ };
+ }
+
+ return {
+ 'height': this.ganttService.rowHeight + 'px',
+ 'line-height': this.ganttService.rowHeight + 'px'
+ };
+ }
+
+ /** Set the zoom level e.g hours, days */
+ zoomTasks(level: string) {
+ this.zoomLevel = level;
+ this.zoom.emit(this.zoomLevel);
+ this.containerWidth = this.calculateContainerWidth();
+ this.setDimensions();
+ document.querySelector('.gantt_activity').scrollLeft = 0; // reset scroll left, replace with @ViewChild?
+ }
+
+ /** Expand the gantt grid and activity area height */
+ expand(force?: boolean): void {
+ const verticalScroll = document.querySelector('.gantt_vertical_scroll');
+ const ganttActivityHeight = `${this.ganttService.TASK_CACHE.length * this.ganttService.rowHeight + this.ganttService.rowHeight * 3}px`;
+
+ if (force && this.activityActions.expanded) {
+ this.ganttActivityHeight = ganttActivityHeight;
+ } else if (this.activityActions.expanded) {
+ this.activityActions.expanded = false;
+ this.activityActions.expandedIcon = this.downTriangle;
+ this.ganttActivityHeight = '300px';
+ } else {
+ verticalScroll.scrollTop = 0;
+
+ this.activityActions.expanded = true;
+ this.activityActions.expandedIcon = this.upTriangle;
+ this.ganttActivityHeight = ganttActivityHeight;
+ }
+ }
+
+ /** Get the status icon unicode string */
+ getStatusIcon(status: string, percentComplete: number): string {
+ const checkMarkIcon = '✔';
+ const upBlackPointer = '▲';
+ const crossMarkIcon = '✘';
+
+ if (status === 'Completed' || percentComplete === 100 && status !== 'Error') {
+ return checkMarkIcon;
+ } else if (status === 'Warning') {
+ return upBlackPointer;
+ } else if (status === 'Error') {
+ return crossMarkIcon;
+ }
+ return '';
+ }
+
+ /** Get the status icon color */
+ getStatusIconColor(status: string, percentComplete: number): string {
+ if (status === 'Completed' || percentComplete === 100 && status !== 'Error') {
+ return 'green';
+ } else if (status === 'Warning') {
+ return 'orange';
+ } else if (status === 'Error') {
+ return 'red';
+ }
+ return '';
+ }
+
+ private setGridScaleStyle() {
+ let height = this.ganttService.rowHeight;
+
+ if (this.zoomLevel === Zooming[Zooming.hours]) {
+ height *= 2;
+ }
+
+ return {
+ 'height': height + 'px',
+ 'line-height': height + 'px',
+ 'width': this.ganttService.gridWidth + 'px'
+ };
+ }
+
+ private calculateContainerHeight(): number {
+ return this.ganttService.TASK_CACHE.length * this.ganttService.rowHeight;
+ }
+
+ private calculateContainerWidth(): number {
+ if (this.zoomLevel === Zooming[Zooming.hours]) {
+ return this.ganttService.TIME_SCALE.length * this.ganttService.hourCellWidth * 24 + this.ganttService.hourCellWidth;
+ } else {
+ return this.ganttService.TIME_SCALE.length * this.ganttService.cellWidth + this.ganttService.cellWidth;
+ }
+ }
+
+ private setSizes(): void {
+ this.ganttActivityHeight = this.activityContainerSizes.height + 'px';
+ this.ganttActivityWidth = this.activityContainerSizes.width;
+ }
+
+}
diff --git a/src/gantt-config.service.ts b/src/gantt-config.service.ts
new file mode 100644
index 0000000..513d28a
--- /dev/null
+++ b/src/gantt-config.service.ts
@@ -0,0 +1,11 @@
+import { Injectable } from '@angular/core';
+
+@Injectable()
+export class GanttConfig {
+ public cellWidth: number = 76;
+ public rowHeight: number = 25;
+ public activityHeight: number = 300;
+ public barHeight = 20;
+ public barLineHeight = 20;
+ public barMoveable = false;
+}
diff --git a/src/gantt-footer/gantt-footer.component.css b/src/gantt-footer/gantt-footer.component.css
new file mode 100644
index 0000000..822dcdb
--- /dev/null
+++ b/src/gantt-footer/gantt-footer.component.css
@@ -0,0 +1,9 @@
+.gantt-footer {
+ background-color: whitesmoke;
+ height: 36px;
+ border-top: 1px solid #e0e0e0;
+}
+
+.gantt-footer-actions {
+ float:right;
+}
diff --git a/src/gantt-footer/gantt-footer.component.html b/src/gantt-footer/gantt-footer.component.html
new file mode 100644
index 0000000..d342b40
--- /dev/null
+++ b/src/gantt-footer/gantt-footer.component.html
@@ -0,0 +1 @@
+
diff --git a/src/gantt-footer/gantt-footer.component.spec.ts b/src/gantt-footer/gantt-footer.component.spec.ts
new file mode 100644
index 0000000..d74f3ef
--- /dev/null
+++ b/src/gantt-footer/gantt-footer.component.spec.ts
@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { GanttFooterComponent } from './gantt-footer.component';
+
+describe('GanttFooterComponent', () => {
+ let component: GanttFooterComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ GanttFooterComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(GanttFooterComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should be created', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/gantt-footer/gantt-footer.component.ts b/src/gantt-footer/gantt-footer.component.ts
new file mode 100644
index 0000000..e3cc787
--- /dev/null
+++ b/src/gantt-footer/gantt-footer.component.ts
@@ -0,0 +1,18 @@
+import {Component, Input, OnInit} from '@angular/core';
+
+@Component({
+ selector: 'app-gantt-footer',
+ templateUrl: './gantt-footer.component.html',
+ styleUrls: ['./gantt-footer.component.css']
+})
+export class GanttFooterComponent implements OnInit {
+
+ @Input() project: any;
+
+ constructor() {
+ }
+
+ ngOnInit() {
+ }
+
+}
diff --git a/src/gantt-header/gantt-header.component.css b/src/gantt-header/gantt-header.component.css
new file mode 100644
index 0000000..1d9ec19
--- /dev/null
+++ b/src/gantt-header/gantt-header.component.css
@@ -0,0 +1,19 @@
+.gantt-header {
+ background-color: whitesmoke;
+ height: 40px;
+ border-bottom: 1px solid #e0e0e0;
+}
+
+.gantt-header-title {
+ padding: 12px;
+ display: flex;
+ flex-wrap:wrap;
+ font-family: Arial, Helvetica, sans-serif;
+ font-size: 16px;
+}
+
+.gantt-header-actions {
+ display: inline;
+ float: right;
+ padding: 6px;
+}
diff --git a/src/gantt-header/gantt-header.component.html b/src/gantt-header/gantt-header.component.html
new file mode 100644
index 0000000..830bb34
--- /dev/null
+++ b/src/gantt-header/gantt-header.component.html
@@ -0,0 +1,6 @@
+
diff --git a/src/gantt-header/gantt-header.component.spec.ts b/src/gantt-header/gantt-header.component.spec.ts
new file mode 100644
index 0000000..9e311bc
--- /dev/null
+++ b/src/gantt-header/gantt-header.component.spec.ts
@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { GanttHeaderComponent } from './gantt-header.component';
+
+describe('GanttHeaderComponent', () => {
+ let component: GanttHeaderComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ GanttHeaderComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(GanttHeaderComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should be created', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/gantt-header/gantt-header.component.ts b/src/gantt-header/gantt-header.component.ts
new file mode 100644
index 0000000..8a5c357
--- /dev/null
+++ b/src/gantt-header/gantt-header.component.ts
@@ -0,0 +1,19 @@
+import {Component, Input, OnInit} from '@angular/core';
+
+@Component({
+ selector: 'app-gantt-header',
+ templateUrl: './gantt-header.component.html',
+ styleUrls: ['./gantt-header.component.css']
+})
+export class GanttHeaderComponent implements OnInit {
+
+ @Input() name: any;
+ @Input() startDate: Date;
+
+ constructor() {
+ }
+
+ ngOnInit() {
+ }
+
+}
diff --git a/src/gantt-time-scale/gantt-time-scale.component.css b/src/gantt-time-scale/gantt-time-scale.component.css
new file mode 100644
index 0000000..c8f19eb
--- /dev/null
+++ b/src/gantt-time-scale/gantt-time-scale.component.css
@@ -0,0 +1,26 @@
+.weekend {
+ background-color: whitesmoke;
+}
+
+.time_scale {
+ font-size: 12px;
+ border-bottom: 1px solid #cecece;
+ background-color: #fff;
+}
+
+.time_scale_line {
+ box-sizing: border-box;
+}
+
+.time_scale_line:first-child {
+ border-top: none;
+}
+
+.time_scale_cell {
+ display: inline-block;
+ white-space: nowrap;
+ overflow: hidden;
+ border-right: 1px solid #cecece;
+ text-align: center;
+ height: 100%;
+}
diff --git a/src/gantt-time-scale/gantt-time-scale.component.html b/src/gantt-time-scale/gantt-time-scale.component.html
new file mode 100644
index 0000000..8e9c650
--- /dev/null
+++ b/src/gantt-time-scale/gantt-time-scale.component.html
@@ -0,0 +1,10 @@
+
+
+
{{date | date: 'dd-MM-yyyy'}}
+
+
+
diff --git a/src/gantt-time-scale/gantt-time-scale.component.spec.ts b/src/gantt-time-scale/gantt-time-scale.component.spec.ts
new file mode 100644
index 0000000..30ebc13
--- /dev/null
+++ b/src/gantt-time-scale/gantt-time-scale.component.spec.ts
@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { GanttTimeScaleComponent } from './gantt-time-scale.component';
+
+describe('GanttTimeScaleComponent', () => {
+ let component: GanttTimeScaleComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ GanttTimeScaleComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(GanttTimeScaleComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should be created', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/gantt-time-scale/gantt-time-scale.component.ts b/src/gantt-time-scale/gantt-time-scale.component.ts
new file mode 100644
index 0000000..68e4339
--- /dev/null
+++ b/src/gantt-time-scale/gantt-time-scale.component.ts
@@ -0,0 +1,63 @@
+import {Component, Input, OnInit} from '@angular/core';
+import {Zooming} from '../interfaces';
+import {GanttService} from '../gantt.service';
+
+@Component({
+ selector: 'app-gantt-time-scale',
+ templateUrl: './gantt-time-scale.component.html',
+ styleUrls: ['./gantt-time-scale.component.css']
+})
+export class GanttTimeScaleComponent implements OnInit {
+
+ @Input() timeScale: any;
+ @Input() dimensions: any;
+ @Input() zoom: any;
+ @Input() zoomLevel: any;
+
+ constructor(public ganttService: GanttService) {
+ }
+
+ ngOnInit() {
+ this.zoom.subscribe((zoomLevel: string) => {
+ this.zoomLevel = zoomLevel;
+ });
+ }
+
+ private setTimescaleStyle() {
+ return {
+ 'width': this.dimensions.width + 'px'
+ };
+ }
+
+ private setTimescaleLineStyle(borderTop: string) {
+ return {
+ 'height': this.ganttService.rowHeight + 'px',
+ 'line-height': this.ganttService.rowHeight + 'px',
+ 'position': 'relative',
+ 'border-top': borderTop
+ };
+ }
+
+ private setTimescaleCellStyle() {
+ let width = this.ganttService.cellWidth;
+ const hoursInDay = 24;
+ const hourSeperatorPixels = 23; // we don't include the first
+
+ if (this.zoomLevel === Zooming[Zooming.hours]) {
+ width = this.ganttService.hourCellWidth * hoursInDay + hourSeperatorPixels;
+ }
+
+ return {
+ 'width': width + 'px'
+ };
+ }
+
+ private isDayWeekend(date: Date): boolean {
+ return this.ganttService.isDayWeekend(date);
+ }
+
+ private getHours(): string[] {
+ return this.ganttService.getHours(this.timeScale.length);
+ }
+
+}
diff --git a/src/gantt.module.ts b/src/gantt.module.ts
new file mode 100644
index 0000000..e276e4c
--- /dev/null
+++ b/src/gantt.module.ts
@@ -0,0 +1,33 @@
+import {NgModule} from '@angular/core';
+
+import {GanttActivityComponent} from './gantt-activity/gantt-activity.component';
+import {GroupByPipe} from './group-by.pipe';
+import {GanttComponent} from './gantt/gantt.component';
+import {GanttHeaderComponent} from './gantt-header/gantt-header.component';
+import {GanttFooterComponent} from './gantt-footer/gantt-footer.component';
+import {GanttActivityBackgroundComponent} from './gantt-activity-background/gantt-activity-background.component';
+import {GanttActivityBarsComponent} from './gantt-activity-bars/gantt-activity-bars.component';
+import {GanttTimeScaleComponent} from './gantt-time-scale/gantt-time-scale.component';
+import {CommonModule} from '@angular/common';
+
+@NgModule({
+ declarations: [
+ GanttComponent,
+ GanttActivityComponent,
+ GroupByPipe,
+ GanttComponent,
+ GanttHeaderComponent,
+ GanttFooterComponent,
+ GanttActivityBackgroundComponent,
+ GanttActivityBarsComponent,
+ GanttTimeScaleComponent
+ ],
+ imports: [
+ CommonModule,
+ ],
+ providers: [],
+ exports: [GanttComponent],
+ bootstrap: [GanttComponent]
+})
+export class GanttModule {
+}
diff --git a/src/gantt.service.spec.ts b/src/gantt.service.spec.ts
new file mode 100644
index 0000000..90887a7
--- /dev/null
+++ b/src/gantt.service.spec.ts
@@ -0,0 +1,15 @@
+import { TestBed, inject } from '@angular/core/testing';
+
+import { GanttService } from './gantt.service';
+
+describe('GanttService', () => {
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ providers: [GanttService]
+ });
+ });
+
+ it('should be created', inject([GanttService], (service: GanttService) => {
+ expect(service).toBeTruthy();
+ }));
+});
diff --git a/src/gantt.service.ts b/src/gantt.service.ts
new file mode 100644
index 0000000..26eba04
--- /dev/null
+++ b/src/gantt.service.ts
@@ -0,0 +1,528 @@
+import {Injectable} from '@angular/core';
+import {IBarStyle, IScale, Task} from './interfaces';
+import {GanttConfig} from './gantt-config.service';
+import {GroupByPipe} from './group-by.pipe';
+
+@Injectable()
+export class GanttService {
+
+ public rowHeight = 0;
+ public hourCellWidth = 60; // change to 60 so minutes can been seen more easily
+ public hoursCellWidth: number = this.hourCellWidth * 25;
+ public cellWidth = 0;
+ public windowInnerWidth = 0;
+ public activityHeight = 0;
+ public barHeight = 0;
+ public barLineHeight = 0;
+ public barTop = 0;
+ public barMoveable = false;
+ public gridWidth = 560;
+ private barStyles: IBarStyle[] = [
+ {status: 'information', backgroundColor: 'rgb(18,195, 244)', border: '1px solid #2196F3', progressBackgroundColor: '#2196F3'},
+ {status: 'warning', backgroundColor: '#FFA726', border: '1px solid #EF6C00', progressBackgroundColor: '#EF6C00'},
+ {status: 'error', backgroundColor: '#EF5350', border: '1px solid #C62828', progressBackgroundColor: '#C62828'},
+ {status: 'completed', backgroundColor: '#66BB6A', border: '1px solid #2E7D32', progressBackgroundColor: '#2E7D32'}
+ ];
+ public TASK_CACHE: any[];
+ public TIME_SCALE: any[];
+
+ constructor() {
+ const _ganttConfig = new GanttConfig();
+
+ this.rowHeight = _ganttConfig.rowHeight;
+ this.cellWidth = _ganttConfig.cellWidth;
+ this.activityHeight = _ganttConfig.activityHeight;
+ this.barHeight = _ganttConfig.barHeight;
+ this.barLineHeight = _ganttConfig.barLineHeight;
+ this.barTop = _ganttConfig.rowHeight;
+ this.barMoveable = _ganttConfig.barMoveable;
+ }
+
+ private calculateBarWidth(start: Date, end: Date, hours?: boolean): number {
+ if (typeof start === 'string') {
+ start = new Date(start);
+ }
+
+ if (typeof end === 'string') {
+ end = new Date(end);
+ }
+
+ const days = this.calculateDiffDays(start, end);
+ let width: number = days * this.cellWidth + days;
+
+ if (hours) {
+ width = days * this.hourCellWidth * 24 + days * 24;
+ }
+
+ return width;
+ }
+
+ private calculateBarLeft(start: Date, scale: any[], hours?: boolean): number {
+ let left = 0;
+ const hoursInDay = 24;
+
+ if (start != null) {
+ if (typeof start === 'string') {
+ start = new Date();
+ }
+
+ for (let i = 0; i < scale.length; i++) {
+ if (start.getDate() === scale[i].getDate()) {
+ if (hours) {
+ left = i * hoursInDay * this.hourCellWidth + hoursInDay * i + this.calculateBarLeftDelta(start, hours);
+ } else {
+ left = i * this.cellWidth + i + this.calculateBarLeftDelta(start, hours);
+ }
+ break;
+ }
+ }
+ }
+ return left;
+ }
+
+ /** Calculates the height of the gantt grid, activity and vertical scroll */
+ public calculateGanttHeight(): string {
+ return `${this.TASK_CACHE.length * this.rowHeight + this.rowHeight * 3}px`;
+ }
+
+ private calculateBarLeftDelta(start: Date, hours?: boolean): number {
+ let offset = 0;
+ const hoursInDay = 24;
+ const minutesInHour = 60;
+ const secondsInHour = 3600;
+ const startHours: number = start.getHours() + start.getMinutes() / minutesInHour + start.getSeconds() / secondsInHour;
+
+ if (hours) {
+ offset = this.hoursCellWidth / hoursInDay * startHours - startHours;
+ } else {
+ offset = this.cellWidth / hoursInDay * startHours;
+ }
+ return offset;
+ }
+
+ public isParent(treePath: string): boolean {
+
+ try {
+ const depth = treePath.split('/').length;
+
+ if (depth === 1) {
+ return true;
+ }
+ } catch (err) {
+ return false;
+ }
+ return false;
+ }
+
+ public isChild(treePath: string) {
+ if (this.isParent(treePath)) {
+ return '0px';
+ }
+ return '20px';
+ }
+
+ /** Calculate the bar styles */
+ public calculateBar(task: any, index: number, scale: any, hours?: boolean) {
+ const barStyle = this.getBarStyle(task.status);
+ return {
+ 'top': this.barTop * index + 2 + 'px',
+ 'left': this.calculateBarLeft(task.start, scale, hours) + 'px',
+ 'height': this.barHeight + 'px',
+ 'line-height': this.barLineHeight + 'px',
+ 'width': this.calculateBarWidth(task.start, task.end, hours) + 'px',
+ 'background-color': barStyle['background-color'],
+ 'border': barStyle['border']
+ };
+ }
+
+ /** Get the bar style based on task status */
+ private getBarStyle(taskStatus = ''): any {
+ const style = {};
+
+ try {
+ taskStatus = taskStatus.toLowerCase();
+ } catch (err) {
+ taskStatus = '';
+ }
+
+ switch (taskStatus) {
+ case 'information':
+ style['background-color'] = this.barStyles[0].backgroundColor;
+ style['border'] = this.barStyles[0].border;
+ break;
+ case 'warning':
+ style['background-color'] = this.barStyles[1].backgroundColor;
+ style['border'] = this.barStyles[1].border;
+ break;
+ case 'error':
+ style['background-color'] = this.barStyles[2].backgroundColor;
+ style['border'] = this.barStyles[2].border;
+ break;
+ case 'completed':
+ style['background-color'] = this.barStyles[3].backgroundColor;
+ style['border'] = this.barStyles[3].border;
+ break;
+ default:
+ style['background-color'] = 'rgb(18,195, 244)';
+ style['border'] = '1px solid #2196F3';
+ break;
+ }
+
+ return style;
+ }
+
+ /** Get the progresss bar background colour based on task status */
+ public getBarProgressStyle(taskStatus = ''): any {
+ const style = {};
+
+ try {
+ taskStatus = taskStatus.toLowerCase();
+ } catch (err) {
+ taskStatus = '';
+ }
+
+ switch (taskStatus) {
+ case 'information':
+ style['background-color'] = this.barStyles[0].progressBackgroundColor;
+ break;
+ case 'warning':
+ style['background-color'] = this.barStyles[1].progressBackgroundColor;
+ break;
+ case 'error':
+ style['background-color'] = this.barStyles[2].progressBackgroundColor;
+ break;
+ case 'completed':
+ style['background-color'] = this.barStyles[3].progressBackgroundColor;
+ break;
+ default:
+ style['background-color'] = this.barStyles[0].progressBackgroundColor;
+ break;
+ }
+
+ return style;
+ }
+
+ /** Calculates the bar progress width in pixels given task percent complete */
+ public calculateBarProgress(width: number, percent: number): string {
+ if (typeof percent === 'number') {
+ if (percent > 100) {
+ percent = 100;
+ }
+ const progress: number = (width / 100) * percent - 2;
+
+ return `${progress}px`;
+ }
+ return `${0}px`;
+ }
+
+ /** Calculates the difference in two dates and returns number of days */
+ public calculateDiffDays(start: Date, end: Date): number {
+ try {
+ const oneDay = 24 * 60 * 60 * 1000; // hours*minutes*seconds*milliseconds /ms
+ const diffDays = Math.abs((start.getTime() - end.getTime()) / (oneDay));
+ const days = diffDays; // don't use Math.round as it will draw an incorrect bar
+
+ return days;
+ } catch (err) {
+ return 0;
+ }
+ }
+
+ /** Calculates the difference in two dates and returns number of hours */
+ public calculateDuration(task: any): string {
+ try {
+ if (task.start != null && task.end != null) {
+ const oneHour = 60 * 60 * 1000;
+ const diffHours = (Math.abs((task.start.getTime() - task.end.getTime()) / oneHour));
+ const duration = diffHours;
+
+ if (duration > 24) {
+ return `${Math.round(duration / 24)} day(s)`; // duration in days
+ } else if (duration > 1) {
+ return `${Math.round(duration)} hr(s)`; // duration in hours
+ } else {
+ const minutes = duration * 60;
+
+ if (minutes < 1) {
+ return `${Math.round(minutes * 60)} second(s)`; // duration in seconds
+ }
+ return `${Math.round(minutes)} min(s)`; // duration in minutes
+ }
+ }
+
+ return '';
+ } catch (err) {
+ return '';
+ }
+ }
+
+ calculateTotalDuration(tasks: any[]): string {
+ try {
+ tasks = tasks.filter(t => t.parentId === t.id); // only calculate total duration with parent tasks
+
+ let totalHours = 0;
+ const oneHour = 60 * 60 * 1000;
+ for (let i = 0; i < tasks.length; i++) {
+ const start = tasks[i].start;
+ const end = tasks[i].end;
+
+ if (start != null && end != null) {
+ const duration = Math.abs(tasks[i].end.getTime() - tasks[i].start.getTime()) / oneHour; // duration in hours
+ totalHours += duration;
+ }
+ }
+
+ if (totalHours === 0) {
+ return '';
+ }
+
+ if (totalHours > 24) {
+ return `${Math.round(totalHours / 24)} day(s)`; // duration in days
+ } else if (totalHours > 1) {
+ return `${Math.round(totalHours)} hr(s)`; // duration in hours
+ } else {
+ const minutes = totalHours * 60;
+
+ if (minutes < 1) {
+ return `${Math.round(minutes * 60)} second(s)`; // duration in seconds
+ }
+ return `${Math.round(minutes)} min(s)`; // duration in minutes
+ }
+ } catch (err) {
+ return '';
+ }
+ }
+
+ /** Calculate the total percentage of a group of tasks */
+ calculateTotalPercentage(node: any): number {
+ let totalPercent = 0;
+ const children = node.children;
+
+ if (children.length > 0) {
+ children.forEach((child: any) => {
+ totalPercent += isNaN(child.percentComplete) ? 0 : child.percentComplete;
+ });
+
+ return Math.ceil(totalPercent / children.length);
+ } else {
+ return isNaN(node.percentComplete) ? 0 : node.percentComplete;
+ }
+ }
+
+ /** Calculate the total percent of the parent task */
+ calculateParentTotalPercentage(parent: any, tasks: any[]) {
+ const children = tasks.filter((task: any) => {
+ return task.parentId === parent.id && task.id !== parent.id;
+ }); // get only children tasks ignoring parent.
+
+ let totalPercent = 0;
+
+ if (children.length > 0) {
+ children.forEach((child: any) => {
+ totalPercent += isNaN(child.percentComplete) ? 0 : child.percentComplete;
+ });
+
+ return Math.ceil(totalPercent / children.length);
+ } else {
+ return isNaN(parent.percentComplete) ? 0 : parent.percentComplete;
+ }
+ }
+
+ /** Calculate the gantt scale range given the start and end date of tasks*/
+ public calculateScale(start: Date = new Date(), end: Date = this.addDays(start, 7)) {
+ const scale: any[] = [];
+
+ try {
+ while (start.getTime() <= end.getTime()) {
+ scale.push(start);
+ start = this.addDays(start, 1);
+ }
+ return scale;
+
+ } catch (err) {
+ return scale;
+ }
+ }
+
+ /** Determines whether given date is a weekend */
+ public isDayWeekend(date: Date): boolean {
+ const day = date.getDay();
+
+ if (day === 6 || day === 0) {
+ return true;
+ }
+ return false;
+ }
+
+ /** Add x number of days to a date object */
+ public addDays(date: Date, days: number): Date {
+ const result = new Date(date);
+ result.setDate(result.getDate() + days);
+ return result;
+ }
+
+ /** Remove x number of days from a date object */
+ public removeDays(date: Date, days: number): Date {
+ const result = new Date(date);
+ result.setDate(result.getDate() - days);
+ return result;
+ }
+
+ /** Calculates the grid scale for gantt based on tasks start and end dates */
+ public calculateGridScale(tasks: Task[]): IScale {
+ let start: Date;
+ let end: Date;
+ const dates = tasks.map((task: any) => {
+ return {
+ start: new Date(task.start),
+ end: new Date(task.end)
+ };
+ });
+
+ start = new Date(Math.min.apply(null, dates.map(function (t) {
+ return t.start;
+ })));
+
+ end = new Date(Math.max.apply(null, dates.map(function (t) {
+ return t.end;
+ })));
+
+ return {
+ start: start,
+ end: end
+ };
+ }
+
+ /** Create an hours array for use in time scale component */
+ public getHours(cols: number): string[] {
+ const hours: string[] = [];
+
+ while (hours.length <= cols * 24) {
+ for (let i = 0; i <= 23; i++) {
+ if (i < 10) {
+ hours.push('0' + i.toString());
+ } else {
+ hours.push(i.toString());
+ }
+ }
+ }
+
+ return hours;
+ }
+
+ public getComputedStyle(element: any, attribute: any) {
+ return parseInt(document.defaultView.getComputedStyle(element)[attribute], 10);
+ }
+
+ // TODO(dale): determine whether this is needed
+ public calculateContainerWidth(): number {
+ this.windowInnerWidth = window.innerWidth;
+ const containerWidth = (innerWidth - 18);
+
+ return containerWidth;
+ }
+
+ public calculateActivityContainerDimensions(): any {
+ const scrollWidth = 18;
+ this.windowInnerWidth = window.innerWidth;
+ const width = this.windowInnerWidth - this.gridWidth - scrollWidth;
+
+ return {height: this.activityHeight, width: width};
+ }
+
+ /** Set the vertical scroll top positions for gantt */
+ public scrollTop(verticalScrollElem: any, ganttGridElem: any, ganttActivityAreaElem: any) {
+ const verticalScrollTop = verticalScrollElem.scrollTop;
+ const scroll = this.setScrollTop;
+
+ // debounce
+ if (verticalScrollTop !== null && verticalScrollTop !== undefined) {
+ setTimeout(function () {
+ scroll(verticalScrollTop, ganttActivityAreaElem);
+ scroll(ganttActivityAreaElem.scrollTop, ganttGridElem);
+
+ }, 50);
+ }
+ }
+
+ /** Group data by id , only supports one level*/
+ public groupData(tasks: any): any {
+ const merged: any = [];
+ const groups: any = new GroupByPipe().transform(tasks, (task: any) => {
+ return [task.treePath.split('/')[0]];
+ });
+ return merged.concat.apply([], groups);
+ }
+
+ /** Create tree of data */
+ public transformData(input: any): any {
+ const output: any = [];
+ for (let i = 0; i < input.length; i++) {
+ const chain: any = input[i].id.split('/');
+ let currentNode: any = output;
+ for (let j = 0; j < chain.length; j++) {
+ const wantedNode: any = chain[j];
+ const lastNode: any = currentNode;
+ for (let k = 0; k < currentNode.length; k++) {
+ if (currentNode[k].name === wantedNode) {
+ currentNode = currentNode[k].children;
+ break;
+ }
+ }
+ }
+ }
+ return output;
+ }
+
+ /** Checks whether any new data needs to be added to task cache */
+ public doTaskCheck(tasks: any[], treeExpanded: boolean): boolean {
+ const cachedTaskIds = this.TASK_CACHE.map((task: any) => {
+ return task.id;
+ });
+ const itemsToCache: any[] = [];
+
+ if (treeExpanded) {
+ // push children and parent tasks that are not cached
+ tasks.filter((task: any) => {
+ return cachedTaskIds.indexOf(task.id) === -1;
+ }).forEach((task: any) => {
+ itemsToCache.push(task);
+ });
+ } else {
+ // only look at tasks that are not cached
+ tasks.filter((task: any) => {
+ return cachedTaskIds.indexOf(task.id) === -1 && task.treePath.split('/').length === 1;
+ }).forEach((task: any) => {
+ itemsToCache.push(task);
+ });
+ }
+
+ itemsToCache.forEach((item: any) => {
+ this.TASK_CACHE.push(item);
+ });
+
+ if (itemsToCache.length > 0) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /** Set a id prefix so CSS3 query selector can work with ids that contain numbers */
+ public setIdPrefix(id: string): string {
+ return `_${id}`;
+ }
+
+ // /** Remove the id prefix to allow querying of data */
+ // public removeIdPrefix(id: string): string {
+ // return id.substring(1, id.length - 1);
+ // }
+
+ /** Set the scroll top property of a native DOM element */
+ private setScrollTop(scrollTop: number, element: any): void {
+ if (element !== null && element !== undefined) {
+ element.scrollTop = scrollTop;
+ }
+ }
+
+}
diff --git a/src/gantt/gantt.component.css b/src/gantt/gantt.component.css
new file mode 100644
index 0000000..ecae734
--- /dev/null
+++ b/src/gantt/gantt.component.css
@@ -0,0 +1,8 @@
+.gantt_container {
+ font-family: Arial, serif;
+ font-size: 13px;
+ border: 1px solid #cecece;
+ position: relative;
+ white-space: nowrap;
+ margin-top: 0;
+}
diff --git a/src/gantt/gantt.component.html b/src/gantt/gantt.component.html
new file mode 100644
index 0000000..650a72c
--- /dev/null
+++ b/src/gantt/gantt.component.html
@@ -0,0 +1,7 @@
+
diff --git a/src/gantt/gantt.component.spec.ts b/src/gantt/gantt.component.spec.ts
new file mode 100644
index 0000000..61ef0fe
--- /dev/null
+++ b/src/gantt/gantt.component.spec.ts
@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { GanttComponent } from './gantt.component';
+
+describe('GanttComponent', () => {
+ let component: GanttComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ GanttComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(GanttComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should be created', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/gantt/gantt.component.ts b/src/gantt/gantt.component.ts
new file mode 100644
index 0000000..918d848
--- /dev/null
+++ b/src/gantt/gantt.component.ts
@@ -0,0 +1,81 @@
+import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
+import {IGanttOptions, Project} from '../interfaces';
+import {GanttService} from '../gantt.service';
+
+@Component({
+ selector: 'app-gantt',
+ templateUrl: './gantt.component.html',
+ styleUrls: ['./gantt.component.css']
+})
+export class GanttComponent implements OnInit {
+ _project: Project;
+ _options: IGanttOptions;
+
+ // TODO(dale): this may be causing an issue in the tree builder?
+ @Input()
+ set project(project: any) {
+ if (project) {
+ this._project = project;
+ } else {
+ this.setDefaultProject();
+ }
+ }
+
+ get project() {
+ return this._project;
+ }
+
+ @Input()
+ set options(options: any) {
+ if (options.scale) {
+ this._options = options;
+ } else {
+ this.setDefaultOptions();
+ }
+ }
+
+ get options() {
+ return this._options;
+ }
+
+ @Output() onGridRowClick: EventEmitter = new EventEmitter();
+
+ private ganttContainerWidth: number;
+
+ constructor(private ganttService: GanttService) {
+ }
+
+ ngOnInit() {
+
+ }
+
+ setSizes(): void {
+ this.ganttContainerWidth = this.ganttService.calculateContainerWidth();
+ }
+
+ setDefaultOptions() {
+ const scale = this.ganttService.calculateGridScale(this._project.tasks);
+
+ this._options = {
+ scale: scale
+ };
+ }
+
+ setDefaultProject() {
+ this._project = {
+ id: '',
+ name: '',
+ startDate: null,
+ tasks: []
+ };
+ }
+
+ gridRowClicked(task: any) {
+ this.onGridRowClick.emit(task);
+ }
+
+ onResize($event: any): void {
+ this.setSizes();
+ }
+
+}
diff --git a/src/group-by.pipe.spec.ts b/src/group-by.pipe.spec.ts
new file mode 100644
index 0000000..ebd3bd0
--- /dev/null
+++ b/src/group-by.pipe.spec.ts
@@ -0,0 +1,8 @@
+import { GroupByPipe } from './group-by.pipe';
+
+describe('GroupByPipe', () => {
+ it('create an instance', () => {
+ const pipe = new GroupByPipe();
+ expect(pipe).toBeTruthy();
+ });
+});
diff --git a/src/group-by.pipe.ts b/src/group-by.pipe.ts
new file mode 100644
index 0000000..cfcf313
--- /dev/null
+++ b/src/group-by.pipe.ts
@@ -0,0 +1,20 @@
+import {Pipe, PipeTransform} from '@angular/core';
+
+@Pipe({
+ name: 'groupBy'
+})
+export class GroupByPipe implements PipeTransform {
+
+ transform(value: any, args?: any): any {
+ const groups = {};
+ value.forEach((o: any) => {
+ const group = JSON.stringify(args(o));
+ groups[group] = groups[group] || [];
+ groups[group].push(o);
+ });
+ return Object.keys(groups).map((group: any) => {
+ return groups[group];
+ });
+ }
+
+}
diff --git a/src/index.ts b/src/index.ts
new file mode 100644
index 0000000..2034557
--- /dev/null
+++ b/src/index.ts
@@ -0,0 +1,53 @@
+import { NgModule, ModuleWithProviders } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import {GanttActivityComponent} from './gantt-activity/gantt-activity.component';
+import {GroupByPipe} from './group-by.pipe';
+import {GanttComponent} from './gantt/gantt.component';
+import {GanttHeaderComponent} from './gantt-header/gantt-header.component';
+import {GanttFooterComponent} from './gantt-footer/gantt-footer.component';
+import {GanttActivityBackgroundComponent} from './gantt-activity-background/gantt-activity-background.component';
+import {GanttActivityBarsComponent} from './gantt-activity-bars/gantt-activity-bars.component';
+import {GanttTimeScaleComponent} from './gantt-time-scale/gantt-time-scale.component';
+import { GanttService } from './gantt.service';
+
+export * from './gantt/gantt.component';;
+export * from './group-by.pipe';
+export * from './gantt.service';
+
+@NgModule({
+ imports: [
+ CommonModule
+ ],
+ declarations: [
+ GanttComponent,
+ GanttActivityComponent,
+ GanttComponent,
+ GanttHeaderComponent,
+ GanttFooterComponent,
+ GanttActivityBackgroundComponent,
+ GanttActivityBarsComponent,
+ GanttTimeScaleComponent,
+ GroupByPipe,
+ GanttService
+ ],
+ exports: [
+ GanttComponent,
+ GanttActivityComponent,
+ GanttComponent,
+ GanttHeaderComponent,
+ GanttFooterComponent,
+ GanttActivityBackgroundComponent,
+ GanttActivityBarsComponent,
+ GanttTimeScaleComponent,
+ GroupByPipe,
+ GanttService
+ ]
+})
+export class GanttModule {
+ static forRoot(): ModuleWithProviders {
+ return {
+ ngModule: GanttModule,
+ providers: [GanttService]
+ };
+ }
+}
diff --git a/src/interfaces.ts b/src/interfaces.ts
new file mode 100644
index 0000000..f243112
--- /dev/null
+++ b/src/interfaces.ts
@@ -0,0 +1,40 @@
+export interface Project {
+ id: string;
+ name: string;
+ startDate?: Date;
+ tasks: Task[]
+}
+
+export interface Task {
+ id: string;
+ treePath: string;
+ parentId: string;
+ name: string;
+ resource?: string;
+ start: Date;
+ end?: Date;
+ percentComplete?: number;
+ status?: string;
+}
+
+export interface IGanttOptions {
+ scale?: IScale;
+ zooming?: string;
+}
+
+export interface IScale {
+ start?: Date;
+ end?: Date;
+}
+
+export interface IBarStyle {
+ status: string;
+ backgroundColor: string;
+ border: string;
+ progressBackgroundColor: string;
+}
+
+export enum Zooming {
+ hours,
+ days
+}
\ No newline at end of file
diff --git a/src/package.json b/src/package.json
new file mode 100644
index 0000000..11de191
--- /dev/null
+++ b/src/package.json
@@ -0,0 +1,28 @@
+{
+ "name": "angular4-gantt",
+ "version": "0.1.0",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/jwiesmann/angular4-gantt"
+ },
+ "author": {
+ "name": "joerg.wiesmann",
+ "email": "joerg.wiesmann@gmail.com"
+ },
+ "keywords": [
+ "angular"
+ ],
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/jwiesmann/angular4-gantt/issues"
+ },
+ "main": "angular4-gantt.umd.js",
+ "module": "angular4-gantt.js",
+ "jsnext:main": "angular4-gantt.js",
+ "typings": "angular4-gantt.d.ts",
+ "peerDependencies": {
+ "@angular/core": "^4.0.0",
+ "rxjs": "^5.1.0",
+ "zone.js": "^0.8.4"
+ }
+}
diff --git a/src/tsconfig.es5.json b/src/tsconfig.es5.json
new file mode 100644
index 0000000..fffe094
--- /dev/null
+++ b/src/tsconfig.es5.json
@@ -0,0 +1,30 @@
+{
+ "compilerOptions": {
+ "declaration": true,
+ "module": "es2015",
+ "target": "es5",
+ "baseUrl": ".",
+ "stripInternal": true,
+ "emitDecoratorMetadata": true,
+ "experimentalDecorators": true,
+ "moduleResolution": "node",
+ "outDir": "../build",
+ "rootDir": ".",
+ "lib": [
+ "es2015",
+ "dom"
+ ],
+ "skipLibCheck": true,
+ "types": []
+ },
+ "angularCompilerOptions": {
+ "annotateForClosureCompiler": true,
+ "strictMetadataEmit": true,
+ "skipTemplateCodegen": true,
+ "flatModuleOutFile": "angular4-gantt.js",
+ "flatModuleId": "angular4-gantt"
+ },
+ "files": [
+ "./index.ts"
+ ]
+}
diff --git a/src/tsconfig.spec.json b/src/tsconfig.spec.json
new file mode 100644
index 0000000..e752544
--- /dev/null
+++ b/src/tsconfig.spec.json
@@ -0,0 +1,18 @@
+{
+ "extends": "./tsconfig.es5.json",
+ "compilerOptions": {
+ "emitDecoratorMetadata": true,
+ "experimentalDecorators": true,
+ "outDir": "../out-tsc/spec",
+ "module": "commonjs",
+ "target": "es6",
+ "baseUrl": "",
+ "types": [
+ "jest",
+ "node"
+ ]
+ },
+ "files": [
+ "**/*.spec.ts"
+ ]
+}
diff --git a/tools/gulp/inline-resources.js b/tools/gulp/inline-resources.js
new file mode 100644
index 0000000..9601ead
--- /dev/null
+++ b/tools/gulp/inline-resources.js
@@ -0,0 +1,156 @@
+/* eslint-disable */
+// https://github.com/filipesilva/angular-quickstart-lib/blob/master/inline-resources.js
+'use strict';
+
+const fs = require('fs');
+const path = require('path');
+const glob = require('glob');
+const sass = require('node-sass');
+const tildeImporter = require('node-sass-tilde-importer');
+
+/**
+ * Simple Promiseify function that takes a Node API and return a version that supports promises.
+ * We use promises instead of synchronized functions to make the process less I/O bound and
+ * faster. It also simplifies the code.
+ */
+function promiseify(fn) {
+ return function () {
+ const args = [].slice.call(arguments, 0);
+ return new Promise((resolve, reject) => {
+ fn.apply(this, args.concat([function (err, value) {
+ if (err) {
+ reject(err);
+ } else {
+ resolve(value);
+ }
+ }]));
+ });
+ };
+}
+
+const readFile = promiseify(fs.readFile);
+const writeFile = promiseify(fs.writeFile);
+
+/**
+ * Inline resources in a tsc/ngc compilation.
+ * @param projectPath {string} Path to the project.
+ */
+function inlineResources(projectPath) {
+
+ // Match only TypeScript files in projectPath.
+ const files = glob.sync('**/*.ts', {cwd: projectPath});
+
+ // For each file, inline the templates and styles under it and write the new file.
+ return Promise.all(files.map(filePath => {
+ const fullFilePath = path.join(projectPath, filePath);
+ return readFile(fullFilePath, 'utf-8')
+ .then(content => inlineResourcesFromString(content, url => {
+ // Resolve the template url.
+ return path.join(path.dirname(fullFilePath), url);
+ }))
+ .then(content => writeFile(fullFilePath, content))
+ .catch(err => {
+ console.error('An error occured: ', err);
+ });
+ }));
+}
+
+/**
+ * Inline resources from a string content.
+ * @param content {string} The source file's content.
+ * @param urlResolver {Function} A resolver that takes a URL and return a path.
+ * @returns {string} The content with resources inlined.
+ */
+function inlineResourcesFromString(content, urlResolver) {
+ // Curry through the inlining functions.
+ return [
+ inlineTemplate,
+ inlineStyle,
+ removeModuleId
+ ].reduce((content, fn) => fn(content, urlResolver), content);
+}
+
+/**
+ * Inline the templates for a source file. Simply search for instances of `templateUrl: ...` and
+ * replace with `template: ...` (with the content of the file included).
+ * @param content {string} The source file's content.
+ * @param urlResolver {Function} A resolver that takes a URL and return a path.
+ * @return {string} The content with all templates inlined.
+ */
+function inlineTemplate(content, urlResolver) {
+ return content.replace(/templateUrl:\s*'([^']+?\.html)'/g, function (m, templateUrl) {
+ const templateFile = urlResolver(templateUrl);
+ const templateContent = fs.readFileSync(templateFile, 'utf-8');
+ const shortenedTemplate = templateContent
+ .replace(/([\n\r]\s*)+/gm, ' ')
+ .replace(/"/g, '\\"');
+ return `template: "${shortenedTemplate}"`;
+ });
+}
+
+
+/**
+ * Inline the styles for a source file. Simply search for instances of `styleUrls: [...]` and
+ * replace with `styles: [...]` (with the content of the file included).
+ * @param urlResolver {Function} A resolver that takes a URL and return a path.
+ * @param content {string} The source file's content.
+ * @return {string} The content with all styles inlined.
+ */
+function inlineStyle(content, urlResolver) {
+ return content.replace(/styleUrls\s*:\s*(\[[\s\S]*?\])/gm, function (m, styleUrls) {
+ const urls = eval(styleUrls);
+ return 'styles: ['
+ + urls.map(styleUrl => {
+ const styleFile = urlResolver(styleUrl);
+ const originContent = fs.readFileSync(styleFile, 'utf-8');
+ const styleContent = styleFile.endsWith('.scss') ? buildSass(originContent, styleFile) : originContent;
+ const shortenedStyle = styleContent
+ .replace(/([\n\r]\s*)+/gm, ' ')
+ .replace(/"/g, '\\"');
+ return `"${shortenedStyle}"`;
+ })
+ .join(',\n')
+ + ']';
+ });
+}
+
+/**
+ * build sass content to css
+ * @param content {string} the css content
+ * @param sourceFile {string} the scss file sourceFile
+ * @return {string} the generated css, empty string if error occured
+ */
+function buildSass(content, sourceFile) {
+ try {
+ const result = sass.renderSync({
+ data: content,
+ file: sourceFile,
+ importer: tildeImporter
+ });
+ return result.css.toString()
+ } catch (e) {
+ console.error('\x1b[41m');
+ console.error('at ' + sourceFile + ':' + e.line + ":" + e.column);
+ console.error(e.formatted);
+ console.error('\x1b[0m');
+ return "";
+ }
+}
+
+/**
+ * Remove every mention of `moduleId: module.id`.
+ * @param content {string} The source file's content.
+ * @returns {string} The content with all moduleId: mentions removed.
+ */
+function removeModuleId(content) {
+ return content.replace(/\s*moduleId:\s*module\.id\s*,?\s*/gm, '');
+}
+
+module.exports = inlineResources;
+module.exports.inlineResourcesFromString = inlineResourcesFromString;
+
+// Run inlineResources if module is being called directly from the CLI with arguments.
+if (require.main === module && process.argv.length > 2) {
+ console.log('Inlining resources from project:', process.argv[2]);
+ return inlineResources(process.argv[2]);
+}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..5a61547
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,14 @@
+{
+ "compilerOptions": {
+ "baseUrl": "./src",
+ "experimentalDecorators": true,
+ "moduleResolution": "node",
+ "rootDir": "./src",
+ "lib": [
+ "es2015",
+ "dom"
+ ],
+ "skipLibCheck": true,
+ "types": []
+ }
+}
diff --git a/tslint.json b/tslint.json
new file mode 100644
index 0000000..f341e78
--- /dev/null
+++ b/tslint.json
@@ -0,0 +1,104 @@
+{
+ "rulesDirectory": [
+ "node_modules/codelyzer"
+ ],
+ "rules": {
+ "class-name": true,
+ "comment-format": [
+ true,
+ "check-space"
+ ],
+ "curly": true,
+ "eofline": true,
+ "forin": true,
+ "indent": [
+ true,
+ "spaces"
+ ],
+ "label-position": true,
+ "max-line-length": [
+ true,
+ 140
+ ],
+ "member-access": false,
+ "member-ordering": [
+ true,
+ "static-before-instance",
+ "variables-before-functions"
+ ],
+ "no-arg": true,
+ "no-bitwise": true,
+ "no-console": [
+ true,
+ "debug",
+ "info",
+ "time",
+ "timeEnd",
+ "trace"
+ ],
+ "no-construct": true,
+ "no-debugger": true,
+ "no-duplicate-variable": true,
+ "no-empty": false,
+ "no-eval": true,
+ "no-inferrable-types": true,
+ "no-shadowed-variable": true,
+ "no-string-literal": false,
+ "no-switch-case-fall-through": true,
+ "no-trailing-whitespace": true,
+ "no-unused-expression": true,
+ "no-unused-variable": true,
+ "no-use-before-declare": true,
+ "no-var-keyword": true,
+ "object-literal-sort-keys": false,
+ "one-line": [
+ true,
+ "check-open-brace",
+ "check-catch",
+ "check-else",
+ "check-whitespace"
+ ],
+ "quotemark": [
+ true,
+ "single"
+ ],
+ "radix": true,
+ "semicolon": [
+ "always"
+ ],
+ "triple-equals": [
+ true,
+ "allow-null-check"
+ ],
+ "typedef-whitespace": [
+ true,
+ {
+ "call-signature": "nospace",
+ "index-signature": "nospace",
+ "parameter": "nospace",
+ "property-declaration": "nospace",
+ "variable-declaration": "nospace"
+ }
+ ],
+ "variable-name": false,
+ "whitespace": [
+ true,
+ "check-branch",
+ "check-decl",
+ "check-operator",
+ "check-separator",
+ "check-type"
+ ],
+ "directive-selector": [true, "attribute", "", "camelCase"],
+ "component-selector": [true, "element", "", "kebab-case"],
+ "use-input-property-decorator": true,
+ "use-output-property-decorator": true,
+ "use-host-property-decorator": true,
+ "no-input-rename": true,
+ "no-output-rename": true,
+ "use-life-cycle-interface": true,
+ "use-pipe-transform-interface": true,
+ "component-class-suffix": true,
+ "directive-class-suffix": true
+ }
+}