├── .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 | 
4 | 
5 |
6 |
7 |
8 | Angular 13+ timeline calendar
9 |
10 |
11 |
12 | [](https://github.com/oOps1627)
13 | [](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 | 
4 | 
5 |
6 |
7 |
8 | Angular 13+ timeline calendar
9 |
10 |
11 |
12 | [](https://github.com/oOps1627)
13 | [](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 |
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 |
3 | {{ 'today'}}
4 |
5 |
6 |
7 |
12 | -
13 |
14 | +
15 |
16 |
17 |
19 | Fit to content
20 |
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 |
--------------------------------------------------------------------------------