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

atinc / ngx-tethys / 3b40a702-4b4d-4ddb-81a7-a96baae6d682

08 Nov 2024 05:40AM UTC coverage: 90.395% (-0.04%) from 90.431%
3b40a702-4b4d-4ddb-81a7-a96baae6d682

push

circleci

why520crazy
Merge branch 'master' into feat-theme

5503 of 6730 branches covered (81.77%)

Branch coverage included in aggregate %.

424 of 431 new or added lines in 171 files covered. (98.38%)

344 existing lines in 81 files now uncovered.

13150 of 13905 relevant lines covered (94.57%)

999.86 hits per line

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

91.74
/src/icon/icon.component.ts
1
import { take } from 'rxjs/operators';
2
import { useHostRenderer } from '@tethys/cdk/dom';
3

4
import {
5
    ChangeDetectionStrategy,
6
    Component,
7
    ElementRef,
8
    HostBinding,
1✔
9
    Input,
10
    OnChanges,
11
    OnInit,
12
    Renderer2,
13
    SimpleChanges,
14
    ViewEncapsulation,
15
    numberAttribute,
16
    inject
17
} from '@angular/core';
1✔
18

19
import { getWhetherPrintErrorWhenIconNotFound } from './config';
6,652✔
20
import { ThyIconRegistry } from './icon-registry';
6,652✔
21
import { coerceBooleanProperty } from 'ngx-tethys/util';
6,652✔
22

6,652✔
23
const iconSuffixMap = {
6,652✔
24
    fill: 'fill',
6,652✔
25
    twotone: 'tt'
26
};
27

6,642✔
28
/**
6,642✔
29
 * 图标组件
30
 * @name thy-icon,[thy-icon]
31
 * @order 10
6,742✔
32
 */
100✔
33
@Component({
34
    selector: 'thy-icon, [thy-icon]',
35
    template: '<ng-content></ng-content>',
36
    changeDetection: ChangeDetectionStrategy.OnPush,
37
    encapsulation: ViewEncapsulation.None,
88✔
38
    standalone: true,
39
    host: {
12✔
40
        class: 'thy-icon'
10✔
41
    }
42
})
43
export class ThyIcon implements OnInit, OnChanges {
44
    private render = inject(Renderer2);
45
    private elementRef = inject(ElementRef);
6,730✔
46
    private iconRegistry = inject(ThyIconRegistry);
6,730✔
47

6,585✔
48
    private initialized = false;
6,583✔
49

50
    /**
51
     * 图标的类型
52
     * @type outline | fill | twotone
1,269✔
53
     */
54
    @Input('thyIconType') iconType: 'outline' | 'fill' | 'twotone' = 'outline';
5,112!
UNCOV
55

×
56
    @Input('thyTwotoneColor') iconTwotoneColor: string;
57

58
    /**
6,583!
59
     * 图标的名字
60
     */
61
    @Input('thyIconName') iconName: string;
2✔
62

63
    /**
64
     * 图标的旋转角度
2✔
65
     * @default 0
66
     */
67
    @Input({ alias: 'thyIconRotate', transform: numberAttribute }) iconRotate: number;
68

69
    @Input('thyIconSet') iconSet: string;
1,279✔
70

22✔
71
    /**
72
     * 图标打底色,镂空的图标,会透过颜色来
73
     * @default false
74
     */
75
    @HostBinding(`class.thy-icon-legging`)
1,269✔
76
    @Input({ alias: 'thyIconLegging', transform: coerceBooleanProperty })
77
    iconLegging: boolean;
78

79
    @Input({ alias: 'thyIconLinearGradient', transform: coerceBooleanProperty })
1,269✔
80
    iconLinearGradient: boolean;
1,269✔
81

16✔
82
    private hostRenderer = useHostRenderer();
83

1,269✔
84
    ngOnInit() {
1✔
85
        this.updateClasses();
1!
86
        this.initialized = true;
1✔
87
    }
2✔
88

1✔
89
    ngOnChanges(changes: SimpleChanges) {
90
        if (this.initialized) {
91
            if (
92
                changes['iconName'] ||
93
                changes['iconSet'] ||
94
                changes['iconTwotoneColor'] ||
95
                changes['iconType'] ||
96
                changes['iconLinearGradient']
97
            ) {
98
                this.updateClasses();
99
            } else if (changes['iconRotate']) {
100
                this.setStyleRotate();
101
            }
1,269✔
102
        }
1✔
103
    }
1✔
104

105
    private updateClasses() {
1,269✔
106
        const [namespace, iconName] = this.iconRegistry.splitIconName(this.iconName);
1,269✔
107
        if (iconName) {
108
            if (this.iconRegistry.iconMode === 'svg') {
109
                this.iconRegistry
1,269✔
110
                    .getSvgIcon(this.buildIconNameByType(iconName), namespace)
1,269✔
111
                    .pipe(take(1))
112
                    .subscribe(
113
                        svg => {
114
                            this.setSvgElement(svg);
115
                        },
116
                        (error: Error) => {
1,269✔
117
                            if (getWhetherPrintErrorWhenIconNotFound()) {
7✔
118
                                console.error(`Error retrieving icon: ${error.message}`);
119
                            }
120
                        }
7!
121
                    );
7✔
122
                this.hostRenderer.updateClass([`thy-icon${namespace ? `-${namespace}` : ``}-${this.buildIconNameByType(iconName)}`]);
123
            } else {
124
                const fontSetClass = this.iconSet
125
                    ? this.iconRegistry.getFontSetClassByAlias(this.iconSet)
126
                    : this.iconRegistry.getDefaultFontSetClass();
127
                this.hostRenderer.updateClass([fontSetClass, `${fontSetClass}-${this.iconName}`]);
13,166✔
128
            }
6✔
129
        }
6✔
130
    }
131

132
    private setStyleRotate() {
13,160✔
133
        if (this.iconRotate !== undefined) {
134
            this.render.setStyle(this.elementRef.nativeElement.querySelector('svg'), 'transform', `rotate(${this.iconRotate}deg)`);
135
        }
136
    }
137

138
    //#region svg element
139

140
    private setSvgElement(svg: SVGElement) {
1✔
141
        this.clearSvgElement();
1✔
142

1!
UNCOV
143
        // Workaround for IE11 and Edge ignoring `style` tags inside dynamically-created SVGs.
×
144
        // See: https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/10898469/
145
        // Do this before inserting the element into the DOM, in order to avoid a style recalculation.
1!
UNCOV
146
        const styleTags = svg.querySelectorAll('style') as NodeListOf<HTMLStyleElement>;
×
147

148
        for (let i = 0; i < styleTags.length; i++) {
149
            styleTags[i].textContent += ' ';
150
        }
151

1✔
152
        if (this.iconType === 'twotone') {
1✔
153
            const allPaths = svg.querySelectorAll('path');
154
            if (allPaths.length > 1) {
1✔
155
                allPaths.forEach((child, index: number) => {
156
                    if (child.getAttribute('id').includes('secondary-color')) {
157
                        child.setAttribute('fill', this.iconTwotoneColor);
158
                    }
159
                });
160
            }
161
        }
162

163
        // Note: we do this fix here, rather than the icon registry, because the
164
        // references have to point to the URL at the time that the icon was created.
1✔
165
        // if (this._location) {
166
        //     const path = this._location.getPathname();
167
        //     this._previousPath = path;
168
        //     this._cacheChildrenWithExternalReferences(svg);
169
        //     this._prependPathToReferences(path);
170
        // }
171
        if (this.iconLinearGradient) {
172
            this.setBaseUrl(svg);
173
            this.clearTitleElement(svg);
174
        }
175

176
        this.elementRef.nativeElement.appendChild(svg);
177
        this.setStyleRotate();
178
    }
179

180
    private clearSvgElement() {
181
        const layoutElement: HTMLElement = this.elementRef.nativeElement;
182
        let childCount = layoutElement.childNodes.length;
183

184
        // if (this._elementsWithExternalReferences) {
185
        //     this._elementsWithExternalReferences.clear();
186
        // }
187

188
        // Remove existing non-element child nodes and SVGs, and add the new SVG element. Note that
189
        // we can't use innerHTML, because IE will throw if the element has a data binding.
190
        while (childCount--) {
191
            const child = layoutElement.childNodes[childCount];
192

193
            // 1 corresponds to Node.ELEMENT_NODE. We remove all non-element nodes in order to get rid
194
            // of any loose text nodes, as well as any SVG elements in order to remove any old icons.
195
            if (child.nodeType !== 1 || child.nodeName.toLowerCase() === 'svg') {
196
                layoutElement.removeChild(child);
197
            }
198
        }
199
    }
200

201
    //#endregion
202

203
    private buildIconNameByType(iconName: string) {
204
        if (this.iconType && ['fill', 'twotone'].indexOf(this.iconType) >= 0) {
205
            const suffix = iconSuffixMap[this.iconType];
206
            return iconName.includes(`-${suffix}`) ? iconName : `${iconName}-${suffix}`;
207
        } else {
208
            return iconName;
209
        }
210
    }
211

212
    /**
213
     * Support Safari SVG LinearGradient.
214
     * @param svg
215
     */
216
    private setBaseUrl(svg: SVGElement) {
217
        const styleElements = svg.querySelectorAll('style');
218
        styleElements.forEach((n: HTMLElement) => {
219
            if (n.style.cssText.includes('url')) {
220
                n.style.fill = n.style.fill.replace('url("', 'url("' + location.pathname);
221
            }
222
            if (n.style.cssText.includes('clip-path')) {
223
                n.style.clipPath = n.style.clipPath.replace('url("', 'url("' + location.pathname);
224
            }
225
        });
226
    }
227

228
    private clearTitleElement(svg: SVGElement) {
229
        const titleElement = svg.querySelector('title');
230
        titleElement && titleElement.remove();
231
    }
232
}
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

© 2026 Coveralls, Inc