• 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

3.31
/src/image/preview/image-preview.component.ts
1
import {
2
    ChangeDetectionStrategy,
3
    Component,
4
    OnInit,
5
    ViewEncapsulation,
6
    ChangeDetectorRef,
7
    ElementRef,
8
    ViewChild,
9
    NgZone,
10
    OnDestroy,
11
    Output,
12
    EventEmitter
13
} from '@angular/core';
14
import { InternalImageInfo, ThyImageInfo, ThyImagePreviewMode, ThyImagePreviewOperation, ThyImagePreviewOptions } from '../image.class';
15
import { MixinBase, mixinUnsubscribe } from 'ngx-tethys/core';
16
import { fromEvent, Observable } from 'rxjs';
17
import { takeUntil } from 'rxjs/operators';
18
import { ThyDialog } from 'ngx-tethys/dialog';
19
import { getClientSize, getFitContentPosition, getOffset, humanizeBytes, isNumber, isUndefinedOrNull } from 'ngx-tethys/util';
20
import { ThyFullscreen } from 'ngx-tethys/fullscreen';
21
import { ThyCopyEvent, ThyCopyDirective } from 'ngx-tethys/copy';
1✔
22
import { ThyNotifyService } from 'ngx-tethys/notify';
23
import { DomSanitizer } from '@angular/platform-browser';
24
import { fetchImageBlob } from '../utils';
25
import { ThyDividerComponent } from 'ngx-tethys/divider';
1✔
26
import { ThyIconComponent } from 'ngx-tethys/icon';
1✔
27
import { ThyLoadingComponent } from 'ngx-tethys/loading';
1✔
28
import { CdkDrag } from '@angular/cdk/drag-drop';
1✔
29
import { ThyActionComponent, ThyActionsComponent } from 'ngx-tethys/action';
30
import { ThyTooltipDirective } from 'ngx-tethys/tooltip';
31
import { NgIf, NgFor } from '@angular/common';
32

33
const initialPosition = {
34
    x: 0,
1✔
35
    y: 0
36
};
×
37
const IMAGE_MAX_ZOOM = 3;
×
38
const IMAGE_MIN_ZOOM = 0.1;
×
39
const HORIZONTAL_SPACE = 100 * 2; // left: 100px; right: 100px
40
const VERTICAL_SPACE = 96 + 106; // top: 96px; bottom: 106px
×
41

42
/**
43
 * 图片预览组件
×
44
 * @name thy-image-preview
×
45
 * @order 20
×
46
 */
47
@Component({
×
48
    selector: 'thy-image-preview',
49
    exportAs: 'thyImagePreview',
50
    templateUrl: './image-preview.component.html',
×
51
    changeDetection: ChangeDetectionStrategy.OnPush,
×
52
    encapsulation: ViewEncapsulation.None,
53
    host: {
×
54
        class: 'thy-image-preview-wrap',
55
        '[class.thy-image-preview-moving]': 'isDragging'
56
    },
57
    standalone: true,
58
    imports: [
59
        NgIf,
×
60
        ThyTooltipDirective,
×
61
        ThyActionComponent,
×
62
        CdkDrag,
×
63
        NgFor,
×
64
        ThyLoadingComponent,
×
65
        ThyIconComponent,
×
66
        ThyActionsComponent,
×
67
        ThyDividerComponent,
×
68
        ThyCopyDirective
×
69
    ]
×
70
})
×
71
export class ThyImagePreviewComponent extends mixinUnsubscribe(MixinBase) implements OnInit, OnDestroy {
×
72
    @Output() downloadClicked: EventEmitter<ThyImageInfo> = new EventEmitter();
×
73
    images: InternalImageInfo[] = [];
×
74
    previewIndex: number = 0;
×
75
    previewConfig: ThyImagePreviewOptions;
×
76
    previewImageTransform = '';
×
77
    previewImageWrapperTransform = '';
×
78
    zoomDisabled = false;
×
79
    zoom: number = 1;
×
80
    position = { ...initialPosition };
×
81
    isDragging = false;
82
    isLoadingDone = false;
83
    isFullScreen = false;
84
    isInsideScreen = true;
85
    currentImageMode: ThyImagePreviewMode = 'original-scale';
×
86
    previewOperations: ThyImagePreviewOperation[];
87
    defaultPreviewOperations: ThyImagePreviewOperation[] = [
88
        {
89
            icon: 'zoom-out',
90
            name: '缩小',
91
            action: (image: ThyImageInfo) => {
92
                this.zoomOut();
93
            },
×
94
            type: 'zoom-out'
95
        },
96
        {
97
            icon: 'zoom-in',
98
            name: '放大',
99
            action: (image: ThyImageInfo) => {
100
                this.zoomIn();
101
            },
×
102
            type: 'zoom-in'
103
        },
104
        {
105
            icon: 'one-to-one',
106
            name: '原始比例',
107
            action: (image: ThyImageInfo) => {
108
                this.setOriginalSize();
109
            },
×
110
            type: 'original-scale'
111
        },
112
        {
113
            icon: 'max-view',
114
            name: '适应屏幕',
115
            action: () => {
116
                this.setFitScreen();
117
            },
×
118
            type: 'fit-screen'
119
        },
120
        {
121
            icon: 'expand-arrows',
122
            name: '全屏显示',
123
            action: () => {
124
                this.fullScreen();
125
            },
×
126
            type: 'full-screen'
127
        },
128
        {
129
            icon: 'rotate-right',
130
            name: '旋转',
131
            action: (image: ThyImageInfo) => {
132
                this.rotateRight();
133
            },
×
134
            type: 'rotate-right'
135
        },
136
        {
137
            icon: 'download',
138
            name: '下载',
139
            action: (image: ThyImageInfo) => {
140
                this.download(image);
141
            },
×
142
            type: 'download'
143
        },
144
        {
145
            icon: 'preview',
146
            name: '查看原图',
147
            action: () => {
148
                this.viewOriginal();
149
            },
150
            type: 'view-original'
151
        },
152
        {
153
            icon: 'link-insert',
×
154
            name: '复制链接',
×
155
            type: 'copyLink'
×
156
        }
157
    ];
158

×
159
    private rotate: number;
160

161
    get previewImage(): InternalImageInfo {
×
162
        const image = this.images[this.previewIndex];
163
        if (image.size) {
164
            image.size = isNumber(image.size) ? humanizeBytes(image.size) : image.size;
×
165
        }
166
        return image;
167
    }
×
168

169
    get previewImageOriginSrc() {
170
        let imageSrc = this.previewImage.origin?.src || this.previewImage.src;
171
        if (imageSrc.startsWith('./')) {
172
            return window.location.host + '/' + imageSrc.split('./')[1];
×
173
        }
×
174
        return imageSrc;
×
175
    }
×
176

×
177
    get defaultZoom(): number {
×
178
        if (this.previewConfig?.zoom && this.previewConfig?.zoom > 0) {
×
179
            return this.previewConfig.zoom >= IMAGE_MAX_ZOOM
180
                ? IMAGE_MAX_ZOOM
181
                : this.previewConfig.zoom <= IMAGE_MIN_ZOOM
×
182
                ? IMAGE_MIN_ZOOM
×
183
                : this.previewConfig.zoom;
×
184
        }
185
    }
186

×
187
    @ViewChild('imgRef', { static: false }) imageRef!: ElementRef<HTMLImageElement>;
×
188
    @ViewChild('imagePreviewWrapper', { static: true }) imagePreviewWrapper!: ElementRef<HTMLElement>;
×
189

×
190
    constructor(
×
191
        public thyDialog: ThyDialog,
192
        public thyFullscreen: ThyFullscreen,
×
193
        private cdr: ChangeDetectorRef,
194
        private ngZone: NgZone,
195
        private notifyService: ThyNotifyService,
×
196
        private host: ElementRef<HTMLElement>,
×
197
        private sanitizer: DomSanitizer
×
198
    ) {
×
199
        super();
×
200
    }
×
201

×
202
    ngOnInit(): void {
×
203
        this.initPreview();
×
204
        this.ngZone.runOutsideAngular(() => {
×
205
            fromEvent(this.host.nativeElement, 'click')
×
206
                .pipe(takeUntil(this.ngUnsubscribe$))
×
207
                .subscribe(event => {
208
                    if (
209
                        (event.target === event.currentTarget ||
×
210
                            (this.isInsideScreen && this.imagePreviewWrapper.nativeElement.contains(event.target as HTMLElement))) &&
211
                        !this.previewConfig?.disableClose
×
212
                    ) {
×
213
                        this.ngZone.run(() => !this.isFullScreen && this.thyDialog.close());
×
214
                    }
×
215
                });
216

×
217
            fromEvent(this.imagePreviewWrapper.nativeElement, 'mousedown')
218
                .pipe(takeUntil(this.ngUnsubscribe$))
219
                .subscribe(() => {
220
                    this.isDragging = !this.isInsideScreen && true;
×
221
                });
×
222
        });
223
    }
×
224

×
225
    setOriginalSize() {
226
        this.reset();
227
        this.currentImageMode = 'fit-screen';
×
228
        this.zoom = 1;
×
229
        this.updatePreviewImageTransform();
230
        this.calculateInsideScreen();
×
231
        this.isLoadingDone = true;
×
232
        this.cdr.detectChanges();
233
    }
234

×
235
    setFitScreen() {
236
        this.reset();
237
        this.isInsideScreen = true;
238
        this.updatePreviewImage();
239
    }
×
240

×
241
    useDefaultZoomUpdate(isUpdateImageWrapper: boolean) {
×
242
        this.zoom = this.defaultZoom;
×
243
        this.isLoadingDone = true;
×
244
        this.updatePreviewImageTransform();
×
245
        if (isUpdateImageWrapper) {
246
            this.updatePreviewImageWrapperTransform();
×
247
        }
×
248
        this.cdr.detectChanges();
×
249
    }
250

251
    useCalculateZoomUpdate(isUpdateImageWrapper?: boolean) {
×
252
        let img = new Image();
×
253
        img.src = this.previewImage.src;
×
254
        img.onload = () => {
×
255
            const { width: offsetWidth, height: offsetHeight } = getClientSize();
×
256
            const innerWidth = offsetWidth - HORIZONTAL_SPACE;
×
257
            const innerHeight = offsetHeight - VERTICAL_SPACE;
×
258
            const { naturalWidth, naturalHeight } = img;
259
            const xRatio = innerWidth / naturalWidth;
×
260
            const yRatio = innerHeight / naturalHeight;
×
261
            const zoom = Math.min(xRatio, yRatio);
262
            if (zoom > 1) {
263
                this.zoom = 1;
264
            } else {
265
                this.zoom = zoom;
266
            }
×
267
            this.isLoadingDone = true;
×
268
            this.updatePreviewImageTransform();
269
            if (isUpdateImageWrapper) {
270
                this.updatePreviewImageWrapperTransform();
×
271
            }
272
            this.cdr.detectChanges();
×
273
        };
×
274
    }
275

276
    updatePreviewImage() {
×
277
        this.resolvePreviewImage().subscribe(result => {
×
278
            if (!result) {
×
279
                // error
280
                this.isLoadingDone = true;
281
                return;
×
282
            }
×
283
            // image size
×
284
            if (!this.previewImage.size && this.previewImage.blob) {
×
285
                this.previewImage.size = humanizeBytes(this.previewImage.blob.size);
×
286
            }
×
287
            if (this.defaultZoom) {
288
                this.useDefaultZoomUpdate(true);
289
            } else {
290
                this.useCalculateZoomUpdate();
×
291
            }
×
292
        });
×
293
    }
×
294

×
295
    resolvePreviewImage() {
296
        return new Observable<Boolean>(subscriber => {
297
            if (this.previewImage.src.startsWith('blob:')) {
298
                this.previewImage.objectURL = this.sanitizer.bypassSecurityTrustUrl(this.previewImage.src);
×
299
                subscriber.next(true);
×
300
                subscriber.complete();
×
301
                return;
×
302
            }
×
303
            if (this.previewImage.objectURL || !this.previewConfig.resolveSize) {
304
                subscriber.next(true);
305
                subscriber.complete();
306
            } else {
×
307
                fetchImageBlob(this.previewImage.src).subscribe(
×
308
                    blob => {
×
309
                        const urlCreator = window.URL || window.webkitURL;
×
310
                        const objectURL = urlCreator.createObjectURL(blob);
×
311
                        this.previewImage.objectURL = this.sanitizer.bypassSecurityTrustUrl(objectURL);
312
                        this.previewImage.blob = blob;
313
                        subscriber.next(true);
×
314
                        subscriber.complete();
315
                    },
316
                    error => {
317
                        subscriber.next(false);
×
318
                        subscriber.complete();
319
                    }
320
                );
×
321
            }
×
322
        });
323
    }
324

×
325
    initPreview() {
×
326
        if (Array.isArray(this.previewConfig?.operations) && this.previewConfig?.operations.length) {
×
327
            this.previewOperations = this.defaultPreviewOperations.filter(item => this.previewConfig.operations.includes(item.type));
328
        } else {
329
            this.previewOperations = this.defaultPreviewOperations;
×
330
        }
×
331
        this.rotate = this.previewConfig?.rotate ?? 0;
×
332
        this.updatePreviewImage();
333
    }
334

335
    download(image: ThyImageInfo) {
×
336
        this.downloadClicked.emit(image);
×
337
        const src = image.origin?.src || image.src;
338
        fetchImageBlob(src)
339
            .pipe(takeUntil(this.ngUnsubscribe$))
×
340
            .subscribe(blob => {
341
                const urlCreator = window.URL || window.webkitURL;
342
                const objectURL = urlCreator.createObjectURL(blob);
343
                let a = document.createElement('a');
×
344
                a.download = image.name || 'default.png';
×
345
                a.href = objectURL;
×
346
                a.click();
×
347
            });
×
348
    }
349

350
    zoomIn(): void {
351
        if (this.zoom < IMAGE_MAX_ZOOM) {
×
352
            this.zoom = Math.min(this.zoom + 0.1, IMAGE_MAX_ZOOM);
×
353
            this.calculateInsideScreen();
×
354
            this.updatePreviewImageTransform();
×
355
            this.position = { ...initialPosition };
×
356
        }
357
    }
358

359
    zoomOut(): void {
×
360
        if (this.zoom > IMAGE_MIN_ZOOM) {
×
361
            this.zoom = Math.max(this.zoom - 0.1, IMAGE_MIN_ZOOM);
×
362
            this.calculateInsideScreen();
×
363
            this.updatePreviewImageTransform();
×
364
            this.position = { ...initialPosition };
×
365
        }
×
366
    }
×
367

×
368
    calculateInsideScreen() {
369
        const width = this.imageRef.nativeElement.offsetWidth * this.zoom;
370
        const height = this.imageRef.nativeElement.offsetHeight * this.zoom;
371
        const { width: clientWidth, height: clientHeight } = getClientSize();
372
        if (width >= clientWidth || height >= clientHeight) {
373
            this.isInsideScreen = false;
×
374
        } else {
×
375
            this.isInsideScreen = true;
×
376
        }
377
    }
378

379
    viewOriginal() {
×
380
        window.open(this.previewImage?.origin?.src || this.previewImage.src, '_blank');
×
381
    }
×
382

×
383
    rotateRight(): void {
384
        this.rotate += 90;
385
        this.updatePreviewImageTransform();
×
386
    }
387

388
    fullScreen(): void {
×
389
        const targetElement = this.host.nativeElement.querySelector('.thy-image-preview');
390
        this.isFullScreen = true;
391
        const fullscreenRef = this.thyFullscreen.launch({
×
392
            target: targetElement
393
        });
1✔
394
        fullscreenRef.afterExited().subscribe(() => {
395
            this.isFullScreen = false;
396
            this.cdr.markForCheck();
397
        });
398
    }
399

400
    copyLink(event: ThyCopyEvent) {
401
        if (event.isSuccess) {
402
            this.notifyService.success('复制图片地址成功');
1✔
403
        } else {
404
            this.notifyService.error('复制图片地址失败');
405
        }
406
    }
407

408
    prev() {
1✔
409
        if (this.previewIndex > 0) {
410
            this.previewIndex--;
411
            this.isLoadingDone = false;
412
            this.reset();
413
            this.updatePreviewImage();
414
        }
415
    }
416

417
    next() {
418
        if (this.previewIndex < this.images.length - 1) {
419
            this.previewIndex++;
420
            this.isLoadingDone = false;
421
            this.reset();
422
            this.updatePreviewImage();
423
        }
424
    }
425

426
    dragReleased() {
427
        this.isDragging = false;
428
        const width = this.imageRef.nativeElement.offsetWidth * this.zoom;
429
        const height = this.imageRef.nativeElement.offsetHeight * this.zoom;
430
        const { left, top } = getOffset(this.imageRef.nativeElement, window);
431
        const { width: clientWidth, height: clientHeight } = getClientSize();
432
        const isRotate = this.rotate % 180 !== 0;
433
        const fitContentParams = {
434
            width: isRotate ? height : width,
435
            height: isRotate ? width : height,
436
            left,
437
            top,
438
            clientWidth,
439
            clientHeight
440
        };
441
        const fitContentPos = getFitContentPosition(fitContentParams);
442
        if (!isUndefinedOrNull(fitContentPos.x) || !isUndefinedOrNull(fitContentPos.y)) {
443
            this.position = { ...this.position, ...fitContentPos };
444
        }
445
    }
446

447
    private reset(): void {
448
        this.currentImageMode = 'original-scale';
449
        this.rotate = this.previewConfig?.rotate ?? 0;
450
        this.position = { ...initialPosition };
451
        this.cdr.detectChanges();
452
    }
453

454
    private updatePreviewImageTransform(): void {
455
        this.previewImageTransform = `scale3d(${this.zoom}, ${this.zoom}, 1) rotate(${this.rotate}deg)`;
456
    }
457

458
    private updatePreviewImageWrapperTransform(): void {
459
        this.previewImageWrapperTransform = `translate3d(${this.position.x}px, ${this.position.y}px, 0)`;
460
    }
461

462
    ngOnDestroy(): void {
463
        super.ngOnDestroy();
464
    }
465
}
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