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

atinc / ngx-tethys / 881c8997-29c3-4d01-9ef1-22092f16cec2

03 Apr 2024 03:31AM UTC coverage: 90.404% (-0.2%) from 90.585%
881c8997-29c3-4d01-9ef1-22092f16cec2

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()

5411 of 6635 branches covered (81.55%)

Branch coverage included in aggregate %.

217 of 223 new or added lines in 82 files covered. (97.31%)

201 existing lines in 53 files now uncovered.

13176 of 13925 relevant lines covered (94.62%)

980.1 hits per line

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

94.89
/src/resizable/resizable.directive.ts
1
import {
2
    Directive,
3
    AfterViewInit,
4
    OnDestroy,
5
    ElementRef,
6
    Renderer2,
7
    NgZone,
8
    Input,
9
    Output,
1✔
10
    EventEmitter,
11
    ChangeDetectorRef,
12
    booleanAttribute,
13
    numberAttribute
14
} from '@angular/core';
1✔
15
import { Constructor, ThyUnsubscribe, MixinBase, mixinUnsubscribe } from 'ngx-tethys/core';
16
import { ThyResizableService } from './resizable.service';
32✔
17
import { Platform } from '@angular/cdk/platform';
32✔
18
import { takeUntil } from 'rxjs/operators';
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

32✔
24
const _MixinBase: Constructor<ThyUnsubscribe> & typeof MixinBase = mixinUnsubscribe(MixinBase);
32✔
25

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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