• 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.35
/src/layout/sidebar.component.ts
1
import { NgClass, NgIf, NgStyle, NgTemplateOutlet } from '@angular/common';
2
import {
3
    Component,
4
    Directive,
5
    ElementRef,
6
    EventEmitter,
7
    Host,
8
    HostBinding,
9
    HostListener,
10
    Input,
1✔
11
    OnDestroy,
1✔
12
    OnInit,
1✔
13
    Optional,
14
    Output,
15
    TemplateRef,
16
    booleanAttribute,
17
    inject,
18
    numberAttribute
1✔
19
} from '@angular/core';
20
import { ThyHotkeyDispatcher } from '@tethys/cdk/hotkey';
45✔
21
import { isMacPlatform } from '@tethys/cdk/is';
2✔
22
import { ThyIcon } from 'ngx-tethys/icon';
23
import { ThyResizableDirective, ThyResizeEvent, ThyResizeHandle } from 'ngx-tethys/resizable';
45✔
24
import { ThyTooltipDirective } from 'ngx-tethys/tooltip';
25
import { Subscription } from 'rxjs';
26
import { ThyLayoutDirective } from './layout.component';
42✔
27

28
const LG_WIDTH = 300;
29
const SIDEBAR_DEFAULT_WIDTH = 240;
102✔
30
const SIDEBAR_COLLAPSED_WIDTH = 20;
31

32
export type ThySidebarTheme = 'white' | 'light' | 'dark';
29✔
33

34
export type ThySidebarDirection = 'left' | 'right';
35

29✔
36
/**
37
 * 侧边栏布局指令
38
 * @name thySidebar
42✔
39
 * @order 20
42✔
40
 */
42✔
41
@Directive({
42✔
42
    selector: '[thySidebar]',
43
    host: {
44
        class: 'thy-layout-sidebar',
42!
45
        '[class.thy-layout-sidebar-right]': 'thyDirection === "right"',
42✔
46
        '[class.thy-layout-sidebar--clear-border-right]': 'thyDirection === "left" && !isDivided',
47
        '[class.thy-layout-sidebar--clear-border-left]': 'thyDirection === "right" && !isDivided',
42✔
48
        '[class.sidebar-theme-light]': 'thyTheme === "light"',
14✔
49
        '[class.sidebar-theme-dark]': 'thyTheme === "dark"',
50
        '[class.thy-layout-sidebar-isolated]': 'sidebarIsolated'
51
    },
1✔
52
    standalone: true
53
})
54
export class ThySidebarDirective implements OnInit {
1✔
55
    sidebarIsolated = false;
56

57
    isDivided = true;
58

59
    @HostBinding('style.width.px') thyLayoutSidebarWidth: number;
60

61
    /**
62
     * sidebar 位置,默认在左侧
63
     */
64
    @Input() thyDirection: ThySidebarDirection = 'left';
65

1✔
66
    /**
67
     * 主题
68
     * @type white | light | dark
69
     * @default white
70
     */
71
    @Input() thyTheme: ThySidebarTheme;
72

73
    /**
74
     * 宽度,默认是 240px,传入 `lg` 大小时宽度是300px
75
     * @default 240px
76
     */
77
    @Input('thyWidth')
78
    set thyWidth(value: string | number) {
79
        if (value === 'lg') {
80
            value = LG_WIDTH;
81
        }
82
        this.thyLayoutSidebarWidth = (value as number) || SIDEBAR_DEFAULT_WIDTH;
83
    }
84

85
    /**
86
     * 是否和右侧 /左侧隔离,当为 true 时距右侧 /左侧会有 margin,同时边框会去掉
87
     * @default false
1✔
88
     */
89
    @Input({ transform: booleanAttribute })
134✔
90
    set thyIsolated(value: boolean) {
6✔
91
        this.sidebarIsolated = value;
92
    }
93

128✔
94
    /**
95
     * sidebar 是否有分割线。当`thyDirection`值为`left`时,控制右侧是否有分割线;当`thyDirection`值为`right`时,控制左侧是否有分割线。
96
     * @default true
97
     */
2✔
98
    @Input({ transform: booleanAttribute })
99
    set thyDivided(value: boolean) {
100
        this.isDivided = value;
2✔
101
    }
102

103
    /**
37✔
104
     * 右侧是否有边框,已废弃,请使用 thyDivided
37✔
105
     * @deprecated please use thyDivided
9✔
106
     * @default true
107
     */
108
    @Input({ transform: booleanAttribute })
28✔
109
    set thyHasBorderRight(value: boolean) {
110
        this.thyDivided = value;
111
    }
112

415✔
113
    /**
114
     * 左侧是否有边框,已废弃,请使用 thyDivided
115
     * @deprecated please use thyDivided
39✔
116
     * @default true
117
     */
118
    @Input({ transform: booleanAttribute })
252✔
119
    set thyHasBorderLeft(value: boolean) {
120
        this.thyDivided = value;
121
    }
134✔
122

123
    constructor(@Optional() @Host() private thyLayoutDirective: ThyLayoutDirective) {}
124

134✔
125
    ngOnInit() {
126
        if (this.thyLayoutDirective) {
127
            this.thyLayoutDirective.hasSidebar = true;
24✔
128
        }
129
        if (this.thyDirection === 'right') {
130
            this.thyLayoutDirective.isSidebarRight = true;
30✔
131
        }
30✔
132
    }
30✔
133
}
30✔
134

30✔
135
/**
30✔
136
 * 侧边栏布局组件
30✔
137
 * @name thy-sidebar
30✔
138
 * @order 21
30✔
139
 */
30✔
140
@Component({
141
    selector: 'thy-sidebar',
142
    preserveWhitespaces: false,
30✔
143
    template: `
144
        <ng-content></ng-content>
145
        <div
9✔
146
            thyResizable
2✔
147
            class="sidebar-drag"
148
            *ngIf="thyDraggable"
149
            thyBounds="window"
150
            [thyMaxWidth]="thyDragMaxWidth"
37✔
151
            [thyMinWidth]="dragMinWidth"
37!
152
            (thyResize)="resizeHandler($event)"
153
            (thyResizeStart)="resizeStart()"
154
            (thyResizeEnd)="resizeEnd()"
7✔
155
            [style.display]="!isResizable ? 'contents' : null">
1✔
156
            <thy-resize-handle
157
                *ngIf="!thyCollapsed"
6!
UNCOV
158
                [thyDirection]="sidebarDirective.thyDirection === 'right' ? 'left' : 'right'"
×
159
                class="sidebar-resize-handle"
160
                thyLine="true"
6✔
161
                (mouseenter)="toggleResizable($event, 'enter')"
2✔
162
                (mouseleave)="toggleResizable($event, 'leave')"
2✔
163
                (dblclick)="restoreToDefaultWidth()">
2✔
164
            </thy-resize-handle>
2✔
165
        </div>
2✔
166
        <div *ngIf="thyCollapsible" class="sidebar-collapse-line"></div>
2✔
167
        <div
168
            *ngIf="thyCollapsible && thyTrigger !== null"
4✔
169
            class="sidebar-collapse"
4✔
170
            [ngClass]="{ 'collapse-visible': collapseVisible, 'collapse-hidden': collapseHidden }"
171
            (click)="toggleCollapse($event)"
172
            [thyTooltip]="!thyTrigger && collapseTip">
5✔
173
            <ng-template [ngTemplateOutlet]="thyTrigger || defaultTrigger"></ng-template>
5✔
174
            <ng-template #defaultTrigger>
5✔
175
                <thy-icon class="sidebar-collapse-icon" [thyIconName]="this.thyCollapsed ? 'indent' : 'outdent'"></thy-icon>
176
            </ng-template>
177
        </div>
4✔
178
    `,
4✔
179
    hostDirectives: [
180
        {
181
            directive: ThySidebarDirective,
4✔
182
            inputs: ['thyTheme', 'thyDirection', 'thyWidth', 'thyIsolated', 'thyDivided', 'thyHasBorderLeft', 'thyHasBorderRight']
183
        }
184
    ],
5✔
185
    standalone: true,
5✔
186
    imports: [NgTemplateOutlet, NgIf, ThyResizeHandle, ThyResizableDirective, ThyIcon, ThyTooltipDirective, NgClass, NgStyle]
5✔
187
})
188
export class ThySidebar implements OnInit, OnDestroy {
189
    sidebarDirective = inject(ThySidebarDirective);
2✔
190

191
    @HostBinding('style.width.px') get sidebarWidth() {
192
        if (this.thyCollapsible && this.thyCollapsed) {
1!
UNCOV
193
            return this.thyCollapsedWidth;
×
194
        } else {
195
            return this.sidebarDirective.thyLayoutSidebarWidth;
1!
196
        }
1✔
197
    }
198

199
    @HostListener('mouseenter', ['$event'])
30✔
200
    mouseenter($event: MouseEvent) {
201
        this.resizeHandleHover($event, 'enter');
1✔
202
    }
203

204
    @HostListener('mouseleave', ['$event'])
205
    mouseleave($event: MouseEvent) {
1✔
206
        this.resizeHandleHover($event, 'leave');
207
    }
208

209
    /**
210
     * 宽度是否可以拖拽
211
     * @default false
212
     */
213
    @Input({ transform: booleanAttribute }) thyDraggable: boolean = false;
214

215
    /**
216
     * 拖拽的最大宽度
217
     */
218
    @Input({ transform: numberAttribute }) thyDragMaxWidth: number;
219

220
    /**
221
     * 拖拽的最小宽度
222
     */
223
    @Input({ transform: numberAttribute }) thyDragMinWidth: number;
1✔
224

225
    /**
226
     * 展示收起的触发器自定义模板,默认显示展开收起的圆形图标,设置为 null 表示不展示触发元素,手动控制展开收起状态
227
     * @type null | undefined | TemplateRef<any>
228
     * @default undefined
229
     */
230
    @Input() thyTrigger: null | undefined | TemplateRef<any> = undefined;
231

232
    /**
233
     * 收起状态改变后的事件
234
     */
235
    @Output()
236
    thyCollapsedChange = new EventEmitter<boolean>();
237

238
    /**
239
     * 拖拽宽度的修改事件
240
     */
241
    @Output()
242
    thyDragWidthChange = new EventEmitter<number>();
243

244
    /**
245
     * 开启收起/展开功能
246
     * @default false
247
     */
248
    @Input({ transform: booleanAttribute }) set thyCollapsible(collapsible: boolean) {
249
        this.collapsible = collapsible;
250
        if (this.collapsible) {
251
            this.subscribeHotkeyEvent();
252
        } else {
253
            this.hotkeySubscription?.unsubscribe();
254
        }
255
    }
256

257
    get thyCollapsible() {
258
        return this.collapsible;
259
    }
260

261
    /**
262
     * 是否是收起
263
     * @default false
264
     */
265
    @Input({ transform: booleanAttribute }) set thyCollapsed(value: boolean) {
266
        this.isCollapsed = value;
267
    }
268

269
    get thyCollapsed() {
270
        return this.isCollapsed;
271
    }
272

273
    /**
274
     * 收起后的宽度
275
     */
276
    @Input({ transform: numberAttribute }) thyCollapsedWidth = SIDEBAR_COLLAPSED_WIDTH;
277

278
    /**
279
     * 默认宽度,双击后可恢复到此宽度,默认是 240px,传入 lg 大小时宽度是300px
280
     */
281
    @Input() thyDefaultWidth: string | number;
282

283
    @HostBinding('class.sidebar-collapse-show')
284
    get collapseVisibility() {
285
        return this.thyCollapsed;
286
    }
287

288
    @HostBinding('class.remove-transition')
289
    get removeTransition() {
290
        return this.isRemoveTransition;
291
    }
292

293
    collapseTip: string;
294

295
    collapsible: boolean;
296

297
    isCollapsed = false;
298

299
    originWidth: number = SIDEBAR_DEFAULT_WIDTH;
300

301
    collapseVisible: boolean;
302

303
    collapseHidden: boolean;
304

305
    isRemoveTransition: boolean;
306

307
    isResizable: boolean;
308

309
    get dragMinWidth() {
310
        return this.thyDragMinWidth || this.thyCollapsedWidth;
311
    }
312

313
    private hotkeySubscription: Subscription;
314

315
    constructor(public elementRef: ElementRef, private hotkeyDispatcher: ThyHotkeyDispatcher) {}
316

317
    ngOnInit() {
318
        this.updateCollapseTip();
319
    }
320

321
    private subscribeHotkeyEvent() {
322
        this.hotkeySubscription = this.hotkeyDispatcher.keydown(['Control+/', 'Meta+/']).subscribe(() => {
323
            this.toggleCollapse();
324
        });
325
    }
326

327
    private updateCollapseTip() {
328
        this.collapseTip = this.thyCollapsed ? '展开' : '收起';
329
        this.collapseTip = this.collapseTip + (isMacPlatform() ? `(⌘ + /)` : `(Ctrl + /)`);
330
    }
331

332
    resizeHandler({ width }: ThyResizeEvent) {
333
        if (width === this.sidebarDirective.thyLayoutSidebarWidth) {
334
            return;
335
        }
336
        if (this.thyCollapsible && width < this.thyCollapsedWidth) {
337
            return;
338
        }
339
        if (this.thyCollapsible && width === this.thyCollapsedWidth) {
340
            this.thyCollapsed = true;
341
            setTimeout(() => this.updateCollapseTip(), 200);
342
            this.thyCollapsedChange.emit(this.isCollapsed);
343
            this.sidebarDirective.thyLayoutSidebarWidth = this.originWidth;
344
            this.collapseVisible = false;
345
            return;
346
        }
347
        this.sidebarDirective.thyLayoutSidebarWidth = width;
348
        this.thyDragWidthChange.emit(width);
349
    }
350

351
    resizeStart() {
352
        this.originWidth = this.sidebarDirective.thyLayoutSidebarWidth;
353
        this.collapseHidden = true;
354
        this.isRemoveTransition = true;
355
    }
356

357
    resizeEnd() {
358
        this.collapseHidden = false;
359
        this.isRemoveTransition = false;
360
    }
361

362
    resizeHandleHover(event: MouseEvent, type: 'enter' | 'leave') {
363
        this.collapseVisible = type === 'enter' && !this.thyCollapsed ? true : false;
364
    }
365

366
    toggleCollapse(event?: MouseEvent) {
367
        this.thyCollapsed = !this.thyCollapsed;
368
        setTimeout(() => this.updateCollapseTip(), 200);
369
        this.thyCollapsedChange.emit(this.isCollapsed);
370
    }
371

372
    public toggleResizable(event: MouseEvent, type: 'enter' | 'leave') {
373
        this.isResizable = type === 'enter' ? true : false;
374
    }
375

376
    restoreToDefaultWidth() {
377
        if (this.thyDefaultWidth === 'lg') {
378
            this.thyDefaultWidth = LG_WIDTH;
379
        }
380
        this.sidebarDirective.thyLayoutSidebarWidth = (this.thyDefaultWidth as number) || SIDEBAR_DEFAULT_WIDTH;
381
        this.thyDragWidthChange.emit(this.sidebarDirective.thyLayoutSidebarWidth);
382
    }
383

384
    ngOnDestroy(): void {
385
        this.hotkeySubscription?.unsubscribe();
386
    }
387
}
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