• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

atinc / ngx-tethys / c0ef8457-a839-451f-8b72-80fd73106231

02 Apr 2024 02:27PM UTC coverage: 90.524% (-0.06%) from 90.585%
c0ef8457-a839-451f-8b72-80fd73106231

Pull #3062

circleci

minlovehua
refactor(all): use the transform attribute of @Input() instead of @InputBoolean() and @InputNumber()
Pull Request #3062: refactor(all): use the transform attribute of @input() instead of @InputBoolean() and @InputNumber()

4987 of 6108 branches covered (81.65%)

Branch coverage included in aggregate %.

217 of 223 new or added lines in 82 files covered. (97.31%)

202 existing lines in 53 files now uncovered.

12246 of 12929 relevant lines covered (94.72%)

1055.59 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.01
/src/time-picker/time-picker-panel.component.ts
1
import {
2
    booleanAttribute,
3
    ChangeDetectionStrategy,
4
    ChangeDetectorRef,
5
    Component,
6
    ElementRef,
7
    EventEmitter,
8
    forwardRef,
9
    Input,
10
    NgZone,
11
    OnDestroy,
12
    OnInit,
13
    Output,
14
    ViewChild
1✔
15
} from '@angular/core';
16
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
25✔
17
import { isValid } from 'date-fns';
24✔
18
import { reqAnimFrame } from 'ngx-tethys/core';
24!
19
import { TinyDate } from 'ngx-tethys/util';
24✔
20
import { ThyButton } from 'ngx-tethys/button';
24✔
21
import { NgIf, NgFor, DecimalPipe } from '@angular/common';
22

23
/**
1✔
24
 * 时间选择面板组件
1✔
25
 * @name thy-time-picker-panel
1✔
26
 */
27
@Component({
75✔
28
    selector: 'thy-time-picker-panel',
25✔
29
    templateUrl: './time-picker-panel.component.html',
30
    changeDetection: ChangeDetectionStrategy.OnPush,
31
    providers: [
23✔
32
        {
23✔
33
            provide: NG_VALUE_ACCESSOR,
23✔
34
            multi: true,
23✔
35
            useExisting: forwardRef(() => ThyTimePanel)
23✔
36
        }
23✔
37
    ],
23✔
38
    host: {
23✔
39
        class: 'thy-time-picker-panel',
23✔
40
        '[class.thy-time-picker-panel-has-bottom-operation]': `thyShowOperations`,
41
        '[class.thy-time-picker-panel-columns-2]': `showColumnCount === 2`,
23✔
42
        '[class.thy-time-picker-panel-columns-3]': `showColumnCount === 3`
23✔
43
    },
23✔
44
    standalone: true,
23✔
45
    imports: [NgIf, NgFor, ThyButton, DecimalPipe]
23✔
46
})
23✔
47
export class ThyTimePanel implements OnInit, OnDestroy, ControlValueAccessor {
23✔
48
    @ViewChild('hourListElement', { static: false }) hourListRef: ElementRef<HTMLElement>;
23✔
49

23✔
50
    @ViewChild('minuteListElement', { static: false }) minuteListRef: ElementRef<HTMLElement>;
23✔
51

23✔
52
    @ViewChild('secondListElement', { static: false }) secondListRef: ElementRef<HTMLElement>;
23✔
53

54
    /**
55
     * 展示的日期格式,支持 'HH:mm:ss' | 'HH:mm' | 'mm:ss'
23✔
56
     * @type string
23✔
57
     * @default HH:mm:ss
23✔
58
     */
23✔
59
    @Input() set thyFormat(value: string) {
60
        if (value) {
61
            const formatSet = new Set(value);
62
            this.showHourColumn = formatSet.has('H') || formatSet.has('h');
23✔
63
            this.showMinuteColumn = formatSet.has('m');
23✔
64
            this.showSecondColumn = formatSet.has('s');
23✔
65
        } else {
66
            this.showHourColumn = true;
67
            this.showMinuteColumn = true;
4✔
68
            this.showSecondColumn = true;
4✔
69
        }
4✔
70
        this.showColumnCount = [this.showHourColumn, this.showMinuteColumn, this.showSecondColumn].filter(m => m).length;
4✔
71
        this.cdr.markForCheck();
72
    }
73

3✔
74
    /**
3✔
75
     * 小时间隔步长
3✔
76
     * @type number
3✔
77
     */
78
    @Input() thyHourStep: number = 1;
79

3✔
80
    /**
3✔
81
     * 分钟间隔步长
3✔
82
     * @type number
3✔
83
     */
84
    @Input() thyMinuteStep: number = 1;
85

1✔
86
    /**
1✔
87
     * 秒间隔步长
1✔
88
     * @type number
1✔
89
     */
90
    @Input() thySecondStep: number = 1;
91

2!
92
    /**
2✔
93
     * 展示选择此刻
94
     * @type boolean
10!
95
     */
121✔
96
    @Input({ transform: booleanAttribute }) thyShowSelectNow = true;
121✔
97

98
    /**
99
     * 展示底部操作
60✔
100
     * @type boolean
31✔
101
     */
31✔
102
    @Input({ transform: booleanAttribute }) thyShowOperations = true;
103

104
    /**
29✔
105
     * 选择时间触发的事件
106
     * @type EventEmitter<Date>
60✔
107
     */
60✔
108
    @Output() thyPickChange = new EventEmitter<Date>();
109

110
    /**
23✔
111
     * 关闭面板事件
112
     * @type EventEmitter<void>
113
     */
23✔
114
    @Output() thyClosePanel = new EventEmitter<void>();
115

116
    // margin-top + 1px border
7,628✔
117
    SCROLL_OFFSET_SPACING = 5;
118

119
    SCROLL_DEFAULT_DURATION = 120;
52✔
120

52✔
121
    prefixCls = 'thy-time-picker-panel';
52✔
122

52✔
123
    hourRange: ReadonlyArray<{ value: number; disabled: boolean }> = [];
124

180✔
125
    minuteRange: ReadonlyArray<{ value: number; disabled: boolean }> = [];
69✔
126

3,052✔
127
    secondRange: ReadonlyArray<{ value: number; disabled: boolean }> = [];
3,052✔
128

129
    showHourColumn = true;
130

131
    showMinuteColumn = true;
132

133
    showSecondColumn = true;
134

32✔
135
    showColumnCount: number = 3;
32✔
136

32✔
137
    value: Date;
138

139
    hour: number;
23!
140

23✔
141
    minute: number;
142

23!
143
    second: number;
23✔
144

145
    initialScrollPosition: boolean;
23!
146

23✔
147
    onValueChangeFn: (val: Date) => void = () => void 0;
148

23✔
149
    onTouchedFn: () => void = () => void 0;
150

×
151
    constructor(private cdr: ChangeDetectorRef, private ngZone: NgZone) {}
265✔
152

114✔
153
    ngOnInit(): void {
114✔
154
        this.generateTimeRange();
155
        this.initialValue();
151✔
156
        setTimeout(() => {
151✔
157
            this.initialScrollPosition = true;
151✔
158
        });
151✔
159
    }
144✔
160

144!
UNCOV
161
    generateTimeRange() {
×
162
        this.hourRange = this.buildTimeRange(24, this.thyHourStep);
163
        this.minuteRange = this.buildTimeRange(60, this.thyMinuteStep);
144✔
164
        this.secondRange = this.buildTimeRange(60, this.thySecondStep);
165
    }
166

167
    pickHours(hours: { value: number; disabled: boolean }, index: number) {
×
168
        this.value.setHours(hours.value);
60✔
169
        this.hour = hours.value;
165✔
170
        this.thyPickChange.emit(this.value);
171
        this.scrollTo(this.hourListRef.nativeElement, index);
60✔
172
    }
313✔
173

174
    pickMinutes(minutes: { value: number; disabled: boolean }, index: number) {
60✔
175
        this.value.setMinutes(minutes.value);
203✔
176
        this.minute = minutes.value;
177
        this.thyPickChange.emit(this.value);
178
        this.scrollTo(this.minuteListRef.nativeElement, index);
179
    }
180

23✔
181
    pickSeconds(seconds: { value: number; disabled: boolean }, index: number) {
23✔
182
        this.value.setSeconds(seconds.value);
183
        this.second = seconds.value;
184
        this.thyPickChange.emit(this.value);
1✔
185
        this.scrollTo(this.secondListRef.nativeElement, index);
186
    }
187

188
    selectNow() {
1✔
189
        this.value = new Date();
190
        this.setHMSProperty();
191
        this.thyPickChange.emit(this.value);
192
        this.thyClosePanel.emit();
193
    }
194

195
    confirmPickTime() {
196
        this.onValueChangeFn(this.value || new Date());
197
        this.thyClosePanel.emit();
198
    }
199

200
    scrollTo(container: HTMLElement, index: number = 0, duration: number = this.SCROLL_DEFAULT_DURATION) {
201
        const offsetTop = (container.children[index] as HTMLElement).offsetTop - this.SCROLL_OFFSET_SPACING;
202
        this.runScrollAnimationFrame(container, offsetTop, duration);
1✔
203
    }
204

205
    writeValue(value: Date | number): void {
206
        if (value && isValid(value)) {
207
            this.value = new Date(value);
208
            this.setHMSProperty();
209
        } else {
210
            this.initialValue();
211
        }
23✔
212
        this.autoScroll(this.initialScrollPosition ? this.SCROLL_DEFAULT_DURATION : 0);
213

214
        this.cdr.markForCheck();
215
    }
216

217
    registerOnChange(fn: (value: Date) => void): void {
218
        this.onValueChangeFn = fn;
219
    }
220

221
    registerOnTouched(fn: () => void): void {
222
        this.onTouchedFn = fn;
223
    }
224

225
    trackByFn(index: number): number {
226
        return index;
227
    }
228

229
    private initialValue() {
230
        this.hour = 0;
231
        this.minute = 0;
232
        this.second = 0;
233
        this.value = new TinyDate().setHms(0, 0, 0).nativeDate;
234
    }
235

236
    private buildTimeRange(length: number, step: number = 1, start: number = 0, disables: number[] = []) {
237
        return new Array(Math.ceil(length / step)).fill(0).map((_, i) => {
238
            const value = (i + start) * step;
239
            return {
240
                value: value,
241
                disabled: disables.indexOf(value) > -1
242
            };
243
        });
244
    }
245

246
    private setHMSProperty() {
247
        this.hour = this.value.getHours();
248
        this.minute = this.value.getMinutes();
249
        this.second = this.value.getSeconds();
250
    }
251

252
    private resetScrollPosition() {
253
        if (this.hourListRef) {
254
            this.hourListRef.nativeElement.scrollTop = 0;
255
        }
256
        if (this.minuteListRef) {
257
            this.minuteListRef.nativeElement.scrollTop = 0;
258
        }
259
        if (this.secondListRef) {
260
            this.secondListRef.nativeElement.scrollTop = 0;
261
        }
262
        this.initialScrollPosition = false;
263
    }
264

265
    private runScrollAnimationFrame(container: HTMLElement, to: number, duration: number = this.SCROLL_DEFAULT_DURATION) {
266
        if (duration <= 0) {
267
            container.scrollTop = to;
268
            return;
269
        }
270
        const offset = to - container.scrollTop;
271
        const frame = (offset / duration) * 10;
272
        this.ngZone.runOutsideAngular(() => {
273
            reqAnimFrame(() => {
274
                container.scrollTop += frame;
275
                if (container.scrollTop === to) {
276
                    return;
277
                }
278
                this.runScrollAnimationFrame(container, to, duration - 10);
279
            });
280
        });
281
    }
282

283
    private autoScroll(duration: number = this.SCROLL_DEFAULT_DURATION) {
284
        if (this.hourListRef) {
285
            this.scrollTo(
286
                this.hourListRef.nativeElement,
287
                this.hourRange.findIndex(m => m.value === this.hour),
288
                duration
289
            );
290
        }
291
        if (this.minuteListRef) {
292
            this.scrollTo(
293
                this.minuteListRef.nativeElement,
294
                this.minuteRange.findIndex(m => m.value === this.minute),
295
                duration
296
            );
297
        }
298
        if (this.secondListRef) {
299
            this.scrollTo(
300
                this.secondListRef.nativeElement,
301
                this.secondRange.findIndex(m => m.value === this.second),
302
                duration
303
            );
304
        }
305
    }
306

307
    ngOnDestroy(): void {
308
        // 关闭面板时有 0.2s 的动画,所以延迟 200ms 再重置滚动位置
309
        setTimeout(() => {
310
            this.resetScrollPosition();
311
        }, 200);
312
    }
313
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc