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

atinc / ngx-tethys / 17048981-56c3-47da-832d-c095aeaf8b61

30 Aug 2023 07:38AM UTC coverage: 90.118% (+0.002%) from 90.116%
17048981-56c3-47da-832d-c095aeaf8b61

Pull #2819

circleci

minlovehua
Merge branch 'master' of github.com:atinc/ngx-tethys into minlovehua/sidebar-min-width
Pull Request #2819: feat(layout): sidebar support thyDragMinWidth INFR-3948

5130 of 6351 branches covered (0.0%)

Branch coverage included in aggregate %.

1 of 1 new or added line in 1 file covered. (100.0%)

12963 of 13726 relevant lines covered (94.44%)

974.91 hits per line

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

94.4
/src/layout/sidebar.component.ts
1
import { NgClass, NgIf, NgStyle, NgTemplateOutlet } from '@angular/common';
2
import {
3
    Component,
4
    ElementRef,
5
    EventEmitter,
6
    Host,
7
    HostBinding,
8
    HostListener,
9
    Input,
10
    OnDestroy,
11
    OnInit,
12
    Optional,
1✔
13
    Output,
1✔
14
    TemplateRef
1✔
15
} from '@angular/core';
16
import { ThyHotkeyDispatcher } from '@tethys/cdk/hotkey';
17
import { isMacPlatform } from '@tethys/cdk/is';
18
import { InputBoolean, InputNumber } from 'ngx-tethys/core';
19
import { ThyIconComponent } from 'ngx-tethys/icon';
20
import { ThyResizableDirective, ThyResizeEvent, ThyResizeHandleComponent } from 'ngx-tethys/resizable';
1✔
21
import { ThyTooltipDirective } from 'ngx-tethys/tooltip';
22
import { coerceBooleanProperty } from 'ngx-tethys/util';
134✔
23
import { Subscription } from 'rxjs';
6✔
24
import { ThyLayoutComponent } from './layout.component';
25

26
const LG_WIDTH = 300;
128✔
27
const SIDEBAR_DEFAULT_WIDTH = 240;
28
const SIDEBAR_COLLAPSED_WIDTH = 20;
29

30
export type ThySidebarTheme = 'white' | 'light' | 'dark';
2✔
31

32
export type ThySidebarDirection = 'left' | 'right';
33

2✔
34
/**
35
 * 布局侧边栏组件
36
 * @name thy-sidebar
31✔
37
 * @order 20
1✔
38
 */
39
@Component({
31✔
40
    selector: 'thy-sidebar',
41
    preserveWhitespaces: false,
42
    template: `
88✔
43
        <ng-content></ng-content>
44
        <div
45
            thyResizable
29✔
46
            class="sidebar-drag"
47
            *ngIf="thyDraggable"
48
            thyBounds="window"
29✔
49
            [thyMaxWidth]="thyDragMaxWidth"
50
            [thyMinWidth]="dragMinWidth"
51
            (thyResize)="resizeHandler($event)"
29✔
52
            (thyResizeStart)="resizeStart()"
53
            (thyResizeEnd)="resizeEnd()"
54
            [style.display]="!isResizable ? 'contents' : null">
37✔
55
            <thy-resize-handle
37✔
56
                *ngIf="!thyCollapsed"
9✔
57
                [thyDirection]="thyDirection === 'right' ? 'left' : 'right'"
58
                class="sidebar-resize-handle"
59
                thyLine="true"
28✔
60
                (mouseenter)="toggleResizable($event, 'enter')"
61
                (mouseleave)="toggleResizable($event, 'leave')"
62
                (dblclick)="restoreToDefaultWidth()">
63
            </thy-resize-handle>
415✔
64
        </div>
65
        <div *ngIf="thyCollapsible" class="sidebar-collapse-line"></div>
66
        <div
39✔
67
            *ngIf="thyCollapsible && thyTrigger !== null"
68
            class="sidebar-collapse"
69
            [ngClass]="{ 'collapse-visible': collapseVisible, 'collapse-hidden': collapseHidden }"
252✔
70
            (click)="toggleCollapse($event)"
71
            [thyTooltip]="!thyTrigger && collapseTip">
72
            <ng-template [ngTemplateOutlet]="thyTrigger || defaultTrigger"></ng-template>
134✔
73
            <ng-template #defaultTrigger>
74
                <thy-icon class="sidebar-collapse-icon" [thyIconName]="this.thyCollapsed ? 'indent' : 'outdent'"></thy-icon>
75
            </ng-template>
134✔
76
        </div>
77
    `,
78
    host: {
24✔
79
        class: 'thy-layout-sidebar',
80
        '[class.thy-layout-sidebar-right]': 'thyDirection === "right"',
81
        '[class.thy-layout-sidebar--clear-border-right]': 'thyDirection === "left" && !isDivided',
30✔
82
        '[class.thy-layout-sidebar--clear-border-left]': 'thyDirection === "right" && !isDivided',
30✔
83
        '[class.sidebar-theme-light]': 'thyTheme === "light"',
30✔
84
        '[class.sidebar-theme-dark]': 'thyTheme === "dark"',
30✔
85
        '[class.thy-layout-sidebar-isolated]': 'sidebarIsolated'
30✔
86
    },
30✔
87
    standalone: true,
30✔
88
    imports: [
30✔
89
        NgTemplateOutlet,
30✔
90
        NgIf,
30✔
91
        ThyResizeHandleComponent,
30✔
92
        ThyResizableDirective,
30✔
93
        ThyIconComponent,
30✔
94
        ThyTooltipDirective,
95
        NgClass,
96
        NgStyle
30!
97
    ]
30✔
98
})
99
export class ThySidebarComponent implements OnInit, OnDestroy {
30✔
100
    thyLayoutSidebarWidth: number;
2✔
101

102
    isDivided = true;
30✔
103

104
    sidebarIsolated = false;
105

9✔
106
    @HostBinding('style.width.px') get sidebarWidth() {
2✔
107
        if (this.thyCollapsible && this.thyCollapsed) {
108
            return this.thyCollapsedWidth;
109
        } else {
110
            return this.thyLayoutSidebarWidth;
37✔
111
        }
37!
112
    }
113

114
    @HostListener('mouseenter', ['$event'])
7✔
115
    mouseenter($event: MouseEvent) {
1✔
116
        this.resizeHandleHover($event, 'enter');
117
    }
6!
118

×
119
    @HostListener('mouseleave', ['$event'])
120
    mouseleave($event: MouseEvent) {
6✔
121
        this.resizeHandleHover($event, 'leave');
2✔
122
    }
2✔
123

2✔
124
    /**
2✔
125
     * 宽度,默认是 240px,传入 `lg` 大小时宽度是300px
2✔
126
     * @default 240px
2✔
127
     */
128
    @Input('thyWidth')
4✔
129
    set thyWidth(value: any) {
4✔
130
        if (value === 'lg') {
131
            value = LG_WIDTH;
132
        }
5✔
133
        this.thyLayoutSidebarWidth = value || SIDEBAR_DEFAULT_WIDTH;
5✔
134
    }
5✔
135

136
    /**
137
     * sidebar 位置,默认在左侧
4✔
138
     */
4✔
139
    @Input() thyDirection: ThySidebarDirection = 'left';
140

141
    /**
4✔
142
     * sidebar 是否有分割线。当`thyDirection`值为`left`时,控制右侧是否有分割线;当`thyDirection`值为`right`时,控制左侧是否有分割线。
143
     * @default true
144
     */
5✔
145
    @Input('thyDivided')
5✔
146
    set thyDivided(value: string) {
5✔
147
        this.isDivided = coerceBooleanProperty(value);
148
    }
149

2✔
150
    /**
151
     * 右侧是否有边框,已废弃,请使用 thyDivided
152
     * @deprecated please use thyDivided
1!
153
     * @default true
×
154
     */
155
    @Input('thyHasBorderRight')
1!
156
    set thyHasBorderRight(value: string) {
1✔
157
        this.thyDivided = value;
158
    }
159

30✔
160
    /**
161
     * 左侧是否有边框,已废弃,请使用 thyDivided
1✔
162
     * @deprecated please use thyDivided
163
     * @default true
164
     */
165
    @Input('thyHasBorderLeft')
166
    set thyHasBorderLeft(value: string) {
1✔
167
        this.thyDivided = value;
168
    }
169

170
    /**
171
     * 是否和右侧 /左侧隔离,当为 true 时距右侧 /左侧会有 margin,同时边框会去掉
172
     * @default false
173
     */
174
    @Input('thyIsolated')
175
    set thyIsolated(value: string) {
176
        this.sidebarIsolated = coerceBooleanProperty(value);
177
    }
178

179
    /**
180
     * 宽度是否可以拖拽
181
     * @default false
182
     */
183
    @Input() @InputBoolean() thyDraggable: boolean = false;
184

185
    /**
186
     * 拖拽的最大宽度
187
     */
188
    @Input() @InputNumber() thyDragMaxWidth: number;
189

190
    /**
191
     * 拖拽的最小宽度
1✔
192
     */
193
    @Input() @InputNumber() thyDragMinWidth: number;
194

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

202
    /**
203
     * 收起状态改变后的事件
1✔
204
     */
205
    @Output()
206
    thyCollapsedChange = new EventEmitter<boolean>();
207

208
    /**
1✔
209
     * 拖拽宽度的修改事件
210
     */
211
    @Output()
212
    thyDragWidthChange = new EventEmitter<number>();
213

1✔
214
    /**
215
     * 开启收起/展开功能
216
     * @default false
217
     */
1✔
218
    @Input() @InputBoolean() set thyCollapsible(collapsible: boolean) {
219
        this.collapsible = collapsible;
220
        if (this.collapsible) {
221
            this.subscribeHotkeyEvent();
222
        } else {
223
            this.hotkeySubscription?.unsubscribe();
224
        }
225
    }
226

227
    get thyCollapsible() {
228
        return this.collapsible;
229
    }
230

231
    /**
232
     * 是否是收起
233
     * @default false
234
     */
235
    @Input() @InputBoolean() set thyCollapsed(value: boolean) {
236
        this.isCollapsed = value;
237
    }
238

239
    get thyCollapsed() {
240
        return this.isCollapsed;
241
    }
242

243
    /**
244
     * 收起后的宽度
245
     */
246
    @Input() @InputNumber() thyCollapsedWidth = SIDEBAR_COLLAPSED_WIDTH;
247

248
    /**
249
     * 主题
250
     * @type white | light | dark
251
     * @default white
252
     */
253
    @Input() thyTheme: ThySidebarTheme;
254

255
    /**
256
     * 默认宽度,双击后可恢复到此宽度,默认是 240px,传入 lg 大小时宽度是300px
257
     */
258
    @Input() thyDefaultWidth: string | number;
259

260
    @HostBinding('class.sidebar-collapse-show')
261
    get collapseVisibility() {
262
        return this.thyCollapsed;
263
    }
264

265
    @HostBinding('class.remove-transition')
266
    get removeTransition() {
267
        return this.isRemoveTransition;
268
    }
269

270
    collapseTip: string;
271

272
    collapsible: boolean;
273

274
    isCollapsed = false;
275

276
    originWidth: number = SIDEBAR_DEFAULT_WIDTH;
277

278
    collapseVisible: boolean;
279

280
    collapseHidden: boolean;
281

282
    isRemoveTransition: boolean;
283

284
    isResizable: boolean;
285

286
    get dragMinWidth() {
287
        return this.thyDragMinWidth || this.thyCollapsedWidth;
288
    }
289

290
    private hotkeySubscription: Subscription;
291

292
    constructor(
293
        @Optional() @Host() private thyLayoutComponent: ThyLayoutComponent,
294
        public elementRef: ElementRef,
295
        private hotkeyDispatcher: ThyHotkeyDispatcher
296
    ) {}
297

298
    ngOnInit() {
299
        if (this.thyLayoutComponent) {
300
            this.thyLayoutComponent.hasSidebar = true;
301
        }
302
        if (this.thyDirection === 'right') {
303
            this.thyLayoutComponent.isSidebarRight = true;
304
        }
305
        this.updateCollapseTip();
306
    }
307

308
    private subscribeHotkeyEvent() {
309
        this.hotkeySubscription = this.hotkeyDispatcher.keydown(['Control+/', 'Meta+/']).subscribe(() => {
310
            this.toggleCollapse();
311
        });
312
    }
313

314
    private updateCollapseTip() {
315
        this.collapseTip = this.thyCollapsed ? '展开' : '收起';
316
        this.collapseTip = this.collapseTip + (isMacPlatform() ? `(⌘ + /)` : `(Ctrl + /)`);
317
    }
318

319
    resizeHandler({ width }: ThyResizeEvent) {
320
        if (width === this.thyLayoutSidebarWidth) {
321
            return;
322
        }
323
        if (this.thyCollapsible && width < this.thyCollapsedWidth) {
324
            return;
325
        }
326
        if (this.thyCollapsible && width === this.thyCollapsedWidth) {
327
            this.thyCollapsed = true;
328
            setTimeout(() => this.updateCollapseTip(), 200);
329
            this.thyCollapsedChange.emit(this.isCollapsed);
330
            this.thyLayoutSidebarWidth = this.originWidth;
331
            this.collapseVisible = false;
332
            return;
333
        }
334
        this.thyLayoutSidebarWidth = width;
335
        this.thyDragWidthChange.emit(width);
336
    }
337

338
    resizeStart() {
339
        this.originWidth = this.thyLayoutSidebarWidth;
340
        this.collapseHidden = true;
341
        this.isRemoveTransition = true;
342
    }
343

344
    resizeEnd() {
345
        this.collapseHidden = false;
346
        this.isRemoveTransition = false;
347
    }
348

349
    resizeHandleHover(event: MouseEvent, type: 'enter' | 'leave') {
350
        this.collapseVisible = type === 'enter' && !this.thyCollapsed ? true : false;
351
    }
352

353
    toggleCollapse(event?: MouseEvent) {
354
        this.thyCollapsed = !this.thyCollapsed;
355
        setTimeout(() => this.updateCollapseTip(), 200);
356
        this.thyCollapsedChange.emit(this.isCollapsed);
357
    }
358

359
    public toggleResizable(event: MouseEvent, type: 'enter' | 'leave') {
360
        this.isResizable = type === 'enter' ? true : false;
361
    }
362

363
    restoreToDefaultWidth() {
364
        if (this.thyDefaultWidth === 'lg') {
365
            this.thyDefaultWidth = LG_WIDTH;
366
        }
367
        this.thyLayoutSidebarWidth = (this.thyDefaultWidth as number) || SIDEBAR_DEFAULT_WIDTH;
368
        this.thyDragWidthChange.emit(this.thyLayoutSidebarWidth);
369
    }
370

371
    ngOnDestroy(): void {
372
        this.hotkeySubscription?.unsubscribe();
373
    }
374
}
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