2 |
5 |
12), 'animated-pointer': !touching }"
7 | [style.transform]="'rotate(' + angle + 'deg)'">
8 |
9 |
10 |
12 | ·
13 |
14 |
15 |
16 |
17 |
20 | ·
21 |
22 |
23 |
24 |
27 | {{ digit.display }}
28 | {{ digit.display === 60 ? '00' : digit.display }}
29 |
30 |
31 |
32 |
33 |
34 |
37 | {{ digit.display === 24 ? '00' : digit.display }}
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/projects/mat-timepicker/src/lib/clock/clock.component.scss:
--------------------------------------------------------------------------------
1 | .root {
2 | width: 256px;
3 | height: 256px;
4 | cursor: default;
5 | }
6 |
7 | .circle {
8 | width: 256px;
9 | height: 256px;
10 | border-radius: 50%;
11 | position: relative;
12 | background: #ededed;
13 | cursor: pointer;
14 | }
15 |
16 | .number {
17 | width: 32px;
18 | height: 32px;
19 | border: 0px;
20 | left: calc(50% - 16px);
21 | top: calc(50% - 16px);
22 | position: absolute;
23 | text-align: center;
24 | line-height: 32px;
25 | cursor: pointer;
26 | font-size: 14px;
27 | pointer-events: none;
28 | user-select: none;
29 | display: flex;
30 | align-items: center;
31 | justify-content: center;
32 | flex-direction: column;
33 | background-color: transparent !important;
34 | background: transparent !important;
35 | box-shadow: 0px -1px 5px -200px rgba(0,0,0,1) !important;
36 | -webkit-box-shadow: 0px -1px 5px -200px rgba(0,0,0,1) !important;
37 | -moz-box-shadow: 0px -1px 5px -200px rgba(0,0,0,1) !important;
38 |
39 | &.disabled {
40 | color: rgba(1,1,1,.1);
41 | }
42 |
43 | &:not(.selected):not(.disabled) {
44 | color: rgba(0,0,0,.87);
45 | }
46 |
47 | &:not(.disabled).minute-dot {
48 | color: rgba(1, 1, 1, 0.7);
49 | &.selected {
50 | color: transparent;
51 | }
52 | }
53 | }
54 |
55 | .small-number {
56 | font-size: 12px;
57 |
58 | &:not(.selected):not(.disabled) {
59 | color: rgba(0,0,0,.67);
60 | }
61 | }
62 |
63 | .pointer-container {
64 | width: calc(50% - 20px);
65 | height: 2;
66 | position: absolute;
67 | left: 50%;
68 | top: calc(50% - 1px);
69 | transform-origin: left center;
70 | pointer-events: none;
71 |
72 | &.disabled {
73 | * {
74 | background-color: transparent;
75 | }
76 | }
77 | }
78 |
79 | .pointer {
80 | height: 1px;
81 | }
82 |
83 | .animated-pointer {
84 | transition: all 200ms ease-out;
85 | }
86 |
87 | .small-pointer {
88 | width: calc(50% - 52px);
89 | }
90 |
91 | .inner-dot {
92 | position: absolute;
93 | top: -3px;
94 | left: -4px;
95 | width: 8px;
96 | height: 8px;
97 | border-radius: 50%;
98 | box-shadow: 0px 3px 5px -1px rgb(0 0 0 / 0%), 0px 6px 10px 0px rgb(0 0 0 / 0%), 0px 1px 18px 0px rgb(0 0 0 / 0%) !important;
99 | -webkit-box-shadow: 0px 3px 5px -1px rgb(0 0 0 / 0%), 0px 6px 10px 0px rgb(0 0 0 / 0%), 0px 1px 18px 0px rgb(0 0 0 / 0%) !important;
100 | -moz-box-shadow: 0px 3px 5px -1px rgb(0 0 0 / 0%), 0px 6px 10px 0px rgb(0 0 0 / 0%), 0px 1px 18px 0px rgb(0 0 0 / 0%) !important;
101 |
102 |
103 |
104 | }
105 |
106 | .outer-dot {
107 | width: 32px;
108 | height: 32px;
109 | position: absolute;
110 | right: -16px;
111 | border-radius: 50%;
112 | box-sizing: content-box;
113 | box-shadow: 0px 3px 5px -1px rgb(0 0 0 / 0%), 0px 6px 10px 0px rgb(0 0 0 / 0%), 0px 1px 18px 0px rgb(0 0 0 / 0%) !important;
114 | -webkit-box-shadow: 0px 3px 5px -1px rgb(0 0 0 / 0%), 0px 6px 10px 0px rgb(0 0 0 / 0%), 0px 1px 18px 0px rgb(0 0 0 / 0%) !important;
115 | -moz-box-shadow: 0px 3px 5px -1px rgb(0 0 0 / 0%), 0px 6px 10px 0px rgb(0 0 0 / 0%), 0px 1px 18px 0px rgb(0 0 0 / 0%) !important;
116 | }
117 |
118 | .outer-dot-odd {
119 | width: 32px;
120 | height: 32px;
121 | display: flex;
122 | align-items: center;
123 | justify-content: center;
124 | flex-direction: column;
125 | box-shadow: 0px 3px 5px -1px rgb(0 0 0 / 0%), 0px 6px 10px 0px rgb(0 0 0 / 0%), 0px 1px 18px 0px rgb(0 0 0 / 0%) !important;
126 | -webkit-box-shadow: 0px 3px 5px -1px rgb(0 0 0 / 0%), 0px 6px 10px 0px rgb(0 0 0 / 0%), 0px 1px 18px 0px rgb(0 0 0 / 0%) !important;
127 | -moz-box-shadow: 0px 3px 5px -1px rgb(0 0 0 / 0%), 0px 6px 10px 0px rgb(0 0 0 / 0%), 0px 1px 18px 0px rgb(0 0 0 / 0%) !important;
128 | }
--------------------------------------------------------------------------------
/projects/mat-timepicker/src/lib/clock/clock.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
2 |
3 | import { ClockComponent } from './clock.component';
4 |
5 | describe('ClockComponent', () => {
6 | let component: ClockComponent;
7 | let fixture: ComponentFixture
;
8 |
9 | beforeEach(waitForAsync(() => {
10 | TestBed.configureTestingModule({
11 | declarations: [ ClockComponent ]
12 | })
13 | .compileComponents();
14 | }));
15 |
16 | beforeEach(() => {
17 | fixture = TestBed.createComponent(ClockComponent);
18 | component = fixture.componentInstance;
19 | fixture.detectChanges();
20 | });
21 |
22 | it('should create', () => {
23 | expect(component).toBeTruthy();
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/projects/mat-timepicker/src/lib/clock/clock.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Input, Output, EventEmitter, OnChanges, SimpleChanges, ChangeDetectionStrategy } from '@angular/core';
2 | import { ClockViewType, ClockNumber, ITimeData, ClockMode } from '../interfaces-and-types';
3 | import { isAllowed, getIsAvailabeFn } from '../util';
4 |
5 | @Component({
6 | selector: 'mat-clock',
7 | templateUrl: './clock.component.html',
8 | styleUrls: ['./clock.component.scss'],
9 | changeDetection: ChangeDetectionStrategy.OnPush
10 | })
11 | export class ClockComponent implements OnChanges {
12 |
13 | @Input() mode: ClockMode;
14 | @Input() viewType: ClockViewType;
15 | @Input() color = 'primary';
16 | @Input() formattedValue: number;
17 | @Input() minDate: Date;
18 | @Input() maxDate: Date;
19 | @Input() isPm: boolean;
20 | @Input() formattedHours: number;
21 | @Input() minutes: number;
22 | @Output() changeEvent: EventEmitter = new EventEmitter();
23 | @Output() unavailableSelection: EventEmitter = new EventEmitter();
24 | @Output() invalidMeridiem: EventEmitter = new EventEmitter();
25 | @Output() invalidSelection: EventEmitter = new EventEmitter();
26 | @Output() clearInvalidMeridiem: EventEmitter = new EventEmitter();
27 |
28 | @Input() allowed12HourMap = null;
29 | @Input() allowed24HourMap = null;
30 |
31 | isFormattedValueAllowed = true;
32 |
33 | isAvailableFn: ReturnType;
34 |
35 | meridiem = null;
36 | touching = false;
37 | angle: number;
38 | numbers: ClockNumber[] = [];
39 | secondaryNumbers: ClockNumber[] = [];
40 | minuteDots: ClockNumber[] = [];
41 | invalidMeridiemEmitted = true;
42 |
43 | initIsAllowedFn() {
44 | if (!this.allowed12HourMap && !this.allowed24HourMap) { return; }
45 | this.isAvailableFn = getIsAvailabeFn(this.allowed12HourMap, this.allowed24HourMap, this.mode);
46 | }
47 |
48 | isAvailable(value) {
49 | return this.isAvailableFn ? this.isAvailableFn(value, this.viewType, this.isPm, this.formattedHours) : true;
50 | }
51 |
52 | ngOnChanges(simpleChanges: SimpleChanges) {
53 |
54 | if (
55 | simpleChanges.allowed12HourMap ||
56 | simpleChanges.allowed24HourMap ||
57 | (simpleChanges.mode && !simpleChanges.mode.firstChange)
58 | ) {
59 | this.initIsAllowedFn();
60 | }
61 |
62 | this.calculateAngule();
63 | this.setNumbers();
64 | this.meridiem = this.isPm ? 'PM' : 'AM';
65 |
66 | if (simpleChanges.formattedValue && (this.allowed12HourMap || this.allowed24HourMap)) {
67 | this.isFormattedValueAllowed = this.isAvailable(this.formattedValue);
68 | }
69 |
70 | const isSelectedTimeAvailable = (this.isAvailableFn) ?
71 | // when calling isAvailableFn here we should always set the viewType to minutes because we want to check the hours and the minutes
72 | this.isAvailableFn(this.minutes, 'minutes', this.isPm, this.formattedHours) : true;
73 |
74 | // if (this.mode === '24h' && this.viewType === 'minutes' && this.isAvailableFn) {
75 | // const areMinitesAvailable = this.isAvailableFn(this.minutes, 'minutes', this.isPm, this.formattedHours);
76 | // if (!areMinitesAvailable) {
77 | // if (this.minDate && this.minDate.getMinutes() > this.minutes) {
78 | // setTimeout(() => { this.changeEvent.emit({ value: this.minDate.getMinutes(), type: 'minutes' }); });
79 | // } else {
80 | // setTimeout(() => { this.changeEvent.emit({ value: this.maxDate.getMinutes(), type: 'minutes' }); });
81 | // }
82 | // }
83 | // }
84 |
85 | if (isSelectedTimeAvailable && this.invalidMeridiemEmitted) {
86 | this.clearInvalidMeridiem.emit();
87 | this.invalidMeridiemEmitted = false;
88 | }
89 |
90 | this.invalidSelection.emit(!isSelectedTimeAvailable);
91 | }
92 |
93 | calculateAngule() {
94 | this.angle = this.getPointerAngle(this.formattedValue, this.viewType);
95 | }
96 |
97 | setNumbers() {
98 | if (this.viewType === 'hours') {
99 | if (this.mode === '12h') {
100 | const meridiem = this.isPm ? 'pm' : 'am';
101 | const isAllowedFn = this.allowed12HourMap ? num => this.allowed12HourMap[meridiem][num + 1][0] : undefined;
102 | this.numbers = this.getNumbers(12, { size: 256 }, isAllowedFn);
103 | this.secondaryNumbers = [];
104 | this.minuteDots = [];
105 | } else if (this.mode === '24h') {
106 | const isAllowedFn = this.allowed24HourMap ? num => this.allowed24HourMap[num][0] : undefined;
107 | this.numbers = this.getNumbers(12, { size: 256 }, isAllowedFn);
108 | this.secondaryNumbers = this.getNumbers(12, { size: 256 - 64, start: 13 }, isAllowedFn);
109 | this.minuteDots = [];
110 | }
111 | } else {
112 | const meridiem = this.isPm ? 'pm' : 'am';
113 | const isAllowedFn =
114 | !!this.allowed12HourMap ? num => this.allowed12HourMap[meridiem][this.formattedHours][num] :
115 | !!this.allowed24HourMap ? num => this.allowed24HourMap[this.formattedHours][num] : undefined;
116 |
117 | this.numbers = this.getNumbers(12, { size: 256, start: 5, step: 5 }, isAllowedFn);
118 | this.minuteDots = this.getNumbers(60, { size: 256, start: 13 }).map(digit => {
119 | if (digit.display <= 59) {
120 | digit.allowed = isAllowedFn ? isAllowedFn(digit.display) : true;
121 | return digit;
122 | }
123 | digit.display = digit.display - 60;
124 | digit.allowed = isAllowedFn ? isAllowedFn(digit.display) : true;
125 | return digit;
126 | });
127 | this.secondaryNumbers = [];
128 | }
129 | }
130 |
131 | disableAnimatedPointer() {
132 | this.touching = true;
133 | }
134 |
135 | enableAnimatedPointer() {
136 | this.touching = false;
137 | }
138 |
139 | handleTouchMove = (e: any) => {
140 | e.preventDefault(); // prevent scrolling behind the clock on iOS
141 | const rect = e.target.getBoundingClientRect();
142 | this.movePointer(e.changedTouches[0].clientX - rect.left, e.changedTouches[0].clientY - rect.top);
143 | }
144 |
145 | handleTouchEnd(e: any) {
146 | this.handleTouchMove(e);
147 | this.enableAnimatedPointer();
148 | }
149 |
150 | handleMouseMove(e: any) {
151 | // MouseEvent.which is deprecated, but MouseEvent.buttons is not supported in Safari
152 | if ((e.buttons === 1 || e.which === 1) && this.touching) {
153 | const rect = e.target.getBoundingClientRect();
154 | this.movePointer(e.clientX - rect.left, e.clientY - rect.top);
155 | }
156 | }
157 |
158 | handleClick(e: any) {
159 | const rect = e.target.getBoundingClientRect();
160 | this.movePointer(e.clientX - rect.left, e.clientY - rect.top);
161 | }
162 |
163 | movePointer(x, y) {
164 | const value = this.getPointerValue(x, y, 256);
165 | if (!this.isAvailable(value)) {
166 | this.unavailableSelection.emit();
167 | return;
168 | }
169 | if (value !== this.formattedValue) {
170 | this.changeEvent.emit({ value, type: this.viewType });
171 | if (this.viewType !== 'minutes') {
172 | if (!this.isAvailable(value)) {
173 | if (this.minDate && this.isAvailable(value)
174 | ) {
175 | this.changeEvent.emit({ value: this.minDate.getMinutes(), type: 'minutes' });
176 | } else if (this.maxDate && this.isAvailable(value)) {
177 | this.changeEvent.emit({ value: this.maxDate.getMinutes(), type: 'minutes' });
178 | }
179 | }
180 | }
181 | }
182 | }
183 |
184 | getNumbers(count, { size, start = 1, step = 1 }, isAllowedFn?: (num: number) => boolean) {
185 | return Array.apply(null, Array(count)).map((_, i) => ({
186 | display: i * step + start,
187 | translateX: (size / 2 - 20) * Math.cos(2 * Math.PI * (i - 2) / count),
188 | translateY: (size / 2 - 20) * Math.sin(2 * Math.PI * (i - 2) / count),
189 | allowed: isAllowedFn ? isAllowedFn(i) : true
190 | }));
191 | }
192 |
193 | getPointerAngle(value, mode: ClockViewType) {
194 | if (this.viewType === 'hours') {
195 | return this.mode === '12h' ? 360 / 12 * (value - 3) : 360 / 12 * (value % 12 - 3);
196 | }
197 | return 360 / 60 * (value - 15);
198 | }
199 |
200 | getPointerValue(x, y, size) {
201 | let value;
202 | let angle = Math.atan2(size / 2 - x, size / 2 - y) / Math.PI * 180;
203 | if (angle < 0) {
204 | angle = 360 + angle;
205 | }
206 |
207 | if (this.viewType === 'hours') {
208 | if (this.mode === '12h') {
209 | value = 12 - Math.round(angle * 12 / 360);
210 | return value === 0 ? 12 : value;
211 | }
212 |
213 | const radius = Math.sqrt(Math.pow(size / 2 - x, 2) + Math.pow(size / 2 - y, 2));
214 | value = 12 - Math.round(angle * 12 / 360);
215 | if (value === 0) { value = 12; }
216 | if (radius < size / 2 - 32) { value = value === 12 ? 0 : value + 12; }
217 | return value;
218 |
219 | }
220 |
221 | value = Math.round(60 - 60 * angle / 360);
222 | return value === 60 ? 0 : value;
223 | }
224 | }
225 |
--------------------------------------------------------------------------------
/projects/mat-timepicker/src/lib/interfaces-and-types.ts:
--------------------------------------------------------------------------------
1 | export type ClockViewType = 'hours' | 'minutes';
2 | export type ClockMode = '12h' | '24h';
3 |
4 | export interface ClockNumber {
5 | display: number;
6 | translateX: number;
7 | translateY: number;
8 | allowed: boolean;
9 | }
10 |
11 | export interface ITimeData {
12 | minutes: number;
13 | hours: number;
14 | meridiem: string;
15 | }
16 |
17 | export interface IAllowed24HourMap { [hour: number]: { [minute: number]: boolean; }; }
18 | export interface IAllowed12HourMap {
19 | am: { [hour: number]: { [minute: number]: boolean } };
20 | pm: { [hour: number]: { [minute: number]: boolean } };
21 | }
22 |
--------------------------------------------------------------------------------
/projects/mat-timepicker/src/lib/mat-timepicker.module.ts:
--------------------------------------------------------------------------------
1 | import { MatDialogModule } from '@angular/material/dialog';
2 | import { MatButtonModule } from '@angular/material/button';
3 | import { MatToolbarModule } from '@angular/material/toolbar';
4 | import { MatIconModule } from '@angular/material/icon';
5 | import { MatInputModule } from '@angular/material/input';
6 |
7 | import { NgModule } from '@angular/core';
8 | import { CommonModule } from '@angular/common';
9 | import { ClockComponent } from './clock/clock.component';
10 | import { MatTimepickerComponentDialogComponent } from './timepicker-dialog/timepicker-dialog.component';
11 | import { MatTimepickerDirective } from './timepicker.directive';
12 |
13 | @NgModule({
14 | declarations: [
15 | ClockComponent,
16 | MatTimepickerDirective,
17 | MatTimepickerComponentDialogComponent
18 | ],
19 | imports: [
20 | CommonModule,
21 | MatDialogModule,
22 | MatButtonModule,
23 | MatToolbarModule,
24 | MatIconModule,
25 | MatInputModule
26 | ],
27 | exports: [
28 | MatTimepickerDirective
29 | ]
30 | })
31 | export class MatTimepickerModule { }
32 |
--------------------------------------------------------------------------------
/projects/mat-timepicker/src/lib/timepicker-dialog/timepicker-dialog.component.html:
--------------------------------------------------------------------------------
1 |
2 | {{cancelLabel}}
3 |
4 |
5 | {{okLabel}}
6 |
7 |
8 |
9 |
47 |
48 |
49 |
51 |
52 |
54 |
55 |
--------------------------------------------------------------------------------
/projects/mat-timepicker/src/lib/timepicker-dialog/timepicker-dialog.component.scss:
--------------------------------------------------------------------------------
1 | mat-dialog-content {
2 | min-height: 395px;
3 | padding: 0px;
4 | margin-top: -24px;
5 | overflow: hidden;
6 | }
7 |
8 | mat-dialog-actions {
9 | justify-content: flex-end;
10 | margin-right: -8px;
11 | margin-left: -8px;
12 | }
13 |
14 | .root {
15 | min-width: 282px;
16 | }
17 |
18 | .header {
19 | border-top-left-radius: 2px;
20 | border-top-right-radius: 2px;
21 | padding: 20px 0;
22 | line-height: 58px;
23 | font-size: 58px;
24 | display: flex;
25 | justify-content: center;
26 | align-items: center;
27 | user-select: none;
28 | height: 98px;
29 |
30 | .fixed-font-size {
31 | font-size: 58px;
32 | }
33 |
34 | .time-frame {
35 | height: 60px;
36 | }
37 | }
38 |
39 | .time {
40 | transition: all 200ms ease-out;
41 | cursor: pointer;
42 | &:not(.select) {
43 | opacity: .6;
44 | }
45 | }
46 |
47 | .placeholder {
48 | flex: 1;
49 | }
50 |
51 | .ampm {
52 | display: flex;
53 | flex-direction: column-reverse;
54 | flex: 1;
55 | font-size: 14px;
56 | line-height: 20px;
57 | margin-left: 16px;
58 | font-weight: 700px;
59 | }
60 |
61 | .select {
62 | color: white;
63 | }
64 |
65 | .body {
66 | padding: 24px 16px;
67 | padding-bottom: 20px;
68 | display: flex;
69 | justify-content: center;
70 | }
71 |
--------------------------------------------------------------------------------
/projects/mat-timepicker/src/lib/timepicker-dialog/timepicker-dialog.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
2 |
3 | import { MatTimepickerComponentDialogComponent } from './timepicker-dialog.component';
4 |
5 | describe('TimePickerComponent', () => {
6 | let component: MatTimepickerComponentDialogComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(waitForAsync(() => {
10 | TestBed.configureTestingModule({
11 | declarations: [MatTimepickerComponentDialogComponent]
12 | })
13 | .compileComponents();
14 | }));
15 |
16 | beforeEach(() => {
17 | fixture = TestBed.createComponent(MatTimepickerComponentDialogComponent);
18 | component = fixture.componentInstance;
19 | fixture.detectChanges();
20 | });
21 |
22 | it('should create', () => {
23 | expect(component).toBeTruthy();
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/projects/mat-timepicker/src/lib/timepicker-dialog/timepicker-dialog.component.ts:
--------------------------------------------------------------------------------
1 | import { MAT_DIALOG_DATA } from '@angular/material/dialog';
2 | import { Component, EventEmitter, Output, Inject, DoCheck, TemplateRef } from '@angular/core';
3 | import { ClockViewType, ClockMode, IAllowed24HourMap, IAllowed12HourMap } from '../interfaces-and-types';
4 | import { twoDigits, convertHoursForMode } from '../util';
5 | import { MatTimepickerButtonTemplateContext } from '../timepicker.directive';
6 |
7 | @Component({
8 | selector: 'mat-timepicker-dialog',
9 | templateUrl: './timepicker-dialog.component.html',
10 | styleUrls: ['./timepicker-dialog.component.scss']
11 | })
12 | export class MatTimepickerComponentDialogComponent implements DoCheck {
13 |
14 | twoDigits = twoDigits;
15 |
16 | @Output() changeEvent: EventEmitter = new EventEmitter();
17 | @Output() okClickEvent: EventEmitter = new EventEmitter();
18 | @Output() cancelClickEvent: EventEmitter = new EventEmitter();
19 |
20 | allowed24HourMap: IAllowed24HourMap = null;
21 | allowed12HourMap: IAllowed12HourMap = null;
22 |
23 | invalidSelection = false;
24 |
25 | okLabel: string;
26 | cancelLabel: string;
27 |
28 | okButtonTemplate: TemplateRef;
29 | cancelButtonTemplate: TemplateRef;
30 |
31 | anteMeridiemAbbreviation: string;
32 | postMeridiemAbbreviation: string;
33 |
34 | set value(value: any) {
35 | value = value || this.minDate || this.maxDate || new Date();
36 | this.hours = value.getHours();
37 | this.minutes = value.getMinutes();
38 | this._value = value;
39 | }
40 |
41 | get value() { return this._value; }
42 |
43 | mode: ClockMode;
44 | viewType: ClockViewType = 'hours';
45 |
46 | minutes: any;
47 | color: string;
48 | isPm = false;
49 | skipMinuteAutoSwitch = false;
50 | autoSwitchID = null;
51 | invalidMedianID = null;
52 | hasInvalidMeridiem = false;
53 | editHoursClicked = false;
54 | isClosing = false;
55 |
56 | minDate: Date;
57 | maxDate: Date;
58 |
59 | // tslint:disable-next-line:variable-name
60 | _formattedHour: any;
61 | // tslint:disable-next-line:variable-name
62 | _hours: any;
63 | // tslint:disable-next-line:variable-name
64 | _value: Date;
65 |
66 | set hours(value: any) {
67 | this._hours = value;
68 | this._formattedHour = convertHoursForMode(this.hours, this.mode).hour;
69 | }
70 | get hours() { return this._hours; }
71 |
72 | get formattedHours() { return this._formattedHour; }
73 |
74 | bindData(data: any) {
75 | this.mode = data.mode;
76 | this.okLabel = data.okLabel;
77 | this.cancelLabel = data.cancelLabel;
78 | this.okButtonTemplate = data.okButtonTemplate;
79 | this.cancelButtonTemplate = data.cancelButtonTemplate;
80 | this.anteMeridiemAbbreviation = data.anteMeridiemAbbreviation;
81 | this.postMeridiemAbbreviation = data.postMeridiemAbbreviation;
82 | this.color = data.color;
83 | this.minDate = data.minDate;
84 | this.maxDate = data.maxDate;
85 | this.allowed12HourMap = data.allowed12HourMap;
86 | this.allowed24HourMap = data.allowed24HourMap;
87 | }
88 |
89 | constructor(@Inject(MAT_DIALOG_DATA) public data) {
90 | this.isPm = data.isPm;
91 | this.bindData(data);
92 | // keep this always at the bottom
93 | this.value = data.value;
94 | }
95 |
96 | ngDoCheck() { this.bindData(this.data); }
97 |
98 | handleClockChange({ value, type }: { value: number, type: 'minutes' | 'hours' }) {
99 | const is24hoursAutoMeridiemChange = this.mode === '24h' && type === 'hours' && (
100 | (this.hours >= 12 && value < 12) || (this.hours < 12 && value >= 12));
101 | if ((this.hasInvalidMeridiem && this.mode === '12h') || is24hoursAutoMeridiemChange) {
102 | this.isPm = !this.isPm;
103 | this.hasInvalidMeridiem = false;
104 | }
105 |
106 | if ((type && type === 'hours') || (!type && this.viewType === 'hours')) {
107 | this.hours = value;
108 | } else if ((type && type === 'minutes') || (!type && this.viewType === 'minutes')) {
109 | this.minutes = value;
110 | }
111 |
112 | const newValue = new Date();
113 | const hours = this.isPm ? this.hours < 12 ? this.hours + 12 : this.hours : this.hours === 12 ? 0 : this.hours;
114 | newValue.setHours(hours);
115 | newValue.setMinutes(this.minutes);
116 | newValue.setSeconds(0);
117 | newValue.setMilliseconds(0);
118 | this.value = newValue;
119 | this.changeEvent.emit(newValue);
120 | }
121 |
122 | clearInvalidMeridiem() {
123 | this.hasInvalidMeridiem = false;
124 | }
125 |
126 | handleUnavailableSelection() {
127 | clearTimeout(this.autoSwitchID);
128 | }
129 |
130 | handleClockChangeDone(e) {
131 | e.preventDefault(); // prevent mouseUp after touchEnd
132 |
133 | if (this.viewType === 'hours' && !this.skipMinuteAutoSwitch) {
134 | this.autoSwitchID = setTimeout(() => {
135 | this.editMinutes();
136 | this.autoSwitchID = null;
137 | }, 300);
138 | }
139 | }
140 |
141 | editHours() {
142 | this.viewType = 'hours';
143 | this.editHoursClicked = true;
144 | setTimeout(() => { this.editHoursClicked = false; }, 0);
145 | }
146 |
147 | editMinutes() {
148 | if (this.hasInvalidMeridiem) {
149 | this.isPm = !this.isPm;
150 | this.hasInvalidMeridiem = false;
151 | }
152 | this.viewType = 'minutes';
153 | }
154 |
155 | invalidSelectionHandler(value) {
156 | this.invalidSelection = value;
157 | }
158 |
159 |
160 | invalidMeridiem() {
161 | if (this.viewType !== 'minutes' && this.editHoursClicked) {
162 | if (this.invalidMedianID) { return; }
163 | this.invalidMedianID = setTimeout(() => {
164 | this.isPm = !this.isPm;
165 | this.hasInvalidMeridiem = false;
166 | }, 0);
167 | return;
168 | }
169 | this.hasInvalidMeridiem = true;
170 | }
171 |
172 | meridiemChange(hours) {
173 | const changeData = {
174 | type: this.viewType,
175 | value: this.viewType === 'hours' ? hours : this.value.getMinutes()
176 | };
177 | this.handleClockChange(changeData);
178 | }
179 |
180 |
181 | setAm() {
182 | if (this.hours >= 12) {
183 | this.hours = this.hours - 12;
184 | }
185 | this.isPm = false;
186 |
187 | this.meridiemChange(this.hours);
188 | }
189 |
190 | setPm() {
191 | if (this.hours < 12) {
192 | this.hours = this.hours + 12;
193 | }
194 | this.isPm = true;
195 | this.meridiemChange(this.hours);
196 | }
197 |
198 | okClickHandler = () => {
199 | if (this.hasInvalidMeridiem) {
200 | this.isPm = !this.isPm;
201 | this.hasInvalidMeridiem = false;
202 | }
203 | this.okClickEvent.emit(this.value);
204 | }
205 |
206 | cancelClickHandler = () => {
207 | this.cancelClickEvent.emit();
208 | }
209 |
210 | }
211 |
--------------------------------------------------------------------------------
/projects/mat-timepicker/src/lib/timepicker.directive.spec.ts:
--------------------------------------------------------------------------------
1 | // import { TimepickerDirective } from './timepicker.directive';
2 |
3 | // describe('TimepickerDirective', () => {
4 | // it('should create an instance', () => {
5 | // const directive = new TimepickerDirective();
6 | // expect(directive).toBeTruthy();
7 | // });
8 | // });
9 |
--------------------------------------------------------------------------------
/projects/mat-timepicker/src/lib/timepicker.directive.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ControlValueAccessor,
3 | NgForm,
4 | NgControl,
5 | FormGroupDirective,
6 | FormControl,
7 | FormControlName,
8 | Validators,
9 | FormGroup,
10 | FormControlDirective,
11 | } from '@angular/forms';
12 | import {
13 | Directive,
14 | OnInit,
15 | EventEmitter,
16 | Input,
17 | ElementRef,
18 | OnChanges,
19 | Renderer2,
20 | AfterViewInit,
21 | OnDestroy,
22 | Optional,
23 | SimpleChanges,
24 | NgZone,
25 | HostBinding,
26 | Self,
27 | Output,
28 | HostListener,
29 | TemplateRef,
30 | } from '@angular/core';
31 | import { MatDialog, MatDialogRef } from '@angular/material/dialog';
32 | import {
33 | MatFormFieldControl,
34 | MatFormField,
35 | } from '@angular/material/form-field';
36 | import {
37 | ClockMode,
38 | IAllowed24HourMap,
39 | IAllowed12HourMap,
40 | } from './interfaces-and-types';
41 | import {
42 | twoDigits,
43 | convertHoursForMode,
44 | isAllowed,
45 | isDateInRange,
46 | isTimeInRange,
47 | } from './util';
48 | import { MatTimepickerComponentDialogComponent } from './timepicker-dialog/timepicker-dialog.component';
49 | import { Subject } from 'rxjs';
50 | import { takeUntil, first } from 'rxjs/operators';
51 | import { FocusMonitor } from '@angular/cdk/a11y';
52 | import { coerceBooleanProperty } from '@angular/cdk/coercion';
53 | import { ErrorStateMatcher } from '@angular/material/core';
54 | import { Platform } from '@angular/cdk/platform';
55 |
56 | export interface MatTimepickerButtonTemplateContext {
57 | $implicit: () => void;
58 | label: string;
59 | }
60 |
61 | @Directive({
62 | selector: 'input[matTimepicker]',
63 | providers: [
64 | { provide: MatFormFieldControl, useExisting: MatTimepickerDirective },
65 | ],
66 | // tslint:disable-next-line:no-host-metadata-property
67 | host: {
68 | /**
69 | * @breaking-change 8.0.0 remove .mat-form-field-autofill-control in favor of AutofillMonitor.
70 | */
71 | // tslint:disable-next-line:object-literal-key-quotes
72 | class: 'mat-input-element mat-form-field-autofill-control',
73 | '[class.mat-input-server]': '_isServer',
74 | // Native input properties that are overwritten by Angular inputs need to be synced with
75 | // the native input element. Otherwise property bindings for those don't work.
76 | '[attr.id]': 'id',
77 | '[attr.placeholder]': 'placeholder',
78 | '[disabled]': 'disabled',
79 | '[required]': 'required',
80 | '[attr.readonly]': 'readonly || null',
81 | '[attr.aria-invalid]': 'errorState',
82 | '[attr.aria-required]': 'required.toString()',
83 | },
84 | exportAs: 'matTimepicker',
85 | })
86 | export class MatTimepickerDirective
87 | implements
88 | OnInit,
89 | OnChanges,
90 | AfterViewInit,
91 | OnDestroy,
92 | ControlValueAccessor,
93 | MatFormFieldControl
94 | {
95 | static nextId = 0;
96 |
97 | /** Whether the component is being rendered on the server. */
98 | // tslint:disable-next-line:variable-name
99 | readonly _isServer: boolean;
100 |
101 | // tslint:disable-next-line:variable-name
102 | _errorState = false;
103 | get errorState() {
104 | const oldState = this._errorState;
105 | const parent = this._parentFormGroup || this._parentForm;
106 | const control = this.ngControl
107 | ? (this.ngControl.control as FormControl)
108 | : null;
109 | const newState = this.errorStateMatcher
110 | ? this.errorStateMatcher.isErrorState(control, parent)
111 | : oldState;
112 |
113 | if (newState !== oldState) {
114 | this._errorState = newState;
115 | this.stateChanges.next();
116 | }
117 |
118 | return newState;
119 | }
120 |
121 | @Input()
122 | get disabled(): boolean {
123 | if (this.ngControl && this.ngControl.disabled !== null) {
124 | return this.ngControl.disabled;
125 | }
126 | return this._disabled;
127 | }
128 | set disabled(value: boolean) {
129 | this._disabled = coerceBooleanProperty(value);
130 |
131 | // Browsers may not fire the blur event if the input is disabled too quickly.
132 | // Reset from here to ensure that the element doesn't become stuck.
133 | if (this.focused) {
134 | this.focused = false;
135 | this.stateChanges.next();
136 | }
137 | }
138 | // tslint:disable-next-line:variable-name
139 | protected _disabled = false;
140 |
141 | @Input() get id(): string {
142 | return this._id;
143 | }
144 | set id(value: string) {
145 | this._id = value || this._uid;
146 | }
147 | // tslint:disable-next-line:variable-name
148 | protected _id: string;
149 |
150 | @Input() get readonly(): boolean {
151 | return this._readonly;
152 | }
153 | set readonly(value: boolean) {
154 | this._readonly = coerceBooleanProperty(value);
155 | }
156 | // tslint:disable-next-line:variable-name
157 | private _readonly = false;
158 |
159 | private isAlive: Subject = new Subject();
160 | stateChanges = new Subject();
161 |
162 | // tslint:disable-next-line:variable-name
163 | protected _uid = `mat-time-picker-${MatTimepickerDirective.nextId++}`;
164 | @HostBinding('class.floating') get shouldLabelFloat() {
165 | return this.focused || !this.empty;
166 | }
167 | @HostBinding('attr.aria-describedby') describedBy = '';
168 |
169 | @Input() errorStateMatcher: ErrorStateMatcher;
170 |
171 | @Input() get required() {
172 | return this._required;
173 | }
174 |
175 | set required(req) {
176 | this._required = coerceBooleanProperty(req);
177 | this.stateChanges.next();
178 | }
179 | // tslint:disable-next-line:variable-name
180 | private _required = false;
181 |
182 | @Input() get placeholder() {
183 | return this._placeholder;
184 | }
185 | set placeholder(plh) {
186 | this._placeholder = plh;
187 | this.stateChanges.next();
188 | }
189 | // tslint:disable-next-line:variable-name
190 | private _placeholder: string;
191 |
192 | focused = false;
193 | private pattern: RegExp;
194 |
195 | private allowed24HourMap: IAllowed24HourMap = null;
196 | private allowed12HourMap: IAllowed12HourMap = null;
197 |
198 | private isInputFocused = false;
199 |
200 | /* Use a custom template for the ok button */
201 | @Input()
202 | okButtonTemplate: TemplateRef | null = null;
203 | /* Use a custom template for the cancel button */
204 | @Input()
205 | cancelButtonTemplate: TemplateRef | null =
206 | null;
207 |
208 | /** Override the label of the ok button. */
209 | @Input() okLabel = 'Ok';
210 | /** Override the label of the cancel button. */
211 | @Input() cancelLabel = 'Cancel';
212 | /** Override the ante meridiem abbreviation. */
213 | @Input() anteMeridiemAbbreviation = 'am';
214 | /** Override the post meridiem abbreviation. */
215 | @Input() postMeridiemAbbreviation = 'pm';
216 |
217 | /** Sets the clock mode, 12-hour or 24-hour clocks are supported. */
218 | @Input() mode: ClockMode = '24h';
219 | @Input() color = 'primary';
220 | @Input() disableDialogOpenOnClick = false;
221 | @Input() strict = true;
222 |
223 | controlType = 'angular-material-timepicker';
224 |
225 | private listeners: (() => void)[] = [];
226 |
227 | @Input() minDate: Date;
228 | @Input() maxDate: Date;
229 |
230 | // tslint:disable-next-line:variable-name
231 | private _isPm: boolean;
232 | // tslint:disable-next-line:variable-name
233 | private _value: Date;
234 | // tslint:disable-next-line:variable-name
235 | private _formattedValueString: string;
236 |
237 | // tslint:disable-next-line:variable-name
238 | private _skipValueChangeEmission = true;
239 |
240 | @Input() set value(value: Date) {
241 | if (value === this._value) {
242 | return;
243 | }
244 | this._value = value;
245 | if (!value) {
246 | this._formattedValueString = null;
247 | this.setInputElementValue('');
248 | this.currentValue = value;
249 | return;
250 | }
251 |
252 | const { hour, isPm } = convertHoursForMode(value.getHours(), this.mode);
253 | this._isPm = isPm;
254 | this._formattedValueString =
255 | this.mode === '12h'
256 | ? `${hour}:${twoDigits(value.getMinutes())} ${isPm ? this.postMeridiemAbbreviation : this.anteMeridiemAbbreviation
257 | }`
258 | : `${twoDigits(value.getHours())}:${twoDigits(value.getMinutes())}`;
259 |
260 | if (!this.isInputFocused) {
261 | this.setInputElementValue(this.formattedValueString);
262 | }
263 | this.currentValue = value;
264 | this.stateChanges.next();
265 |
266 | if (this._skipValueChangeEmission) {
267 | return;
268 | }
269 | this.timeChange.emit(this.currentValue);
270 | }
271 |
272 | get value() {
273 | return this._value;
274 | }
275 |
276 | get isPm() {
277 | return this._isPm;
278 | }
279 |
280 | get empty() {
281 | return !(this.currentValue instanceof Date);
282 | }
283 |
284 | private get formattedValueString() {
285 | return this._formattedValueString;
286 | }
287 |
288 | private currentValue: Date;
289 | private modalRef: MatDialogRef;
290 |
291 | private onChangeFn: any;
292 | private onTouchedFn: any;
293 | private combination: string[] = [];
294 |
295 | @Output() timeChange: EventEmitter = new EventEmitter();
296 | @Output() invalidInput: EventEmitter = new EventEmitter();
297 |
298 | @HostListener('input') inputHandler() {
299 | let value = (this.elRef.nativeElement as any).value as string;
300 | const length = value.length;
301 | if (length === 0) {
302 | this.writeValue(null, true);
303 | if (this.onChangeFn) {
304 | this.onChangeFn(null);
305 | }
306 | return;
307 | }
308 |
309 | const meridiemResult = value.match(/am|pm/i);
310 | let meridiem: string | null = null;
311 | if (meridiemResult) {
312 | value = value.replace(meridiemResult[0], '');
313 | [meridiem] = meridiemResult;
314 | }
315 | const valueHasColumn = value.includes(':');
316 | let [hours, minutes]: any =
317 | length === 1
318 | ? [value, 0]
319 | : length === 2 && !valueHasColumn
320 | ? [value, 0]
321 | : valueHasColumn
322 | ? value.split(':')
323 | : value.split(/(\d\d)/).filter((v) => v);
324 |
325 | hours = +hours;
326 |
327 | if (/\s/.test(minutes)) {
328 | let other;
329 | [minutes, other] = minutes.split(/\s/);
330 | if (other === 'pm' && !isNaN(hours) && hours < 12) {
331 | hours += 12;
332 | }
333 | }
334 |
335 | minutes = +minutes;
336 |
337 | if (isNaN(hours) || isNaN(minutes)) {
338 | this.writeValue(null, true);
339 | return;
340 | }
341 |
342 | if (hours < 12 && meridiem && meridiem.toLowerCase() === 'pm') {
343 | hours += 12;
344 | } else if (hours >= 12 && meridiem && meridiem.toLowerCase() === 'am') {
345 | hours -= 12;
346 | }
347 |
348 | if (this.mode === '12h' && +hours < 0) {
349 | hours = '0';
350 | } else {
351 | if (+hours > 24) {
352 | hours = '24';
353 | } else if (+hours < 0) {
354 | hours = '0';
355 | }
356 | }
357 |
358 | if (+minutes > 59) {
359 | minutes = '59';
360 | } else if (+minutes < 0) {
361 | minutes = '0';
362 | }
363 |
364 | const d = this.value ? new Date(this.value.getTime()) : new Date();
365 | d.setHours(+hours);
366 | d.setMinutes(+minutes);
367 | d.setSeconds(0);
368 | d.setMilliseconds(0);
369 |
370 | const isValueInRange = isDateInRange(this.minDate, this.maxDate, d);
371 | if (!isValueInRange) {
372 | this.invalidInput.emit();
373 | }
374 |
375 | this.writeValue(d, true);
376 | if (this.onChangeFn) {
377 | this.onChangeFn(d);
378 | }
379 | }
380 |
381 | @HostListener('keydown', ['$event']) keydownHandler(event: any) {
382 | if (event.metaKey || event.ctrlKey || event.altKey) {
383 | this.combination = this.combination.concat(event.code);
384 | return;
385 | }
386 | if (!/^[0-9a-zA-Z\s]{0,1}$/.test(event.key)) {
387 | return;
388 | }
389 | const target = event.target;
390 | const tValue = target.value;
391 | const value = `${tValue.slice(0, target.selectionStart)}${event.key
392 | }${tValue.slice(target.selectionEnd)}`;
393 | if (value.match(this.pattern) || this.combination.length > 0) {
394 | return true;
395 | }
396 | event.preventDefault();
397 | event.stopImmediatePropagation();
398 | }
399 |
400 | @HostListener('keyup', ['$event']) keyupHandler(event: any) {
401 | this.combination = this.combination.filter((v) => v !== event.code);
402 | }
403 |
404 | @HostListener('focus') focusHandler() {
405 | this.isInputFocused = true;
406 | }
407 |
408 | @HostListener('focusout') focusoutHandler() {
409 | this.isInputFocused = false;
410 | this.setInputElementValue(this.formattedValueString);
411 | if (this.onTouchedFn && !this.modalRef) {
412 | this.onTouchedFn();
413 | }
414 | }
415 |
416 | constructor(
417 | @Optional() @Self() public ngControl: NgControl,
418 | public dialog: MatDialog,
419 | private renderer: Renderer2,
420 | private zone: NgZone,
421 | private fm: FocusMonitor,
422 | private elRef: ElementRef,
423 | // tslint:disable-next-line:variable-name
424 | protected _platform: Platform,
425 | // tslint:disable-next-line:variable-name
426 | @Optional() private _parentForm: NgForm,
427 | // tslint:disable-next-line:variable-name
428 | @Optional() private _matFormFiled: MatFormField,
429 | // tslint:disable-next-line:variable-name
430 | @Optional() private _parentFormGroup: FormGroupDirective,
431 | // tslint:disable-next-line:variable-name
432 | _defaultErrorStateMatcher: ErrorStateMatcher
433 | ) {
434 | this.id = this.id;
435 |
436 | this.errorStateMatcher = _defaultErrorStateMatcher;
437 | if (this.ngControl != null) {
438 | this.ngControl.valueAccessor = this;
439 | }
440 |
441 |
442 | if (_platform.IOS) {
443 | zone.runOutsideAngular(() => {
444 | elRef.nativeElement.addEventListener('keyup', (event: Event) => {
445 | const el = event.target as HTMLInputElement;
446 | if (!el.value && !el.selectionStart && !el.selectionEnd) {
447 | // Note: Just setting `0, 0` doesn't fix the issue. Setting
448 | // `1, 1` fixes it for the first time that you type text and
449 | // then hold delete. Toggling to `1, 1` and then back to
450 | // `0, 0` seems to completely fix it.
451 | el.setSelectionRange(1, 1);
452 | el.setSelectionRange(0, 0);
453 | }
454 | });
455 | });
456 | }
457 |
458 | this._isServer = !this._platform.isBrowser;
459 | }
460 |
461 | setDescribedByIds(ids: string[]) {
462 | this.describedBy = ids.join(' ');
463 | }
464 |
465 | onContainerClick(event: MouseEvent) {
466 | if ((event.target as Element).tagName.toLowerCase() !== 'input') {
467 | this.elRef.nativeElement.focus();
468 | }
469 | }
470 |
471 | setInputElementValue(value: any) {
472 | if (value === null || value === undefined) {
473 | value = '';
474 | }
475 | Promise.resolve().then(() => {
476 | this.zone.runOutsideAngular(() => {
477 | this.renderer.setProperty(this.elRef.nativeElement, 'value', value);
478 | });
479 | });
480 | }
481 |
482 | validate() {
483 | if (this.currentValue === null || this.currentValue === undefined) {
484 | return null;
485 | }
486 |
487 | const isValueInRange = this.strict
488 | ? isDateInRange(this.minDate, this.maxDate, this.currentValue)
489 | : isTimeInRange(this.minDate, this.maxDate, this.currentValue);
490 |
491 | return isValueInRange ? null : { dateRange: true };
492 | }
493 |
494 | ngAfterViewInit() {
495 | this.listeners.push(
496 | this.renderer.listen(
497 | this._matFormFiled
498 | ? this._matFormFiled._elementRef.nativeElement
499 | : this.elRef.nativeElement,
500 | 'click',
501 | this.clickHandler
502 | )
503 | );
504 | }
505 |
506 | clickHandler = (e: FocusEvent) => {
507 | if (
508 | (this.modalRef && this.modalRef.componentInstance.isClosing) ||
509 | this.disabled ||
510 | this.disableDialogOpenOnClick
511 | ) {
512 | return;
513 | }
514 | if (!this.modalRef && !this.disableDialogOpenOnClick) {
515 | this.showDialog();
516 | }
517 | };
518 |
519 | ngOnInit() {
520 | if (this.ngControl && this.ngControl.control?.parent) {
521 | const [key] = Object.entries(this.ngControl.control.parent.controls).find(([, c]) => c === this.ngControl.control);
522 | const control = this.ngControl.control.parent.get(key);
523 | this.required = !!control?.hasValidator(Validators.required);
524 | } else if (this.ngControl) {
525 | const control = (this.ngControl as FormControlName)?.formDirective?.control?.get(this.ngControl.path) || null;
526 | this.required = !!control?.hasValidator(Validators.required);
527 | }
528 |
529 | if (this._platform.isBrowser) {
530 | this.fm.monitor(this.elRef.nativeElement, true).subscribe((origin) => {
531 | this.focused = !!origin;
532 | this.stateChanges.next();
533 | });
534 | }
535 |
536 | const hasMaxDate = !!this.maxDate;
537 | const hasMinDate = !!this.minDate;
538 |
539 | if (hasMinDate || hasMaxDate) {
540 | if (hasMinDate) {
541 | this.minDate.setSeconds(0);
542 | this.minDate.setMilliseconds(0);
543 | }
544 | if (hasMaxDate) {
545 | this.maxDate.setSeconds(0);
546 | this.maxDate.setMilliseconds(0);
547 | }
548 | Promise.resolve().then(() => this.generateAllowedMap());
549 |
550 | if (!(this.ngControl as any)._rawValidators.find((v) => v === this)) {
551 | this.ngControl.control.setValidators(
552 | ((this.ngControl as any)._rawValidators as any[]).concat(this)
553 | );
554 | this.ngControl.control.updateValueAndValidity();
555 | }
556 | }
557 |
558 | this._skipValueChangeEmission = false;
559 | }
560 |
561 | generateAllowedMap() {
562 | const isStrictMode = this.strict && this.value instanceof Date;
563 | if (this.mode === '24h') {
564 | this.allowed24HourMap = {};
565 | for (let h = 0; h < 24; h++) {
566 | for (let m = 0; m < 60; m++) {
567 | const hourMap = this.allowed24HourMap[h] || {};
568 | if (isStrictMode) {
569 | const currentDate = new Date(this.value.getTime());
570 | currentDate.setHours(h);
571 | currentDate.setMinutes(m);
572 | currentDate.setSeconds(0);
573 | currentDate.setMilliseconds(0);
574 | hourMap[m] = isDateInRange(this.minDate, this.maxDate, currentDate);
575 | } else {
576 | hourMap[m] = isAllowed(h, m, this.minDate, this.maxDate, '24h');
577 | }
578 | this.allowed24HourMap[h] = hourMap;
579 | }
580 | }
581 | } else {
582 | this.allowed12HourMap = { am: {}, pm: {} };
583 | for (let h = 0; h < 24; h++) {
584 | const meridiem = h < 12 ? 'am' : 'pm';
585 | for (let m = 0; m < 60; m++) {
586 | const hour = h > 12 ? h - 12 : h === 0 ? 12 : h;
587 | const hourMap = this.allowed12HourMap[meridiem][hour] || {};
588 | if (isStrictMode) {
589 | const currentDate = new Date(this.value.getTime());
590 | currentDate.setHours(h);
591 | currentDate.setMinutes(m);
592 | currentDate.setSeconds(0);
593 | currentDate.setMilliseconds(0);
594 | hourMap[m] = isDateInRange(this.minDate, this.maxDate, currentDate);
595 | } else {
596 | hourMap[m] = isAllowed(h, m, this.minDate, this.maxDate, '24h');
597 | }
598 | this.allowed12HourMap[meridiem][hour] = hourMap;
599 | }
600 | }
601 | }
602 | }
603 |
604 | ngOnChanges(simpleChanges: SimpleChanges) {
605 | this.pattern =
606 | this.mode === '24h'
607 | ? /^[0-9]{1,2}:?([0-9]{1,2})?$/
608 | : /^[0-9]{1,2}:?([0-9]{1,2})?\s?(a|p)?m?$/;
609 |
610 | if (
611 | (simpleChanges.minDate &&
612 | !simpleChanges.minDate.isFirstChange() &&
613 | +simpleChanges.minDate.currentValue !==
614 | simpleChanges.minDate.previousValue) ||
615 | (simpleChanges.maxDate &&
616 | !simpleChanges.maxDate.isFirstChange() &&
617 | +simpleChanges.maxDate.currentValue !==
618 | simpleChanges.maxDate.previousValue) ||
619 | (simpleChanges.disableLimitBase &&
620 | !simpleChanges.disableLimitBase.isFirstChange() &&
621 | +simpleChanges.disableLimitBase.currentValue !==
622 | simpleChanges.disableLimitBase.previousValue)
623 | ) {
624 | this.generateAllowedMap();
625 | this.ngControl.control.updateValueAndValidity();
626 | }
627 |
628 | if (!this.modalRef || !this.modalRef.componentInstance) {
629 | return;
630 | }
631 |
632 | this.modalRef.componentInstance.data = {
633 | mode: this.mode,
634 | value: this.currentValue,
635 | okLabel: this.okLabel,
636 | cancelLabel: this.cancelLabel,
637 | okButtonTemplate: this.okButtonTemplate,
638 | cancelButtonTemplate: this.cancelButtonTemplate,
639 | anteMeridiemAbbreviation: this.anteMeridiemAbbreviation,
640 | postMeridiemAbbreviation: this.postMeridiemAbbreviation,
641 | color: this.color,
642 | isPm: this.isPm,
643 | minDate: this.minDate,
644 | maxDate: this.maxDate,
645 | allowed12HourMap: this.allowed12HourMap,
646 | allowed24HourMap: this.allowed24HourMap,
647 | };
648 | }
649 |
650 | checkValidity(value: Date) {
651 | if (!value) {
652 | return false;
653 | }
654 | const hour = value.getHours();
655 | const minutes = value.getMinutes();
656 | const meridiem = this.isPm ? 'PM' : 'AM';
657 | return isAllowed(
658 | hour,
659 | minutes,
660 | this.minDate,
661 | this.maxDate,
662 | this.mode,
663 | meridiem
664 | );
665 | }
666 |
667 | writeValue(value: Date, isInnerCall = false): void {
668 | if (!isInnerCall) {
669 | this._skipValueChangeEmission = true;
670 | Promise.resolve().then(() => (this._skipValueChangeEmission = false));
671 | }
672 |
673 | if (value) {
674 | value.setSeconds(0);
675 | value.setMilliseconds(0);
676 | }
677 |
678 | if (+this.value !== +value) {
679 | this.value = value;
680 | }
681 | }
682 |
683 | registerOnChange(fn: any): void {
684 | this.onChangeFn = fn;
685 | }
686 |
687 | registerOnTouched(fn: any): void {
688 | this.onTouchedFn = fn;
689 | }
690 |
691 | setDisabledState?(isDisabled: boolean): void {
692 | this.disabled = isDisabled;
693 | }
694 |
695 | showDialog() {
696 | if (this.disabled) {
697 | return;
698 | }
699 | this.isInputFocused = false;
700 | this.modalRef = this.dialog.open(MatTimepickerComponentDialogComponent, {
701 | autoFocus: false,
702 | data: {
703 | mode: this.mode,
704 | value: this.currentValue,
705 | okLabel: this.okLabel,
706 | cancelLabel: this.cancelLabel,
707 | okButtonTemplate: this.okButtonTemplate,
708 | cancelButtonTemplate: this.cancelButtonTemplate,
709 | anteMeridiemAbbreviation: this.anteMeridiemAbbreviation,
710 | postMeridiemAbbreviation: this.postMeridiemAbbreviation,
711 | color: this.color,
712 | isPm: this.isPm,
713 | minDate: this.minDate,
714 | maxDate: this.maxDate,
715 | allowed12HourMap: this.allowed12HourMap,
716 | allowed24HourMap: this.allowed24HourMap,
717 | },
718 | });
719 | const instance = this.modalRef.componentInstance;
720 | instance.changeEvent
721 | .pipe(takeUntil(this.isAlive))
722 | .subscribe(this.handleChange);
723 | instance.okClickEvent
724 | .pipe(takeUntil(this.isAlive))
725 | .subscribe(this.handleOk);
726 | instance.cancelClickEvent
727 | .pipe(takeUntil(this.isAlive))
728 | .subscribe(this.handleCancel);
729 | this.modalRef
730 | .beforeClosed()
731 | .pipe(first())
732 | .subscribe(() => (instance.isClosing = true));
733 | this.modalRef
734 | .afterClosed()
735 | .pipe(first())
736 | .subscribe(() => {
737 | if (this.onTouchedFn) {
738 | this.onTouchedFn();
739 | }
740 | this.modalRef = null;
741 | this.elRef.nativeElement.focus();
742 | });
743 |
744 | this.currentValue = this.value as Date;
745 | }
746 |
747 | handleChange = (newValue) => {
748 | if (!(newValue instanceof Date)) {
749 | return;
750 | }
751 | const v =
752 | this.value instanceof Date ? new Date(this.value.getTime()) : new Date();
753 | v.setHours(newValue.getHours());
754 | v.setMinutes(newValue.getMinutes());
755 | v.setSeconds(0);
756 | v.setMilliseconds(0);
757 | this.currentValue = v;
758 | };
759 |
760 | handleOk = (value) => {
761 | if (!this.currentValue && value) {
762 | this.currentValue = value;
763 | }
764 | if (this.onChangeFn) {
765 | this.onChangeFn(this.currentValue);
766 | }
767 | this.value = this.currentValue;
768 | this.modalRef.close();
769 | };
770 |
771 | handleCancel = () => {
772 | this.modalRef.close();
773 | };
774 |
775 | ngOnDestroy() {
776 | this.isAlive.next();
777 | this.isAlive.complete();
778 | this.stateChanges.complete();
779 |
780 | if (this._platform.isBrowser) {
781 | this.fm.stopMonitoring(this.elRef.nativeElement);
782 | }
783 |
784 | this.listeners.forEach((l) => l());
785 | }
786 | }
787 |
--------------------------------------------------------------------------------
/projects/mat-timepicker/src/lib/util.ts:
--------------------------------------------------------------------------------
1 | import { ITimeData, ClockViewType, ClockMode } from './interfaces-and-types';
2 |
3 | export function twoDigits(n) {
4 | return n < 10 ? `0${n}` : `${n}`;
5 | }
6 |
7 | export function addDays(date: Date, days: number) {
8 | const result = new Date(date);
9 | result.setDate(result.getDate() + days);
10 | return result;
11 | }
12 |
13 | export function convertHoursForMode(hour: number, mode: ClockMode) {
14 | const isPm = hour >= 12;
15 | if (mode === '24h') {
16 | return { hour, isPm };
17 | } else if (hour === 0 || hour === 12) {
18 | return { hour: 12, isPm };
19 | } else if (hour < 12) {
20 | return { hour, isPm };
21 | }
22 | return { hour: hour - 12, isPm };
23 | }
24 |
25 | function mod(a, b) {
26 | return a - Math.floor(a / b) * b;
27 | }
28 |
29 | export function getShortestAngle(from, to) {
30 | const difference = to - from;
31 | return from + mod(difference + 180, 360) - 180;
32 | }
33 |
34 | export function isDateInRange(minDate: Date, maxDate: Date, current: Date) {
35 | const unixCurrentDate = +current;
36 | return (!minDate || +minDate <= unixCurrentDate) && (!maxDate || unixCurrentDate <= +maxDate);
37 | }
38 |
39 | export function isTimeInRange(minDate: Date, maxDate: Date, current: Date) {
40 | if (minDate instanceof Date) {
41 | const newMinDate = new Date();
42 | newMinDate.setHours(minDate.getHours());
43 | newMinDate.setMinutes(minDate.getMinutes());
44 | newMinDate.setSeconds(0);
45 | newMinDate.setMilliseconds(0);
46 | minDate = newMinDate;
47 | }
48 | if (maxDate instanceof Date) {
49 | const newMaxDate = new Date();
50 | newMaxDate.setHours(maxDate.getHours());
51 | newMaxDate.setMinutes(maxDate.getMinutes());
52 | newMaxDate.setSeconds(0);
53 | newMaxDate.setMilliseconds(0);
54 | maxDate = newMaxDate;
55 | }
56 | if (current instanceof Date) {
57 | const newCurrent = new Date();
58 | newCurrent.setHours(current.getHours());
59 | newCurrent.setMinutes(current.getMinutes());
60 | newCurrent.setSeconds(0);
61 | newCurrent.setMilliseconds(0);
62 | current = newCurrent;
63 | }
64 | const unixCurrentDate = +current;
65 | return (!minDate || +minDate <= unixCurrentDate) && (!maxDate || unixCurrentDate <= +maxDate);
66 | }
67 |
68 | // used when generating the allowed maps
69 |
70 | export function isAllowed(
71 | hour: number,
72 | minutes: number,
73 | minDate: Date,
74 | maxDate: Date,
75 | clockMode: ClockMode,
76 | selectedMeridiem?: 'AM' | 'PM'
77 | ) {
78 | if (hour > 24 || hour < 0 || minutes > 60 || minutes < 0) { return false; }
79 |
80 | if (!minDate && !maxDate) { return true; }
81 |
82 | if (clockMode === '12h') {
83 | if (hour === 12 && selectedMeridiem === 'AM') { hour = 0; }
84 | if (hour > 12) { hour -= 12; }
85 | }
86 | const checkDate = new Date();
87 |
88 | checkDate.setHours(hour);
89 | checkDate.setMinutes(minutes);
90 | checkDate.setSeconds(0);
91 | checkDate.setMilliseconds(0);
92 |
93 | return isDateInRange(minDate, maxDate, checkDate);
94 | }
95 |
96 | // used by the clock component to visually disable the not allowed values
97 |
98 | export function getIsAvailabeFn(allowed12HourMap, allowed24HourMap, mode: ClockMode) {
99 | return (value: number, viewType: ClockViewType, isPm: boolean, h?: number) => {
100 | const isHourCheck = viewType === 'hours';
101 | const [hour, minutes] = isHourCheck ? [value, null] : [h, value];
102 |
103 | if (mode === '12h') {
104 | if (!allowed12HourMap) { return true; }
105 | const meridiem = isPm ? 'pm' : 'am';
106 | if (isHourCheck) {
107 | return !!Object.values(allowed12HourMap[meridiem][hour]).find(v => v === true);
108 | }
109 | return allowed12HourMap[meridiem][hour][minutes];
110 | }
111 |
112 | if (!allowed24HourMap) { return true; }
113 |
114 | if (isHourCheck) {
115 | return !!Object.values(allowed24HourMap[hour]).find(v => v === true);
116 | }
117 | return allowed24HourMap[hour][minutes];
118 | };
119 | }
120 |
--------------------------------------------------------------------------------
/projects/mat-timepicker/src/public-api.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Public API Surface of mat-timepicker
3 | */
4 |
5 | export * from './lib/timepicker.directive';
6 | export * from './lib/mat-timepicker.module';
7 |
--------------------------------------------------------------------------------
/projects/mat-timepicker/src/test.ts:
--------------------------------------------------------------------------------
1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files
2 |
3 | import 'core-js/es7/reflect';
4 | import 'zone.js';
5 | import 'zone.js/testing';
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: any;
13 |
14 | // First, initialize the Angular testing environment.
15 | getTestBed().initTestEnvironment(
16 | BrowserDynamicTestingModule,
17 | platformBrowserDynamicTesting(), {
18 | teardown: { destroyAfterEach: false }
19 | }
20 | );
21 | // Then we find all the tests.
22 | const context = require.context('./', true, /\.spec\.ts$/);
23 | // And load the modules.
24 | context.keys().map(context);
25 |
--------------------------------------------------------------------------------
/projects/mat-timepicker/tsconfig.lib.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../../out-tsc/lib",
5 | "declarationMap": true,
6 | "target": "es2020",
7 | "declaration": true,
8 | "inlineSources": true,
9 | "types": [],
10 | "lib": [
11 | "dom",
12 | "es2018"
13 | ]
14 | },
15 | "angularCompilerOptions": {
16 | "skipTemplateCodegen": true,
17 | "strictMetadataEmit": true,
18 | "enableResourceInlining": true
19 | },
20 | "exclude": [
21 | "src/test.ts",
22 | "**/*.spec.ts"
23 | ]
24 | }
25 |
--------------------------------------------------------------------------------
/projects/mat-timepicker/tsconfig.lib.prod.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.lib.json",
3 | "compilerOptions": {
4 | "declarationMap": false
5 | },
6 | "angularCompilerOptions": {
7 | "compilationMode": "partial"
8 | }
9 | }
--------------------------------------------------------------------------------
/projects/mat-timepicker/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../../out-tsc/spec",
5 | "types": [
6 | "jasmine",
7 | "node"
8 | ]
9 | },
10 | "files": [
11 | "src/test.ts"
12 | ],
13 | "include": [
14 | "**/*.spec.ts",
15 | "**/*.d.ts"
16 | ]
17 | }
18 |
--------------------------------------------------------------------------------
/projects/mat-timepicker/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tslint.json",
3 | "rules": {
4 | "directive-selector": [
5 | true,
6 | "attribute",
7 | "mat",
8 | "camelCase"
9 | ],
10 | "component-selector": [
11 | true,
12 | "element",
13 | "mat",
14 | "kebab-case"
15 | ]
16 | }
17 | }
--------------------------------------------------------------------------------
/projects/mat-timepicker/yarn.lock:
--------------------------------------------------------------------------------
1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2 | # yarn lockfile v1
3 |
4 |
5 |
--------------------------------------------------------------------------------
/src/app/app.component.html:
--------------------------------------------------------------------------------
1 |
122 | Reactive Form
123 |
--------------------------------------------------------------------------------
/src/app/app.component.scss:
--------------------------------------------------------------------------------
1 | #timepicker-example-1 {
2 | max-width: 200px;
3 | }
4 |
5 | #timepicker-example-2 {
6 | max-width: 178px;
7 | }
8 |
9 | #timepicker-example-3 {
10 | max-width: 178px;
11 | }
--------------------------------------------------------------------------------
/src/app/app.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestBed, waitForAsync } from '@angular/core/testing';
2 | import { AppComponent } from './app.component';
3 |
4 | describe('AppComponent', () => {
5 | beforeEach(waitForAsync(() => {
6 | TestBed.configureTestingModule({
7 | declarations: [
8 | AppComponent
9 | ],
10 | }).compileComponents();
11 | }));
12 |
13 | it('should create the app', () => {
14 | const fixture = TestBed.createComponent(AppComponent);
15 | const app = fixture.debugElement.componentInstance;
16 | expect(app).toBeTruthy();
17 | });
18 |
19 | it(`should have as title 'angular-material-timepicker'`, () => {
20 | const fixture = TestBed.createComponent(AppComponent);
21 | const app = fixture.debugElement.componentInstance;
22 | expect(app.title).toEqual('angular-material-timepicker');
23 | });
24 |
25 | it('should render title in a h1 tag', () => {
26 | const fixture = TestBed.createComponent(AppComponent);
27 | fixture.detectChanges();
28 | const compiled = fixture.debugElement.nativeElement;
29 | expect(compiled.querySelector('h1').textContent).toContain('Welcome to angular-material-timepicker!');
30 | });
31 | });
32 |
--------------------------------------------------------------------------------
/src/app/app.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 | import {
3 | ShowOnDirtyErrorStateMatcher,
4 | ErrorStateMatcher,
5 | } from '@angular/material/core';
6 | import {
7 | FormControl,
8 | FormBuilder,
9 | FormGroup,
10 | Validators,
11 | } from '@angular/forms';
12 |
13 | class CustomErrorStateMatcher implements ErrorStateMatcher {
14 | isErrorState(control: FormControl | null) {
15 | return control.invalid;
16 | }
17 | }
18 |
19 | @Component({
20 | selector: 'app-root',
21 | templateUrl: './app.component.html',
22 | styleUrls: ['./app.component.scss'],
23 | })
24 | export class AppComponent {
25 | title = 'angular-material-timepicker';
26 | minValue: Date;
27 | maxValue: Date;
28 | defaultValue: Date;
29 |
30 | showOnDirtyErrorStateMatcher = new ShowOnDirtyErrorStateMatcher();
31 | customErrorStateMatcher = new CustomErrorStateMatcher();
32 |
33 | form: FormGroup;
34 |
35 | constructor(private formBuilder: FormBuilder) {
36 | const minValue = new Date();
37 | minValue.setHours(6);
38 | minValue.setMinutes(10);
39 | this.minValue = minValue;
40 |
41 | const maxValue = new Date();
42 | maxValue.setHours(18);
43 | maxValue.setMinutes(10);
44 | this.maxValue = maxValue;
45 |
46 | const d = new Date();
47 | d.setDate(1);
48 | d.setMonth(2);
49 | d.setHours(7);
50 | d.setMinutes(0);
51 | d.setSeconds(1);
52 | d.setMilliseconds(10);
53 | this.defaultValue = d;
54 |
55 | this.form = this.formBuilder.group({
56 | time: [this.defaultValue, Validators.required],
57 | });
58 | }
59 |
60 | timeChangeHandler(data) {
61 | console.log('time changed to', data);
62 | }
63 |
64 | invalidInputHandler() {
65 | console.log('invalid input');
66 | }
67 |
68 | changeMaxValue() {
69 | const maxValue = new Date();
70 | maxValue.setHours(20);
71 | maxValue.setMinutes(10);
72 | this.maxValue = maxValue;
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/app/app.module.ts:
--------------------------------------------------------------------------------
1 | import { BrowserModule } from '@angular/platform-browser';
2 | import { NgModule } from '@angular/core';
3 |
4 | import { AppComponent } from './app.component';
5 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
6 |
7 | import { MatTimepickerModule } from 'mat-timepicker';
8 | import { MatIconModule } from '@angular/material/icon';
9 | import { FormsModule, ReactiveFormsModule } from '@angular/forms';
10 | import { MatFormFieldModule } from '@angular/material/form-field';
11 | import { MatInputModule } from '@angular/material/input';
12 |
13 | @NgModule({
14 | declarations: [
15 | AppComponent
16 | ],
17 | imports: [
18 | BrowserModule,
19 | FormsModule,
20 | BrowserAnimationsModule,
21 | MatTimepickerModule,
22 | MatFormFieldModule,
23 | MatIconModule,
24 | MatInputModule,
25 | ReactiveFormsModule
26 | ],
27 | providers: [],
28 | bootstrap: [AppComponent]
29 | })
30 | export class AppModule { }
31 |
--------------------------------------------------------------------------------
/src/assets/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IliaIdakiev/angular-material-timepicker/3b98106b0effb9db46ed25a1d88bcffad3e75281/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 --prod` 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/IliaIdakiev/angular-material-timepicker/3b98106b0effb9db46ed25a1d88bcffad3e75281/src/favicon.ico
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | AngularMaterialTimepicker
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/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 | * Load `$localize` onto the global scope - used if i18n tags appear in Angular templates.
3 | */
4 | import '@angular/localize/init';
5 | /**
6 | * This file includes polyfills needed by Angular and is loaded before the app.
7 | * You can add your own extra polyfills to this file.
8 | *
9 | * This file is divided into 2 sections:
10 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
11 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main
12 | * file.
13 | *
14 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
15 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
16 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
17 | *
18 | * Learn more in https://angular.io/guide/browser-support
19 | */
20 |
21 | /***************************************************************************************************
22 | * BROWSER POLYFILLS
23 | */
24 |
25 | /**
26 | * By default, zone.js will patch all possible macroTask and DomEvents
27 | * user can disable parts of macroTask/DomEvents patch by setting following flags
28 | * because those flags need to be set before `zone.js` being loaded, and webpack
29 | * will put import in the top of bundle, so user need to create a separate file
30 | * in this directory (for example: zone-flags.ts), and put the following flags
31 | * into that file, and then add the following code before importing zone.js.
32 | * import './zone-flags';
33 | *
34 | * The flags allowed in zone-flags.ts are listed here.
35 | *
36 | * The following flags will work for all browsers.
37 | *
38 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
39 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
40 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
41 | *
42 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
43 | * with the following flag, it will bypass `zone.js` patch for IE/Edge
44 | *
45 | * (window as any).__Zone_enable_cross_context_check = true;
46 | *
47 | */
48 |
49 | /***************************************************************************************************
50 | * Zone JS is required by default for Angular itself.
51 | */
52 | import 'zone.js'; // Included with Angular CLI.
53 |
54 |
55 | /***************************************************************************************************
56 | * APPLICATION IMPORTS
57 | */
58 |
--------------------------------------------------------------------------------
/src/styles.scss:
--------------------------------------------------------------------------------
1 | /* You can add global styles to this file, and also import other style files */
2 | @use '@angular/material' as mat;
3 | @include mat.core();
4 |
5 |
6 | $my-app-primary: mat.define-palette(mat.$blue-grey-palette);
7 | $my-app-accent: mat.define-palette(mat.$pink-palette, 500, 900, A100);
8 | $my-app-warn: mat.define-palette(mat.$deep-orange-palette);
9 | $my-app-theme: mat.define-light-theme($my-app-primary, $my-app-accent, $my-app-warn);
10 | @include mat.all-component-themes($my-app-theme);
11 | .alternate-theme {
12 | $alternate-primary: mat.define-palette(mat.$light-blue-palette);
13 | $alternate-accent: mat.define-palette(mat.$yellow-palette, 400);
14 | $alternate-theme: mat.define-light-theme($alternate-primary, $alternate-accent);
15 | @include mat.all-component-themes($alternate-theme);
16 | }
17 |
18 | html, body { height: 100%; }
19 | body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }
20 |
--------------------------------------------------------------------------------
/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 | declare const require: {
11 | context(path: string, deep?: boolean, filter?: RegExp): {
12 | keys(): string[];
13 | (id: string): T;
14 | };
15 | };
16 |
17 | // First, initialize the Angular testing environment.
18 | getTestBed().initTestEnvironment(
19 | BrowserDynamicTestingModule,
20 | platformBrowserDynamicTesting(), {
21 | teardown: { destroyAfterEach: false }
22 | }
23 | );
24 | // Then we find all the tests.
25 | const context = require.context('./', true, /\.spec\.ts$/);
26 | // And load the modules.
27 | context.keys().map(context);
28 |
--------------------------------------------------------------------------------
/timepicker-hours.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IliaIdakiev/angular-material-timepicker/3b98106b0effb9db46ed25a1d88bcffad3e75281/timepicker-hours.png
--------------------------------------------------------------------------------
/timepicker-min.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IliaIdakiev/angular-material-timepicker/3b98106b0effb9db46ed25a1d88bcffad3e75281/timepicker-min.png
--------------------------------------------------------------------------------
/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "./out-tsc/app",
5 | "types": []
6 | },
7 | "files": [
8 | "src/main.ts",
9 | "src/polyfills.ts"
10 | ],
11 | "include": [
12 | "src/**/*.d.ts"
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "baseUrl": "./",
5 | "outDir": "./dist/out-tsc",
6 | "sourceMap": true,
7 | "declaration": false,
8 | "downlevelIteration": true,
9 | "experimentalDecorators": true,
10 | "module": "esnext",
11 | "moduleResolution": "node",
12 | "importHelpers": true,
13 | "target": "es2020",
14 | "lib": [
15 | "es2018",
16 | "dom"
17 | ],
18 | "paths": {
19 | "mat-timepicker": [
20 | "dist/mat-timepicker/mat-timepicker",
21 | "dist/mat-timepicker"
22 | ]
23 | }
24 | },
25 | "angularCompilerOptions": {
26 | "fullTemplateTypeCheck": true,
27 | "strictInjectionParameters": true
28 | }
29 | }
--------------------------------------------------------------------------------
/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "./out-tsc/spec",
5 | "types": [
6 | "jasmine",
7 | "node"
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 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "tslint:recommended",
3 | "rules": {
4 | "align": {
5 | "options": [
6 | "parameters",
7 | "statements"
8 | ]
9 | },
10 | "array-type": false,
11 | "arrow-return-shorthand": true,
12 | "curly": true,
13 | "deprecation": {
14 | "severity": "warning"
15 | },
16 | "component-class-suffix": true,
17 | "contextual-lifecycle": true,
18 | "directive-class-suffix": true,
19 | "directive-selector": [
20 | true,
21 | "attribute",
22 | "app",
23 | "camelCase"
24 | ],
25 | "component-selector": [
26 | true,
27 | "element",
28 | "app",
29 | "kebab-case"
30 | ],
31 | "eofline": true,
32 | "import-blacklist": [
33 | true,
34 | "rxjs/Rx"
35 | ],
36 | "import-spacing": true,
37 | "indent": {
38 | "options": [
39 | "spaces"
40 | ]
41 | },
42 | "max-classes-per-file": false,
43 | "max-line-length": [
44 | true,
45 | 140
46 | ],
47 | "member-ordering": [
48 | true,
49 | {
50 | "order": [
51 | "static-field",
52 | "instance-field",
53 | "static-method",
54 | "instance-method"
55 | ]
56 | }
57 | ],
58 | "no-console": [
59 | true,
60 | "debug",
61 | "info",
62 | "time",
63 | "timeEnd",
64 | "trace"
65 | ],
66 | "no-empty": false,
67 | "no-inferrable-types": [
68 | true,
69 | "ignore-params"
70 | ],
71 | "no-non-null-assertion": true,
72 | "no-redundant-jsdoc": true,
73 | "no-switch-case-fall-through": true,
74 | "no-var-requires": false,
75 | "object-literal-key-quotes": [
76 | true,
77 | "as-needed"
78 | ],
79 | "quotemark": [
80 | true,
81 | "single"
82 | ],
83 | "semicolon": {
84 | "options": [
85 | "always"
86 | ]
87 | },
88 | "space-before-function-paren": {
89 | "options": {
90 | "anonymous": "never",
91 | "asyncArrow": "always",
92 | "constructor": "never",
93 | "method": "never",
94 | "named": "never"
95 | }
96 | },
97 | "typedef-whitespace": {
98 | "options": [
99 | {
100 | "call-signature": "nospace",
101 | "index-signature": "nospace",
102 | "parameter": "nospace",
103 | "property-declaration": "nospace",
104 | "variable-declaration": "nospace"
105 | },
106 | {
107 | "call-signature": "onespace",
108 | "index-signature": "onespace",
109 | "parameter": "onespace",
110 | "property-declaration": "onespace",
111 | "variable-declaration": "onespace"
112 | }
113 | ]
114 | },
115 | "variable-name": {
116 | "options": [
117 | "ban-keywords",
118 | "check-format",
119 | "allow-pascal-case"
120 | ]
121 | },
122 | "whitespace": {
123 | "options": [
124 | "check-branch",
125 | "check-decl",
126 | "check-operator",
127 | "check-separator",
128 | "check-type",
129 | "check-typecast"
130 | ]
131 | },
132 | "no-conflicting-lifecycle": true,
133 | "no-host-metadata-property": true,
134 | "no-input-rename": true,
135 | "no-inputs-metadata-property": true,
136 | "no-output-native": true,
137 | "no-output-on-prefix": true,
138 | "no-output-rename": true,
139 | "no-outputs-metadata-property": true,
140 | "template-banana-in-box": true,
141 | "template-no-negated-async": true,
142 | "use-lifecycle-interface": true,
143 | "use-pipe-transform-interface": true
144 | },
145 | "rulesDirectory": [
146 | "codelyzer"
147 | ]
148 | }
--------------------------------------------------------------------------------