Skip to content

Commit 3a638af

Browse files
authored
feat(module:cron-expression): add cron-expression component (#7677)
* feat(module:cron-expression): add cron-expression component * feat(module:cron-expression): add cron-expression component i18n * feat(module:cron-expression): modify cron-expression i18.interface * feat(module:cron-expression): detail modification * feat(module:cron-expression): cron-expression support spring * feat(module:cron-expression): adjust the layout
1 parent e3103f0 commit 3a638af

30 files changed

Lines changed: 954 additions & 0 deletions

components/components.less

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,4 @@
6262
@import './result/style/entry.less';
6363
@import './space/style/entry.less';
6464
@import './image/style/entry.less';
65+
@import './cron-expression/style/entry.less';
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/**
2+
* Use of this source code is governed by an MIT-style license that can be
3+
* found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE
4+
*/
5+
6+
import { Component, ViewEncapsulation, ChangeDetectionStrategy, Input, Output, EventEmitter } from '@angular/core';
7+
8+
import { CronChangeType, TimeType } from './typings';
9+
10+
@Component({
11+
changeDetection: ChangeDetectionStrategy.OnPush,
12+
encapsulation: ViewEncapsulation.None,
13+
selector: 'nz-cron-expression-input',
14+
exportAs: 'nzCronExpression',
15+
template: `
16+
<div class="ant-cron-expression-input">
17+
<input
18+
nz-input
19+
[(ngModel)]="value"
20+
[name]="label"
21+
(focus)="focusInputEffect($event)"
22+
(blur)="blurInputEffect()"
23+
(ngModelChange)="setValue()"
24+
/>
25+
</div>
26+
`
27+
})
28+
export class NzCronExpressionInputComponent {
29+
@Input() value: string = '0';
30+
@Input() label: TimeType = 'second';
31+
@Output() readonly focusEffect = new EventEmitter<TimeType>();
32+
@Output() readonly blurEffect = new EventEmitter<void>();
33+
@Output() readonly getValue = new EventEmitter<CronChangeType>();
34+
35+
constructor() {}
36+
37+
focusInputEffect(event: FocusEvent): void {
38+
this.focusEffect.emit(this.label);
39+
(event.target as HTMLInputElement).select();
40+
}
41+
42+
blurInputEffect(): void {
43+
this.blurEffect.emit();
44+
}
45+
46+
setValue(): void {
47+
this.getValue.emit({ label: this.label, value: this.value });
48+
}
49+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/**
2+
* Use of this source code is governed by an MIT-style license that can be
3+
* found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE
4+
*/
5+
6+
import { Component, ViewEncapsulation, ChangeDetectionStrategy, Input, OnInit } from '@angular/core';
7+
8+
import { NzCronExpressionLabelI18n } from 'ng-zorro-antd/i18n';
9+
10+
import { TimeType, TimeTypeError } from './typings';
11+
12+
@Component({
13+
changeDetection: ChangeDetectionStrategy.OnPush,
14+
encapsulation: ViewEncapsulation.None,
15+
selector: 'nz-cron-expression-label',
16+
exportAs: 'nzCronExpression',
17+
template: `
18+
<div
19+
class="ant-cron-expression-label"
20+
[class.ant-cron-expression-label-foucs]="labelFocus === type"
21+
[class.ant-cron-expression-error]="!valid"
22+
>
23+
<label nz-tooltip [nzTooltipTitle]="error" [nzTooltipVisible]="!valid" nzTooltipPlacement="bottom">
24+
{{ locale[type] }}
25+
</label>
26+
</div>
27+
<ng-template #error>
28+
<div class="ant-cron-expression-hint" [innerHTML]="locale[labelError]"></div>
29+
</ng-template>
30+
`
31+
})
32+
export class NzCronExpressionLabelComponent implements OnInit {
33+
@Input() type: TimeType = 'second';
34+
@Input() valid: boolean = true;
35+
@Input() locale!: NzCronExpressionLabelI18n;
36+
@Input() labelFocus: string | null = null;
37+
labelError: TimeTypeError = 'secondError';
38+
39+
constructor() {}
40+
41+
ngOnInit(): void {
42+
this.labelError = `${this.type}Error`;
43+
}
44+
}
Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
/**
2+
* Use of this source code is governed by an MIT-style license that can be
3+
* found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE
4+
*/
5+
6+
import {
7+
ChangeDetectionStrategy,
8+
ChangeDetectorRef,
9+
Component,
10+
forwardRef,
11+
Input,
12+
OnDestroy,
13+
OnInit,
14+
TemplateRef,
15+
ViewEncapsulation
16+
} from '@angular/core';
17+
import {
18+
AsyncValidator,
19+
ControlValueAccessor,
20+
FormControl,
21+
NG_ASYNC_VALIDATORS,
22+
NG_VALUE_ACCESSOR,
23+
UntypedFormBuilder,
24+
UntypedFormGroup,
25+
ValidationErrors,
26+
Validators
27+
} from '@angular/forms';
28+
import { Observable, of, Subject } from 'rxjs';
29+
import { takeUntil } from 'rxjs/operators';
30+
31+
import { CronExpression, parseExpression } from 'cron-parser';
32+
33+
import { NzSafeAny } from 'ng-zorro-antd/core/types';
34+
import { InputBoolean } from 'ng-zorro-antd/core/util';
35+
import { NzCronExpressionI18nInterface, NzI18nService } from 'ng-zorro-antd/i18n';
36+
37+
import { CronChangeType, CronType, NzCronExpressionSize, TimeType } from './typings';
38+
39+
@Component({
40+
changeDetection: ChangeDetectionStrategy.OnPush,
41+
encapsulation: ViewEncapsulation.None,
42+
selector: 'nz-cron-expression',
43+
exportAs: 'nzCronExpression',
44+
template: `
45+
<div class="ant-cron-expression">
46+
<div class="ant-cron-expression-content">
47+
<div
48+
class="ant-cron-expression-input-group"
49+
[class.ant-cron-expression-input-group-lg]="nzSize === 'large'"
50+
[class.ant-cron-expression-input-group-sm]="nzSize === 'small'"
51+
[class.ant-cron-expression-input-group-focus]="focus"
52+
[class.ant-cron-expression-input-group-error]="!validateForm.valid"
53+
[class.ant-cron-expression-input-group-error-focus]="!validateForm.valid && focus"
54+
>
55+
<ng-container *ngFor="let label of labels">
56+
<nz-cron-expression-input
57+
[value]="this.validateForm.controls[label].value"
58+
[label]="label"
59+
(focusEffect)="focusEffect($event)"
60+
(blurEffect)="blurEffect()"
61+
(getValue)="getValue($event)"
62+
></nz-cron-expression-input>
63+
</ng-container>
64+
</div>
65+
<div class="ant-cron-expression-label-group">
66+
<ng-container *ngFor="let label of labels">
67+
<nz-cron-expression-label
68+
[type]="label"
69+
[valid]="this.validateForm.controls[label].valid"
70+
[labelFocus]="labelFocus"
71+
[locale]="locale"
72+
></nz-cron-expression-label>
73+
</ng-container>
74+
</div>
75+
<nz-collapse *ngIf="!nzCollapseDisable" [nzBordered]="false">
76+
<nz-collapse-panel [nzHeader]="nextDate">
77+
<ng-container *ngIf="validateForm.valid">
78+
<ul class="ant-cron-expression-preview-date">
79+
<li style="margin: 0" *ngFor="let dateItem of nextTimeList">
80+
{{ dateItem | date: 'YYYY-MM-dd HH:mm:ss' }}
81+
</li>
82+
<li><a (click)="loadMorePreview()">···</a></li>
83+
</ul>
84+
</ng-container>
85+
<ng-container *ngIf="!validateForm.valid">{{ locale.cronError }}</ng-container>
86+
</nz-collapse-panel>
87+
</nz-collapse>
88+
</div>
89+
<div class="ant-cron-expression-map" *ngIf="nzExtra">
90+
<ng-template [ngTemplateOutlet]="nzExtra"></ng-template>
91+
</div>
92+
<ng-template #nextDate>
93+
<ng-container *ngIf="validateForm.valid">
94+
{{ dateTime | date: 'YYYY-MM-dd HH:mm:ss' }}
95+
</ng-container>
96+
<ng-container *ngIf="!validateForm.valid">{{ locale.cronError }}</ng-container>
97+
</ng-template>
98+
</div>
99+
`,
100+
providers: [
101+
{
102+
provide: NG_ASYNC_VALIDATORS,
103+
useExisting: forwardRef(() => NzCronExpressionComponent),
104+
multi: true
105+
},
106+
{
107+
provide: NG_VALUE_ACCESSOR,
108+
useExisting: forwardRef(() => NzCronExpressionComponent),
109+
multi: true
110+
}
111+
]
112+
})
113+
export class NzCronExpressionComponent implements OnInit, ControlValueAccessor, AsyncValidator, OnDestroy {
114+
@Input() nzSize: NzCronExpressionSize = 'default';
115+
@Input() nzType: 'linux' | 'spring' = 'linux';
116+
@Input() @InputBoolean() nzCollapseDisable: boolean = false;
117+
@Input() nzExtra?: TemplateRef<void> | null = null;
118+
119+
locale!: NzCronExpressionI18nInterface;
120+
focus: boolean = false;
121+
labelFocus: TimeType | null = null;
122+
validLabel: string | null = null;
123+
labels: TimeType[] = [];
124+
interval!: CronExpression<false>;
125+
nextTimeList: Date[] = [];
126+
dateTime: Date = new Date();
127+
private destroy$ = new Subject<void>();
128+
129+
validateForm!: UntypedFormGroup;
130+
131+
onChange: NzSafeAny = () => {};
132+
onTouch: () => void = () => null;
133+
134+
convertFormat(value: string): void {
135+
const values = value.split(' ');
136+
const valueObject: CronType = {};
137+
this.labels.map((a, b) => {
138+
valueObject[a] = values[b];
139+
});
140+
this.validateForm.patchValue(valueObject);
141+
}
142+
143+
writeValue(value: string | null): void {
144+
if (value) {
145+
this.convertFormat(value);
146+
}
147+
}
148+
149+
registerOnChange(fn: NzSafeAny): void {
150+
this.onChange = fn;
151+
}
152+
153+
registerOnTouched(fn: NzSafeAny): void {
154+
this.onTouch = fn;
155+
}
156+
157+
validate(): Observable<ValidationErrors | null> {
158+
if (this.validateForm.valid) {
159+
return of(null);
160+
} else {
161+
return of({ error: true });
162+
}
163+
}
164+
165+
constructor(private formBuilder: UntypedFormBuilder, private cdr: ChangeDetectorRef, private i18n: NzI18nService) {}
166+
167+
ngOnInit(): void {
168+
if (this.nzType === 'spring') {
169+
this.labels = ['second', 'minute', 'hour', 'day', 'month', 'week'];
170+
this.validateForm = this.formBuilder.group({
171+
second: ['0', Validators.required, this.checkValid],
172+
minute: ['*', Validators.required, this.checkValid],
173+
hour: ['*', Validators.required, this.checkValid],
174+
day: ['*', Validators.required, this.checkValid],
175+
month: ['*', Validators.required, this.checkValid],
176+
week: ['*', Validators.required, this.checkValid]
177+
});
178+
} else {
179+
this.labels = ['minute', 'hour', 'day', 'month', 'week'];
180+
this.validateForm = this.formBuilder.group({
181+
minute: ['*', Validators.required, this.checkValid],
182+
hour: ['*', Validators.required, this.checkValid],
183+
day: ['*', Validators.required, this.checkValid],
184+
month: ['*', Validators.required, this.checkValid],
185+
week: ['*', Validators.required, this.checkValid]
186+
});
187+
}
188+
this.i18n.localeChange.pipe(takeUntil(this.destroy$)).subscribe(() => {
189+
this.locale = this.i18n.getLocaleData('CronExpression');
190+
this.cdr.markForCheck();
191+
});
192+
193+
this.previewDate(this.validateForm.value);
194+
195+
this.validateForm.valueChanges.pipe(takeUntil(this.destroy$)).subscribe(value => {
196+
this.onChange(Object.values(value).join(' '));
197+
this.previewDate(value);
198+
this.cdr.markForCheck();
199+
});
200+
}
201+
202+
previewDate(value: CronType): void {
203+
try {
204+
this.interval = parseExpression(Object.values(value).join(' '));
205+
this.dateTime = this.interval.next().toDate();
206+
this.nextTimeList = [
207+
this.interval.next().toDate(),
208+
this.interval.next().toDate(),
209+
this.interval.next().toDate(),
210+
this.interval.next().toDate(),
211+
this.interval.next().toDate()
212+
];
213+
} catch (err: NzSafeAny) {
214+
return;
215+
}
216+
}
217+
218+
loadMorePreview(): void {
219+
this.nextTimeList = [
220+
...this.nextTimeList,
221+
this.interval.next().toDate(),
222+
this.interval.next().toDate(),
223+
this.interval.next().toDate(),
224+
this.interval.next().toDate(),
225+
this.interval.next().toDate()
226+
];
227+
this.cdr.markForCheck();
228+
}
229+
230+
focusEffect(value: TimeType): void {
231+
this.focus = true;
232+
this.labelFocus = value;
233+
this.cdr.markForCheck();
234+
}
235+
236+
blurEffect(): void {
237+
this.focus = false;
238+
this.labelFocus = null;
239+
this.cdr.markForCheck();
240+
}
241+
242+
getValue(item: CronChangeType): void {
243+
this.validLabel = item.label;
244+
this.validateForm.controls[item.label].patchValue(item.value);
245+
this.cdr.markForCheck();
246+
}
247+
248+
checkValid = (control: FormControl): Observable<ValidationErrors | null> => {
249+
if (control.value) {
250+
try {
251+
const cron: string[] = [];
252+
this.labels.forEach(label => {
253+
label === this.validLabel ? cron.push(control.value) : cron.push('*');
254+
});
255+
parseExpression(cron.join(' '));
256+
} catch (err: unknown) {
257+
return of({ error: true });
258+
}
259+
}
260+
return of(null);
261+
};
262+
263+
ngOnDestroy(): void {
264+
this.destroy$.next();
265+
this.destroy$.complete();
266+
}
267+
}

0 commit comments

Comments
 (0)