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

atinc / ngx-tethys / 5ba5b9d7-3ca9-4ff2-bbba-bde58c0f849f

22 Feb 2024 09:41AM UTC coverage: 90.604%. Remained the same
5ba5b9d7-3ca9-4ff2-bbba-bde58c0f849f

Pull #3027

circleci

minlovehua
feat(schematics): provide schematics for removing the suffix of standalone components #INFR-11662
Pull Request #3027: refactor: remove the component suffix for standalone components and provide schematics #INFR-10654

5425 of 6642 branches covered (81.68%)

Branch coverage included in aggregate %.

323 of 333 new or added lines in 193 files covered. (97.0%)

36 existing lines in 8 files now uncovered.

13504 of 14250 relevant lines covered (94.76%)

981.28 hits per line

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

80.88
/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 { 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';
1✔
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 = {
34
    x: 0,
1✔
35
    y: 0
36
};
493✔
37
const IMAGE_MAX_ZOOM = 3;
493✔
38
const IMAGE_MIN_ZOOM = 0.1;
477!
39
const HORIZONTAL_SPACE = 100 * 2; // left: 100px; right: 100px
40
const VERTICAL_SPACE = 96 + 106; // top: 96px; bottom: 106px
493✔
41

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

148
    private rotate: number;
149

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

447
    private updatePreviewImageWrapperTransform(): void {
448
        this.previewImageWrapperTransform = `translate3d(${this.position.x}px, ${this.position.y}px, 0)`;
449
    }
450

451
    ngOnDestroy(): void {
452
        super.ngOnDestroy();
453
    }
454
}
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