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

atinc / ngx-tethys / 5ba5b9d7-3ca9-4ff2-bbba-bde58c0f849f

22 Feb 2024 09:41AM UTC coverage: 90.604%. Remained the same
5ba5b9d7-3ca9-4ff2-bbba-bde58c0f849f

Pull #3027

circleci

minlovehua
feat(schematics): provide schematics for removing the suffix of standalone components #INFR-11662
Pull Request #3027: refactor: remove the component suffix for standalone components and provide schematics #INFR-10654

5425 of 6642 branches covered (81.68%)

Branch coverage included in aggregate %.

323 of 333 new or added lines in 193 files covered. (97.0%)

36 existing lines in 8 files now uncovered.

13504 of 14250 relevant lines covered (94.76%)

981.28 hits per line

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

94.42
/src/nav/nav.component.ts
1
import { Constructor, InputBoolean, MixinBase, mixinUnsubscribe, ThyUnsubscribe } 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,
1✔
18
    NgZone,
19
    OnChanges,
20
    OnDestroy,
21
    OnInit,
22
    QueryList,
23
    SimpleChanges,
24
    TemplateRef,
25
    ViewChild
26
} from '@angular/core';
27

28
import { RouterLinkActive } from '@angular/router';
1✔
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';
33
import { ThyIcon } from 'ngx-tethys/icon';
1✔
34
import { NgClass, NgTemplateOutlet, NgIf, NgFor } from '@angular/common';
35

36
const _MixinBase: Constructor<ThyUnsubscribe> & typeof MixinBase = mixinUnsubscribe(MixinBase);
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

72✔
42
const navTypeClassesMap = {
72✔
43
    pulled: ['thy-nav-pulled'],
22✔
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',
278✔
57
    sm: 'thy-nav-sm'
278✔
58
};
59

60
const tabItemRight = 20;
77✔
61

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

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

8✔
102
    public hiddenItems: ThyNavItemDirective[] = [];
21✔
103

8✔
104
    public moreActive: boolean;
105

106
    public showMore = true;
50✔
107

140!
108
    private moreBtnOffset: { height: number; width: number } = { height: 0, width: 0 };
109

10✔
110
    private hostRenderer = useHostRenderer();
2✔
111

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

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

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

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

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

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

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

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

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

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

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

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

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

×
215
    @ViewChild('moreOperationContainer') defaultMoreOperation: ElementRef<HTMLAnchorElement>;
216

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

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

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

235
    private curActiveIndex: number;
236

237
    private prevActiveIndex: number = NaN;
2✔
238

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

248
    ngOnInit() {
1✔
249
        if (!this.thyResponsive) {
250
            this.initialized = true;
251
        }
137✔
252

14✔
253
        this.updateClasses();
14✔
254
    }
255

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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