├── .editorconfig ├── .gitignore ├── README.md ├── angular.json ├── karma.conf.js ├── package-lock.json ├── package.json ├── projects └── angular-calendar-timeline │ ├── README.md │ ├── karma.conf.js │ ├── ng-package.json │ ├── package.json │ ├── src │ ├── lib │ │ ├── components │ │ │ ├── date-marker │ │ │ │ ├── timeline-date-marker.component.html │ │ │ │ ├── timeline-date-marker.component.scss │ │ │ │ └── timeline-date-marker.component.ts │ │ │ ├── item │ │ │ │ ├── timeline-item.component.html │ │ │ │ ├── timeline-item.component.scss │ │ │ │ └── timeline-item.component.ts │ │ │ ├── panel │ │ │ │ ├── timeline-panel.component.html │ │ │ │ ├── timeline-panel.component.scss │ │ │ │ └── timeline-panel.component.ts │ │ │ └── scale-header │ │ │ │ ├── timeline-scale-header.component.html │ │ │ │ ├── timeline-scale-header.component.scss │ │ │ │ └── timeline-scale-header.component.ts │ │ ├── formatters │ │ │ ├── day-scale-formatter.ts │ │ │ ├── month-scale-formatter.ts │ │ │ └── week-scale-formatter.ts │ │ ├── helpers │ │ │ ├── cache.ts │ │ │ ├── date-helpers.ts │ │ │ └── row-determinant.ts │ │ ├── items-iterator │ │ │ └── items-iterator.ts │ │ ├── models │ │ │ ├── date-input.ts │ │ │ ├── events.ts │ │ │ ├── id-object.ts │ │ │ ├── index.ts │ │ │ ├── item.ts │ │ │ ├── items-iterator.ts │ │ │ ├── scale.ts │ │ │ ├── view-adapter.ts │ │ │ ├── zoom.ts │ │ │ └── zooms-builder.ts │ │ ├── scale-generator │ │ │ ├── base-scale-generator.ts │ │ │ ├── day-scale-generator.ts │ │ │ ├── month-scale-generator.ts │ │ │ └── week-scale-generator.ts │ │ ├── strategy-manager.ts │ │ ├── timeline.component.html │ │ ├── timeline.component.scss │ │ ├── timeline.component.ts │ │ ├── timeline.module.ts │ │ ├── view-mode-adaptor │ │ │ ├── base-view-mode-adaptor.ts │ │ │ ├── days-view-mode-adaptor.spec.ts │ │ │ ├── days-view-mode-adaptor.ts │ │ │ ├── months-view-mode-adaptor.spec.ts │ │ │ ├── months-view-mode-adaptor.ts │ │ │ ├── weeks-view-mode-adaptor.spec.ts │ │ │ └── weeks-view-mode-adaptor.ts │ │ └── zooms-handler │ │ │ ├── zooms-handler.ts │ │ │ └── zooms.ts │ ├── public-api.ts │ └── test.ts │ ├── tsconfig.lib.json │ ├── tsconfig.lib.prod.json │ └── tsconfig.spec.json ├── src ├── app │ ├── app.component.html │ ├── app.component.scss │ ├── app.component.ts │ ├── app.module.ts │ ├── custom-strategy.ts │ ├── portfolio.interface.ts │ └── timeline-zoom │ │ ├── timeline-zoom.component.html │ │ ├── timeline-zoom.component.scss │ │ └── timeline-zoom.component.ts ├── assets │ └── .gitkeep ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── main.ts ├── polyfills.ts ├── styles.scss └── test.ts ├── tsconfig.app.json ├── tsconfig.json └── tsconfig.spec.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | # Only exists if Bazel was run 8 | /bazel-out 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | # profiling files 14 | chrome-profiler-events*.json 15 | 16 | # IDEs and editors 17 | /.idea 18 | .project 19 | .classpath 20 | .c9/ 21 | *.launch 22 | .settings/ 23 | *.sublime-workspace 24 | 25 | # IDE - VSCode 26 | .vscode/* 27 | !.vscode/settings.json 28 | !.vscode/tasks.json 29 | !.vscode/launch.json 30 | !.vscode/extensions.json 31 | .history/* 32 | 33 | # misc 34 | /.angular/cache 35 | /.sass-cache 36 | /connect.lock 37 | /coverage 38 | /libpeerconnection.log 39 | npm-debug.log 40 | yarn-error.log 41 | testem.log 42 | /typings 43 | 44 | # System Files 45 | .DS_Store 46 | Thumbs.db 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | ![#1589F0](https://placehold.co/150x20/1589F0/1589F0.png)
4 | ![#c5f015](https://placehold.co/150x20/c5f015/c5f015.png) 5 | 6 |
7 | 8 |

Angular 13+ timeline calendar

9 | 10 |
11 | 12 | [![Sponsorship](https://img.shields.io/badge/funding-github-%23EA4AAA)](https://github.com/oOps1627) 13 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 14 | 15 | 16 | 17 |
18 | 19 |

Demo

20 | 21 |
22 | 23 | https://codesandbox.io/s/tender-cerf-zk0ewt 24 | 25 |
26 | 27 |

About

28 | 29 | A timeline for angular 13+ that shows tasks or events on a timeline in different modes: days, weeks, and 30 | months. 31 | 32 | This library is pretty small and DOESN'T use big dependencies like JQuery or Moment.js. 33 | Library also supports SSR. 34 | 35 |

Getting started

36 | 37 | Install through npm: 38 | 39 | ```bash 40 | npm install --save angular-calendar-timeline 41 | ``` 42 | 43 | Then import the timeline module into the module where you want to use the timeline. 44 | 45 | Don't forget to call forChild() method: 46 | 47 | ```typescript 48 | import { NgModule } from '@angular/core'; 49 | import { TimelineModule } from 'angular-timeline-calendar'; 50 | 51 | @NgModule({ 52 | imports: [ 53 | TimelineModule.forChild(), 54 | ], 55 | }) 56 | export class MyModule { 57 | } 58 | ``` 59 | 60 | That's it, you can then use it in your component as: 61 | 62 | ```typescript 63 | import { ITimelineItem } from "angular-timeline-calendar"; 64 | 65 | @Component({ 66 | template: ` ` 67 | }) 68 | export class MyTimelineComponent implements AfterViewInit { 69 | items: ITimelineItem[] = []; 70 | } 71 | ``` 72 | 73 |

Customization

74 | 75 |

1. Localization

76 | 77 | Change localization is very simple, just pass locale code to the 'locale' component input. 78 | Make sure that the current locale is registered by Angular: 79 | 80 | ```typescript 81 | import localeUk from "@angular/common/locales/uk"; 82 | 83 | registerLocaleData(localeUk); 84 | 85 | @Component({ 86 | template: `` 87 | }) 88 | ``` 89 | 90 |

2. Header

91 | 92 | You can customize the scale view by providing a config for each view mode. 93 | In case you need to change the format of the dates in the header or change start or end dates, you can provide a config for each view mode: 94 | 95 | ```typescript 96 | import { NgModule } from '@angular/core'; 97 | import { TimelineModule, 98 | IScaleFormatter, 99 | IScaleGeneratorConfig, 100 | IItemsIterator } from 'angular-timeline-calendar'; 101 | 102 | const myCustomFormatter: IScaleFormatter = { 103 | formatColumn(column: IScaleColumn, columnWidth: number, locale: string): string { 104 | return column.date.toString(); 105 | } 106 | } 107 | 108 | @NgModule({ 109 | imports: [ 110 | TimelineModule.forChild({ 111 | // Customization dates range and their format in the header for day view mode. 112 | dayScaleConfig: { 113 | formatter: myCustomFormatter, 114 | getStartDate: (itemsIterator: IItemsIterator) => itemsIterator.getFirstItem(true).startDate, 115 | getEndDate: (itemsIterator: IItemsIterator) => new Date(), 116 | } as Partial, 117 | // Customization dates format in the header for week view mode. 118 | weekScaleConfig: { 119 | formatter: myCustomFormatter 120 | } as Partial 121 | }), 122 | ], 123 | }) 124 | export class MyModule { 125 | } 126 | ``` 127 | 128 |

3. Zooms

129 | 130 | You can simply set your own zooms if you want to add more. 131 | For changing the current zoom use TimelineComponent API. Here is an example: 132 | 133 | ```typescript 134 | import { AfterViewInit, ViewChild } from "@angular/core"; 135 | import { TimelineComponent, 136 | ITimelineZoom, 137 | DefaultZooms, 138 | TimelineViewMode } from "angular-timeline-calendar"; 139 | 140 | @Component({ 141 | template: `` 142 | }) 143 | export class MyTimelineComponent implements AfterViewInit { 144 | @ViewChild('timeline') timeline: TimelineComponent; 145 | 146 | zooms: ITimelineZoom[] = [ 147 | {columnWidth: 50, viewMode: TimelineViewMode.Month}, 148 | {columnWidth: 55, viewMode: TimelineViewMode.Month}, 149 | {columnWidth: 50, viewMode: TimelineViewMode.Days}, 150 | DefaultZooms[0] // You can import and use default array; 151 | ]; 152 | 153 | ngAfterViewInit(): void { 154 | // Change current zoom to any of passed to 'zooms' @Input. 155 | this.timeline.changeZoom(this.timeline.zooms[1]); 156 | 157 | // Change current zoom by one step next. 158 | this.timeline.zoomIn(); 159 | 160 | // Change current zoom by one step back. 161 | this.timeline.zoomOut(); 162 | } 163 | } 164 | ``` 165 | 166 | This is not all API of component. Maybe later I will add some documentation. Currently, you can see comments inside 167 | TimelineComponent. 168 | 169 |

4. Templates

170 | 171 | You easily can customize timeline items view, date marker, and left panel by passing custom TemplateRef: 172 | 173 | ```html 174 | 178 | 179 | 180 | {{item.name}} {{item.startDate}} {{item.endDate}} {{locale}} 181 | 182 | 183 | 184 | dateMarkerTemplate 185 | 186 | ``` 187 | 188 |

5. View modes

189 | 190 | The library allows you to add custom view modes, for example, years, hours, minutes, etc. All you have to do is extends StrategyManager 191 | class. 192 | Based on the current view type it returns different strategies with a common interface which needs for calculating operations between dates and generating scale. 193 | 194 | 195 | Here is an example of how it should look, when you want to add some additional view modes: 196 | 197 | ```typescript 198 | import { NgModule } from '@angular/core'; 199 | import { 200 | TimelineModule, 201 | TimelineViewMode, 202 | IScaleFormatter, 203 | IStrategyManager, 204 | StrategyManager, 205 | } from 'angular-timeline-calendar'; 206 | 207 | enum CustomViewMode { 208 | CustomView = 1, 209 | Day = TimelineViewMode.Day, 210 | Week = TimelineViewMode.Week, 211 | Month = TimelineViewMode.Month, 212 | } 213 | 214 | class CustomStrategyManager extends StrategyManager implements IStrategyManager { 215 | getScaleGenerator(viewMode): IScaleGenerator { 216 | if (viewMode === CustomViewMode.Custom) { 217 | return {...}; // your custom logic here 218 | } 219 | 220 | return super.getScaleGenerator(viewMode); 221 | }; 222 | 223 | getViewModeAdaptor(viewMode): IViewModeAdaptor { 224 | if (viewMode === CustomViewMode.Custom) { 225 | return {...} // custom adaptor; 226 | } 227 | 228 | return super.getViewModeAdaptor(viewMode); 229 | } 230 | } 231 | 232 | @NgModule({ 233 | imports: [ 234 | TimelineModule.forChild({ 235 | strategyManager: StrategyManager, 236 | }), 237 | ], 238 | providers: [{ 239 | provide: StrategyManager, 240 | useClass: CustomStrategyManager, 241 | }], 242 | }) 243 | export class MyModule { 244 | } 245 | ``` 246 | 247 | 248 |

Angular versions

249 | 250 |
  • For Angular = 13 use 0.4
  • 251 |
  • For Angular >= 14 use 0.5
  • 252 |
  • For Angular = 16 use 0.6
  • 253 |
  • For Angular = 17 use 0.7
  • 254 |
  • For Angular > 18 use 0.8
  • 255 | 256 |
    257 | 258 | Have an issue? Leave it here: https://github.com/oOps1627/angular-calendar-timeline/issues 259 | 260 | You can support me by donation: 261 | * https://www.paypal.com/donate/?hosted_button_id=38ZN57VTQ9TQC 262 | * https://buymeacoffee.com/andriy1627 263 | 264 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "calendar-timeline": { 7 | "projectType": "application", 8 | "schematics": { 9 | "@schematics/angular:component": { 10 | "style": "scss" 11 | }, 12 | "@schematics/angular:application": { 13 | "strict": true 14 | } 15 | }, 16 | "root": "", 17 | "sourceRoot": "src", 18 | "prefix": "app", 19 | "architect": { 20 | "build": { 21 | "builder": "@angular-devkit/build-angular:browser", 22 | "options": { 23 | "outputPath": "dist/calendar-timeline", 24 | "index": "src/index.html", 25 | "main": "src/main.ts", 26 | "polyfills": "src/polyfills.ts", 27 | "tsConfig": "tsconfig.app.json", 28 | "inlineStyleLanguage": "scss", 29 | "assets": [ 30 | "src/favicon.ico", 31 | "src/assets" 32 | ], 33 | "styles": [ 34 | "src/styles.scss" 35 | ], 36 | "scripts": [] 37 | }, 38 | "configurations": { 39 | "production": { 40 | "budgets": [ 41 | { 42 | "type": "initial", 43 | "maximumWarning": "500kb", 44 | "maximumError": "1mb" 45 | }, 46 | { 47 | "type": "anyComponentStyle", 48 | "maximumWarning": "2kb", 49 | "maximumError": "4kb" 50 | } 51 | ], 52 | "fileReplacements": [ 53 | { 54 | "replace": "src/environments/environment.ts", 55 | "with": "src/environments/environment.prod.ts" 56 | } 57 | ], 58 | "outputHashing": "all" 59 | }, 60 | "development": { 61 | "buildOptimizer": false, 62 | "optimization": false, 63 | "vendorChunk": true, 64 | "extractLicenses": false, 65 | "sourceMap": true, 66 | "namedChunks": true 67 | } 68 | }, 69 | "defaultConfiguration": "production" 70 | }, 71 | "serve": { 72 | "builder": "@angular-devkit/build-angular:dev-server", 73 | "configurations": { 74 | "production": { 75 | "buildTarget": "calendar-timeline:build:production" 76 | }, 77 | "development": { 78 | "buildTarget": "calendar-timeline:build:development" 79 | } 80 | }, 81 | "defaultConfiguration": "development" 82 | }, 83 | "extract-i18n": { 84 | "builder": "@angular-devkit/build-angular:extract-i18n", 85 | "options": { 86 | "buildTarget": "calendar-timeline:build" 87 | } 88 | }, 89 | "test": { 90 | "builder": "@angular-devkit/build-angular:karma", 91 | "options": { 92 | "main": "src/test.ts", 93 | "polyfills": "src/polyfills.ts", 94 | "tsConfig": "tsconfig.spec.json", 95 | "karmaConfig": "karma.conf.js", 96 | "inlineStyleLanguage": "scss", 97 | "assets": [ 98 | "src/favicon.ico", 99 | "src/assets" 100 | ], 101 | "styles": [ 102 | "src/styles.scss" 103 | ], 104 | "scripts": [] 105 | } 106 | } 107 | } 108 | }, 109 | "angular-calendar-timeline": { 110 | "projectType": "library", 111 | "root": "projects/angular-calendar-timeline", 112 | "sourceRoot": "projects/angular-calendar-timeline/src", 113 | "prefix": "lib", 114 | "architect": { 115 | "build": { 116 | "builder": "@angular-devkit/build-angular:ng-packagr", 117 | "options": { 118 | "project": "projects/angular-calendar-timeline/ng-package.json" 119 | }, 120 | "configurations": { 121 | "production": { 122 | "tsConfig": "projects/angular-calendar-timeline/tsconfig.lib.prod.json" 123 | }, 124 | "development": { 125 | "tsConfig": "projects/angular-calendar-timeline/tsconfig.lib.json" 126 | } 127 | }, 128 | "defaultConfiguration": "production" 129 | }, 130 | "test": { 131 | "builder": "@angular-devkit/build-angular:karma", 132 | "options": { 133 | "main": "projects/angular-calendar-timeline/src/test.ts", 134 | "tsConfig": "projects/angular-calendar-timeline/tsconfig.spec.json", 135 | "karmaConfig": "projects/angular-calendar-timeline/karma.conf.js" 136 | } 137 | } 138 | } 139 | } 140 | }, 141 | "cli": { 142 | "analytics": "d47c56b0-4c70-43c0-ba49-77e0436bf5a9" 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | jasmine: { 17 | // you can add configuration options for Jasmine here 18 | // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html 19 | // for example, you can disable the random execution with `random: false` 20 | // or set a specific seed with `seed: 4321` 21 | }, 22 | clearContext: false // leave Jasmine Spec Runner output visible in browser 23 | }, 24 | jasmineHtmlReporter: { 25 | suppressAll: true // removes the duplicated traces 26 | }, 27 | coverageReporter: { 28 | dir: require('path').join(__dirname, './coverage/calendar-timeline'), 29 | subdir: '.', 30 | reporters: [ 31 | { type: 'html' }, 32 | { type: 'text-summary' } 33 | ] 34 | }, 35 | reporters: ['progress', 'kjhtml'], 36 | port: 9876, 37 | colors: true, 38 | logLevel: config.LOG_INFO, 39 | autoWatch: true, 40 | browsers: ['Chrome'], 41 | singleRun: false, 42 | restartOnFileChange: true 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-calendar-timeline", 3 | "version": "0.8.0", 4 | "description": "A timeline for angular that shows events on a timeline board in different modes: days, weeks, and months.", 5 | "author": "Andrii Krashivskyi", 6 | "license": "MIT", 7 | "homepage": "https://github.com/oOps1627/angular-calendar-timeline#readme", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/oOps1627/angular-calendar-timeline" 11 | }, 12 | "keywords": [ 13 | "angular", 14 | "angular2", 15 | "timeline", 16 | "calendar", 17 | "schedule", 18 | "scheduler", 19 | "events", 20 | "board", 21 | "events-schedule", 22 | "events-board" 23 | ], 24 | "scripts": { 25 | "ng": "ng", 26 | "start": "ng serve", 27 | "build": "ng build", 28 | "watch": "ng build --watch --configuration development", 29 | "test": "ng test", 30 | "build:copy-package-json": "copyfiles README.md package.json projects/angular-calendar-timeline", 31 | "build:lib": "ng build angular-calendar-timeline --configuration production", 32 | "release:npm-publish": "npm publish ./dist/angular-calendar-timeline" 33 | }, 34 | "private": false, 35 | "dependencies": { 36 | "angular-draggable-droppable": "6.0.0", 37 | "angular-resizable-element": "5.0.0", 38 | "tslib": "^2.3.0" 39 | }, 40 | "peerDependencies": {}, 41 | "devDependencies": { 42 | "rxjs": "~7.4.0", 43 | "zone.js": "~0.14.10", 44 | "@angular/animations": "^18.2.13", 45 | "@angular/cdk": "^18.2.14", 46 | "@angular/common": "^18.2.13", 47 | "@angular/compiler": "^18.2.13", 48 | "@angular/core": "^18.2.13", 49 | "@angular/forms": "^18.2.13", 50 | "@angular/platform-browser": "^18.2.13", 51 | "@angular/platform-browser-dynamic": "^18.2.13", 52 | "@angular/router": "^18.2.13", 53 | "@angular-devkit/build-angular": "^18.2.16", 54 | "@angular/cli": "^18.2.16", 55 | "@angular/compiler-cli": "^18.2.13", 56 | "@types/jasmine": "~3.10.0", 57 | "@types/node": "^12.11.1", 58 | "jasmine-core": "~3.10.0", 59 | "karma": "~6.3.0", 60 | "karma-chrome-launcher": "~3.1.0", 61 | "karma-coverage": "~2.0.3", 62 | "karma-jasmine": "~4.0.0", 63 | "karma-jasmine-html-reporter": "~1.7.0", 64 | "ng-packagr": "^18.2.1", 65 | "typescript": "~5.4.5", 66 | "copyfiles": "^2.4.1" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /projects/angular-calendar-timeline/README.md: -------------------------------------------------------------------------------- 1 |
    2 | 3 | ![#1589F0](https://placehold.co/150x20/1589F0/1589F0.png)
    4 | ![#c5f015](https://placehold.co/150x20/c5f015/c5f015.png) 5 | 6 |
    7 | 8 |

    Angular 13+ timeline calendar

    9 | 10 |
    11 | 12 | [![Sponsorship](https://img.shields.io/badge/funding-github-%23EA4AAA)](https://github.com/oOps1627) 13 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 14 | 15 | 16 | 17 |
    18 | 19 |

    Demo

    20 | 21 |
    22 | 23 | https://codesandbox.io/s/tender-cerf-zk0ewt 24 | 25 |
    26 | 27 |

    About

    28 | 29 | A timeline for angular 13+ that shows tasks or events on a timeline in different modes: days, weeks, and 30 | months. 31 | 32 | This library is pretty small and DOESN'T use big dependencies like JQuery or Moment.js. 33 | Library also supports SSR. 34 | 35 |

    Getting started

    36 | 37 | Install through npm: 38 | 39 | ```bash 40 | npm install --save angular-calendar-timeline 41 | ``` 42 | 43 | Then import the timeline module into the module where you want to use the timeline. 44 | 45 | Don't forget to call forChild() method: 46 | 47 | ```typescript 48 | import { NgModule } from '@angular/core'; 49 | import { TimelineModule } from 'angular-timeline-calendar'; 50 | 51 | @NgModule({ 52 | imports: [ 53 | TimelineModule.forChild(), 54 | ], 55 | }) 56 | export class MyModule { 57 | } 58 | ``` 59 | 60 | That's it, you can then use it in your component as: 61 | 62 | ```typescript 63 | import { ITimelineItem } from "angular-timeline-calendar"; 64 | 65 | @Component({ 66 | template: ` ` 67 | }) 68 | export class MyTimelineComponent implements AfterViewInit { 69 | items: ITimelineItem[] = []; 70 | } 71 | ``` 72 | 73 |

    Customization

    74 | 75 |

    1. Localization

    76 | 77 | Change localization is very simple, just pass locale code to the 'locale' component input. 78 | Make sure that the current locale is registered by Angular: 79 | 80 | ```typescript 81 | import localeUk from "@angular/common/locales/uk"; 82 | 83 | registerLocaleData(localeUk); 84 | 85 | @Component({ 86 | template: `` 87 | }) 88 | ``` 89 | 90 |

    2. Header

    91 | 92 | You can customize the scale view by providing a config for each view mode. 93 | In case you need to change the format of the dates in the header or change start or end dates, you can provide a config for each view mode: 94 | 95 | ```typescript 96 | import { NgModule } from '@angular/core'; 97 | import { TimelineModule, 98 | IScaleFormatter, 99 | IScaleGeneratorConfig, 100 | IItemsIterator } from 'angular-timeline-calendar'; 101 | 102 | const myCustomFormatter: IScaleFormatter = { 103 | formatColumn(column: IScaleColumn, columnWidth: number, locale: string): string { 104 | return column.date.toString(); 105 | } 106 | } 107 | 108 | @NgModule({ 109 | imports: [ 110 | TimelineModule.forChild({ 111 | // Customization dates range and their format in the header for day view mode. 112 | dayScaleConfig: { 113 | formatter: myCustomFormatter, 114 | getStartDate: (itemsIterator: IItemsIterator) => itemsIterator.getFirstItem(true).startDate, 115 | getEndDate: (itemsIterator: IItemsIterator) => new Date(), 116 | } as Partial, 117 | // Customization dates format in the header for week view mode. 118 | weekScaleConfig: { 119 | formatter: myCustomFormatter 120 | } as Partial 121 | }), 122 | ], 123 | }) 124 | export class MyModule { 125 | } 126 | ``` 127 | 128 |

    3. Zooms

    129 | 130 | You can simply set your own zooms if you want to add more. 131 | For changing the current zoom use TimelineComponent API. Here is an example: 132 | 133 | ```typescript 134 | import { AfterViewInit, ViewChild } from "@angular/core"; 135 | import { TimelineComponent, 136 | ITimelineZoom, 137 | DefaultZooms, 138 | TimelineViewMode } from "angular-timeline-calendar"; 139 | 140 | @Component({ 141 | template: `` 142 | }) 143 | export class MyTimelineComponent implements AfterViewInit { 144 | @ViewChild('timeline') timeline: TimelineComponent; 145 | 146 | zooms: ITimelineZoom[] = [ 147 | {columnWidth: 50, viewMode: TimelineViewMode.Month}, 148 | {columnWidth: 55, viewMode: TimelineViewMode.Month}, 149 | {columnWidth: 50, viewMode: TimelineViewMode.Days}, 150 | DefaultZooms[0] // You can import and use default array; 151 | ]; 152 | 153 | ngAfterViewInit(): void { 154 | // Change current zoom to any of passed to 'zooms' @Input. 155 | this.timeline.changeZoom(this.timeline.zooms[1]); 156 | 157 | // Change current zoom by one step next. 158 | this.timeline.zoomIn(); 159 | 160 | // Change current zoom by one step back. 161 | this.timeline.zoomOut(); 162 | } 163 | } 164 | ``` 165 | 166 | This is not all API of component. Maybe later I will add some documentation. Currently, you can see comments inside 167 | TimelineComponent. 168 | 169 |

    4. Templates

    170 | 171 | You easily can customize timeline items view, date marker, and left panel by passing custom TemplateRef: 172 | 173 | ```html 174 | 178 | 179 | 180 | {{item.name}} {{item.startDate}} {{item.endDate}} {{locale}} 181 | 182 | 183 | 184 | dateMarkerTemplate 185 | 186 | ``` 187 | 188 |

    5. View modes

    189 | 190 | The library allows you to add custom view modes, for example, years, hours, minutes, etc. All you have to do is extends StrategyManager 191 | class. 192 | Based on the current view type it returns different strategies with a common interface which needs for calculating operations between dates and generating scale. 193 | 194 | 195 | Here is an example of how it should look, when you want to add some additional view modes: 196 | 197 | ```typescript 198 | import { NgModule } from '@angular/core'; 199 | import { 200 | TimelineModule, 201 | TimelineViewMode, 202 | IScaleFormatter, 203 | IStrategyManager, 204 | StrategyManager, 205 | } from 'angular-timeline-calendar'; 206 | 207 | enum CustomViewMode { 208 | CustomView = 1, 209 | Day = TimelineViewMode.Day, 210 | Week = TimelineViewMode.Week, 211 | Month = TimelineViewMode.Month, 212 | } 213 | 214 | class CustomStrategyManager extends StrategyManager implements IStrategyManager { 215 | getScaleGenerator(viewMode): IScaleGenerator { 216 | if (viewMode === CustomViewMode.Custom) { 217 | return {...}; // your custom logic here 218 | } 219 | 220 | return super.getScaleGenerator(viewMode); 221 | }; 222 | 223 | getViewModeAdaptor(viewMode): IViewModeAdaptor { 224 | if (viewMode === CustomViewMode.Custom) { 225 | return {...} // custom adaptor; 226 | } 227 | 228 | return super.getViewModeAdaptor(viewMode); 229 | } 230 | } 231 | 232 | @NgModule({ 233 | imports: [ 234 | TimelineModule.forChild({ 235 | strategyManager: StrategyManager, 236 | }), 237 | ], 238 | providers: [{ 239 | provide: StrategyManager, 240 | useClass: CustomStrategyManager, 241 | }], 242 | }) 243 | export class MyModule { 244 | } 245 | ``` 246 | 247 | 248 |

    Angular versions

    249 | 250 |
  • For Angular = 13 use 0.4
  • 251 |
  • For Angular >= 14 use 0.5
  • 252 |
  • For Angular = 16 use 0.6
  • 253 |
  • For Angular = 17 use 0.7
  • 254 |
  • For Angular > 18 use 0.8
  • 255 | 256 |
    257 | 258 | Have an issue? Leave it here: https://github.com/oOps1627/angular-calendar-timeline/issues 259 | 260 | You can support me by donation: 261 | * https://www.paypal.com/donate/?hosted_button_id=38ZN57VTQ9TQC 262 | * https://buymeacoffee.com/andriy1627 263 | 264 | -------------------------------------------------------------------------------- /projects/angular-calendar-timeline/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | jasmine: { 17 | // you can add configuration options for Jasmine here 18 | // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html 19 | // for example, you can disable the random execution with `random: false` 20 | // or set a specific seed with `seed: 4321` 21 | }, 22 | clearContext: false // leave Jasmine Spec Runner output visible in browser 23 | }, 24 | jasmineHtmlReporter: { 25 | suppressAll: true // removes the duplicated traces 26 | }, 27 | coverageReporter: { 28 | dir: require('path').join(__dirname, '../../coverage/angular-calendar-timeline'), 29 | subdir: '.', 30 | reporters: [ 31 | { type: 'html' }, 32 | { type: 'text-summary' } 33 | ] 34 | }, 35 | reporters: ['progress', 'kjhtml'], 36 | port: 9876, 37 | colors: true, 38 | logLevel: config.LOG_INFO, 39 | autoWatch: true, 40 | browsers: ['Chrome'], 41 | singleRun: false, 42 | restartOnFileChange: true 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /projects/angular-calendar-timeline/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/angular-calendar-timeline", 4 | "lib": { 5 | "entryFile": "src/public-api.ts" 6 | }, 7 | "allowedNonPeerDependencies": [ 8 | "angular-draggable-droppable", 9 | "angular-resizable-element", 10 | "tslib" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /projects/angular-calendar-timeline/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-calendar-timeline", 3 | "version": "0.8.0", 4 | "description": "A timeline for angular that shows events on a timeline board in different modes: days, weeks, and months.", 5 | "author": "Andrii Krashivskyi", 6 | "license": "MIT", 7 | "homepage": "https://github.com/oOps1627/angular-calendar-timeline#readme", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/oOps1627/angular-calendar-timeline" 11 | }, 12 | "keywords": [ 13 | "angular", 14 | "angular2", 15 | "timeline", 16 | "calendar", 17 | "schedule", 18 | "scheduler", 19 | "events", 20 | "board", 21 | "events-schedule", 22 | "events-board" 23 | ], 24 | "scripts": { 25 | "ng": "ng", 26 | "start": "ng serve", 27 | "build": "ng build", 28 | "watch": "ng build --watch --configuration development", 29 | "test": "ng test", 30 | "build:copy-package-json": "copyfiles README.md package.json projects/angular-calendar-timeline", 31 | "build:lib": "ng build angular-calendar-timeline --configuration production", 32 | "release:npm-publish": "npm publish ./dist/angular-calendar-timeline" 33 | }, 34 | "private": false, 35 | "dependencies": { 36 | "angular-draggable-droppable": "6.0.0", 37 | "angular-resizable-element": "5.0.0", 38 | "tslib": "^2.3.0" 39 | }, 40 | "peerDependencies": {}, 41 | "devDependencies": { 42 | "rxjs": "~7.4.0", 43 | "zone.js": "~0.14.10", 44 | "@angular/animations": "^18.2.13", 45 | "@angular/cdk": "^18.2.14", 46 | "@angular/common": "^18.2.13", 47 | "@angular/compiler": "^18.2.13", 48 | "@angular/core": "^18.2.13", 49 | "@angular/forms": "^18.2.13", 50 | "@angular/platform-browser": "^18.2.13", 51 | "@angular/platform-browser-dynamic": "^18.2.13", 52 | "@angular/router": "^18.2.13", 53 | "@angular-devkit/build-angular": "^18.2.16", 54 | "@angular/cli": "^18.2.16", 55 | "@angular/compiler-cli": "^18.2.13", 56 | "@types/jasmine": "~3.10.0", 57 | "@types/node": "^12.11.1", 58 | "jasmine-core": "~3.10.0", 59 | "karma": "~6.3.0", 60 | "karma-chrome-launcher": "~3.1.0", 61 | "karma-coverage": "~2.0.3", 62 | "karma-jasmine": "~4.0.0", 63 | "karma-jasmine-html-reporter": "~1.7.0", 64 | "ng-packagr": "^18.2.1", 65 | "typescript": "~5.4.5", 66 | "copyfiles": "^2.4.1" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /projects/angular-calendar-timeline/src/lib/components/date-marker/timeline-date-marker.component.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 |
    11 |
    12 |
    13 | -------------------------------------------------------------------------------- /projects/angular-calendar-timeline/src/lib/components/date-marker/timeline-date-marker.component.scss: -------------------------------------------------------------------------------- 1 | .date-marker { 2 | position: absolute; 3 | width: 2px; 4 | height: 100%; 5 | background-color: #2ac226; 6 | z-index: 2; 7 | display: flex; 8 | flex-direction: column; 9 | transform: translateX(-50%); 10 | } 11 | -------------------------------------------------------------------------------- /projects/angular-calendar-timeline/src/lib/components/date-marker/timeline-date-marker.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, TemplateRef } from '@angular/core'; 2 | import { IScale } from "../../models"; 3 | 4 | @Component({ 5 | selector: 'timeline-date-marker', 6 | templateUrl: './timeline-date-marker.component.html', 7 | styleUrls: ['./timeline-date-marker.component.scss'], 8 | changeDetection: ChangeDetectionStrategy.OnPush 9 | }) 10 | export class TimelineDateMarkerComponent { 11 | isInScaleRange: boolean = true; 12 | 13 | @Input() leftPosition: number = 0; 14 | 15 | @Input() headerHeight: number; 16 | 17 | @Input() customTemplate: TemplateRef<{ leftPosition: number }> | undefined; 18 | 19 | @Input() set scale(scale: IScale) { 20 | this._checkIsInScaleRange(scale); 21 | }; 22 | 23 | constructor(private _cdr: ChangeDetectorRef) { 24 | } 25 | 26 | private _checkIsInScaleRange(scale: IScale): void { 27 | const now = Date.now(); 28 | this.isInScaleRange = scale.startDate.getTime() <= now && scale.endDate.getTime() >= now; 29 | this._cdr.detectChanges(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /projects/angular-calendar-timeline/src/lib/components/item/timeline-item.component.html: -------------------------------------------------------------------------------- 1 |
    17 |
    18 | 21 |
    22 | 23 | 24 |
    25 | {{item.name}} 26 |
    27 |
    28 | 29 |
    34 |
    39 |
    40 | -------------------------------------------------------------------------------- /projects/angular-calendar-timeline/src/lib/components/item/timeline-item.component.scss: -------------------------------------------------------------------------------- 1 | .timeline-item { 2 | height: 100%; 3 | position: absolute; 4 | box-sizing: border-box; 5 | overflow: hidden; 6 | top: 5px; 7 | } 8 | 9 | .item-custom-template { 10 | width: 100%; 11 | height: 100%; 12 | position: relative; 13 | box-sizing: border-box; 14 | } 15 | 16 | .default-content { 17 | height: 100%; 18 | width: 100%; 19 | background-color: #098ed2; 20 | padding: 2px 10px; 21 | display: flex; 22 | align-items: center; 23 | color: #f1f1f1; 24 | box-sizing: border-box; 25 | border-radius: 2px; 26 | cursor: pointer; 27 | } 28 | 29 | mwlResizable { 30 | box-sizing: border-box; // required for the enableGhostResize option to work 31 | } 32 | 33 | .resize-handle-left, 34 | .resize-handle-right { 35 | position: absolute; 36 | height: 100%; 37 | cursor: col-resize; 38 | width: 5px; 39 | top: 0; 40 | } 41 | 42 | .resize-handle-left { 43 | left: 0; 44 | } 45 | 46 | .resize-handle-right { 47 | right: 0; 48 | } 49 | -------------------------------------------------------------------------------- /projects/angular-calendar-timeline/src/lib/components/item/timeline-item.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ChangeDetectionStrategy, 3 | ChangeDetectorRef, 4 | Component, 5 | EventEmitter, 6 | Input, 7 | Output, Renderer2, 8 | TemplateRef 9 | } from '@angular/core'; 10 | import { ResizeEvent } from "angular-resizable-element"; 11 | import { DragEndEvent } from "angular-draggable-droppable/lib/draggable.directive"; 12 | import { ITimelineItem, IScale } from "../../models"; 13 | 14 | @Component({ 15 | selector: 'timeline-item', 16 | templateUrl: './timeline-item.component.html', 17 | styleUrls: ['./timeline-item.component.scss'], 18 | changeDetection: ChangeDetectionStrategy.OnPush, 19 | }) 20 | export class TimelineItemComponent { 21 | private _item: ITimelineItem; 22 | 23 | private _scale: IScale; 24 | 25 | isInScaleRange = true; 26 | 27 | isItemResizingStarted = false; 28 | 29 | @Input() set item(item: ITimelineItem | undefined) { 30 | this._item = item; 31 | item.updateView = () => this._cdr.detectChanges(); 32 | this._checkIsInScaleRange(); 33 | }; 34 | 35 | @Input() set scale(scale: IScale | undefined) { 36 | this._scale = scale; 37 | this._checkIsInScaleRange(); 38 | }; 39 | 40 | @Input() rowContainer: HTMLElement; 41 | 42 | @Input() height: number; 43 | 44 | @Input() rowHeight: number; 45 | 46 | @Input() locale: string; 47 | 48 | @Input() contentTemplate: TemplateRef<{ $implicit: ITimelineItem, locale: string }> | undefined; 49 | 50 | @Output() itemResized = new EventEmitter<{ event: ResizeEvent, item: ITimelineItem }>(); 51 | 52 | @Output() itemMoved = new EventEmitter<{ event: DragEndEvent, item: ITimelineItem }>(); 53 | 54 | get item(): ITimelineItem { 55 | return this._item; 56 | } 57 | 58 | constructor(private _cdr: ChangeDetectorRef, 59 | private _renderer: Renderer2) { 60 | } 61 | 62 | onItemResizeStart(event: ResizeEvent): void { 63 | this.isItemResizingStarted = true; 64 | this._cdr.markForCheck(); 65 | } 66 | 67 | onItemResizeEnd(event: ResizeEvent): void { 68 | this.itemResized.emit({event, item: this._item}); 69 | setTimeout(() => this.isItemResizingStarted = false); 70 | } 71 | 72 | onItemDragStart(event): void { 73 | this._setRowZIndex(1000); 74 | } 75 | 76 | onItemDropped(event: DragEndEvent): void { 77 | if (!this.isItemResizingStarted) { 78 | this.itemMoved.emit({event, item: this._item}); 79 | } 80 | 81 | this._setRowZIndex(1); 82 | } 83 | 84 | private _checkIsInScaleRange(): void { 85 | if (!this._item || !this._scale) { 86 | return; 87 | } 88 | 89 | if (!this._item.startDate || !this._item.endDate) { 90 | this.isInScaleRange = true; 91 | this._cdr.markForCheck(); 92 | return; 93 | } 94 | 95 | this.isInScaleRange = this._scale.startDate.getTime() <= this._item.startDate.getTime() 96 | && this._scale.endDate.getTime() >= this._item.endDate.getTime(); 97 | this._cdr.markForCheck(); 98 | } 99 | 100 | private _setRowZIndex(index: number): void { 101 | this._renderer.setStyle(this.rowContainer, 'z-index', index); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /projects/angular-calendar-timeline/src/lib/components/panel/timeline-panel.component.html: -------------------------------------------------------------------------------- 1 |
    5 | 6 |
    7 | 8 |
    {{label}}
    9 | 10 | 13 | 14 | 15 | 16 | 17 | 19 | 20 | 21 |
    22 | 26 |
    27 |
    28 |
    29 | 30 | 31 |
    35 | 36 |
    37 |
    39 |
    40 | 41 | {{item.name}} 42 |
    43 |
    44 |
    45 | -------------------------------------------------------------------------------- /projects/angular-calendar-timeline/src/lib/components/panel/timeline-panel.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: contents; 3 | } 4 | 5 | .panel { 6 | height: 100%; 7 | display: flex; 8 | flex-direction: column; 9 | position: sticky; 10 | left: 0; 11 | z-index: 1001; 12 | flex-shrink: 0; 13 | background-color: #F8F8F8; 14 | border-right: 1px solid #d0d0d0; 15 | } 16 | 17 | .label { 18 | display: flex; 19 | justify-content: center; 20 | align-items: center; 21 | width: 100%; 22 | position: sticky; 23 | top: 0; 24 | left: 0; 25 | z-index: 100; 26 | flex-shrink: 0; 27 | border-bottom: #d0d0d0 1px solid; 28 | } 29 | 30 | 31 | .panel-item { 32 | border-bottom: #d0d0d0 1px solid; 33 | padding: 5px 10px; 34 | overflow: hidden; 35 | text-overflow: ellipsis; 36 | white-space: nowrap; 37 | box-sizing: border-box; 38 | display: flex; 39 | 40 | &.can-collapse { 41 | .collapse-icon { 42 | display: inline-block !important; 43 | } 44 | 45 | .item-content { 46 | cursor: pointer; 47 | } 48 | } 49 | 50 | .item-content { 51 | display: inline-flex; 52 | align-items: center; 53 | box-sizing: border-box; 54 | max-width: 100%; 55 | 56 | .collapse-icon { 57 | border: solid #5a5a5a; 58 | border-width: 0 2px 2px 0; 59 | display: none; 60 | padding: 2.5px; 61 | margin-right: 8px; 62 | transform: rotate(45deg); 63 | 64 | 65 | &.collapsed { 66 | transform: rotate(-45deg); 67 | } 68 | } 69 | 70 | .item-name { 71 | text-overflow: ellipsis; 72 | white-space: nowrap; 73 | overflow: hidden; 74 | width: auto; 75 | } 76 | } 77 | } 78 | 79 | [mwlResizeHandle] { 80 | position: absolute; 81 | top: 0; 82 | right: 0; 83 | box-sizing: border-box; 84 | width: 5px; 85 | height: 100%; 86 | cursor: col-resize; 87 | z-index: 10000; 88 | } 89 | -------------------------------------------------------------------------------- /projects/angular-calendar-timeline/src/lib/components/panel/timeline-panel.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ChangeDetectionStrategy, ChangeDetectorRef, 3 | Component, 4 | EventEmitter, 5 | Input, 6 | OnChanges, 7 | Output, 8 | SimpleChanges, 9 | TemplateRef 10 | } from "@angular/core"; 11 | import { ResizeEvent } from "angular-resizable-element"; 12 | import { ITimelineItem, IIdObject } from "../../models"; 13 | 14 | @Component({ 15 | selector: 'timeline-panel', 16 | templateUrl: 'timeline-panel.component.html', 17 | styleUrls: ['timeline-panel.component.scss'], 18 | changeDetection: ChangeDetectionStrategy.OnPush 19 | }) 20 | export class TimelinePanelComponent implements OnChanges { 21 | @Input() items: ITimelineItem[] = []; 22 | 23 | @Input() label: string; 24 | 25 | @Input() width: number; 26 | 27 | @Input() resizable: boolean; 28 | 29 | @Input() minWidth: number; 30 | 31 | @Input() maxWidth: number; 32 | 33 | @Input() headerHeight: number; 34 | 35 | @Input() rowHeight: number; 36 | 37 | @Input() locale: string; 38 | 39 | @Input() childGroupOffset: number = 15; 40 | 41 | @Input() itemTemplate: TemplateRef<{ item: ITimelineItem, index: number, depth: number, locale: string }> 42 | 43 | @Output() widthChanged = new EventEmitter(); 44 | 45 | constructor(private _cdr: ChangeDetectorRef) { 46 | } 47 | 48 | ngOnChanges(changes: SimpleChanges): void { 49 | if (Object.keys(changes).some(key => ['width', 'minWidth', 'maxWidth'].includes(key))) { 50 | this._validateWidth(); 51 | } 52 | } 53 | 54 | trackById(index: number, item: IIdObject): number | string { 55 | return item.id; 56 | } 57 | 58 | handleResize(event: ResizeEvent) { 59 | const newWidth = event.rectangle.width; 60 | 61 | if (newWidth < this.minWidth || newWidth > this.maxWidth) 62 | return; 63 | 64 | this.width = newWidth; 65 | this.widthChanged.emit(this.width); 66 | } 67 | 68 | toggleExpand(item: ITimelineItem): void { 69 | item.childrenItemsExpanded = !item.childrenItemsExpanded; 70 | this._cdr.markForCheck(); 71 | } 72 | 73 | private _validateWidth(): void { 74 | if (this.width < this.minWidth) { 75 | this.width = this.minWidth; 76 | } 77 | 78 | if (this.width > this.maxWidth) { 79 | this.width = this.maxWidth; 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /projects/angular-calendar-timeline/src/lib/components/scale-header/timeline-scale-header.component.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    5 |
    {{group.name}}
    6 |
    7 |
    8 |
    9 |
    10 |
    11 | {{formatter.formatColumn(column, zoom.columnWidth, locale)}} 12 |
    13 |
    14 |
    15 |
    16 | 17 | -------------------------------------------------------------------------------- /projects/angular-calendar-timeline/src/lib/components/scale-header/timeline-scale-header.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | z-index: 9; 3 | position: sticky; 4 | top: 0; 5 | display: flex; 6 | flex-direction: row; 7 | background: #F8F8F8; 8 | } 9 | 10 | .wrapper { 11 | display: flex; 12 | flex-direction: column; 13 | } 14 | 15 | .groups { 16 | display: flex; 17 | flex-grow: 3; 18 | 19 | .group { 20 | height: 100%; 21 | display: flex; 22 | align-items: center; 23 | justify-content: center; 24 | border-bottom: 1px solid #d0d0d0; 25 | position: relative; 26 | box-sizing: border-box; 27 | padding: 0 4px; 28 | 29 | &:first-child { 30 | &:after { 31 | display: none; 32 | } 33 | } 34 | 35 | &:after { 36 | content: ""; 37 | position: absolute; 38 | width: 1px; 39 | height: 100%; 40 | box-sizing: border-box; 41 | left: 0; 42 | top: 0; 43 | background: #d0d0d0; 44 | } 45 | } 46 | 47 | } 48 | 49 | .columns { 50 | display: flex; 51 | flex-grow: 2; 52 | 53 | .column { 54 | height: 100%; 55 | position: relative; 56 | display: flex; 57 | justify-content: center; 58 | align-items: center; 59 | 60 | &:after { 61 | content: ""; 62 | width: 1px; 63 | height: 100%; 64 | left: 100%; 65 | position: absolute; 66 | background: #d0d0d0; 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /projects/angular-calendar-timeline/src/lib/components/scale-header/timeline-scale-header.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } from '@angular/core'; 2 | import { IIdObject, ITimelineZoom, IScale, IScaleColumn, IScaleFormatter, IScaleGroup } from "../../models"; 3 | 4 | interface IGeneratedGroup { 5 | id: string; 6 | 7 | name: string; 8 | 9 | width: number; 10 | } 11 | 12 | @Component({ 13 | selector: 'timeline-scale-header', 14 | templateUrl: 'timeline-scale-header.component.html', 15 | styleUrls: ['timeline-scale-header.component.scss'], 16 | changeDetection: ChangeDetectionStrategy.OnPush, 17 | }) 18 | export class TimelineScaleHeaderComponent implements OnChanges { 19 | @Input() height: number; 20 | 21 | @Input() scale: IScale; 22 | 23 | @Input() formatter: IScaleFormatter; 24 | 25 | @Input() locale: string; 26 | 27 | @Input() zoom: ITimelineZoom; 28 | 29 | public groups: IGeneratedGroup[] = []; 30 | 31 | get columns(): IScaleColumn[] { 32 | return this.scale?.columns ?? []; 33 | } 34 | 35 | ngOnChanges(changes: SimpleChanges) { 36 | this._generateGroups(); 37 | } 38 | 39 | trackById(index: number, item: IIdObject): number | string { 40 | return item.id; 41 | } 42 | 43 | private _groupColumnGroups(): { [groupId: string]: IScaleGroup[] } { 44 | return this.scale.columns.reduce((groupsMap, column) => { 45 | column.groups.forEach(group => { 46 | groupsMap[group.id] = groupsMap[group.id] ?? []; 47 | groupsMap[group.id].push(group); 48 | }); 49 | 50 | return groupsMap; 51 | }, {}) 52 | } 53 | 54 | private _generateGroups(): void { 55 | const groupedGroups = this._groupColumnGroups(); 56 | 57 | this.groups = Object.keys(groupedGroups).map(groupId => ({ 58 | id: groupId, 59 | name: this.formatter.formatGroup(groupedGroups[groupId][0], this.locale), 60 | width: groupedGroups[groupId].reduce((acc, curr) => acc + this.zoom.columnWidth * curr.coverageInPercents / 100, 0) 61 | })); 62 | } 63 | } 64 | 65 | 66 | -------------------------------------------------------------------------------- /projects/angular-calendar-timeline/src/lib/formatters/day-scale-formatter.ts: -------------------------------------------------------------------------------- 1 | import { IScaleColumn, IScaleFormatter, IScaleGroup } from "../models"; 2 | import { formatDate } from "@angular/common"; 3 | import { Injectable } from "@angular/core"; 4 | 5 | @Injectable() 6 | export class DayScaleFormatter implements IScaleFormatter { 7 | formatColumn(column: IScaleColumn, columnWidth: number, locale: string): string { 8 | if (columnWidth < 65) 9 | return formatDate(column.date, 'dd', locale); 10 | 11 | if (columnWidth > 180) 12 | return formatDate(column.date, 'EEEE dd/MM', locale); 13 | 14 | return formatDate(column.date, 'EEE dd/MM', locale); 15 | } 16 | 17 | formatGroup(group: IScaleGroup, locale: string): string { 18 | return formatDate(group.date, 'LLLL', locale); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /projects/angular-calendar-timeline/src/lib/formatters/month-scale-formatter.ts: -------------------------------------------------------------------------------- 1 | import { IScaleColumn, IScaleFormatter, IScaleGroup } from "../models"; 2 | import { formatDate } from "@angular/common"; 3 | import { Injectable } from "@angular/core"; 4 | 5 | @Injectable() 6 | export class MonthScaleFormatter implements IScaleFormatter { 7 | formatColumn(column: IScaleColumn, columnWidth: number, locale: string): string { 8 | if (columnWidth < 65) 9 | return String(column.index); 10 | 11 | if (columnWidth > 180) 12 | return formatDate(column.date, 'LLLL', locale); 13 | 14 | return formatDate(column.date, 'LLL', locale); 15 | } 16 | 17 | formatGroup(group: IScaleGroup, locale: string): string { 18 | return String(group.date.getFullYear()); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /projects/angular-calendar-timeline/src/lib/formatters/week-scale-formatter.ts: -------------------------------------------------------------------------------- 1 | import { IScaleColumn, IScaleFormatter, IScaleGroup } from "../models"; 2 | import { Injectable } from "@angular/core"; 3 | import { formatDate, FormStyle, getLocaleDayNames, TranslationWidth } from "@angular/common"; 4 | 5 | @Injectable() 6 | export class WeekScaleFormatter implements IScaleFormatter { 7 | formatColumn(column: IScaleColumn, columnWidth: number, locale: string): string { 8 | if (columnWidth > 100) { 9 | const days = getLocaleDayNames(locale, FormStyle.Format, TranslationWidth.Abbreviated); 10 | 11 | return `${days[1]}-${days[0]} (${column.index})` 12 | } 13 | 14 | return String(column.index); 15 | } 16 | 17 | formatGroup(group: IScaleGroup, locale: string): string { 18 | return formatDate(group.date, 'LLLL y', locale); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /projects/angular-calendar-timeline/src/lib/helpers/cache.ts: -------------------------------------------------------------------------------- 1 | import { DateHelpers } from "./date-helpers"; 2 | 3 | export function DatesCacheDecorator(): Function { 4 | return function(target: any, methodName: string, descriptor: PropertyDescriptor) { 5 | if (!target.__datesCache) { 6 | target.__datesCache = new Map(); 7 | } 8 | 9 | const originalMethod = descriptor.value; 10 | 11 | descriptor.value = function(...args: Date[]) { 12 | const cacheKey = `${methodName}-${[...args].map(date => DateHelpers.generateDateId(date)).join('-')}`; 13 | 14 | if (target.__datesCache.has(cacheKey)) { 15 | return target.__datesCache.get(cacheKey); 16 | } 17 | 18 | const result = originalMethod.apply(this, args); 19 | target.__datesCache.set(cacheKey, result); 20 | 21 | return result; 22 | }; 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /projects/angular-calendar-timeline/src/lib/helpers/date-helpers.ts: -------------------------------------------------------------------------------- 1 | import { DateInput } from "../models"; 2 | 3 | export class DateHelpers { 4 | static generateDateId(date: Date): string { 5 | return `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}-${date.getHours()}-${date.getMinutes()}`; 6 | } 7 | 8 | static lastDayOfMonth(date: DateInput): Date { 9 | const dateWithLastDayOfMonth = new Date(date); 10 | dateWithLastDayOfMonth.setMonth(dateWithLastDayOfMonth.getMonth() + 1); 11 | dateWithLastDayOfMonth.setDate(0); 12 | 13 | return dateWithLastDayOfMonth; 14 | } 15 | 16 | static getDaysInMonth(date: Date): number { 17 | return new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate(); 18 | } 19 | 20 | static firstMondayOfMonth(date: Date): Date { 21 | const firstDay = new Date(new Date(date).setDate(1)); 22 | const monday = DateHelpers.firstDayOfWeek(firstDay); 23 | 24 | return monday.getMonth() === date.getMonth() ? monday : new Date(monday.setDate(monday.getDate() + 7)); 25 | } 26 | 27 | static firstDayOfWeek(date: DateInput): Date { 28 | date = new Date(date); 29 | const first = date.getDate() - date.getDay() + 1; 30 | 31 | return new Date(new Date(date).setDate(first)); 32 | } 33 | 34 | static lastDayOfWeek(date: DateInput): Date { 35 | date = new Date(date); 36 | const dayOfWeek = date.getDay(); 37 | const diffToSunday = (dayOfWeek === 0) ? 0 : 7 - dayOfWeek; 38 | date.setDate(date.getDate() + diffToSunday); 39 | 40 | return date; 41 | } 42 | 43 | static dayBeginningTime(day: Date): Date { 44 | day = new Date(day); 45 | day.setHours(0, 0, 0, 0); 46 | 47 | return day; 48 | } 49 | 50 | static dayEndingTime(day: Date): Date { 51 | day = new Date(day); 52 | day.setHours(23, 59, 59, 999); 53 | 54 | return day; 55 | } 56 | } 57 | 58 | export enum MillisecondsToTime { 59 | Minute = 1000 * 60, 60 | Day = 86400000, 61 | Week = MillisecondsToTime.Day * 7 62 | } 63 | -------------------------------------------------------------------------------- /projects/angular-calendar-timeline/src/lib/helpers/row-determinant.ts: -------------------------------------------------------------------------------- 1 | import { IItemsIterator, ITimelineItem } from "../models"; 2 | 3 | interface IRow { 4 | items: ITimelineItem[]; 5 | stream: ITimelineItem; 6 | } 7 | 8 | export class RowDeterminant { 9 | rows: IRow[]; 10 | 11 | constructor(private _itemsIterator: IItemsIterator) { 12 | this._generateMap(); 13 | } 14 | 15 | private _generateMap(): void { 16 | const map: IRow[] = []; 17 | const iterate = (items: ITimelineItem[]): void => { 18 | (items ?? []).forEach(item => { 19 | if (item.streamItems) { 20 | item._streamLevels.forEach((levelArr, index) => { 21 | map.push({stream: item, items: levelArr}) 22 | }); 23 | } else { 24 | map.push({stream: item, items: [item]}); 25 | } 26 | 27 | if (item.childrenItemsExpanded) { 28 | iterate(item.childrenItems ?? []); 29 | } 30 | }); 31 | } 32 | 33 | iterate(this._itemsIterator.items); 34 | this.rows = map; 35 | } 36 | 37 | getRowIndexByItem(item: ITimelineItem): number { 38 | let index; 39 | 40 | for (let i = 0; i < this.rows.length; i++) { 41 | const group = this.rows[i]; 42 | 43 | if (item.id === group.stream.id) { 44 | index = i; 45 | break; 46 | } 47 | 48 | const hasChild = group.items.find(i => i.id === item.id); 49 | if (hasChild) { 50 | index = i; 51 | } 52 | } 53 | 54 | return index; 55 | } 56 | 57 | getStreamByRowIndex(index: number): ITimelineItem | undefined { 58 | return this.rows[index]?.stream; 59 | } 60 | } 61 | 62 | -------------------------------------------------------------------------------- /projects/angular-calendar-timeline/src/lib/items-iterator/items-iterator.ts: -------------------------------------------------------------------------------- 1 | import { ITimelineItem, IItemsIterator } from "../models"; 2 | 3 | export class ItemsIterator implements IItemsIterator { 4 | private _items: ITimelineItem[] = []; 5 | 6 | get items(): ITimelineItem[] { 7 | return this._items; 8 | } 9 | 10 | setItems(items: ITimelineItem[]) { 11 | this._items = items; 12 | this._validate(); 13 | this._createItemsLevels(); 14 | } 15 | 16 | isEmpty(): boolean { 17 | return !this._items?.length; 18 | } 19 | 20 | getFirstItem(onlyVisible: boolean): ITimelineItem { 21 | let firstItem = null; 22 | 23 | this.forEach((item, parent) => { 24 | if (!item.startDate || !item.endDate) { 25 | return; 26 | } 27 | 28 | if (!firstItem || new Date(firstItem.startDate).getTime() > new Date(item.startDate).getTime()) { 29 | firstItem = item; 30 | } 31 | }, onlyVisible); 32 | 33 | return firstItem; 34 | } 35 | 36 | getLastItem(onlyVisible: boolean): ITimelineItem { 37 | let lastItem = null; 38 | 39 | this.forEach((item, parent) => { 40 | if (!item.startDate || !item.endDate) { 41 | return; 42 | } 43 | 44 | if (!lastItem || new Date(lastItem.endDate).getTime() < new Date(item.endDate).getTime()) { 45 | lastItem = item; 46 | } 47 | }, onlyVisible); 48 | 49 | return lastItem; 50 | } 51 | 52 | forEach(handler: (item: ITimelineItem, parent: (ITimelineItem | null)) => void, onlyVisible = false): void { 53 | function iterateAll(items: ITimelineItem[], parent: ITimelineItem | null): void { 54 | (items ?? []).forEach(item => { 55 | handler(item, parent); 56 | iterateAll(item.streamItems ?? [], item); 57 | if (!onlyVisible || item.childrenItemsExpanded) { 58 | iterateAll(item.childrenItems ?? [], item); 59 | } 60 | }); 61 | } 62 | 63 | iterateAll(this._items, null); 64 | } 65 | 66 | private _createItemsLevels(): void { 67 | this.forEach((item, parent) => { 68 | if (item.streamItems) { 69 | item._streamLevels = this._createItemLevels(item); 70 | } 71 | }); 72 | } 73 | 74 | private _createItemLevels(item: ITimelineItem): ITimelineItem[][] { 75 | const levels: ITimelineItem[][] = []; 76 | 77 | item.streamItems.forEach(item => { 78 | let isLevelFound = false; 79 | let currentLevelIndex = 0; 80 | while (!isLevelFound) { 81 | 82 | const levelItems = levels[currentLevelIndex]; 83 | if (!levelItems) { 84 | levels[currentLevelIndex] = [item]; 85 | isLevelFound = true; 86 | break; 87 | } 88 | 89 | const isItemCollides = levelItems.some(levelItem => this._isItemsCollides(levelItem, item)); 90 | if (!isItemCollides) { 91 | levels[currentLevelIndex].push(item); 92 | isLevelFound = true; 93 | break; 94 | } 95 | 96 | currentLevelIndex++; 97 | } 98 | }) 99 | 100 | return levels; 101 | } 102 | 103 | private _isItemsCollides(item1: ITimelineItem, item2: ITimelineItem): boolean { 104 | const item1Start = item1._left; 105 | const item1End = item1._left + item1._width; 106 | const item2Start = item2._left; 107 | const item2End = item2._left + item2._width; 108 | 109 | return item1Start === item2Start || item1End === item2End || 110 | item1End > item2Start && item1Start < item2End || 111 | item2End > item1Start && item2Start < item1End; 112 | } 113 | 114 | private _validate(): void { 115 | this.forEach((item: ITimelineItem) => { 116 | if ((item.startDate && !item.endDate) || (item.endDate && !item.startDate)) { 117 | this._removeItemDates(item); 118 | } 119 | 120 | if (item.streamItems) { 121 | this._removeItemDates(item); 122 | } 123 | }); 124 | } 125 | 126 | private _removeItemDates(item: ITimelineItem): void { 127 | delete item.startDate; 128 | delete item.endDate; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /projects/angular-calendar-timeline/src/lib/models/date-input.ts: -------------------------------------------------------------------------------- 1 | export type DateInput = Date | string | number; 2 | -------------------------------------------------------------------------------- /projects/angular-calendar-timeline/src/lib/models/events.ts: -------------------------------------------------------------------------------- 1 | import { ITimelineItem } from "./item"; 2 | 3 | export interface IItemTimeChangedEvent { 4 | item: ITimelineItem; 5 | newStartDate?: Date; 6 | newEndDate?: Date 7 | } 8 | 9 | export interface IItemRowChangedEvent { 10 | item: ITimelineItem; 11 | newRow: ITimelineItem | undefined; 12 | oldRow: ITimelineItem | undefined; 13 | } 14 | -------------------------------------------------------------------------------- /projects/angular-calendar-timeline/src/lib/models/id-object.ts: -------------------------------------------------------------------------------- 1 | export interface IIdObject { 2 | id: number | string; 3 | } 4 | -------------------------------------------------------------------------------- /projects/angular-calendar-timeline/src/lib/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./id-object"; 2 | export * from "./item"; 3 | export * from "./zoom"; 4 | export * from "./date-input"; 5 | export * from "./view-adapter"; 6 | export * from "./scale"; 7 | export * from "./items-iterator"; 8 | export * from "./zooms-builder"; 9 | export * from "./events"; 10 | -------------------------------------------------------------------------------- /projects/angular-calendar-timeline/src/lib/models/item.ts: -------------------------------------------------------------------------------- 1 | import { IIdObject } from "./id-object"; 2 | 3 | export interface ITimelineItem extends IIdObject { 4 | /** 5 | * Name of item. Needs only for view. 6 | */ 7 | name: string; 8 | 9 | /** 10 | * Date when item starts. If 'streamItems' property is not empty then 'startDate' will be cleared. 11 | */ 12 | startDate?: Date; 13 | 14 | /** 15 | * Date when item ends. If 'streamItems' property is not empty then 'endDate' will be cleared. 16 | */ 17 | endDate?: Date; 18 | 19 | /** 20 | * Allows to disable / enable resize to left 21 | */ 22 | canResizeLeft?: boolean; 23 | 24 | /** 25 | * Allows to disable / enable resize to right 26 | */ 27 | canResizeRight?: boolean; 28 | 29 | /** 30 | * Disable / enable vertical item dragging 31 | */ 32 | canDragY?: boolean; 33 | 34 | /** 35 | * Disable / enable horizontal item dragging 36 | */ 37 | canDragX?: boolean; 38 | 39 | /** 40 | * These items will be determined like children and will be displayed under the current item in separate rows. 41 | */ 42 | childrenItems?: ITimelineItem[]; 43 | 44 | /** 45 | * Transforms current item into the stream. It allows adding multiple items into one row. 46 | * Can't be used simultaneously with the startDate/endDate of the current item. 47 | * Also these items can't contain childrenItems. 48 | */ 49 | streamItems?: (Omit, 'childrenItems'>)[]; 50 | 51 | /** 52 | * Show / hide inner items. Can toggle in left panel. Works only if item has not empty array in "innerItems" property. 53 | */ 54 | childrenItemsExpanded?: boolean; 55 | 56 | /** 57 | * Here can be added some custom data about item. It not uses in the library. 58 | */ 59 | meta?: Meta; 60 | 61 | /** 62 | * Trigger Change Detection in component created for this item. 63 | */ 64 | updateView?(): void; 65 | 66 | _width?: number; 67 | 68 | _left?: number; 69 | 70 | _streamLevels?: ITimelineItem[][]; 71 | } 72 | -------------------------------------------------------------------------------- /projects/angular-calendar-timeline/src/lib/models/items-iterator.ts: -------------------------------------------------------------------------------- 1 | import { ITimelineItem } from "./item"; 2 | 3 | export interface IItemsIterator { 4 | readonly items: ITimelineItem[]; 5 | 6 | setItems(items: ITimelineItem[]): void; 7 | 8 | isEmpty(): boolean; 9 | 10 | forEach(handler: (item: ITimelineItem, parent: ITimelineItem | null, onlyVisible?: boolean) => void): void; 11 | 12 | getFirstItem(onlyExpanded: boolean): ITimelineItem; 13 | 14 | getLastItem(onlyExpanded: boolean): ITimelineItem; 15 | } 16 | -------------------------------------------------------------------------------- /projects/angular-calendar-timeline/src/lib/models/scale.ts: -------------------------------------------------------------------------------- 1 | import { IItemsIterator } from "./items-iterator"; 2 | 3 | export interface IScaleColumn { 4 | id: string; 5 | 6 | date: Date; 7 | 8 | index: number; 9 | 10 | groups?: IScaleGroup[]; 11 | } 12 | 13 | export interface IScaleGroup { 14 | id: string; 15 | 16 | date: Date; 17 | 18 | coverageInPercents: number; 19 | } 20 | 21 | export interface IScale { 22 | startDate: Date; 23 | 24 | endDate: Date; 25 | 26 | columns: IScaleColumn[]; 27 | } 28 | 29 | export interface IScaleGeneratorConfig { 30 | /** 31 | * Text formatter for dates in the header. 32 | */ 33 | formatter: IScaleFormatter; 34 | 35 | /** 36 | * Sets the first date when the scale is starting. 37 | */ 38 | getStartDate?: (iterator: IItemsIterator) => Date; 39 | 40 | /** 41 | * Sets the last date in the scale. 42 | */ 43 | getEndDate?: (iterator: IItemsIterator) => Date; 44 | } 45 | 46 | export interface IScaleFormatter { 47 | formatColumn(column: IScaleColumn, columnWidth: number, locale: string): string; 48 | 49 | formatGroup?(group: IScaleGroup, locale: string): string; 50 | } 51 | 52 | export interface IScaleGenerator { 53 | /** 54 | * Formatter transforms date object into text in the header of timeline 55 | */ 56 | formatter: IScaleFormatter; 57 | 58 | /** 59 | * Generate the scale with start date, end date, and columns. Groups are not required. 60 | */ 61 | generateScale(startDate: Date, endDate: Date): IScale; 62 | 63 | /** 64 | * Returns the date when the scale starts depending on the items list. 65 | * By default, it takes the date of the first item and subtracts some time for free space. 66 | */ 67 | getStartDate(itemsBuilder: IItemsIterator): Date; 68 | 69 | /** 70 | * Returns the date when the scale ends depending on the items list. 71 | * By default, it takes the date of the last item and adds some time for free space. 72 | */ 73 | getEndDate(itemsBuilder: IItemsIterator): Date; 74 | } 75 | -------------------------------------------------------------------------------- /projects/angular-calendar-timeline/src/lib/models/view-adapter.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * View mode means the minimal unit of measurement of current zoom. 3 | * It can be 'Month', 'Week', 'Day' or your custom. 4 | * ViewModeAdapter performs calculations between dates with own strategy depending on what unit of measurement they are connected to. 5 | */ 6 | export interface IViewModeAdaptor { 7 | /** 8 | * Transforms duration between dates into columns and returns date which placed in the center of the columns. 9 | * Example: 10 | * When view mode == Months: 11 | * 1) firstDate = 01.02.2023, lastDate = 02.02.2023, result = 14.02.2023 (Middle date in February); 12 | * 2) firstDate = 10.01.2023, lastDate = 10.03.2023, result = 01.02.2023 (Middle date between 3 months); 13 | * When view mode == Days: 14 | * 1) firstDate = 01.02.2023, lastDate = 02.02.2023, result = starting of 02.02.2023 or ending of 01.02.2023. 15 | * 2) firstDate = 01.02.2023, lastDate = 05.02.2023, result = 03.02.2023. 16 | */ 17 | getMiddleDate(startDate: Date, endDate: Date): Date; 18 | 19 | /** 20 | * Returns the unique count of columns with includes two dates between them. 21 | * Result should be always integer. 22 | */ 23 | getUniqueColumnsWithinRange(startDate: Date, endDate: Date): number; 24 | 25 | /** 26 | * Returns the duration between dates which is transformed into a number of columns. 27 | */ 28 | getDurationInColumns(startDate: Date, endDate: Date): number; 29 | 30 | /** 31 | * Adds one unit of measurement to date. 32 | */ 33 | addColumnToDate(date: Date, units: number): Date; 34 | 35 | /** 36 | * Returns the date where column is beginning. 37 | * @param date 38 | * @returns date 39 | */ 40 | getBeginningDateOfColumn(date: Date): Date; 41 | 42 | /** 43 | * Returns the date where column is ending. 44 | * @param date 45 | * @returns date 46 | */ 47 | getEndingDateOfColumn(date: Date): Date; 48 | } 49 | -------------------------------------------------------------------------------- /projects/angular-calendar-timeline/src/lib/models/zoom.ts: -------------------------------------------------------------------------------- 1 | export enum TimelineViewMode { 2 | Month = 101, 3 | Week = 102, 4 | Day = 103 5 | } 6 | 7 | export interface ITimelineZoom { 8 | viewMode: ViewMode; 9 | 10 | columnWidth: number; 11 | } 12 | -------------------------------------------------------------------------------- /projects/angular-calendar-timeline/src/lib/models/zooms-builder.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from "rxjs"; 2 | import { ITimelineZoom, TimelineViewMode } from "./zoom"; 3 | 4 | export interface IIndexedZoom extends ITimelineZoom { 5 | index: number; 6 | } 7 | 8 | export interface IZoomsHandler { 9 | activeZoom$: Observable>; 10 | 11 | readonly activeZoom: IIndexedZoom; 12 | 13 | readonly zooms: IIndexedZoom[]; 14 | 15 | setZooms(zooms: ITimelineZoom[]): void; 16 | 17 | getFirstZoom(): IIndexedZoom; 18 | 19 | getLastZoom(): IIndexedZoom; 20 | 21 | zoomIn(): void; 22 | 23 | zoomOut(): void; 24 | 25 | changeActiveZoom(zoom: ITimelineZoom): void; 26 | 27 | isZoomActive(zoom: ITimelineZoom): boolean; 28 | } 29 | -------------------------------------------------------------------------------- /projects/angular-calendar-timeline/src/lib/scale-generator/base-scale-generator.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DateInput, 3 | IItemsIterator, 4 | IScale, 5 | IScaleColumn, 6 | IScaleFormatter, 7 | IScaleGenerator, 8 | IScaleGeneratorConfig, 9 | IScaleGroup 10 | } from "../models"; 11 | import { Injectable } from "@angular/core"; 12 | import { DateHelpers } from "../helpers/date-helpers"; 13 | import { DatesCacheDecorator } from "../helpers/cache"; 14 | 15 | @Injectable() 16 | export abstract class BaseScaleGenerator implements IScaleGenerator { 17 | protected _config: IScaleGeneratorConfig; 18 | 19 | public formatter: IScaleFormatter; 20 | 21 | constructor() { 22 | this._config = this._getConfig(); 23 | this.formatter = this._config.formatter; 24 | } 25 | 26 | protected abstract _getConfig(): IScaleGeneratorConfig; 27 | 28 | protected abstract _validateStartDate(startDate: DateInput): Date; 29 | 30 | protected abstract _validateEndDate(endDate: DateInput): Date; 31 | 32 | protected abstract _generateGroups(date: Date): IScaleGroup[]; 33 | 34 | protected abstract _getColumnIndex(date: Date): number; 35 | 36 | protected abstract _getNextColumnDate(date: Date): Date; 37 | 38 | public getStartDate(itemsBuilder: IItemsIterator): Date { 39 | if (this._config.getStartDate) { 40 | return this._config.getStartDate(itemsBuilder); 41 | } 42 | 43 | const firstItem = itemsBuilder.getFirstItem(false); 44 | const now = Date.now(); 45 | const firstItemTime = new Date(firstItem?.startDate ?? now).getTime(); 46 | 47 | return this._validateStartDate(firstItemTime < now ? firstItemTime : now); 48 | } 49 | 50 | public getEndDate(itemsBuilder: IItemsIterator): Date { 51 | if (this._config.getEndDate) { 52 | return this._config.getEndDate(itemsBuilder); 53 | } 54 | 55 | const lastItem = itemsBuilder.getLastItem(false); 56 | const now = Date.now(); 57 | const lastItemDate = new Date(lastItem?.endDate ?? now); 58 | 59 | return this._validateEndDate(lastItemDate.getTime() < now ? now : lastItemDate); 60 | } 61 | 62 | @DatesCacheDecorator() 63 | generateScale(startDate: Date, endDate: Date): IScale { 64 | let currentDate: Date = new Date(startDate); 65 | const endTime: number = endDate.getTime(); 66 | const columns: IScaleColumn[] = []; 67 | while (currentDate.getTime() <= endTime) { 68 | const date = new Date(currentDate); 69 | columns.push({ 70 | id: DateHelpers.generateDateId(date), 71 | date: date, 72 | index: this._getColumnIndex(date), 73 | groups: this._generateGroups(date), 74 | }); 75 | 76 | currentDate = this._getNextColumnDate(currentDate); 77 | } 78 | 79 | return { 80 | startDate, 81 | endDate, 82 | columns 83 | }; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /projects/angular-calendar-timeline/src/lib/scale-generator/day-scale-generator.ts: -------------------------------------------------------------------------------- 1 | import { BaseScaleGenerator } from './base-scale-generator'; 2 | import { DateInput, IScaleGenerator, IScaleGeneratorConfig, IScaleGroup } from '../models'; 3 | import { DateHelpers } from "../helpers/date-helpers"; 4 | import { inject, Injectable, InjectionToken } from "@angular/core"; 5 | import { DayScaleFormatter } from "../formatters/day-scale-formatter"; 6 | 7 | export const DAY_SCALE_GENERATOR_CONFIG = new InjectionToken('Day scale config'); 8 | 9 | 10 | const DefaultConfig: IScaleGeneratorConfig = { 11 | formatter: new DayScaleFormatter(), 12 | } 13 | 14 | @Injectable() 15 | export class DefaultDayScaleGenerator extends BaseScaleGenerator implements IScaleGenerator { 16 | protected _getConfig(): IScaleGeneratorConfig { 17 | return {...DefaultConfig, ...inject(DAY_SCALE_GENERATOR_CONFIG, {})}; 18 | } 19 | 20 | protected _validateStartDate(startDate: DateInput): Date { 21 | const countOfEmptyMonthsBefore = 1; 22 | startDate = new Date(startDate); 23 | startDate.setDate(1); 24 | startDate = DateHelpers.dayBeginningTime(startDate); 25 | startDate.setMonth(startDate.getMonth() - countOfEmptyMonthsBefore); 26 | 27 | return startDate; 28 | } 29 | 30 | protected _validateEndDate(endDate: DateInput): Date { 31 | const countOfEmptyMonthsAfter = 1; 32 | endDate = new Date(endDate); 33 | return new Date(DateHelpers.lastDayOfMonth(endDate).setMonth(endDate.getMonth() + countOfEmptyMonthsAfter)); 34 | } 35 | 36 | protected _generateGroups(date: Date): IScaleGroup[] { 37 | date = new Date(date.getFullYear(), date.getMonth(), 1, 0, 0, 0, 0); 38 | return [{date, id: DateHelpers.generateDateId(date), coverageInPercents: 100}]; 39 | } 40 | 41 | protected _getColumnIndex(date: Date): number { 42 | return date.getDate(); 43 | } 44 | 45 | protected _getNextColumnDate(date: Date): Date { 46 | return new Date(date.setDate(date.getDate() + 1)); 47 | } 48 | } 49 | 50 | @Injectable() 51 | export class DayScaleGenerator extends DefaultDayScaleGenerator { 52 | } 53 | -------------------------------------------------------------------------------- /projects/angular-calendar-timeline/src/lib/scale-generator/month-scale-generator.ts: -------------------------------------------------------------------------------- 1 | import { BaseScaleGenerator } from './base-scale-generator'; 2 | import { DateInput, IScaleGenerator, IScaleGeneratorConfig, IScaleGroup } from '../models'; 3 | import { DateHelpers } from "../helpers/date-helpers"; 4 | import { inject, Injectable, InjectionToken } from "@angular/core"; 5 | import { MonthScaleFormatter } from "../formatters/month-scale-formatter"; 6 | 7 | export const MONTH_SCALE_GENERATOR_CONFIG = new InjectionToken('Month scale config'); 8 | 9 | const DefaultConfig: IScaleGeneratorConfig = { 10 | formatter: new MonthScaleFormatter(), 11 | } 12 | 13 | @Injectable() 14 | export class DefaultMonthScaleGenerator extends BaseScaleGenerator implements IScaleGenerator { 15 | protected _getConfig(): IScaleGeneratorConfig { 16 | return {...DefaultConfig, ...inject(MONTH_SCALE_GENERATOR_CONFIG, {})}; 17 | } 18 | 19 | protected _validateStartDate(startDate: DateInput): Date { 20 | const newDate = new Date(startDate); 21 | const countOfEmptyYearsBefore = 1; 22 | newDate.setDate(1); 23 | newDate.setMonth(0); 24 | newDate.setFullYear(newDate.getFullYear() - countOfEmptyYearsBefore); 25 | 26 | return newDate; 27 | } 28 | 29 | protected _validateEndDate(endDate: DateInput): Date { 30 | const newDate = DateHelpers.lastDayOfMonth(endDate); 31 | const countOfEmptyYearsAfter = 1; 32 | newDate.setMonth(11); 33 | newDate.setFullYear(newDate.getFullYear() + countOfEmptyYearsAfter); 34 | 35 | return newDate; 36 | } 37 | 38 | protected _generateGroups(date: Date): IScaleGroup[] { 39 | date = new Date(date.getFullYear(), 1, 0, 0, 0, 0, 0); 40 | return [{date, id: DateHelpers.generateDateId(date), coverageInPercents: 100}]; 41 | } 42 | 43 | protected _getColumnIndex(date: Date): number { 44 | return date.getMonth() + 1; 45 | } 46 | 47 | protected _getNextColumnDate(date: Date): Date { 48 | return new Date(date.setMonth(date.getMonth() + 1)); 49 | } 50 | } 51 | 52 | @Injectable() 53 | export class MonthScaleGenerator extends DefaultMonthScaleGenerator { 54 | } 55 | -------------------------------------------------------------------------------- /projects/angular-calendar-timeline/src/lib/scale-generator/week-scale-generator.ts: -------------------------------------------------------------------------------- 1 | import { BaseScaleGenerator } from './base-scale-generator'; 2 | import { DateInput, IScaleGenerator, IScaleGeneratorConfig, IScaleGroup } from '../models'; 3 | import { DateHelpers } from "../helpers/date-helpers"; 4 | import { WeekScaleFormatter } from "../formatters/week-scale-formatter"; 5 | import { inject, Injectable, InjectionToken } from "@angular/core"; 6 | 7 | export const WEEK_SCALE_GENERATOR_CONFIG = new InjectionToken('Week scale config'); 8 | 9 | const DefaultConfig: IScaleGeneratorConfig = { 10 | formatter: new WeekScaleFormatter(), 11 | } 12 | 13 | @Injectable() 14 | export class DefaultWeekScaleGenerator extends BaseScaleGenerator implements IScaleGenerator { 15 | protected _getConfig(): IScaleGeneratorConfig { 16 | return {...DefaultConfig, ...inject(WEEK_SCALE_GENERATOR_CONFIG, {})}; 17 | } 18 | 19 | protected _validateStartDate(startDate: DateInput): Date { 20 | const countOfEmptyMonthsBefore = 1; 21 | const newDate: Date = new Date(startDate); 22 | newDate.setMonth(newDate.getMonth() - countOfEmptyMonthsBefore); 23 | 24 | return DateHelpers.firstMondayOfMonth(newDate); 25 | } 26 | 27 | protected _validateEndDate(endDate: DateInput): Date { 28 | const countOfEmptyMonthsAfter = 1; 29 | const newDate: Date = new Date(endDate); 30 | newDate.setMonth(newDate.getMonth() + countOfEmptyMonthsAfter); 31 | 32 | return DateHelpers.lastDayOfWeek(newDate); 33 | } 34 | 35 | protected _generateGroups(date: Date): IScaleGroup[] { 36 | const weekStart: Date = DateHelpers.firstDayOfWeek(date); 37 | const weekEnd: Date = DateHelpers.lastDayOfWeek(date); 38 | const weekRelatedToTwoMonths: boolean = weekStart.getMonth() !== weekEnd.getMonth(); 39 | 40 | const weekStartGroupDate: Date = new Date(weekStart.getFullYear(), weekStart.getMonth(), 1, 0, 0, 0, 0); 41 | 42 | const groups: IScaleGroup[] = [ 43 | {date: weekStartGroupDate, id: DateHelpers.generateDateId(weekStartGroupDate), coverageInPercents: 100} 44 | ]; 45 | 46 | if (weekRelatedToTwoMonths) { 47 | groups[0].coverageInPercents = (DateHelpers.getDaysInMonth(weekStart) - (weekStart.getDate() - 1)) / 7 * 100; 48 | 49 | const weekEndGroupDate: Date = new Date(weekEnd.getFullYear(), weekEnd.getMonth(), 1, 0, 0, 0, 0); 50 | 51 | groups.push({ 52 | date: weekEndGroupDate, 53 | id: DateHelpers.generateDateId(weekEndGroupDate), 54 | coverageInPercents: 100 - groups[0].coverageInPercents 55 | }) 56 | } 57 | 58 | return groups; 59 | } 60 | 61 | protected _getColumnIndex(date: Date): number { 62 | const weekMonday: Date = DateHelpers.firstDayOfWeek(date); 63 | 64 | return Math.ceil(weekMonday.getDate() / 7); 65 | } 66 | 67 | protected _getNextColumnDate(date: Date): Date { 68 | return new Date(date.setDate(date.getDate() + 7)); 69 | } 70 | } 71 | 72 | @Injectable() 73 | export class WeekScaleGenerator extends DefaultWeekScaleGenerator { 74 | } 75 | -------------------------------------------------------------------------------- /projects/angular-calendar-timeline/src/lib/strategy-manager.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IViewModeAdaptor, 3 | IScaleGenerator, 4 | TimelineViewMode 5 | } from "./models"; 6 | import { Inject, Injectable } from "@angular/core"; 7 | import { DayScaleGenerator } from "./scale-generator/day-scale-generator"; 8 | import { WeekScaleGenerator } from "./scale-generator/week-scale-generator"; 9 | import { MonthScaleGenerator } from "./scale-generator/month-scale-generator"; 10 | import { DaysViewModeAdaptor } from "./view-mode-adaptor/days-view-mode-adaptor"; 11 | import { WeeksViewModeAdaptor } from "./view-mode-adaptor/weeks-view-mode-adaptor"; 12 | import { MonthsViewModeAdaptor } from "./view-mode-adaptor/months-view-mode-adaptor"; 13 | 14 | export interface IStrategyManager { 15 | getScaleGenerator(viewMode: ViewMode): IScaleGenerator; 16 | 17 | getViewModeAdaptor(viewMode: ViewMode): IViewModeAdaptor; 18 | } 19 | 20 | @Injectable() 21 | export class DefaultStrategyManager implements IStrategyManager { 22 | protected _generatorsDictionary = { 23 | [TimelineViewMode.Day]: this._dayGenerator, 24 | [TimelineViewMode.Week]: this._weekGenerator, 25 | [TimelineViewMode.Month]: this._monthGenerator, 26 | }; 27 | 28 | protected _calculatorsDictionary = { 29 | [TimelineViewMode.Day]: new DaysViewModeAdaptor(), 30 | [TimelineViewMode.Week]: new WeeksViewModeAdaptor(), 31 | [TimelineViewMode.Month]: new MonthsViewModeAdaptor(), 32 | }; 33 | 34 | constructor(@Inject(DayScaleGenerator) protected _dayGenerator: IScaleGenerator, 35 | @Inject(WeekScaleGenerator) protected _weekGenerator: IScaleGenerator, 36 | @Inject(MonthScaleGenerator) protected _monthGenerator: IScaleGenerator, 37 | ) { 38 | } 39 | 40 | getViewModeAdaptor(viewMode: ViewMode): IViewModeAdaptor { 41 | return this._calculatorsDictionary[viewMode as unknown as TimelineViewMode]; 42 | } 43 | 44 | getScaleGenerator(viewMode: ViewMode): IScaleGenerator { 45 | return this._generatorsDictionary[viewMode as unknown as TimelineViewMode]; 46 | } 47 | } 48 | 49 | @Injectable() 50 | export class StrategyManager extends DefaultStrategyManager { 51 | } 52 | -------------------------------------------------------------------------------- /projects/angular-calendar-timeline/src/lib/timeline.component.html: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 |
    17 | 24 | 25 |
    26 | 27 |
    30 |
    31 | 32 | 34 | 35 |
    36 |
    37 | 38 | 44 | 45 |
    46 | 47 | 48 |
    49 | 52 |
    53 |
    54 | 55 | 58 |
    59 | 60 | 61 | 71 | 72 | 73 | 74 |
    75 |
    76 | 77 | 80 | 81 |
    82 |
    83 | 84 |
    85 |
    86 | 87 | 88 |
    89 |
    90 | 93 |
    94 | 95 | 98 |
    99 |
    100 |
    101 |
    102 |
    103 | -------------------------------------------------------------------------------- /projects/angular-calendar-timeline/src/lib/timeline.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | height: 100%; 3 | overflow: auto; 4 | display: flex; 5 | flex-grow: 1; 6 | flex-direction: row; 7 | font-size: 14px; 8 | position: relative; 9 | } 10 | 11 | .content-wrapper { 12 | position: relative; 13 | height: fit-content; 14 | min-height: 100%; 15 | } 16 | 17 | .column-separators { 18 | .line { 19 | position: absolute; 20 | width: 1px; 21 | height: 100%; 22 | background-color: #d0d0d0; 23 | } 24 | } 25 | 26 | .timeline-items { 27 | display: flex; 28 | flex-direction: column; 29 | height: auto; 30 | 31 | .item-row { 32 | display: flex; 33 | align-items: center; 34 | flex-direction: row; 35 | position: relative; 36 | z-index: 7; 37 | box-sizing: border-box; 38 | } 39 | } 40 | 41 | -------------------------------------------------------------------------------- /projects/angular-calendar-timeline/src/lib/timeline.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AfterViewInit, 3 | ChangeDetectionStrategy, 4 | ChangeDetectorRef, 5 | Component, 6 | ElementRef, 7 | EventEmitter, 8 | HostListener, 9 | Inject, 10 | Input, 11 | OnDestroy, 12 | Output, 13 | PLATFORM_ID, 14 | TemplateRef, 15 | } from '@angular/core'; 16 | import { ResizeEvent } from 'angular-resizable-element'; 17 | import { interval, Subject, takeUntil } from 'rxjs'; 18 | import { startWith } from 'rxjs/operators'; 19 | import { 20 | IViewModeAdaptor, 21 | IIdObject, 22 | IItemsIterator, 23 | IScale, 24 | IScaleGenerator, 25 | ITimelineItem, 26 | ITimelineZoom, IZoomsHandler, IScaleColumn, IItemTimeChangedEvent, IItemRowChangedEvent, TimelineViewMode 27 | } from './models'; 28 | import { isPlatformBrowser } from "@angular/common"; 29 | import { MillisecondsToTime } from "./helpers/date-helpers"; 30 | import { ItemsIterator } from "./items-iterator/items-iterator"; 31 | import { ZoomsHandler } from "./zooms-handler/zooms-handler"; 32 | import { DefaultZooms } from "./zooms-handler/zooms"; 33 | import { DragEndEvent } from "angular-draggable-droppable/lib/draggable.directive"; 34 | import { StrategyManager } from "./strategy-manager"; 35 | import { RowDeterminant } from "./helpers/row-determinant"; 36 | 37 | @Component({ 38 | selector: 'timeline-calendar', 39 | templateUrl: './timeline.component.html', 40 | styleUrls: ['./timeline.component.scss'], 41 | changeDetection: ChangeDetectionStrategy.OnPush, 42 | }) 43 | export class TimelineComponent implements AfterViewInit, OnDestroy { 44 | /** 45 | * Indicates the current shown date in the middle of user`s screen. 46 | */ 47 | public currentDate: Date = new Date(); 48 | 49 | /** 50 | * Scale generator changes depending on current view type. 51 | */ 52 | public scaleGenerator: IScaleGenerator; 53 | 54 | /** 55 | * View mode adaptor changes depending on current view type. 56 | */ 57 | public viewModeAdaptor: IViewModeAdaptor; 58 | 59 | public dateMarkerLeftPosition: number = 0; 60 | 61 | public scale: IScale | undefined; 62 | 63 | public itemsIterator: IItemsIterator = new ItemsIterator(); 64 | 65 | public zoomsHandler: IZoomsHandler = new ZoomsHandler(DefaultZooms as any); 66 | 67 | private _ignoreNextScrollEvent: boolean = false; 68 | 69 | private _destroy$: Subject = new Subject(); 70 | 71 | /** 72 | * Emits event when startDate and endDate of some item was changed by resizing/moving it. 73 | */ 74 | @Output() itemTimeChanged: EventEmitter = new EventEmitter(); 75 | 76 | /** 77 | * Emits event when item was moved by Y axis. 78 | */ 79 | @Output() itemRowChanged: EventEmitter = new EventEmitter(); 80 | 81 | /** 82 | * Emits event when current zoom was changed. 83 | */ 84 | @Output() zoomChanged: EventEmitter> = new EventEmitter>(); 85 | 86 | /** 87 | * Emits event when user clicked somewhere on time grid. 88 | */ 89 | @Output() timeGridClicked: EventEmitter<{originalEvent: Event, row: ITimelineItem, column: IScaleColumn}> = new EventEmitter(); 90 | 91 | /** 92 | * The locale used to format dates. By default is 'en' 93 | */ 94 | @Input() locale: string = 'en'; 95 | 96 | /** 97 | * Height of the each row in pixels. By default is 40. 98 | */ 99 | @Input() rowHeight: number = 40; 100 | 101 | /** 102 | * Height of the each timeline item in pixels. Can't be bigger then 'rowHeight' property. By default is 30. 103 | */ 104 | @Input() itemHeight: number = 30; 105 | 106 | /** 107 | * Height of top dates panel in pixels. By default is 60. 108 | */ 109 | @Input() headerHeight: number = 60; 110 | 111 | /** 112 | * The label of left panel. By default is empty. 113 | */ 114 | @Input() panelLabel: string = ''; 115 | 116 | /** 117 | * Width of left panel in pixels. By default is 160. 118 | */ 119 | @Input() panelWidth: number = 160; 120 | 121 | /** 122 | * Minimal width of left panel in pixels. By default is 50. 123 | */ 124 | @Input() minPanelWidth: number = 50; 125 | 126 | /** 127 | * Maximal width of left panel in pixels. By default is 400. 128 | */ 129 | @Input() maxPanelWidth: number = 400; 130 | 131 | /** 132 | * Sets the left displacement in pixels between parent and child groups in left panel. By default is 15. 133 | */ 134 | @Input() offsetForChildPanelItem: number = 15; 135 | 136 | /** 137 | * Can resize panel. By default is true. 138 | */ 139 | @Input() isPanelResizable: boolean = true; 140 | 141 | /** 142 | * If false then date marker will be not visible. 143 | */ 144 | @Input() showDateMarket: boolean = true; 145 | 146 | /** 147 | * Custom template for item in left panel. 148 | */ 149 | @Input() panelItemTemplate: TemplateRef<{ item: ITimelineItem, index: number, depth: number, locale: string }> 150 | 151 | /** 152 | * Custom template for item in timeline. 153 | */ 154 | @Input() itemContentTemplate: TemplateRef<{ $implicit: ITimelineItem, locale: string }> | undefined; 155 | 156 | /** 157 | * Custom template for separators between columns. 158 | */ 159 | @Input() columnSeparatorTemplate: TemplateRef<{column: IScaleColumn, index: number, columnWidth: number, headerHeight: number}> | undefined; 160 | 161 | /** 162 | * Custom template for marker that indicates current time. 163 | */ 164 | @Input() dateMarkerTemplate: TemplateRef<{ leftPosition: number }> | undefined; 165 | 166 | /** 167 | * Register array of custom zooms. 168 | * Current zoom can be changed to any existed in this array by calling method "changeZoom()" 169 | */ 170 | @Input() set zooms(value: ITimelineZoom[]) { 171 | this.zoomsHandler.setZooms(value); 172 | } 173 | 174 | /** 175 | * The items of timeline. 176 | */ 177 | @Input() 178 | set items(items: ITimelineItem[]) { 179 | this.itemsIterator.setItems(items); 180 | this.redraw(); 181 | } 182 | 183 | /** 184 | * Visible timeline width (container visible width - panel width = timeline visible width). 185 | */ 186 | get visibleScaleWidth(): number { 187 | return this._elementRef.nativeElement.clientWidth - this.panelWidth; 188 | } 189 | 190 | /** 191 | * Active zoom. 192 | */ 193 | get zoom(): ITimelineZoom { 194 | return this.zoomsHandler.activeZoom; 195 | } 196 | 197 | /** 198 | * Registered zooms list. 199 | */ 200 | get zooms(): ITimelineZoom[] { 201 | return this.zoomsHandler.zooms; 202 | } 203 | 204 | constructor(private _cdr: ChangeDetectorRef, 205 | private _strategyManager: StrategyManager, 206 | @Inject(ElementRef) private _elementRef: ElementRef, 207 | @Inject(PLATFORM_ID) private _platformId: object) { 208 | this._setStrategies(this.zoom); 209 | } 210 | 211 | ngAfterViewInit(): void { 212 | this.zoomsHandler.activeZoom$ 213 | .pipe(takeUntil(this._destroy$)) 214 | .subscribe((zoom) => { 215 | this._setStrategies(zoom); 216 | this.redraw(); 217 | this.zoomChanged.emit(zoom); 218 | }); 219 | 220 | if (isPlatformBrowser(this._platformId)) { 221 | interval(MillisecondsToTime.Minute) 222 | .pipe(startWith(''), takeUntil(this._destroy$)) 223 | .subscribe(() => this._recalculateDateMarkerPosition()); 224 | } 225 | } 226 | 227 | /** 228 | * Recalculate and update view. 229 | */ 230 | redraw(): void { 231 | this._generateScale(); 232 | this._updateItemsPosition(); 233 | this.itemsIterator.setItems([...this.itemsIterator.items]); 234 | this._recalculateDateMarkerPosition(); 235 | this._ignoreNextScrollEvent = true; 236 | this._cdr.detectChanges(); 237 | this.attachCameraToDate(this.currentDate); 238 | } 239 | 240 | /** 241 | * Set horizontal scroll in the middle of the date 242 | */ 243 | attachCameraToDate(date: Date): void { 244 | this.currentDate = date; 245 | const duration = this.viewModeAdaptor.getDurationInColumns(this.scale.startDate, date); 246 | const scrollLeft = (duration * this.zoom.columnWidth) - (this.visibleScaleWidth / 2); 247 | this._ignoreNextScrollEvent = true; 248 | 249 | if (this._elementRef.nativeElement) { 250 | this._elementRef.nativeElement.scrollLeft = scrollLeft < 0 ? 0 : scrollLeft; 251 | } 252 | } 253 | 254 | /** 255 | * Automatically chooses the most optimal zoom and sets horizontal scroll to the center of the items. 256 | * Padding sets minimal spacing from left and right to the first and last items. 257 | */ 258 | fitToContent(paddings: number): void { 259 | const firstItem = this.itemsIterator.getFirstItem(true); 260 | const lastItem = this.itemsIterator.getLastItem(true); 261 | 262 | if (!firstItem || !lastItem) 263 | return; 264 | 265 | const startDate = new Date(firstItem.startDate); 266 | const endDate = new Date(lastItem.endDate); 267 | const zoom = this._calculateOptimalZoom(startDate, endDate, paddings); 268 | const viewModeAdaptor = this._strategyManager.getViewModeAdaptor(zoom.viewMode); 269 | 270 | this.currentDate = new Date(viewModeAdaptor.getMiddleDate(startDate, endDate)); 271 | 272 | if (this.zoomsHandler.isZoomActive(zoom)) { 273 | this.attachCameraToDate(this.currentDate); 274 | } else { 275 | this.changeZoom(zoom); 276 | } 277 | } 278 | 279 | /** 280 | * Change zoom to one of the existed 281 | */ 282 | changeZoom(zoom: ITimelineZoom): void { 283 | this.zoomsHandler.changeActiveZoom(zoom); 284 | } 285 | 286 | /** 287 | * Find zoom by its index and change 288 | */ 289 | changeZoomByIndex(index: number): void { 290 | const zoom = this.zoomsHandler.zooms.find(zoom => zoom.index === index); 291 | if (zoom) { 292 | this.zoomsHandler.changeActiveZoom(zoom); 293 | } else { 294 | console.error(`Cannot find zoom with index ${index}`); 295 | } 296 | } 297 | 298 | /** 299 | * Changes zoom to the max value 300 | */ 301 | zoomFullIn(): void { 302 | this.zoomsHandler.changeActiveZoom(this.zoomsHandler.getLastZoom()); 303 | } 304 | 305 | /** 306 | * Changes zoom to the min value 307 | */ 308 | zoomFullOut(): void { 309 | this.zoomsHandler.changeActiveZoom(this.zoomsHandler.getFirstZoom()); 310 | } 311 | 312 | /** 313 | * Changes zoom for 1 step back 314 | */ 315 | zoomIn(): void { 316 | this.zoomsHandler.zoomIn(); 317 | } 318 | 319 | /** 320 | * Changes zoom for 1 step forward 321 | */ 322 | zoomOut(): void { 323 | this.zoomsHandler.zoomOut(); 324 | } 325 | 326 | /** 327 | * Accepts the relative coordinates to the timeline container and returns the row and column. 328 | */ 329 | getCellByCoordinates(x: number, y: number): {row: ITimelineItem | undefined, column: IScaleColumn} { 330 | const rowDeterminant = new RowDeterminant(this.itemsIterator); 331 | const rowIndex = Math.floor((y - this.headerHeight) / this.rowHeight); 332 | const row: ITimelineItem = rowDeterminant.getStreamByRowIndex(rowIndex); 333 | 334 | const columnIndex = Math.floor((x - this.panelWidth) / this.zoom.columnWidth); 335 | const column: IScaleColumn = this.scale.columns[columnIndex]; 336 | 337 | return {column, row}; 338 | } 339 | 340 | _getCurrentDate(): Date { 341 | const currentScrollLeft = this._elementRef.nativeElement.scrollLeft ?? 0; 342 | const scrollLeftToCenterScreen = currentScrollLeft + (this.visibleScaleWidth / 2); 343 | const columns = Math.round(scrollLeftToCenterScreen / this.zoom.columnWidth); 344 | 345 | return this.viewModeAdaptor.addColumnToDate(this.scale.startDate, columns); 346 | } 347 | 348 | _onItemMoved(event: DragEndEvent, item: ITimelineItem): void { 349 | if (event.y) { 350 | this._onItemMovedVertically(event, item); 351 | } 352 | 353 | if (event.x) { 354 | this._onItemMovedHorizontally(event, item); 355 | } 356 | } 357 | 358 | private _onItemMovedHorizontally(event: DragEndEvent, item: ITimelineItem): void { 359 | const transferColumns = Math.round(event.x / this.zoom.columnWidth); 360 | const newStartDate = this.viewModeAdaptor.addColumnToDate(new Date(item.startDate), transferColumns); 361 | const newEndDate = this.viewModeAdaptor.addColumnToDate(new Date(item.endDate), transferColumns); 362 | this.itemTimeChanged.emit({item, newStartDate, newEndDate}); 363 | } 364 | 365 | private _onItemMovedVertically(event: DragEndEvent, item: ITimelineItem): void { 366 | const rowDeterminant = new RowDeterminant(this.itemsIterator); 367 | const rowIndex = rowDeterminant.getRowIndexByItem(item); 368 | const transferRows = event.y / this.rowHeight; 369 | const newRowIndex = rowIndex + transferRows; 370 | 371 | if (rowIndex === newRowIndex) 372 | return; 373 | 374 | const oldRow = rowDeterminant.getStreamByRowIndex(rowIndex); 375 | const newRow = rowDeterminant.getStreamByRowIndex(newRowIndex); 376 | 377 | this.itemRowChanged.emit({item, oldRow, newRow}); 378 | } 379 | 380 | private _calculateOptimalZoom(startDate: Date, endDate: Date, paddings = 15): ITimelineZoom { 381 | let possibleZoom: ITimelineZoom = this.zoomsHandler.getFirstZoom(); 382 | 383 | for (let i = this.zoomsHandler.getLastZoom().index; i >= this.zoomsHandler.getFirstZoom().index; i--) { 384 | const currentZoom = this.zoomsHandler.zooms[i]; 385 | const viewModeAdaptor = this._strategyManager.getViewModeAdaptor(currentZoom.viewMode); 386 | const countOfColumns = viewModeAdaptor.getUniqueColumnsWithinRange(startDate, endDate); 387 | 388 | if (countOfColumns * currentZoom.columnWidth < (this.visibleScaleWidth - paddings * 2)) { 389 | possibleZoom = currentZoom; 390 | break; 391 | } 392 | } 393 | 394 | return possibleZoom; 395 | } 396 | 397 | _trackById(index: number, item: IIdObject): number | string { 398 | return item.id; 399 | } 400 | 401 | _handleContentClick(event: MouseEvent): void { 402 | const scrollLeft: number = this._elementRef.nativeElement.scrollLeft; 403 | const scrollTop: number = this._elementRef.nativeElement.scrollTop; 404 | const rect = this._elementRef.nativeElement.getBoundingClientRect(); 405 | const xClick = event.clientX - rect.left + scrollLeft; 406 | const yClick = event.clientY - rect.top + scrollTop; 407 | const cell = this.getCellByCoordinates(xClick, yClick); 408 | 409 | this.timeGridClicked.emit({originalEvent: event, column: cell.column, row: cell.row}); 410 | } 411 | 412 | _onItemResized(event: ResizeEvent, item: ITimelineItem): void { 413 | const calculateNewDate = (movedPx: number, oldDate: Date): Date => { 414 | const countOfColumnsMoved = Math.round(movedPx as number / this.zoom.columnWidth); 415 | return this.viewModeAdaptor.addColumnToDate(oldDate, countOfColumnsMoved); 416 | } 417 | 418 | if (event.edges.left) { 419 | const newStartDate = calculateNewDate(event.edges.left, new Date(item.startDate)); 420 | const isNewStartDateValid: boolean = 421 | this.viewModeAdaptor.getBeginningDateOfColumn(newStartDate).getTime() <= new Date(item.endDate).getTime(); 422 | if (isNewStartDateValid) { 423 | this.itemTimeChanged.emit({item, newStartDate}); 424 | } 425 | } else { 426 | const newEndDate = calculateNewDate(event.edges.right, new Date(item.endDate)); 427 | const isNewEndDateValid: boolean = 428 | this.viewModeAdaptor.getEndingDateOfColumn(newEndDate).getTime() >= new Date(item.startDate).getTime(); 429 | if (isNewEndDateValid) { 430 | this.itemTimeChanged.emit({item, newEndDate}); 431 | } 432 | } 433 | } 434 | 435 | @HostListener('scroll', ['$event']) 436 | private _onScroll(event: Event): void { 437 | if (!this._ignoreNextScrollEvent) { 438 | this.currentDate = this._getCurrentDate(); 439 | } 440 | this._ignoreNextScrollEvent = false; 441 | } 442 | 443 | private _generateScale(): void { 444 | const scaleStartDate = this.scaleGenerator.getStartDate(this.itemsIterator); 445 | const scaleEndDate = this.scaleGenerator.getEndDate(this.itemsIterator); 446 | this.scale = this.scaleGenerator.generateScale(scaleStartDate, scaleEndDate); 447 | } 448 | 449 | private _updateItemsPosition(): void { 450 | this.itemsIterator.forEach((item) => this._updateItemPosition(item)); 451 | } 452 | 453 | private _updateItemPosition(item: ITimelineItem): void { 454 | item._width = this._calculateItemWidth(item); 455 | item._left = this._calculateItemLeftPosition(item); 456 | item.updateView && item.updateView(); 457 | } 458 | 459 | private _calculateItemLeftPosition(item: ITimelineItem): number { 460 | if (!item.startDate || !item.endDate) 461 | return 0; 462 | 463 | const columnsOffsetFromStart = this.viewModeAdaptor.getUniqueColumnsWithinRange(this.scale.startDate, new Date(item.startDate)) - 1; 464 | 465 | return columnsOffsetFromStart * this.zoom.columnWidth; 466 | } 467 | 468 | private _calculateItemWidth(item: ITimelineItem): number { 469 | if (!item.startDate || !item.endDate) 470 | return 0; 471 | 472 | const columnsOccupied = this.viewModeAdaptor.getUniqueColumnsWithinRange(new Date(item.startDate), new Date(item.endDate)); 473 | 474 | return columnsOccupied * this.zoom.columnWidth; 475 | } 476 | 477 | private _recalculateDateMarkerPosition(): void { 478 | const countOfColumns = this.viewModeAdaptor.getDurationInColumns(this.scale.startDate, new Date()); 479 | 480 | this.dateMarkerLeftPosition = countOfColumns * this.zoom.columnWidth; 481 | } 482 | 483 | private _setStrategies(zoom: ITimelineZoom): void { 484 | this.viewModeAdaptor = this._strategyManager.getViewModeAdaptor(zoom.viewMode); 485 | this.scaleGenerator = this._strategyManager.getScaleGenerator(zoom.viewMode); 486 | } 487 | 488 | ngOnDestroy(): void { 489 | this._destroy$.next(); 490 | this._destroy$.complete(); 491 | } 492 | } 493 | -------------------------------------------------------------------------------- /projects/angular-calendar-timeline/src/lib/timeline.module.ts: -------------------------------------------------------------------------------- 1 | import { ModuleWithProviders, NgModule, Provider } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { TimelineComponent } from './timeline.component'; 4 | import { TimelineItemComponent } from './components/item/timeline-item.component'; 5 | import { ResizableModule } from 'angular-resizable-element'; 6 | import { DragAndDropModule } from 'angular-draggable-droppable'; 7 | import { TimelineDateMarkerComponent } from './components/date-marker/timeline-date-marker.component'; 8 | import { TimelineScaleHeaderComponent } from './components/scale-header/timeline-scale-header.component'; 9 | import { DAY_SCALE_GENERATOR_CONFIG, DayScaleGenerator } from "./scale-generator/day-scale-generator"; 10 | import { WEEK_SCALE_GENERATOR_CONFIG, WeekScaleGenerator } from "./scale-generator/week-scale-generator"; 11 | import { MONTH_SCALE_GENERATOR_CONFIG, MonthScaleGenerator } from "./scale-generator/month-scale-generator"; 12 | import { TimelinePanelComponent } from "./components/panel/timeline-panel.component"; 13 | import { IScaleGeneratorConfig, ITimelineZoom } from "./models"; 14 | import { StrategyManager } from "./strategy-manager"; 15 | 16 | interface ITimelineModuleInitializationConfig { 17 | /** 18 | * Provide it when you want to extend current timeline logic and add some new view types. 19 | * Should be provided StrategyManager class with IStrategyManager implementation. 20 | */ 21 | strategyManager?: Provider; 22 | 23 | /** 24 | * Should be provided DayScaleGenerator class with IScaleGenerator implementation. 25 | */ 26 | dayScaleGenerator?: Provider; 27 | 28 | /** 29 | * Should be provided WeekScaleGenerator class with IScaleGenerator implementation. 30 | */ 31 | weekScaleGenerator?: Provider; 32 | 33 | /** 34 | * Should be provided MonthScaleGenerator class with IScaleGenerator implementation. 35 | */ 36 | monthScaleGenerator?: Provider; 37 | 38 | /** 39 | * List of zooms. 40 | */ 41 | zooms?: ITimelineZoom[]; 42 | 43 | /** 44 | * Settings for the scale generation in day mode. 45 | */ 46 | dayScaleConfig?: Partial; 47 | 48 | /** 49 | * Settings for the scale generation in week mode. 50 | */ 51 | weekScaleConfig?: Partial; 52 | 53 | /** 54 | * Settings for the scale generation in month mode. 55 | */ 56 | monthScaleConfig?: Partial; 57 | } 58 | 59 | @NgModule({ 60 | declarations: [ 61 | TimelineComponent, 62 | TimelineItemComponent, 63 | TimelineDateMarkerComponent, 64 | TimelineScaleHeaderComponent, 65 | TimelinePanelComponent 66 | ], 67 | imports: [ 68 | CommonModule, 69 | ResizableModule, 70 | DragAndDropModule, 71 | ], 72 | exports: [ 73 | TimelineComponent, 74 | ], 75 | }) 76 | export class TimelineModule { 77 | static forChild(config?: ITimelineModuleInitializationConfig): ModuleWithProviders { 78 | return { 79 | ngModule: TimelineModule, 80 | providers: [ 81 | config?.strategyManager ?? StrategyManager, 82 | config?.dayScaleGenerator ?? DayScaleGenerator, 83 | config?.weekScaleGenerator ?? WeekScaleGenerator, 84 | config?.monthScaleGenerator ?? MonthScaleGenerator, 85 | { 86 | provide: DAY_SCALE_GENERATOR_CONFIG, 87 | useValue: config?.dayScaleConfig 88 | }, 89 | { 90 | provide: WEEK_SCALE_GENERATOR_CONFIG, 91 | useValue: config?.weekScaleConfig 92 | }, 93 | { 94 | provide: MONTH_SCALE_GENERATOR_CONFIG, 95 | useValue: config?.monthScaleConfig 96 | }, 97 | ] 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /projects/angular-calendar-timeline/src/lib/view-mode-adaptor/base-view-mode-adaptor.ts: -------------------------------------------------------------------------------- 1 | import { IViewModeAdaptor } from "../models"; 2 | 3 | export abstract class BaseViewModeAdaptor implements IViewModeAdaptor { 4 | abstract getBeginningDateOfColumn(date: Date): Date; 5 | abstract getEndingDateOfColumn(date: Date): Date; 6 | abstract addColumnToDate(date: Date, columns: number): Date; 7 | abstract getUniqueColumnsWithinRange(date: Date, date2: Date): number; 8 | abstract getDurationInColumns(startDate: Date, endDate: Date): number; 9 | 10 | getMiddleDate(startDate: Date, endDate: Date): Date { 11 | const uniqueColumns = this.getUniqueColumnsWithinRange(startDate, endDate); 12 | 13 | return this.addColumnToDate(this.getBeginningDateOfColumn(startDate), uniqueColumns / 2); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /projects/angular-calendar-timeline/src/lib/view-mode-adaptor/days-view-mode-adaptor.spec.ts: -------------------------------------------------------------------------------- 1 | import { DaysViewModeAdaptor } from './days-view-mode-adaptor'; 2 | 3 | describe('DaysViewModeAdaptor', () => { 4 | let viewModeAdaptor: DaysViewModeAdaptor; 5 | 6 | beforeEach(() => { 7 | viewModeAdaptor = new DaysViewModeAdaptor(); 8 | }); 9 | 10 | it('Midway time between between 18.11.2022 and 20.11.2022 should be 19.11.2022', () => { 11 | const firstDate = new Date(2022, 10, 18); 12 | const secondDate = new Date(2022, 10, 20); 13 | const midwayDate = new Date(viewModeAdaptor.getMiddleDate(firstDate, secondDate)); 14 | 15 | expect(midwayDate.getDate()).toEqual(19); 16 | }); 17 | 18 | it('Midway time between between 18.11.2022 and 19.11.2022 should be beginning of 19.11.2022', () => { 19 | const firstDate = new Date(2022, 10, 18); 20 | const secondDate = new Date(2022, 10, 19); 21 | const midwayDate = new Date(viewModeAdaptor.getMiddleDate(firstDate, secondDate)); 22 | 23 | expect(midwayDate.getDate()).toEqual(19); 24 | }); 25 | 26 | it('Hours of average date between start and end dates should be 12', () => { 27 | const firstDate = new Date(); 28 | const secondDate = new Date(); 29 | expect(new Date(viewModeAdaptor.getMiddleDate(firstDate, secondDate)).getHours()).toEqual(12); 30 | }); 31 | 32 | it('Count of unique days between 31.12.2021 and 03.01.2022 should be 4', () => { 33 | const firstDate = new Date(2021, 11, 31); 34 | const secondDate = new Date(2022, 0, 3); 35 | const countOfDays = viewModeAdaptor.getUniqueColumnsWithinRange(firstDate, secondDate); 36 | expect(countOfDays).toEqual(4); 37 | }); 38 | 39 | it('Count of unique days between the same dates should be 1', () => { 40 | const firstDate = new Date(2022, 11, 31); 41 | const secondDate = new Date(2022, 11, 31, 2); 42 | const countOfDays = viewModeAdaptor.getUniqueColumnsWithinRange(firstDate, secondDate); 43 | expect(countOfDays).toEqual(1); 44 | }); 45 | 46 | it('Time duration in days between 31.12.2021 00:00 and 01.01.2022 00:00 should be 1', () => { 47 | const firstDate = new Date(2021, 11, 31, 0, 0, 0, 0); 48 | const secondDate = new Date(2022, 0, 1, 0, 0, 0, 0); 49 | const countOfDays = viewModeAdaptor.getDurationInColumns(firstDate, secondDate); 50 | expect(countOfDays).toEqual(1); 51 | }); 52 | 53 | it('Time duration in days between 31.12.2021 12:00 and 01.01.2022 11:00 should be less then 1', () => { 54 | const firstDate = new Date(2021, 11, 31, 12, 0, 0, 0); 55 | const secondDate = new Date(2022, 0, 1, 11, 0, 0, 0); 56 | const countOfDays = viewModeAdaptor.getDurationInColumns(firstDate, secondDate); 57 | expect(countOfDays).toBeLessThan(1); 58 | }); 59 | 60 | it('Column should begins at 00:00', () => { 61 | const date = new Date(2023, 11, 30); 62 | const beginningDate = viewModeAdaptor.getBeginningDateOfColumn(date); 63 | expect(beginningDate.getHours()).toBe(0); 64 | expect(beginningDate.getMinutes()).toBe(0); 65 | expect(beginningDate.getDate()).toBe(30); 66 | expect(beginningDate.getMonth()).toBe(11); 67 | expect(beginningDate.getFullYear()).toBe(2023); 68 | }); 69 | 70 | it('Column should ends at 23:59', () => { 71 | const date = new Date(2023, 11, 30); 72 | const beginningDate = viewModeAdaptor.getEndingDateOfColumn(date); 73 | expect(beginningDate.getHours()).toBe(23); 74 | expect(beginningDate.getMinutes()).toBe(59); 75 | expect(beginningDate.getDate()).toBe(30); 76 | expect(beginningDate.getMonth()).toBe(11); 77 | expect(beginningDate.getFullYear()).toBe(2023); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /projects/angular-calendar-timeline/src/lib/view-mode-adaptor/days-view-mode-adaptor.ts: -------------------------------------------------------------------------------- 1 | import { DatesCacheDecorator } from '../helpers/cache'; 2 | import { DateHelpers, MillisecondsToTime } from "../helpers/date-helpers"; 3 | import { BaseViewModeAdaptor} from "./base-view-mode-adaptor"; 4 | import { IViewModeAdaptor } from "../models"; 5 | 6 | export class DaysViewModeAdaptor extends BaseViewModeAdaptor implements IViewModeAdaptor { 7 | @DatesCacheDecorator() 8 | getUniqueColumnsWithinRange(start: Date, end: Date): number { 9 | const startDate = new Date(start.getFullYear(), start.getMonth(), start.getDate()); 10 | const endDate = new Date(end.getFullYear(), end.getMonth(), end.getDate()); 11 | 12 | return Math.round(Math.abs((startDate.getTime() - endDate.getTime()) / MillisecondsToTime.Day)) + 1; 13 | } 14 | 15 | @DatesCacheDecorator() 16 | getDurationInColumns(startDate: Date, endDate: Date): number { 17 | return Math.abs((startDate.getTime() - endDate.getTime()) / MillisecondsToTime.Day); 18 | } 19 | 20 | addColumnToDate(date: Date, days: number): Date { 21 | const newDate = new Date(date); 22 | newDate.setDate(date.getDate() + days); 23 | newDate.setHours(newDate.getHours() + ((days % 1) * 24)); 24 | 25 | return newDate; 26 | } 27 | 28 | getEndingDateOfColumn(date: Date): Date { 29 | return DateHelpers.dayEndingTime(date); 30 | } 31 | 32 | getBeginningDateOfColumn(date: Date): Date { 33 | return DateHelpers.dayBeginningTime(date); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /projects/angular-calendar-timeline/src/lib/view-mode-adaptor/months-view-mode-adaptor.spec.ts: -------------------------------------------------------------------------------- 1 | import { MonthsViewModeAdaptor } from './months-view-mode-adaptor'; 2 | 3 | describe('MonthsViewModeAdaptor', () => { 4 | let viewModeAdaptor: MonthsViewModeAdaptor; 5 | 6 | beforeEach(() => { 7 | viewModeAdaptor = new MonthsViewModeAdaptor(); 8 | }); 9 | 10 | it('Midway time between between 18.11.2022 and 19.11.2022 should be 15.11.2022', () => { 11 | const firstDate = new Date(2022, 10, 18); 12 | const secondDate = new Date(2022, 10, 19); 13 | const midwayDate = new Date(viewModeAdaptor.getMiddleDate(firstDate, secondDate)); 14 | 15 | expect(midwayDate.getDate()).toEqual(16); 16 | expect(midwayDate.getHours()).toEqual(0); 17 | expect(midwayDate.getMonth()).toEqual(firstDate.getMonth()); 18 | }); 19 | 20 | it('Midway time between between 18.10.2022 and 18.11.2022 should be 30.10.2022 or 01.11.2022', () => { 21 | const firstDate = new Date(2022, 9, 18); 22 | const secondDate = new Date(2022, 10, 20); 23 | const midwayDate = new Date(viewModeAdaptor.getMiddleDate(firstDate, secondDate)); 24 | 25 | expect([31, 1].includes(midwayDate.getDate())).toBeTrue(); 26 | expect([9, 10].includes(midwayDate.getMonth())).toBeTrue(); 27 | }); 28 | 29 | it('Midway time in January 2022 should be middle of 16.01.2022', () => { 30 | const firstDate = new Date(2022, 0, 10); 31 | const secondDate = new Date(2022, 0, 13); 32 | const midwayDate = new Date(viewModeAdaptor.getMiddleDate(firstDate, secondDate)); 33 | expect(midwayDate.getDate()).toEqual(16); 34 | expect(midwayDate.getHours()).toEqual(12); 35 | }); 36 | 37 | it('Count of unique months between 31.12.2021 and 03.01.2022 should be 2', () => { 38 | const firstDate = new Date(2021, 11, 31); 39 | const secondDate = new Date(2022, 0, 3); 40 | const countOfDays = viewModeAdaptor.getUniqueColumnsWithinRange(firstDate, secondDate); 41 | expect(countOfDays).toEqual(2); 42 | }); 43 | 44 | it('Count of unique months between 30.12.2021 and 31.01.2022 should be 1', () => { 45 | const firstDate = new Date(2022, 11, 30); 46 | const secondDate = new Date(2022, 11, 31); 47 | const countOfMonths = viewModeAdaptor.getUniqueColumnsWithinRange(firstDate, secondDate); 48 | expect(countOfMonths).toEqual(1); 49 | }); 50 | 51 | it('Time duration in months between 01.01.2022 and 01.02.2022 should be close to 1', () => { 52 | const firstDate = new Date(2022, 0, 1, 0, 0, 0, 0); 53 | const secondDate = new Date(2022, 1, 1, 0, 0, 0, 0); 54 | const countOfMonths = viewModeAdaptor.getDurationInColumns(firstDate, secondDate); 55 | expect(countOfMonths).toBeCloseTo(1); 56 | }); 57 | 58 | it('Time duration in months between 01.01.2022 and 01.05.2022 should be close to 4', () => { 59 | const firstDate = new Date(2022, 0, 1, 0, 0, 0, 0); 60 | const secondDate = new Date(2022, 4, 1, 0, 0, 0, 0); 61 | const countOfMonths = viewModeAdaptor.getDurationInColumns(firstDate, secondDate); 62 | expect(countOfMonths).toBeCloseTo(4); 63 | }); 64 | 65 | it('Time duration in months between 31.12.2021 and 29.01.2022 should be less then 1', () => { 66 | const firstDate = new Date(2021, 11, 31); 67 | const secondDate = new Date(2022, 0, 1); 68 | const countOfMonths = viewModeAdaptor.getDurationInColumns(firstDate, secondDate); 69 | expect(countOfMonths).toBeLessThan(1); 70 | }); 71 | 72 | it('Column should begins on 01.12.2023 when column date is in (01-31).12.2023 dates range', () => { 73 | const date = new Date(2023, 11, 30); 74 | const beginningDate = viewModeAdaptor.getBeginningDateOfColumn(date); 75 | expect(beginningDate.getDate()).toBe(1); 76 | expect(beginningDate.getMonth()).toBe(11); 77 | expect(beginningDate.getFullYear()).toBe(2023); 78 | }); 79 | 80 | it('Column should ends on 31.12.2023 when column date is in (01-31).12.2023 dates range', () => { 81 | const date = new Date(2023, 11, 30); 82 | const beginningDate = viewModeAdaptor.getEndingDateOfColumn(date); 83 | expect(beginningDate.getDate()).toBe(31); 84 | expect(beginningDate.getMonth()).toBe(11); 85 | expect(beginningDate.getFullYear()).toBe(2023); 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /projects/angular-calendar-timeline/src/lib/view-mode-adaptor/months-view-mode-adaptor.ts: -------------------------------------------------------------------------------- 1 | import { DatesCacheDecorator } from '../helpers/cache'; 2 | import { DateHelpers } from "../helpers/date-helpers"; 3 | import { BaseViewModeAdaptor} from "./base-view-mode-adaptor"; 4 | import { IViewModeAdaptor } from "../models"; 5 | 6 | export class MonthsViewModeAdaptor extends BaseViewModeAdaptor implements IViewModeAdaptor { 7 | getBeginningDateOfColumn(date: Date): Date { 8 | const start = new Date(date); 9 | start.setDate(1); 10 | 11 | return DateHelpers.dayBeginningTime(start); 12 | } 13 | 14 | getEndingDateOfColumn(date: Date): Date { 15 | const end = new Date(date); 16 | end.setDate(DateHelpers.lastDayOfMonth(date).getDate()); 17 | 18 | return DateHelpers.dayEndingTime(end); 19 | } 20 | 21 | @DatesCacheDecorator() 22 | getUniqueColumnsWithinRange(startDate: Date, endDate: Date): number { 23 | const diff = this._getCountOfFullMonths(startDate, endDate); 24 | 25 | return (diff < 0 ? 0 : diff) + 1; 26 | } 27 | 28 | @DatesCacheDecorator() 29 | getDurationInColumns(startDate: Date, endDate: Date): number { 30 | const diff = this._getCountOfFullMonths(startDate, endDate); 31 | const firstMonthCompletedPercent = ((startDate.getDate() - 1) + (startDate.getHours() / 24)) / DateHelpers.getDaysInMonth(startDate); 32 | const secondMonthCompletedPercent = ((endDate.getDate() - 1) + (endDate.getHours() / 24)) / DateHelpers.getDaysInMonth(endDate); 33 | 34 | return diff - firstMonthCompletedPercent + secondMonthCompletedPercent; 35 | } 36 | 37 | addColumnToDate(date: Date, months: number): Date { 38 | const newDate = new Date(date); 39 | newDate.setMonth(date.getMonth() + months); 40 | const days = DateHelpers.getDaysInMonth(newDate) * (months % 1); 41 | newDate.setDate(newDate.getDate() + days); 42 | newDate.setHours(newDate.getHours() + ((days % 1) * 24)); 43 | 44 | return newDate; 45 | } 46 | 47 | private _getCountOfFullMonths(startDate: Date, endDate: Date): number { 48 | const yearsDiff = endDate.getFullYear() - startDate.getFullYear(); 49 | const startMonth = startDate.getMonth(); 50 | const endMonth = endDate.getMonth() + (12 * yearsDiff); 51 | 52 | return endMonth - startMonth; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /projects/angular-calendar-timeline/src/lib/view-mode-adaptor/weeks-view-mode-adaptor.spec.ts: -------------------------------------------------------------------------------- 1 | import { WeeksViewModeAdaptor } from './weeks-view-mode-adaptor'; 2 | 3 | describe('WeeksViewModeAdaptor', () => { 4 | let viewModeAdaptor: WeeksViewModeAdaptor; 5 | 6 | beforeEach(() => { 7 | viewModeAdaptor = new WeeksViewModeAdaptor(); 8 | }); 9 | 10 | it('Midway time between between 18.11.2022 and 19.11.2022 should be 17.11.2022', () => { 11 | const firstDate = new Date(2022, 10, 18); 12 | const secondDate = new Date(2022, 10, 19); 13 | const midwayDate = new Date(viewModeAdaptor.getMiddleDate(firstDate, secondDate)); 14 | expect(midwayDate.getDate()).toEqual(17); 15 | }); 16 | 17 | 18 | it('Count of unique weeks between 06.06.2021 and 01.07.2021 should be 4', () => { 19 | const firstDate = new Date('2021-06-06T21:00:00.000Z'); 20 | const secondDate = new Date('2021-07-01T18:00:00.000Z'); 21 | const countOfDays = viewModeAdaptor.getUniqueColumnsWithinRange(firstDate, secondDate); 22 | expect(countOfDays).toEqual(4); 23 | }); 24 | 25 | it('Count of unique weeks between 15.12.2021 and 16.12.2021 should be 1', () => { 26 | const firstDate = new Date(2021, 11, 15); 27 | const secondDate = new Date(2021, 11, 16); 28 | const countOfWeeks = viewModeAdaptor.getUniqueColumnsWithinRange(firstDate, secondDate); 29 | expect(countOfWeeks).toEqual(1); 30 | }); 31 | 32 | it('Count of unique weeks between 15.12.2021 and 20.12.2021 should be 2', () => { 33 | const firstDate = new Date(2021, 11, 15); 34 | const secondDate = new Date(2021, 11, 20); 35 | const countOfWeeks = viewModeAdaptor.getUniqueColumnsWithinRange(firstDate, secondDate); 36 | expect(countOfWeeks).toEqual(2); 37 | }); 38 | 39 | it('Time duration in weeks between 14.12.2021 and 21.12.2021 should be 1', () => { 40 | const firstDate = new Date(2021, 11, 14, 0, 0, 0, 0); 41 | const secondDate = new Date(2021, 11, 21, 0, 0, 0, 0); 42 | const countOfWeeks = viewModeAdaptor.getDurationInColumns(firstDate, secondDate); 43 | expect(countOfWeeks).toEqual(1); 44 | }); 45 | 46 | it('Column should begins on 05.06.2023 when date is 06.11.06.2023', () => { 47 | const date = new Date(2023, 5, 6); 48 | const beginningDate = viewModeAdaptor.getBeginningDateOfColumn(date); 49 | expect(beginningDate.getDate()).toBe(5); 50 | expect(beginningDate.getMonth()).toBe(5); 51 | expect(beginningDate.getFullYear()).toBe(2023); 52 | }); 53 | 54 | it('Column should begins on 10.06.2024 when date is 10.06.2024', () => { 55 | const date = new Date(2024, 5, 10); 56 | const beginningDate = viewModeAdaptor.getBeginningDateOfColumn(date); 57 | expect(beginningDate.getDate()).toBe(10); 58 | expect(beginningDate.getMonth()).toBe(5); 59 | expect(beginningDate.getFullYear()).toBe(2024); 60 | }); 61 | 62 | it('Column should ends on 11.06.2023 when date is in 07.06.2023', () => { 63 | const date = new Date(2023, 5, 7); 64 | const beginningDate = viewModeAdaptor.getEndingDateOfColumn(date); 65 | expect(beginningDate.getDate()).toBe(11); 66 | expect(beginningDate.getMonth()).toBe(5); 67 | expect(beginningDate.getFullYear()).toBe(2023); 68 | }); 69 | 70 | it('Column should ends on 18.06.2023 when date is 17.06.2023', () => { 71 | const date = new Date(2023, 5, 17); 72 | const beginningDate = viewModeAdaptor.getEndingDateOfColumn(date); 73 | expect(beginningDate.getDate()).toBe(18); 74 | expect(beginningDate.getMonth()).toBe(5); 75 | expect(beginningDate.getFullYear()).toBe(2023); 76 | }); 77 | 78 | it('Column should ends on 9.06.2024 when date is 9.06.2024', () => { 79 | const date = new Date(2024, 5, 9); 80 | const beginningDate = viewModeAdaptor.getEndingDateOfColumn(date); 81 | expect(beginningDate.getDate()).toBe(9); 82 | expect(beginningDate.getMonth()).toBe(5); 83 | expect(beginningDate.getFullYear()).toBe(2024); 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /projects/angular-calendar-timeline/src/lib/view-mode-adaptor/weeks-view-mode-adaptor.ts: -------------------------------------------------------------------------------- 1 | import { DatesCacheDecorator } from '../helpers/cache'; 2 | import { DateHelpers, MillisecondsToTime } from "../helpers/date-helpers"; 3 | import { BaseViewModeAdaptor} from "./base-view-mode-adaptor"; 4 | import { IViewModeAdaptor } from "../models"; 5 | 6 | export class WeeksViewModeAdaptor extends BaseViewModeAdaptor implements IViewModeAdaptor { 7 | @DatesCacheDecorator() 8 | getUniqueColumnsWithinRange(start: Date, end: Date): number { 9 | const monday = DateHelpers.firstDayOfWeek(start); 10 | const last = DateHelpers.lastDayOfWeek(end); 11 | 12 | return Math.round(this.getDurationInColumns(monday, last)); 13 | } 14 | 15 | @DatesCacheDecorator() 16 | getDurationInColumns(startDate: Date, endDate: Date): number { 17 | return Math.abs((startDate.getTime() - endDate.getTime()) / MillisecondsToTime.Week); 18 | } 19 | 20 | addColumnToDate(date: Date, weeks: number): Date { 21 | const newDate = new Date(date); 22 | newDate.setDate(date.getDate() + (7 * weeks)); 23 | newDate.setHours(newDate.getHours() + (((weeks / 7) % 1) * 24)); 24 | 25 | return newDate; 26 | } 27 | 28 | getBeginningDateOfColumn(date: Date): Date { 29 | const start = DateHelpers.firstDayOfWeek(new Date(date)); 30 | 31 | return DateHelpers.dayBeginningTime(start); 32 | } 33 | 34 | getEndingDateOfColumn(date: Date): Date { 35 | const end = DateHelpers.lastDayOfWeek(new Date(date)); 36 | 37 | return DateHelpers.dayEndingTime(end); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /projects/angular-calendar-timeline/src/lib/zooms-handler/zooms-handler.ts: -------------------------------------------------------------------------------- 1 | import { BehaviorSubject, Observable } from "rxjs"; 2 | import { ITimelineZoom, TimelineViewMode } from "../models/zoom"; 3 | import { IIndexedZoom, IZoomsHandler } from "../models"; 4 | 5 | export class ZoomsHandler implements IZoomsHandler { 6 | private _zooms: IIndexedZoom[]; 7 | private _activeZoom$ = new BehaviorSubject>(null); 8 | 9 | activeZoom$: Observable> = this._activeZoom$.asObservable(); 10 | 11 | get activeZoom(): IIndexedZoom { 12 | return this._activeZoom$.value; 13 | } 14 | 15 | get zooms(): IIndexedZoom[] { 16 | return this._zooms; 17 | } 18 | 19 | constructor(zooms: ITimelineZoom[]) { 20 | this.setZooms(zooms); 21 | } 22 | 23 | setZooms(zooms: ITimelineZoom[]): void { 24 | this._zooms = (zooms ?? []).map((item, index) => ({...item, index})); 25 | this._activeZoom$.next(this.getLastZoom()); 26 | } 27 | 28 | getFirstZoom(): IIndexedZoom { 29 | return this._zooms[0]; 30 | } 31 | 32 | getLastZoom(): IIndexedZoom { 33 | return this._zooms[this._zooms.length - 1]; 34 | } 35 | 36 | zoomIn(): void { 37 | let newZoomIndex = this.activeZoom.index + 1; 38 | const lastZoomIndex = this.getLastZoom().index; 39 | if (newZoomIndex > lastZoomIndex) { 40 | newZoomIndex = lastZoomIndex; 41 | } 42 | 43 | this.changeActiveZoom(this._zooms[newZoomIndex]); 44 | } 45 | 46 | zoomOut(): void { 47 | let newZoomIndex = this.activeZoom.index - 1; 48 | const firstZoomIndex = this.getFirstZoom().index; 49 | if (newZoomIndex < firstZoomIndex) { 50 | newZoomIndex = firstZoomIndex; 51 | } 52 | 53 | this.changeActiveZoom(this._zooms[newZoomIndex]); 54 | } 55 | 56 | changeActiveZoom(zoom: ITimelineZoom): void { 57 | if (zoom) { 58 | this._activeZoom$.next(this._zooms[this._findZoomIndex(zoom)]); 59 | } 60 | } 61 | 62 | isZoomActive(zoom: ITimelineZoom): boolean { 63 | return this._findZoomIndex(zoom) === this.activeZoom.index; 64 | } 65 | 66 | private _findZoomIndex(zoom: ITimelineZoom): number { 67 | return this._zooms.findIndex(i => i.columnWidth === zoom.columnWidth && i.viewMode === zoom.viewMode); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /projects/angular-calendar-timeline/src/lib/zooms-handler/zooms.ts: -------------------------------------------------------------------------------- 1 | import { InjectionToken } from "@angular/core"; 2 | import { ITimelineZoom, TimelineViewMode } from "../models"; 3 | 4 | export const ZOOMS = new InjectionToken('Zooms'); 5 | 6 | export const DefaultZooms: ITimelineZoom[] = [ 7 | {columnWidth: 45, viewMode: TimelineViewMode.Month}, 8 | {columnWidth: 60, viewMode: TimelineViewMode.Month}, 9 | {columnWidth: 80, viewMode: TimelineViewMode.Month}, 10 | {columnWidth: 110, viewMode: TimelineViewMode.Month}, 11 | {columnWidth: 140, viewMode: TimelineViewMode.Month}, 12 | {columnWidth: 200, viewMode: TimelineViewMode.Month}, 13 | {columnWidth: 240, viewMode: TimelineViewMode.Month}, 14 | {columnWidth: 60, viewMode: TimelineViewMode.Week}, 15 | {columnWidth: 80, viewMode: TimelineViewMode.Week}, 16 | {columnWidth: 110, viewMode: TimelineViewMode.Week}, 17 | {columnWidth: 140, viewMode: TimelineViewMode.Week}, 18 | {columnWidth: 200, viewMode: TimelineViewMode.Week}, 19 | {columnWidth: 240, viewMode: TimelineViewMode.Week}, 20 | {columnWidth: 45, viewMode: TimelineViewMode.Day}, 21 | {columnWidth: 60, viewMode: TimelineViewMode.Day}, 22 | {columnWidth: 80, viewMode: TimelineViewMode.Day}, 23 | {columnWidth: 110, viewMode: TimelineViewMode.Day}, 24 | {columnWidth: 140, viewMode: TimelineViewMode.Day}, 25 | {columnWidth: 200, viewMode: TimelineViewMode.Day}, 26 | {columnWidth: 240, viewMode: TimelineViewMode.Day}, 27 | ]; 28 | -------------------------------------------------------------------------------- /projects/angular-calendar-timeline/src/public-api.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Public API Surface of angular-calendar-timeline 3 | */ 4 | 5 | export * from './lib/timeline.module'; 6 | export * from './lib/timeline.component'; 7 | export * from './lib/models'; 8 | export * from './lib/strategy-manager'; 9 | export { DefaultZooms } from "./lib/zooms-handler/zooms"; 10 | export { DayScaleGenerator } from "./lib/scale-generator/day-scale-generator"; 11 | export { WeekScaleGenerator } from "./lib/scale-generator/week-scale-generator"; 12 | export { MonthScaleGenerator } from "./lib/scale-generator/month-scale-generator"; 13 | -------------------------------------------------------------------------------- /projects/angular-calendar-timeline/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js'; 4 | import 'zone.js/testing'; 5 | import { getTestBed } from '@angular/core/testing'; 6 | import { 7 | BrowserDynamicTestingModule, 8 | platformBrowserDynamicTesting 9 | } from '@angular/platform-browser-dynamic/testing'; 10 | 11 | // First, initialize the Angular testing environment. 12 | getTestBed().initTestEnvironment( 13 | BrowserDynamicTestingModule, 14 | platformBrowserDynamicTesting(), 15 | ); 16 | -------------------------------------------------------------------------------- /projects/angular-calendar-timeline/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../out-tsc/lib", 6 | "declaration": true, 7 | "declarationMap": true, 8 | "inlineSources": true, 9 | "preserveSymlinks": true, 10 | "types": [] 11 | }, 12 | "exclude": [ 13 | "src/test.ts", 14 | "**/*.spec.ts" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /projects/angular-calendar-timeline/tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.lib.json", 4 | "compilerOptions": { 5 | "declarationMap": false, 6 | }, 7 | "angularCompilerOptions": { 8 | "compilationMode": "partial" 9 | }, 10 | } 11 | -------------------------------------------------------------------------------- /projects/angular-calendar-timeline/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts" 12 | ], 13 | "include": [ 14 | "**/*.spec.ts", 15 | "**/*.d.ts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 |
    2 | 3 | 4 |
    5 | 11 | 12 |
    13 |
    14 | -------------------------------------------------------------------------------- /src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | .timeline-wrapper { 2 | height: 700px; 3 | } 4 | 5 | .line { 6 | position: absolute; 7 | width: 1px; 8 | height: 100%; 9 | background-color: #d0d0d0; 10 | } 11 | 12 | .item-content { 13 | height: 100%; 14 | width: 100%; 15 | color: #fff; 16 | padding: 5px; 17 | background-color: #ce9e4a; 18 | border: 1px solid black; 19 | box-sizing: border-box; 20 | } 21 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ViewChild } from '@angular/core'; 2 | import { 3 | IItemRowChangedEvent, 4 | IItemTimeChangedEvent, 5 | ITimelineItem, 6 | TimelineComponent 7 | } from "angular-calendar-timeline"; 8 | import { registerLocaleData } from "@angular/common"; 9 | import localeUk from "@angular/common/locales/uk"; 10 | import { CustomViewMode } from "./custom-strategy"; 11 | 12 | registerLocaleData(localeUk); 13 | 14 | @Component({ 15 | selector: 'app-root', 16 | templateUrl: './app.component.html', 17 | styleUrls: ['./app.component.scss'] 18 | }) 19 | export class AppComponent { 20 | @ViewChild('timelineComponent', {static: true}) timeline: TimelineComponent; 21 | 22 | label = 'Label'; 23 | 24 | locale = 'en'; 25 | 26 | items: ITimelineItem[] = [ 27 | { 28 | startDate: new Date('2025-07-08T00:00:00'), 29 | endDate: new Date('2025-07-09T00:00:00'), 30 | id: 1, 31 | name: "First", 32 | canResizeLeft: true, 33 | canResizeRight: true, 34 | canDragX: true, 35 | canDragY: true, 36 | }, 37 | { 38 | startDate: new Date('2025-07-09T00:00:00'), 39 | endDate: new Date('2025-07-19T00:00:00'), 40 | id: 2, 41 | name: "Second", 42 | canResizeLeft: true, 43 | canResizeRight: true, 44 | canDragX: true, 45 | canDragY: true, 46 | childrenItems: [ 47 | { 48 | startDate: new Date('2025-07-09T00:00:00'), 49 | endDate: new Date('2025-07-20T00:00:00'), 50 | id: 3, 51 | name: "2.1", 52 | canResizeLeft: true, 53 | canResizeRight: true, 54 | canDragX: true, 55 | canDragY: true, 56 | childrenItems: [ 57 | { 58 | startDate: new Date('2025-07-19T00:00:00'), 59 | endDate: new Date('2025-07-20T00:00:00'), 60 | id: 7, 61 | name: "2.1.1", 62 | canResizeLeft: true, 63 | canResizeRight: true, 64 | canDragX: true, 65 | canDragY: true, 66 | } 67 | ] 68 | }, 69 | { 70 | startDate: new Date('2025-07-09T00:00:00'), 71 | endDate: new Date('2025-07-20T00:00:00'), 72 | id: 6, 73 | name: "2.2", 74 | canResizeLeft: true, 75 | canResizeRight: true, 76 | canDragX: true, 77 | canDragY: true, 78 | } 79 | ] 80 | }, 81 | { 82 | startDate: new Date('2025-08-09T00:00:00'), 83 | endDate: new Date('2025-08-19T00:00:00'), 84 | id: 4, 85 | name: "Third", 86 | canResizeLeft: true, 87 | canResizeRight: true, 88 | canDragX: true, 89 | canDragY: true, 90 | childrenItems: [ 91 | { 92 | startDate: new Date('2025-08-09T00:00:00'), 93 | endDate: new Date('2025-08-20T00:00:00'), 94 | id: 5, 95 | name: "3.1", 96 | canResizeLeft: true, 97 | canResizeRight: true, 98 | canDragX: true, 99 | canDragY: true, 100 | } 101 | ] 102 | }, 103 | { 104 | name: "Stream", 105 | id: 6, 106 | streamItems: [ 107 | { 108 | startDate: new Date('2025-08-09T00:00:00'), 109 | endDate: new Date('2025-08-20T00:00:00'), 110 | id: 7, 111 | name: "4", 112 | canResizeLeft: true, 113 | canResizeRight: true, 114 | canDragX: true, 115 | canDragY: true, 116 | }, 117 | { 118 | startDate: new Date('2025-08-09T00:00:00'), 119 | endDate: new Date('2025-08-20T00:00:00'), 120 | id: 8, 121 | name: "5", 122 | canResizeLeft: true, 123 | canResizeRight: true, 124 | canDragX: true, 125 | canDragY: true, 126 | }, 127 | { 128 | startDate: new Date('2025-07-09T00:00:00'), 129 | endDate: new Date('2025-07-20T00:00:00'), 130 | id: 9, 131 | name: "6", 132 | canResizeLeft: true, 133 | canResizeRight: true, 134 | canDragX: true, 135 | canDragY: true, 136 | } 137 | ], 138 | childrenItems: [ 139 | { 140 | id: 11, 141 | name: "Stream 11", 142 | streamItems: [ 143 | { 144 | startDate: new Date('2025-07-09T00:00:00'), 145 | endDate: new Date('2025-07-20T00:00:00'), 146 | id: 14, 147 | name: "Stream 11 (1)", 148 | canResizeLeft: true, 149 | canResizeRight: true, 150 | canDragX: true, 151 | canDragY: true, 152 | }, 153 | { 154 | startDate: new Date('2025-07-09T00:00:00'), 155 | endDate: new Date('2025-07-20T00:00:00'), 156 | id: 15, 157 | name: "Stream 11 (2)", 158 | canResizeLeft: true, 159 | canResizeRight: true, 160 | canDragX: true, 161 | canDragY: true, 162 | }, 163 | ] 164 | }, 165 | { 166 | startDate: new Date('2025-08-09T00:00:00'), 167 | endDate: new Date('2025-08-20T00:00:00'), 168 | id: 12, 169 | name: "21e25", 170 | canResizeLeft: true, 171 | canResizeRight: true, 172 | canDragX: true, 173 | canDragY: true, 174 | childrenItems: [ 175 | { 176 | startDate: new Date('2025-09-09T00:00:00'), 177 | endDate: new Date('2025-09-20T00:00:00'), 178 | id: 13, 179 | name: "asda", 180 | canResizeLeft: true, 181 | canResizeRight: true, 182 | canDragX: true, 183 | canDragY: true, 184 | }, 185 | ] 186 | }, 187 | ] 188 | } 189 | ]; 190 | 191 | onItemTimeChanged(event: IItemTimeChangedEvent): void { 192 | const item = event.item; 193 | item.startDate = event.newStartDate ?? item.startDate; 194 | item.endDate = event.newEndDate ?? item.endDate; 195 | this.items = [...this.items]; 196 | } 197 | 198 | onItemRowChanged(event: IItemRowChangedEvent): void { 199 | console.log(event); 200 | } 201 | } 202 | 203 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { AppComponent } from './app.component'; 4 | import { RouterModule } from "@angular/router"; 5 | import { TimelineModule } from "angular-calendar-timeline"; 6 | import { TimelineZoomComponent } from "./timeline-zoom/timeline-zoom.component"; 7 | 8 | @NgModule({ 9 | declarations: [ 10 | AppComponent, 11 | TimelineZoomComponent 12 | ], 13 | imports: [ 14 | BrowserModule, 15 | TimelineModule.forChild(), 16 | RouterModule.forRoot([ 17 | { 18 | path: '', 19 | component: AppComponent 20 | } 21 | ]) 22 | ], 23 | 24 | bootstrap: [AppComponent] 25 | }) 26 | export class AppModule { 27 | } 28 | -------------------------------------------------------------------------------- /src/app/custom-strategy.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IScaleGenerator, 3 | IStrategyManager, ITimelineZoom, 4 | IViewModeAdaptor, 5 | StrategyManager, 6 | TimelineViewMode 7 | } from "angular-calendar-timeline"; 8 | 9 | export enum CustomViewMode { 10 | Custom = 1, 11 | Day = TimelineViewMode.Day, 12 | Week = TimelineViewMode.Week, 13 | Month = TimelineViewMode.Month, 14 | } 15 | 16 | export class TimelineZoom implements ITimelineZoom { 17 | viewMode!: CustomViewMode; 18 | columnWidth!: number; 19 | } 20 | 21 | class CustomStrategyManager extends StrategyManager implements IStrategyManager { 22 | override getScaleGenerator(viewMode): IScaleGenerator { 23 | if (viewMode === CustomViewMode.Custom) { 24 | return null; // your custom logic here 25 | } 26 | 27 | return super.getScaleGenerator(viewMode); 28 | }; 29 | 30 | override getViewModeAdaptor(viewMode): IViewModeAdaptor { 31 | if (viewMode === CustomViewMode.Custom) { 32 | return null // custom adaptor; 33 | } 34 | 35 | return super.getViewModeAdaptor(viewMode); //This should be super.getViewModeAdaptor(viewMode); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/app/portfolio.interface.ts: -------------------------------------------------------------------------------- 1 | import { IIdObject } from "../timeline/models"; 2 | 3 | 4 | export interface IPortfolioProject extends IIdObject { 5 | portfolioId: number; 6 | name: string; 7 | description: string; 8 | startDate: string; 9 | endDate: string; 10 | allocatedBudget: number; 11 | budgetProgress: number; 12 | isProjectUsedInPortfolio: boolean; 13 | plannedAmountProject: number; 14 | allocatedBudgetProject: number; 15 | actualAmountProject: number; 16 | owner: { 17 | email: string; 18 | id: number; 19 | name: string; 20 | thumbnailUrl: string; 21 | }; 22 | } 23 | 24 | export interface IPortfolioSelectionProject extends IIdObject { 25 | name: string; 26 | isProjectUsedInPortfolio?: boolean; 27 | } 28 | 29 | export interface IPortfolioCategory extends IIdObject { 30 | name: string; 31 | parentId?: number; 32 | } 33 | 34 | export interface IPortfolio extends IIdObject { 35 | name: string; 36 | description: string; 37 | projects: IPortfolioProject[]; 38 | owner: IPortfolioManager; 39 | managers: IPortfolioManager[]; 40 | periodStartDate: string; 41 | periodEndDate: string; 42 | actualAmountProject: number; 43 | allocatedBudgetPortfolio: number; 44 | allocatedBudgetProject: number; 45 | budgetProgress: number; 46 | currencyCode: string; 47 | currencyId: number; 48 | currentProgress: number; 49 | totalProgress: number; 50 | attachments: IPortfolioAttachment[]; 51 | creatorId: number; 52 | parentId?: number; 53 | } 54 | 55 | export interface IPortfolioDetails { 56 | portfolio?: IPortfolio; 57 | categories?: IPortfolioCategory[]; 58 | } 59 | 60 | export interface IPortfolioAttachment extends IIdObject { 61 | expenseId: number; 62 | fileName: string; 63 | name: string; 64 | thumbnailUrl: string; 65 | attachmentType: any; 66 | uploadedAt?: string; 67 | contentType?: string; 68 | url?: string; 69 | } 70 | 71 | export interface IPortfolioManager extends IIdObject { 72 | name: string; 73 | email: string; 74 | } 75 | 76 | export interface IPortfolioProjects extends IIdObject { 77 | name: string; 78 | } 79 | -------------------------------------------------------------------------------- /src/app/timeline-zoom/timeline-zoom.component.html: -------------------------------------------------------------------------------- 1 | 5 | 6 |
    7 | 12 | 13 | 14 | 15 |
    16 | 17 | 21 | -------------------------------------------------------------------------------- /src/app/timeline-zoom/timeline-zoom.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: flex; 3 | align-items: center; 4 | margin: 20px 0; 5 | } 6 | 7 | button { 8 | margin: 0 5px; 9 | min-width: 60px; 10 | cursor: pointer; 11 | } 12 | -------------------------------------------------------------------------------- /src/app/timeline-zoom/timeline-zoom.component.ts: -------------------------------------------------------------------------------- 1 | import { AfterViewInit, Component, Input } from '@angular/core'; 2 | import { TimelineComponent } from "angular-calendar-timeline"; 3 | import { CustomViewMode } from "../custom-strategy"; 4 | 5 | @Component({ 6 | selector: 'app-timeline-zoom', 7 | templateUrl: 'timeline-zoom.component.html', 8 | styleUrls: ['./timeline-zoom.component.scss'], 9 | }) 10 | export class TimelineZoomComponent implements AfterViewInit { 11 | minZoomIndex: number; 12 | maxZoomIndex: number; 13 | currentZoomIndex: number; 14 | 15 | @Input() timelineComponent: TimelineComponent; 16 | 17 | ngAfterViewInit(): void { 18 | this.minZoomIndex = this.timelineComponent.zoomsHandler.getFirstZoom().index; 19 | this.maxZoomIndex = this.timelineComponent.zoomsHandler.getLastZoom().index; 20 | 21 | this.timelineComponent.zoomsHandler.activeZoom$ 22 | .subscribe((zoom) => this.currentZoomIndex = zoom.index); 23 | 24 | this.zoomAndFitToContent(); 25 | } 26 | 27 | zoomIn(): void { 28 | this.timelineComponent.zoomIn(); 29 | } 30 | 31 | zoomOut(): void { 32 | this.timelineComponent.zoomOut(); 33 | } 34 | 35 | scrollToToday(): void { 36 | this.timelineComponent.zoomFullIn(); 37 | this.timelineComponent.attachCameraToDate(new Date()); 38 | } 39 | 40 | zoomAndFitToContent(): void { 41 | this.timelineComponent.fitToContent(15); 42 | } 43 | 44 | changeZoom(event: Event): void { 45 | this.timelineComponent.changeZoomByIndex(+(event.target as HTMLInputElement).value); 46 | } 47 | } 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oOps1627/angular-calendar-timeline/68e3b211d0a0d820dda2d1b64728e2b1e96930b7/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/plugins/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oOps1627/angular-calendar-timeline/68e3b211d0a0d820dda2d1b64728e2b1e96930b7/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CalendarTimeline 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.error(err)); 13 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes recent versions of Safari, Chrome (including 12 | * Opera), Edge on the desktop, and iOS and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** 22 | * By default, zone.js will patch all possible macroTask and DomEvents 23 | * user can disable parts of macroTask/DomEvents patch by setting following flags 24 | * because those flags need to be set before `zone.js` being loaded, and webpack 25 | * will put import in the top of bundle, so user need to create a separate file 26 | * in this directory (for example: zone-flags.ts), and put the following flags 27 | * into that file, and then add the following code before importing zone.js. 28 | * import './zone-flags'; 29 | * 30 | * The flags allowed in zone-flags.ts are listed here. 31 | * 32 | * The following flags will work for all browsers. 33 | * 34 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 35 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 36 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 37 | * 38 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 39 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 40 | * 41 | * (window as any).__Zone_enable_cross_context_check = true; 42 | * 43 | */ 44 | 45 | /*************************************************************************************************** 46 | * Zone JS is required by default for Angular itself. 47 | */ 48 | import 'zone.js'; // Included with Angular CLI. 49 | 50 | 51 | /*************************************************************************************************** 52 | * APPLICATION IMPORTS 53 | */ 54 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | // First, initialize the Angular testing environment. 11 | getTestBed().initTestEnvironment( 12 | BrowserDynamicTestingModule, 13 | platformBrowserDynamicTesting(), 14 | ); 15 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": [ 9 | "src/main.ts", 10 | "src/polyfills.ts" 11 | ], 12 | "include": [ 13 | "src/**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "paths": { 6 | "angular-calendar-timeline": [ 7 | "projects/angular-calendar-timeline/src/public-api" 8 | ] 9 | }, 10 | "baseUrl": "./", 11 | "outDir": "./dist/out-tsc", 12 | "forceConsistentCasingInFileNames": true, 13 | "strict": false, 14 | "noImplicitOverride": true, 15 | "noPropertyAccessFromIndexSignature": true, 16 | "noImplicitReturns": true, 17 | "noFallthroughCasesInSwitch": true, 18 | "sourceMap": true, 19 | "declaration": false, 20 | "downlevelIteration": true, 21 | "experimentalDecorators": true, 22 | "moduleResolution": "node", 23 | "importHelpers": true, 24 | "target": "ES2022", 25 | "module": "es2020", 26 | "lib": [ 27 | "es2020", 28 | "dom" 29 | ], 30 | "useDefineForClassFields": false 31 | }, 32 | "angularCompilerOptions": { 33 | "enableI18nLegacyMessageIdFormat": false, 34 | "strictInjectionParameters": true, 35 | "strictInputAccessModifiers": true, 36 | "strictTemplates": true 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | --------------------------------------------------------------------------------