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

atinc / ngx-tethys / 68ef226c-f83e-44c1-b8ed-e420a83c5d84

28 May 2025 10:31AM UTC coverage: 10.352% (-80.0%) from 90.316%
68ef226c-f83e-44c1-b8ed-e420a83c5d84

Pull #3460

circleci

pubuzhixing8
chore: xxx
Pull Request #3460: refactor(icon): migrate signal input #TINFR-1476

132 of 6823 branches covered (1.93%)

Branch coverage included in aggregate %.

10 of 14 new or added lines in 1 file covered. (71.43%)

11648 existing lines in 344 files now uncovered.

2078 of 14525 relevant lines covered (14.31%)

6.69 hits per line

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

2.11
/src/time-picker/time-picker-panel.component.ts
1
import { DecimalPipe } from '@angular/common';
2
import {
3
    ChangeDetectionStrategy,
4
    ChangeDetectorRef,
5
    Component,
6
    ElementRef,
7
    EventEmitter,
8
    forwardRef,
9
    inject,
10
    Input,
11
    NgZone,
12
    OnDestroy,
13
    OnInit,
14
    Output,
1✔
15
    Signal,
UNCOV
16
    ViewChild
×
UNCOV
17
} from '@angular/core';
×
UNCOV
18
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
×
UNCOV
19
import { ThyButton } from 'ngx-tethys/button';
×
UNCOV
20
import { reqAnimFrame } from 'ngx-tethys/core';
×
UNCOV
21
import { injectLocale, ThyTimePickerLocale } from 'ngx-tethys/i18n';
×
UNCOV
22
import { coerceBooleanProperty, isValid, TinyDate } from 'ngx-tethys/util';
×
UNCOV
23

×
UNCOV
24
/**
×
UNCOV
25
 * 时间选择面板组件
×
26
 * @name thy-time-picker-panel
UNCOV
27
 */
×
UNCOV
28
@Component({
×
UNCOV
29
    selector: 'thy-time-picker-panel',
×
UNCOV
30
    templateUrl: './time-picker-panel.component.html',
×
UNCOV
31
    changeDetection: ChangeDetectionStrategy.OnPush,
×
UNCOV
32
    providers: [
×
UNCOV
33
        {
×
UNCOV
34
            provide: NG_VALUE_ACCESSOR,
×
UNCOV
35
            multi: true,
×
UNCOV
36
            useExisting: forwardRef(() => ThyTimePanel)
×
UNCOV
37
        }
×
UNCOV
38
    ],
×
39
    host: {
40
        class: 'thy-time-picker-panel',
UNCOV
41
        '[class.thy-time-picker-panel-has-bottom-operation]': `thyShowOperations`,
×
UNCOV
42
        '[class.thy-time-picker-panel-columns-2]': `showColumnCount === 2`,
×
UNCOV
43
        '[class.thy-time-picker-panel-columns-3]': `showColumnCount === 3`
×
UNCOV
44
    },
×
UNCOV
45
    imports: [ThyButton, DecimalPipe]
×
46
})
47
export class ThyTimePanel implements OnInit, OnDestroy, ControlValueAccessor {
UNCOV
48
    private cdr = inject(ChangeDetectorRef);
×
UNCOV
49
    private ngZone = inject(NgZone);
×
UNCOV
50
    locale: Signal<ThyTimePickerLocale> = injectLocale('timePicker');
×
51

UNCOV
52
    @ViewChild('hourListElement', { static: false }) hourListRef: ElementRef<HTMLElement>;
×
UNCOV
53

×
54
    @ViewChild('minuteListElement', { static: false }) minuteListRef: ElementRef<HTMLElement>;
55

UNCOV
56
    @ViewChild('secondListElement', { static: false }) secondListRef: ElementRef<HTMLElement>;
×
UNCOV
57

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

×
78
    /**
79
     * 小时间隔步长
UNCOV
80
     * @type number
×
UNCOV
81
     */
×
UNCOV
82
    @Input() thyHourStep: number = 1;
×
UNCOV
83

×
84
    /**
85
     * 分钟间隔步长
UNCOV
86
     * @type number
×
UNCOV
87
     */
×
UNCOV
88
    @Input() thyMinuteStep: number = 1;
×
UNCOV
89

×
90
    /**
91
     * 秒间隔步长
UNCOV
92
     * @type number
×
UNCOV
93
     */
×
94
    @Input() thySecondStep: number = 1;
95

×
UNCOV
96
    /**
×
UNCOV
97
     * 展示选择此刻
×
98
     * @type boolean
99
     */
UNCOV
100
    @Input({ transform: coerceBooleanProperty }) thyShowSelectNow = true;
×
UNCOV
101

×
UNCOV
102
    /**
×
103
     * 展示底部操作
104
     * @type boolean
UNCOV
105
     */
×
106
    @Input({ transform: coerceBooleanProperty }) thyShowOperations = true;
UNCOV
107

×
UNCOV
108
    /**
×
109
     * 选择时间触发的事件
110
     * @type EventEmitter<Date>
UNCOV
111
     */
×
112
    @Output() thyPickChange = new EventEmitter<Date>();
113

UNCOV
114
    /**
×
115
     * 关闭面板事件
116
     * @type EventEmitter<void>
UNCOV
117
     */
×
UNCOV
118
    @Output() thyClosePanel = new EventEmitter<void>();
×
UNCOV
119

×
UNCOV
120
    // margin-top + 1px border
×
121
    SCROLL_OFFSET_SPACING = 5;
122

×
UNCOV
123
    SCROLL_DEFAULT_DURATION = 120;
×
UNCOV
124

×
UNCOV
125
    prefixCls = 'thy-time-picker-panel';
×
126

127
    hourRange: ReadonlyArray<{ value: number; disabled: boolean }> = [];
128

129
    minuteRange: ReadonlyArray<{ value: number; disabled: boolean }> = [];
130

131
    secondRange: ReadonlyArray<{ value: number; disabled: boolean }> = [];
UNCOV
132

×
UNCOV
133
    showHourColumn = true;
×
UNCOV
134

×
135
    showMinuteColumn = true;
136

UNCOV
137
    showSecondColumn = true;
×
UNCOV
138

×
139
    showColumnCount: number = 3;
UNCOV
140

×
UNCOV
141
    value: Date;
×
142

UNCOV
143
    hour: number;
×
UNCOV
144

×
145
    minute: number;
UNCOV
146

×
147
    second: number;
148

×
UNCOV
149
    initialScrollPosition: boolean;
×
UNCOV
150

×
UNCOV
151
    onValueChangeFn: (val: Date) => void = () => void 0;
×
152

UNCOV
153
    onTouchedFn: () => void = () => void 0;
×
UNCOV
154

×
UNCOV
155
    ngOnInit(): void {
×
UNCOV
156
        this.generateTimeRange();
×
UNCOV
157
        this.initialValue();
×
UNCOV
158
        setTimeout(() => {
×
159
            this.initialScrollPosition = true;
×
160
        });
UNCOV
161
    }
×
162

163
    generateTimeRange() {
164
        this.hourRange = this.buildTimeRange(24, this.thyHourStep);
165
        this.minuteRange = this.buildTimeRange(60, this.thyMinuteStep);
×
UNCOV
166
        this.secondRange = this.buildTimeRange(60, this.thySecondStep);
×
UNCOV
167
    }
×
168

UNCOV
169
    pickHours(hours: { value: number; disabled: boolean }, index: number) {
×
UNCOV
170
        this.value.setHours(hours.value);
×
171
        this.hour = hours.value;
UNCOV
172
        this.thyPickChange.emit(this.value);
×
UNCOV
173
        this.scrollTo(this.hourListRef.nativeElement, index);
×
174
    }
175

176
    pickMinutes(minutes: { value: number; disabled: boolean }, index: number) {
177
        this.value.setMinutes(minutes.value);
UNCOV
178
        this.minute = minutes.value;
×
UNCOV
179
        this.thyPickChange.emit(this.value);
×
180
        this.scrollTo(this.minuteListRef.nativeElement, index);
181
    }
182

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

190
    selectNow() {
191
        this.value = new TinyDate()?.nativeDate;
192
        this.setHMSProperty();
193
        this.thyPickChange.emit(this.value);
194
        this.thyClosePanel.emit();
195
    }
196

1✔
197
    confirmPickTime() {
198
        this.onValueChangeFn(this.value || new TinyDate()?.nativeDate);
199
        this.thyClosePanel.emit();
200
    }
201

202
    scrollTo(container: HTMLElement, index: number = 0, duration: number = this.SCROLL_DEFAULT_DURATION) {
203
        const offsetTop = (container.children[index] as HTMLElement).offsetTop - this.SCROLL_OFFSET_SPACING;
204
        this.runScrollAnimationFrame(container, offsetTop, duration);
UNCOV
205
    }
×
206

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

216
        this.cdr.markForCheck();
217
    }
218

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

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

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

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

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

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

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

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

305
    ngOnDestroy(): void {
306
        // 关闭面板时有 0.2s 的动画,所以延迟 200ms 再重置滚动位置
307
        setTimeout(() => {
308
            this.resetScrollPosition();
309
        }, 200);
310
    }
311
}
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