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

IgniteUI / igniteui-angular / 12197035990

06 Dec 2024 10:18AM UTC coverage: 91.581% (-0.03%) from 91.613%
12197035990

Pull #15150

github

web-flow
Merge 01de31589 into 07d155736
Pull Request #15150: fix(*): icon service doesn't work with scoped themes

12977 of 15215 branches covered (85.29%)

56 of 64 new or added lines in 5 files covered. (87.5%)

3 existing lines in 2 files now uncovered.

26303 of 28721 relevant lines covered (91.58%)

33939.44 hits per line

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

91.74
/projects/igniteui-angular/src/lib/icon/icon.service.ts
1
import { Inject, Injectable, Optional, SecurityContext } from "@angular/core";
2
import { DomSanitizer, SafeHtml } from "@angular/platform-browser";
3
import { DOCUMENT } from "@angular/common";
4
import { HttpClient } from "@angular/common/http";
5
import { Observable, Subject } from "rxjs";
6
import { PlatformUtil } from "../core/utils";
7
import { iconReferences } from './icon.references'
8
import { IconFamily, IconMeta, FamilyMeta } from "./types";
9
import type { IconType, IconReference } from './types';
10
import { IgxTheme, THEME_TOKEN, ThemeToken } from "../services/theme/theme.token";
11
import { IndigoIcons } from "./icons.indigo";
12

13
/**
14
 * Event emitted when a SVG icon is loaded through
15
 * a HTTP request.
16
 */
17
export interface IgxIconLoadedEvent {
18
    /** Name of the icon */
19
    name: string;
20
    /** The actual SVG text, if any */
21
    value?: string;
22
    /** The font-family for the icon. Defaults to material. */
23
    family: string;
24
}
25

26
/**
27
 * **Ignite UI for Angular Icon Service** -
28
 *
29
 * The Ignite UI Icon Service makes it easy for developers to include custom SVG images and use them with IgxIconComponent.
30
 * In addition it could be used to associate a custom class to be applied on IgxIconComponent according to given font-family.
31
 *
32
 * Example:
33
 * ```typescript
34
 * this.iconService.setFamily('material', { className: 'material-icons', type: 'font' });
35
 * this.iconService.addSvgIcon('aruba', '/assets/svg/country_flags/aruba.svg', 'svg-flags');
36
 * ```
37
 */
38
@Injectable({
39
    providedIn: "root",
40
})
41
export class IgxIconService {
2✔
42
    /**
43
     * Observable that emits when an icon is successfully loaded
44
     * through a HTTP request.
45
     *
46
     * @example
47
     * ```typescript
48
     * this.service.iconLoaded.subscribe((ev: IgxIconLoadedEvent) => ...);
49
     * ```
50
     */
51
    public iconLoaded: Observable<IgxIconLoadedEvent>;
52

53
    private _defaultFamily: IconFamily = {
3,749✔
54
        name: "material",
55
        meta: { className: "material-icons", type: "liga" },
56
    };
57
    private _iconRefs = new Map<string, Map<string, IconMeta>>();
3,749✔
58
    private _families = new Map<string, FamilyMeta>();
3,749✔
59
    private _cachedIcons = new Map<string, Map<string, SafeHtml>>();
3,749✔
60
    private _iconLoaded = new Subject<IgxIconLoadedEvent>();
3,749✔
61
    private _domParser: DOMParser;
62

63
    constructor(
64
        @Optional() private _sanitizer: DomSanitizer,
3,749✔
65
        @Optional() private _httpClient: HttpClient,
3,749✔
66
        @Optional() private _platformUtil: PlatformUtil,
3,749✔
67
        @Optional() @Inject(THEME_TOKEN) private _themeToken: ThemeToken,
3,749✔
68
        @Optional() @Inject(DOCUMENT) protected document: Document,
3,749✔
69
    ) {
70

71
        this.iconLoaded = this._iconLoaded.asObservable();
3,749✔
72
        this.setFamily(this._defaultFamily.name, this._defaultFamily.meta);
3,749✔
73

74
        this._themeToken?.onChange((theme) => {
3,749✔
75
            this.setRefsByTheme(theme);
3,746✔
76
        });
77

78
        if (this._platformUtil?.isBrowser) {
3,749✔
79
            this._domParser = new DOMParser();
3,745✔
80

81
            for (const [name, svg] of IndigoIcons) {
3,745✔
82
                this.addSvgIconFromText(name, svg.value, `internal_${svg.fontSet}`, true);
123,585✔
83
            }
84
        }
85
    }
86

87
    /**
88
     *  Returns the default font-family.
89
     * ```typescript
90
     *   const defaultFamily = this.iconService.defaultFamily;
91
     * ```
92
     */
93
    public get defaultFamily(): IconFamily {
94
        return this._defaultFamily;
63,881✔
95
    }
96

97
    /**
98
     *  Sets the default font-family.
99
     * ```typescript
100
     *   this.iconService.defaultFamily = 'svg-flags';
101
     * ```
102
     */
103
    public set defaultFamily(family: IconFamily) {
104
        this._defaultFamily = family;
3✔
105
        this.setFamily(this._defaultFamily.name, this._defaultFamily.meta);
3✔
106
    }
107

108
    /**
109
     *  Registers a custom class to be applied to IgxIconComponent for a given font-family.
110
     * ```typescript
111
     *   this.iconService.registerFamilyAlias('material', 'material-icons');
112
     * ```
113
     * @deprecated in version 18.1.0. Use `setFamily` instead.
114
     */
115
    public registerFamilyAlias(
116
        alias: string,
117
        className: string = alias,
×
118
        type: IconType = "font",
×
119
    ): this {
120
        this.setFamily(alias, { className, type });
×
121
        return this;
×
122
    }
123

124
    /**
125
     *  Returns the custom class, if any, associated to a given font-family.
126
     * ```typescript
127
     *   const familyClass = this.iconService.familyClassName('material');
128
     * ```
129
     */
130
    public familyClassName(alias: string): string {
131
        return this._families.get(alias)?.className || alias;
127,992✔
132
    }
133

134
    /** @hidden @internal */
135
    private familyType(alias: string): IconType {
136
        return this._families.get(alias)?.type;
335,643✔
137
    }
138

139
    /** @hidden @internal */
140
    public setRefsByTheme(theme: IgxTheme) {
141
        for (const { alias, target } of iconReferences) {
3,746✔
142
            const external = this._iconRefs.get(alias.family)?.get(alias.name)?.external;
333,394✔
143

144
            const _ref = this._iconRefs.get('default')?.get(alias.name) ?? {};
333,394✔
145
            const _target = target.get(theme) ?? target.get('default')!;
333,394✔
146

147
            const icon = target.get(theme) ?? target.get('default')!;
333,394✔
148
            const overwrite = !external && !(JSON.stringify(_ref) === JSON.stringify(_target));
333,394✔
149

150
            this._setIconRef(
333,394✔
151
                alias.name,
152
                alias.family,
153
                icon,
154
                overwrite
155
            );
156
        }
157
    }
158

159
    public _setIconRef(name: string, family: string, icon: IconMeta, overwrite = false) {
×
160
        let familyRef = this._iconRefs.get(family);
333,394✔
161

162
        if (!familyRef) {
333,394✔
163
            familyRef = new Map<string, IconMeta>();
3,745✔
164
            this._iconRefs.set(family, familyRef);
3,745✔
165
        }
166

167
        if (overwrite) {
333,394✔
168
            const familyType = this.familyType(icon?.family);
333,359✔
169
            familyRef.set(name, { ...icon, type: icon.type ?? familyType });
333,359✔
170
            this._iconLoaded.next({ name, family });
333,359✔
171
        }
172
    }
173

174
    /**
175
     *  Creates a family to className relationship that is applied to the IgxIconComponent
176
     *   whenever that family name is used.
177
     * ```typescript
178
     *   this.iconService.setFamily('material', { className: 'material-icons', type: 'liga' });
179
     * ```
180
     */
181
    public setFamily(name: string, meta: FamilyMeta) {
182
        this._families.set(name, meta);
3,772✔
183
    }
184

185
    /**
186
     *  Adds an icon reference meta for an icon in a meta family.
187
     *  Executes only if no icon reference is found.
188
     * ```typescript
189
     *   this.iconService.addIconRef('aruba', 'default', { name: 'aruba', family: 'svg-flags' });
190
     * ```
191
     */
192
    public addIconRef(name: string, family: string, icon: IconMeta) {
193
        const iconRef = this._iconRefs.get(family)?.get(name);
16✔
194

195
        if (!iconRef) {
16✔
196
            this.setIconRef(name, family, icon);
15✔
197
        }
198
    }
199

200
    /**
201
     *  Similar to addIconRef, but always sets the icon reference meta for an icon in a meta family.
202
     * ```typescript
203
     *   this.iconService.setIconRef('aruba', 'default', { name: 'aruba', family: 'svg-flags' });
204
     * ```
205
     */
206
    public setIconRef(name: string, family: string, icon: IconMeta) {
207
        let familyRef = this._iconRefs.get(family);
18✔
208

209
        if (!familyRef) {
18!
UNCOV
210
            familyRef = new Map<string, IconMeta>();
×
UNCOV
211
            this._iconRefs.set(family, familyRef);
×
212
        }
213

214
        const familyType = this.familyType(icon?.family);
18✔
215
        familyRef.set(name, { ...icon, type: icon.type ?? familyType, external: true });
18✔
216

217
        this._iconLoaded.next({ name, family });
18✔
218
    }
219

220
    /**
221
     *  Returns the icon reference meta for an icon in a given family.
222
     * ```typescript
223
     *   const iconRef = this.iconService.getIconRef('aruba', 'default');
224
     * ```
225
     */
226
    public getIconRef(name: string, family: string): IconReference {
227
        const icon = this._iconRefs.get(family)?.get(name);
127,991✔
228

229
        const iconFamily = icon?.family ?? family;
127,991✔
230
        const _name = icon?.name ?? name;
127,991✔
231
        const className = this.familyClassName(iconFamily);
127,991✔
232
        const prefix = this._families.get(iconFamily)?.prefix;
127,991✔
233

234
        // Handle name prefixes
235
        let iconName = _name;
127,991✔
236

237
        if (iconName && prefix) {
127,991✔
238
            iconName = _name.includes(prefix) ? _name : `${prefix}${_name}`;
6!
239
        }
240

241
        const cached = this.isSvgIconCached(iconName, iconFamily);
127,991✔
242
        const type = cached ? "svg" : icon?.type ?? this.familyType(iconFamily);
127,991✔
243

244
        return {
127,991✔
245
            className,
246
            type,
247
            name: iconName,
248
            family: iconFamily,
249
        };
250
    }
251

252
    private getOrCreateSvgFamily(family: string) {
253
        if (!this._families.has(family)) {
161,343✔
254
            this._families.set(family, { className: family, type: "svg" });
4,658✔
255
        }
256

257
        return this._families.get(family);
161,343✔
258
    }
259
    /**
260
     *  Adds an SVG image to the cache. SVG source is an url.
261
     * ```typescript
262
     *   this.iconService.addSvgIcon('aruba', '/assets/svg/country_flags/aruba.svg', 'svg-flags');
263
     * ```
264
     */
265
    public addSvgIcon(
266
        name: string,
267
        url: string,
268
        family = this._defaultFamily.name,
×
269
        stripMeta = false,
2✔
270
    ) {
271
        if (name && url) {
2!
272
            const safeUrl = this._sanitizer.bypassSecurityTrustResourceUrl(url);
2✔
273

274
            if (!safeUrl) {
2!
275
                throw new Error(
×
276
                    `The provided URL could not be processed as trusted resource URL by Angular's DomSanitizer: "${url}".`,
277
                );
278
            }
279

280
            const sanitizedUrl = this._sanitizer.sanitize(
2✔
281
                SecurityContext.RESOURCE_URL,
282
                safeUrl,
283
            );
284

285
            if (!sanitizedUrl) {
2!
286
                throw new Error(
×
287
                    `The URL provided was not trusted as a resource URL: "${url}".`,
288
                );
289
            }
290

291
            if (!this.isSvgIconCached(name, family)) {
2✔
292
                this.getOrCreateSvgFamily(family);
2✔
293

294
                this.fetchSvg(url).subscribe((res) => {
2✔
295
                    this.cacheSvgIcon(name, res, family, stripMeta);
×
296
                });
297
            }
298
        } else {
299
            throw new Error(
×
300
                "You should provide at least `name` and `url` to register an svg icon.",
301
            );
302
        }
303
    }
304

305
    /**
306
     *  Adds an SVG image to the cache. SVG source is its text.
307
     * ```typescript
308
     *   this.iconService.addSvgIconFromText('simple', '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
309
     *   <path d="M74 74h54v54H74" /></svg>', 'svg-flags');
310
     * ```
311
     */
312
    public addSvgIconFromText(
313
        name: string,
314
        iconText: string,
315
        family = this._defaultFamily.name,
×
316
        stripMeta = false,
102,565✔
317
    ) {
318
        if (name && iconText) {
226,234!
319
            if (this.isSvgIconCached(name, family)) {
226,234✔
320
                return;
64,893✔
321
            }
322

323
            this.getOrCreateSvgFamily(family);
161,341✔
324
            this.cacheSvgIcon(name, iconText, family, stripMeta);
161,341✔
325
        } else {
326
            throw new Error(
×
327
                "You should provide at least `name` and `iconText` to register an svg icon.",
328
            );
329
        }
330
    }
331

332
    /**
333
     *  Returns whether a given SVG image is present in the cache.
334
     * ```typescript
335
     *   const isSvgCached = this.iconService.isSvgIconCached('aruba', 'svg-flags');
336
     * ```
337
     */
338
    public isSvgIconCached(name: string, family: string): boolean {
339
        if (this._cachedIcons.has(family)) {
382,798✔
340
            const familyRegistry = this._cachedIcons.get(
262,189✔
341
                family,
342
            ) as Map<string, SafeHtml>;
343

344
            return familyRegistry.has(name);
262,189✔
345
        }
346

347
        return false;
120,609✔
348
    }
349

350
    /**
351
     *  Returns the cached SVG image as string.
352
     * ```typescript
353
     *   const svgIcon = this.iconService.getSvgIcon('aruba', 'svg-flags');
354
     * ```
355
     */
356
    public getSvgIcon(name: string, family: string) {
357
        return this._cachedIcons.get(family)?.get(name);
28,570✔
358
    }
359

360
    /**
361
     * @hidden
362
     */
363
    private fetchSvg(url: string): Observable<string> {
364
        const req = this._httpClient.get(url, { responseType: "text" });
2✔
365
        return req;
2✔
366
    }
367

368
    /**
369
     * @hidden
370
     */
371
    private cacheSvgIcon(
372
        name: string,
373
        value: string,
374
        family = this._defaultFamily.name,
×
375
        stripMeta: boolean,
376
    ) {
377
        if (this._platformUtil?.isBrowser && name && value) {
161,341✔
378
            const doc = this._domParser.parseFromString(value, "image/svg+xml");
161,341✔
379
            const svg = doc.querySelector("svg") as SVGElement;
161,341✔
380

381
            if (!this._cachedIcons.has(family)) {
161,341✔
382
                this._cachedIcons.set(family, new Map<string, SafeHtml>());
4,659✔
383
            }
384

385
            if (svg) {
161,341✔
386
                svg.setAttribute("fit", "");
161,341✔
387
                svg.setAttribute("preserveAspectRatio", "xMidYMid meet");
161,341✔
388

389
                if (stripMeta) {
161,341✔
390
                    const title = svg.querySelector("title");
123,669✔
391
                    const desc = svg.querySelector("desc");
123,669✔
392

393
                    if (title) {
123,669✔
394
                        svg.removeChild(title);
84✔
395
                    }
396

397
                    if (desc) {
123,669✔
398
                        svg.removeChild(desc);
84✔
399
                    }
400
                }
401

402
                const safeSvg = this._sanitizer.bypassSecurityTrustHtml(
161,341✔
403
                    svg.outerHTML,
404
                );
405

406
                this._cachedIcons.get(family).set(name, safeSvg);
161,341✔
407
                this._iconLoaded.next({ name, value, family });
161,341✔
408
            }
409
        }
410
    }
411
}
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