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

atinc / ngx-tethys / e42eb54e-db75-4ac6-9d54-d98ab6d94e44

04 Sep 2023 08:42AM UTC coverage: 90.2%. Remained the same
e42eb54e-db75-4ac6-9d54-d98ab6d94e44

Pull #2829

circleci

cmm-va
fix: reset form code
Pull Request #2829: fix: add tabIndex

5163 of 6383 branches covered (0.0%)

Branch coverage included in aggregate %.

76 of 76 new or added lines in 24 files covered. (100.0%)

13015 of 13770 relevant lines covered (94.52%)

972.29 hits per line

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

94.39
/src/nav/nav.component.ts
1
import { InputBoolean, UnsubscribeMixin } from 'ngx-tethys/core';
2
import { ThyPopover } from 'ngx-tethys/popover';
3
import { merge, Observable, of } from 'rxjs';
4
import { debounceTime, take, takeUntil, tap } from 'rxjs/operators';
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
    ElementRef,
16
    HostBinding,
1✔
17
    Input,
18
    NgZone,
19
    OnChanges,
20
    OnDestroy,
21
    OnInit,
22
    QueryList,
23
    SimpleChanges,
24
    TemplateRef,
25
    ViewChild
26
} from '@angular/core';
27

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

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

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

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

58
const tabItemRight = 20;
59

76✔
60
/**
76!
61
 * 导航组件
76✔
62
 * @name thy-nav
63
 * @order 10
76✔
64
 */
48✔
65
@Component({
66
    selector: 'thy-nav',
76✔
67
    templateUrl: './nav.component.html',
68
    host: {
69
        class: 'thy-nav'
49✔
70
    },
49✔
71
    changeDetection: ChangeDetectionStrategy.OnPush,
49✔
72
    standalone: true,
49✔
73
    imports: [
49✔
74
        NgClass,
49✔
75
        NgTemplateOutlet,
49✔
76
        NgIf,
49✔
77
        ThyNavItemDirective,
49✔
78
        ThyIconComponent,
79
        ThyNavInkBarDirective,
80
        ThyDropdownMenuComponent,
81
        NgFor,
82
        ThyDropdownMenuItemDirective,
83
        ThyDropdownMenuItemActiveDirective,
49✔
84
        BypassSecurityTrustHtmlPipe
49✔
85
    ]
49✔
86
})
49✔
87
export class ThyNavComponent
49✔
88
    extends UnsubscribeMixin
49✔
89
    implements OnInit, AfterViewInit, AfterContentInit, AfterContentChecked, OnChanges, OnDestroy
49✔
90
{
91
    private type: ThyNavType = 'pulled';
92
    private size: ThyNavSize = 'md';
49✔
93
    public initialized = false;
41✔
94

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

8✔
103
    public hiddenItems: ThyNavItemDirective[] = [];
104

105
    public moreActive: boolean;
49✔
106

138!
107
    public showMore = true;
108

10✔
109
    private moreBtnOffset: { height: number; width: number } = { height: 0, width: 0 };
2✔
110

2✔
111
    private hostRenderer = useHostRenderer();
2✔
112

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

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

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

149
    /**
187✔
150
     * 是否垂直排列
187✔
151
     * @default false
187✔
152
     */
153
    @HostBinding('class.thy-nav--vertical')
154
    @Input()
155
    @InputBoolean()
156
    thyVertical: boolean;
137✔
157

42✔
158
    /**
159
     * 是否是填充模式
137✔
160
     */
161
    @HostBinding('class.thy-nav--fill')
162
    @Input()
10✔
163
    @InputBoolean()
10✔
164
    thyFill: boolean = false;
10✔
165

1✔
166
    /**
1✔
167
     * 是否响应式,自动计算宽度存放 thyNavItem,并添加更多弹框
1✔
168
     * @default false
169
     */
9✔
170
    @Input()
9✔
171
    @InputBoolean()
9!
172
    thyResponsive: boolean;
7✔
173

174
    /**
9!
175
     * 更多操作的菜单点击内部是否可关闭
9!
176
     */
21✔
177
    @Input()
178
    @InputBoolean()
9✔
179
    thyInsideClosable = true;
9✔
180

181
    /**
182
     * 右侧额外区域模板
8✔
183
     * @type TemplateRef
8✔
184
     */
8✔
185
    @Input() thyExtra: TemplateRef<unknown>;
8✔
186

21✔
187
    /**
21✔
188
     * @private
8✔
189
     */
8✔
190
    @ContentChildren(ThyNavItemDirective, { descendants: true }) links: QueryList<ThyNavItemDirective>;
1✔
191

192
    /**
193
     * @private
7✔
194
     */
195
    @ContentChildren(RouterLinkActive, { descendants: true }) routers: QueryList<RouterLinkActive>;
8✔
196

197
    /**
198
     * 响应式模式下更多操作模板
13✔
199
     * @type TemplateRef
13✔
200
     */
201
    @ContentChild('more') moreOperation: TemplateRef<unknown>;
202

8✔
203
    /**
204
     * 响应式模式下更多弹框模板
205
     * @type TemplateRef
1✔
206
     */
1✔
207
    @ContentChild('morePopover') morePopover: TemplateRef<unknown>;
1✔
208

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

216
    @ViewChild('moreOperationContainer') defaultMoreOperation: ElementRef<HTMLAnchorElement>;
1✔
217

218
    @ViewChild(ThyNavInkBarDirective, { static: true }) inkBar!: ThyNavInkBarDirective;
1✔
219

220
    get showInkBar(): boolean {
221
        const showTypes: ThyNavType[] = ['pulled', 'tabs'];
2✔
222
        return showTypes.includes(this.type);
2✔
223
    }
224

225
    private updateClasses() {
1✔
226
        let classNames: string[] = [];
227
        if (navTypeClassesMap[this.type]) {
228
            classNames = [...navTypeClassesMap[this.type]];
10✔
229
        }
10!
230
        if (navSizeClassesMap[this.size]) {
10!
231
            classNames.push(navSizeClassesMap[this.size]);
12✔
232
        }
12✔
233
        this.hostRenderer.updateClass(classNames);
234
    }
235

236
    private curActiveIndex: number;
2✔
237

238
    private prevActiveIndex: number = NaN;
239

240
    constructor(
241
        private elementRef: ElementRef,
242
        private ngZone: NgZone,
243
        private changeDetectorRef: ChangeDetectorRef,
244
        private popover: ThyPopover
245
    ) {
246
        super();
247
    }
1✔
248

249
    ngOnInit() {
250
        if (!this.thyResponsive) {
135✔
251
            this.initialized = true;
14✔
252
        }
14✔
253

254
        this.updateClasses();
121✔
255
    }
121✔
256

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

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

296
    ngAfterContentChecked() {
297
        this.calculateMoreIsActive();
298

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

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

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

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

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

344
        const endIndex = this.thyVertical ? this.getShowItemsEndIndexWhenVertical(tabs) : this.getShowItemsEndIndexWhenHorizontal(tabs);
345

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

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

356
        this.showMore = this.hiddenItems.length > 0;
357
        this.initialized = true;
358
    }
359

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

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

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

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

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

426
    navItemClick(item: ThyNavItemDirective) {
427
        item.elementRef.nativeElement.click();
428
    }
429

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

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

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

451
        if (thyType?.currentValue !== thyType?.previousValue || thyVertical?.currentValue !== thyVertical?.previousValue) {
452
            this.alignInkBarToSelectedTab();
453
        }
454
    }
455

456
    ngOnDestroy() {
457
        this.ngUnsubscribe$.next();
458
        this.ngUnsubscribe$.complete();
459
    }
460
}
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

© 2026 Coveralls, Inc