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

atinc / ngx-tethys / 16c04211-96d9-4801-9a77-7aba7f7b00c3

14 Nov 2024 03:47AM UTC coverage: 90.323% (-0.07%) from 90.395%
16c04211-96d9-4801-9a77-7aba7f7b00c3

Pull #3229

circleci

minlovehua
feat(watermark): support dark theme for watermark component #TINFR-501 
Pull Request #3229: feat(watermark): support dark theme for watermark component #TINFR-501

5511 of 6750 branches covered (81.64%)

Branch coverage included in aggregate %.

40 of 48 new or added lines in 3 files covered. (83.33%)

3 existing lines in 1 file now uncovered.

13176 of 13939 relevant lines covered (94.53%)

997.38 hits per line

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

88.52
/src/watermark/watermark.directive.ts
1
import { Directive, Input, ElementRef, OnInit, SimpleChanges, OnChanges, inject, DestroyRef } 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 } from 'ngx-tethys/util';
7
import { observeTheme, ThyThemeStore } from 'ngx-tethys/core';
8

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

36
/**
37
 * 水印指令
38
 * @name thyWatermark
4✔
39
 */
40
@Directive({
41
    selector: '[thyWatermark]',
42
    standalone: true
7✔
43
})
7✔
44
export class ThyWatermarkDirective implements OnInit, OnChanges {
6✔
45
    private el = inject(ElementRef);
4✔
46

2!
47
    /**
2✔
48
     * 是否禁用,默认为 false
49
     */
50
    @Input({ transform: coerceBooleanProperty })
7✔
51
    thyDisabled: boolean = false;
5✔
52

4✔
53
    content: string;
1!
54
    /**
55
     * 水印内容
7✔
56
     */
7✔
57
    @Input()
58
    set thyWatermark(value: string) {
59
        value = value?.replace(/^\"|\"$/g, '');
2✔
60
        this.content = !!value ? value : '';
2✔
61
    }
62

63
    /**
3!
64
     * 水印样式配置
3✔
65
     */
3✔
66
    @Input() thyCanvasConfig: ThyCanvasConfigType;
67

68
    private createWatermark$ = new Subject<string>();
69

7✔
70
    private observer: MutationObserver;
71

14✔
72
    private themeObserver: MutationObserver;
73

7✔
74
    private canvas: HTMLCanvasElement;
7✔
75

7✔
76
    private wmDiv: HTMLElement;
7✔
77

7✔
78
    private readonly destroyRef = inject(DestroyRef);
7✔
79

7✔
80
    private thyThemeStore = inject(ThyThemeStore);
81

82
    ngOnInit() {
83
        if (!this.thyDisabled) {
84
            this.createWatermark$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {
85
                this.observeAttributes()
86
                    .pipe(takeUntilDestroyed(this.destroyRef))
87
                    .subscribe(() => {});
88

89
                const themeChangedAction = () => {
56✔
90
                    this.refreshWatermark();
7✔
91
                };
7✔
92
                observeTheme(this.themeObserver, this.destroyRef, themeChangedAction)
7✔
93
                    .pipe(takeUntilDestroyed(this.destroyRef))
7✔
94
                    .subscribe(() => {});
7✔
95
            });
7✔
96

97
            this.createWatermark();
7✔
98
        }
7✔
99
    }
7✔
100

7✔
101
    ngOnChanges(changes: SimpleChanges): void {
7✔
102
        const { thyWatermark, thyDisabled } = changes;
7✔
103
        const thyWatermarkChange = () => {
7✔
104
            if (thyWatermark.firstChange) return;
7✔
105
            if (thyWatermark.currentValue) {
7✔
106
                this.refreshWatermark();
7✔
107
            }
7✔
108
        };
7✔
109
        const thyDisabledChange = () => {
7✔
110
            if (thyDisabled.firstChange) return;
7✔
111
            thyDisabled?.currentValue ? this.removeWatermark() : this.refreshWatermark();
7✔
112
        };
7✔
113
        thyWatermark && thyWatermarkChange();
114
        thyDisabled && thyDisabledChange();
7✔
115
    }
7✔
116

117
    private refreshWatermark() {
6✔
118
        this.removeWatermark();
6✔
119
        this.createWatermark();
6!
120
    }
6✔
121

122
    private removeWatermark() {
123
        if (this.wmDiv) {
124
            this.wmDiv.remove();
60✔
125
            this.wmDiv = null;
6✔
126
        }
6!
127
    }
6✔
128

6✔
129
    createCanvas() {
6✔
130
        let { gutter, fontSize, color, degree, textLineHeight } = {
6✔
131
            ...DEFAULT_CANVAS_CONFIG,
132
            ...(this.thyCanvasConfig || {})
6✔
133
        };
134
        color = this.thyThemeStore.normalizeColor(color);
135

6✔
136
        const [xGutter, yGutter] = gutter;
6✔
137
        const canvas = document.createElement('canvas');
6✔
138
        const ctx = canvas.getContext('2d');
6✔
139

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

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

1✔
163
        const angle = (degree * Math.PI) / 180;
164
        const contentArr = this.content.split('\\n');
165
        const canvasHeight = Math.sin(angle) * fakeBoxWidth + fakeBoxHeight;
166

167
        let start = Math.ceil(Math.sin(angle) * fakeBoxWidth * Math.sin(angle));
168
        const canvasWidth = start + fakeBoxWidth;
169
        canvas.setAttribute('width', '' + (canvasWidth + xGutter));
170
        canvas.setAttribute('height', '' + (canvasHeight + yGutter));
171

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

185
    private createWatermark(isRefresh = true) {
186
        const watermarkDiv = this.wmDiv || document.createElement('div');
187

188
        const background = !isRefresh ? this.canvas.toDataURL() : this.createCanvas().toDataURL();
189
        const watermarkStyle = {
190
            ...DEFAULT_WATERMARK_CONFIG,
191
            'background-image': `url(${background})`
192
        };
193

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

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

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