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

atinc / ngx-tethys / 9e3e73ff-1b5a-45c0-8d98-40e0cfd13cc3

28 Nov 2023 02:12AM UTC coverage: 90.303% (+0.01%) from 90.289%
9e3e73ff-1b5a-45c0-8d98-40e0cfd13cc3

push

circleci

web-flow
feat(layout): add directives for the layout component series and refactor existing layout components #INFR-10500 (#2919)

5316 of 6547 branches covered (0.0%)

Branch coverage included in aggregate %.

79 of 80 new or added lines in 9 files covered. (98.75%)

3 existing lines in 2 files now uncovered.

13253 of 14016 relevant lines covered (94.56%)

975.85 hits per line

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

94.66
/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,
11
    OnDestroy,
12
    OnInit,
1✔
13
    Optional,
1✔
14
    Output,
1✔
15
    TemplateRef,
16
    inject
17
} from '@angular/core';
18
import { ThyHotkeyDispatcher } from '@tethys/cdk/hotkey';
19
import { isMacPlatform } from '@tethys/cdk/is';
20
import { InputBoolean, InputNumber } from 'ngx-tethys/core';
1✔
21
import { ThyIconComponent } from 'ngx-tethys/icon';
22
import { ThyResizableDirective, ThyResizeEvent, ThyResizeHandleComponent } from 'ngx-tethys/resizable';
45✔
23
import { ThyTooltipDirective } from 'ngx-tethys/tooltip';
2✔
24
import { coerceBooleanProperty } from 'ngx-tethys/util';
25
import { Subscription } from 'rxjs';
45✔
26
import { ThyLayoutDirective } from './layout.component';
27

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

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

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

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

57
    isDivided = true;
1✔
58

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

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

66
    /**
67
     * 主题
68
     * @type white | light | dark
1✔
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
     */
89
    @Input('thyIsolated')
90
    set thyIsolated(value: string) {
1✔
91
        this.sidebarIsolated = coerceBooleanProperty(value);
92
    }
134✔
93

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

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

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

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

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

1✔
200
    @HostBinding('style.width.px') get sidebarWidth() {
201
        if (this.thyCollapsible && this.thyCollapsed) {
202
            return this.thyCollapsedWidth;
30✔
203
        } else {
204
            return this.sidebarDirective.thyLayoutSidebarWidth;
1✔
205
        }
206
    }
207

208
    @HostListener('mouseenter', ['$event'])
1✔
209
    mouseenter($event: MouseEvent) {
210
        this.resizeHandleHover($event, 'enter');
211
    }
212

213
    @HostListener('mouseleave', ['$event'])
214
    mouseleave($event: MouseEvent) {
215
        this.resizeHandleHover($event, 'leave');
216
    }
217

218
    /**
219
     * 宽度是否可以拖拽
220
     * @default false
221
     */
222
    @Input() @InputBoolean() thyDraggable: boolean = false;
223

224
    /**
225
     * 拖拽的最大宽度
226
     */
1✔
227
    @Input() @InputNumber() thyDragMaxWidth: number;
228

229
    /**
230
     * 拖拽的最小宽度
1✔
231
     */
232
    @Input() @InputNumber() thyDragMinWidth: number;
233

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

241
    /**
242
     * 收起状态改变后的事件
243
     */
1✔
244
    @Output()
245
    thyCollapsedChange = new EventEmitter<boolean>();
246

247
    /**
248
     * 拖拽宽度的修改事件
1✔
249
     */
250
    @Output()
251
    thyDragWidthChange = new EventEmitter<number>();
252

1✔
253
    /**
254
     * 开启收起/展开功能
255
     * @default false
256
     */
257
    @Input() @InputBoolean() set thyCollapsible(collapsible: boolean) {
258
        this.collapsible = collapsible;
259
        if (this.collapsible) {
260
            this.subscribeHotkeyEvent();
261
        } else {
262
            this.hotkeySubscription?.unsubscribe();
263
        }
264
    }
265

266
    get thyCollapsible() {
267
        return this.collapsible;
268
    }
269

270
    /**
271
     * 是否是收起
272
     * @default false
273
     */
274
    @Input() @InputBoolean() set thyCollapsed(value: boolean) {
275
        this.isCollapsed = value;
276
    }
277

278
    get thyCollapsed() {
279
        return this.isCollapsed;
280
    }
281

282
    /**
283
     * 收起后的宽度
284
     */
285
    @Input() @InputNumber() thyCollapsedWidth = SIDEBAR_COLLAPSED_WIDTH;
286

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

292
    @HostBinding('class.sidebar-collapse-show')
293
    get collapseVisibility() {
294
        return this.thyCollapsed;
295
    }
296

297
    @HostBinding('class.remove-transition')
298
    get removeTransition() {
299
        return this.isRemoveTransition;
300
    }
301

302
    collapseTip: string;
303

304
    collapsible: boolean;
305

306
    isCollapsed = false;
307

308
    originWidth: number = SIDEBAR_DEFAULT_WIDTH;
309

310
    collapseVisible: boolean;
311

312
    collapseHidden: boolean;
313

314
    isRemoveTransition: boolean;
315

316
    isResizable: boolean;
317

318
    get dragMinWidth() {
319
        return this.thyDragMinWidth || this.thyCollapsedWidth;
320
    }
321

322
    private hotkeySubscription: Subscription;
323

324
    constructor(public elementRef: ElementRef, private hotkeyDispatcher: ThyHotkeyDispatcher) {}
325

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

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

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

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

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

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

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

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

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

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

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