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

atinc / ngx-tethys / c0ef8457-a839-451f-8b72-80fd73106231

02 Apr 2024 02:27PM UTC coverage: 90.524% (-0.06%) from 90.585%
c0ef8457-a839-451f-8b72-80fd73106231

Pull #3062

circleci

minlovehua
refactor(all): use the transform attribute of @Input() instead of @InputBoolean() and @InputNumber()
Pull Request #3062: refactor(all): use the transform attribute of @input() instead of @InputBoolean() and @InputNumber()

4987 of 6108 branches covered (81.65%)

Branch coverage included in aggregate %.

217 of 223 new or added lines in 82 files covered. (97.31%)

202 existing lines in 53 files now uncovered.

12246 of 12929 relevant lines covered (94.72%)

1055.59 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 { Constructor, 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
    booleanAttribute,
11
    ChangeDetectionStrategy,
12
    ChangeDetectorRef,
13
    Component,
14
    ContentChild,
15
    ContentChildren,
16
    ElementRef,
1✔
17
    HostBinding,
1✔
18
    Input,
19
    NgZone,
20
    OnChanges,
21
    OnDestroy,
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, NgIf, NgFor } from '@angular/common';
36

37
const _MixinBase: Constructor<ThyUnsubscribe> & typeof MixinBase = mixinUnsubscribe(MixinBase);
38

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

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

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

77✔
61
const tabItemRight = 20;
77!
62

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

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

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

105
    public moreActive: boolean;
106

50✔
107
    public showMore = true;
140!
108

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

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

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

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

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

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

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

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

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

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

183
    /**
8✔
184
     * @private
8✔
185
     */
8✔
186
    @ContentChildren(ThyNavItemDirective, { descendants: true }) links: QueryList<ThyNavItemDirective>;
8✔
187

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

193
    /**
194
     * 响应式模式下更多操作模板
7✔
195
     * @type TemplateRef
196
     */
8✔
197
    @ContentChild('more') moreOperation: TemplateRef<unknown>;
198

199
    /**
13✔
200
     * 响应式模式下更多弹框模板
13✔
201
     * @type TemplateRef
202
     */
203
    @ContentChild('morePopover') morePopover: TemplateRef<unknown>;
8✔
204

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

3✔
212
    @ViewChild('moreOperationContainer') defaultMoreOperation: ElementRef<HTMLAnchorElement>;
1✔
213

1!
UNCOV
214
    @ViewChild(ThyNavInkBarDirective, { static: true }) inkBar!: ThyNavInkBarDirective;
×
215

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

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

10!
232
    private curActiveIndex: number;
12✔
233

12✔
234
    private prevActiveIndex: number = NaN;
235

236
    constructor(
237
        private elementRef: ElementRef,
2✔
238
        private ngZone: NgZone,
239
        private changeDetectorRef: ChangeDetectorRef,
240
        private popover: ThyPopover
241
    ) {
242
        super();
243
    }
244

245
    ngOnInit() {
246
        if (!this.thyResponsive) {
247
            this.initialized = true;
248
        }
1✔
249

250
        this.updateClasses();
251
    }
137✔
252

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

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

292
    ngAfterContentChecked() {
293
        this.calculateMoreIsActive();
294

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

303
    private setMoreBtnOffset() {
304
        this.moreBtnOffset = {
305
            height: this.defaultMoreOperation?.nativeElement?.offsetHeight,
306
            width: this.defaultMoreOperation?.nativeElement?.offsetWidth
307
        };
308
    }
309

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

324
    private calculateMoreIsActive() {
325
        this.moreActive = this.hiddenItems.some(item => {
326
            return item.linkIsActive();
327
        });
328
        this.changeDetectorRef.detectChanges();
329
    }
330

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

340
        const endIndex = this.thyVertical ? this.getShowItemsEndIndexWhenVertical(tabs) : this.getShowItemsEndIndexWhenHorizontal(tabs);
341

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

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

352
        this.showMore = this.hiddenItems.length > 0;
353
        this.initialized = true;
354
    }
355

356
    private getShowItemsEndIndexWhenHorizontal(tabs: ThyNavItemDirective[]) {
357
        const tabsLength = tabs.length;
358
        let endIndex = tabsLength;
359
        let totalWidth = 0;
360

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

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

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

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

422
    navItemClick(item: ThyNavItemDirective) {
423
        item.elementRef.nativeElement.click();
424
    }
425

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

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

444
    ngOnChanges(changes: SimpleChanges): void {
445
        const { thyVertical, thyType } = changes;
446

447
        if (thyType?.currentValue !== thyType?.previousValue || thyVertical?.currentValue !== thyVertical?.previousValue) {
448
            this.alignInkBarToSelectedTab();
449
        }
450
    }
451

452
    ngOnDestroy() {
453
        this.ngUnsubscribe$.next();
454
        this.ngUnsubscribe$.complete();
455
    }
456
}
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