@hug/ngx-time-picker 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/README.md +6 -0
- package/karma.conf.js +45 -0
- package/ng-package.json +10 -0
- package/package.json +46 -0
- package/src/index.ts +1 -0
- package/src/test.ts +28 -0
- package/src/time-picker.component.html +14 -0
- package/src/time-picker.component.scss +27 -0
- package/src/time-picker.component.ts +317 -0
- package/tsconfig.lib.json +15 -0
- package/tsconfig.lib.prod.json +10 -0
- package/tsconfig.spec.json +17 -0
package/CHANGELOG.md
ADDED
package/README.md
ADDED
package/karma.conf.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
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 = 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/time-picker'),
|
|
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
|
+
failOnEmptyTestSuite: false,
|
|
43
|
+
restartOnFileChange: true
|
|
44
|
+
});
|
|
45
|
+
};
|
package/ng-package.json
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hug/ngx-time-picker",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "HUG Angular - time picker component",
|
|
5
|
+
"homepage": "https://github.com/dsi-hug/ngx-components",
|
|
6
|
+
"license": "GPL-3.0-only",
|
|
7
|
+
"author": "HUG - Hôpitaux Universitaires Genève",
|
|
8
|
+
"contributors": [
|
|
9
|
+
"badisi (https://github.com/badisi)",
|
|
10
|
+
"vapkse (https://github.com/vapkse)"
|
|
11
|
+
],
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/dsi-hug/ngx-components.git"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"angular",
|
|
18
|
+
"material",
|
|
19
|
+
"material design",
|
|
20
|
+
"components"
|
|
21
|
+
],
|
|
22
|
+
"sideEffects": false,
|
|
23
|
+
"scripts": {
|
|
24
|
+
"lint": "eslint . --fix",
|
|
25
|
+
"test": "ng test time-picker",
|
|
26
|
+
"test:ci": "ng test time-picker --watch=false --browsers=ChromeHeadless",
|
|
27
|
+
"build:ng": "ng build time-picker -c=production",
|
|
28
|
+
"build": "nx build:ng @hug/ngx-time-picker --verbose",
|
|
29
|
+
"release": "nx release -p=@hug/ngx-time-picker --yes --verbose",
|
|
30
|
+
"release:dry-run": "nx release -p=@hug/ngx-time-picker --verbose --dry-run"
|
|
31
|
+
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"@angular/common": ">= 14",
|
|
34
|
+
"@angular/core": ">= 14",
|
|
35
|
+
"@angular/cdk": ">= 14",
|
|
36
|
+
"@angular/forms": ">= 14",
|
|
37
|
+
"@angular/material": ">= 14",
|
|
38
|
+
"rxjs": ">= 7.0.0",
|
|
39
|
+
"date-fns": "^2.30.0",
|
|
40
|
+
"@hug/ngx-core": "1.1.4",
|
|
41
|
+
"@hug/ngx-numeric-stepper": "1.1.1"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"tslib": "^2.6.3"
|
|
45
|
+
}
|
|
46
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './time-picker.component';
|
package/src/test.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
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
|
+
|
|
6
|
+
import { getTestBed } from '@angular/core/testing';
|
|
7
|
+
import {
|
|
8
|
+
BrowserDynamicTestingModule,
|
|
9
|
+
platformBrowserDynamicTesting
|
|
10
|
+
} from '@angular/platform-browser-dynamic/testing';
|
|
11
|
+
|
|
12
|
+
declare const require: {
|
|
13
|
+
context: (path: string, deep?: boolean, filter?: RegExp) => {
|
|
14
|
+
<T>(id: string): T;
|
|
15
|
+
keys: () => string[];
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// First, initialize the Angular testing environment.
|
|
20
|
+
getTestBed().initTestEnvironment(
|
|
21
|
+
BrowserDynamicTestingModule,
|
|
22
|
+
platformBrowserDynamicTesting()
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
// Then we find all the tests.
|
|
26
|
+
const context = require.context('./', true, /\.spec\.ts$/);
|
|
27
|
+
// And load the modules.
|
|
28
|
+
context.keys().forEach(context);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<!-- Hours -->
|
|
2
|
+
<mat-form-field [appearance]="appearance" class="hours" [class.disabled]="disabled || mode === 'fullTimeWithHoursDisabled'" [style.display]="mode === 'minutesOnly' ? 'none' : 'block'" floatLabel="never">
|
|
3
|
+
<input #hours matInput type="text" [ngModel]="hoursValue | number:'2.'" (change)="onHoursChange$.next($event)" (keydown)="onKeyDown($event, 'hours')" [disabled]="disabled || mode === 'fullTimeWithHoursDisabled'" [maxLength]="2" autocomplete="off" [placeholder]="defaultPlaceholderHours" (click)="onClick('hours')" />
|
|
4
|
+
<numeric-stepper (increment)="incrementValue('hours')" (decrement)="decrementValue('hours')"></numeric-stepper>
|
|
5
|
+
</mat-form-field>
|
|
6
|
+
|
|
7
|
+
<!-- Separator -->
|
|
8
|
+
<span class="time-separator" [style.display]="mode === 'minutesOnly' || mode === 'hoursOnly' ? 'none' : 'block'">:</span>
|
|
9
|
+
|
|
10
|
+
<!-- Minutes -->
|
|
11
|
+
<mat-form-field [appearance]="appearance" class="minutes" [class.disabled]="disabled || mode === 'fullTimeWithMinutesDisabled'" [style.display]="mode === 'hoursOnly' ? 'none' : 'block'" floatLabel="never">
|
|
12
|
+
<input #minutes matInput type="text" [ngModel]="minutesValue | number:'2.'" (change)="onMinutesChange$.next($event)" (keydown)="onKeyDown($event, 'minutes')" [disabled]="disabled || mode === 'fullTimeWithMinutesDisabled'" [maxLength]="2" autocomplete="off" [placeholder]="defaultPlaceholderMinutes" (click)="onClick('minutes')" />
|
|
13
|
+
<numeric-stepper (increment)="incrementValue('minutes')" (decrement)="decrementValue('minutes')"></numeric-stepper>
|
|
14
|
+
</mat-form-field>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
time-picker {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: row;
|
|
4
|
+
align-items: baseline;
|
|
5
|
+
justify-content: center;
|
|
6
|
+
|
|
7
|
+
&:not([inform]) .mat-form-field-appearance-outline {
|
|
8
|
+
.mat-form-field-wrapper {
|
|
9
|
+
padding-bottom: 0;
|
|
10
|
+
|
|
11
|
+
.mat-form-field-infix {
|
|
12
|
+
border-bottom: 0.7em solid transparent;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
mat-form-field[numeric-stepper-form-field] {
|
|
18
|
+
width: 48px;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.time-separator {
|
|
22
|
+
width: 4px;
|
|
23
|
+
font-size: 14.5px;
|
|
24
|
+
line-height: 17px;
|
|
25
|
+
margin: 0 8px;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import { BooleanInput, coerceBooleanProperty, coerceNumberProperty, NumberInput } from '@angular/cdk/coercion';
|
|
2
|
+
import { CommonModule } from '@angular/common';
|
|
3
|
+
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, Input, Optional, Output, Self, ViewChild, ViewEncapsulation } from '@angular/core';
|
|
4
|
+
import { ControlValueAccessor, FormsModule, NgControl } from '@angular/forms';
|
|
5
|
+
import { MatFormFieldAppearance, MatFormFieldModule } from '@angular/material/form-field';
|
|
6
|
+
import { MatInputModule } from '@angular/material/input';
|
|
7
|
+
import { Destroy } from '@hug/ngx-core';
|
|
8
|
+
import { NumericStepperComponent } from '@hug/ngx-numeric-stepper';
|
|
9
|
+
import { isSameHour, set } from 'date-fns';
|
|
10
|
+
import { debounce, distinctUntilChanged, map, Subject, takeUntil, timer } from 'rxjs';
|
|
11
|
+
|
|
12
|
+
export type TimePickerDisplayMode = 'fullTime' | 'fullTimeWithHoursDisabled' | 'fullTimeWithMinutesDisabled' | 'hoursOnly' | 'minutesOnly';
|
|
13
|
+
|
|
14
|
+
export type DateOrDuration = Date | Duration;
|
|
15
|
+
|
|
16
|
+
type DataType = 'date' | 'duration';
|
|
17
|
+
|
|
18
|
+
type FieldType = 'hours' | 'minutes';
|
|
19
|
+
|
|
20
|
+
// TODO sdil refactor rxjs flows
|
|
21
|
+
@Component({
|
|
22
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
23
|
+
selector: 'time-picker',
|
|
24
|
+
styleUrls: ['./time-picker.component.scss'],
|
|
25
|
+
templateUrl: './time-picker.component.html',
|
|
26
|
+
encapsulation: ViewEncapsulation.None,
|
|
27
|
+
standalone: true,
|
|
28
|
+
imports: [
|
|
29
|
+
CommonModule,
|
|
30
|
+
FormsModule,
|
|
31
|
+
MatFormFieldModule,
|
|
32
|
+
MatInputModule,
|
|
33
|
+
NumericStepperComponent
|
|
34
|
+
]
|
|
35
|
+
})
|
|
36
|
+
export class TimePickerComponent extends Destroy implements ControlValueAccessor {
|
|
37
|
+
@ViewChild('hours') public hours?: ElementRef<HTMLInputElement>;
|
|
38
|
+
@ViewChild('minutes') public minutes?: ElementRef<HTMLInputElement>;
|
|
39
|
+
|
|
40
|
+
@Output() public readonly timeChange = new EventEmitter<DateOrDuration>();
|
|
41
|
+
|
|
42
|
+
/** Display mode for the time-picker */
|
|
43
|
+
@Input() public mode: TimePickerDisplayMode = 'fullTime';
|
|
44
|
+
|
|
45
|
+
/** Data type to manage (Date or Duration) */
|
|
46
|
+
@Input() public dataType: DataType = 'date';
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Force the hour or minute to be null (only if the other field is disabled)
|
|
50
|
+
*
|
|
51
|
+
* For instance, if set to true and the hours are disabled, the minutes input value will be null
|
|
52
|
+
* This is useful to force the user to provide a value
|
|
53
|
+
*/
|
|
54
|
+
@Input() public forceNullValue = false;
|
|
55
|
+
|
|
56
|
+
@Input() public appearance: MatFormFieldAppearance = 'outline';
|
|
57
|
+
|
|
58
|
+
@Input()
|
|
59
|
+
public set autoFocus(value: BooleanInput) {
|
|
60
|
+
this._autoFocus = coerceBooleanProperty(value);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@Input() public defaultPlaceholderHours = '_ _';
|
|
64
|
+
@Input() public defaultPlaceholderMinutes = '_ _';
|
|
65
|
+
|
|
66
|
+
@Input()
|
|
67
|
+
public set time(value: DateOrDuration | undefined) {
|
|
68
|
+
this.writeValue(value);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
public get time(): DateOrDuration | undefined {
|
|
72
|
+
return this.value;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Step of the arrows */
|
|
76
|
+
@Input()
|
|
77
|
+
public set step(value: NumberInput) {
|
|
78
|
+
this._step = coerceNumberProperty(value);
|
|
79
|
+
this.changeDetectorRef.markForCheck();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** To get the step of the minutes arrows */
|
|
83
|
+
public get step(): NumberInput {
|
|
84
|
+
return this._step;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Disabled property setter. Can be string or empty so you can use it like : <time-picker disabled></time-picker> */
|
|
88
|
+
@Input()
|
|
89
|
+
public set disabled(value: BooleanInput) {
|
|
90
|
+
this._disabled = coerceBooleanProperty(value);
|
|
91
|
+
this.changeDetectorRef.markForCheck();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** To get disabled attribute. */
|
|
95
|
+
public get disabled(): BooleanInput {
|
|
96
|
+
return this._disabled;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
public onHoursChange$ = new Subject<Event | number>();
|
|
100
|
+
public onMinutesChange$ = new Subject<Event | number>();
|
|
101
|
+
public _step = 1;
|
|
102
|
+
private _disabled = false;
|
|
103
|
+
private _value?: DateOrDuration;
|
|
104
|
+
private _autoFocus = true;
|
|
105
|
+
|
|
106
|
+
public constructor(
|
|
107
|
+
private changeDetectorRef: ChangeDetectorRef,
|
|
108
|
+
@Self() @Optional() public control: NgControl
|
|
109
|
+
) {
|
|
110
|
+
super();
|
|
111
|
+
|
|
112
|
+
if (this.control) {
|
|
113
|
+
this.control.valueAccessor = this;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
this.onHoursChange$.pipe(
|
|
117
|
+
debounce(hours => timer(typeof hours === 'object' ? 0 : 10)),
|
|
118
|
+
distinctUntilChanged(),
|
|
119
|
+
map(hours => {
|
|
120
|
+
if (typeof hours === 'object') {
|
|
121
|
+
const value = (hours.target as HTMLInputElement).value;
|
|
122
|
+
return [value !== undefined ? parseInt(value, 10) : undefined, true] as const;
|
|
123
|
+
}
|
|
124
|
+
return [!isNaN(hours) ? hours : 0, false] as const;
|
|
125
|
+
}),
|
|
126
|
+
takeUntil(this.destroyed$)
|
|
127
|
+
).subscribe(([hours, isEvent]) => {
|
|
128
|
+
if (!this.value) {
|
|
129
|
+
this.value = this.dataType === 'date' ? set(new Date(), { hours, minutes: 0, seconds: 0, milliseconds: 0 }) : { hours, minutes: 0 } as Duration;
|
|
130
|
+
} else if (this.value instanceof Date) {
|
|
131
|
+
const value = this.value?.getTime();
|
|
132
|
+
const clone = new Date(value);
|
|
133
|
+
if (hours !== undefined) {
|
|
134
|
+
clone.setHours(hours);
|
|
135
|
+
}
|
|
136
|
+
this.value = clone;
|
|
137
|
+
} else {
|
|
138
|
+
this.value = {
|
|
139
|
+
hours: hours && hours < 0 ? 0 : hours,
|
|
140
|
+
minutes: this.value.minutes
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
this.changeDetectorRef.markForCheck();
|
|
144
|
+
|
|
145
|
+
if (isEvent && this._autoFocus && this.minutes) {
|
|
146
|
+
this.minutes.nativeElement.focus({
|
|
147
|
+
preventScroll: true
|
|
148
|
+
});
|
|
149
|
+
this.minutes.nativeElement.select();
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
this.onMinutesChange$.pipe(
|
|
154
|
+
debounce(minutes => timer(typeof minutes === 'object' ? 0 : 10)),
|
|
155
|
+
distinctUntilChanged(),
|
|
156
|
+
map(event => {
|
|
157
|
+
let minutes: number | undefined;
|
|
158
|
+
if (typeof event === 'object') {
|
|
159
|
+
const value = (event.target as HTMLInputElement).value;
|
|
160
|
+
minutes = value !== undefined ? parseInt(value, 10) : undefined;
|
|
161
|
+
} else {
|
|
162
|
+
minutes = event;
|
|
163
|
+
}
|
|
164
|
+
return minutes && !isNaN(minutes) && minutes || 0;
|
|
165
|
+
}),
|
|
166
|
+
takeUntil(this.destroyed$)
|
|
167
|
+
).subscribe(minutes => {
|
|
168
|
+
if (!this.value) {
|
|
169
|
+
this.value = this.dataType === 'date' ? set(new Date(), { hours: 0, minutes, seconds: 0, milliseconds: 0 }) : { hours: 0, minutes } as Duration;
|
|
170
|
+
} else if (this.value instanceof Date) {
|
|
171
|
+
const newValue = new Date(this.value.getTime());
|
|
172
|
+
if (minutes < 0) {
|
|
173
|
+
minutes += 60;
|
|
174
|
+
} else if (minutes >= 60) {
|
|
175
|
+
minutes -= 60;
|
|
176
|
+
}
|
|
177
|
+
newValue.setMinutes(minutes);
|
|
178
|
+
|
|
179
|
+
if (this.mode !== 'fullTimeWithHoursDisabled' || (this.mode === 'fullTimeWithHoursDisabled' && isSameHour(this.value, newValue))) {
|
|
180
|
+
this.value = newValue;
|
|
181
|
+
}
|
|
182
|
+
} else {
|
|
183
|
+
this.value = {
|
|
184
|
+
hours: this.value.hours,
|
|
185
|
+
minutes: minutes < 0 || minutes >= 60 ? 0 : minutes
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
this.changeDetectorRef.markForCheck();
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
public onKeyDown($event: KeyboardEvent, mode: 'hours' | 'minutes'): void {
|
|
193
|
+
// Get input element
|
|
194
|
+
const inputElement = mode === 'hours' ? this.hours?.nativeElement : this.minutes?.nativeElement;
|
|
195
|
+
if ($event.key?.toLowerCase() === 'd') {
|
|
196
|
+
$event.stopPropagation();
|
|
197
|
+
$event.preventDefault();
|
|
198
|
+
this.value = new Date();
|
|
199
|
+
} else if (inputElement) {
|
|
200
|
+
if ($event.key?.toLowerCase() === 'a' && $event.ctrlKey) {
|
|
201
|
+
inputElement.select();
|
|
202
|
+
} else if ($event.key && !['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Backspace', 'Delete', 'Tab', 'Enter', 'Control', 'Shift'].includes($event.key)) {
|
|
203
|
+
// Set regex for input format validation (differs if we are dealing with a date or a duration)
|
|
204
|
+
let regex;
|
|
205
|
+
if (mode === 'hours') {
|
|
206
|
+
regex = this.dataType === 'date' ? /^(\d|[01]\d|2[0-3])$/ : /^(\d+)$/;
|
|
207
|
+
} else {
|
|
208
|
+
regex = /^(\d|[0-5]\d)$/;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Get the selection in input element
|
|
212
|
+
const [selectionStart, selectionEnd] = [inputElement.selectionStart || 0, inputElement.selectionEnd || 0].sort((a, b) => a - b);
|
|
213
|
+
const selectionDiff = selectionEnd - selectionStart;
|
|
214
|
+
|
|
215
|
+
// Get the current value in input element and update it with the new touched key
|
|
216
|
+
const inputValue = inputElement.value || '';
|
|
217
|
+
const inputValueArr = Array.from(inputValue);
|
|
218
|
+
inputValueArr.splice(selectionStart, selectionDiff, $event.key);
|
|
219
|
+
const newInputValue = inputValueArr.join('');
|
|
220
|
+
|
|
221
|
+
// Prevent event if the time is not valid
|
|
222
|
+
if (!regex.test(newInputValue)) {
|
|
223
|
+
$event.stopPropagation();
|
|
224
|
+
$event.preventDefault();
|
|
225
|
+
} else if (this._autoFocus && mode === 'hours' && ((this.dataType === 'date' && parseFloat(newInputValue) >= 3) || newInputValue.length === 2)) {
|
|
226
|
+
this.onHoursChange$.next($event);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
public get hoursValue(): number | undefined {
|
|
233
|
+
if (!this.value || (this.forceNullValue && this.mode === 'fullTimeWithMinutesDisabled' && this.control.pristine)) {
|
|
234
|
+
return undefined;
|
|
235
|
+
}
|
|
236
|
+
return this.value instanceof Date ? this.value.getHours() : this.value.hours;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
public get minutesValue(): number | undefined {
|
|
240
|
+
if (!this.value || (this.forceNullValue && this.mode === 'fullTimeWithHoursDisabled' && this.control.pristine)) {
|
|
241
|
+
return undefined;
|
|
242
|
+
}
|
|
243
|
+
return this.value instanceof Date ? this.value.getMinutes() : this.value.minutes;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
public incrementValue(fieldType: FieldType): void {
|
|
247
|
+
if (fieldType === 'hours') {
|
|
248
|
+
this.onHoursChange$.next((this.hoursValue || 0) + 1);
|
|
249
|
+
} else {
|
|
250
|
+
this.onMinutesChange$.next((this.minutesValue || 0) + this._step);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
public decrementValue(fieldType: FieldType): void {
|
|
255
|
+
if (fieldType === 'hours') {
|
|
256
|
+
this.onHoursChange$.next((this.hoursValue || 0) - 1);
|
|
257
|
+
} else {
|
|
258
|
+
this.onMinutesChange$.next((this.minutesValue || 0) - this._step);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
public onClick(mode: 'hours' | 'minutes'): void {
|
|
263
|
+
if (this._autoFocus) {
|
|
264
|
+
if (mode === 'hours') {
|
|
265
|
+
this.hours?.nativeElement.select();
|
|
266
|
+
} else {
|
|
267
|
+
this.minutes?.nativeElement.select();
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ************* ControlValueAccessor Implementation **************
|
|
273
|
+
/** set accessor including call the onchange callback */
|
|
274
|
+
public set value(v: DateOrDuration | undefined) {
|
|
275
|
+
if (v !== this._value) {
|
|
276
|
+
this.writeValue(v);
|
|
277
|
+
this.onChangeCallback(v);
|
|
278
|
+
this.timeChange.emit(v);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** get accessor */
|
|
283
|
+
public get value(): DateOrDuration | undefined {
|
|
284
|
+
return this._value;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** From ControlValueAccessor interface */
|
|
288
|
+
public writeValue(value: DateOrDuration | undefined): void {
|
|
289
|
+
if ((value ?? null) !== (this._value ?? null)) {
|
|
290
|
+
if (value instanceof Date) {
|
|
291
|
+
this._value = value ? new Date(value.getTime()) : set(new Date(), { hours: 0, minutes: 0, seconds: 0 });
|
|
292
|
+
} else {
|
|
293
|
+
this._value = value;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
this.changeDetectorRef.markForCheck();
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** From ControlValueAccessor interface */
|
|
301
|
+
public registerOnChange(fn: (_a: unknown) => void): void {
|
|
302
|
+
this.onChangeCallback = fn;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** From ControlValueAccessor interface */
|
|
306
|
+
public registerOnTouched(fn: () => void): void {
|
|
307
|
+
this.onTouchedCallback = fn;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
public setDisabledState(isDisabled: boolean): void {
|
|
311
|
+
this.disabled = isDisabled;
|
|
312
|
+
}
|
|
313
|
+
// ************* End of ControlValueAccessor Implementation **************
|
|
314
|
+
|
|
315
|
+
protected onChangeCallback = (_a: unknown): void => undefined;
|
|
316
|
+
protected onTouchedCallback = (): void => undefined;
|
|
317
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
|
2
|
+
{
|
|
3
|
+
"extends": "../../tsconfig.base.json",
|
|
4
|
+
"compilerOptions": {
|
|
5
|
+
"outDir": "../../out-tsc/lib",
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"declarationMap": true,
|
|
8
|
+
"inlineSources": true,
|
|
9
|
+
"types": []
|
|
10
|
+
},
|
|
11
|
+
"exclude": [
|
|
12
|
+
"src/test.ts",
|
|
13
|
+
"**/*.spec.ts"
|
|
14
|
+
]
|
|
15
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
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
|
+
}
|