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

atinc / ngx-tethys / d9ae709b-3c27-4b69-b125-b8b80b54f90b

pending completion
d9ae709b-3c27-4b69-b125-b8b80b54f90b

Pull #2757

circleci

mengshuicmq
fix: fix code review
Pull Request #2757: feat(color-picker): color-picker support disabled (#INFR-8645)

98 of 6315 branches covered (1.55%)

Branch coverage included in aggregate %.

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

2392 of 13661 relevant lines covered (17.51%)

83.12 hits per line

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

5.93
/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
} from '@angular/core';
13
import { Constructor, ThyUnsubscribe, MixinBase, mixinUnsubscribe, InputBoolean, InputNumber } from 'ngx-tethys/core';
14
import { ThyResizableService } from './resizable.service';
1✔
15
import { Platform } from '@angular/cdk/platform';
16
import { takeUntil } from 'rxjs/operators';
×
17
import { ThyResizeHandleMouseDownEvent } from './resize-handle.component';
×
18
import { ThyResizeEvent } from './interface';
×
19
import { getEventWithPoint, ensureInBounds, setCompatibleStyle } from './utils';
×
20
import { fromEvent } from 'rxjs';
×
21

×
22
const _MixinBase: Constructor<ThyUnsubscribe> & typeof MixinBase = mixinUnsubscribe(MixinBase);
×
23

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

×
46
    /**
×
47
     * 最大高度(超过边界部分忽略)
×
48
     */
49
    @Input() @InputNumber() thyMaxHeight?: number;
50

×
51
    /**
×
52
     * 最大宽度(超过边界部分忽略)
53
     */
×
54
    @Input() @InputNumber() thyMaxWidth?: number;
55

×
56
    /**
×
57
     * 最小高度
×
58
     */
×
59
    @Input() @InputNumber() thyMinHeight: number = 40;
×
60

61
    /**
×
62
     * 最小宽度
×
63
     */
64
    @Input() @InputNumber() thyMinWidth: number = 40;
65

×
66
    /**
×
67
     * 栅格列数(-1 为不栅格)
×
68
     */
69
    @Input() @InputNumber() thyGridColumnCount: number = -1;
70

71
    /**
72
     * 栅格最大列数
×
73
     */
×
74
    @Input() @InputNumber() thyMaxColumn: number = -1;
75

×
76
    /**
×
77
     * 栅格最小列数
×
78
     */
×
79
    @Input() @InputNumber() thyMinColumn: number = -1;
80

81
    /**
×
82
     * 锁定宽高比
83
     */
×
84
    @Input() @InputBoolean() thyLockAspectRatio: boolean = false;
85

86
    /**
×
87
     * 是否预览模式
88
     */
89
    @Input() @InputBoolean() thyPreview: boolean = false;
90

91
    /**
×
92
     * 是否禁用调整大小
93
     */
94
    @Input() @InputBoolean() thyDisabled: boolean = false;
×
95

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

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

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

111
    resizing = false;
112
    private nativeElement!: HTMLElement;
×
113
    private nativeElementRect!: ClientRect | DOMRect;
×
114
    private sizeCache: ThyResizeEvent | null = null;
×
115
    private ghostElement: HTMLDivElement | null = null;
116
    private currentHandleEvent: ThyResizeHandleMouseDownEvent | null = null;
117

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

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

×
155
        this.thyResizableService.documentMouseMoveOutsideAngular$.pipe(takeUntil(this.ngUnsubscribe$)).subscribe(event => {
×
156
            if (this.resizing) {
157
                this.resize(event);
×
158
            }
×
159
        });
×
160
    }
161

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

×
175
            fromEvent(this.nativeElement, 'mouseleave')
176
                .pipe(takeUntil(this.ngUnsubscribe$))
×
177
                .subscribe(() => {
×
178
                    this.thyResizableService.mouseEnteredOutsideAngular$.next(false);
179
                });
180
        });
×
181
    }
×
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':
×
190
            case 'bottom':
191
                this.renderer.setStyle(document.body, 'cursor', 'ns-resize');
192
                break;
193
            case 'topLeft':
194
            case 'bottomRight':
195
                this.renderer.setStyle(document.body, 'cursor', 'nwse-resize');
196
                break;
197
            case 'topRight':
×
198
            case 'bottomLeft':
×
199
                this.renderer.setStyle(document.body, 'cursor', 'nesw-resize');
×
200
                break;
×
201
        }
×
202
        setCompatibleStyle(document.body, 'user-select', 'none');
×
203
    }
×
204

×
205
    setPosition(): void {
×
206
        const position = getComputedStyle(this.nativeElement).position;
×
207
        if (position === 'static' || !position) {
×
208
            this.renderer.setStyle(this.nativeElement, 'position', 'relative');
209
        }
210
    }
×
211

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

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

285
        if (this.thyPreview) {
286
            this.previewResize(size);
287
        }
288
    }
1✔
289

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

1✔
318
        maxWidth = ensureInBounds(this.thyMaxWidth!, boundWidth);
319
        maxHeight = ensureInBounds(this.thyMaxHeight!, boundHeight);
320

321
        if (this.thyGridColumnCount !== -1) {
1✔
322
            spanWidth = maxWidth / this.thyGridColumnCount;
323
            minWidth = this.thyMinColumn !== -1 ? spanWidth * this.thyMinColumn : minWidth;
324
            maxWidth = this.thyMaxColumn !== -1 ? spanWidth * this.thyMaxColumn : maxWidth;
325
        }
1✔
326

327
        if (ratio !== -1) {
328
            if (/(left|right)/i.test(this.currentHandleEvent!.direction)) {
329
                newWidth = Math.min(Math.max(width, minWidth), maxWidth);
1✔
330
                newHeight = Math.min(Math.max(newWidth / ratio, this.thyMinHeight), maxHeight);
331
                if (newHeight >= maxHeight || newHeight <= this.thyMinHeight) {
332
                    newWidth = Math.min(Math.max(newHeight * ratio, minWidth), maxWidth);
333
                }
1✔
334
            } else {
335
                newHeight = Math.min(Math.max(height, this.thyMinHeight), maxHeight);
336
                newWidth = Math.min(Math.max(newHeight * ratio, minWidth), maxWidth);
337
                if (newWidth >= maxWidth || newWidth <= minWidth) {
1✔
338
                    newHeight = Math.min(Math.max(newWidth / ratio, this.thyMinHeight), maxHeight);
339
                }
340
            }
341
        } else {
1✔
342
            newWidth = Math.min(Math.max(width, minWidth), maxWidth);
343
            newHeight = Math.min(Math.max(height, this.thyMinHeight), maxHeight);
344
        }
345

1✔
346
        if (this.thyGridColumnCount !== -1) {
347
            col = Math.round(newWidth / spanWidth);
348
            newWidth = col * spanWidth;
349
        }
350

351
        return {
352
            col,
353
            width: newWidth,
354
            height: newHeight
355
        };
356
    }
357

358
    previewResize({ width, height }: ThyResizeEvent): void {
359
        this.createGhostElement();
360
        this.renderer.setStyle(this.ghostElement, 'width', `${width}px`);
361
        this.renderer.setStyle(this.ghostElement, 'height', `${height}px`);
362
    }
363

364
    createGhostElement(): void {
365
        if (!this.ghostElement) {
366
            this.ghostElement = this.renderer.createElement('div');
367
            this.renderer.setAttribute(this.ghostElement, 'class', 'thy-resizable-preview');
368
        }
369
        this.renderer.appendChild(this.nativeElement, this.ghostElement);
370
    }
371

372
    removeGhostElement(): void {
373
        if (this.ghostElement) {
374
            this.renderer.removeChild(this.nativeElement, this.ghostElement);
375
        }
376
    }
377

378
    ngOnDestroy(): void {
379
        this.ghostElement = null;
380
        this.sizeCache = null;
381
        super.ngOnDestroy();
382
    }
383
}
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