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

atinc / ngx-tethys / 5fa9630c-19a1-4ee7-af3d-6a0c3535952a

08 Oct 2024 08:24AM UTC coverage: 90.438% (+0.007%) from 90.431%
5fa9630c-19a1-4ee7-af3d-6a0c3535952a

push

circleci

minlovehua
refactor: refactor all control-flow directives to new control-flow #TINFR-381

5511 of 6738 branches covered (81.79%)

Branch coverage included in aggregate %.

98 of 104 new or added lines in 58 files covered. (94.23%)

113 existing lines in 17 files now uncovered.

13253 of 14010 relevant lines covered (94.6%)

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

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

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

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

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

57
    isDivided = true;
58

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

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

66
    /**
1✔
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
88
     */
1✔
89
    @Input({ transform: coerceBooleanProperty })
90
    set thyIsolated(value: boolean) {
134✔
91
        this.sidebarIsolated = value;
6✔
92
    }
93

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

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

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

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

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

196
    @HostBinding('style.width.px') get sidebarWidth() {
1!
197
        if (this.thyCollapsible && this.thyCollapsed) {
1✔
198
            return this.thyCollapsedWidth;
199
        } else {
200
            return this.sidebarDirective.thyLayoutSidebarWidth;
30✔
201
        }
202
    }
1✔
203

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

209
    @HostListener('mouseleave', ['$event'])
210
    mouseleave($event: MouseEvent) {
211
        this.resizeHandleHover($event, 'leave');
212
    }
213

214
    /**
215
     * 宽度是否可以拖拽
216
     * @default false
217
     */
218
    @Input({ transform: coerceBooleanProperty }) thyDraggable: boolean = false;
219

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

1✔
225
    /**
226
     * 拖拽的最小宽度
227
     */
228
    @Input({ transform: numberAttribute }) thyDragMinWidth: number;
229

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

237
    /**
238
     * 收起状态改变后的事件
239
     */
240
    @Output()
241
    thyCollapsedChange = new EventEmitter<boolean>();
242

243
    /**
244
     * 拖拽宽度的修改事件
245
     */
246
    @Output()
247
    thyDragWidthChange = new EventEmitter<number>();
248

249
    /**
250
     * 开启收起/展开功能
251
     * @default false
252
     */
253
    @Input({ transform: coerceBooleanProperty }) set thyCollapsible(collapsible: boolean) {
254
        this.collapsible = collapsible;
255
        if (this.collapsible) {
256
            this.subscribeHotkeyEvent();
257
        } else {
258
            this.hotkeySubscription?.unsubscribe();
259
        }
260
    }
261

262
    get thyCollapsible() {
263
        return this.collapsible;
264
    }
265

266
    /**
267
     * 是否是收起
268
     * @default false
269
     */
270
    @Input({ transform: coerceBooleanProperty }) set thyCollapsed(value: boolean) {
271
        this.isCollapsed = value;
272
    }
273

274
    get thyCollapsed() {
275
        return this.isCollapsed;
276
    }
277

278
    /**
279
     * 收起后的宽度
280
     */
281
    @Input({ transform: numberAttribute }) thyCollapsedWidth = SIDEBAR_COLLAPSED_WIDTH;
282

283
    /**
284
     * 默认宽度,双击后可恢复到此宽度,默认是 240px,传入 lg 大小时宽度是300px
285
     */
286
    @Input() thyDefaultWidth: string | number;
287

288
    @HostBinding('class.sidebar-collapse-show')
289
    get collapseVisibility() {
290
        return this.thyCollapsed;
291
    }
292

293
    @HostBinding('class.remove-transition')
294
    get removeTransition() {
295
        return this.isRemoveTransition;
296
    }
297

298
    collapseTip: string;
299

300
    collapsible: boolean;
301

302
    isCollapsed = false;
303

304
    originWidth: number = SIDEBAR_DEFAULT_WIDTH;
305

306
    collapseVisible: boolean;
307

308
    collapseHidden: boolean;
309

310
    isRemoveTransition: boolean;
311

312
    isResizable: boolean;
313

314
    get dragMinWidth() {
315
        return this.thyDragMinWidth || this.thyCollapsedWidth;
316
    }
317

318
    private hotkeySubscription: Subscription;
319

320
    constructor(
321
        public elementRef: ElementRef,
322
        private hotkeyDispatcher: ThyHotkeyDispatcher
323
    ) {}
324

325
    ngOnInit() {
326
        this.updateCollapseTip();
327
    }
328

329
    private subscribeHotkeyEvent() {
330
        this.hotkeySubscription = this.hotkeyDispatcher.keydown(['Control+/', 'Meta+/']).subscribe(() => {
331
            this.toggleCollapse();
332
        });
333
    }
334

335
    private updateCollapseTip() {
336
        this.collapseTip = this.thyCollapsed ? '展开' : '收起';
337
        this.collapseTip = this.collapseTip + (isMacPlatform() ? `(⌘ + /)` : `(Ctrl + /)`);
338
    }
339

340
    resizeHandler({ width }: ThyResizeEvent) {
341
        if (width === this.sidebarDirective.thyLayoutSidebarWidth) {
342
            return;
343
        }
344
        if (this.thyCollapsible && width < this.thyCollapsedWidth) {
345
            return;
346
        }
347
        if (this.thyCollapsible && width === this.thyCollapsedWidth) {
348
            this.thyCollapsed = true;
349
            setTimeout(() => this.updateCollapseTip(), 200);
350
            this.thyCollapsedChange.emit(this.isCollapsed);
351
            this.sidebarDirective.thyLayoutSidebarWidth = this.originWidth;
352
            this.collapseVisible = false;
353
            return;
354
        }
355
        this.sidebarDirective.thyLayoutSidebarWidth = width;
356
        this.thyDragWidthChange.emit(width);
357
    }
358

359
    resizeStart() {
360
        this.originWidth = this.sidebarDirective.thyLayoutSidebarWidth;
361
        this.collapseHidden = true;
362
        this.isRemoveTransition = true;
363
    }
364

365
    resizeEnd() {
366
        this.collapseHidden = false;
367
        this.isRemoveTransition = false;
368
    }
369

370
    resizeHandleHover(event: MouseEvent, type: 'enter' | 'leave') {
371
        this.collapseVisible = type === 'enter' && !this.thyCollapsed ? true : false;
372
    }
373

374
    toggleCollapse(event?: MouseEvent) {
375
        this.thyCollapsed = !this.thyCollapsed;
376
        setTimeout(() => this.updateCollapseTip(), 200);
377
        this.thyCollapsedChange.emit(this.isCollapsed);
378
    }
379

380
    public toggleResizable(event: MouseEvent, type: 'enter' | 'leave') {
381
        this.isResizable = type === 'enter' ? true : false;
382
    }
383

384
    restoreToDefaultWidth() {
385
        if (this.thyDefaultWidth === 'lg') {
386
            this.thyDefaultWidth = LG_WIDTH;
387
        }
388
        this.sidebarDirective.thyLayoutSidebarWidth = (this.thyDefaultWidth as number) || SIDEBAR_DEFAULT_WIDTH;
389
        this.thyDragWidthChange.emit(this.sidebarDirective.thyLayoutSidebarWidth);
390
    }
391

392
    ngOnDestroy(): void {
393
        this.hotkeySubscription?.unsubscribe();
394
    }
395
}
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