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

atinc / ngx-tethys / e62d3b10-1466-49c3-aabd-707148681fc8

14 Jun 2024 08:24AM UTC coverage: 90.422%. Remained the same
e62d3b10-1466-49c3-aabd-707148681fc8

push

circleci

minlovehua
feat: use the ngx-tethys/util's coerceBooleanProperty instead of booleanAttribute #INFR-12648

5467 of 6692 branches covered (81.69%)

Branch coverage included in aggregate %.

117 of 120 new or added lines in 66 files covered. (97.5%)

183 existing lines in 46 files now uncovered.

13216 of 13970 relevant lines covered (94.6%)

985.91 hits per line

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

94.85
/src/resizable/resizable.directive.ts
1
import {
2
    Directive,
3
    AfterViewInit,
4
    OnDestroy,
5
    ElementRef,
6
    Renderer2,
7
    NgZone,
8
    Input,
9
    Output,
10
    EventEmitter,
11
    ChangeDetectorRef,
12
    inject,
13
    DestroyRef,
1✔
14
    numberAttribute
15
} from '@angular/core';
32✔
16
import { ThyResizableService } from './resizable.service';
32✔
17
import { Platform } from '@angular/cdk/platform';
32✔
18
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
32✔
19
import { ThyResizeHandleMouseDownEvent } from './resize-handle.component';
32✔
20
import { ThyResizeEvent } from './interface';
32✔
21
import { getEventWithPoint, ensureInBounds, setCompatibleStyle } from './utils';
32✔
22
import { fromEvent } from 'rxjs';
32✔
23
import { coerceBooleanProperty } from 'ngx-tethys/util';
32✔
24

32✔
25
/**
32✔
26
 * 调整尺寸
32✔
27
 * @name thyResizable
32✔
28
 */
32✔
29
@Directive({
32✔
30
    selector: '[thyResizable]',
32✔
31
    providers: [ThyResizableService],
32✔
32
    host: {
32✔
33
        class: 'thy-resizable',
32✔
34
        '[class.thy-resizable-resizing]': 'resizing',
32✔
35
        '[class.thy-resizable-disabled]': 'thyDisabled'
32✔
36
    },
32✔
37
    standalone: true
32✔
38
})
32✔
39
export class ThyResizableDirective implements AfterViewInit, OnDestroy {
31✔
40
    /**
1✔
41
     * 调整尺寸的边界
42
     * @default parent
30✔
43
     * @type 'window' | 'parent' | ElementRef<HTMLElement>
30✔
44
     */
30✔
45
    @Input() thyBounds: 'window' | 'parent' | ElementRef<HTMLElement> = 'parent';
30✔
46

30✔
47
    /**
48
     * 最大高度(超过边界部分忽略)
49
     */
30✔
50
    @Input({ transform: numberAttribute }) thyMaxHeight?: number;
6✔
51

52
    /**
30✔
53
     * 最大宽度(超过边界部分忽略)
54
     */
32✔
55
    @Input({ transform: numberAttribute }) thyMaxWidth?: number;
52✔
56

26✔
57
    /**
26✔
58
     * 最小高度
26✔
59
     */
60
    @Input({ transform: numberAttribute }) thyMinHeight: number = 40;
26✔
61

26✔
62
    /**
63
     * 最小宽度
64
     */
32✔
65
    @Input({ transform: numberAttribute }) thyMinWidth: number = 40;
27!
66

27✔
67
    /**
68
     * 栅格列数(-1 为不栅格)
69
     */
70
    @Input({ transform: numberAttribute }) thyGridColumnCount: number = -1;
71

32!
UNCOV
72
    /**
×
73
     * 栅格最大列数
74
     */
32✔
75
    @Input({ transform: numberAttribute }) thyMaxColumn: number = -1;
32✔
76

32✔
77
    /**
78
     * 栅格最小列数
79
     */
2✔
80
    @Input({ transform: numberAttribute }) thyMinColumn: number = -1;
81

32✔
82
    /**
83
     * 锁定宽高比
84
     */
2✔
85
    @Input({ transform: coerceBooleanProperty }) thyLockAspectRatio: boolean = false;
86

87
    /**
88
     * 是否预览模式
89
     */
30✔
90
    @Input({ transform: coerceBooleanProperty }) thyPreview: boolean = false;
91

92
    /**
11✔
93
     * 是否禁用调整大小
11✔
94
     */
95
    @Input({ transform: coerceBooleanProperty }) thyDisabled: boolean = false;
96

3✔
97
    /**
3✔
98
     * 调整尺寸时的事件
99
     */
100
    @Output() readonly thyResize = new EventEmitter<ThyResizeEvent>();
14✔
101

14✔
102
    /**
103
     * 开始调整尺寸时的事件
104
     */
2✔
105
    @Output() readonly thyResizeStart = new EventEmitter<ThyResizeEvent>();
2✔
106

107
    /**
30✔
108
     * 结束调整尺寸时的事件
109
     */
110
    @Output() readonly thyResizeEnd = new EventEmitter<ThyResizeEvent>();
26✔
111

26✔
112
    resizing = false;
26✔
113
    private nativeElement!: HTMLElement;
26!
114
    private nativeElementRect!: ClientRect | DOMRect;
115
    private sizeCache: ThyResizeEvent | null = null;
116
    private ghostElement: HTMLDivElement | null = null;
117
    private currentHandleEvent: ThyResizeHandleMouseDownEvent | null = null;
118
    private readonly destroyRef = inject(DestroyRef);
119

120
    constructor(
121
        private elementRef: ElementRef<HTMLElement>,
26✔
122
        private renderer: Renderer2,
5✔
123
        private platform: Platform,
5✔
124
        private ngZone: NgZone,
125
        private thyResizableService: ThyResizableService,
126
        private changeDetectorRef: ChangeDetectorRef
127
    ) {
128
        this.thyResizableService.handleMouseDownOutsideAngular$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(event => {
129
            if (this.thyDisabled) {
26✔
130
                return;
26✔
131
            }
132
            this.resizing = true;
133
            const { mouseEvent } = event;
27✔
134
            this.thyResizableService.startResizing(mouseEvent);
27✔
135
            this.currentHandleEvent = event;
27✔
136
            this.setCursor();
27✔
137
            // Re-enter the Angular zone and run the change detection only if there're any `thyResizeStart` listeners,
27✔
138
            // e.g.: `<div thyResizable (thyResizeStart)="..."></div>`.
27✔
139
            if (this.thyResizeStart.observers.length) {
27✔
140
                this.ngZone.run(() => this.thyResizeStart.emit({ mouseEvent }));
141
            }
10✔
142
            this.nativeElementRect = this.nativeElement.getBoundingClientRect();
10✔
143
        });
10✔
144

145
        this.thyResizableService.documentMouseUpOutsideAngular$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(event => {
1✔
146
            if (this.resizing) {
1✔
147
                this.ngZone.run(() => {
1✔
148
                    this.resizing = false;
149
                    this.changeDetectorRef.markForCheck();
1✔
150
                });
1✔
151
                this.thyResizableService.documentMouseUpOutsideAngular$.next(event);
1✔
152
                this.endResize(event);
153
            }
1✔
154
        });
1✔
155

1✔
156
        this.thyResizableService.documentMouseMoveOutsideAngular$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(event => {
157
            if (this.resizing) {
2✔
158
                this.resize(event);
2✔
159
            }
160
        });
9✔
161
    }
9✔
162

163
    ngAfterViewInit(): void {
1✔
164
        if (!this.platform.isBrowser) {
1✔
165
            return;
166
        }
2✔
167
        this.nativeElement = this.elementRef.nativeElement;
168
        this.ngZone.runOutsideAngular(() => {
27✔
169
            fromEvent(this.nativeElement, 'mouseenter')
27✔
170
                .pipe(takeUntilDestroyed(this.destroyRef))
171
                .subscribe(() => {
172
                    this.thyResizableService.mouseEnteredOutsideAngular$.next(true);
27✔
173
                });
26✔
174

26✔
175
            fromEvent(this.nativeElement, 'mouseleave')
176
                .pipe(takeUntilDestroyed(this.destroyRef))
177
                .subscribe(() => {
178
                    this.thyResizableService.mouseEnteredOutsideAngular$.next(false);
179
                });
180
        });
27✔
181
    }
1✔
182

183
    setCursor(): void {
184
        switch (this.currentHandleEvent!.direction) {
185
            case 'left':
186
            case 'right':
187
                this.renderer.setStyle(document.body, 'cursor', 'ew-resize');
188
                break;
189
            case 'top':
27✔
190
            case 'bottom':
27✔
191
                this.renderer.setStyle(document.body, 'cursor', 'ns-resize');
27✔
192
                break;
27✔
193
            case 'topLeft':
27✔
194
            case 'bottomRight':
27✔
195
                this.renderer.setStyle(document.body, 'cursor', 'nwse-resize');
19✔
196
                break;
19!
197
            case 'topRight':
19✔
198
            case 'bottomLeft':
19✔
199
                this.renderer.setStyle(document.body, 'cursor', 'nesw-resize');
19✔
200
                break;
201
        }
202
        setCompatibleStyle(document.body, 'user-select', 'none');
8✔
203
    }
7!
204

7✔
205
    endResize(event: MouseEvent | TouchEvent): void {
7✔
206
        this.renderer.setStyle(document.body, 'cursor', '');
207
        setCompatibleStyle(document.body, 'user-select', '');
208
        this.removeGhostElement();
1!
209
        const size = this.sizeCache
1✔
210
            ? { ...this.sizeCache }
1✔
211
            : {
1✔
212
                  width: this.nativeElementRect.width,
213
                  height: this.nativeElementRect.height
27✔
214
              };
27✔
215
        // Re-enter the Angular zone and run the change detection only if there're any `thyResizeEnd` listeners,
27✔
216
        // e.g.: `<div thyResizable (thyResizeEnd)="..."></div>`.
2✔
217
        if (this.thyResizeEnd.observers.length) {
2!
218
            this.ngZone.run(() => {
2!
219
                this.thyResizeEnd.emit({
220
                    ...size,
27✔
221
                    mouseEvent: event
3✔
222
                });
2✔
223
            });
2✔
224
        }
2✔
225
        this.sizeCache = null;
1✔
226
        this.currentHandleEvent = null;
227
    }
228

229
    resize(event: MouseEvent | TouchEvent): void {
1✔
230
        const nativeElementRect = this.nativeElementRect;
1✔
231
        const resizeEvent = getEventWithPoint(event);
1!
UNCOV
232
        const handleEvent = getEventWithPoint(this.currentHandleEvent!.mouseEvent);
×
233
        let width = nativeElementRect.width;
234
        let height = nativeElementRect.height;
235
        const ratio = this.thyLockAspectRatio ? width / height : -1;
236
        switch (this.currentHandleEvent!.direction) {
237
            case 'bottomRight':
24✔
238
                width = resizeEvent.clientX - nativeElementRect.left;
24✔
239
                height = resizeEvent.clientY - nativeElementRect.top;
240
                break;
27✔
241
            case 'bottomLeft':
2✔
242
                width = nativeElementRect.width + (handleEvent.clientX - resizeEvent.clientX);
2✔
243
                height = resizeEvent.clientY - nativeElementRect.top;
244
                break;
27✔
245
            case 'topRight':
246
                width = resizeEvent.clientX - nativeElementRect.left;
247
                height = nativeElementRect.height + (handleEvent.clientY - resizeEvent.clientY);
248
                break;
249
            case 'topLeft':
250
                width = nativeElementRect.width + (handleEvent.clientX - resizeEvent.clientX);
251
                height = nativeElementRect.height + (handleEvent.clientY - resizeEvent.clientY);
1✔
252
                break;
1✔
253
            case 'top':
1✔
254
                height = nativeElementRect.height + (handleEvent.clientY - resizeEvent.clientY);
255
                break;
256
            case 'right':
1!
257
                width = resizeEvent.clientX - nativeElementRect.left;
1✔
258
                break;
1✔
259
            case 'bottom':
260
                height = resizeEvent.clientY - nativeElementRect.top;
1✔
261
                break;
262
            case 'left':
263
                width = nativeElementRect.width + (handleEvent.clientX - resizeEvent.clientX);
26✔
264
        }
1✔
265
        const size = this.calcSize(width, height, ratio);
266
        this.sizeCache = { ...size };
267
        // Re-enter the Angular zone and run the change detection only if there're any `thyResize` listeners,
268
        // e.g.: `<div thyResizable (thyResize)="..."></div>`.
32✔
269
        if (this.thyResize.observers.length) {
32✔
270
            this.ngZone.run(() => {
271
                this.thyResize.emit({
1✔
272
                    ...size,
273
                    mouseEvent: event
274
                });
275
            });
276
        }
277

278
        if (this.thyPreview) {
279
            this.previewResize(size);
1✔
280
        }
281
    }
282

283
    calcSize(width: number, height: number, ratio: number): ThyResizeEvent {
284
        let newWidth: number;
285
        let newHeight: number;
286
        let maxWidth: number;
287
        let maxHeight: number;
288
        let col = 0;
289
        let spanWidth = 0;
290
        let minWidth = this.thyMinWidth;
291
        let boundWidth = Infinity;
292
        let boundHeight = Infinity;
293
        if (this.thyBounds === 'parent') {
294
            const parent = this.renderer.parentNode(this.nativeElement);
295
            if (parent instanceof HTMLElement) {
296
                const parentRect = parent.getBoundingClientRect();
1✔
297
                boundWidth = parentRect.width;
298
                boundHeight = parentRect.height;
299
            }
300
        } else if (this.thyBounds === 'window') {
301
            if (typeof window !== 'undefined') {
302
                boundWidth = window.innerWidth;
303
                boundHeight = window.innerHeight;
304
            }
305
        } else if (this.thyBounds && this.thyBounds.nativeElement && this.thyBounds.nativeElement instanceof HTMLElement) {
306
            const boundsRect = this.thyBounds.nativeElement.getBoundingClientRect();
307
            boundWidth = boundsRect.width;
308
            boundHeight = boundsRect.height;
309
        }
310

311
        maxWidth = ensureInBounds(this.thyMaxWidth!, boundWidth);
312
        maxHeight = ensureInBounds(this.thyMaxHeight!, boundHeight);
313

314
        if (this.thyGridColumnCount !== -1) {
315
            spanWidth = maxWidth / this.thyGridColumnCount;
316
            minWidth = this.thyMinColumn !== -1 ? spanWidth * this.thyMinColumn : minWidth;
317
            maxWidth = this.thyMaxColumn !== -1 ? spanWidth * this.thyMaxColumn : maxWidth;
318
        }
319

320
        if (ratio !== -1) {
321
            if (/(left|right)/i.test(this.currentHandleEvent!.direction)) {
322
                newWidth = Math.min(Math.max(width, minWidth), maxWidth);
323
                newHeight = Math.min(Math.max(newWidth / ratio, this.thyMinHeight), maxHeight);
324
                if (newHeight >= maxHeight || newHeight <= this.thyMinHeight) {
325
                    newWidth = Math.min(Math.max(newHeight * ratio, minWidth), maxWidth);
326
                }
327
            } else {
328
                newHeight = Math.min(Math.max(height, this.thyMinHeight), maxHeight);
329
                newWidth = Math.min(Math.max(newHeight * ratio, minWidth), maxWidth);
330
                if (newWidth >= maxWidth || newWidth <= minWidth) {
331
                    newHeight = Math.min(Math.max(newWidth / ratio, this.thyMinHeight), maxHeight);
332
                }
333
            }
334
        } else {
335
            newWidth = Math.min(Math.max(width, minWidth), maxWidth);
336
            newHeight = Math.min(Math.max(height, this.thyMinHeight), maxHeight);
337
        }
338

339
        if (this.thyGridColumnCount !== -1) {
340
            col = Math.round(newWidth / spanWidth);
341
            newWidth = col * spanWidth;
342
        }
343

344
        return {
345
            col,
346
            width: newWidth,
347
            height: newHeight
348
        };
349
    }
350

351
    previewResize({ width, height }: ThyResizeEvent): void {
352
        this.createGhostElement();
353
        this.renderer.setStyle(this.ghostElement, 'width', `${width}px`);
354
        this.renderer.setStyle(this.ghostElement, 'height', `${height}px`);
355
    }
356

357
    createGhostElement(): void {
358
        if (!this.ghostElement) {
359
            this.ghostElement = this.renderer.createElement('div');
360
            this.renderer.setAttribute(this.ghostElement, 'class', 'thy-resizable-preview');
361
        }
362
        this.renderer.appendChild(this.nativeElement, this.ghostElement);
363
    }
364

365
    removeGhostElement(): void {
366
        if (this.ghostElement) {
367
            this.renderer.removeChild(this.nativeElement, this.ghostElement);
368
        }
369
    }
370

371
    ngOnDestroy(): void {
372
        this.ghostElement = null;
373
        this.sizeCache = null;
374
    }
375
}
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