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

atinc / ngx-tethys / d955d1af-f7dd-4f8a-91dd-f3e4a25da35a

05 Aug 2024 09:40AM UTC coverage: 90.465% (-0.003%) from 90.468%
d955d1af-f7dd-4f8a-91dd-f3e4a25da35a

Pull #3134

circleci

minlovehua
feat: switch-handle background always white
Pull Request #3134: 【草稿】给设计师看暗主题下对比色效果

5491 of 6714 branches covered (81.78%)

Branch coverage included in aggregate %.

13238 of 13989 relevant lines covered (94.63%)

997.49 hits per line

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

80.81
/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
    Output,
11
    EventEmitter,
12
    inject,
13
    DestroyRef
14
} from '@angular/core';
15
import { InternalImageInfo, ThyImageInfo, ThyImagePreviewMode, ThyImagePreviewOperation, ThyImagePreviewOptions } from '../image.class';
16
import { fromEvent, Observable } from 'rxjs';
17
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
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';
1✔
21
import { ThyCopyEvent, ThyCopyDirective } from 'ngx-tethys/copy';
22
import { ThyNotifyService } from 'ngx-tethys/notify';
23
import { DomSanitizer } from '@angular/platform-browser';
24
import { fetchImageBlob } from '../utils';
1✔
25
import { ThyDivider } from 'ngx-tethys/divider';
1✔
26
import { ThyIcon } from 'ngx-tethys/icon';
1✔
27
import { ThyLoading } from 'ngx-tethys/loading';
1✔
28
import { CdkDrag } from '@angular/cdk/drag-drop';
29
import { ThyAction, ThyActions } from 'ngx-tethys/action';
30
import { ThyTooltipDirective } from 'ngx-tethys/tooltip';
31
import { NgIf, NgFor } from '@angular/common';
32

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

42
/**
38✔
43
 * 图片预览组件
38!
44
 * @name thy-image-preview
×
45
 * @order 20
46
 */
38✔
47
@Component({
48
    selector: 'thy-image-preview',
49
    exportAs: 'thyImagePreview',
26✔
50
    templateUrl: './image-preview.component.html',
10!
51
    changeDetection: ChangeDetectionStrategy.OnPush,
52
    encapsulation: ViewEncapsulation.None,
10!
53
    host: {
54
        class: 'thy-image-preview-wrap',
55
        '[class.thy-image-preview-moving]': 'isDragging'
56
    },
57
    standalone: true,
58
    imports: [NgIf, ThyTooltipDirective, ThyAction, CdkDrag, NgFor, ThyLoading, ThyIcon, ThyActions, ThyDivider, ThyCopyDirective]
18✔
59
})
18✔
60
export class ThyImagePreview implements OnInit {
18✔
61
    @Output() downloadClicked: EventEmitter<ThyImageInfo> = new EventEmitter();
18✔
62

18✔
63
    private readonly destroyRef = inject(DestroyRef);
18✔
64

18✔
65
    images: InternalImageInfo[] = [];
18✔
66
    previewIndex: number = 0;
18✔
67
    previewConfig: ThyImagePreviewOptions;
18✔
68
    previewImageTransform = '';
18✔
69
    previewImageWrapperTransform = '';
18✔
70
    zoomDisabled = false;
18✔
71
    zoom: number = 1;
18✔
72
    position = { ...initialPosition };
18✔
73
    isDragging = false;
18✔
74
    isLoadingDone = false;
18✔
75
    isFullScreen = false;
18✔
76
    isInsideScreen = true;
18✔
77
    currentImageMode: ThyImagePreviewMode = 'original-scale';
18✔
78
    previewOperations: ThyImagePreviewOperation[];
18✔
79
    defaultPreviewOperations: ThyImagePreviewOperation[] = [
18✔
80
        {
81
            icon: 'zoom-out',
82
            name: '缩小',
83
            action: (image: ThyImageInfo) => {
84
                this.zoomOut();
2✔
85
            },
86
            type: 'zoom-out'
87
        },
88
        {
89
            icon: 'zoom-in',
90
            name: '放大',
91
            action: (image: ThyImageInfo) => {
92
                this.zoomIn();
2✔
93
            },
94
            type: 'zoom-in'
95
        },
96
        {
97
            icon: 'one-to-one',
98
            name: '原始比例',
99
            action: (image: ThyImageInfo) => {
100
                this.setOriginalSize();
1✔
101
            },
102
            type: 'original-scale'
103
        },
104
        {
105
            icon: 'max-view',
106
            name: '适应屏幕',
107
            action: () => {
108
                this.setFitScreen();
1✔
109
            },
110
            type: 'fit-screen'
111
        },
112
        {
113
            icon: 'expand-arrows',
114
            name: '全屏显示',
115
            action: () => {
116
                this.fullScreen();
1✔
117
            },
118
            type: 'full-screen'
119
        },
120
        {
121
            icon: 'rotate-right',
122
            name: '旋转',
123
            action: (image: ThyImageInfo) => {
124
                this.rotateRight();
1✔
125
            },
126
            type: 'rotate-right'
127
        },
128
        {
129
            icon: 'download',
130
            name: '下载',
131
            action: (image: ThyImageInfo) => {
132
                this.download(image);
2✔
133
            },
134
            type: 'download'
135
        },
136
        {
137
            icon: 'preview',
138
            name: '查看原图',
139
            action: () => {
140
                this.viewOriginal();
1✔
141
            },
142
            type: 'view-original'
143
        },
144
        {
145
            icon: 'link-insert',
146
            name: '复制链接',
147
            type: 'copyLink'
148
        }
149
    ];
150

151
    private rotate: number;
152

18✔
153
    get previewImage(): InternalImageInfo {
18✔
154
        const image = this.images[this.previewIndex];
18✔
155
        if (image.size) {
156
            image.size = isNumber(image.size) ? humanizeBytes(image.size) : image.size;
157
        }
16✔
158
        return image;
159
    }
160

2✔
161
    get previewImageOriginSrc() {
162
        let imageSrc = this.previewImage.origin?.src || this.previewImage.src;
163
        if (imageSrc.startsWith('./')) {
18✔
164
            return window.location.host + '/' + imageSrc.split('./')[1];
165
        }
166
        return imageSrc;
×
167
    }
168

169
    get defaultZoom(): number {
170
        if (this.previewConfig?.zoom && this.previewConfig?.zoom > 0) {
171
            return this.previewConfig.zoom >= IMAGE_MAX_ZOOM
1✔
172
                ? IMAGE_MAX_ZOOM
1✔
173
                : this.previewConfig.zoom <= IMAGE_MIN_ZOOM
1✔
174
                ? IMAGE_MIN_ZOOM
1✔
175
                : this.previewConfig.zoom;
1✔
176
        }
1✔
177
    }
1✔
178

179
    @ViewChild('imgRef', { static: false }) imageRef!: ElementRef<HTMLImageElement>;
180
    @ViewChild('imagePreviewWrapper', { static: true }) imagePreviewWrapper!: ElementRef<HTMLElement>;
1✔
181

1✔
182
    constructor(
1✔
183
        public thyDialog: ThyDialog,
184
        public thyFullscreen: ThyFullscreen,
185
        private cdr: ChangeDetectorRef,
5✔
186
        private ngZone: NgZone,
5✔
187
        private notifyService: ThyNotifyService,
5✔
188
        private host: ElementRef<HTMLElement>,
5!
189
        private sanitizer: DomSanitizer
5✔
190
    ) {}
191

5✔
192
    ngOnInit(): void {
193
        this.initPreview();
194
        this.ngZone.runOutsideAngular(() => {
16✔
195
            fromEvent(this.host.nativeElement, 'click')
16✔
196
                .pipe(takeUntilDestroyed(this.destroyRef))
16✔
197
                .subscribe(event => {
6✔
198
                    if (
6✔
199
                        (event.target === event.currentTarget ||
6✔
200
                            (this.isInsideScreen && this.imagePreviewWrapper.nativeElement.contains(event.target as HTMLElement))) &&
6✔
201
                        !this.previewConfig?.disableClose
6✔
202
                    ) {
6✔
203
                        this.ngZone.run(() => !this.isFullScreen && this.thyDialog.close());
6✔
204
                    }
6!
205
                });
6✔
206

207
            fromEvent(this.imagePreviewWrapper.nativeElement, 'mousedown')
208
                .pipe(takeUntilDestroyed(this.destroyRef))
×
209
                .subscribe(() => {
210
                    this.isDragging = !this.isInsideScreen && true;
6✔
211
                });
6✔
212
        });
6!
213
    }
×
214

215
    setOriginalSize() {
6✔
216
        this.reset();
217
        this.currentImageMode = 'fit-screen';
218
        this.zoom = 1;
219
        this.updatePreviewImageTransform();
21✔
220
        this.calculateInsideScreen();
21!
221
        this.isLoadingDone = true;
222
        this.cdr.detectChanges();
×
223
    }
×
224

225
    setFitScreen() {
226
        this.reset();
21!
227
        this.isInsideScreen = true;
×
228
        this.updatePreviewImage();
229
    }
21✔
230

5✔
231
    useDefaultZoomUpdate(isUpdateImageWrapper: boolean) {
232
        this.zoom = this.defaultZoom;
233
        this.isLoadingDone = true;
16✔
234
        this.updatePreviewImageTransform();
235
        if (isUpdateImageWrapper) {
236
            this.updatePreviewImageWrapperTransform();
237
        }
238
        this.cdr.detectChanges();
21✔
239
    }
21✔
240

1✔
241
    useCalculateZoomUpdate(isUpdateImageWrapper?: boolean) {
1✔
242
        let img = new Image();
1✔
243
        img.src = this.previewImage.src;
1✔
244
        img.onload = () => {
245
            const { width: offsetWidth, height: offsetHeight } = getClientSize();
20✔
246
            const innerWidth = offsetWidth - HORIZONTAL_SPACE;
19✔
247
            const innerHeight = offsetHeight - VERTICAL_SPACE;
19✔
248
            const { naturalWidth, naturalHeight } = img;
249
            const xRatio = innerWidth / naturalWidth;
250
            const yRatio = innerHeight / naturalHeight;
1✔
251
            const zoom = Math.min(xRatio, yRatio);
1!
252
            if (zoom > 1) {
1✔
253
                this.zoom = 1;
1✔
254
            } else {
1✔
255
                this.zoom = zoom;
1✔
256
            }
1✔
257
            this.isLoadingDone = true;
258
            this.updatePreviewImageTransform();
×
259
            if (isUpdateImageWrapper) {
×
260
                this.updatePreviewImageWrapperTransform();
261
            }
262
            this.cdr.detectChanges();
263
        };
264
    }
265

18✔
266
    updatePreviewImage() {
9✔
267
        this.resolvePreviewImage().subscribe(result => {
268
            if (!result) {
269
                // error
17✔
270
                this.isLoadingDone = true;
271
                return;
18✔
272
            }
18✔
273
            // image size
274
            if (!this.previewImage.size && this.previewImage.blob) {
275
                this.previewImage.size = humanizeBytes(this.previewImage.blob.size);
2✔
276
            }
2!
277
            if (this.defaultZoom) {
2✔
278
                this.useDefaultZoomUpdate(true);
279
            } else {
280
                this.useCalculateZoomUpdate();
1!
281
            }
1✔
282
        });
1✔
283
    }
1!
284

1✔
285
    resolvePreviewImage() {
1✔
286
        return new Observable<Boolean>(subscriber => {
287
            if (this.previewImage.src.startsWith('blob:')) {
288
                this.previewImage.objectURL = this.sanitizer.bypassSecurityTrustUrl(this.previewImage.src);
289
                subscriber.next(true);
2✔
290
                subscriber.complete();
1✔
291
                return;
1✔
292
            }
1✔
293
            if (this.previewImage.objectURL || !this.previewConfig.resolveSize) {
1✔
294
                subscriber.next(true);
295
                subscriber.complete();
296
            } else {
297
                fetchImageBlob(this.previewImage.src).subscribe(
2✔
298
                    blob => {
1✔
299
                        const urlCreator = window.URL || window.webkitURL;
1✔
300
                        const objectURL = urlCreator.createObjectURL(blob);
1✔
301
                        this.previewImage.objectURL = this.sanitizer.bypassSecurityTrustUrl(objectURL);
1✔
302
                        this.previewImage.blob = blob;
303
                        subscriber.next(true);
304
                        subscriber.complete();
305
                    },
3✔
306
                    error => {
3✔
307
                        subscriber.next(false);
3✔
308
                        subscriber.complete();
3!
309
                    }
×
310
                );
311
            }
312
        });
3✔
313
    }
314

315
    initPreview() {
316
        if (Array.isArray(this.previewConfig?.operations) && this.previewConfig?.operations.length) {
1!
317
            this.previewOperations = this.defaultPreviewOperations.filter(item => this.previewConfig.operations.includes(item.type));
318
        } else {
319
            this.previewOperations = this.defaultPreviewOperations;
1✔
320
        }
1✔
321
        this.rotate = this.previewConfig?.rotate ?? 0;
322
        this.updatePreviewImage();
323
    }
1✔
324

1✔
325
    download(image: ThyImageInfo) {
1✔
326
        this.downloadClicked.emit(image);
327
        const src = image.origin?.src || image.src;
328
        fetchImageBlob(src)
1✔
329
            .pipe(takeUntilDestroyed(this.destroyRef))
×
330
            .subscribe(blob => {
×
331
                const urlCreator = window.URL || window.webkitURL;
332
                const objectURL = urlCreator.createObjectURL(blob);
333
                let a = document.createElement('a');
334
                a.download = image.name || 'default.png';
×
335
                a.href = objectURL;
×
336
                a.click();
337
            });
338
    }
×
339

340
    zoomIn(): void {
341
        if (this.zoom < IMAGE_MAX_ZOOM) {
342
            this.zoom = Math.min(this.zoom + 0.1, IMAGE_MAX_ZOOM);
2✔
343
            this.calculateInsideScreen();
1✔
344
            this.updatePreviewImageTransform();
1✔
345
            this.position = { ...initialPosition };
1✔
346
        }
1✔
347
    }
348

349
    zoomOut(): void {
350
        if (this.zoom > IMAGE_MIN_ZOOM) {
2✔
351
            this.zoom = Math.max(this.zoom - 0.1, IMAGE_MIN_ZOOM);
1✔
352
            this.calculateInsideScreen();
1✔
353
            this.updatePreviewImageTransform();
1✔
354
            this.position = { ...initialPosition };
1✔
355
        }
356
    }
357

358
    calculateInsideScreen() {
×
359
        const width = this.imageRef.nativeElement.offsetWidth * this.zoom;
×
360
        const height = this.imageRef.nativeElement.offsetHeight * this.zoom;
×
361
        const { width: clientWidth, height: clientHeight } = getClientSize();
×
362
        if (width >= clientWidth || height >= clientHeight) {
×
363
            this.isInsideScreen = false;
×
364
        } else {
×
365
            this.isInsideScreen = true;
×
366
        }
×
367
    }
368

369
    viewOriginal() {
370
        window.open(this.previewImage?.origin?.src || this.previewImage.src, '_blank');
371
    }
372

×
373
    rotateRight(): void {
×
374
        this.rotate += 90;
×
375
        this.updatePreviewImageTransform();
376
    }
377

378
    fullScreen(): void {
4✔
379
        const targetElement = this.host.nativeElement.querySelector('.thy-image-preview');
4✔
380
        this.isFullScreen = true;
4✔
381
        const fullscreenRef = this.thyFullscreen.launch({
4✔
382
            target: targetElement
383
        });
384
        fullscreenRef.afterExited().subscribe(() => {
15✔
385
            this.isFullScreen = false;
386
            this.cdr.markForCheck();
387
        });
5✔
388
    }
389

1✔
390
    copyLink(event: ThyCopyEvent) {
391
        if (event.isSuccess) {
392
            this.notifyService.success('复制图片地址成功');
393
        } else {
394
            this.notifyService.error('复制图片地址失败');
395
        }
396
    }
397

398
    prev() {
1✔
399
        if (this.previewIndex > 0) {
400
            this.previewIndex--;
401
            this.isLoadingDone = false;
402
            this.reset();
403
            this.updatePreviewImage();
404
        }
1✔
405
    }
406

407
    next() {
408
        if (this.previewIndex < this.images.length - 1) {
409
            this.previewIndex++;
410
            this.isLoadingDone = false;
411
            this.reset();
412
            this.updatePreviewImage();
413
        }
414
    }
415

416
    dragReleased() {
417
        this.isDragging = false;
418
        const width = this.imageRef.nativeElement.offsetWidth * this.zoom;
419
        const height = this.imageRef.nativeElement.offsetHeight * this.zoom;
420
        const { left, top } = getOffset(this.imageRef.nativeElement, window);
421
        const { width: clientWidth, height: clientHeight } = getClientSize();
422
        const isRotate = this.rotate % 180 !== 0;
423
        const fitContentParams = {
424
            width: isRotate ? height : width,
425
            height: isRotate ? width : height,
426
            left,
427
            top,
428
            clientWidth,
429
            clientHeight
430
        };
431
        const fitContentPos = getFitContentPosition(fitContentParams);
432
        if (!isUndefinedOrNull(fitContentPos.x) || !isUndefinedOrNull(fitContentPos.y)) {
433
            this.position = { ...this.position, ...fitContentPos };
434
        }
435
    }
436

437
    private reset(): void {
438
        this.currentImageMode = 'original-scale';
439
        this.rotate = this.previewConfig?.rotate ?? 0;
440
        this.position = { ...initialPosition };
441
        this.cdr.detectChanges();
442
    }
443

444
    private updatePreviewImageTransform(): void {
445
        this.previewImageTransform = `scale3d(${this.zoom}, ${this.zoom}, 1) rotate(${this.rotate}deg)`;
446
    }
447

448
    private updatePreviewImageWrapperTransform(): void {
449
        this.previewImageWrapperTransform = `translate3d(${this.position.x}px, ${this.position.y}px, 0)`;
450
    }
451
}
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