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

atinc / ngx-tethys / 3b40a702-4b4d-4ddb-81a7-a96baae6d682

08 Nov 2024 05:40AM UTC coverage: 90.395% (-0.04%) from 90.431%
3b40a702-4b4d-4ddb-81a7-a96baae6d682

push

circleci

why520crazy
Merge branch 'master' into feat-theme

5503 of 6730 branches covered (81.77%)

Branch coverage included in aggregate %.

424 of 431 new or added lines in 171 files covered. (98.38%)

344 existing lines in 81 files now uncovered.

13150 of 13905 relevant lines covered (94.57%)

999.86 hits per line

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

94.29
/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

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

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

49✔
60
const tabItemRight = 20;
49✔
61

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

75✔
93
    private readonly destroyRef = inject(DestroyRef);
94

75✔
95
    private type: ThyNavType = 'pulled';
47✔
96
    private size: ThyNavSize = 'md';
97
    public initialized = false;
75✔
98

99
    public horizontal: ThyNavHorizontal;
100
    public wrapperOffset: { height: number; width: number; left: number; top: number } = {
49✔
101
        height: 0,
41✔
102
        width: 0,
103
        left: 0,
49✔
104
        top: 0
105
    };
106

49✔
107
    public hiddenItems: ThyNavItemDirective[] = [];
8✔
108

8✔
109
    public moreActive: boolean;
21✔
110

8✔
111
    public showMore = true;
112

113
    private moreBtnOffset: { height: number; width: number } = { height: 0, width: 0 };
49✔
114

137!
115
    private hostRenderer = useHostRenderer();
116

10✔
117
    private innerLinks: QueryList<ThyNavItemDirective>;
2✔
118

2✔
119
    /**
2✔
120
     * 导航类型
121
     * @type pulled | tabs | pills | lite | primary | secondary | thirdly | secondary-divider
122
     * @default pulled
123
     */
10✔
124
    @Input()
125
    set thyType(type: ThyNavType) {
126
        this.type = type || 'pulled';
127
        if (this.initialized) {
128
            this.updateClasses();
49✔
129
        }
8✔
130
    }
8✔
131

132
    /**
133
     * 导航大小
134
     * @type lg | md | sm
135
     * @default md
136✔
136
     */
211✔
137
    @Input()
136✔
138
    set thySize(size: ThyNavSize) {
25✔
139
        this.size = size;
140
        if (this.initialized) {
111✔
141
            this.updateClasses();
53✔
142
        }
143
    }
144

145
    /**
8✔
146
     * 水平排列
147
     * @type '' | 'start' | 'center' | 'end'
148
     * @default false
149
     */
150
    @Input()
151
    set thyHorizontal(horizontal: ThyNavHorizontal) {
186!
152
        this.horizontal = (horizontal as string) === 'right' ? 'end' : horizontal;
153
    }
154

186✔
UNCOV
155
    /**
×
156
     * 是否垂直排列
157
     * @default false
186✔
158
     */
186✔
159
    @HostBinding('class.thy-nav--vertical')
186✔
160
    @Input({ transform: coerceBooleanProperty })
161
    thyVertical: boolean;
162

163
    /**
164
     * 是否是填充模式
138✔
165
     */
42✔
166
    @HostBinding('class.thy-nav--fill')
167
    @Input({ transform: coerceBooleanProperty })
138✔
168
    thyFill: boolean = false;
169

170
    /**
10✔
171
     * 是否响应式,自动计算宽度存放 thyNavItem,并添加更多弹框
10✔
172
     * @default false
10✔
173
     */
1✔
174
    @Input({ transform: coerceBooleanProperty })
1✔
175
    thyResponsive: boolean;
1✔
176

177
    /**
9✔
178
     * 更多操作的菜单点击内部是否可关闭
9✔
179
     */
9!
180
    @Input({ transform: coerceBooleanProperty })
7✔
181
    thyInsideClosable = true;
182

9!
183
    /**
9!
184
     * 右侧额外区域模板
21✔
185
     * @type TemplateRef
186
     */
9✔
187
    @Input() thyExtra: TemplateRef<unknown>;
9✔
188

189
    /**
190
     * @private
8✔
191
     */
8✔
192
    @ContentChildren(ThyNavItemDirective, { descendants: true })
8✔
193
    set links(value) {
8✔
194
        this.innerLinks = value;
21✔
195
        this.prevActiveIndex = NaN;
21✔
196
    }
8✔
197
    get links(): QueryList<ThyNavItemDirective> {
8✔
198
        return this.innerLinks;
1✔
199
    }
200

201
    /**
7✔
202
     * @private
203
     */
8✔
204
    @ContentChildren(RouterLinkActive, { descendants: true }) routers: QueryList<RouterLinkActive>;
205

206
    /**
13✔
207
     * 响应式模式下更多操作模板
13✔
208
     * @type TemplateRef
209
     */
210
    @ContentChild('more') moreOperation: TemplateRef<unknown>;
8✔
211

212
    /**
213
     * 响应式模式下更多弹框模板
1✔
214
     * @type TemplateRef
1✔
215
     */
1✔
216
    @ContentChild('morePopover') morePopover: TemplateRef<unknown>;
1✔
217

3✔
218
    /**
3✔
219
     * 右侧额外区域模板,支持 thyExtra 传参和 <ng-template #extra></ng-template> 模板
1✔
220
     * @name extra
1!
UNCOV
221
     * @type TemplateRef
×
222
     */
223
    @ContentChild('extra') extra: TemplateRef<unknown>;
224

1✔
225
    @ViewChild('moreOperationContainer') defaultMoreOperation: ElementRef<HTMLAnchorElement>;
226

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

229
    get showInkBar(): boolean {
2✔
230
        const showTypes: ThyNavType[] = ['pulled', 'tabs'];
2✔
231
        return showTypes.includes(this.type);
232
    }
233

1✔
234
    private updateClasses() {
235
        let classNames: string[] = [];
236
        if (navTypeClassesMap[this.type]) {
10✔
237
            classNames = [...navTypeClassesMap[this.type]];
10!
238
        }
10!
239
        if (navSizeClassesMap[this.size]) {
12✔
240
            classNames.push(navSizeClassesMap[this.size]);
12✔
241
        }
242
        this.hostRenderer.updateClass(classNames);
243
    }
244

2✔
245
    private curActiveIndex: number;
246

247
    private prevActiveIndex: number = NaN;
248

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

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

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

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

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

314
    createResizeObserver(element: HTMLElement) {
315
        return typeof ResizeObserver === 'undefined'
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
}
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