• 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

3.36
/src/watermark/watermark.directive.ts
1
import { Directive, ElementRef, OnInit, inject, DestroyRef, effect, input, computed } from '@angular/core';
2
import { Subject, Observable } from 'rxjs';
3
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
4
import { DEFAULT_WATERMARK_CONFIG, DEFAULT_CANVAS_CONFIG } from './config';
5
import { MutationObserverFactory } from '@angular/cdk/observers';
6
import { coerceBooleanProperty, ThyBooleanInput } from 'ngx-tethys/util';
7
import { ThyThemeStore } from 'ngx-tethys/core';
8

9
/**
10
 * @public
11
 * 水印样式配置
12
 */
13
export interface ThyCanvasConfigType {
14
    /**
1✔
15
     * 偏移角度
UNCOV
16
     */
×
UNCOV
17
    degree?: number;
×
UNCOV
18
    /**
×
UNCOV
19
     * 字体颜色。如果传的是数组,第一个为默认主题的字体颜色,第二个为黑暗主题的字体颜色
×
UNCOV
20
     */
×
UNCOV
21
    color?: string | string[];
×
UNCOV
22
    /**
×
23
     * 字体大小
UNCOV
24
     */
×
UNCOV
25
    fontSize?: number | string;
×
UNCOV
26
    /**
×
UNCOV
27
     * 文本行高
×
UNCOV
28
     */
×
UNCOV
29
    textLineHeight?: number;
×
30
    /**
31
     * 横纵间距
UNCOV
32
     */
×
UNCOV
33
    gutter?: number[];
×
UNCOV
34
}
×
UNCOV
35

×
36
/**
37
 * 水印指令
UNCOV
38
 * @name thyWatermark
×
UNCOV
39
 */
×
UNCOV
40
@Directive({
×
UNCOV
41
    selector: '[thyWatermark]'
×
42
})
43
export class ThyWatermarkDirective implements OnInit {
UNCOV
44
    private el = inject(ElementRef);
×
45

46
    /**
47
     * 是否禁用,默认为 false
48
     */
UNCOV
49
    readonly thyDisabled = input<boolean, ThyBooleanInput>(false, { transform: coerceBooleanProperty });
×
UNCOV
50

×
UNCOV
51
    /**
×
52
     * 水印内容
53
     */
54
    readonly thyWatermark = input<string>(undefined);
UNCOV
55

×
56
    /**
57
     * 水印样式配置
58
     */
UNCOV
59
    readonly thyCanvasConfig = input<ThyCanvasConfigType>(undefined);
×
UNCOV
60

×
61
    readonly content = computed(() => {
62
        const value = this.thyWatermark()?.replace(/^\"|\"$/g, '');
UNCOV
63
        return value || '';
×
UNCOV
64
    });
×
UNCOV
65

×
66
    private createWatermark$ = new Subject<string>();
67

68
    private observer: MutationObserver;
UNCOV
69

×
70
    private canvas: HTMLCanvasElement;
71

×
72
    private wmDiv: HTMLElement;
UNCOV
73

×
UNCOV
74
    private readonly destroyRef = inject(DestroyRef);
×
UNCOV
75

×
UNCOV
76
    private thyThemeStore = inject(ThyThemeStore);
×
UNCOV
77

×
UNCOV
78
    constructor() {
×
UNCOV
79
        effect(() => {
×
80
            if (!this.thyDisabled() && this.thyThemeStore.theme()) {
81
                this.refreshWatermark();
82
            }
83
        });
84

85
        effect(() => {
86
            const thyWatermark = this.thyWatermark();
87
            if (thyWatermark) {
88
                this.refreshWatermark();
UNCOV
89
            }
×
UNCOV
90
        });
×
UNCOV
91

×
UNCOV
92
        effect(() => {
×
UNCOV
93
            const thyDisabled = this.thyDisabled();
×
UNCOV
94
            if (thyDisabled) {
×
UNCOV
95
                this.removeWatermark();
×
96
            } else {
UNCOV
97
                this.refreshWatermark();
×
UNCOV
98
            }
×
UNCOV
99
        });
×
UNCOV
100
    }
×
UNCOV
101

×
UNCOV
102
    ngOnInit() {
×
UNCOV
103
        if (!this.thyDisabled()) {
×
UNCOV
104
            this.createWatermark$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {
×
UNCOV
105
                this.observeAttributes()
×
UNCOV
106
                    .pipe(takeUntilDestroyed(this.destroyRef))
×
UNCOV
107
                    .subscribe(() => {});
×
UNCOV
108
            });
×
UNCOV
109

×
UNCOV
110
            this.createWatermark();
×
UNCOV
111
        }
×
UNCOV
112
    }
×
113

UNCOV
114
    private refreshWatermark() {
×
UNCOV
115
        this.removeWatermark();
×
116
        this.createWatermark();
117
    }
×
UNCOV
118

×
UNCOV
119
    private removeWatermark() {
×
UNCOV
120
        if (this.wmDiv) {
×
121
            this.wmDiv.remove();
122
            this.wmDiv = null;
123
        }
UNCOV
124
    }
×
UNCOV
125

×
UNCOV
126
    createCanvas() {
×
UNCOV
127
        let { gutter, fontSize, color, degree, textLineHeight } = {
×
UNCOV
128
            ...DEFAULT_CANVAS_CONFIG,
×
UNCOV
129
            ...(this.thyCanvasConfig() || {})
×
UNCOV
130
        };
×
131
        color = this.thyThemeStore.normalizeColor(color);
UNCOV
132

×
133
        const [xGutter, yGutter] = gutter;
134
        const canvas = document.createElement('canvas');
UNCOV
135
        const ctx = canvas.getContext('2d');
×
UNCOV
136

×
UNCOV
137
        const getFakeSize = () => {
×
UNCOV
138
            const fakeBox = document.createElement('div');
×
UNCOV
139
            const fakeBoxStyle: Record<string, string | number> = {
×
UNCOV
140
                position: 'absolute',
×
141
                top: 0,
142
                left: 0,
143
                display: 'inline-block',
UNCOV
144
                'font-size': `${parseFloat('' + fontSize)}px`,
×
145
                'word-wrap': 'break-word',
×
146
                'font-family': 'inherit',
×
147
                'white-space': 'pre-line'
×
148
            };
149
            const styleStr = Object.keys(fakeBoxStyle).reduce((pre, next) => ((pre += `${next}:${fakeBoxStyle[next]};`), pre), '');
UNCOV
150
            fakeBox.setAttribute('style', styleStr);
×
UNCOV
151

×
UNCOV
152
            fakeBox.innerHTML = this.content().replace(/(\\n)/gm, '</br>');
×
153
            document.querySelector('body').insertBefore(fakeBox, document.querySelector('body').firstChild);
154
            const { width, height } = fakeBox.getBoundingClientRect();
155
            fakeBox.remove();
156
            return { width, height };
1✔
157
        };
1✔
158
        const { width: fakeBoxWidth, height: fakeBoxHeight } = getFakeSize();
159

160
        const angle = (degree * Math.PI) / 180;
161
        const contentArr = this.content().split('\\n');
162
        const canvasHeight = Math.sin(angle) * fakeBoxWidth + fakeBoxHeight;
163

1✔
164
        let start = Math.ceil(Math.sin(angle) * fakeBoxWidth * Math.sin(angle));
165
        const canvasWidth = start + fakeBoxWidth;
166
        canvas.setAttribute('width', '' + (canvasWidth + xGutter));
167
        canvas.setAttribute('height', '' + (canvasHeight + yGutter));
168

169
        ctx.font = `${parseFloat('' + fontSize)}px microsoft yahei`;
170
        ctx.textAlign = 'center';
171
        ctx.textBaseline = 'top';
172
        ctx.fillStyle = color;
173
        ctx.rotate(0 - (degree * Math.PI) / 180);
174
        contentArr.map((k, i) => {
175
            ctx.fillText(k, -start + Math.ceil(canvasWidth / 2), Math.sin(angle) * canvasWidth + textLineHeight * i);
176
            start += Math.sin(angle) * textLineHeight;
177
        });
178
        this.canvas = canvas;
179
        return canvas;
180
    }
181

182
    private createWatermark(isRefresh = true) {
183
        const watermarkDiv = this.wmDiv || document.createElement('div');
184

185
        const background = !isRefresh ? this.canvas.toDataURL() : this.createCanvas().toDataURL();
186
        const watermarkStyle: Record<string, string | number> = {
187
            ...DEFAULT_WATERMARK_CONFIG,
188
            'background-image': `url(${background})`
189
        };
190

191
        const styleStr = Object.keys(watermarkStyle).reduce((pre, next) => ((pre += `${next}:${watermarkStyle[next]};`), pre), '');
192
        watermarkDiv.setAttribute('style', styleStr);
193

194
        if (!this.wmDiv) {
195
            const parentNode = this.el.nativeElement;
196
            watermarkDiv.classList.add(`_vm`);
197
            this.wmDiv = watermarkDiv;
198
            parentNode.insertBefore(watermarkDiv, parentNode.firstChild);
199
        }
200
        this.createWatermark$.next('');
201
    }
202

203
    private observeAttributes() {
204
        this.observer?.disconnect();
205
        return new Observable(observe => {
206
            const stream = new Subject<MutationRecord[]>();
207
            this.observer = new MutationObserverFactory().create(mutations => stream.next(mutations));
208
            if (this.observer) {
209
                this.observer.observe(this.wmDiv, {
210
                    attributes: true
211
                });
212
            }
213
            stream.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {
214
                if (this.wmDiv) {
215
                    this?.observer?.disconnect();
216
                    this.createWatermark(false);
217
                }
218
            });
219
            observe.next(stream);
220
            return () => {
221
                this.observer?.disconnect();
222
            };
223
        });
224
    }
225
}
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