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

atinc / ngx-tethys / 5fa9630c-19a1-4ee7-af3d-6a0c3535952a

08 Oct 2024 08:24AM UTC coverage: 90.438% (+0.007%) from 90.431%
5fa9630c-19a1-4ee7-af3d-6a0c3535952a

push

circleci

minlovehua
refactor: refactor all control-flow directives to new control-flow #TINFR-381

5511 of 6738 branches covered (81.79%)

Branch coverage included in aggregate %.

98 of 104 new or added lines in 58 files covered. (94.23%)

113 existing lines in 17 files now uncovered.

13253 of 14010 relevant lines covered (94.6%)

991.73 hits per line

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

94.31
/src/nav/nav.component.ts
1
import { ThyPopover } from 'ngx-tethys/popover';
2
import { merge, Observable, of } from 'rxjs';
3
import { debounceTime, take, tap } from 'rxjs/operators';
4
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
5
import { useHostRenderer } from '@tethys/cdk/dom';
6
import {
7
    AfterContentChecked,
8
    AfterContentInit,
9
    AfterViewInit,
10
    ChangeDetectionStrategy,
11
    ChangeDetectorRef,
12
    Component,
13
    ContentChild,
14
    ContentChildren,
15
    DestroyRef,
16
    ElementRef,
17
    HostBinding,
1✔
18
    inject,
19
    Input,
20
    NgZone,
21
    OnChanges,
22
    OnInit,
23
    QueryList,
24
    SimpleChanges,
25
    TemplateRef,
26
    ViewChild
27
} from '@angular/core';
28

1✔
29
import { RouterLinkActive } from '@angular/router';
30
import { ThyNavInkBarDirective } from './nav-ink-bar.directive';
31
import { ThyNavItemDirective } from './nav-item.directive';
32
import { BypassSecurityTrustHtmlPipe } from './nav.pipe';
33
import { ThyDropdownMenuComponent, ThyDropdownMenuItemDirective, ThyDropdownMenuItemActiveDirective } from 'ngx-tethys/dropdown';
1✔
34
import { ThyIcon } from 'ngx-tethys/icon';
35
import { NgClass, NgTemplateOutlet } from '@angular/common';
36
import { coerceBooleanProperty } from 'ngx-tethys/util';
37

38
export type ThyNavType = 'pulled' | 'tabs' | 'pills' | 'lite' | 'primary' | 'secondary' | 'thirdly' | 'secondary-divider';
39
export type ThyNavSize = 'lg' | 'md' | 'sm';
1✔
40
export type ThyNavHorizontal = '' | 'start' | 'center' | 'end';
41

70✔
42
const navTypeClassesMap = {
70✔
43
    pulled: ['thy-nav-pulled'],
21✔
44
    tabs: ['thy-nav-tabs'],
45
    pills: ['thy-nav-pills'],
46
    lite: ['thy-nav-lite'],
47
    //如下类型已经废弃
52✔
48
    primary: ['thy-nav-primary'],
52✔
49
    secondary: ['thy-nav-secondary'],
5✔
50
    thirdly: ['thy-nav-thirdly'],
51
    'secondary-divider': ['thy-nav-secondary-divider']
52
};
53

21!
54
const navSizeClassesMap = {
55
    lg: 'thy-nav-lg',
56
    md: 'thy-nav-md',
55✔
57
    sm: 'thy-nav-sm'
55✔
58
};
59

60
const tabItemRight = 20;
643✔
61

62
/**
63
 * 导航组件
273✔
64
 * @name thy-nav
273✔
65
 * @order 10
66
 */
67
@Component({
75✔
68
    selector: 'thy-nav',
75!
69
    templateUrl: './nav.component.html',
75✔
70
    host: {
71
        class: 'thy-nav'
75✔
72
    },
47✔
73
    changeDetection: ChangeDetectionStrategy.OnPush,
74
    standalone: true,
75✔
75
    imports: [
76
        NgClass,
77
        NgTemplateOutlet,
49✔
78
        ThyNavItemDirective,
49✔
79
        ThyIcon,
49✔
80
        ThyNavInkBarDirective,
49✔
81
        ThyDropdownMenuComponent,
49✔
82
        ThyDropdownMenuItemDirective,
49✔
83
        ThyDropdownMenuItemActiveDirective,
49✔
84
        BypassSecurityTrustHtmlPipe
49✔
85
    ]
49✔
86
})
87
export class ThyNav implements OnInit, AfterViewInit, AfterContentInit, AfterContentChecked, OnChanges {
88
    private readonly destroyRef = inject(DestroyRef);
89

90
    private type: ThyNavType = 'pulled';
91
    private size: ThyNavSize = 'md';
49✔
92
    public initialized = false;
49✔
93

49✔
94
    public horizontal: ThyNavHorizontal;
49✔
95
    public wrapperOffset: { height: number; width: number; left: number; top: number } = {
49✔
96
        height: 0,
49✔
97
        width: 0,
49✔
98
        left: 0,
99
        top: 0
100
    };
49✔
101

41✔
102
    public hiddenItems: ThyNavItemDirective[] = [];
103

49✔
104
    public moreActive: boolean;
105

106
    public showMore = true;
49✔
107

8✔
108
    private moreBtnOffset: { height: number; width: number } = { height: 0, width: 0 };
8✔
109

21✔
110
    private hostRenderer = useHostRenderer();
8✔
111

112
    private innerLinks: QueryList<ThyNavItemDirective>;
113

49✔
114
    /**
137!
115
     * 导航类型
116
     * @type pulled | tabs | pills | lite | primary | secondary | thirdly | secondary-divider
10✔
117
     * @default pulled
2✔
118
     */
2✔
119
    @Input()
2✔
120
    set thyType(type: ThyNavType) {
121
        this.type = type || 'pulled';
122
        if (this.initialized) {
123
            this.updateClasses();
10✔
124
        }
125
    }
126

127
    /**
128
     * 导航大小
49✔
129
     * @type lg | md | sm
8✔
130
     * @default md
8✔
131
     */
132
    @Input()
133
    set thySize(size: ThyNavSize) {
134
        this.size = size;
135
        if (this.initialized) {
136✔
136
            this.updateClasses();
211✔
137
        }
136✔
138
    }
25✔
139

140
    /**
111✔
141
     * 水平排列
53✔
142
     * @type '' | 'start' | 'center' | 'end'
143
     * @default false
144
     */
145
    @Input()
8✔
146
    set thyHorizontal(horizontal: ThyNavHorizontal) {
147
        this.horizontal = (horizontal as string) === 'right' ? 'end' : horizontal;
148
    }
149

150
    /**
151
     * 是否垂直排列
186!
152
     * @default false
153
     */
154
    @HostBinding('class.thy-nav--vertical')
186✔
UNCOV
155
    @Input({ transform: coerceBooleanProperty })
×
156
    thyVertical: boolean;
157

186✔
158
    /**
186✔
159
     * 是否是填充模式
186✔
160
     */
161
    @HostBinding('class.thy-nav--fill')
162
    @Input({ transform: coerceBooleanProperty })
163
    thyFill: boolean = false;
164

138✔
165
    /**
42✔
166
     * 是否响应式,自动计算宽度存放 thyNavItem,并添加更多弹框
167
     * @default false
138✔
168
     */
169
    @Input({ transform: coerceBooleanProperty })
170
    thyResponsive: boolean;
10✔
171

10✔
172
    /**
10✔
173
     * 更多操作的菜单点击内部是否可关闭
1✔
174
     */
1✔
175
    @Input({ transform: coerceBooleanProperty })
1✔
176
    thyInsideClosable = true;
177

9✔
178
    /**
9✔
179
     * 右侧额外区域模板
9!
180
     * @type TemplateRef
7✔
181
     */
182
    @Input() thyExtra: TemplateRef<unknown>;
9!
183

9!
184
    /**
21✔
185
     * @private
186
     */
9✔
187
    @ContentChildren(ThyNavItemDirective, { descendants: true })
9✔
188
    set links(value) {
189
        this.innerLinks = value;
190
        this.prevActiveIndex = NaN;
8✔
191
    }
8✔
192
    get links(): QueryList<ThyNavItemDirective> {
8✔
193
        return this.innerLinks;
8✔
194
    }
21✔
195

21✔
196
    /**
8✔
197
     * @private
8✔
198
     */
1✔
199
    @ContentChildren(RouterLinkActive, { descendants: true }) routers: QueryList<RouterLinkActive>;
200

201
    /**
7✔
202
     * 响应式模式下更多操作模板
203
     * @type TemplateRef
8✔
204
     */
205
    @ContentChild('more') moreOperation: TemplateRef<unknown>;
206

13✔
207
    /**
13✔
208
     * 响应式模式下更多弹框模板
209
     * @type TemplateRef
210
     */
8✔
211
    @ContentChild('morePopover') morePopover: TemplateRef<unknown>;
212

213
    /**
1✔
214
     * 右侧额外区域模板,支持 thyExtra 传参和 <ng-template #extra></ng-template> 模板
1✔
215
     * @name extra
1✔
216
     * @type TemplateRef
1✔
217
     */
3✔
218
    @ContentChild('extra') extra: TemplateRef<unknown>;
3✔
219

1✔
220
    @ViewChild('moreOperationContainer') defaultMoreOperation: ElementRef<HTMLAnchorElement>;
1!
UNCOV
221

×
222
    @ViewChild(ThyNavInkBarDirective, { static: true }) inkBar!: ThyNavInkBarDirective;
223

224
    get showInkBar(): boolean {
1✔
225
        const showTypes: ThyNavType[] = ['pulled', 'tabs'];
226
        return showTypes.includes(this.type);
1✔
227
    }
228

229
    private updateClasses() {
2✔
230
        let classNames: string[] = [];
2✔
231
        if (navTypeClassesMap[this.type]) {
232
            classNames = [...navTypeClassesMap[this.type]];
233
        }
1✔
234
        if (navSizeClassesMap[this.size]) {
235
            classNames.push(navSizeClassesMap[this.size]);
236
        }
10✔
237
        this.hostRenderer.updateClass(classNames);
10!
238
    }
10!
239

12✔
240
    private curActiveIndex: number;
12✔
241

242
    private prevActiveIndex: number = NaN;
243

244
    constructor(
2✔
245
        private elementRef: ElementRef,
246
        private ngZone: NgZone,
247
        private changeDetectorRef: ChangeDetectorRef,
248
        private popover: ThyPopover
249
    ) {}
250

251
    ngOnInit() {
252
        if (!this.thyResponsive) {
253
            this.initialized = true;
254
        }
255

1✔
256
        this.updateClasses();
257
    }
258

135✔
259
    ngAfterViewInit() {
14✔
260
        if (this.thyResponsive) {
14✔
261
            this.setMoreBtnOffset();
262
            this.ngZone.onStable.pipe(take(1)).subscribe(() => {
121✔
263
                this.links.toArray().forEach(link => link.setOffset());
121✔
264
                this.setHiddenItems();
121✔
265
            });
121✔
266
        }
2✔
267
        this.ngZone.runOutsideAngular(() => {
268
            merge(
121✔
269
                this.links.changes,
71✔
270
                this.createResizeObserver(this.elementRef.nativeElement).pipe(debounceTime(100)),
71✔
271
                ...this.links.map(item => this.createResizeObserver(item.elementRef.nativeElement).pipe(debounceTime(100))),
272
                ...(this.routers || []).map(router => router?.isActiveChange)
273
            )
274
                .pipe(
80✔
275
                    takeUntilDestroyed(this.destroyRef),
80✔
276
                    tap(() => {
72✔
277
                        if (this.thyResponsive) {
278
                            this.resetSizes();
279
                            this.setHiddenItems();
1✔
280
                            this.calculateMoreIsActive();
281
                        }
282
                    })
283
                )
284
                .subscribe(() => {
285
                    this.alignInkBarToSelectedTab();
1✔
286
                });
287
        });
288
    }
289

290
    ngAfterContentInit(): void {
291
        if (this.thyResponsive) {
292
            this.ngZone.onStable.pipe(take(1)).subscribe(() => {
293
                this.resetSizes();
294
            });
295
        }
296
    }
297

298
    ngAfterContentChecked() {
299
        this.calculateMoreIsActive();
300

301
        this.curActiveIndex = this.links && this.links.length ? this.links.toArray().findIndex(item => item.linkIsActive()) : -1;
302
        if (this.curActiveIndex < 0) {
303
            this.inkBar.hide();
1✔
304
        } else if (this.curActiveIndex !== this.prevActiveIndex) {
305
            this.alignInkBarToSelectedTab();
306
        }
307
    }
308

309
    private setMoreBtnOffset() {
310
        this.moreBtnOffset = {
311
            height: this.defaultMoreOperation?.nativeElement?.offsetHeight,
312
            width: this.defaultMoreOperation?.nativeElement?.offsetWidth
313
        };
314
    }
315

316
    createResizeObserver(element: HTMLElement) {
317
        return typeof ResizeObserver === 'undefined'
318
            ? of(null)
319
            : new Observable(observer => {
320
                  const resize = new ResizeObserver(entries => {
321
                      observer.next(entries);
322
                  });
323
                  resize.observe(element);
324
                  return () => {
325
                      resize.disconnect();
326
                  };
327
              });
328
    }
329

330
    private calculateMoreIsActive() {
331
        this.moreActive = this.hiddenItems.some(item => {
332
            return item.linkIsActive();
333
        });
334
        this.changeDetectorRef.detectChanges();
335
    }
336

337
    private setHiddenItems() {
338
        this.moreActive = false;
339
        const tabs = this.links.toArray();
340
        if (!tabs.length) {
341
            this.hiddenItems = [];
342
            this.showMore = this.hiddenItems.length > 0;
343
            return;
344
        }
345

346
        const endIndex = this.thyVertical ? this.getShowItemsEndIndexWhenVertical(tabs) : this.getShowItemsEndIndexWhenHorizontal(tabs);
347

348
        const showItems = tabs.slice(0, endIndex + 1);
349
        (showItems || []).forEach(item => {
350
            item.setNavLinkHidden(false);
351
        });
352

353
        this.hiddenItems = endIndex === tabs.length - 1 ? [] : tabs.slice(endIndex + 1);
354
        (this.hiddenItems || []).forEach(item => {
355
            item.setNavLinkHidden(true);
356
        });
357

358
        this.showMore = this.hiddenItems.length > 0;
359
        this.initialized = true;
360
    }
361

362
    private getShowItemsEndIndexWhenHorizontal(tabs: ThyNavItemDirective[]) {
363
        const tabsLength = tabs.length;
364
        let endIndex = tabsLength;
365
        let totalWidth = 0;
366

367
        for (let i = 0; i < tabsLength; i += 1) {
368
            const _totalWidth = i === tabsLength - 1 ? totalWidth + tabs[i].offset.width : totalWidth + tabs[i].offset.width + tabItemRight;
369
            if (_totalWidth > this.wrapperOffset.width) {
370
                let moreOperationWidth = this.moreBtnOffset.width;
371
                if (totalWidth + moreOperationWidth <= this.wrapperOffset.width) {
372
                    endIndex = i - 1;
373
                } else {
374
                    endIndex = i - 2;
375
                }
376
                break;
377
            } else {
378
                totalWidth = _totalWidth;
379
                endIndex = i;
380
            }
381
        }
382
        return endIndex;
383
    }
384

385
    private getShowItemsEndIndexWhenVertical(tabs: ThyNavItemDirective[]) {
386
        const tabsLength = tabs.length;
387
        let endIndex = tabsLength;
388
        let totalHeight = 0;
389
        for (let i = 0; i < tabsLength; i += 1) {
390
            const _totalHeight = totalHeight + tabs[i].offset.height;
391
            if (_totalHeight > this.wrapperOffset.height) {
392
                let moreOperationHeight = this.moreBtnOffset.height;
393
                if (totalHeight + moreOperationHeight <= this.wrapperOffset.height) {
394
                    endIndex = i - 1;
395
                } else {
396
                    endIndex = i - 2;
397
                }
398
                break;
399
            } else {
400
                totalHeight = _totalHeight;
401
                endIndex = i;
402
            }
403
        }
404
        return endIndex;
405
    }
406

407
    private resetSizes() {
408
        this.wrapperOffset = {
409
            height: this.elementRef.nativeElement.offsetHeight || 0,
410
            width: this.elementRef.nativeElement.offsetWidth || 0,
411
            left: this.elementRef.nativeElement.offsetLeft || 0,
412
            top: this.elementRef.nativeElement.offsetTop || 0
413
        };
414
    }
415

416
    openMore(event: Event, template: TemplateRef<any>) {
417
        this.popover.open(template, {
418
            origin: event.currentTarget as HTMLElement,
419
            hasBackdrop: true,
420
            backdropClosable: true,
421
            insideClosable: this.thyInsideClosable,
422
            placement: 'bottom',
423
            panelClass: 'thy-nav-list-popover',
424
            originActiveClass: 'thy-nav-origin-active'
425
        });
426
    }
427

428
    navItemClick(item: ThyNavItemDirective) {
429
        item.elementRef.nativeElement.click();
430
    }
431

432
    private alignInkBarToSelectedTab(): void {
433
        if (!this.showInkBar) {
434
            this.inkBar.hide();
435
            return;
436
        }
437
        const tabs = this.links?.toArray() ?? [];
438
        const selectedItem = tabs.find(item => item.linkIsActive());
439
        let selectedItemElement: HTMLElement = selectedItem && selectedItem.elementRef.nativeElement;
440

441
        if (selectedItem && this.moreActive) {
442
            selectedItemElement = this.defaultMoreOperation.nativeElement;
443
        }
444
        if (selectedItemElement) {
445
            this.prevActiveIndex = this.curActiveIndex;
446
            this.inkBar.alignToElement(selectedItemElement);
447
        }
448
    }
449

450
    ngOnChanges(changes: SimpleChanges): void {
451
        const { thyVertical, thyType } = changes;
452

453
        if (thyType?.currentValue !== thyType?.previousValue || thyVertical?.currentValue !== thyVertical?.previousValue) {
454
            this.alignInkBarToSelectedTab();
455
        }
456
    }
457
}
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