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

atinc / ngx-tethys / 68ef226c-f83e-44c1-b8ed-e420a83c5d84

28 May 2025 10:31AM UTC coverage: 10.352% (-80.0%) from 90.316%
68ef226c-f83e-44c1-b8ed-e420a83c5d84

Pull #3460

circleci

pubuzhixing8
chore: xxx
Pull Request #3460: refactor(icon): migrate signal input #TINFR-1476

132 of 6823 branches covered (1.93%)

Branch coverage included in aggregate %.

10 of 14 new or added lines in 1 file covered. (71.43%)

11648 existing lines in 344 files now uncovered.

2078 of 14525 relevant lines covered (14.31%)

6.69 hits per line

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

1.74
/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';
UNCOV
16
import { Platform } from '@angular/cdk/platform';
×
UNCOV
17
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
×
UNCOV
18
import { ThyResizeHandleMouseDownEvent } from './interface';
×
UNCOV
19
import { ThyResizeEvent } from './interface';
×
UNCOV
20
import { getEventWithPoint, ensureInBounds, setCompatibleStyle } from './utils';
×
UNCOV
21
import { fromEvent } from 'rxjs';
×
UNCOV
22
import { coerceBooleanProperty, ThyNumberInput } from 'ngx-tethys/util';
×
UNCOV
23

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

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

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

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

×
61
    /**
62
     * 最小高度
UNCOV
63
     */
×
UNCOV
64
    readonly thyMinHeight = input(40, { transform: numberAttribute });
×
UNCOV
65

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

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

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

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

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

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

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

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

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

×
UNCOV
111
    /**
×
UNCOV
112
     * 结束调整尺寸时的事件
×
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;
UNCOV
120
    private ghostElement: HTMLDivElement | null = null;
×
UNCOV
121
    private currentHandleEvent: ThyResizeHandleMouseDownEvent | null = null;
×
122
    private readonly destroyRef = inject(DestroyRef);
123

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

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

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

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

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

×
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':
UNCOV
184
            case 'bottom':
×
UNCOV
185
                this.renderer.setStyle(document.body, 'cursor', 'ns-resize');
×
UNCOV
186
                break;
×
UNCOV
187
            case 'topLeft':
×
UNCOV
188
            case 'bottomRight':
×
UNCOV
189
                this.renderer.setStyle(document.body, 'cursor', 'nwse-resize');
×
UNCOV
190
                break;
×
UNCOV
191
            case 'topRight':
×
UNCOV
192
            case 'bottomLeft':
×
UNCOV
193
                this.renderer.setStyle(document.body, 'cursor', 'nesw-resize');
×
UNCOV
194
                break;
×
UNCOV
195
        }
×
196
        setCompatibleStyle(document.body, 'user-select', 'none');
197
    }
UNCOV
198

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

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

UNCOV
268
        if (this.thyPreview()) {
×
UNCOV
269
            this.previewResize(size);
×
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