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

atinc / ngx-tethys / 1a366fff-0cd2-43aa-abbe-535f24a2de30

21 Nov 2024 11:29AM UTC coverage: 90.351% (+0.008%) from 90.343%
1a366fff-0cd2-43aa-abbe-535f24a2de30

push

circleci

web-flow
feat(i18n): support 简体中文, 繁體中文, English, 日本語, Deutsch #TINFR-1022 (#3271)

* feat: support 简体中文, 繁體中文, English, 日本語, Deutsch #TINFR-1022
* feat(nav): support i18n #TINFR-1042

5543 of 6783 branches covered (81.72%)

Branch coverage included in aggregate %.

13 of 14 new or added lines in 4 files covered. (92.86%)

3 existing lines in 2 files now uncovered.

13240 of 14006 relevant lines covered (94.53%)

993.87 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,
18
    inject,
1✔
19
    Input,
20
    NgZone,
21
    OnChanges,
22
    OnInit,
23
    QueryList,
24
    Signal,
25
    SimpleChanges,
26
    TemplateRef,
27
    ViewChild
28
} from '@angular/core';
29

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

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

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

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

49✔
62
const tabItemRight = 20;
49✔
63

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

75✔
95
    private readonly destroyRef = inject(DestroyRef);
96

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

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

49✔
109
    public hiddenItems: ThyNavItemDirective[] = [];
8✔
110

8✔
111
    public moreActive: boolean;
21✔
112

8✔
113
    public showMore = true;
114

115
    private moreBtnOffset: { height: number; width: number } = { height: 0, width: 0 };
49✔
116

137!
117
    private hostRenderer = useHostRenderer();
118

10✔
119
    private innerLinks: QueryList<ThyNavItemDirective>;
2✔
120

2✔
121
    locale: Signal<ThyNavLocale> = injectLocale('nav');
2✔
122

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

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

149
    /**
150
     * 水平排列
151
     * @type '' | 'start' | 'center' | 'end'
152
     * @default false
153
     */
186!
154
    @Input()
155
    set thyHorizontal(horizontal: ThyNavHorizontal) {
156
        this.horizontal = (horizontal as string) === 'right' ? 'end' : horizontal;
186✔
UNCOV
157
    }
×
158

159
    /**
186✔
160
     * 是否垂直排列
186✔
161
     * @default false
186✔
162
     */
163
    @HostBinding('class.thy-nav--vertical')
164
    @Input({ transform: coerceBooleanProperty })
165
    thyVertical: boolean;
166

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

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

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

21✔
187
    /**
188
     * 右侧额外区域模板
9✔
189
     * @type TemplateRef
9✔
190
     */
191
    @Input() thyExtra: TemplateRef<unknown>;
192

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

205
    /**
8✔
206
     * @private
207
     */
208
    @ContentChildren(RouterLinkActive, { descendants: true }) routers: QueryList<RouterLinkActive>;
13✔
209

13✔
210
    /**
211
     * 响应式模式下更多操作模板
212
     * @type TemplateRef
8✔
213
     */
214
    @ContentChild('more') moreOperation: TemplateRef<unknown>;
215

1✔
216
    /**
1✔
217
     * 响应式模式下更多弹框模板
1✔
218
     * @type TemplateRef
1✔
219
     */
3✔
220
    @ContentChild('morePopover') morePopover: TemplateRef<unknown>;
3✔
221

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

1✔
229
    @ViewChild('moreOperationContainer') defaultMoreOperation: ElementRef<HTMLAnchorElement>;
230

231
    @ViewChild(ThyNavInkBarDirective, { static: true }) inkBar!: ThyNavInkBarDirective;
2✔
232

2✔
233
    get showInkBar(): boolean {
234
        const showTypes: ThyNavType[] = ['pulled', 'tabs'];
235
        return showTypes.includes(this.type);
1✔
236
    }
237

238
    private updateClasses() {
10✔
239
        let classNames: string[] = [];
10!
240
        if (navTypeClassesMap[this.type]) {
10!
241
            classNames = [...navTypeClassesMap[this.type]];
12✔
242
        }
12✔
243
        if (navSizeClassesMap[this.size]) {
244
            classNames.push(navSizeClassesMap[this.size]);
245
        }
246
        this.hostRenderer.updateClass(classNames);
2✔
247
    }
248

249
    private curActiveIndex: number;
250

251
    private prevActiveIndex: number = NaN;
252

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

1✔
258
        this.updateClasses();
259
    }
260

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

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

1✔
300
    ngAfterContentChecked() {
301
        this.calculateMoreIsActive();
302

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

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

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

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

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

348
        const endIndex = this.thyVertical ? this.getShowItemsEndIndexWhenVertical(tabs) : this.getShowItemsEndIndexWhenHorizontal(tabs);
349

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

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

360
        this.showMore = this.hiddenItems.length > 0;
361
        this.initialized = true;
362
    }
363

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

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

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

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

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

430
    navItemClick(item: ThyNavItemDirective) {
431
        item.elementRef.nativeElement.click();
432
    }
433

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

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

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

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

© 2026 Coveralls, Inc