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

atinc / ngx-tethys / 6d07b3c1-c212-4499-ba96-b047add67dde

28 May 2025 03:25AM UTC coverage: 90.224% (+0.003%) from 90.221%
6d07b3c1-c212-4499-ba96-b047add67dde

Pull #3441

circleci

invalid-email-address
build: fix ci
Pull Request #3441: feat(resizable): migration signal for resizable #TINFR-1764 @wumeimin

5548 of 6810 branches covered (81.47%)

Branch coverage included in aggregate %.

42 of 42 new or added lines in 4 files covered. (100.0%)

16 existing lines in 2 files now uncovered.

13612 of 14426 relevant lines covered (94.36%)

905.77 hits per line

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

94.78
/src/resizable/resizable.directive.ts
1
import {
2
    Directive,
3
    OnDestroy,
4
    ElementRef,
5
    Renderer2,
6
    NgZone,
7
    inject,
8
    DestroyRef,
9
    numberAttribute,
10
    input,
11
    output,
12
    afterNextRender,
13
    signal
14
} from '@angular/core';
1✔
15
import { ThyResizableService } from './resizable.service';
16
import { Platform } from '@angular/cdk/platform';
31✔
17
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
31✔
18
import { ThyResizeHandleMouseDownEvent } from './interface';
31✔
19
import { ThyResizeEvent } from './interface';
31✔
20
import { getEventWithPoint, ensureInBounds, setCompatibleStyle } from './utils';
31✔
21
import { fromEvent } from 'rxjs';
31✔
22
import { coerceBooleanProperty, ThyNumberInput } from 'ngx-tethys/util';
31✔
23

31✔
24
/**
31✔
25
 * 调整尺寸
31✔
26
 * @name thyResizable
31✔
27
 */
31✔
28
@Directive({
31✔
29
    selector: '[thyResizable]',
31✔
30
    providers: [ThyResizableService],
31✔
31
    host: {
31✔
32
        class: 'thy-resizable',
31✔
33
        '[class.thy-resizable-resizing]': 'resizing()',
31✔
34
        '[class.thy-resizable-disabled]': 'thyDisabled()'
31✔
35
    }
31✔
36
})
31✔
37
export class ThyResizableDirective implements OnDestroy {
31✔
38
    private elementRef = inject<ElementRef<HTMLElement>>(ElementRef);
31✔
39
    private renderer = inject(Renderer2);
31✔
40
    private platform = inject(Platform);
31✔
41
    private ngZone = inject(NgZone);
29✔
42
    private thyResizableService = inject(ThyResizableService);
1✔
43

44
    /**
28✔
45
     * 调整尺寸的边界
28✔
46
     * @default parent
28✔
47
     * @type 'window' | 'parent' | ElementRef<HTMLElement>
28✔
48
     */
28✔
49
    readonly thyBounds = input<'window' | 'parent' | ElementRef<HTMLElement>>('parent');
50

51
    /**
28✔
52
     * 最大高度(超过边界部分忽略)
28✔
53
     */
54
    readonly thyMaxHeight = input<number, ThyNumberInput>(undefined, { transform: numberAttribute });
31✔
55

52✔
56
    /**
26✔
57
     * 最大宽度(超过边界部分忽略)
26✔
58
     */
59
    readonly thyMaxWidth = input<number, ThyNumberInput>(undefined, { transform: numberAttribute });
26✔
60

26✔
61
    /**
62
     * 最小高度
63
     */
31✔
64
    readonly thyMinHeight = input(40, { transform: numberAttribute });
27!
65

27✔
66
    /**
67
     * 最小宽度
68
     */
31✔
69
    readonly thyMinWidth = input(40, { transform: numberAttribute });
31!
70

×
71
    /**
72
     * 栅格列数(-1 为不栅格)
31✔
73
     */
31✔
74
    readonly thyGridColumnCount = input(-1, { transform: numberAttribute });
31✔
75

76
    /**
77
     * 栅格最大列数
2✔
78
     */
79
    readonly thyMaxColumn = input(-1, { transform: numberAttribute });
31✔
80

81
    /**
82
     * 栅格最小列数
2✔
83
     */
84
    readonly thyMinColumn = input(-1, { transform: numberAttribute });
85

86
    /**
87
     * 锁定宽高比
88
     */
28✔
89
    readonly thyLockAspectRatio = input(false, { transform: coerceBooleanProperty });
90

91
    /**
11✔
92
     * 是否预览模式
11✔
93
     */
94
    readonly thyPreview = input(false, { transform: coerceBooleanProperty });
95

3✔
96
    /**
3✔
97
     * 是否禁用调整大小
98
     */
99
    readonly thyDisabled = input(false, { transform: coerceBooleanProperty });
12✔
100

12✔
101
    /**
102
     * 调整尺寸时的事件
103
     */
2✔
104
    readonly thyResize = output<ThyResizeEvent>();
2✔
105

106
    /**
28✔
107
     * 开始调整尺寸时的事件
108
     */
109
    readonly thyResizeStart = output<ThyResizeEvent>();
26✔
110

26✔
111
    /**
26✔
112
     * 结束调整尺寸时的事件
26!
113
     */
114
    readonly thyResizeEnd = output<ThyResizeEvent>();
115

116
    resizing = signal(false);
117
    private nativeElement!: HTMLElement;
118
    private nativeElementRect!: ClientRect | DOMRect;
119
    private sizeCache: ThyResizeEvent | null = null;
120
    private ghostElement: HTMLDivElement | null = null;
26✔
121
    private currentHandleEvent: ThyResizeHandleMouseDownEvent | null = null;
26✔
122
    private readonly destroyRef = inject(DestroyRef);
123

124
    constructor() {
125
        this.thyResizableService.handleMouseDownOutsideAngular$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(event => {
126
            if (this.thyDisabled()) {
26✔
127
                return;
26✔
128
            }
129
            this.resizing.set(true);
130
            const { mouseEvent } = event;
27✔
131
            this.thyResizableService.startResizing(mouseEvent);
27✔
132
            this.currentHandleEvent = event;
27✔
133
            this.setCursor();
27✔
134
            // Re-enter the Angular zone and run the change detection only if there're any `thyResizeStart` listeners,
27✔
135
            // e.g.: `<div thyResizable (thyResizeStart)="..."></div>`.
27✔
136
            this.ngZone.run(() => this.thyResizeStart.emit({ mouseEvent }));
27✔
137
            this.nativeElementRect = this.nativeElement.getBoundingClientRect();
138
        });
10✔
139

10✔
140
        this.thyResizableService.documentMouseUpOutsideAngular$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(event => {
10✔
141
            if (this.resizing()) {
142
                this.ngZone.run(() => {
1✔
143
                    this.resizing.set(false);
1✔
144
                });
1✔
145
                this.thyResizableService.documentMouseUpOutsideAngular$.next(event);
146
                this.endResize(event);
1✔
147
            }
1✔
148
        });
1✔
149

150
        this.thyResizableService.documentMouseMoveOutsideAngular$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(event => {
1✔
151
            if (this.resizing()) {
1✔
152
                this.resize(event);
1✔
153
            }
154
        });
2✔
155

2✔
156
        afterNextRender(() => {
157
            if (!this.platform.isBrowser) {
9✔
158
                return;
9✔
159
            }
160
            this.nativeElement = this.elementRef.nativeElement;
1✔
161
            this.ngZone.runOutsideAngular(() => {
1✔
162
                fromEvent(this.nativeElement, 'mouseenter')
163
                    .pipe(takeUntilDestroyed(this.destroyRef))
2✔
164
                    .subscribe(() => {
165
                        this.thyResizableService.mouseEnteredOutsideAngular$.next(true);
27✔
166
                    });
27✔
167

168
                fromEvent(this.nativeElement, 'mouseleave')
169
                    .pipe(takeUntilDestroyed(this.destroyRef))
27✔
170
                    .subscribe(() => {
27✔
171
                        this.thyResizableService.mouseEnteredOutsideAngular$.next(false);
172
                    });
173
            });
174
        });
175
    }
27✔
176

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

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

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

268
        if (this.thyPreview()) {
31✔
269
            this.previewResize(size);
31✔
270
        }
271
    }
1✔
272

1✔
273
    calcSize(width: number, height: number, ratio: number): ThyResizeEvent {
274
        let newWidth: number;
275
        let newHeight: number;
276
        let maxWidth: number;
277
        let maxHeight: number;
278
        let col = 0;
279
        let spanWidth = 0;
280
        let minWidth = this.thyMinWidth();
281
        let boundWidth = Infinity;
282
        let boundHeight = Infinity;
283
        const bounds = this.thyBounds();
284
        if (bounds === 'parent') {
285
            const parent = this.renderer.parentNode(this.nativeElement);
286
            if (parent instanceof HTMLElement) {
287
                const parentRect = parent.getBoundingClientRect();
288
                boundWidth = parentRect.width;
289
                boundHeight = parentRect.height;
1✔
290
            }
291
        } else if (bounds === 'window') {
292
            if (typeof window !== 'undefined') {
293
                boundWidth = window.innerWidth;
294
                boundHeight = window.innerHeight;
295
            }
296
        } else if (bounds && bounds.nativeElement && bounds.nativeElement instanceof HTMLElement) {
297
            const boundsRect = bounds.nativeElement.getBoundingClientRect();
298
            boundWidth = boundsRect.width;
299
            boundHeight = boundsRect.height;
300
        }
301

302
        maxWidth = ensureInBounds(this.thyMaxWidth()!, boundWidth);
303
        maxHeight = ensureInBounds(this.thyMaxHeight()!, boundHeight);
304

305
        const gridColumnCount = this.thyGridColumnCount();
306
        if (gridColumnCount !== -1) {
307
            spanWidth = maxWidth / gridColumnCount;
308
            const minColumn = this.thyMinColumn();
309
            minWidth = minColumn !== -1 ? spanWidth * minColumn : minWidth;
310
            const maxColumn = this.thyMaxColumn();
311
            maxWidth = maxColumn !== -1 ? spanWidth * maxColumn : maxWidth;
312
        }
313

314
        const minHeight = this.thyMinHeight();
315
        if (ratio !== -1) {
316
            if (/(left|right)/i.test(this.currentHandleEvent!.direction)) {
317
                newWidth = Math.min(Math.max(width, minWidth), maxWidth);
318
                newHeight = Math.min(Math.max(newWidth / ratio, minHeight), maxHeight);
319
                if (newHeight >= maxHeight || newHeight <= minHeight) {
320
                    newWidth = Math.min(Math.max(newHeight * ratio, minWidth), maxWidth);
321
                }
322
            } else {
323
                newHeight = Math.min(Math.max(height, minHeight), maxHeight);
324
                newWidth = Math.min(Math.max(newHeight * ratio, minWidth), maxWidth);
325
                if (newWidth >= maxWidth || newWidth <= minWidth) {
326
                    newHeight = Math.min(Math.max(newWidth / ratio, minHeight), maxHeight);
327
                }
328
            }
329
        } else {
330
            newWidth = Math.min(Math.max(width, minWidth), maxWidth);
331
            newHeight = Math.min(Math.max(height, minHeight), maxHeight);
332
        }
333

334
        if (gridColumnCount !== -1) {
335
            col = Math.round(newWidth / spanWidth);
336
            newWidth = col * spanWidth;
337
        }
338

339
        return {
340
            col,
341
            width: newWidth,
342
            height: newHeight
343
        };
344
    }
345

346
    previewResize({ width, height }: ThyResizeEvent): void {
347
        this.createGhostElement();
348
        this.renderer.setStyle(this.ghostElement, 'width', `${width}px`);
349
        this.renderer.setStyle(this.ghostElement, 'height', `${height}px`);
350
    }
351

352
    createGhostElement(): void {
353
        if (!this.ghostElement) {
354
            this.ghostElement = this.renderer.createElement('div');
355
            this.renderer.setAttribute(this.ghostElement, 'class', 'thy-resizable-preview');
356
        }
357
        this.renderer.appendChild(this.nativeElement, this.ghostElement);
358
    }
359

360
    removeGhostElement(): void {
361
        if (this.ghostElement) {
362
            this.renderer.removeChild(this.nativeElement, this.ghostElement);
363
        }
364
    }
365

366
    ngOnDestroy(): void {
367
        this.ghostElement = null;
368
        this.sizeCache = null;
369
    }
370
}
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